Hi @santiagoahl
what is the definition of state in your graph? And what is the graph itself? Could you share as many code snippets as possible?
The chat history is preserved by your graph state and reducers (for example, using add_messages) or by a checkpointer. If your state replaces the message list with only the ToolMessage output, the next LLM call will start with a tool role message and OpenAI will reject it with the 400 error you saw.
1) Am I using it wrongly?
Likely yes - it seems that your state is not accumulating messages. ToolNode expects a state containing a list of messages (default key is "messages") where the last message is an AIMessage with tool_calls. It runs the tools and emits ToolMessage(s). If your state reducer overwrote messages with those ToolMessage(s) (instead of appending), the next model call begins with a tool message and violates OpenAI’s rule that tool outputs must respond to a preceding assistant message with tool_calls.
A minimal, correct state shape uses a message-accumulating reducer:
from typing import Annotated, List
from langchain_core.messages import BaseMessage
from langgraph.graph import StateGraph
from langgraph.graph.message import add_messages
class State(TypedDict):
messages: Annotated[List[BaseMessage], add_messages]
Make sure your graph uses the same key that ToolNode reads/writes (default messages, configurable via messages_key).
2) Is this a common error?
Yes. It happens whenever the state loses the preceding assistant message with tool_calls and only forwards the tool output to the next LLM invocation. OpenAI enforces: a message with role tool must be a response to a preceding message with tool_calls. If the history isn’t accumulated or persisted (e.g., no checkpointer/threaded memory), you’ll see this error.
3) When to use ToolNode and best practices
- When to use: In agent graphs where the model decides to call tools; route to
ToolNodeonly when the lastAIMessagehastool_calls(usetools_condition).ToolNodeexecutes tools (in parallel if multiple), returningToolMessage(s) to be added to your state. - Best practices:
- Use a message-accumulating reducer for state (
add_messages) so history is appended, not replaced. - Keep the key consistent with
ToolNode(messages_key=...)if you customize it. - Persist conversation across turns with a checkpointer (e.g.,
MemorySaver) and athread_id. - Use
tools_conditionto branch only when tool calls exist. - Configure error handling with
handle_tool_errorsto return aToolMessageon exceptions instead of crashing the graph. - If tools need graph context or a store, use injected arguments (e.g.,
InjectedState,InjectedStore).
- Use a message-accumulating reducer for state (
Why the error appears
ToolNodeemitsToolMessage(s). If your state reducer replaced the message list with just thoseToolMessage(s), the next LLM call starts with atoolmessage. OpenAI requires a precedingassistantmessage withtool_callsthat those tool outputs are replying to. Without it, the API returns:
Invalid parameter: messages with role ‘tool’ must be a response to a preceding message with ‘tool_calls’.
Fix by accumulating messages (append, don’t replace) and/or persisting the thread state.
References (official docs and source)
ToolNodeAPI and behavior (executes tools from the lastAIMessage, supportsmessages_key,handle_tool_errors, parallel execution): Redirecting...- How to call tools and handle errors with
ToolNode: Redirecting... - Managing conversation history and persistence (checkpointers,
thread_id): Redirecting... - Message state and reducers (
add_messagesfor accumulation): https://langchain-ai.github.io/langgraph/concepts/state/#a-message-state (see theadd_messagesreducer) - Source:
ToolNodeimplementation: https://github.com/langchain-ai/langgraph/blob/main/langgraph/prebuilt/tool_node.py - Source: Message reducer
add_messages: https://github.com/langchain-ai/langgraph/blob/main/langgraph/graph/message.py - Source: Prebuilt ReAct agent wiring (for comparison): https://github.com/langchain-ai/langgraph/blob/main/langgraph/prebuilt/react.py
- OpenAI tools rule (tool outputs must reply to an assistant message with
tool_calls): https://platform.openai.com/docs/guides/tools