Hard to Reliably Separate Final Answer Tokens from Intermediate Reasoning in DeepAgents REST Streaming #3276
Replies: 2 comments
|
The cleanest way to separate final-answer tokens from intermediate reasoning is to use the In a typical LangGraph agent, intermediate tool calls and reasoning happen in nodes like async for event in client.runs.stream(
thread_id=thread_id,
assistant_id=assistant_id,
input=input,
stream_mode=["messages", "updates"],
):
if event.event == "updates":
# intermediate node updates, tool calls, etc.
pass
elif event.event == "messages/delta":
node = event.metadata.get("langgraph_node", "")
if node == "respond": # replace with your final-output node name
# this is the user-facing delta
yield event.dataIf your graph does not have a dedicated respond node, you can add one whose only job is to format and emit the final answer. That makes the separation clean and reliable regardless of how many intermediate reasoning steps happen before it. |
|
The cleanest separation is to filter by the LangGraph node name in the Here is the pattern: async for chunk in agent.astream({"messages": messages}, stream_mode="updates"):
for node_name, update in chunk.items():
if node_name == "tools":
# intermediate: tool progress, reasoning steps
for msg in update.get("messages", []):
emit_reasoning_delta(msg.content)
elif node_name in ("agent", "__end__"):
# check if this is the final human-facing message
for msg in update.get("messages", []):
if hasattr(msg, "content") and not getattr(msg, "tool_calls", None):
emit_message_delta(msg.content)The key distinction: an If you want true token-level streaming for the final answer, switch to async for msg, metadata in agent.astream({"messages": messages}, stream_mode="messages"):
if metadata.get("langgraph_node") == "agent" and not getattr(msg, "tool_calls", None):
emit_message_delta(msg.content)The |
Uh oh!
There was an error while loading. Please reload this page.
We are using DeepAgents through the LangGraph REST streaming API.
Current stream modes include:
messages
custom
updates
Our goal is to display two separate streams in the frontend:
reasoning_delta: intermediate process / tool progress / agent workflow
message_delta: final user-facing answer only
However, with the current streaming structure, messages contains both intermediate agent output and final answer output.
For example, during one run, the main model node produced intermediate text such as:
The latest hourly data is 2026-05-09 15:00. Querying today’s real-time data.
Now fetching meteorological data.
These are not final answer content. They are process messages before tool calls.
But they arrive as normal messages chunks from:
langgraph_node = model
type = AIMessageChunk
content != empty
At the moment those tokens arrive, they do not yet contain tool_call_chunks. Later chunks from the same model run end with:
finish_reason = tool_calls
So the final classification is only obvious after the model run finishes.
The actual final answer comes from a later model run with:
langgraph_node = model
finish_reason = stop
tool_calls = []
tool_call_chunks = []
This creates a problem for token-level streaming:
If we immediately forward all model text chunks, intermediate reasoning leaks into the final answer.
If we buffer until finish_reason is known, final answer streaming is delayed.
We also observed other noisy or unsafe streams:
PermissionMiddleware.before_agent
This outputs permission-analysis text and should not be shown as final answer.
ExpandQuestionMiddleware.after_agent
This outputs follow-up question generation tokens and should not be treated as final answer.
langgraph_node = tools
This may contain raw tool results, JSON payloads, skill files, prompts, or internal artifacts, so it should not be shown directly.
The current reliable backend filtering rule appears to be:
Only treat messages as final answer if:
But the last condition requires buffering by message.id / chunk.id.
We also considered adding a DeepAgents subagent for final-answer generation. However, in DeepAgents a subagent is invoked like a tool, and its result comes back to the
main agent as a ToolMessage. The final output still returns through the main agent unless we add a separate explicit output mechanism. Therefore, simply adding a
subagent does not fully solve the stream separation problem.
The cleanest design we see is:
Use DeepAgents for planning, skill selection, tool calls, and analysis.
Emit intermediate workflow events as reasoning_delta.
Expose the final answer through an explicit custom event, for example custom.final_message_delta.
Only persist and display custom.final_message_delta as the final answer.
Question for the community:
In DeepAgents + LangGraph REST streaming, is there a recommended way to reliably identify the final user-facing answer stream separately from intermediate model text,
tool-call planning text, middleware output, and subagent/tool messages, while still preserving token-level streaming?
All reactions