Distinguishing internal vs final streamed chunks in Supervisor multi-agent architecture

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:

  1. Internal reasoning / intermediate agent communication

  2. 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:

  • Tag or identify chunks as:

    • internal (agent-to-agent communication)

    • final (user-facing output)

  • Or otherwise distinguish:

    • Supervisor final response vs intermediate agent outputs

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: