Skip to content

ai: stream Kimi XTML thinking live with delta-level marker sanitation so the empty-recovery wrapper can commit on it #1737

Description

@code-yeongyu

Summary

#1733 makes the empty-assistant recovery wrapper forward a wrapped model's thinking live. The Kimi XTML lane is deliberately excluded (CommitPolicy.thinkingCommits = false when hasKimiTextToolCallRecovery(model)): its thinking channel is where misrouted text tool calls land, and wrapStreamWithKimiThinkingRecovery (packages/ai/src/tool-call-middleware/protocols/kimi-xtml/thinking-recovery-stream.ts) forwards thinking_delta events untouched and only rewrites the finished message through recoverKimiXtmlThinking. Forwarding those deltas live would expose XTML fragments (<|open|>/<|close|>/<|sep|>) that the recovery later removes - the production incident #759 fixed - and the wrapper cannot retract what it already forwarded.

So Kimi thinking is still withheld until the first visible text or tool call: in seven days of sessions, 60-83% of Kimi K3 turns with thinking were held for a median of 6.7-27.6 s (p90 26-52 s).

Expected (ideal state)

Kimi thinking streams live like every other reasoning model, with zero protocol leakage:

  1. wrapStreamWithKimiThinkingRecovery sanitizes the thinking channel as it streams: it forwards thinking_delta text only up to the first XTML marker (holding back a trailing partial marker prefix such as <, <|, <|clo until it is disambiguated), suppresses the remainder of a thinking block once a marker appears, and rewrites partial content and thinking_end.content to the same sanitized text so subscribers that render from the partial never see markers either.
  2. The finished-message recovery keeps working unchanged (tool-call promotion, response-channel promotion, closing-only marker sanitation).
  3. With that in place, packages/agent/src/empty-assistant-recovery.ts drops the Kimi exception so thinking_delta commits for the Kimi lane too, and agent-loop-empty-assistant-recovery.test.ts "keeps the Kimi XTML lane on the buffered contract" is replaced by a live-forwarding case that also asserts no marker bytes reach the subscriber.

Acceptance: the kimi-xtml-thinking-recovery-runtime-boundary cases still pass (no <|close|> or U+200B in any emitted event), the real zero-width fixture streams its safe prefix only, and a Kimi turn's first thinking_delta reaches the RPC surface before the first text delta.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions