📋 Prerequisites
Describe the bug
When a streaming turn is interrupted after the agent has emitted a tool call but before the tool result is recorded (client disconnect, pod restart, the pre-#2113 3-minute A2A client timeout, OOM, ...), the session event log is persisted with a function_call event that has no matching function_response.
On every subsequent turn, the stored history is replayed verbatim to the model. The Anthropic API strictly requires each tool_use block to be followed by a tool_result block in the next message, so it rejects the entire conversation:
Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error',
'message': 'messages.90: `tool_use` ids were found without `tool_result` blocks
immediately after: toolu_01...'}}
From that point the session is permanently unusable: every message fails with the same 400 until the session is deleted. Callers (e.g. an A2A client driving a chat surface) receive an opaque JSON-RPC error and have no way to repair it.
Why this belongs in kagent
#2113 removed the most common interrupter (the hardcoded 3-minute a2a-go client timeout), but interruptions can never be fully prevented: pod restarts, OOM kills, network resets, and client-side cancellations all leave the same partial event log. The session store is kagent's and the history replay is kagent's, so the repair belongs here too. AnthropicLlm (google-adk, subclassed by kagent-adk) converts events to provider messages with no pairing validation. The same failure class is widely reported against other frameworks that replay history naively (vercel/ai#8516, warpdotdev/warp#7775, langchain4j#4900).
Proposed fix
Repair at message-build time (source-agnostic, no migration needed): when converting session events to Anthropic messages, detect any tool_use whose id has no tool_result in the immediately following message and synthesize one, e.g.
{"type": "tool_result", "tool_use_id": "...", "content": "[tool execution was interrupted; no result]", "is_error": true}
Optionally add the same guard for the mirrored case (an orphaned tool_result). This keeps the session usable and preserves its context instead of forcing a session delete.
Alternatives considered: transactional persistence of call/response pairs (larger change, does not heal existing rows), and an API-side session repair endpoint (pushes the burden to every client).
Steps to reproduce
- Agent with an Anthropic model and any slow tool.
- Start a turn that calls the tool; kill the connection (or the agent pod) mid-tool.
- Send any follow-up message on the same session: it fails with 400
tool_use ids were found without tool_result blocks, as does every later turn.
Observed on
kagent 0.9.9 (Anthropic provider), driven over A2A from a Slack gateway; recovered only by soft-deleting the session row.
📋 Prerequisites
Describe the bug
When a streaming turn is interrupted after the agent has emitted a tool call but before the tool result is recorded (client disconnect, pod restart, the pre-#2113 3-minute A2A client timeout, OOM, ...), the session event log is persisted with a
function_callevent that has no matchingfunction_response.On every subsequent turn, the stored history is replayed verbatim to the model. The Anthropic API strictly requires each
tool_useblock to be followed by atool_resultblock in the next message, so it rejects the entire conversation:From that point the session is permanently unusable: every message fails with the same 400 until the session is deleted. Callers (e.g. an A2A client driving a chat surface) receive an opaque JSON-RPC error and have no way to repair it.
Why this belongs in kagent
#2113 removed the most common interrupter (the hardcoded 3-minute a2a-go client timeout), but interruptions can never be fully prevented: pod restarts, OOM kills, network resets, and client-side cancellations all leave the same partial event log. The session store is kagent's and the history replay is kagent's, so the repair belongs here too.
AnthropicLlm(google-adk, subclassed by kagent-adk) converts events to provider messages with no pairing validation. The same failure class is widely reported against other frameworks that replay history naively (vercel/ai#8516, warpdotdev/warp#7775, langchain4j#4900).Proposed fix
Repair at message-build time (source-agnostic, no migration needed): when converting session events to Anthropic messages, detect any
tool_usewhose id has notool_resultin the immediately following message and synthesize one, e.g.{"type": "tool_result", "tool_use_id": "...", "content": "[tool execution was interrupted; no result]", "is_error": true}Optionally add the same guard for the mirrored case (an orphaned
tool_result). This keeps the session usable and preserves its context instead of forcing a session delete.Alternatives considered: transactional persistence of call/response pairs (larger change, does not heal existing rows), and an API-side session repair endpoint (pushes the burden to every client).
Steps to reproduce
tool_use ids were found without tool_result blocks, as does every later turn.Observed on
kagent 0.9.9 (Anthropic provider), driven over A2A from a Slack gateway; recovered only by soft-deleting the session row.