Skip to main content
The useStream React hook provides built-in support for deep agent streaming. It automatically tracks subagent lifecycles, separates subagent messages from the main conversation, and exposes a rich API for building multi-agent UIs. Key features for deep agents:
  • Subagent tracking — Automatic lifecycle management for each subagent (pending, running, complete, error)
  • Message filtering — Separate subagent messages from the main conversation stream
  • Tool call visibility — Access tool calls and results from within subagent execution
  • State reconstruction — Restore subagent state from thread history on page reload

Installation

Install the LangGraph SDK to use the useStream hook in your React application:
npm install @langchain/langgraph-sdk

Basic usage

To stream from a deep agent with subagents, configure useStream with filterSubagentMessages and pass streamSubgraphs: true when submitting:
import { useStream } from "@langchain/langgraph-sdk/react";
import type { agent } from "./agent";

function DeepAgentChat() {
  const stream = useStream<typeof agent>({
    assistantId: "deep-agent",
    apiUrl: "http://localhost:2024",
    filterSubagentMessages: true,  // Keep subagent messages separate
  });

  const handleSubmit = (message: string) => {
    stream.submit(
      { messages: [{ content: message, type: "human" }] },
      { streamSubgraphs: true }  // Enable subagent streaming
    );
  };

  return (
    <div>
      {/* Main conversation messages (subagent messages filtered out) */}
      {stream.messages.map((message, idx) => (
        <div key={message.id ?? idx}>
          {message.type}: {message.content}
        </div>
      ))}

      {/* Subagent progress */}
      {stream.activeSubagents.length > 0 && (
        <div>
          <h3>Active subagents:</h3>
          {stream.activeSubagents.map((subagent) => (
            <SubagentCard key={subagent.id} subagent={subagent} />
          ))}
        </div>
      )}

      {stream.isLoading && <div>Loading...</div>}
    </div>
  );
}
Learn how to deploy your deep agents to LangSmith for production-ready hosting with built-in observability, authentication, and scaling.
In addition to the standard useStream parameters, deep agent streaming supports:
filterSubagentMessages
boolean
default:"false"
When true, subagent messages are excluded from the main stream.messages array. Access them instead via stream.subagents.get(id).messages. This keeps the main conversation clean.
subagentToolNames
string[]
default:"['task']"
The tool names that spawn subagents. By default, deep agents use the task tool to delegate work to subagents. Only change this if you’ve customized the tool name.
In addition to the standard return values, deep agent streaming provides:
subagents
Map<string, SubagentStream>
A map of all subagents, keyed by tool call ID. Each subagent includes its messages, status, tool calls, and result.
activeSubagents
SubagentStream[]
An array of currently running subagents (status is "pending" or "running").
getSubagent
(toolCallId: string) => SubagentStream | undefined
Get a specific subagent by its tool call ID.
getSubagentsByMessage
(messageId: string) => SubagentStream[]
Get all subagents triggered by a specific AI message. Useful for associating subagents with the message that spawned them.
getSubagentsByType
(type: string) => SubagentStream[]
Filter subagents by their subagent_type (e.g., "researcher", "writer").

Subagent stream interface

Each subagent in the stream.subagents map exposes a stream-like interface:
interface SubagentStream {
  // Identity
  id: string;                    // Tool call ID
  toolCall: {                    // Original task tool call
    subagent_type: string;
    description: string;
  };

  // Lifecycle
  status: "pending" | "running" | "complete" | "error";
  startedAt: Date | null;
  completedAt: Date | null;
  isLoading: boolean;

  // Content
  messages: Message[];           // Subagent's messages
  values: Record<string, any>;   // Subagent's state
  result: string | null;         // Final result
  error: string | null;          // Error message

  // Tool calls
  toolCalls: ToolCallWithResult[];
  getToolCalls: (message: Message) => ToolCallWithResult[];

  // Hierarchy
  depth: number;                 // Nesting depth (0 for top-level subagents)
  parentId: string | null;       // Parent subagent ID (for nested subagents)
}

Rendering subagent streams

Subagent cards

Build cards that show each subagent’s streaming content, status, and progress:
import { AIMessage } from "langchain";
import { useStream, type SubagentStream } from "@langchain/langgraph-sdk/react";
import type { Message } from "@langchain/langgraph-sdk";
import type { agent } from "./agent";

function SubagentCard({ subagent }: { subagent: SubagentStream<typeof agent> }) {
  const content = getStreamingContent(subagent.messages);

  return (
    <div className="border rounded-lg p-4">
      {/* Header */}
      <div className="flex items-center gap-2 mb-2">
        <StatusIcon status={subagent.status} />
        <span className="font-medium">{subagent.toolCall.subagent_type}</span>
        <span className="text-sm text-gray-500">
          {subagent.toolCall.description}
        </span>
      </div>

      {/* Streaming content */}
      {content && (
        <div className="prose text-sm mt-2">
          {content}
        </div>
      )}

      {/* Result */}
      {subagent.status === "complete" && subagent.result && (
        <div className="mt-2 p-2 bg-green-50 rounded text-sm">
          {subagent.result}
        </div>
      )}

      {/* Error */}
      {subagent.status === "error" && subagent.error && (
        <div className="mt-2 p-2 bg-red-50 rounded text-sm text-red-700">
          {subagent.error}
        </div>
      )}
    </div>
  );
}

function StatusIcon({ status }: { status: string }) {
  switch (status) {
    case "pending":
      return <span className="text-gray-400"></span>;
    case "running":
      return <span className="animate-spin">⚙️</span>;
    case "complete":
      return <span className="text-green-500"></span>;
    case "error":
      return <span className="text-red-500"></span>;
    default:
      return null;
  }
}

/** Extract text content from subagent messages */
function getStreamingContent(messages: Message[]): string {
  return messages
    .filter((m) => m.type === "ai")
    .map((m) => {
      if (typeof m.content === "string") return m.content;
      if (Array.isArray(m.content)) {
        return m.content
          .filter((c): c is { type: "text"; text: string } =>
            c.type === "text" && "text" in c
          )
          .map((c) => c.text)
          .join("");
      }
      return "";
    })
    .join("");
}

Map subagents to messages

Use getSubagentsByMessage to associate subagent cards with the AI message that triggered them:
import { useMemo } from "react";
import { useStream } from "@langchain/langgraph-sdk/react";
import type { agent } from "./agent";

function DeepAgentChat() {
  const stream = useStream<typeof agent>({
    assistantId: "deep-agent",
    apiUrl: "http://localhost:2024",
    filterSubagentMessages: true,
  });

  // Map subagents to the human message that triggered them
  const subagentsByMessage = useMemo(() => {
    const result = new Map();
    const messages = stream.messages;

    for (let i = 0; i < messages.length; i++) {
      if (messages[i].type !== "human") continue;

      // The next message should be the AI message with task tool calls
      const next = messages[i + 1];
      if (!next || next.type !== "ai" || !next.id) continue;

      const subagents = stream.getSubagentsByMessage(next.id);
      if (subagents.length > 0) {
        result.set(messages[i].id, subagents);
      }
    }
    return result;
  }, [stream.messages, stream.subagents]);

  return (
    <div>
      {stream.messages.map((message, idx) => (
        <div key={message.id ?? idx}>
          <MessageBubble message={message} />

          {/* Show subagent pipeline after the human message that triggered it */}
          {message.type === "human" && subagentsByMessage.has(message.id) && (
            <SubagentPipeline
              subagents={subagentsByMessage.get(message.id)!}
              isLoading={stream.isLoading}
            />
          )}
        </div>
      ))}
    </div>
  );
}

Subagent pipeline with progress

Show a progress bar and grid of subagent cards:
function SubagentPipeline({
  subagents,
  isLoading,
}: {
  subagents: SubagentStream[];
  isLoading: boolean;
}) {
  const completed = subagents.filter(
    (s) => s.status === "complete" || s.status === "error"
  ).length;

  const allDone = completed === subagents.length;

  return (
    <div className="my-4 space-y-3">
      {/* Progress header */}
      <div className="flex items-center justify-between text-sm">
        <span className="font-medium">
          Subagents ({completed}/{subagents.length})
        </span>
        {allDone && isLoading && (
          <span className="text-blue-500 animate-pulse">
            Synthesizing results...
          </span>
        )}
      </div>

      {/* Progress bar */}
      <div className="h-1.5 bg-gray-200 rounded-full overflow-hidden">
        <div
          className="h-full bg-blue-500 transition-all duration-300"
          style={{ width: `${(completed / subagents.length) * 100}%` }}
        />
      </div>

      {/* Subagent cards */}
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
        {subagents.map((subagent) => (
          <SubagentCard key={subagent.id} subagent={subagent} />
        ))}
      </div>
    </div>
  );
}

Rendering tool calls

Display tool calls and results from within subagent execution using the toolCalls property:
function SubagentWithTools({ subagent }: { subagent: SubagentStream }) {
  return (
    <div className="border rounded-lg p-4">
      <div className="flex items-center gap-2 mb-3">
        <StatusIcon status={subagent.status} />
        <span className="font-medium">{subagent.toolCall.subagent_type}</span>
        {subagent.toolCalls.length > 0 && (
          <span className="text-xs bg-gray-100 px-2 py-0.5 rounded-full">
            {subagent.toolCalls.length} tool calls
          </span>
        )}
      </div>

      {/* Tool calls */}
      {subagent.toolCalls.map((tc) => (
        <div key={tc.call.id} className="mb-2 p-2 bg-gray-50 rounded text-sm">
          <div className="flex items-center gap-2">
            <span className="font-mono text-xs">{tc.call.name}</span>
            {tc.result !== undefined ? (
              <span className="text-green-600 text-xs">completed</span>
            ) : (
              <span className="text-yellow-600 text-xs animate-pulse">
                running...
              </span>
            )}
          </div>

          {/* Tool arguments */}
          <pre className="text-xs text-gray-600 mt-1 overflow-x-auto">
            {JSON.stringify(tc.call.args, null, 2)}
          </pre>

          {/* Tool result */}
          {tc.result !== undefined && (
            <div className="mt-1 pt-1 border-t text-xs">
              {typeof tc.result === "string"
                ? tc.result.slice(0, 200)
                : JSON.stringify(tc.result, null, 2)}
            </div>
          )}
        </div>
      ))}

      {/* Streaming content */}
      <div className="mt-2 prose text-sm">
        {getStreamingContent(subagent.messages)}
      </div>
    </div>
  );
}

Thread persistence

Persist thread IDs across page reloads so users can return to their deep agent conversations:
import { useCallback, useState, useEffect } from "react";
import { useStream } from "@langchain/langgraph-sdk/react";
import type { agent } from "./agent";

function useThreadIdParam() {
  const [threadId, setThreadId] = useState<string | null>(() => {
    const params = new URLSearchParams(window.location.search);
    return params.get("threadId");
  });

  const updateThreadId = useCallback((id: string) => {
    setThreadId(id);
    const url = new URL(window.location.href);
    url.searchParams.set("threadId", id);
    window.history.replaceState({}, "", url.toString());
  }, []);

  return [threadId, updateThreadId] as const;
}

function PersistentDeepAgentChat() {
  const [threadId, onThreadId] = useThreadIdParam();

  const stream = useStream<typeof agent>({
    assistantId: "deep-agent",
    apiUrl: "http://localhost:2024",
    filterSubagentMessages: true,
    threadId,
    onThreadId,
    reconnectOnMount: true,  // Auto-resume stream after page reload
  });

  return (
    <div>
      {stream.messages.map((message, idx) => (
        <div key={message.id ?? idx}>
          {message.type}: {message.content}
        </div>
      ))}

      {/* Subagents are reconstructed from thread history on reload */}
      {[...stream.subagents.values()].map((subagent) => (
        <SubagentCard key={subagent.id} subagent={subagent} />
      ))}
    </div>
  );
}
When a page reloads, useStream reconstructs subagent state from thread history. Completed subagents are restored with their final status and result, so users see the full conversation history including subagent work.

Type safety

For full type safety, pass your agent type to useStream. This gives you typed access to state, messages, tool calls, and subagent data:
import { useStream } from "@langchain/langgraph-sdk/react";
import type { agent } from "./agent";

function TypedDeepAgentChat() {
  const stream = useStream<typeof agent>({
    assistantId: "deep-agent",
    apiUrl: "http://localhost:2024",
    filterSubagentMessages: true,
  });

  // stream.values is typed to your agent's state
  // stream.messages has typed tool calls
  // stream.subagents has typed subagent data
}

Complete examples

For full working implementations that combine all the patterns above, see these examples in the LangGraph.js repository:

Deep agent example

Parallel subagents with a grid layout, streaming content, progress tracking, and synthesis detection.

Deep agent with tool calls

Tool call visibility, thread persistence, expandable subagent cards, and automatic reconnection on page reload.

Connect these docs to Claude, VSCode, and more via MCP for real-time answers.