Render sub-agent conversations and handoffs inside tool calls. Build supervisor and multi-agent patterns in a React chat UI with assistant-ui.
In a multi-agent (orchestrator) architecture, a main agent invokes sub-agents via tool calls. Each sub-agent may produce its own conversation (user/assistant messages, tool calls, etc.). assistant-ui supports rendering these nested conversations using the MessagePartPrimitive.Messages primitive.
Overview
When a tool call includes a messages field (ToolCallMessagePart.messages), it represents a sub-agent's conversation history. MessagePartPrimitive.Messages reads this field from the current tool call part and renders it as a nested thread.
Key behaviors:
- Scope inheritance — Parent toolkit renderers are available in sub-agent messages. A
Tools({ toolkit })registration at the top level works inside sub-agent conversations too. - Recursive — Sub-agent messages can contain tool calls that themselves have nested messages. Just use
MessagePartPrimitive.Messagesagain. - Read-only — Sub-agent messages are rendered in a readonly context. No editing, branching, or composing.
Quick Start
Register a Tool UI for the Sub-Agent
import {
Tools,
type Toolkit,
MessagePartPrimitive,
} from "@assistant-ui/react";
const toolkit = {
invoke_researcher: {
type: "backend",
render: ({ args, status }) => (
<div className="my-2 rounded-lg border p-4">
<div className="mb-2 text-sm font-medium text-gray-500">
Researcher Agent {status.type === "running" && "(working...)"}
</div>
<MessagePartPrimitive.Messages>
{({ message }) => {
if (message.role === "user") return <MyUserMessage />;
return <MyAssistantMessage />;
}}
</MessagePartPrimitive.Messages>
</div>
),
},
} satisfies Toolkit;Provide the Messages from the Backend
Your backend must populate the messages field on the tool call result.
tools: {
invoke_researcher: tool({
description: "Invoke the researcher sub-agent",
parameters: z.object({ query: z.string() }),
execute: async ({ query }) => {
const subAgentMessages = await runResearcherAgent(query);
return {
answer: subAgentMessages.at(-1)?.content,
messages: subAgentMessages,
};
},
}),
},With @assistant-ui/react-langgraph, use unstable_createLangGraphStream and set unstable_allowCancellation: true to wire up the stop button. When your graph runs a subgraph, the subgraph's messages appear on ToolCallMessagePart.messages automatically once you handle the onSubgraphValues / onSubgraphUpdates events.
import {
useLangGraphRuntime,
unstable_createLangGraphStream,
} from "@assistant-ui/react-langgraph";
const runtime = useLangGraphRuntime({
unstable_allowCancellation: true,
stream: unstable_createLangGraphStream({
client,
assistantId,
// "custom" is required for generative UI; "updates" for subgraph events
streamMode: ["messages", "updates", "custom"],
// abort the run server-side when the user clicks stop
onDisconnect: "cancel",
}),
eventHandlers: {
onSubgraphValues: (namespace, values) => {
// namespace = e.g. "tools:call_abc" — the sub-agent's node path
// values contains the subgraph state, including its messages array
},
onSubgraphUpdates: (namespace, updates) => {
// incremental state updates from the subgraph
},
onSubgraphError: (namespace, error) => {
// error scoped to the subgraph; does not mark the parent message failed
},
onMessageChunk: (chunk, metadata) => {
// metadata.namespace is set when the chunk originates from a subgraph
// use it to attribute the chunk to the correct sub-agent
},
},
});See LangGraph Streaming for the full event handler reference.
The key requirement is that the tool result's corresponding
ToolCallMessagePart includes a messages array of ThreadMessage objects.
Register the Tool UI Component
function App() {
return (
<AssistantRuntimeProvider runtime={runtime}>
<Thread />
<ResearchAgentToolUI />
</AssistantRuntimeProvider>
);
}Subgraph Namespace Events
When using LangGraph, subgraph events (onSubgraphValues / onSubgraphUpdates / onSubgraphError, plus the namespace on onMessageChunk) carry a namespace that identifies which sub-agent emitted them, letting you attribute messages and state to specific sub-agents. See LangGraph Streaming for the full reference.
Recursive Sub-Agents
If a sub-agent's tool calls also have nested messages, the same pattern applies recursively:
const toolkit = {
invoke_planner: {
type: "backend",
render: () => (
<div className="rounded border p-3">
<h4>Planner Agent</h4>
<MessagePartPrimitive.Messages>
{({ message }) => {
if (message.role === "user") return <MyUserMessage />;
return (
<MessagePrimitive.Parts>
{({ part }) => {
if (part.type === "text") return <MyText />;
if (part.type === "tool-call" && part.toolName === "invoke_researcher") return (
<div className="ml-4 rounded border p-3">
<h5>Researcher Agent</h5>
{/* Nested sub-agent renders recursively */}
<MessagePartPrimitive.Messages>
{({ message }) => {
if (message.role === "user") return <MyUserMessage />;
return <MyAssistantMessage />;
}}
</MessagePartPrimitive.Messages>
</div>
);
if (part.type === "tool-call") return <MyToolFallback {...part} />;
return null;
}}
</MessagePrimitive.Parts>
);
}}
</MessagePartPrimitive.Messages>
</div>
),
},
} satisfies Toolkit;ReadonlyThreadProvider
For advanced use cases where you have a ThreadMessage[] array and want to render it as a thread outside of a tool call context, use ReadonlyThreadProvider directly:
import {
ReadonlyThreadProvider,
ThreadPrimitive,
type ThreadMessage,
} from "@assistant-ui/react";
function SubConversation({
messages,
}: {
messages: readonly ThreadMessage[];
}) {
return (
<ReadonlyThreadProvider messages={messages}>
<ThreadPrimitive.Messages>
{({ message }) => {
if (message.role === "user") return <MyUserMessage />;
return <MyAssistantMessage />;
}}
</ThreadPrimitive.Messages>
</ReadonlyThreadProvider>
);
}ReadonlyThreadProvider inherits the parent's tool UI registrations and model context through scope inheritance.
Related
- Generative UI — Creating tool call UIs
- MessagePartPrimitive — API reference for message part primitives
- Sub-Agent Model Tracking — Track delegated model usage and costs in the Cloud dashboard
- LangGraph Streaming — Event handlers, subgraph events, and message metadata
- LangGraph Generative UI — Structured UI components emitted by your graph