[GH-ISSUE #131] GraphInterrupt thrown from tool loses interrupts property during error propagation #36

Closed
opened 2026-02-16 06:16:56 -05:00 by yindo · 3 comments
Owner

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

GraphInterrupt thrown from tool loses interrupts property during error propagation

Description

When a tool throws a GraphInterrupt in DeepAgents, the error loses its interrupts property during propagation through the tool execution chain, causing a TypeError in LangGraph's internal _commit() function.

This prevents implementing Human-in-the-Loop (HITL) approval workflows where tools need to pause execution for human review.

Environment

{
  "deepagents": "^1.5.0",
  "@langchain/langgraph": "^1.1.1",
  "@langchain/core": "^0.3.29",
  "runtime": "Bun v1.1.42 (but issue likely affects Node.js too)",
  "os": "macOS"
}

Problem

LangGraph's interrupt() function is designed to be called from graph nodes. When attempting to call it (or manually construct a GraphInterrupt) from within a tool, the error propagates incorrectly:

  1. Tool constructs GraphInterrupt with proper interrupts array
  2. Console logs confirm interrupts property exists with correct structure
  3. Error propagates through DeepAgents/LangGraph middleware
  4. By the time it reaches LangGraph's _commit(), the interrupts property is undefined
  5. TypeError: undefined is not an object (evaluating 'error.interrupts.length')

Minimal Reproduction

import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
import { GraphInterrupt } from "@langchain/langgraph";
import { createDeepAgent } from "deepagents";

// Tool that throws GraphInterrupt for approval
const approvalTool = new DynamicStructuredTool({
  name: "request_approval",
  description: "Request human approval",
  schema: z.object({
    action: z.string(),
  }),
  func: async (input) => {
    // Construct GraphInterrupt with proper structure
    const graphInterrupt = new GraphInterrupt([
      {
        id: `interrupt-${Date.now()}`,
        value: {
          type: "approval_required",
          action: input.action,
        },
      }
    ]);

    // Verify it's properly constructed
    console.log("interrupts:", graphInterrupt.interrupts);
    console.log("length:", graphInterrupt.interrupts?.length); // Logs: 1

    throw graphInterrupt;
  },
});

// Create agent with the tool
const agent = createDeepAgent({
  systemPrompt: "When user asks to delete, use request_approval tool",
  tools: [approvalTool],
});

// Invoke
const result = await agent.invoke({
  messages: [{ role: "user", content: "Delete my files" }]
});

Expected Behavior

  • Tool throws GraphInterrupt with interrupts array
  • Error propagates with properties intact
  • Caller can detect via isInterrupted(result) or catch the error
  • Can resume with Command({ resume: approvalData })

Actual Behavior

Console output from tool:

interrupts: [ { id: "interrupt-1769014658696", value: { ... } } ]
length: 1

Then error in LangGraph:

TypeError: undefined is not an object (evaluating 'error.interrupts.length')
    at _commit (/node_modules/@langchain/langgraph/dist/pregel/runner.js:164:14)
    at tick (/node_modules/@langchain/langgraph/dist/pregel/runner.js:62:9)

Error location in LangGraph source (runner.js:163-164):

if (error !== void 0) if (isGraphInterrupt(error)) {
    if (error.interrupts.length) {  // ← TypeError: error.interrupts is undefined

Analysis

The GraphInterrupt is correctly constructed with the interrupts property, but somewhere in the error propagation chain (DeepAgents' tool execution or LangGraph's tool calling layer), the error gets caught and re-thrown without preserving the interrupts array.

Evidence:

  • Error passes isGraphInterrupt(error) type check ✓
  • Error lacks interrupts property ✗
  • Likely cause: Error serialization or catch-rethrow without spreading properties

Impact

This blocks implementing HITL workflows where:

  • Tools pause for human approval
  • User reviews and approves/rejects
  • Execution resumes from checkpoint

Use Case

We're building a coding assistant where certain operations (spawning expensive subagents, making destructive changes) require user approval. The natural place to request approval is from within the tool that performs the action, not from a separate node.

Workarounds Attempted

1. Using interrupt() directly

const approval = interrupt({ type: "approval_required", ... });

Result: Same issue - creates malformed GraphInterrupt from tool context

2. Manual GraphInterrupt construction

throw new GraphInterrupt([{ id: "...", value: {...} }]);

Result: Correctly constructed but interrupts lost during propagation (shown above)

3. External state storage

Result: Works but requires side effects and race condition handling - not ideal

Questions

  1. Is calling interrupt() from tools a supported use case?
  2. If not, what's the recommended pattern for tool-level approval workflows in DeepAgents?
  3. Can error propagation be fixed to preserve GraphInterrupt properties?
  4. Should DeepAgents handle GraphInterrupt specially to preserve structure?

Request

Please either:

  1. Fix error propagation to preserve GraphInterrupt.interrupts when thrown from tools, OR
  2. Document the limitation that interrupt() only works from nodes, with recommended patterns for tool-level approval workflows

Additional Context

We've done extensive investigation and documented everything:

  • Full technical analysis
  • Debug logs showing property loss
  • Attempted workarounds
  • Production HITL implementation using keyword detection (current workaround)

Happy to provide more details or collaborate on a fix!

Related

Originally created by @suhasdeshpande on GitHub (Jan 21, 2026). Original GitHub issue: https://github.com/langchain-ai/deepagentsjs/issues/131 # GraphInterrupt thrown from tool loses `interrupts` property during error propagation ## Description When a tool throws a `GraphInterrupt` in DeepAgents, the error loses its `interrupts` property during propagation through the tool execution chain, causing a TypeError in LangGraph's internal `_commit()` function. This prevents implementing Human-in-the-Loop (HITL) approval workflows where tools need to pause execution for human review. ## Environment ```json { "deepagents": "^1.5.0", "@langchain/langgraph": "^1.1.1", "@langchain/core": "^0.3.29", "runtime": "Bun v1.1.42 (but issue likely affects Node.js too)", "os": "macOS" } ``` ## Problem LangGraph's `interrupt()` function is designed to be called from graph **nodes**. When attempting to call it (or manually construct a `GraphInterrupt`) from within a **tool**, the error propagates incorrectly: 1. ✅ Tool constructs `GraphInterrupt` with proper `interrupts` array 2. ✅ Console logs confirm `interrupts` property exists with correct structure 3. ❌ Error propagates through DeepAgents/LangGraph middleware 4. ❌ By the time it reaches LangGraph's `_commit()`, the `interrupts` property is undefined 5. ❌ TypeError: `undefined is not an object (evaluating 'error.interrupts.length')` ## Minimal Reproduction ```typescript import { DynamicStructuredTool } from "@langchain/core/tools"; import { z } from "zod"; import { GraphInterrupt } from "@langchain/langgraph"; import { createDeepAgent } from "deepagents"; // Tool that throws GraphInterrupt for approval const approvalTool = new DynamicStructuredTool({ name: "request_approval", description: "Request human approval", schema: z.object({ action: z.string(), }), func: async (input) => { // Construct GraphInterrupt with proper structure const graphInterrupt = new GraphInterrupt([ { id: `interrupt-${Date.now()}`, value: { type: "approval_required", action: input.action, }, } ]); // Verify it's properly constructed console.log("interrupts:", graphInterrupt.interrupts); console.log("length:", graphInterrupt.interrupts?.length); // Logs: 1 throw graphInterrupt; }, }); // Create agent with the tool const agent = createDeepAgent({ systemPrompt: "When user asks to delete, use request_approval tool", tools: [approvalTool], }); // Invoke const result = await agent.invoke({ messages: [{ role: "user", content: "Delete my files" }] }); ``` ## Expected Behavior - Tool throws `GraphInterrupt` with `interrupts` array - Error propagates with properties intact - Caller can detect via `isInterrupted(result)` or catch the error - Can resume with `Command({ resume: approvalData })` ## Actual Behavior **Console output from tool:** ``` interrupts: [ { id: "interrupt-1769014658696", value: { ... } } ] length: 1 ``` **Then error in LangGraph:** ``` TypeError: undefined is not an object (evaluating 'error.interrupts.length') at _commit (/node_modules/@langchain/langgraph/dist/pregel/runner.js:164:14) at tick (/node_modules/@langchain/langgraph/dist/pregel/runner.js:62:9) ``` **Error location in LangGraph source (runner.js:163-164):** ```javascript if (error !== void 0) if (isGraphInterrupt(error)) { if (error.interrupts.length) { // ← TypeError: error.interrupts is undefined ``` ## Analysis The `GraphInterrupt` is **correctly constructed** with the `interrupts` property, but somewhere in the error propagation chain (DeepAgents' tool execution or LangGraph's tool calling layer), the error gets caught and re-thrown without preserving the `interrupts` array. **Evidence:** - Error passes `isGraphInterrupt(error)` type check ✓ - Error lacks `interrupts` property ✗ - Likely cause: Error serialization or catch-rethrow without spreading properties ## Impact This blocks implementing HITL workflows where: - Tools pause for human approval - User reviews and approves/rejects - Execution resumes from checkpoint ## Use Case We're building a coding assistant where certain operations (spawning expensive subagents, making destructive changes) require user approval. The natural place to request approval is from within the tool that performs the action, not from a separate node. ## Workarounds Attempted ### 1. Using `interrupt()` directly ```typescript const approval = interrupt({ type: "approval_required", ... }); ``` **Result:** Same issue - creates malformed GraphInterrupt from tool context ### 2. Manual GraphInterrupt construction ```typescript throw new GraphInterrupt([{ id: "...", value: {...} }]); ``` **Result:** Correctly constructed but `interrupts` lost during propagation (shown above) ### 3. External state storage **Result:** Works but requires side effects and race condition handling - not ideal ## Questions 1. Is calling `interrupt()` from tools a supported use case? 2. If not, what's the recommended pattern for tool-level approval workflows in DeepAgents? 3. Can error propagation be fixed to preserve `GraphInterrupt` properties? 4. Should DeepAgents handle `GraphInterrupt` specially to preserve structure? ## Request Please either: 1. **Fix error propagation** to preserve `GraphInterrupt.interrupts` when thrown from tools, OR 2. **Document the limitation** that `interrupt()` only works from nodes, with recommended patterns for tool-level approval workflows ## Additional Context We've done extensive investigation and documented everything: - Full technical analysis - Debug logs showing property loss - Attempted workarounds - Production HITL implementation using keyword detection (current workaround) Happy to provide more details or collaborate on a fix! ## Related - LangGraph interrupt docs: https://langchain-ai.github.io/langgraphjs/how-tos/human_in_the_loop/ - GraphInterrupt class: `@langchain/langgraph/dist/errors.d.ts` - Interrupt type: `@langchain/langgraph/dist/constants.d.ts`
yindo closed this issue 2026-02-16 06:16:56 -05:00
Author
Owner

@suhasdeshpande commented on GitHub (Jan 21, 2026):

Found the proper solution - LangChain has built-in humanInTheLoopMiddleware that handles this use case correctly. The middleware intercepts tool calls BEFORE execution (not during), which is the designed pattern. Closing as this was user error, not a bug.

@suhasdeshpande commented on GitHub (Jan 21, 2026): Found the proper solution - LangChain has built-in `humanInTheLoopMiddleware` that handles this use case correctly. The middleware intercepts tool calls BEFORE execution (not during), which is the designed pattern. Closing as this was user error, not a bug.
Author
Owner

@suhasdeshpande commented on GitHub (Jan 21, 2026):

Reopening Issue: GraphInterrupt Error Propagation Bug

Why We're Reopening

We initially closed this issue believing LangChain's official humanInTheLoopMiddleware would solve the problem. We were wrong.

After implementing and extensively testing the official middleware, we confirmed it fails with the exact same error. This is a real, confirmed bug in DeepAgents' error propagation that affects ALL Human-in-the-Loop implementations.


The Bug (Confirmed)

When runtime.interrupt() is called from middleware in DeepAgents, the resulting GraphInterrupt error loses its interrupts property during propagation, causing this error:

TypeError: undefined is not an object (evaluating 'error.interrupts.length')
    at _commit (/node_modules/@langchain/langgraph/dist/pregel/runner.js:164:14)

What We Tested (All Failed )

1. LangChain's Official humanInTheLoopMiddleware

import { humanInTheLoopMiddleware } from "langchain";

const hitlMiddleware = humanInTheLoopMiddleware({
  interruptOn: {
    task: {
      allowedDecisions: ["approve", "reject"],
      description: (toolCall) => `Request to use task tool`,
    },
  },
});

const agent = createDeepAgent({
  middleware: [hitlMiddleware],
  tools: [taskTool],
});

// Result: TypeError: error.interrupts is undefined

Why it fails: The middleware internally calls runtime.interrupt(), which throws GraphInterrupt. DeepAgents strips the interrupts property during error propagation.


2. Custom Middleware with wrapToolCall Hook

const customMiddleware = {
  wrapToolCall: async (request, handler) => {
    const { toolCall, runtime } = request;

    if (toolCall.name === "task") {
      // This throws GraphInterrupt with interrupts property
      runtime.interrupt({
        type: "approval_required",
        action: toolCall.name,
      });
    }

    return await handler(request);
  }
};

// Result: TypeError: error.interrupts is undefined

Server logs prove the property exists when thrown:

GraphInterrupt: [
  {
    "id": "c76d2dbf4b0fe3e09e9b7b71fd287ba3",
    "value": { "type": "approval_required", "action": "task" }
  }
]
interrupts: [ [Object ...] ]  ← Property exists here!

// But by the time error reaches _commit(), interrupts is undefined

3. Manual GraphInterrupt Construction

const customMiddleware = {
  wrapToolCall: async (request, handler) => {
    const { toolCall } = request;

    if (toolCall.name === "task") {
      const graphInterrupt = new GraphInterrupt([
        { id: `interrupt-${Date.now()}`, value: { ... } }
      ]);

      console.log("interrupts:", graphInterrupt.interrupts); // Logs: [...]
      throw graphInterrupt;
    }

    return await handler(request);
  }
};

// Result: TypeError: error.interrupts is undefined

Same result: Property exists when thrown, lost during propagation.


Evidence Summary

What Status
GraphInterrupt correctly constructed YES (verified in logs)
interrupts property exists when thrown YES (verified in logs)
Error passes isGraphInterrupt(error) check YES
interrupts property preserved during propagation NO - This is the bug
Error reaches _commit() with interrupts NO
LangGraph can handle the interrupt NO - crashes on undefined

Root Cause

DeepAgents' middleware chain catches and re-throws GraphInterrupt errors without preserving the interrupts property. The error reaches LangGraph as a GraphInterrupt instance (passes type check) but without the required interrupts array (malformed).

Location: Somewhere in DeepAgents' tool execution wrapper or middleware error handling.


Impact

This bug completely blocks implementing Human-in-the-Loop approval workflows in DeepAgents:

  • Can't use LangChain's official HITL middleware
  • Can't use custom middleware with runtime.interrupt()
  • Can't pause tool execution for human approval
  • Forces hacky workarounds like keyword detection

Working Workaround (Not Ideal)

We had to implement keyword-based detection that avoids calling runtime.interrupt() entirely:

// Detect intent BEFORE streaming starts
const mightNeedApproval = keywords.some(k => message.includes(k));

if (mightNeedApproval && !approval) {
  // Return interrupt data as JSON (don't call runtime.interrupt!)
  return c.json({
    status: "awaiting_approval",
    checkpointId: `checkpoint-${Date.now()}`,
    interruptData: { ... },
  });
}

// Resume with Command (this part works fine)
if (approval) {
  const command = new Command({ resume: { decisions: [{ type: "approve" }] } });
  return agent.streamAISDK(command, { threadId });
}

Limitations:

  • Can't intercept actual tool calls (only predict intent)
  • Keyword detection is imprecise
  • Not the proper LangGraph HITL pattern

Request for DeepAgents Maintainers

Please help us resolve this in one of three ways:

Option 1: Fix the Error Propagation (Preferred)

  • Preserve GraphInterrupt.interrupts when catching/re-throwing in middleware chain
  • Ensure compatibility with LangChain's official humanInTheLoopMiddleware
  • Allow runtime.interrupt() to work from middleware hooks

Option 2: Document the Limitation

  • If middleware-based interrupts can't work, document why
  • Explain incompatibility with LangChain's official HITL middleware
  • Provide official guidance on implementing HITL in DeepAgents

Option 3: Provide Alternative Pattern

  • If fixing error propagation is infeasible, suggest proper alternative
  • How should DeepAgents users implement approval workflows?
  • What's the "right" way to do HITL with DeepAgents?

Test Environment

{
  "deepagents": "1.5.0",
  "@langchain/langgraph": "1.1.1",
  "@langchain/core": "0.3.29",
  "langchain": "1.2.11",
  "runtime": "Bun v1.1.42"
}

Files Available

We've documented everything extensively:

  • Full error traces with server logs
  • Test code for all approaches
  • Standalone reproduction script
  • Working workaround implementation

Happy to provide more details, test fixes, or collaborate on a solution!


Summary

This is not a usage issue - it's a real bug in DeepAgents' error handling that makes it incompatible with LangGraph's HITL pattern.

We apologize for the initial premature closure. We now have conclusive evidence that this blocks all proper HITL implementations in DeepAgents.

@suhasdeshpande commented on GitHub (Jan 21, 2026): # Reopening Issue: GraphInterrupt Error Propagation Bug ## Why We're Reopening We initially closed this issue believing LangChain's official `humanInTheLoopMiddleware` would solve the problem. **We were wrong.** After implementing and extensively testing the official middleware, we confirmed it **fails with the exact same error**. This is a real, confirmed bug in DeepAgents' error propagation that affects **ALL** Human-in-the-Loop implementations. --- ## The Bug (Confirmed) When `runtime.interrupt()` is called from middleware in DeepAgents, the resulting `GraphInterrupt` error **loses its `interrupts` property during propagation**, causing this error: ``` TypeError: undefined is not an object (evaluating 'error.interrupts.length') at _commit (/node_modules/@langchain/langgraph/dist/pregel/runner.js:164:14) ``` --- ## What We Tested (All Failed ❌) ### 1. LangChain's Official `humanInTheLoopMiddleware` ❌ ```typescript import { humanInTheLoopMiddleware } from "langchain"; const hitlMiddleware = humanInTheLoopMiddleware({ interruptOn: { task: { allowedDecisions: ["approve", "reject"], description: (toolCall) => `Request to use task tool`, }, }, }); const agent = createDeepAgent({ middleware: [hitlMiddleware], tools: [taskTool], }); // Result: TypeError: error.interrupts is undefined ``` **Why it fails:** The middleware internally calls `runtime.interrupt()`, which throws `GraphInterrupt`. DeepAgents strips the `interrupts` property during error propagation. --- ### 2. Custom Middleware with `wrapToolCall` Hook ❌ ```typescript const customMiddleware = { wrapToolCall: async (request, handler) => { const { toolCall, runtime } = request; if (toolCall.name === "task") { // This throws GraphInterrupt with interrupts property runtime.interrupt({ type: "approval_required", action: toolCall.name, }); } return await handler(request); } }; // Result: TypeError: error.interrupts is undefined ``` **Server logs prove the property exists when thrown:** ``` GraphInterrupt: [ { "id": "c76d2dbf4b0fe3e09e9b7b71fd287ba3", "value": { "type": "approval_required", "action": "task" } } ] interrupts: [ [Object ...] ] ← Property exists here! // But by the time error reaches _commit(), interrupts is undefined ``` --- ### 3. Manual `GraphInterrupt` Construction ❌ ```typescript const customMiddleware = { wrapToolCall: async (request, handler) => { const { toolCall } = request; if (toolCall.name === "task") { const graphInterrupt = new GraphInterrupt([ { id: `interrupt-${Date.now()}`, value: { ... } } ]); console.log("interrupts:", graphInterrupt.interrupts); // Logs: [...] throw graphInterrupt; } return await handler(request); } }; // Result: TypeError: error.interrupts is undefined ``` **Same result:** Property exists when thrown, lost during propagation. --- ## Evidence Summary | What | Status | |------|--------| | `GraphInterrupt` correctly constructed | ✅ YES (verified in logs) | | `interrupts` property exists when thrown | ✅ YES (verified in logs) | | Error passes `isGraphInterrupt(error)` check | ✅ YES | | `interrupts` property preserved during propagation | ❌ NO - **This is the bug** | | Error reaches `_commit()` with `interrupts` | ❌ NO | | LangGraph can handle the interrupt | ❌ NO - crashes on undefined | --- ## Root Cause DeepAgents' middleware chain catches and re-throws `GraphInterrupt` errors without preserving the `interrupts` property. The error reaches LangGraph as a `GraphInterrupt` instance (passes type check) but **without the required `interrupts` array** (malformed). **Location:** Somewhere in DeepAgents' tool execution wrapper or middleware error handling. --- ## Impact This bug **completely blocks** implementing Human-in-the-Loop approval workflows in DeepAgents: - ❌ Can't use LangChain's official HITL middleware - ❌ Can't use custom middleware with `runtime.interrupt()` - ❌ Can't pause tool execution for human approval - ❌ Forces hacky workarounds like keyword detection --- ## Working Workaround (Not Ideal) We had to implement keyword-based detection that avoids calling `runtime.interrupt()` entirely: ```typescript // Detect intent BEFORE streaming starts const mightNeedApproval = keywords.some(k => message.includes(k)); if (mightNeedApproval && !approval) { // Return interrupt data as JSON (don't call runtime.interrupt!) return c.json({ status: "awaiting_approval", checkpointId: `checkpoint-${Date.now()}`, interruptData: { ... }, }); } // Resume with Command (this part works fine) if (approval) { const command = new Command({ resume: { decisions: [{ type: "approve" }] } }); return agent.streamAISDK(command, { threadId }); } ``` **Limitations:** - Can't intercept actual tool calls (only predict intent) - Keyword detection is imprecise - Not the proper LangGraph HITL pattern --- ## Request for DeepAgents Maintainers Please help us resolve this in one of three ways: ### Option 1: Fix the Error Propagation (Preferred) - Preserve `GraphInterrupt.interrupts` when catching/re-throwing in middleware chain - Ensure compatibility with LangChain's official `humanInTheLoopMiddleware` - Allow `runtime.interrupt()` to work from middleware hooks ### Option 2: Document the Limitation - If middleware-based interrupts can't work, document why - Explain incompatibility with LangChain's official HITL middleware - Provide official guidance on implementing HITL in DeepAgents ### Option 3: Provide Alternative Pattern - If fixing error propagation is infeasible, suggest proper alternative - How should DeepAgents users implement approval workflows? - What's the "right" way to do HITL with DeepAgents? --- ## Test Environment ```json { "deepagents": "1.5.0", "@langchain/langgraph": "1.1.1", "@langchain/core": "0.3.29", "langchain": "1.2.11", "runtime": "Bun v1.1.42" } ``` --- ## Files Available We've documented everything extensively: - Full error traces with server logs - Test code for all approaches - Standalone reproduction script - Working workaround implementation Happy to provide more details, test fixes, or collaborate on a solution! --- ## Summary **This is not a usage issue - it's a real bug in DeepAgents' error handling that makes it incompatible with LangGraph's HITL pattern.** We apologize for the initial premature closure. We now have conclusive evidence that this blocks all proper HITL implementations in DeepAgents.
Author
Owner

@suhasdeshpande commented on GitHub (Jan 21, 2026):

Additional Test Evidence

Server Logs Showing Property Loss

When runtime.interrupt() is called from middleware, the server logs show:

[HITL] Tool call intercepted: task
[HITL] Task tool requires approval - calling runtime.interrupt()

// GraphInterrupt is thrown with correct structure:
GraphInterrupt: [
  {
    "id": "c76d2dbf4b0fe3e09e9b7b71fd287ba3",
    "value": {
      "type": "approval_required",
      "action": "task",
      "details": {
        "subagent_type": "code-writer",
        "description": "Write a hello world function..."
      },
      "timestamp": 1769018388270
    }
  }
]
 lc_error_code: undefined,
 interrupts: [
  [Object ...]  // ← Property EXISTS here
],

Stack trace:

at interrupt (/node_modules/@langchain/langgraph/dist/interrupt.js:71:8)
at <anonymous> (/apps/agents-demo/src/routes/chat.ts:407:38)
at wrapToolCall (/apps/agents-demo/src/routes/chat.ts:383:28)
at <anonymous> (/node_modules/langchain/dist/agents/utils.js:316:26)
at <anonymous> (/node_modules/langchain/dist/agents/utils.js:314:33)
at <anonymous> (/node_modules/langchain/dist/agents/utils.js:291:38)
at <anonymous> (/node_modules/deepagents/dist/index.js:775:25)

Client receives:

{
  "type": "error",
  "errorText": "undefined is not an object (evaluating 'error.interrupts.length')"
}

The property exists when thrown but is lost during propagation through the middleware chain.

Test Files Available

All test code and reproduction scripts are available in our repository:

  • /apps/agents-demo/GITHUB_ISSUE.md - Full technical analysis
  • /apps/agents-demo/ISSUE_REPRODUCTION.md - Step-by-step reproduction
  • /apps/agents-demo/reproduce-graphinterrupt-issue.ts - Standalone repro

Working Workaround

We've implemented a keyword-based workaround that avoids runtime.interrupt():

// Detect keywords before streaming starts
const subagentKeywords = [
  "code-writer", "code-reader", "spawn", "subagent",
  "write a", "create a", "implement", "fix the"
];
const mightSpawnSubagent = subagentKeywords.some(k => 
  message.toLowerCase().includes(k.toLowerCase())
);

// Return interrupt data WITHOUT calling runtime.interrupt()
if (enableHITL && mightSpawnSubagent && !approval) {
  return c.json({
    status: "awaiting_approval",
    threadId,
    checkpointId: `checkpoint-${Date.now()}-${threadId}`,
    interruptData: {
      type: "approval_required",
      action: "spawn_subagent",
      details: { ... },
    },
  });
}

// Resume with Command (this part works fine)
if (approval && checkpointId) {
  const command = new Command({ 
    resume: { decisions: [{ type: "approve" }] } 
  });
  
  for await (const chunk of agent.streamAISDK(command, { threadId })) {
    // Stream results
  }
}

Results: 6/6 tests passing with this workaround, but it's not the proper LangGraph HITL pattern.

Why This Matters

This bug makes DeepAgents incompatible with LangGraph's Human-in-the-Loop pattern, which is a core feature for production agentic applications. Every other LangGraph implementation supports HITL via runtime.interrupt(), but DeepAgents cannot.

We're happy to help test any fixes or provide more information!

@suhasdeshpande commented on GitHub (Jan 21, 2026): ## Additional Test Evidence ### Server Logs Showing Property Loss When `runtime.interrupt()` is called from middleware, the server logs show: ```typescript [HITL] Tool call intercepted: task [HITL] Task tool requires approval - calling runtime.interrupt() // GraphInterrupt is thrown with correct structure: GraphInterrupt: [ { "id": "c76d2dbf4b0fe3e09e9b7b71fd287ba3", "value": { "type": "approval_required", "action": "task", "details": { "subagent_type": "code-writer", "description": "Write a hello world function..." }, "timestamp": 1769018388270 } } ] lc_error_code: undefined, interrupts: [ [Object ...] // ← Property EXISTS here ], ``` **Stack trace:** ``` at interrupt (/node_modules/@langchain/langgraph/dist/interrupt.js:71:8) at <anonymous> (/apps/agents-demo/src/routes/chat.ts:407:38) at wrapToolCall (/apps/agents-demo/src/routes/chat.ts:383:28) at <anonymous> (/node_modules/langchain/dist/agents/utils.js:316:26) at <anonymous> (/node_modules/langchain/dist/agents/utils.js:314:33) at <anonymous> (/node_modules/langchain/dist/agents/utils.js:291:38) at <anonymous> (/node_modules/deepagents/dist/index.js:775:25) ``` **Client receives:** ```json { "type": "error", "errorText": "undefined is not an object (evaluating 'error.interrupts.length')" } ``` The property exists when thrown but is lost during propagation through the middleware chain. ### Test Files Available All test code and reproduction scripts are available in our repository: - `/apps/agents-demo/GITHUB_ISSUE.md` - Full technical analysis - `/apps/agents-demo/ISSUE_REPRODUCTION.md` - Step-by-step reproduction - `/apps/agents-demo/reproduce-graphinterrupt-issue.ts` - Standalone repro ### Working Workaround We've implemented a keyword-based workaround that avoids `runtime.interrupt()`: ```typescript // Detect keywords before streaming starts const subagentKeywords = [ "code-writer", "code-reader", "spawn", "subagent", "write a", "create a", "implement", "fix the" ]; const mightSpawnSubagent = subagentKeywords.some(k => message.toLowerCase().includes(k.toLowerCase()) ); // Return interrupt data WITHOUT calling runtime.interrupt() if (enableHITL && mightSpawnSubagent && !approval) { return c.json({ status: "awaiting_approval", threadId, checkpointId: `checkpoint-${Date.now()}-${threadId}`, interruptData: { type: "approval_required", action: "spawn_subagent", details: { ... }, }, }); } // Resume with Command (this part works fine) if (approval && checkpointId) { const command = new Command({ resume: { decisions: [{ type: "approve" }] } }); for await (const chunk of agent.streamAISDK(command, { threadId })) { // Stream results } } ``` **Results:** 6/6 tests passing with this workaround, but it's not the proper LangGraph HITL pattern. ### Why This Matters This bug makes DeepAgents incompatible with LangGraph's Human-in-the-Loop pattern, which is a core feature for production agentic applications. Every other LangGraph implementation supports HITL via `runtime.interrupt()`, but DeepAgents cannot. We're happy to help test any fixes or provide more information!
yindo changed title from GraphInterrupt thrown from tool loses interrupts property during error propagation to [GH-ISSUE #131] GraphInterrupt thrown from tool loses interrupts property during error propagation 2026-06-05 17:21:05 -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#36