Home / Articles

Streaming LangChain Deep Agents: Live Plans, Tool Calls and Tokens

Replace a blocking invoke with a stream so users see a deep agent work: the three stream modes, namespaces for subagents, and filtering summarization tokens.

Publisher: REACTAPP.TOP10 min read2188 words

This article is published in English.

Sources: LangChain Deep Agents: Your AI Agent Is Not Frozen. It Just Cannot Tell You It Is Working. II

Few things erode trust faster than a frozen screen. When a user can't tell whether an agent is thinking, stuck or already dead, they assume the worst. Streaming replaces that silence with a live pulse: the plan, the tool calls and the words show up as they happen, and people can see that something is going on.

Why invoke makes a deep agent look dead

Try running a deep agent on a real task while someone watches. The call goes through invoke, then the display goes still for forty seconds. Inside, the agent is reading files, sketching a plan, running a tool and reasoning it through. The user only sees a motionless box and a spinner, which could mean hard work or a silent crash. Since the two look identical, people reload the page and file a ticket saying it hangs.

Nothing was hung. The agent simply had no channel for narrating itself. invoke blocks by design. It runs the whole loop out of sight and returns a single finished answer, which is exactly right for a script or a cron job that nobody is watching. It's the wrong choice once a human sits on the other end.

The cure leaves the loop alone. It swaps the blocking call for a stream so that plans, tool calls and tokens appear live.

From invoke to stream

Streaming is closely related to invoke. Rather than waiting around for a final state, you loop over agent.stream(...) and a chunk arrives whenever the loop does something. An async twin, agent.astream(...), has the same shape and is what a web server would call.

The mental model is short. The durable LangGraph underneath a deep agent emits an event at every step, and streaming passes those events along as they fire. It isn't a different agent or a different loop, just a window onto the one you already know.

One detail has to be settled first, because every example leans on it: the v2 stream format, enabled with version="v2". Each chunk is a small dictionary with three keys:

  • type says what kind of event it is.
  • ns, the namespace, says who produced it.
  • data holds the payload.

Those three keys stay the same whatever you stream, which keeps the code readable instead of a tangle of tuple unpacking.

Three modes, three altitudes

A stream isn't a single firehose. The stream_mode setting picks the kind of events you receive, and the three common choices line up with three levels of detail.

updates is the step view. One event arrives each time a graph node finishes, which in practice means the model took a turn or a tool ran. It's the level for a progress log, enough to show the agent moving through its work without burying anyone in tokens, and a sensible default for a tidy "here's what the agent is doing" feed.

messages is the token view. The model's output arrives piece by piece as it's generated, the same drip you see when a chat assistant types. Use it when a person reads the agent's prose as it appears. Streaming tool calls land here too, so you can announce "calling read_file" the moment the model commits to the call and not after it completes.

values is the snapshot view. After each step you receive the entire agent state, not just what changed. It's heavier and needed less often, but it's the right pick when the whole picture matters at every point, say to render a live view of the agent's todo list or working files.

Passing a list of modes returns several at once, which is how polished UIs usually work: updates drives a step indicator while messages feeds the visible text. The coarse end looks like this, a clean progress log from updates:

for chunk in agent.stream(
    {”messages”: [{”role”: “user”, “content”: “Summarize the repo’s failing tests”}]},
    stream_mode=”updates”,
    version=”v2”,
):
    if chunk[”type”] == “updates”:
        for node_name in chunk[”data”]:
            print(f”step: {node_name}”)

What prints is a list of graph steps appearing as each one completes, a line for every model turn and every tool run. A screen that looked frozen now visibly advances. Setting stream_mode to "messages" instead would hand you the text itself, ready to print while the agent writes.

Telling the main agent from its subagents

A deep agent seldom works alone for long. As soon as it hands a subtask to a subagent, two or more agents are producing output at once. Send it all to a single stream and the result is a jumble: part of a sentence from the coordinator, a few tokens from a researcher, a tool call from who knows where. The ns field fixes it. Every chunk carries one when you turn on subgraph streaming with subgraphs=True.

The rule is easy to apply. If the namespace is empty, the main agent emitted the event. If it contains a tools: segment, a subagent did, and the segment names which one. Output can then be sorted by origin: coordinator text here, each subagent's work in its own lane.

for chunk in agent.stream(
    {”messages”: [{”role”: “user”, “content”: “Research and summarize quantum computing”}]},
    stream_mode=”messages”,
    subgraphs=True,  # ①
    version=”v2”,
):
    if chunk[”type”] == “messages”:  # ②
        token, metadata = chunk[”data”]  # ③
        is_subagent = any(s.startswith(”tools:”) for s in chunk[”ns”])  # ④
        source = “subagent” if is_subagent else “main”  # ⑤
        if token.content:
            print(f”[{source}] {token.content}”, end=”“, flush=True)  # ⑥

Six details matter in that snippet. First, subgraphs=True is what gives ns its meaning, since without it subagent events carry no distinguishing namespace. Second, the branch keeps only messages events because this loop routes tokens and not steps. Third, each chunk's payload unpacks into a token plus its metadata. Fourth, the namespace test does the real work: a tools: segment anywhere in ns marks the token as coming from a subagent. Fifth, the resulting boolean becomes a readable source label. Sixth, printing with that label as a prefix turns one scrambled stream into two attributed lanes.

Every token is labeled with its producer before printing, so a coordinator handing off to a researcher shows up as two named streams. A full web interface is a larger job, but the namespace on each chunk is the same signal a React front end would read.

The stray tokens that aren't the agent talking

This one surprises people on their first long streamed run. To manage its own context, the harness condenses older messages once the conversation approaches the model's limit. The condensing step is a model call too, so it emits tokens, and they flow into the messages stream next to the agent's real answer. If that stream goes straight to a user, they'll see the agent suddenly reading out a recap of its own history, and it looks like a bug.

Luckily the tokens are tagged. Each messages event has metadata, and summarization tokens carry an lc_source value of "summarization". Skip those and keep the rest.

for chunk in agent.stream(
    {”messages”: [{”role”: “user”, “content”: “...”}]},
    stream_mode=”messages”,
    version=”v2”,
):
    token, metadata = chunk[”data”]
    if metadata.get(”lc_source”) == “summarization”:
        continue
    # ... handle real agent tokens here

One check on the metadata is enough to tell the agent speaking to the user apart from the harness speaking to itself. Only the former gets displayed. It's minor work with a noticeable payoff in polish.

Watching a run versus keeping a conversation

Streaming and conversation often get mixed up, so a line should be drawn. Everything above concerns observing one run as it unfolds. Each stream call, like each invoke call, is one-shot by default. The agent does the task, finishes and forgets, so a follow-up in a fresh call starts with no memory of the earlier exchange.

Holding a conversation open across turns, so that "fix the other test too" works because the agent recalls the first fix, is a separate mechanism. It relies on threads and a checkpointer, the persistence layer where the durable graph writes its state. The distinction to keep: streaming gives visibility inside a run, and threads give continuity between runs. They're independent, and you'll frequently want both.

A small terminal UI that shows the work

Next, assemble everything into a console you could actually run. It should print a line per step as the agent advances and stream the agent's text as it's written, so it asks for two modes together and branches on the chunk type.

from deepagents import create_deep_agent

agent = create_deep_agent(
    model="anthropic:claude-sonnet-4-6",
    system_prompt="You are a code-maintenance assistant for a small Python repo.",
)  # ①
prompt = "List the files you'd look at first to find a failing test, and explain why."
for chunk in agent.stream(
    {"messages": [{"role": "user", "content": prompt}]},
    stream_mode=["updates", "messages"],  # ②
    version="v2",
):
    if chunk["type"] == "updates":  # ③
        for node_name in chunk["data"]:
            print(f"\n[step] {node_name}")
    elif chunk["type"] == "messages":  # ④
        token, metadata = chunk["data"]
        if metadata.get("lc_source") == "summarization":  # ⑤
            continue
        if token.content:
            print(token.content, end="", flush=True)  # ⑥

The code is short but each part has a job. Construction is unchanged, since create_deep_agent builds the agent exactly as before. Two modes requested in one call mean a single loop can feed both a step indicator and a text stream. When a chunk is an updates event, the loop prints a [step] line for the node that just completed. When it's a messages event, the loop prints the generated prose. The summarization check from the previous section sits inside that branch so the harness's housekeeping never reaches the terminal, and empty tokens are skipped before printing.

The result is a [step] line each time the agent moves forward, interleaved with its prose arriving a character at a time, with the filter already in place so nothing leaks. It isn't pretty, but it's honest. A user can see the agent is alive and can watch it think.

Newer Deep Agents releases ship a higher-level API for this, agent.stream_events(...) on version="v3", with typed projections like stream.subagents. The docs steer new applications toward it, because each projection is its own tidy iterator and there's no need to branch on stream_mode or decode namespaces yourself. It's still worth learning the stream_mode route first, since the projections sit on top of these same events. Check the current docs before committing to a big interface.

A checklist to apply today

  • Start by hunting down each invoke that has a person waiting on it, and switch those to agent.stream(...) in updates mode. A rough step log is still better than a blank screen.
  • Look at one real chunk. Run with version="v2", print it, and read the type, ns and data fields so the shape is concrete.
  • Before real users see output, drop any messages chunk whose metadata says lc_source == "summarization".
  • If the agent hands work to subagents, set subgraphs=True and sort the output by namespace, one lane per agent.
  • Keep a small console like the one above in the repo. It lets you watch each new capability run instead of guessing from the last message.

What changed and what didn't

Net result: a blocking call turned into a live feed. stream_mode sets how much detail you get, ns says who produced each event, the summarization filter removes the harness's own chatter, and it should be clear now that observing a run and remembering a run are different problems.

Nothing about the agent or its loop changed. The only addition was a window, and that window marks the line between a tool people trust and one they refresh in a panic. When an agent shows its work, users believe it's working.

So the next time someone reports that the agent "hangs," skip the profiler and try a stream first. The agent was talking the whole time. It needed a channel.