fix(agents): classify Anthropic orphaned tool-use replay errors (#98163)

This commit is contained in:
Masato Hoshino
2026-06-30 09:57:14 -07:00
committed by GitHub
parent 169acd1e4e
commit a75431c586
2 changed files with 18 additions and 0 deletions
@@ -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();
});
@@ -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") ||