interrupt causes "unexpected tool_use_id found in tool_result" errors with Anthropic API #309

Closed
opened 2026-02-15 18:15:45 -05:00 by yindo · 1 comment
Owner

Originally created by @simozampa on GitHub (Jul 20, 2025).

Checked other resources

  • I added a very descriptive title to this issue.
  • I searched the LangGraph.js documentation with the integrated search.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangGraph.js rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangGraph (or the specific integration package).

Example Code


import { ChatAnthropic } from "@langchain/anthropic";
import { StateGraph, Annotation, Command, interrupt } from "@langchain/langgraph";
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
import { ToolNode } from "@langchain/langgraph/prebuilt";

const StateAnnotation = Annotation.Root({
  messages: Annotation<(AIMessage | HumanMessage)[]>({
    reducer: (x, y) => x.concat(y),
  }),
  // ... other state
});

async function callModel(state: typeof StateAnnotation.State) {
  const model = new ChatAnthropic({
    modelName: "claude-sonnet-4-20250514",
    temperature: 0.3,
  }).bindTools(tools);
  
  const response = await model.invoke(state.messages);
  return { messages: [response] };
}

async function postToolReview(state: typeof StateAnnotation.State): Promise<Command> {
  const lastAIMessage = [...state.messages]
    .reverse()
    .find(msg => msg._getType() === "ai") as AIMessage;
    
  if (lastAIMessage?.tool_calls?.length) {
    const feedbackText = interrupt({
      message: "Do you want to continue or make changes?",
    });

    const feedbackMsg = new HumanMessage({
      content: feedbackText,
      name: "user_feedback",
    });

    return new Command({
      goto: "agent",
      update: { messages: [feedbackMsg] },
    });
  }
  
  return new Command({ goto: "agent" });
}

const checkpointer = PostgresSaver.fromConnString(
  "postgresql://postgres:postgres@localhost:5432/langgraph",
  { schema: "langgraph" }
);

const graph = new StateGraph(StateAnnotation)
  .addNode("agent", callModel)
  .addNode("tools", new ToolNode(tools))
  .addNode("post_tool_review", postToolReview)
  .addEdge("tools", "post_tool_review")
  .addEdge("post_tool_review", "agent")
  .addConditionalEdges("agent", routeMessage)
  .compile({ checkpointer });

// Resume after interrupt
const stream = await graph.stream(
  new Command({ resume: lastUserMessage.content }),
  { configurable: { thread_id: threadId } }
);

Steps to Reproduce:

Set up LangGraph with PostgreSQL checkpointer and Anthropic model
Configure human-in-the-loop interrupts after tool execution
Run multiple cycles:

  • Agent calls tool
  • Tool executes
  • Human interrupt for feedback
  • Resume with user feedback

After 2-3 cycles, Anthropic API rejects messages due to invalid tool sequence

Expected Behavior:
PostgreSQL checkpointer should maintain valid message sequences that comply with Anthropic API requirements:

  • Each tool_use must have corresponding tool_result in next message
  • Message ordering should be preserved correctly
  • Tool IDs should remain properly paired

Actual Behavior:
Message sequences become corrupted during checkpoint save/restore cycles, causing:

  • Orphaned tool_result blocks without corresponding tool_use
  • Invalid message ordering that violates Anthropic API constraints
  • Non-deterministic failures after multiple interrupt cycles

Workaround Needed:
LangGraph needs to ensure message sequence integrity specifically for Anthropic API compatibility, possibly by:

  • Validating tool_use/tool_result pairing before API calls
  • Reconstructing messages in correct order during checkpoint restoration
  • Adding Anthropic-specific message validation in the checkpointer

Additional Context:
Problem is non-deterministic - sometimes works several cycles before failing
Related to known Anthropic API sensitivity to message ordering
Similar issues reported in Claude Code and other Anthropic integrations: https://github.com/anthropics/claude-code/issues/473

This appears to be a compatibility issue between LangGraph's checkpoint restoration logic and Anthropic's strict message validation requirements.

Error Message and Stack Trace (if applicable)

BadRequestError: 400 messages.0.content.2: unexpected `tool_use_id` found in `tool_result` blocks: toolu_01L3CdbsdYs9KL5hAMCFFGA3. 
Each `tool_result` block must have a corresponding `tool_use` block in the previous message.

Description

When using PostgreSQL checkpointer with human-in-the-loop interrupts, the message state restoration creates invalid message sequences that violate Anthropic API's strict tool_use/tool_result ordering requirements. This appears to be related to how LangGraph handles message reconstruction after interrupt() and Command({ resume: ... }).

Root Cause:
The Anthropic API is very sensitive to tool message ordering and rejects sequences where:

  1. A tool_result appears without a preceding tool_use in the immediately previous assistant message
  2. Assistant messages have content blocks in unexpected order (tool_use first, text second)
  3. Tool IDs become orphaned during state reconstruction

System Info

{
  "@langchain/langgraph": "^0.3.10",
  "@langchain/langgraph-checkpoint-postgres": "^0.0.5", 
  "@langchain/anthropic": "^0.3.24",
  "@langchain/core": "^0.3.57",
  "@anthropic-ai/sdk": "^0.56.0",
  "node": "20.x",
  "typescript": "^5.8.2"
}
Originally created by @simozampa on GitHub (Jul 20, 2025). ### Checked other resources - [x] I added a very descriptive title to this issue. - [x] I searched the LangGraph.js documentation with the integrated search. - [x] I used the GitHub search to find a similar question and didn't find it. - [x] I am sure that this is a bug in LangGraph.js rather than my code. - [x] The bug is not resolved by updating to the latest stable version of LangGraph (or the specific integration package). ### Example Code ```typescript import { ChatAnthropic } from "@langchain/anthropic"; import { StateGraph, Annotation, Command, interrupt } from "@langchain/langgraph"; import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres"; import { ToolNode } from "@langchain/langgraph/prebuilt"; const StateAnnotation = Annotation.Root({ messages: Annotation<(AIMessage | HumanMessage)[]>({ reducer: (x, y) => x.concat(y), }), // ... other state }); async function callModel(state: typeof StateAnnotation.State) { const model = new ChatAnthropic({ modelName: "claude-sonnet-4-20250514", temperature: 0.3, }).bindTools(tools); const response = await model.invoke(state.messages); return { messages: [response] }; } async function postToolReview(state: typeof StateAnnotation.State): Promise<Command> { const lastAIMessage = [...state.messages] .reverse() .find(msg => msg._getType() === "ai") as AIMessage; if (lastAIMessage?.tool_calls?.length) { const feedbackText = interrupt({ message: "Do you want to continue or make changes?", }); const feedbackMsg = new HumanMessage({ content: feedbackText, name: "user_feedback", }); return new Command({ goto: "agent", update: { messages: [feedbackMsg] }, }); } return new Command({ goto: "agent" }); } const checkpointer = PostgresSaver.fromConnString( "postgresql://postgres:postgres@localhost:5432/langgraph", { schema: "langgraph" } ); const graph = new StateGraph(StateAnnotation) .addNode("agent", callModel) .addNode("tools", new ToolNode(tools)) .addNode("post_tool_review", postToolReview) .addEdge("tools", "post_tool_review") .addEdge("post_tool_review", "agent") .addConditionalEdges("agent", routeMessage) .compile({ checkpointer }); // Resume after interrupt const stream = await graph.stream( new Command({ resume: lastUserMessage.content }), { configurable: { thread_id: threadId } } ); ``` **Steps to Reproduce**: Set up LangGraph with PostgreSQL checkpointer and Anthropic model Configure human-in-the-loop interrupts after tool execution Run multiple cycles: - Agent calls tool - Tool executes - Human interrupt for feedback - Resume with user feedback After 2-3 cycles, Anthropic API rejects messages due to invalid tool sequence **Expected Behavior**: PostgreSQL checkpointer should maintain valid message sequences that comply with Anthropic API requirements: - Each tool_use must have corresponding tool_result in next message - Message ordering should be preserved correctly - Tool IDs should remain properly paired **Actual Behavior**: Message sequences become corrupted during checkpoint save/restore cycles, causing: - Orphaned tool_result blocks without corresponding tool_use - Invalid message ordering that violates Anthropic API constraints - Non-deterministic failures after multiple interrupt cycles **Workaround Needed**: LangGraph needs to ensure message sequence integrity specifically for Anthropic API compatibility, possibly by: - Validating tool_use/tool_result pairing before API calls - Reconstructing messages in correct order during checkpoint restoration - Adding Anthropic-specific message validation in the checkpointer **Additional Context**: Problem is non-deterministic - sometimes works several cycles before failing Related to known Anthropic API sensitivity to message ordering Similar issues reported in Claude Code and other Anthropic integrations: https://github.com/anthropics/claude-code/issues/473 This appears to be a compatibility issue between LangGraph's checkpoint restoration logic and Anthropic's strict message validation requirements. ### Error Message and Stack Trace (if applicable) ``` BadRequestError: 400 messages.0.content.2: unexpected `tool_use_id` found in `tool_result` blocks: toolu_01L3CdbsdYs9KL5hAMCFFGA3. Each `tool_result` block must have a corresponding `tool_use` block in the previous message. ``` ### Description When using PostgreSQL checkpointer with human-in-the-loop interrupts, the message state restoration creates invalid message sequences that violate Anthropic API's strict tool_use/tool_result ordering requirements. This appears to be related to how LangGraph handles message reconstruction after `interrupt()` and `Command({ resume: ... })`. **Root Cause:** The Anthropic API is very sensitive to tool message ordering and rejects sequences where: 1. A tool_result appears without a preceding tool_use in the immediately previous assistant message 2. Assistant messages have content blocks in unexpected order (tool_use first, text second) 3. Tool IDs become orphaned during state reconstruction ### System Info ``` { "@langchain/langgraph": "^0.3.10", "@langchain/langgraph-checkpoint-postgres": "^0.0.5", "@langchain/anthropic": "^0.3.24", "@langchain/core": "^0.3.57", "@anthropic-ai/sdk": "^0.56.0", "node": "20.x", "typescript": "^5.8.2" } ```
yindo closed this issue 2026-02-15 18:15:45 -05:00
Author
Owner

@simozampa commented on GitHub (Aug 6, 2025):

Looks like this was my fault. Turns out I was slicing the chat history to improve the context degradation and that was causing a orphaned tool result in the chat history. Closing the issue

@simozampa commented on GitHub (Aug 6, 2025): Looks like this was my fault. Turns out I was slicing the chat history to improve the context degradation and that was causing a orphaned tool result in the chat history. Closing the issue
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraphjs#309