Frontend ↗
noOriginal Documentation
Documentation Index#
Fetch the complete documentation index at: https://docs.langchain.com/llms.txt Use this file to discover all available pages before exploring further.
Build React UIs that display real-time subagent streams from deep agents
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:
Basic usage#
To stream from a deep agent with subagents, configure useStream with filterSubagentMessages and pass streamSubgraphs: true when submitting:
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 (
{/* Main conversation messages (subagent messages filtered out) */}
{stream.messages.map((message, idx) => (
{message.type}: {message.content}
))}
{/* Subagent progress */}
{stream.activeSubagents.length > 0 && (
<h3>Active subagents:</h3>
{stream.activeSubagents.map((subagent) => (
<SubagentCard key={subagent.id} subagent={subagent} />
))}
)}
{stream.isLoading && Loading...}
);
}Learn how to deploy your deep agents to LangSmith for production-ready hosting with built-in observability, authentication, and scaling.
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 type { Message } from "@langchain/langgraph-sdk";
import type { agent } from "./agent";
function SubagentCard({ subagent }: { subagent: SubagentStream<typeof agent> }) {
const content = getStreamingContent(subagent.messages);
return (
{/* Header */}
<StatusIcon status={subagent.status} />
<span className="font-medium">{subagent.toolCall.subagent_type}</span>
<span className="text-sm text-gray-500">
{subagent.toolCall.description}
</span>
{/* Streaming content */}
{content && (
{content}
)}
{/* Result */}
{subagent.status === "complete" && subagent.result && (
{subagent.result}
)}
{/* Error */}
{subagent.status === "error" && subagent.error && (
{subagent.error}
)}
);
}
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 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 (
{stream.messages.map((message, 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}
/>
)}
))}
);
}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 (
{/* Progress header */}
<span className="font-medium">
Subagents ({completed}/{subagents.length})
</span>
{allDone && isLoading && (
<span className="text-blue-500 animate-pulse">
Synthesizing results...
</span>
)}
{/* Progress bar */}
{/* Subagent cards */}
{subagents.map((subagent) => (
<SubagentCard key={subagent.id} subagent={subagent} />
))}
);
}Rendering tool calls#
Display tool calls and results from within subagent execution using the toolCalls property:
function SubagentWithTools({ subagent }: { subagent: SubagentStream }) {
return (
<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>
)}
{/* Tool calls */}
{subagent.toolCalls.map((tc) => (
<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>
)}
{/* 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 && (
{typeof tc.result === "string"
? tc.result.slice(0, 200)
: JSON.stringify(tc.result, null, 2)}
)}
))}
{/* Streaming content */}
{getStreamingContent(subagent.messages)}
);
}Thread persistence#
Persist thread IDs across page reloads so users can return to their deep agent conversations:
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 (
{stream.messages.map((message, idx) => (
{message.type}: {message.content}
))}
{/* Subagents are reconstructed from thread history on reload */}
{[...stream.subagents.values()].map((subagent) => (
<SubagentCard key={subagent.id} subagent={subagent} />
))}
);
}
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 Python type safety, use type annotations when accessing stream state:
from deepagents import create_deep_agent
agent = create_deep_agent(
system_prompt="You are a helpful assistant",
subagents=[...],
)
# Type hints are inferred from the agent definition
result = agent.invoke(
{"messages": [{"role": "user", "content": "Hello"}]}
)
# result["messages"] is typed as list of messagesComplete examples#
For full working implementations that combine all the patterns above, see these examples in the LangGraph.js repository:
Parallel subagents with a grid layout, streaming content, progress tracking, and synthesis detection.
Tool call visibility, thread persistence, expandable subagent cards, and automatic reconnection on page reload.
Related#
- Streaming overview — Server-side streaming with deep agents
- Subagents — Configure and use subagents with deep agents
- LangChain frontend streaming — General
useStreamdocumentation - useStream API Reference — Full API documentation
- Agent Chat UI — Pre-built chat interface for LangGraph agents
Edit this page on GitHub or file an issue.
Connect these docs to Claude, VSCode, and more via MCP for real-time answers.