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 tool UI registrations are available in sub-agent messages. A
makeAssistantToolUIregistered 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 {
makeAssistantToolUI,
MessagePartPrimitive,
} from "@assistant-ui/react";
const ResearchAgentToolUI = makeAssistantToolUI({
toolName: "invoke_researcher",
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>
),
});
Provide the Messages from the Backend#
Your backend must populate the messages field on the tool call result. For example, with the AI SDK:
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,
// The messages field is picked up by assistant-ui
messages: subAgentMessages,
};
},
}),
},
Register the Tool UI Component#
function App() {
return (
<AssistantRuntimeProvider runtime={runtime}>
<Thread />
<ResearchAgentToolUI />
</AssistantRuntimeProvider>
);
}
Recursive Sub-Agents#
If a sub-agent's tool calls also have nested messages, the same pattern applies recursively:
const OuterAgentToolUI = makeAssistantToolUI({
toolName: "invoke_planner",
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>
),
});
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