bug: BaseChatModel.invoke errors with a null chatMessage #195

Closed
opened 2026-02-15 17:17:07 -05:00 by yindo · 9 comments
Owner

Originally created by @bguiz on GitHub (Mar 10, 2025).

The following error gets output upon a graph.invoke after a node in which there was an interrupt:

file:///Users/user/code/foobar/node_modules/@langchain/core/dist/language_models/chat_models.js:64
        return chatGeneration.message;
                              ^

TypeError: Cannot read properties of undefined (reading 'message')
    at ChatOpenAI.invoke (file:///Users/user/code/foobar/node_modules/@langchain/core/dist/language_models/chat_models.js:64:31)
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async RunnableCallable.agentNode [as func] (file:///Users/user/code/foobar/cqrsAgent-2.js:48:20)
    at async RunnableCallable.invoke (file:///Users/user/code/foobar/node_modules/@langchain/langgraph/dist/utils.js:79:27)
    at async RunnableSequence.invoke (file:///Users/user/code/foobar/node_modules/@langchain/core/dist/runnables/base.js:1274:33)
    at async _runWithRetry (file:///Users/user/code/foobar/node_modules/@langchain/langgraph/dist/pregel/retry.js:67:22)
    at async PregelRunner._executeTasksWithRetry (file:///Users/user/code/foobar/node_modules/@langchain/langgraph/dist/pregel/runner.js:217:33)
    at async PregelRunner.tick (file:///Users/user/code/foobar/node_modules/@langchain/langgraph/dist/pregel/runner.js:45:40)
    at async CompiledStateGraph._runLoop (file:///Users/user/code/foobar/node_modules/@langchain/langgraph/dist/pregel/index.js:1296:17)
    at async createAndRunLoop (file:///Users/user/code/foobar/node_modules/@langchain/langgraph/dist/pregel/index.js:1195:17) {
  pregelTaskId: '69df2cff-4e7c-53d0-a483-37e7c04f0913'
}

Node.js v23.3.0

To reproduce, using 2 different approaches:

Originally created by @bguiz on GitHub (Mar 10, 2025). The following error gets output upon a `graph.invoke` after a node in which there was an `interrupt`: ``` file:///Users/user/code/foobar/node_modules/@langchain/core/dist/language_models/chat_models.js:64 return chatGeneration.message; ^ TypeError: Cannot read properties of undefined (reading 'message') at ChatOpenAI.invoke (file:///Users/user/code/foobar/node_modules/@langchain/core/dist/language_models/chat_models.js:64:31) at process.processTicksAndRejections (node:internal/process/task_queues:105:5) at async RunnableCallable.agentNode [as func] (file:///Users/user/code/foobar/cqrsAgent-2.js:48:20) at async RunnableCallable.invoke (file:///Users/user/code/foobar/node_modules/@langchain/langgraph/dist/utils.js:79:27) at async RunnableSequence.invoke (file:///Users/user/code/foobar/node_modules/@langchain/core/dist/runnables/base.js:1274:33) at async _runWithRetry (file:///Users/user/code/foobar/node_modules/@langchain/langgraph/dist/pregel/retry.js:67:22) at async PregelRunner._executeTasksWithRetry (file:///Users/user/code/foobar/node_modules/@langchain/langgraph/dist/pregel/runner.js:217:33) at async PregelRunner.tick (file:///Users/user/code/foobar/node_modules/@langchain/langgraph/dist/pregel/runner.js:45:40) at async CompiledStateGraph._runLoop (file:///Users/user/code/foobar/node_modules/@langchain/langgraph/dist/pregel/index.js:1296:17) at async createAndRunLoop (file:///Users/user/code/foobar/node_modules/@langchain/langgraph/dist/pregel/index.js:1195:17) { pregelTaskId: '69df2cff-4e7c-53d0-a483-37e7c04f0913' } Node.js v23.3.0 ``` To reproduce, using 2 different approaches: - https://gist.github.com/bguiz/d0d5d3b732f9395c1a6817700fd5a311#file-langgraph-agent-interrupt-1-reject-node-js - https://gist.github.com/bguiz/d0d5d3b732f9395c1a6817700fd5a311#file-langgraph-agent-interrupt-2-update-tool-call-js
yindo added the docs-needed label 2026-02-15 17:17:08 -05:00
yindo closed this issue 2026-02-15 17:17:08 -05:00
Author
Owner

@bguiz commented on GitHub (Mar 10, 2025):

Here is a Stackoverflow question where I ask about this, and the above are the implementations of the two solutions provided in an answer that do not require rewriting history (i.e. going back to a previous check point in the graph execution).

https://stackoverflow.com/q/79471648/194982

@bguiz commented on GitHub (Mar 10, 2025): Here is a Stackoverflow question where I ask about this, and the above are the implementations of the two solutions provided in an answer that do not require rewriting history (i.e. going back to a previous check point in the graph execution). https://stackoverflow.com/q/79471648/194982
Author
Owner

@benjamincburns commented on GitHub (Mar 13, 2025):

@bguiz thanks for sharing code samples, but the samples that you shared are quite complex, and they depend on a file that isn't in the gist (./api/openrouter-openai.js). Would it be possible for you to share a more minimal reproduction of the issue that you're experiencing? If not, could you please add your openrouter-openai implementation to the gist?

It would also help if you could please share the langgraphjs and langchain core versions that you're running against, and/or advise whether the issue is present when running against current latest versions of each?

Finally, are you able to test against the latest version of the official @langchain/openai integration? If you're using openrouter, I believe you should be able to point it at openrouter by configuring the base URL as shown here. Also here are the API reference docs for the object passed to the ChatOpenAI constructor, and here is the API reference for the configuration field of that object

@benjamincburns commented on GitHub (Mar 13, 2025): @bguiz thanks for sharing code samples, but the samples that you shared are quite complex, and they depend on a file that isn't in the gist (`./api/openrouter-openai.js`). Would it be possible for you to share a more minimal reproduction of the issue that you're experiencing? If not, could you please add your `openrouter-openai` implementation to the gist? It would also help if you could please share the langgraphjs and langchain core versions that you're running against, and/or advise whether the issue is present when running against current latest versions of each? Finally, are you able to test against the latest version of the official `@langchain/openai` integration? If you're using openrouter, I believe you should be able to point it at openrouter by configuring the base URL as shown [here](https://js.langchain.com/docs/integrations/chat/openai/#custom-urls). Also here are [the API reference docs for the object passed to the `ChatOpenAI` constructor](https://v03.api.js.langchain.com/interfaces/_langchain_openai.ChatOpenAIFields.html), and here is [the API reference for the `configuration` field of that object](https://v03.api.js.langchain.com/interfaces/_langchain_openai.ClientOptions.html)
Author
Owner

@benjamincburns commented on GitHub (Mar 13, 2025):

@bguiz looking again, this time at the StackOverflow question you posted. I haven't executed your code, but I think that the issue could be that on refusal you're not inserting a ToolMessage into the messages state. There are some docs on this here

You can handle this on refusal by returning a command with an update: field that has a tool message. For example:

async function approveNode (state) {
  console.log('===APPROVE NODE===');
  const lastMsg = state.messages.at(-1);
  const toolCall = lastMsg.tool_calls.at(-1);

  const interruptMessage = `Please review the following tool invocation:
${toolCall.name} with inputs ${JSON.stringify(toolCall.args, undefined, 2)}
Do you approve (y/N)`;

  console.log('=INTERRUPT PRE=');
  const interruptResponse = interrupt(interruptMessage);
  console.log('=INTERRUPT POST=');

  const isApproved = (interruptResponse.trim().charAt(0).toLowerCase() === 'y');
  if (isApproved) {
      return new Command({ goto: 'tools' });
  }

  // rejection case
  return new Command({
    goto: END,
    update: {
      messages: [
        new ToolMessage({
          status: "error"
          content: `The user declined your request to execute the ${toolCall.name} tool, with arguments ${JSON.stringify(toolCall.args)}`,
          tool_call_id: toolCall.id
        }]
    });
}

Also bear in mind that this implementation is not handling parallel tool calls. To handle parallel tool calls you have a few options.

  • Decline to process all tool calls if the user disallows any tool call
    • You'll need to add one rejection ToolMessage per tool call, as shown above
    • In this model you might as well also only call interrupt once for the whole batch of calls
  • Allow approved calls to proceed without running denied calls:
    • Two ways to do this:
    • Option 1: Process all of the interrupts/approvals in a loop and return a Command that routes to tools if any calls are approved (or END if no calls are approved)
      • To prevent the declined calls from processing, you'll want to use a Send object in the goto field and send a copy of the AIMessage with the tool calls filtered down to just the approved list.
      • You'll still need the array of ToolMessage in the update field of the command as above - one for each declined call.
    • Option 2: Use an array of Send in your conditional edge to fan out the tool calls to the tools node (by sending a filtered copy of the AIMessage, as mentioned above) and do the interrupt in the tool handler.
      • Without Send here you would wind up processing all tool messages in the same node, which would cause the approved tool calls to be reprocessed for every time the graph is interrupted after that particular tool call is approved.

Here's a hastily-written example of how you could write a wrapper that requires approval for individual tool handlers for use with the "Option 2" approach mentioned in the last bullet above:

function requiresApproval<ToolHandlerT extends (...args: unknown[]) => unknown>(toolHandler: toolHandlerT) {
  return (...args: unknown[]) => {
    const interruptMessage = `Please review the following tool invocation: ${toolHandler.name}(${args.map(JSON.stringify).join", "})`;
    const interruptResponse = interrupt(interruptMessage);
    const isApproved = (interruptResponse.trim().charAt(0).toLowerCase() === 'y');
    if (isApproved) {
      return toolHandler(..args);
    }
    throw new Error(`The user declined your request to execute the ${toolHandler.name} tool, with arguments ${JSON.stringify(args)}`);
  }
}
@benjamincburns commented on GitHub (Mar 13, 2025): @bguiz looking again, this time at the StackOverflow question you posted. I haven't executed your code, but I think that the issue _could_ be that on refusal you're not inserting a `ToolMessage` into the `messages` state. There are some docs on this [here](https://js.langchain.com/docs/how_to/tool_results_pass_to_model) You can handle this on refusal by returning a command with an `update: ` field that has a tool message. For example: ```typescript async function approveNode (state) { console.log('===APPROVE NODE==='); const lastMsg = state.messages.at(-1); const toolCall = lastMsg.tool_calls.at(-1); const interruptMessage = `Please review the following tool invocation: ${toolCall.name} with inputs ${JSON.stringify(toolCall.args, undefined, 2)} Do you approve (y/N)`; console.log('=INTERRUPT PRE='); const interruptResponse = interrupt(interruptMessage); console.log('=INTERRUPT POST='); const isApproved = (interruptResponse.trim().charAt(0).toLowerCase() === 'y'); if (isApproved) { return new Command({ goto: 'tools' }); } // rejection case return new Command({ goto: END, update: { messages: [ new ToolMessage({ status: "error" content: `The user declined your request to execute the ${toolCall.name} tool, with arguments ${JSON.stringify(toolCall.args)}`, tool_call_id: toolCall.id }] }); } ``` Also bear in mind that this implementation is not handling parallel tool calls. To handle parallel tool calls you have a few options. - Decline to process all tool calls if the user disallows any tool call - You'll need to add one rejection `ToolMessage` per tool call, as shown above - In this model you might as well also only call `interrupt` once for the whole batch of calls - Allow approved calls to proceed without running denied calls: - Two ways to do this: - Option 1: Process all of the interrupts/approvals in a loop and return a Command that routes to `tools` if any calls are approved (or `END` if no calls are approved) - To prevent the declined calls from processing, you'll want to use a [`Send`](https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph.Send.html) object in the `goto` field and send a copy of the `AIMessage` with the tool calls filtered down to just the approved list. - You'll still need the array of `ToolMessage` in the `update` field of the command as above - one for each declined call. - Option 2: Use an array of [`Send`](https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph.Send.html) in your conditional edge to fan out the tool calls to the `tools` node (by sending a filtered copy of the `AIMessage`, as mentioned above) and do the `interrupt` in the tool handler. - Without `Send` here you would wind up processing all tool messages in the same node, which would cause the approved tool calls to be reprocessed for every time the graph is interrupted after that particular tool call is approved. Here's a hastily-written example of how you could write a wrapper that requires approval for individual tool handlers for use with the "Option 2" approach mentioned in the last bullet above: ```typescript function requiresApproval<ToolHandlerT extends (...args: unknown[]) => unknown>(toolHandler: toolHandlerT) { return (...args: unknown[]) => { const interruptMessage = `Please review the following tool invocation: ${toolHandler.name}(${args.map(JSON.stringify).join", "})`; const interruptResponse = interrupt(interruptMessage); const isApproved = (interruptResponse.trim().charAt(0).toLowerCase() === 'y'); if (isApproved) { return toolHandler(..args); } throw new Error(`The user declined your request to execute the ${toolHandler.name} tool, with arguments ${JSON.stringify(args)}`); } } ```
Author
Owner

@benjamincburns commented on GitHub (Mar 13, 2025):

flagged docs-needed because I think it'd be useful if we improved the tool call review howto to address some of the things mentioned here.

@benjamincburns commented on GitHub (Mar 13, 2025): flagged `docs-needed` because I think it'd be useful if we improved [the tool call review howto](https://langchain-ai.github.io/langgraphjs/how-tos/review-tool-calls/) to address some of the things mentioned here.
Author
Owner

@bguiz commented on GitHub (Mar 13, 2025):

👋 hey @benjamincburns thanks for taking the time to respond!

but the samples that you shared are quite complex, and they depend on a file that isn't in the gist (./api/openrouter-openai.js).
That is a standard LLM instance, basically an instance of ChatOpenAI (import { ChatOpenAI } from '@langchain/openai';).

The full implementation is here: https://github.com/hedera-dev/hedera-ai-agent-workshop-langgraph/blob/main/api/openrouter-openai.js

Would it be possible for you to share a more minimal reproduction of the issue that you're experiencing? If not, could you please add your openrouter-openai implementation to the gist?
This is actually the minimal reproduction.

The full implementation would be something like this: https://github.com/hedera-dev/hedera-ai-agent-workshop-langgraph/blob/main/agent.js
(But replacing createReactAgent with an instance of StateGraph, as I have in the gist)

@bguiz commented on GitHub (Mar 13, 2025): 👋 hey @benjamincburns thanks for taking the time to respond! > but the samples that you shared are quite complex, and they depend on a file that isn't in the gist (`./api/openrouter-openai.js`). That is a standard LLM instance, basically an instance of `ChatOpenAI` (`import { ChatOpenAI } from '@langchain/openai';`). The full implementation is here: https://github.com/hedera-dev/hedera-ai-agent-workshop-langgraph/blob/main/api/openrouter-openai.js > Would it be possible for you to share a more minimal reproduction of the issue that you're experiencing? If not, could you please add your `openrouter-openai` implementation to the gist? This *is* actually the minimal reproduction. The full implementation would be something like this: https://github.com/hedera-dev/hedera-ai-agent-workshop-langgraph/blob/main/agent.js (But replacing `createReactAgent` with an instance of `StateGraph`, as I have in the gist)
Author
Owner

@bguiz commented on GitHub (Mar 13, 2025):

Finally, are you able to test against the latest version of the official @langchain/openai integration?

I'm using these versions:

    "@langchain/core": "0.3.40",
    "@langchain/langgraph": "0.2.49",
    "@langchain/openai": "0.4.4",

I'll update to the latest and report back!

If you're using openrouter, I believe you should be able to point it at openrouter by configuring the base URL as shown here. Also here are the API reference docs for the object passed to the ChatOpenAI constructor, and here is the API reference for the configuration field of that object

Yeah this is precisely what I'm doing, with OpenRouter: https://github.com/hedera-dev/hedera-ai-agent-workshop-langgraph/blob/main/api/openrouter-openai.js

@bguiz commented on GitHub (Mar 13, 2025): > Finally, are you able to test against the latest version of the official `@langchain/openai` integration? I'm using these versions: ```json "@langchain/core": "0.3.40", "@langchain/langgraph": "0.2.49", "@langchain/openai": "0.4.4", ``` I'll update to the latest and report back! > If you're using openrouter, I believe you should be able to point it at openrouter by configuring the base URL as shown [here](https://js.langchain.com/docs/integrations/chat/openai/#custom-urls). Also here are [the API reference docs for the object passed to the `ChatOpenAI` constructor](https://v03.api.js.langchain.com/interfaces/_langchain_openai.ChatOpenAIFields.html), and here is [the API reference for the `configuration` field of that object](https://v03.api.js.langchain.com/interfaces/_langchain_openai.ClientOptions.html) Yeah this is precisely what I'm doing, with OpenRouter: https://github.com/hedera-dev/hedera-ai-agent-workshop-langgraph/blob/main/api/openrouter-openai.js
Author
Owner

@bguiz commented on GitHub (Mar 13, 2025):

I haven't executed your code, but I think that the issue could be that on refusal you're not inserting a ToolMessage into the messages state. There are some docs on this here

This sounds promising - it isn't one of the options that was in the Stackoverflow response that I got.
Will try it out and get back to you!

@bguiz commented on GitHub (Mar 13, 2025): > I haven't executed your code, but I think that the issue _could_ be that on refusal you're not inserting a `ToolMessage` into the `messages` state. There are some docs on this [here](https://js.langchain.com/docs/how_to/tool_results_pass_to_model) This sounds promising - it isn't one of the options that was in the Stackoverflow response that I got. Will try it out and get back to you!
Author
Owner

@bguiz commented on GitHub (Mar 13, 2025):

flagged docs-needed because I think it'd be useful if we improved the tool call review howto to address some of the things mentioned here.

Yes, I think specifically, I'd say that:

  // rejection case
  return new Command({
    goto: END,
    update: {
      messages: [
        new ToolMessage({
          status: "error"
          content: `The user declined your request to execute the ${toolCall.name} tool, with arguments ${JSON.stringify(toolCall.args)}`,
          tool_call_id: toolCall.id
        }]
    });
@bguiz commented on GitHub (Mar 13, 2025): > flagged `docs-needed` because I think it'd be useful if we improved [the tool call review howto](https://langchain-ai.github.io/langgraphjs/how-tos/review-tool-calls/) to address some of the things mentioned here. Yes, I think specifically, I'd say that: - This page https://langchain-ai.github.io/langgraphjs/concepts/human_in_the_loop/#interrupt - Should link to this page: https://js.langchain.com/docs/how_to/tool_results_pass_to_model/ - and also feature the code example of a manually created `ToolMessage` within a `Command` to show how to use this within the context of an `interrupt` (taken from one of your responses above) ```js // rejection case return new Command({ goto: END, update: { messages: [ new ToolMessage({ status: "error" content: `The user declined your request to execute the ${toolCall.name} tool, with arguments ${JSON.stringify(toolCall.args)}`, tool_call_id: toolCall.id }] }); ```
Author
Owner

@bguiz commented on GitHub (Mar 14, 2025):

@benjamincburns thanks so much - the Command with a ToolMessage with status as error did the trick!

@bguiz commented on GitHub (Mar 14, 2025): @benjamincburns thanks so much - the `Command` with a `ToolMessage` with `status` as `error` did the trick!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraphjs#195