Skip to content

fix(subagent): re-pair tool-results on gateway-loop replay (AI_MissingToolResultsError)#1934

Open
thomaskong119 wants to merge 1 commit into
garrytan:masterfrom
thomaskong119:fix/subagent-gateway-replay-tool-results
Open

fix(subagent): re-pair tool-results on gateway-loop replay (AI_MissingToolResultsError)#1934
thomaskong119 wants to merge 1 commit into
garrytan:masterfrom
thomaskong119:fix/subagent-gateway-replay-tool-results

Conversation

@thomaskong119

Copy link
Copy Markdown

Problem

Every subagent routed through the gateway tool-loop (agent.use_gateway_loop, i.e. any non-Anthropic provider) that emits parallel tool calls permanently fails on retry with:

[chat(<provider>)] Tool results are missing for tool calls call_…, call_…, call_…

i.e. AI SDK v6's AI_MissingToolResultsError. In production on GLM (glm-coding) this killed cron/agent jobs the day after the gateway loop took over — company-pulse and every dream-synthesize subagent that fanned out parallel tool calls died, and the retries compounded the failure.

Root cause

The gateway loop persists tool executions to subagent_tool_executions, not as subagent_messages rows. On replay, runSubagentViaGateway rebuilt the prior conversation with a naive 1:1 map:

const priorChatMessages = priorMessages.map(m => ({ role: m.role, content: adaptContentBlocksToChatBlocks(m.content_blocks) }));

That yields assistant tool-call turns with no following tool-result message. The loop seeds messages from replayState.priorMessages, so the next generateText() sees unanswered tool-calls and v6 rejects the prompt. A single crashed turn with N parallel calls drops all N tool-results at once.

Fix

  • Extract a pure, unit-testable reconstructReplayMessages(priorMessages, priorToolExecs) that re-inserts a tool-result message after each prior assistant tool-call turn, pairing every call with its persisted outcome (keyed by message_idx + provider tool_use_id). A call with no completed execution (crash before the result was persisted) gets a synthesized error result so the turn still validates and the model recovers next turn.
  • Derive nextMessageIdx, nextTurnIdx, and the initialMessages seed-condition from the persisted-message count (priorMessages), not priorChatMessages.length — the latter now also counts the re-inserted tool-result messages (which are not their own subagent_messages rows), so the loop's messageIdx counter would otherwise skip.

Provider-neutral; only reachable on the gateway path.

Why the existing tests didn't catch it

test/e2e/subagent-crash-replay-multi-provider.test.ts stubs the chat transport (__setChatTransportForTests), so it short-circuits before generateText runs and never exercised the ModelMessage conversion — the same blind spot called out in test/ai/gateway-tools-schema.test.ts.

Testing

New test/subagent-replay-toolresult-repair.test.ts (4 tests, no network):

  • re-pairs every parallel tool-call with its persisted tool-result;
  • no spurious tool-result message for non-tool turns;
  • synthesizes error results for failed / missing executions;
  • drives the real AI SDK v6 generateText (via MockLanguageModelV3) and asserts the re-paired history validates while the naive 1:1 map throws.

Regression: subagent-crash-replay-multi-provider (13), gateway-tools-schema (3), gateway-tool-loop (7) all still pass.

Context: this is the upstream reconciliation of a long-running local fork patch (the f9c22f7 "C-3" the fork notes said should land upstream alongside #1708/#868).

🤖 Generated with Claude Code

…gToolResultsError)

The gateway tool-loop persists tool executions to `subagent_tool_executions`,
not as `subagent_messages` rows. Reconstructing the conversation on replay with
a naive 1:1 map (`priorMessages.map(...)`) therefore yields assistant tool-call
turns with no following tool-result message. On replay the loop seeds its
`messages` from this reconstruction, and the next `generateText()` throws AI SDK
v6's `AI_MissingToolResultsError` ("Tool results are missing for tool calls …").

This is fatal for any non-Anthropic model routed through the gateway loop
(`agent.use_gateway_loop`) that emits PARALLEL tool calls: a single crashed turn
drops several tool-results at once, so every subagent retry permanently fails.
Seen in production on GLM (glm-coding) for cron/agent jobs the day after the
gateway loop took over (company-pulse + dream-synthesize subagents all died).

Fix: extract a pure `reconstructReplayMessages()` that re-inserts a tool-result
message after each prior assistant tool-call turn, pairing every call with its
persisted outcome (keyed by message_idx + provider tool_use_id). A call with no
completed execution (crash before the result was persisted) gets a synthesized
error result so the turn still validates and the model recovers on the next turn.
Counters (`nextMessageIdx`, `nextTurnIdx`, `initialMessages`) now derive from the
persisted-message count (`priorMessages`), not the inflated reconstruction.

The pre-existing crash-replay e2e stubs the chat transport, so it short-circuits
before `generateText` and never exercised the ModelMessage conversion — which is
why this slipped through. The new test drives the real AI SDK v6 `generateText`
(no network) and asserts the re-paired history validates while the naive map throws.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant