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:
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.
- The finished-message recovery keeps working unchanged (tool-call promotion, response-channel promotion, closing-only marker sanitation).
- 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
Summary
#1733 makes the empty-assistant recovery wrapper forward a wrapped model's thinking live. The Kimi XTML lane is deliberately excluded (
CommitPolicy.thinkingCommits = falsewhenhasKimiTextToolCallRecovery(model)): its thinking channel is where misrouted text tool calls land, andwrapStreamWithKimiThinkingRecovery(packages/ai/src/tool-call-middleware/protocols/kimi-xtml/thinking-recovery-stream.ts) forwardsthinking_deltaevents untouched and only rewrites the finished message throughrecoverKimiXtmlThinking. 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:
wrapStreamWithKimiThinkingRecoverysanitizes the thinking channel as it streams: it forwardsthinking_deltatext only up to the first XTML marker (holding back a trailing partial marker prefix such as<,<|,<|clountil it is disambiguated), suppresses the remainder of a thinking block once a marker appears, and rewritespartialcontent andthinking_end.contentto the same sanitized text so subscribers that render from the partial never see markers either.packages/agent/src/empty-assistant-recovery.tsdrops the Kimi exception sothinking_deltacommits for the Kimi lane too, andagent-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-boundarycases 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 firstthinking_deltareaches the RPC surface before the first text delta.Related