From a75431c586ceb0626cd25b487f3230362e2a6bd5 Mon Sep 17 00:00:00 2001 From: Masato Hoshino Date: Wed, 1 Jul 2026 01:57:14 +0900 Subject: [PATCH] fix(agents): classify Anthropic orphaned tool-use replay errors (#98163) --- .../provider-request-error-classifier.test.ts | 14 ++++++++++++++ .../reply/provider-request-error-classifier.ts | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/src/auto-reply/reply/provider-request-error-classifier.test.ts b/src/auto-reply/reply/provider-request-error-classifier.test.ts index da38395b764..b4768dd5e61 100644 --- a/src/auto-reply/reply/provider-request-error-classifier.test.ts +++ b/src/auto-reply/reply/provider-request-error-classifier.test.ts @@ -113,6 +113,10 @@ describe("provider request error classifier", () => { "local replay invariant guard", "invalid_replay_transcript: OpenAI Responses replay contains dangling_tool_call toolCallId=call_1 at message index 4", ], + [ + "Anthropic orphaned tool_use replay", + "messages.1: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_01A09q90qw90lq917835lq9. Each `tool_use` block must have a corresponding `tool_result` block in the next message.", + ], ])("classifies %s as provider conversation-state errors", (_label, message) => { expect(classifyProviderRequestError(new Error(message))).toEqual({ code: "provider_conversation_state_error", @@ -125,6 +129,16 @@ describe("provider request error classifier", () => { expect(classifyProviderRequestError(new Error("400 status code (no body)"))).toBeUndefined(); }); + it("does not classify generic tool_use/tool_result mentions without the orphan signal", () => { + // Both block names appear but there is no "without" orphan signal, so this + // generic guidance text must not trip the conversation-state classifier. + expect( + classifyProviderRequestError( + new Error("Each tool_use block must have a corresponding tool_result block."), + ), + ).toBeUndefined(); + }); + it("leaves explicit HTTP 429 rate-limit failures on the existing rate-limit path", () => { expect(classifyProviderRequestError(new Error("429: rate limit exceeded"))).toBeUndefined(); }); diff --git a/src/auto-reply/reply/provider-request-error-classifier.ts b/src/auto-reply/reply/provider-request-error-classifier.ts index d6d11d04bf6..e42dc040387 100644 --- a/src/auto-reply/reply/provider-request-error-classifier.ts +++ b/src/auto-reply/reply/provider-request-error-classifier.ts @@ -105,6 +105,10 @@ export function isProviderConversationStateErrorMessage(message: string): boolea lower.includes("tooluse") && lower.includes("exceeds the number") && lower.includes("previous turn")) || + // Anthropic/Bedrock orphaned tool-call replay: "`tool_use` ids were found + // without `tool_result` blocks immediately after: ...". Same broken-turn + // shape as the toolResult/toolUse count mismatch above, just snake_case. + (lower.includes("tool_use") && lower.includes("tool_result") && lower.includes("without")) || lower.includes("function call turn comes immediately after") || lower.includes("incorrect role information") || lower.includes("roles must alternate") ||