当 coordinator agent 生成 specialist subagents(researcher、analyst、writer)时, 你需要将 orchestrator messages 与每个 subagent 的 streaming output 分开渲染。 v1 SDK 会将 coordinator messages 保留在 root stream 上,并将 subagents 暴露为 discovery snapshots。 将 snapshot 传给 selector hooks 或 composables,例如 useMessages(stream, subagent), 即可渲染 specialist 的 scoped stream。 这正是 LangChain frontend SDKs 超越 flat chat transcript 的地方: subagents 是 first-class stream entities,拥有自己的 status、messages、 tool-call metadata 和 results。你的 UI 可以展示 delegation、progress、errors 和 final synthesis,而无需让 users 阅读来自每个 worker 的交错 tokens。

为什么使用 selector-based subagent streams

Root stream 会保持聚焦于 coordinator conversation:
  • stream.messages 只包含 coordinator messages
  • stream.subagents 包含带 identity、namespace 和 status 的 discovery snapshots
  • 每个 subagent 的 messages、tool calls 和 values 都通过 selector helpers 读取
  • UI 保持清晰:coordinator reasoning 与 specialists work 分离
这种分离让你可以在一个位置渲染 orchestrator messages,并且只在 user 需要查看 specialist work 时 mount subagent cards。 对于 large tasks,这也能保持 UI scalable。Users 可以浏览 coordinator 的 high-level plan, 只展开自己关心的 specialist work,同时仍保留完整 subagent trace,用于 debugging、audit 或 replay。

设置 useStream

不需要额外 stream options。将 stream 指向 deep agent, 从 stream.messages 渲染 coordinator messages,并使用 stream.subagents 为 active specialists mount cards。在 chat layouts 中,按生成 subagent 的 tool-call ID 索引 subagents,这样每张 card 都会出现在委派该 work 的 coordinator turn 下。
The code examples use useStream<typeof myAgent> for type-safe stream state. See Type inference for Python or JavaScript backends.
import { useStream } from "@langchain/react";
import { AIMessage, HumanMessage } from "langchain";

const AGENT_URL = "http://localhost:2024";

export function DeepAgentChat() {
  const stream = useStream<typeof myAgent>({
    apiUrl: AGENT_URL,
    assistantId: "deep_agent_subagent_cards",
  });
  const subagents = [...stream.subagents.values()];
  const subagentsByCallId = new Map(subagents.map((s) => [s.id, s]));

  return (
    <div>
      {stream.messages.map((msg) => {
        const turnSubagents = AIMessage.isInstance(msg)
          ? (msg.tool_calls ?? [])
              .map((tc) => subagentsByCallId.get(tc.id ?? ""))
              .filter((s): s is NonNullable<typeof s> => !!s)
          : [];

        return (
          <div key={msg.id}>
            {HumanMessage.isInstance(msg) && <HumanBubble>{msg.text}</HumanBubble>}
            {AIMessage.isInstance(msg) && msg.text.trim() && (
              <AIBubble>{msg.text}</AIBubble>
            )}
            {turnSubagents.map((subagent) => (
              <SubagentCard key={subagent.id} stream={stream} subagent={subagent} />
            ))}
          </div>
        );
      })}
    </div>
  );
}

提交 messages

通过 root stream 提交 messages。Deep agent workflows 经常涉及多层 nested subgraphs, 因此如果 agent 可能进行深层 delegation,请设置合适的 recursion limit:
stream.submit(
  { messages: [{ type: "human", content: text }] },
  { config: { recursion_limit: 100 } }
);
Deep Agents 设置的 default recursion limit 为 10,000,足以满足大多数 multi-expert setups。 如有需要,你可以通过 config.recursion_limit 覆盖它。

The SubagentDiscoverySnapshot

每个 SubagentDiscoverySnapshot 都是 thread 内运行的 subagent 的 lightweight discovery record。 它会告诉 UI:subagent 存在、它位于 subagent tree 的哪个位置,以及当前处于哪种 lifecycle state。 Snapshot 包含 subagent 的 streamed messages 或 tool calls。 请改为将 snapshot 传给 useMessages(stream, subagent)useToolCalls(stream, subagent) 等 selector hooks。这些 hooks 会使用 snapshot namespace, 只有在对应 card 或 panel 被 mounted 时才订阅 subagent stream primitives。

构建 SubagentCard

每个 subagent card 都会显示 specialist name、status、streaming content 和 tool calls。 使用 selector hooks 订阅 subagent namespace:
import { useState } from "react";
import { AIMessage } from "langchain";
import {
  useMessages,
  useToolCalls,
  type AnyStream,
  type SubagentDiscoverySnapshot,
} from "@langchain/react";

function SubagentCard({
  stream,
  subagent,
}: {
  stream: AnyStream;
  subagent: SubagentDiscoverySnapshot;
}) {
  const [expanded, setExpanded] = useState(true);
  const messages = useMessages(stream, subagent);
  const toolCalls = useToolCalls(stream, subagent);

  const lastAIMessage = messages
    .filter(AIMessage.isInstance)
    .at(-1);

  const displayContent =
    lastAIMessage?.text ?? subagent.output ?? "";

  return (
    <div className="rounded-lg border bg-white shadow-sm">
      <button
        onClick={() => setExpanded(!expanded)}
        className="flex w-full items-center justify-between p-4"
      >
        <div className="flex items-center gap-3">
          <StatusIcon status={subagent.status} />
          <div>
            <h4 className="font-semibold capitalize">{subagent.name}</h4>
            <p className="text-xs text-gray-500">
              {toolCalls.length} tool call{toolCalls.length === 1 ? "" : "s"}
            </p>
          </div>
        </div>
        <div className="flex items-center gap-2">
          <StatusBadge status={subagent.status} />
        </div>
      </button>

      {expanded && displayContent && (
        <div className="border-t px-4 py-3">
          <div className="prose prose-sm max-w-none line-clamp-6">
            {displayContent}
            {subagent.status === "running" && (
              <span className="inline-block h-4 w-1 animate-pulse bg-blue-500" />
            )}
          </div>
        </div>
      )}
    </div>
  );
}

Progress tracking

显示 progress bar 和 counter,让 users 知道已有多少 subagents 完成:
function SubagentProgress({
  subagents,
}: {
  subagents: SubagentDiscoverySnapshot[];
}) {
  const completed = subagents.filter((s) => s.status === "complete").length;
  const total = subagents.length;
  const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;

  return (
    <div className="space-y-1">
      <div className="flex items-center justify-between text-xs text-gray-500">
        <span>Subagent progress</span>
        <span>
          {completed}/{total} complete
        </span>
      </div>
      <div className="h-2 overflow-hidden rounded-full bg-gray-200">
        <div
          className="h-full rounded-full bg-blue-500 transition-all duration-300"
          style={{ width: `${percentage}%` }}
        />
      </div>
    </div>
  );
}

使用 subagent cards 渲染 messages

关键 layout pattern 是从 root stream 渲染 coordinator messages, 并将 subagent cards 附加到产生这些 subagents 的 AI message 上:
function DeepAgentLayout({ stream }: { stream: AnyStream }) {
  const subagents = [...stream.subagents.values()];
  const subagentsByCallId = new Map(subagents.map((s) => [s.id, s]));

  return (
    <div className="space-y-3">
      {stream.messages.map((message) => {
        const turnSubagents = AIMessage.isInstance(message)
          ? (message.tool_calls ?? [])
              .map((tc) => subagentsByCallId.get(tc.id ?? ""))
              .filter((s): s is SubagentDiscoverySnapshot => !!s)
          : [];

        return (
          <div key={message.id}>
            <Message message={message} />
            {turnSubagents.length > 0 && (
              <div className="ml-4 space-y-3 border-l-2 border-blue-200 pl-4">
                <SubagentProgress subagents={subagents} />
                {turnSubagents.map((subagent) => (
                  <SubagentCard key={subagent.id} stream={stream} subagent={subagent} />
                ))}
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
}
你可以将 inline cards 与 global subagent view 结合使用:对于 transcript cards, 按生成它们的 coordinator tool call 索引 subagents;同时使用 stream.subagents 创建 persistent sidebar,总结所有 active workers。这样 users 既能看到 local context, 也能获得整个 run 的全局视图。

Best practices

  • 只在需要的位置 mount selectors。当 card 调用 useMessages(stream, subagent)useToolCalls(stream, subagent) 时,scoped messages 和 tool calls 才会 stream。
  • 显示 specialist namessubagent.name 会告诉 users 当前 active 的 worker 是谁。
  • 使用 collapsible cards。在包含 5 个以上 subagents 的 workflows 中,自动 collapse completed cards,让 users 聚焦 active work。
  • 只在需要时覆盖 recursion。Deep Agents 设置了较高的 default recursion limit;只有 unusually deep custom workflows 才需要传入 config.recursion_limit
  • 按 subagent 处理 errors。一个 subagent failed 不应让整个 UI 崩溃。在该 subagent card 中显示 error,同时让其他 subagents 继续运行。