Skip to content

Interrupted turn permanently corrupts session history for Anthropic models (dangling tool_use leads to 400 on every later turn) #2277

Description

@QuentinBisson

📋 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

  1. Agent with an Anthropic model and any slow tool.
  2. Start a turn that calls the tool; kill the connection (or the agent pod) mid-tool.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions