Custom reducer seems ok but internal state looks incorrect #134

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

Originally created by @nigel-daniels on GitHub (Nov 14, 2024).

I'm attempting to follow the LangGraph course on DeepLearning.ai, AI Agents in LangGraph. I've been following along converting the Python examples into the equivalent JS code to help me understand the LangGraph.JS codebase. I'm hitting a problem when attempting to modify state using the update state. As per the exercise I have implemented a custom reducer and that seems to be functioning as expected, however calling graph.getState(config) I get a different set of messages in the state in which the first HumanMessage does not have an id as returned by the reducer. If I understand correctly this state should look exactly like the one the custom reducer had returned. Below is the code I am running and the package.json file so you can see the versions and libraries in use.

import { StateGraph, Annotation, END } from "@langchain/langgraph";
import { ChatOpenAI } from '@langchain/openai';
import { MessageUnion, SystemMessage, HumanMessage, AIMessage } from '@langchain/core/messages';
import { ToolMessage } from '@langchain/core/messages/tool'
import { SqliteSaver } from '@langchain/langgraph-checkpoint-sqlite';
import { TavilySearchResults } from '@langchain/community/tools/tavily_search';
import { v4 as uuid4 } from 'uuid';
import confirm from '@inquirer/confirm';

// Here we set up the parts we need for the agent

// First define the memory for persistance
const memory = SqliteSaver.fromConnString(':memory:');

// Set up the tools
const tools = [new TavilySearchResults({maxResults: 2})];

// First we define the state (with a custom reducer)
const AgentState = Annotation.Root({
	messages: Annotation<BaseMessage[]>({
		reducer: (current, update) => reduceMessages(current, update), // See below for function
		default: () => []
	})
});

// Now the system prompt
const system = `You are a smart research assistant. Use the search engine to look up information.
You are allowed to make multiple calls (either together or in sequence).
Only look up information when you are sure of what you want.
If you need to look up some information before asking a follow up question, you are allowed to do that!`;

// Now the model we will use
const model = new ChatOpenAI({
	model: 'gpt-3.5-turbo',
}).bindTools(tools);


// Now we can construct our agent graph
const graph = new StateGraph(AgentState)
	.addNode('llm', callOpenAi) // See below for function
	.addNode('action', takeAction) // See below for function
	.addConditionalEdges(
		'llm',
		existsAction, // See below for function
		{true: 'action', false: END}
	)
	.addEdge('action','llm')
	.setEntryPoint('llm')
	.compile({
		checkpointer: memory,
		interruptBefore: ['action']
	});

const messages = [new HumanMessage('What is the weather in LA?')];

const config = {
	configurable: {thread_id: '1'}
};

for await (const event of await graph.stream({messages: messages}, config)) {
	for (const [node, values] of Object.entries(event)) {
		console.log('\nStreaming:')
		console.log(values.messages);
	}
}

let currentValues = await graph.getState(config);

// This is the current state
console.log('\nCurrent state after streaming until interrupt:');
console.log(currentValues.values); // HERE WE GET A DIFFERENT STATE!
// The state returned at this point now hase a human message without an id!

// Store the tool_call ID
const _id = currentValues.values.messages[currentValues.values.messages.length-1].tool_calls[0].id;
// Now let's replace the tool call with our own, updating the query but preserving the ID
currentValues.values.messages[currentValues.values.messages.length-1].tool_calls = [{
	name: 'tavily_search_results_json',
  	args: {
		query: 'current weather in Louisiana'
	},
	type: 'tool_call',
	id: _id
}];

await graph.updateState(config, currentValues.values);

let newValues = await graph.getState(config);
console.log('\nThe new state after running the update:');
console.log(newValues);
/*
for await (const event of await graph.stream(null, config)) {
	for (const [node, values] of Object.entries(event)) {
		console.log(values.messages);
	}
}
*/

///////// FUNCTIONS USED BY THE GRAPH //////////

// This is the function for the conditional edge
function existsAction(state) {
	const result = state.messages[state.messages.length-1];

	return result.tool_calls ? result.tool_calls.length > 0 : false;
}

// This is the function for the llm node
async function callOpenAi(state) {
	let messages = state.messages;

	if (system) {
		messages = [new SystemMessage(system), ...messages];
	}

	const message = await model.invoke(messages);
	return {messages: [message]};
}

// This is the function for the action node
async function takeAction(state) {
	const results = [];
	const toolCalls = state.messages[state.messages.length-1].tool_calls;
	let result = null;

	for(const t of toolCalls) {
		//console.log('Calling: ' + JSON.stringify(t));
		if (tools.some(tool => tool.name === t.name)) {
			const tool = tools.find(tool => tool.name === t.name);
			result = await tool.invoke(t.args);
		} else {
			//console.log('\n...bad tool name...');
			result = 'bad tool name, retry';
		}
		results.push(new ToolMessage({tool_call_id: t.id, name: t.name, content: result.toString()}));
	}
	//console.log('Back to the model!');
	return {messages: results};
}

// This function is used by the graph state to update the messages list
// This lets us update previous messages based on id (or we add them)
function reduceMessages(current: MessageUnion[], update: MessageUnion[]):MessaggeUnion[] {
	// Ensure any new messages have an id
	for (const newMessage of update) {
		if (newMessage.id === null || newMessage.id === undefined) {
			newMessage.id = uuid4();
		}
	}

	console.log('\nReducer update array:');
	console.log(update);

	// Copy the current set of messages
	const merged = [...current];

	// Now check to see if the new message exists in the curren set of messages
	// if it does we update it if it does not we append it to the end ofg the array
	for (const newMessage of update) {
		const i = merged.findIndex(existingMessage => existingMessage.id === newMessage.id);

		i !== -1 ? merged[i] = newMessage : merged.push(newMessage);
	}

	// Now return the fully merged messages
	console.log('\nReducer returning:');
	console.log(merged);
	return merged;
}

Here is the package.json

{
  "name": "agents-langgraph",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "@langchain/community": "^0.3.8",
    "@langchain/langgraph": "^0.2.20",
    "@langchain/langgraph-checkpoint-sqlite": "^0.1.3",
    "@tavily/core": "^0.0.2",
    "langchain": "^0.3.5",
    "openai": "^4.68.2",
    "tsx": "^4.19.1",
    "uuid": "^11.0.3"
  }
}

Note this code also expects that you have OPENAI_API_KEY and TAVILY_API_KEY exported in your shells env.

Here is the output I get:

Reducer update array:
[
  HumanMessage {
    "id": "7f929582-8d9a-4dc8-af37-dd1be7e2cd76",
    "content": "What is the weather in LA?",
    "additional_kwargs": {},
    "response_metadata": {}
  }
]

Reduce returning:
[
  HumanMessage {
    "id": "7f929582-8d9a-4dc8-af37-dd1be7e2cd76",
    "content": "What is the weather in LA?",
    "additional_kwargs": {},
    "response_metadata": {}
  }
]

Reducer update array:
[
  AIMessage {
    "id": "chatcmpl-ATcjMMghWhQKawZFYFZ8v149AkRKI",
    "content": "",
    "additional_kwargs": {
      "tool_calls": [
        {
          "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N",
          "type": "function",
          "function": "[Object]"
        }
      ]
    },
    "response_metadata": {
      "tokenUsage": {
        "promptTokens": 145,
        "completionTokens": 22,
        "totalTokens": 167
      },
      "finish_reason": "tool_calls"
    },
    "tool_calls": [
      {
        "name": "tavily_search_results_json",
        "args": {
          "input": "current weather in Los Angeles"
        },
        "type": "tool_call",
        "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N"
      }
    ],
    "invalid_tool_calls": [],
    "usage_metadata": {
      "output_tokens": 22,
      "input_tokens": 145,
      "total_tokens": 167,
      "input_token_details": {
        "audio": 0,
        "cache_read": 0
      },
      "output_token_details": {
        "audio": 0,
        "reasoning": 0
      }
    }
  }
]

Reduce returning:
[
  HumanMessage {
    "id": "7f929582-8d9a-4dc8-af37-dd1be7e2cd76",
    "content": "What is the weather in LA?",
    "additional_kwargs": {},
    "response_metadata": {}
  },
  AIMessage {
    "id": "chatcmpl-ATcjMMghWhQKawZFYFZ8v149AkRKI",
    "content": "",
    "additional_kwargs": {
      "tool_calls": [
        {
          "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N",
          "type": "function",
          "function": "[Object]"
        }
      ]
    },
    "response_metadata": {
      "tokenUsage": {
        "promptTokens": 145,
        "completionTokens": 22,
        "totalTokens": 167
      },
      "finish_reason": "tool_calls"
    },
    "tool_calls": [
      {
        "name": "tavily_search_results_json",
        "args": {
          "input": "current weather in Los Angeles"
        },
        "type": "tool_call",
        "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N"
      }
    ],
    "invalid_tool_calls": [],
    "usage_metadata": {
      "output_tokens": 22,
      "input_tokens": 145,
      "total_tokens": 167,
      "input_token_details": {
        "audio": 0,
        "cache_read": 0
      },
      "output_token_details": {
        "audio": 0,
        "reasoning": 0
      }
    }
  }
]

Reducer update array:
[
  AIMessage {
    "id": "chatcmpl-ATcjMMghWhQKawZFYFZ8v149AkRKI",
    "content": "",
    "additional_kwargs": {
      "tool_calls": [
        {
          "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N",
          "type": "function",
          "function": "[Object]"
        }
      ]
    },
    "response_metadata": {
      "tokenUsage": {
        "promptTokens": 145,
        "completionTokens": 22,
        "totalTokens": 167
      },
      "finish_reason": "tool_calls"
    },
    "tool_calls": [
      {
        "name": "tavily_search_results_json",
        "args": {
          "input": "current weather in Los Angeles"
        },
        "type": "tool_call",
        "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N"
      }
    ],
    "invalid_tool_calls": [],
    "usage_metadata": {
      "output_tokens": 22,
      "input_tokens": 145,
      "total_tokens": 167,
      "input_token_details": {
        "audio": 0,
        "cache_read": 0
      },
      "output_token_details": {
        "audio": 0,
        "reasoning": 0
      }
    }
  }
]

Reduce returning:
[
  HumanMessage {
    "id": "7f929582-8d9a-4dc8-af37-dd1be7e2cd76",
    "content": "What is the weather in LA?",
    "additional_kwargs": {},
    "response_metadata": {}
  },
  AIMessage {
    "id": "chatcmpl-ATcjMMghWhQKawZFYFZ8v149AkRKI",
    "content": "",
    "additional_kwargs": {
      "tool_calls": [
        {
          "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N",
          "type": "function",
          "function": "[Object]"
        }
      ]
    },
    "response_metadata": {
      "tokenUsage": {
        "promptTokens": 145,
        "completionTokens": 22,
        "totalTokens": 167
      },
      "finish_reason": "tool_calls"
    },
    "tool_calls": [
      {
        "name": "tavily_search_results_json",
        "args": {
          "input": "current weather in Los Angeles"
        },
        "type": "tool_call",
        "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N"
      }
    ],
    "invalid_tool_calls": [],
    "usage_metadata": {
      "output_tokens": 22,
      "input_tokens": 145,
      "total_tokens": 167,
      "input_token_details": {
        "audio": 0,
        "cache_read": 0
      },
      "output_token_details": {
        "audio": 0,
        "reasoning": 0
      }
    }
  }
]

Streaming:
[
  AIMessage {
    "id": "chatcmpl-ATcjMMghWhQKawZFYFZ8v149AkRKI",
    "content": "",
    "additional_kwargs": {
      "tool_calls": [
        {
          "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N",
          "type": "function",
          "function": "[Object]"
        }
      ]
    },
    "response_metadata": {
      "tokenUsage": {
        "promptTokens": 145,
        "completionTokens": 22,
        "totalTokens": 167
      },
      "finish_reason": "tool_calls"
    },
    "tool_calls": [
      {
        "name": "tavily_search_results_json",
        "args": {
          "input": "current weather in Los Angeles"
        },
        "type": "tool_call",
        "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N"
      }
    ],
    "invalid_tool_calls": [],
    "usage_metadata": {
      "output_tokens": 22,
      "input_tokens": 145,
      "total_tokens": 167,
      "input_token_details": {
        "audio": 0,
        "cache_read": 0
      },
      "output_token_details": {
        "audio": 0,
        "reasoning": 0
      }
    }
  }
]

<<< NOTE: AT THIS POINT THE id IN THE HUMAN MESSAGE IS GONE >>>
Current state after streaming until interrupt:  
{
  messages: [
    HumanMessage {
      "content": "What is the weather in LA?",
      "additional_kwargs": {},
      "response_metadata": {}
    },
    AIMessage {
      "id": "chatcmpl-ATcjMMghWhQKawZFYFZ8v149AkRKI",
      "content": "",
      "additional_kwargs": {
        "tool_calls": [
          {
            "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N",
            "type": "function",
            "function": "[Object]"
          }
        ]
      },
      "response_metadata": {
        "tokenUsage": {
          "promptTokens": 145,
          "completionTokens": 22,
          "totalTokens": 167
        },
        "finish_reason": "tool_calls"
      },
      "tool_calls": [
        {
          "name": "tavily_search_results_json",
          "args": {
            "input": "current weather in Los Angeles"
          },
          "type": "tool_call",
          "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N"
        }
      ],
      "invalid_tool_calls": []
    }
  ]
}

<<< NOW: THE REDUCER GETS THE MESSAGES EXPECTED BUT THE INTERNAL STATE IS DIFFERENT?? >>>
Reducer update array:
[
  HumanMessage {
    "id": "5c95aa4a-23c0-40b8-b814-6ccd97137b70",
    "content": "What is the weather in LA?",
    "additional_kwargs": {},
    "response_metadata": {}
  },
  AIMessage {
    "id": "chatcmpl-ATcjMMghWhQKawZFYFZ8v149AkRKI",
    "content": "",
    "additional_kwargs": {
      "tool_calls": [
        {
          "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N",
          "type": "function",
          "function": "[Object]"
        }
      ]
    },
    "response_metadata": {
      "tokenUsage": {
        "promptTokens": 145,
        "completionTokens": 22,
        "totalTokens": 167
      },
      "finish_reason": "tool_calls"
    },
    "tool_calls": [
      {
        "name": "tavily_search_results_json",
        "args": {
          "query": "current weather in Louisiana"
        },
        "type": "tool_call",
        "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N"
      }
    ],
    "invalid_tool_calls": []
  }
]

<<< NOW: THE RESULT IS THE REDUCE CANNOT MATCH THE HUMAN MESSAGE CORRECTLY SO IT APPENDS IT >>>
Reduce returning:
[
  HumanMessage {
    "content": "What is the weather in LA?",
    "additional_kwargs": {},
    "response_metadata": {}
  },
  AIMessage {
    "id": "chatcmpl-ATcjMMghWhQKawZFYFZ8v149AkRKI",
    "content": "",
    "additional_kwargs": {
      "tool_calls": [
        {
          "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N",
          "type": "function",
          "function": "[Object]"
        }
      ]
    },
    "response_metadata": {
      "tokenUsage": {
        "promptTokens": 145,
        "completionTokens": 22,
        "totalTokens": 167
      },
      "finish_reason": "tool_calls"
    },
    "tool_calls": [
      {
        "name": "tavily_search_results_json",
        "args": {
          "query": "current weather in Louisiana"
        },
        "type": "tool_call",
        "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N"
      }
    ],
    "invalid_tool_calls": []
  },
  HumanMessage {
    "id": "5c95aa4a-23c0-40b8-b814-6ccd97137b70",
    "content": "What is the weather in LA?",
    "additional_kwargs": {},
    "response_metadata": {}
  }
]
...
Originally created by @nigel-daniels on GitHub (Nov 14, 2024). I'm attempting to follow the LangGraph course on [DeepLearning.ai](http://deeplearning.ai/), AI Agents in LangGraph. I've been following along converting the Python examples into the equivalent JS code to help me understand the LangGraph.JS codebase. I'm hitting a problem when attempting to modify state using the update state. As per the exercise I have implemented a custom reducer and that seems to be functioning as expected, however calling `graph.getState(config)` I get a different set of messages in the state in which the first `HumanMessage` does not have an `id` as returned by the reducer. If I understand correctly this state should look exactly like the one the custom reducer had returned. Below is the code I am running and the `package.json` file so you can see the versions and libraries in use. ```javascript import { StateGraph, Annotation, END } from "@langchain/langgraph"; import { ChatOpenAI } from '@langchain/openai'; import { MessageUnion, SystemMessage, HumanMessage, AIMessage } from '@langchain/core/messages'; import { ToolMessage } from '@langchain/core/messages/tool' import { SqliteSaver } from '@langchain/langgraph-checkpoint-sqlite'; import { TavilySearchResults } from '@langchain/community/tools/tavily_search'; import { v4 as uuid4 } from 'uuid'; import confirm from '@inquirer/confirm'; // Here we set up the parts we need for the agent // First define the memory for persistance const memory = SqliteSaver.fromConnString(':memory:'); // Set up the tools const tools = [new TavilySearchResults({maxResults: 2})]; // First we define the state (with a custom reducer) const AgentState = Annotation.Root({ messages: Annotation<BaseMessage[]>({ reducer: (current, update) => reduceMessages(current, update), // See below for function default: () => [] }) }); // Now the system prompt const system = `You are a smart research assistant. Use the search engine to look up information. You are allowed to make multiple calls (either together or in sequence). Only look up information when you are sure of what you want. If you need to look up some information before asking a follow up question, you are allowed to do that!`; // Now the model we will use const model = new ChatOpenAI({ model: 'gpt-3.5-turbo', }).bindTools(tools); // Now we can construct our agent graph const graph = new StateGraph(AgentState) .addNode('llm', callOpenAi) // See below for function .addNode('action', takeAction) // See below for function .addConditionalEdges( 'llm', existsAction, // See below for function {true: 'action', false: END} ) .addEdge('action','llm') .setEntryPoint('llm') .compile({ checkpointer: memory, interruptBefore: ['action'] }); const messages = [new HumanMessage('What is the weather in LA?')]; const config = { configurable: {thread_id: '1'} }; for await (const event of await graph.stream({messages: messages}, config)) { for (const [node, values] of Object.entries(event)) { console.log('\nStreaming:') console.log(values.messages); } } let currentValues = await graph.getState(config); // This is the current state console.log('\nCurrent state after streaming until interrupt:'); console.log(currentValues.values); // HERE WE GET A DIFFERENT STATE! // The state returned at this point now hase a human message without an id! // Store the tool_call ID const _id = currentValues.values.messages[currentValues.values.messages.length-1].tool_calls[0].id; // Now let's replace the tool call with our own, updating the query but preserving the ID currentValues.values.messages[currentValues.values.messages.length-1].tool_calls = [{ name: 'tavily_search_results_json', args: { query: 'current weather in Louisiana' }, type: 'tool_call', id: _id }]; await graph.updateState(config, currentValues.values); let newValues = await graph.getState(config); console.log('\nThe new state after running the update:'); console.log(newValues); /* for await (const event of await graph.stream(null, config)) { for (const [node, values] of Object.entries(event)) { console.log(values.messages); } } */ ///////// FUNCTIONS USED BY THE GRAPH ////////// // This is the function for the conditional edge function existsAction(state) { const result = state.messages[state.messages.length-1]; return result.tool_calls ? result.tool_calls.length > 0 : false; } // This is the function for the llm node async function callOpenAi(state) { let messages = state.messages; if (system) { messages = [new SystemMessage(system), ...messages]; } const message = await model.invoke(messages); return {messages: [message]}; } // This is the function for the action node async function takeAction(state) { const results = []; const toolCalls = state.messages[state.messages.length-1].tool_calls; let result = null; for(const t of toolCalls) { //console.log('Calling: ' + JSON.stringify(t)); if (tools.some(tool => tool.name === t.name)) { const tool = tools.find(tool => tool.name === t.name); result = await tool.invoke(t.args); } else { //console.log('\n...bad tool name...'); result = 'bad tool name, retry'; } results.push(new ToolMessage({tool_call_id: t.id, name: t.name, content: result.toString()})); } //console.log('Back to the model!'); return {messages: results}; } // This function is used by the graph state to update the messages list // This lets us update previous messages based on id (or we add them) function reduceMessages(current: MessageUnion[], update: MessageUnion[]):MessaggeUnion[] { // Ensure any new messages have an id for (const newMessage of update) { if (newMessage.id === null || newMessage.id === undefined) { newMessage.id = uuid4(); } } console.log('\nReducer update array:'); console.log(update); // Copy the current set of messages const merged = [...current]; // Now check to see if the new message exists in the curren set of messages // if it does we update it if it does not we append it to the end ofg the array for (const newMessage of update) { const i = merged.findIndex(existingMessage => existingMessage.id === newMessage.id); i !== -1 ? merged[i] = newMessage : merged.push(newMessage); } // Now return the fully merged messages console.log('\nReducer returning:'); console.log(merged); return merged; } ``` Here is the `package.json` ```json { "name": "agents-langgraph", "version": "1.0.0", "main": "index.js", "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "description": "", "dependencies": { "@langchain/community": "^0.3.8", "@langchain/langgraph": "^0.2.20", "@langchain/langgraph-checkpoint-sqlite": "^0.1.3", "@tavily/core": "^0.0.2", "langchain": "^0.3.5", "openai": "^4.68.2", "tsx": "^4.19.1", "uuid": "^11.0.3" } } ``` Note this code also expects that you have `OPENAI_API_KEY` and `TAVILY_API_KEY` exported in your shells `env`. Here is the output I get: ``` Reducer update array: [ HumanMessage { "id": "7f929582-8d9a-4dc8-af37-dd1be7e2cd76", "content": "What is the weather in LA?", "additional_kwargs": {}, "response_metadata": {} } ] Reduce returning: [ HumanMessage { "id": "7f929582-8d9a-4dc8-af37-dd1be7e2cd76", "content": "What is the weather in LA?", "additional_kwargs": {}, "response_metadata": {} } ] Reducer update array: [ AIMessage { "id": "chatcmpl-ATcjMMghWhQKawZFYFZ8v149AkRKI", "content": "", "additional_kwargs": { "tool_calls": [ { "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N", "type": "function", "function": "[Object]" } ] }, "response_metadata": { "tokenUsage": { "promptTokens": 145, "completionTokens": 22, "totalTokens": 167 }, "finish_reason": "tool_calls" }, "tool_calls": [ { "name": "tavily_search_results_json", "args": { "input": "current weather in Los Angeles" }, "type": "tool_call", "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N" } ], "invalid_tool_calls": [], "usage_metadata": { "output_tokens": 22, "input_tokens": 145, "total_tokens": 167, "input_token_details": { "audio": 0, "cache_read": 0 }, "output_token_details": { "audio": 0, "reasoning": 0 } } } ] Reduce returning: [ HumanMessage { "id": "7f929582-8d9a-4dc8-af37-dd1be7e2cd76", "content": "What is the weather in LA?", "additional_kwargs": {}, "response_metadata": {} }, AIMessage { "id": "chatcmpl-ATcjMMghWhQKawZFYFZ8v149AkRKI", "content": "", "additional_kwargs": { "tool_calls": [ { "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N", "type": "function", "function": "[Object]" } ] }, "response_metadata": { "tokenUsage": { "promptTokens": 145, "completionTokens": 22, "totalTokens": 167 }, "finish_reason": "tool_calls" }, "tool_calls": [ { "name": "tavily_search_results_json", "args": { "input": "current weather in Los Angeles" }, "type": "tool_call", "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N" } ], "invalid_tool_calls": [], "usage_metadata": { "output_tokens": 22, "input_tokens": 145, "total_tokens": 167, "input_token_details": { "audio": 0, "cache_read": 0 }, "output_token_details": { "audio": 0, "reasoning": 0 } } } ] Reducer update array: [ AIMessage { "id": "chatcmpl-ATcjMMghWhQKawZFYFZ8v149AkRKI", "content": "", "additional_kwargs": { "tool_calls": [ { "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N", "type": "function", "function": "[Object]" } ] }, "response_metadata": { "tokenUsage": { "promptTokens": 145, "completionTokens": 22, "totalTokens": 167 }, "finish_reason": "tool_calls" }, "tool_calls": [ { "name": "tavily_search_results_json", "args": { "input": "current weather in Los Angeles" }, "type": "tool_call", "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N" } ], "invalid_tool_calls": [], "usage_metadata": { "output_tokens": 22, "input_tokens": 145, "total_tokens": 167, "input_token_details": { "audio": 0, "cache_read": 0 }, "output_token_details": { "audio": 0, "reasoning": 0 } } } ] Reduce returning: [ HumanMessage { "id": "7f929582-8d9a-4dc8-af37-dd1be7e2cd76", "content": "What is the weather in LA?", "additional_kwargs": {}, "response_metadata": {} }, AIMessage { "id": "chatcmpl-ATcjMMghWhQKawZFYFZ8v149AkRKI", "content": "", "additional_kwargs": { "tool_calls": [ { "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N", "type": "function", "function": "[Object]" } ] }, "response_metadata": { "tokenUsage": { "promptTokens": 145, "completionTokens": 22, "totalTokens": 167 }, "finish_reason": "tool_calls" }, "tool_calls": [ { "name": "tavily_search_results_json", "args": { "input": "current weather in Los Angeles" }, "type": "tool_call", "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N" } ], "invalid_tool_calls": [], "usage_metadata": { "output_tokens": 22, "input_tokens": 145, "total_tokens": 167, "input_token_details": { "audio": 0, "cache_read": 0 }, "output_token_details": { "audio": 0, "reasoning": 0 } } } ] Streaming: [ AIMessage { "id": "chatcmpl-ATcjMMghWhQKawZFYFZ8v149AkRKI", "content": "", "additional_kwargs": { "tool_calls": [ { "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N", "type": "function", "function": "[Object]" } ] }, "response_metadata": { "tokenUsage": { "promptTokens": 145, "completionTokens": 22, "totalTokens": 167 }, "finish_reason": "tool_calls" }, "tool_calls": [ { "name": "tavily_search_results_json", "args": { "input": "current weather in Los Angeles" }, "type": "tool_call", "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N" } ], "invalid_tool_calls": [], "usage_metadata": { "output_tokens": 22, "input_tokens": 145, "total_tokens": 167, "input_token_details": { "audio": 0, "cache_read": 0 }, "output_token_details": { "audio": 0, "reasoning": 0 } } } ] <<< NOTE: AT THIS POINT THE id IN THE HUMAN MESSAGE IS GONE >>> Current state after streaming until interrupt: { messages: [ HumanMessage { "content": "What is the weather in LA?", "additional_kwargs": {}, "response_metadata": {} }, AIMessage { "id": "chatcmpl-ATcjMMghWhQKawZFYFZ8v149AkRKI", "content": "", "additional_kwargs": { "tool_calls": [ { "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N", "type": "function", "function": "[Object]" } ] }, "response_metadata": { "tokenUsage": { "promptTokens": 145, "completionTokens": 22, "totalTokens": 167 }, "finish_reason": "tool_calls" }, "tool_calls": [ { "name": "tavily_search_results_json", "args": { "input": "current weather in Los Angeles" }, "type": "tool_call", "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N" } ], "invalid_tool_calls": [] } ] } <<< NOW: THE REDUCER GETS THE MESSAGES EXPECTED BUT THE INTERNAL STATE IS DIFFERENT?? >>> Reducer update array: [ HumanMessage { "id": "5c95aa4a-23c0-40b8-b814-6ccd97137b70", "content": "What is the weather in LA?", "additional_kwargs": {}, "response_metadata": {} }, AIMessage { "id": "chatcmpl-ATcjMMghWhQKawZFYFZ8v149AkRKI", "content": "", "additional_kwargs": { "tool_calls": [ { "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N", "type": "function", "function": "[Object]" } ] }, "response_metadata": { "tokenUsage": { "promptTokens": 145, "completionTokens": 22, "totalTokens": 167 }, "finish_reason": "tool_calls" }, "tool_calls": [ { "name": "tavily_search_results_json", "args": { "query": "current weather in Louisiana" }, "type": "tool_call", "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N" } ], "invalid_tool_calls": [] } ] <<< NOW: THE RESULT IS THE REDUCE CANNOT MATCH THE HUMAN MESSAGE CORRECTLY SO IT APPENDS IT >>> Reduce returning: [ HumanMessage { "content": "What is the weather in LA?", "additional_kwargs": {}, "response_metadata": {} }, AIMessage { "id": "chatcmpl-ATcjMMghWhQKawZFYFZ8v149AkRKI", "content": "", "additional_kwargs": { "tool_calls": [ { "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N", "type": "function", "function": "[Object]" } ] }, "response_metadata": { "tokenUsage": { "promptTokens": 145, "completionTokens": 22, "totalTokens": 167 }, "finish_reason": "tool_calls" }, "tool_calls": [ { "name": "tavily_search_results_json", "args": { "query": "current weather in Louisiana" }, "type": "tool_call", "id": "call_HmW2oSUscpR3aZHaqgPQ7f9N" } ], "invalid_tool_calls": [] }, HumanMessage { "id": "5c95aa4a-23c0-40b8-b814-6ccd97137b70", "content": "What is the weather in LA?", "additional_kwargs": {}, "response_metadata": {} } ] ... ```
yindo closed this issue 2026-02-15 17:16:07 -05:00
Author
Owner

@jacoblee93 commented on GitHub (Nov 14, 2024):

Hey @nigel-daniels! This is a really pernicious one - you actually have to do this when assigning an id due to serde stuff:

https://github.com/langchain-ai/langgraphjs/blob/main/libs/langgraph/src/graph/message.ts#L36

I think it's related to that. I will see if I can open a PR in core that adds id as a setter so that you don't have to do this.

@jacoblee93 commented on GitHub (Nov 14, 2024): Hey @nigel-daniels! This is a really pernicious one - you actually have to do this when assigning an id due to serde stuff: https://github.com/langchain-ai/langgraphjs/blob/main/libs/langgraph/src/graph/message.ts#L36 I think it's related to that. I will see if I can open a PR in core that adds id as a setter so that you don't have to do this.
Author
Owner

@nigel-daniels commented on GitHub (Nov 14, 2024):

Hi, I'm good with setting the id, that's exactly what happens in the example Harrison Chase gave in the Python code. What surprises me is when I call :

let currentValues = await graph.getState(config);

It does not respond with what the reducer had provided previously.

@nigel-daniels commented on GitHub (Nov 14, 2024): Hi, I'm good with setting the id, that's exactly what happens in the example Harrison Chase gave in the Python code. What surprises me is when I call : ```javascript let currentValues = await graph.getState(config); ``` It does not respond with what the reducer had provided previously.
Author
Owner

@jacoblee93 commented on GitHub (Nov 14, 2024):

Yeah just setting .id isn't enough - you have to set message.lc_kwargs.id at times. That's when I've seen this before, can give this a closer look tomorrow!

@jacoblee93 commented on GitHub (Nov 14, 2024): Yeah just setting `.id` isn't enough - you have to set `message.lc_kwargs.id` at times. That's when I've seen this before, can give this a closer look tomorrow!
Author
Owner

@nigel-daniels commented on GitHub (Nov 15, 2024):

So I made the update suggested an now the state seems to be good, but uncommenting the continuation streaming right after the update now has Tavily returning status 422 (Un-processable entity), which is odd as the tool_calls element before the update was:

"tool_calls": [
        {
          "name": "tavily_search_results_json",
          "args": {
            "input": "current weather in Los Angeles"
          },
          "type": "tool_call",
          "id": "call_zqZr0VfixgrzXgZQw7pLhFWs"
        }
      ],

and afterwards is:

"tool_calls": [
        {
          "name": "tavily_search_results_json",
          "args": {
            "query": "current weather in Louisiana"
          },
          "type": "tool_call",
          "id": "call_zqZr0VfixgrzXgZQw7pLhFWs"
        }
],

Is there something else I should be setting as it looks like only the query text has changed so it should form a valid request? Let me know if this is off topic and should be under a separate issue or if this could be a side effect of the update and id changes.

@nigel-daniels commented on GitHub (Nov 15, 2024): So I made the update suggested an now the state seems to be good, but uncommenting the continuation streaming right after the update now has Tavily returning status 422 (Un-processable entity), which is odd as the `tool_calls` element before the update was: ```json "tool_calls": [ { "name": "tavily_search_results_json", "args": { "input": "current weather in Los Angeles" }, "type": "tool_call", "id": "call_zqZr0VfixgrzXgZQw7pLhFWs" } ], ``` and afterwards is: ```json "tool_calls": [ { "name": "tavily_search_results_json", "args": { "query": "current weather in Louisiana" }, "type": "tool_call", "id": "call_zqZr0VfixgrzXgZQw7pLhFWs" } ], ``` Is there something else I should be setting as it looks like only the query text has changed so it should form a valid request? Let me know if this is off topic and should be under a separate issue or if this could be a side effect of the update and id changes.
Author
Owner

@nigel-daniels commented on GitHub (Nov 15, 2024):

Some further information that makes me think there is an issue in the update. I commented out the update code and added some console logging to the tavily_search.js code just before it makes its fetch call. With the update code removed the request resumes after the interrupt and here is the body that Tavily is getting:

// Tavily Search input: OK Run
{
  query: 'weather in Los Angeles',
  max_results: 2,
  api_key: 'tvly-xxxxxxxxxxxxxxxxxxx'
}

However it is what Tavily is sending after the update has been made even though the tool_calls looks ok in the state.

// Tavily Serach input: Broken run
{
  query: undefined,
  max_results: 2,
  api_key: 'tvly-xxxxxxxxxxxxxxxxxxx'
}
@nigel-daniels commented on GitHub (Nov 15, 2024): Some further information that makes me think there is an issue in the update. I commented out the update code and added some console logging to the `tavily_search.js` code just before it makes its fetch call. With the update code removed the request resumes after the interrupt and here is the body that Tavily is getting: ```javascript // Tavily Search input: OK Run { query: 'weather in Los Angeles', max_results: 2, api_key: 'tvly-xxxxxxxxxxxxxxxxxxx' } ``` However it is what Tavily is sending after the update has been made even though the `tool_calls` looks ok in the state. ```javascript // Tavily Serach input: Broken run { query: undefined, max_results: 2, api_key: 'tvly-xxxxxxxxxxxxxxxxxxx' } ```
Author
Owner

@nigel-daniels commented on GitHub (Nov 15, 2024):

Ok realized my too call changed the input parameter to be query ... my bad

@nigel-daniels commented on GitHub (Nov 15, 2024): Ok realized my too call changed the `input` parameter to be `query` ... my bad
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraphjs#134