Pre-flight Checklist
Describe the Bug
When a LangGraph tool returns a ToolMessage with name=None (which is a valid default — LangChain's ToolMessage has name: Optional[str] = None), the OnToolEnd handler in agent.py correctly falls back to event.get("name", "") when emitting ToolCallStartEvent (line 1243). However, the ToolMessage object itself retains name=None in the LangGraph checkpoint.
This causes two downstream problems:
-
langchain_messages_to_agui converts these messages for the MessagesSnapshotEvent. The AG-UI ToolMessage type expects a non-None name field, and None produces an invalid snapshot or a Pydantic ValidationError depending on the consumer.
-
Command objects — when a tool returns a Command(update={"messages": [ToolMessage(tool_call_id=..., content=..., name=None)]}), the name=None propagates through the same path.
The root cause is that the OnToolEnd handler only compensates for None at the event emission layer (ToolCallStartEvent) but does not fix the underlying ToolMessage object that gets committed to the checkpoint and later fed to langchain_messages_to_agui.
Steps to Reproduce
- Create a LangGraph tool that returns a
ToolMessage without setting name:
from langchain_core.messages import ToolMessage
def my_tool(query: str) -> ToolMessage:
return ToolMessage(content="result", tool_call_id="tc-123")
# name defaults to None
- Run the agent through
ag-ui-langgraph.
- After the tool completes, the
MessagesSnapshotEvent includes a ToolMessage with name=None.
- Depending on the AG-UI client, this causes a validation error or renders incorrectly.
Expected Behavior
The OnToolEnd handler should patch ToolMessage.name on the actual message object (not just on the event), so that downstream consumers (especially langchain_messages_to_agui for MessagesSnapshotEvent) see a valid name.
Suggested fix — patch the ToolMessage before processing:
elif event_type == LangGraphEventTypes.OnToolEnd:
tool_call_output = event["data"]["output"]
tool_name = event.get("name") or "unknown"
# Patch ToolMessage.name when None to avoid downstream validation errors
if isinstance(tool_call_output, ToolMessage) and tool_call_output.name is None:
tool_call_output.name = tool_name
elif isinstance(tool_call_output, Command) and tool_call_output.update:
for msg in tool_call_output.update.get("messages", []):
if isinstance(msg, ToolMessage) and msg.name is None:
msg.name = tool_name
# ... rest of handler
Environment
ag-ui-langgraph: 0.0.35
ag-ui-protocol: 0.1.18
langgraph: 0.4.7
langchain-core: 0.3.59
Python: 3.13
Additional Context
Workaround: override _handle_single_event and patch ToolMessage.name before delegating to the base class.
Happy to send a PR with the fix and a regression test if assigned.
Pre-flight Checklist
Describe the Bug
When a LangGraph tool returns a
ToolMessagewithname=None(which is a valid default — LangChain'sToolMessagehasname: Optional[str] = None), theOnToolEndhandler inagent.pycorrectly falls back toevent.get("name", "")when emittingToolCallStartEvent(line 1243). However, theToolMessageobject itself retainsname=Nonein the LangGraph checkpoint.This causes two downstream problems:
langchain_messages_to_aguiconverts these messages for theMessagesSnapshotEvent. The AG-UIToolMessagetype expects a non-Nonenamefield, andNoneproduces an invalid snapshot or a PydanticValidationErrordepending on the consumer.Commandobjects — when a tool returns aCommand(update={"messages": [ToolMessage(tool_call_id=..., content=..., name=None)]}), thename=Nonepropagates through the same path.The root cause is that the
OnToolEndhandler only compensates forNoneat the event emission layer (ToolCallStartEvent) but does not fix the underlyingToolMessageobject that gets committed to the checkpoint and later fed tolangchain_messages_to_agui.Steps to Reproduce
ToolMessagewithout settingname:ag-ui-langgraph.MessagesSnapshotEventincludes aToolMessagewithname=None.Expected Behavior
The
OnToolEndhandler should patchToolMessage.nameon the actual message object (not just on the event), so that downstream consumers (especiallylangchain_messages_to_aguiforMessagesSnapshotEvent) see a valid name.Suggested fix — patch the
ToolMessagebefore processing:Environment
Additional Context
Workaround: override
_handle_single_eventand patchToolMessage.namebefore delegating to the base class.Happy to send a PR with the fix and a regression test if assigned.