I’m working with a multi-agent setup using the Supervisor Architecture in LangGraph.
In this setup:
-
A supervisor agent orchestrates the workflow
-
It delegates tasks to child agents
-
Multiple internal messages are exchanged between agents before producing a final answer
Problem
When using streaming, I receive a sequence of chunks, but I currently have no clear way to distinguish between:
-
Internal reasoning / intermediate agent communication
-
Final user-facing response generated by the supervisor
This makes it difficult to:
-
Properly render only the final response in the UI
-
Avoid exposing internal agent reasoning/messages
-
Build a clean streaming UX for end users
Expected Behavior
It would be helpful to have a built-in or recommended way to:
Questions
-
Is there a recommended pattern to differentiate these chunks when streaming?
-
Are there metadata fields, message types, or callbacks that can be used for this?
-
Is this something planned for native support in LangGraph?
I am assuming you are building your own Graph for the Supervisor Architecture, rather than using the Sub-Agents from DeepAgent. You can use stream_mode="messages" along with subgraph=True to differentiate between the source of the messages:
from langgraph.graph import StateGraph, START, END
builder = StateGraph(State)
# Name your nodes explicitly
builder.add_node("supervisor", supervisor_fn) # orchestrator
builder.add_node("researcher", researcher_fn) # child agent
builder.add_node("coder", coder_fn) # child agent
SUPERVISOR_NODE = "supervisor"
for chunk in graph.stream(input, stream_mode="messages", subgraphs=True):
namespace, mode, (message, metadata) = chunk
if mode != "messages":
continue
node_name = metadata["langgraph_node"] # exact string from add_node
if node_name == SUPERVISOR_NODE:
stream_to_user(message) # final response
else:
log_internally(message) # internal: researcher, coder, etc.
However, if you are using the SubAgent Supervisor Architecture, then you can do something like:
agent = create_deep_agent(
model="...",
subagents=[...],
name="supervisor", # sets lc_agent_name on main agent chunks
)
for namespace, stream_mode, data in agent.stream(
{"messages": [HumanMessage("Do something")]},
stream_mode="messages",
subgraphs=True, # needed to see subagent chunks at all
config={...},
):
if stream_mode != "messages":
continue
message_chunk, metadata = data
# Option A: namespace check (simplest, matches CLI behavior)
is_main_agent = namespace == ()
# Option B: lc_agent_name (semantic, survives refactors)
is_main_agent = metadata.get("lc_agent_name") == "supervisor"
# Option C: skip summarization regardless
is_summarization = metadata.get("lc_source") == "summarization"
if is_main_agent and not is_summarization:
stream_to_user(message_chunk) # final, user-facing
else:
log_internally(message_chunk) # internal subagent work
Document Reference: