Send API serialize/deserialize issue #247

Open
opened 2026-02-15 18:15:01 -05:00 by yindo · 3 comments
Owner

Originally created by @brettshollenberger on GitHub (Apr 30, 2025).

Originally assigned to: @benjamincburns, @dqbd on GitHub.

Hey all, just noticed an issue with serializing/deserializing objects that are wrapped with some custom class when using the Send API, for example BaseMessage, HumanMessage, etc.

  • For contrast, return a BaseMessage from a regular node, and it will stay wrapped as a BaseMessage
  • But if you queue the state to separate workers using Send, the workers will receive an unwrapped (plain object) with the data.
  • This is easy enough to re-wrap as BaseMessage, but it causes some unexpected interactions with things like createReactAgent, which expect the MessageState interface to be applied (which fails when not wrapped as BaseMessage).

Minimal example:

export interface GraphState {
    userRequest: BaseMessage; 
    chatHistory: BaseMessage[];
    task: Task;
    queue: Task[];
    completedTasks: Task[];
}

const queueTasks = async(state: GraphState) => {
    // in here, state.userRequest and state.chatHistory are fine
    if (!state.queue) {
        return [];
    }
    const queue = state.queue;
    return queue.map(task => new Send("executeTasks", { ...state, task }));
}

const planTasks = async(state: GraphState): Promise<Partial<GraphState>> => {
  //   in here, state.userRequest and state.chatHistory are fine  
  const tasks = [ ... ] // invent some tasks
  return { queue: tasks }
}

const executeTask = async(state: GraphState): Promise<Partial<GraphState>> => {
  // in here, state.userRequest and state.chatHistory are unwrapped/plain objects
  const agent =  createReactAgent(... stuff);
    const agentState = {
    messages: [
      ...(state.chatHistory.map((message) => { return new BaseMessage(message); },
      new HumanMessage(state.userRequest.content) // If we don't do this, createReactAgent will complain
    ]
  }
  agent.invoke(agentState); // This would explode if we don't re-wrap our messages after Send API
}

export const graph = new StateGraph(GraphAnnotation, ConfigurationAnnotation)
    .addNode("planTasks", planTasks)
    .addNode("executeTask", executeTask)
    .addEdge(START, "planTasks")
    .addConditionalEdges("planTasks", queueTasks)
    .addEdge("executeTasks", END)

Just logging the issue, thank you!

Originally created by @brettshollenberger on GitHub (Apr 30, 2025). Originally assigned to: @benjamincburns, @dqbd on GitHub. Hey all, just noticed an issue with serializing/deserializing objects that are wrapped with some custom class when using the `Send` API, for example `BaseMessage`, `HumanMessage`, etc. * For contrast, return a `BaseMessage` from a regular node, and it will stay wrapped as a `BaseMessage` * But if you queue the state to separate workers using `Send`, the workers will receive an unwrapped (plain object) with the data. * This is easy enough to re-wrap as `BaseMessage`, but it causes some unexpected interactions with things like `createReactAgent`, which expect the `MessageState` interface to be applied (which fails when not wrapped as `BaseMessage`). Minimal example: ```typescript export interface GraphState { userRequest: BaseMessage; chatHistory: BaseMessage[]; task: Task; queue: Task[]; completedTasks: Task[]; } const queueTasks = async(state: GraphState) => { // in here, state.userRequest and state.chatHistory are fine if (!state.queue) { return []; } const queue = state.queue; return queue.map(task => new Send("executeTasks", { ...state, task })); } const planTasks = async(state: GraphState): Promise<Partial<GraphState>> => { // in here, state.userRequest and state.chatHistory are fine const tasks = [ ... ] // invent some tasks return { queue: tasks } } const executeTask = async(state: GraphState): Promise<Partial<GraphState>> => { // in here, state.userRequest and state.chatHistory are unwrapped/plain objects const agent = createReactAgent(... stuff); const agentState = { messages: [ ...(state.chatHistory.map((message) => { return new BaseMessage(message); }, new HumanMessage(state.userRequest.content) // If we don't do this, createReactAgent will complain ] } agent.invoke(agentState); // This would explode if we don't re-wrap our messages after Send API } export const graph = new StateGraph(GraphAnnotation, ConfigurationAnnotation) .addNode("planTasks", planTasks) .addNode("executeTask", executeTask) .addEdge(START, "planTasks") .addConditionalEdges("planTasks", queueTasks) .addEdge("executeTasks", END) ``` Just logging the issue, thank you!
yindo added the bug label 2026-02-15 18:15:01 -05:00
Author
Owner

@brettshollenberger commented on GitHub (May 7, 2025):

This seems to be related, but when Sending arrays of messages, I can't get them to serialize properly. When sending a single message, I can send successfully and then unwrap, but when sending messages: [], it blows up when trying to send.

    Troubleshooting URL: https://js.langchain.com/docs/troubleshooting/errors/MESSAGE_COERCION_FAILURE/

        at _constructMessageFromParams (file:///Users/brettshollenberger/programming/business/tools/bolt_me/node_modules/@langchain/core/dist/messages/utils.js:109:47)
        at coerceMessageLikeToMessage (file:///Users/brettshollenberger/programming/business/tools/bolt_me/node_modules/@langchain/core/dist/messages/utils.js:129:16)
        at Array.map (<anonymous>)
        at BinaryOperatorAggregate.messagesStateReducer (/Users/brettshollenberger/programming/business/tools/bolt_me/node_modules/@langchain/langgraph/src/graph/message.ts:29:59)
        at BinaryOperatorAggregate.update (/Users/brettshollenberger/programming/business/tools/bolt_me/node_modules/@langchain/langgraph/src/channels/binop.ts:57:27)
        at _applyWrites (/Users/brettshollenberger/programming/business/tools/bolt_me/node_modules/@langchain/langgraph/src/pregel/algo.ts:340:38)
        at PregelLoop.tick (/Users/brettshollenberger/programming/business/tools/bolt_me/node_modules/@langchain/langgraph/src/pregel/loop.ts:554:34)
        at CompiledStateGraph._runLoop (/Users/brettshollenberger/programming/business/tools/bolt_me/node_modules/@langchain/langgraph/src/pregel/index.ts:2040:20)
        at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
        at createAndRunLoop (/Users/brettshollenberger/programming/business/tools/bolt_me/node_modules/@langchain/langgraph/src/pregel/index.ts:1939:9) {
      lc_error_code: 'MESSAGE_COERCION_FAILURE',
      pregelTaskId: '1e24419c-1e62-55ec-aff7-a1f82fe9ff84'
    },
    Error: Unable to coerce message from array: only human, AI, system, developer, or tool message coercion is currently supported.

    Received: {
      "lc_serializable": true,
      "lc_kwargs": {
        "content": "# content here",
        "additional_kwargs": {
          "id": "msg_01EkxM5kYzR5yMDVMCAYEn2G",
          "type": "message",
          "role": "assistant",
          "model": "claude-3-5-haiku-20241022",
          "stop_reason": "end_turn",
          "stop_sequence": null,
          "usage": {
            "input_tokens": 201,
            "cache_creation_input_tokens": 0,
            "cache_read_input_tokens": 0,
            "output_tokens": 435
          }
        },
        "usage_metadata": {
          "input_tokens": 201,
          "output_tokens": 435,
          "total_tokens": 636,
          "input_token_details": {
            "cache_creation": 0,
            "cache_read": 0
          }
        },
        "response_metadata": {
          "id": "msg_01EkxM5kYzR5yMDVMCAYEn2G",
          "model": "claude-3-5-haiku-20241022",
          "stop_reason": "end_turn",
          "stop_sequence": null,
          "usage": {
            "input_tokens": 201,
            "cache_creation_input_tokens": 0,
            "cache_read_input_tokens": 0,
            "output_tokens": 435
          },
          "type": "message",
          "role": "assistant"
        },
        "id": "msg_01EkxM5kYzR5yMDVMCAYEn2G",
        "tool_calls": [],
        "invalid_tool_calls": []
      },
      "lc_namespace": [
        "langchain_core",
        "messages"
      ],
@brettshollenberger commented on GitHub (May 7, 2025): This seems to be related, but when `Send`ing arrays of messages, I can't get them to serialize properly. When sending a single message, I can send successfully and then unwrap, but when sending `messages: []`, it blows up when trying to send. ```tsx Troubleshooting URL: https://js.langchain.com/docs/troubleshooting/errors/MESSAGE_COERCION_FAILURE/ at _constructMessageFromParams (file:///Users/brettshollenberger/programming/business/tools/bolt_me/node_modules/@langchain/core/dist/messages/utils.js:109:47) at coerceMessageLikeToMessage (file:///Users/brettshollenberger/programming/business/tools/bolt_me/node_modules/@langchain/core/dist/messages/utils.js:129:16) at Array.map (<anonymous>) at BinaryOperatorAggregate.messagesStateReducer (/Users/brettshollenberger/programming/business/tools/bolt_me/node_modules/@langchain/langgraph/src/graph/message.ts:29:59) at BinaryOperatorAggregate.update (/Users/brettshollenberger/programming/business/tools/bolt_me/node_modules/@langchain/langgraph/src/channels/binop.ts:57:27) at _applyWrites (/Users/brettshollenberger/programming/business/tools/bolt_me/node_modules/@langchain/langgraph/src/pregel/algo.ts:340:38) at PregelLoop.tick (/Users/brettshollenberger/programming/business/tools/bolt_me/node_modules/@langchain/langgraph/src/pregel/loop.ts:554:34) at CompiledStateGraph._runLoop (/Users/brettshollenberger/programming/business/tools/bolt_me/node_modules/@langchain/langgraph/src/pregel/index.ts:2040:20) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) at createAndRunLoop (/Users/brettshollenberger/programming/business/tools/bolt_me/node_modules/@langchain/langgraph/src/pregel/index.ts:1939:9) { lc_error_code: 'MESSAGE_COERCION_FAILURE', pregelTaskId: '1e24419c-1e62-55ec-aff7-a1f82fe9ff84' }, Error: Unable to coerce message from array: only human, AI, system, developer, or tool message coercion is currently supported. Received: { "lc_serializable": true, "lc_kwargs": { "content": "# content here", "additional_kwargs": { "id": "msg_01EkxM5kYzR5yMDVMCAYEn2G", "type": "message", "role": "assistant", "model": "claude-3-5-haiku-20241022", "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 201, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 435 } }, "usage_metadata": { "input_tokens": 201, "output_tokens": 435, "total_tokens": 636, "input_token_details": { "cache_creation": 0, "cache_read": 0 } }, "response_metadata": { "id": "msg_01EkxM5kYzR5yMDVMCAYEn2G", "model": "claude-3-5-haiku-20241022", "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 201, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 435 }, "type": "message", "role": "assistant" }, "id": "msg_01EkxM5kYzR5yMDVMCAYEn2G", "tool_calls": [], "invalid_tool_calls": [] }, "lc_namespace": [ "langchain_core", "messages" ], ```
Author
Owner

@brettshollenberger commented on GitHub (May 7, 2025):

In terms of minimal example for this later one:

export interface GraphState {
    messages: BaseMessage[];
    queue: string[];
    task: string;
}

export const GraphAnnotation = Annotation.Root({
    messages: Annotation<BaseMessage[], BaseMessageLike[]>({ 
        default: () => [],
        reducer: messagesStateReducer
    })
})

const queueTasks = async(state: GraphState) => {
    const queue = state.queue;
    return queue.map(task => new Send("executeTasks", { ...state, task }));
}

The queue tasks function will give this last message

@brettshollenberger commented on GitHub (May 7, 2025): In terms of minimal example for this later one: ``` export interface GraphState { messages: BaseMessage[]; queue: string[]; task: string; } export const GraphAnnotation = Annotation.Root({ messages: Annotation<BaseMessage[], BaseMessageLike[]>({ default: () => [], reducer: messagesStateReducer }) }) const queueTasks = async(state: GraphState) => { const queue = state.queue; return queue.map(task => new Send("executeTasks", { ...state, task })); } ``` The queue tasks function will give this last message
Author
Owner

@yzhaoa commented on GitHub (Sep 25, 2025):

Just ran into this issue, got the exact same unable to coerce message ... error trying to use a message state passed through Send.

Looking through the codebase, the problem seems to be https://github.com/langchain-ai/langgraphjs/blob/d2b51aae9c900a5b36289cc8eb321c9f06357da8/libs/langgraph/src/constants.ts#L180, and particularly the recursive object reconstruction in https://github.com/langchain-ai/langgraphjs/blob/d2b51aae9c900a5b36289cc8eb321c9f06357da8/libs/langgraph/src/constants.ts#L479-L517.

This was partially fixed in 5fef2ed73a for lc_serializable objects, which includes BaseMessage. While I haven't tested this yet, I'm still using an older version of langgraphjs, so I suspect this bug was filed before the fix was installed.

The reason I say that this is partially fixed though, is that the _deserializeCommandSendObjectGraph function would still indiscriminantly break other objects that are not directly marked lc_serializable. For example, Set, or Map objects which are supported by langgraph (but not langchain), are erased to {} due to having no owned properties.

Workaround

Before this is fixed more thoroughly, or perhaps closed as is because lc_serializable is working now, you can exploit the nested Command exception, i.e.:

new Send('target', new Command({update: myObject}));

to protect myObject from clobbering.

I have not tested checkpoint restoration with the above workaround yet.

And obviously, write test cases to check whether future changes have broken your workaround.

@yzhaoa commented on GitHub (Sep 25, 2025): Just ran into this issue, got the exact same unable to coerce message ... error trying to use a message state passed through `Send`. Looking through the codebase, the problem seems to be https://github.com/langchain-ai/langgraphjs/blob/d2b51aae9c900a5b36289cc8eb321c9f06357da8/libs/langgraph/src/constants.ts#L180, and particularly the recursive object reconstruction in https://github.com/langchain-ai/langgraphjs/blob/d2b51aae9c900a5b36289cc8eb321c9f06357da8/libs/langgraph/src/constants.ts#L479-L517. This was partially fixed in 5fef2ed73adf0eb49edd9ed5365977243538a02b for lc_serializable objects, which includes BaseMessage. While I haven't tested this yet, I'm still using an older version of langgraphjs, so I suspect this bug was filed before the fix was installed. The reason I say that this is partially fixed though, is that the `_deserializeCommandSendObjectGraph` function would still indiscriminantly break other objects that are not directly marked lc_serializable. For example, `Set`, or `Map` objects which are supported by langgraph (but not langchain), are erased to `{}` due to having no owned properties. ## Workaround Before this is fixed more thoroughly, or perhaps closed as is because lc_serializable is working now, you can exploit the nested `Command` exception, i.e.: ```typescript new Send('target', new Command({update: myObject})); ``` to protect `myObject` from clobbering. I have not tested checkpoint restoration with the above workaround yet. And obviously, write test cases to check whether future changes have broken your workaround.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraphjs#247