[GH-ISSUE #150] Bug: Rejecting an interrupted tool call leaves dangling tool_call_id when two tools are called in parallel #39

Closed
opened 2026-02-16 06:16:57 -05:00 by yindo · 1 comment
Owner

Originally created by @zhdanovartur on GitHub (Jan 27, 2026).
Original GitHub issue: https://github.com/langchain-ai/deepagentsjs/issues/150

Originally assigned to: @christian-bromann on GitHub.

Bug: Rejecting an interrupted tool call leaves dangling tool_call_id when two tools are called in parallel

Summary

When a single user request causes the agent to call two tools in parallel (interrupted_tool + free_tool), and interruptOn pauses on interrupted_tool, rejecting the interrupt via Command({ resume: { decisions: [{ type: "reject" }] } }) can leave a dangling tool_call_id. The provider then throws:

MiddlewareError: 400 An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. The following tool_call_ids did not have response messages: call_...

Expected: this should be handled by dangling tool call repair:

Versions

{
  "@langchain/core": "^1.1.17",
  "@langchain/langgraph": "^1.1.2",
  "@langchain/openai": "^1.2.3",
  "deepagents": "^1.6.0",
  "langchain": "^1.2.14",
  "zod": "^4.3.6"
}

Reproduction

import { createDeepAgent } from "deepagents";
import { tool } from "langchain";
import { Command, MemorySaver } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
import "dotenv/config";

const freeTool = tool(async ({ param }: { param: string }) => ({ result: param }), {
  name: "free_tool",
  description: "A tool that NOT requires human approval",
  schema: z.object({ param: z.string().describe("Some parameter") }),
});

const interruptedTool = tool(async ({ param }: { param: string }) => ({ result: param }), {
  name: "interrupted_tool",
  description: "A tool that REQUIRES human approval",
  schema: z.object({ param: z.string().describe("Some parameter") }),
});

const agent = createDeepAgent({
  model: new ChatOpenAI({ model: "gpt-4.1", temperature: 0 }),
  tools: [freeTool, interruptedTool],
  systemPrompt: "Delegate sensitive operations to worker-agent.",
  checkpointer: new MemorySaver(),
  interruptOn: { [interruptedTool.name]: true },
});

(async () => {
  const result = await agent.invoke(
    {
      messages: [
        {
          role: "user",
          content: "Call `interrupted_tool` and `free_tool` and get the results.",
        },
      ],
    },
    { configurable: { thread_id: "test" } },
  );

  if (result.__interrupt__) {
    agent.invoke(
      new Command({ resume: { decisions: [{ type: "reject" }] } }),
      { configurable: { thread_id: "test" } },
    );
  }

  console.log(result);
})();

Expected behavior

Rejecting the interrupted tool call should not leave dangling tool_call_ids (should be repaired per docs), and the run should complete without the provider 400.

Actual behavior

Provider rejects the message sequence with 400 complaining that a tool_call_id has no corresponding tool response message.

Originally created by @zhdanovartur on GitHub (Jan 27, 2026). Original GitHub issue: https://github.com/langchain-ai/deepagentsjs/issues/150 Originally assigned to: @christian-bromann on GitHub. ## Bug: Rejecting an interrupted tool call leaves dangling `tool_call_id` when two tools are called in parallel ### Summary When a single user request causes the agent to call **two tools in parallel** (`interrupted_tool` + `free_tool`), and `interruptOn` pauses on `interrupted_tool`, rejecting the interrupt via `Command({ resume: { decisions: [{ type: "reject" }] } })` can leave a dangling `tool_call_id`. The provider then throws: ``` MiddlewareError: 400 An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. The following tool_call_ids did not have response messages: call_... ``` Expected: this should be handled by dangling tool call repair: - https://docs.langchain.com/oss/javascript/deepagents/harness#dangling-tool-call-repair ### Versions ```json { "@langchain/core": "^1.1.17", "@langchain/langgraph": "^1.1.2", "@langchain/openai": "^1.2.3", "deepagents": "^1.6.0", "langchain": "^1.2.14", "zod": "^4.3.6" } ``` ### Reproduction ```ts import { createDeepAgent } from "deepagents"; import { tool } from "langchain"; import { Command, MemorySaver } from "@langchain/langgraph"; import { ChatOpenAI } from "@langchain/openai"; import { z } from "zod"; import "dotenv/config"; const freeTool = tool(async ({ param }: { param: string }) => ({ result: param }), { name: "free_tool", description: "A tool that NOT requires human approval", schema: z.object({ param: z.string().describe("Some parameter") }), }); const interruptedTool = tool(async ({ param }: { param: string }) => ({ result: param }), { name: "interrupted_tool", description: "A tool that REQUIRES human approval", schema: z.object({ param: z.string().describe("Some parameter") }), }); const agent = createDeepAgent({ model: new ChatOpenAI({ model: "gpt-4.1", temperature: 0 }), tools: [freeTool, interruptedTool], systemPrompt: "Delegate sensitive operations to worker-agent.", checkpointer: new MemorySaver(), interruptOn: { [interruptedTool.name]: true }, }); (async () => { const result = await agent.invoke( { messages: [ { role: "user", content: "Call `interrupted_tool` and `free_tool` and get the results.", }, ], }, { configurable: { thread_id: "test" } }, ); if (result.__interrupt__) { agent.invoke( new Command({ resume: { decisions: [{ type: "reject" }] } }), { configurable: { thread_id: "test" } }, ); } console.log(result); })(); ``` ### Expected behavior Rejecting the interrupted tool call should not leave dangling `tool_call_id`s (should be repaired per docs), and the run should complete without the provider 400. ### Actual behavior Provider rejects the message sequence with 400 complaining that a `tool_call_id` has no corresponding tool response message.
yindo closed this issue 2026-02-16 06:16:57 -05:00
Author
Owner

@christian-bromann commented on GitHub (Feb 2, 2026):

Thanks for raising a PR @zhdanovartur , I've raised a PR with a fix.

@christian-bromann commented on GitHub (Feb 2, 2026): Thanks for raising a PR @zhdanovartur , I've raised a PR with a fix.
yindo changed title from Bug: Rejecting an interrupted tool call leaves dangling `tool_call_id` when two tools are called in parallel to [GH-ISSUE #150] Bug: Rejecting an interrupted tool call leaves dangling `tool_call_id` when two tools are called in parallel 2026-06-05 17:21:07 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/deepagentsjs#39