Multi-Agent

Render sub-agent conversations inside tool call UIs.

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 makeAssistantToolUI registered 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.Messages again.
  • 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
        components={{
          UserMessage: MyUserMessage,
          AssistantMessage: MyAssistantMessage,
        }}
      />
    </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:

api/chat/route.ts
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,
      };
    },
  }),
},

The exact mechanism for populating messages depends on your backend framework. 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>
  );
}

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
        components={{
          AssistantMessage: () => (
            <MessagePrimitive.Parts
              components={{
                Text: MyText,
                tools: {
                  by_name: {
                    invoke_researcher: () => (
                      <div className="ml-4 rounded border p-3">
                        <h5>Researcher Agent</h5>
                        {/* Nested sub-agent renders recursively */}
                        <MessagePartPrimitive.Messages
                          components={{
                            UserMessage: MyUserMessage,
                            AssistantMessage: MyAssistantMessage,
                          }}
                        />
                      </div>
                    ),
                  },
                  Fallback: MyToolFallback,
                },
              }}
            />
          ),
          UserMessage: MyUserMessage,
        }}
      />
    </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
        components={{
          UserMessage: MyUserMessage,
          AssistantMessage: MyAssistantMessage,
        }}
      />
    </ReadonlyThreadProvider>
  );
}

ReadonlyThreadProvider inherits the parent's tool UI registrations and model context through scope inheritance.