Skip to content

fix: tolerate Anthropic-style streams that omit text payloads - #2925

Open
Yorha9e wants to merge 2 commits into
MoonshotAI:mainfrom
Yorha9e:fix/assistant-delta-undefined
Open

fix: tolerate Anthropic-style streams that omit text payloads#2925
Yorha9e wants to merge 2 commits into
MoonshotAI:mainfrom
Yorha9e:fix/assistant-delta-undefined

Conversation

@Yorha9e

@Yorha9e Yorha9e commented Aug 14, 2026

Copy link
Copy Markdown

Related Issue

Resolve #2924

Problem

See linked issue. In short: when an Anthropic-compatible endpoint (typically a relay/proxy) streams content_block_start / text_delta events that omit the text field, the engine produces text parts with text: undefined; JSON serialization then drops the delta key from the assistant.delta event, and the TUI crashes on event.delta.trim().

What changed

  • Parsers (packages/kosong and packages/agent-core-v2 Anthropic bases): coerce missing text on content_block_start, text_delta, and the non-stream text block to '' — the same pattern already used for thinking ?? '' right below.
  • TUI (handleAssistantDelta): coerce a non-string event.delta to '' before use, so a malformed wire record can never crash the handler regardless of engine version.
  • Tests: regression coverage for both layers — a malformed Anthropic stream (kosong + v2 composition) and a missing-payload assistant.delta event (TUI handler).

The empty-string coercion preserves existing behavior for compliant providers (Anthropic already emits text: '' on block start), and matches how thinking deltas are handled today.

Round 2: assembly-layer filtering + boundary hardening

Review surfaced one real follow-up risk and a few defense gaps; this round addresses them:

  • Assembly (kosong/src/generate.ts, agent-core-v2/src/kosong/contract/generate.ts): after mergeInPlace, drop text === '' parts only when the message has tool calls or any non-empty content. A part that stays empty after merging means the provider sent a bare vacuous block; replaying it on the next tool-use round makes strict Anthropic endpoints answer 400: text content blocks must be non-empty. All-empty responses are deliberately kept intact so the existing settleStep removal path handles them unchanged (filtering them to content: [] would instead trip the retryable APIEmptyResponseError).
  • Loop choke point (agent-core-v2/src/agent/loop/loopService.ts): part.text ?? '' / part.think ?? '' where delta events are published, protecting every event consumer independent of provider.
  • Dead code (agent-core-v2 anthropic base): remove ?? '' under an existing !== undefined guard.
  • TUI: same typeof coercion for thinking.delta and hook.result content; formatHookResultBody now tolerates a missing content field, which also covers the subagent/btw panel renderers that share it.
  • Tests: pin the new filtering behavior at both assembly layers (empty text dropped while tool calls stay intact; all-empty response untouched); restore exact provider-level coercion assertions; cover malformed thinking.delta / hook.result payloads. One pre-existing regression pin (standalone empty TextPart is kept in content) is intentionally inverted to match the new assembly contract.

Deliberate non-goals

  • The swarm progress panel (agent-swarm-progress.ts appendModelDelta) has the same class of exposure on a missing delta, but it is a separate display path; left untouched to keep this PR focused. Happy to follow up if you want it covered here.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

- Anthropic stream parsers (kosong + agent-core-v2) coerce missing text
  on content_block_start / text_delta to empty strings
- TUI handleAssistantDelta coerces a missing delta payload instead of
  crashing on .trim() of undefined
- Add malformed-stream and missing-delta regression tests
@changeset-bot

changeset-bot Bot commented Aug 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a9823cf

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 081439f997

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// Malformed/provider-quirk wire records can arrive without a delta payload
// (e.g. an Anthropic content_block_start whose text block omits `text` —
// JSON serialization then drops the key). Coerce instead of crashing.
const delta = typeof event.delta === 'string' ? event.delta : '';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize child assistant deltas too

When a malformed assistant.delta belongs to a child agent (agentId !== 'main'), handleEvent returns from routeChildAgentEvent before reaching this guard, and the child-agent paths still pass event.delta through directly (the AgentSwarm progress path reads input.delta.length). That means the same missing-delta wire record this fallback is meant to tolerate can still crash or corrupt the TUI during subagent output from a replayed/older malformed event; normalize the delta before child routing or apply the same coercion in the subagent handlers.

Useful? React with 👍 / 👎.

- kosong + agent-core-v2 generate(): filter empty text parts when the
  message has tool calls or any non-empty content, so a tool-use round
  never replays an empty text block to strict Anthropic endpoints;
  all-empty responses are kept intact for downstream settleStep removal
- agent-core-v2 loopService: coerce missing text/think payloads at the
  delta publish choke point
- agent-core-v2 anthropic: remove dead ?? '' under a non-null guard
- TUI: typeof-coerce thinking.delta and hook.result content; guard
  formatHookResultBody so all callers survive a missing content field
- tests: pin the new filtering behavior, restore exact assertions for
  provider-level coercion, cover malformed thinking/hook payloads
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] TUI crashes on assistant.delta events with missing delta payload (Cannot read properties of undefined (reading 'trim'))

1 participant