Sub-Agents
Decompose work across multiple specialized agents with a visible delegation log.
import { openai } from "@ai-sdk/openai";import { Agent } from "@mastra/core/agent";import { weatherTool, stockPriceTool, queryDataTool, manageSalesTodosTool, getSalesTodosTool, scheduleMeetingTool, searchFlightsTool, generateA2uiTool, setNotesTool, researchAgentTool, writingAgentTool, critiqueAgentTool,} from "@/mastra/tools";import { LibSQLStore } from "@mastra/libsql";import { z } from "zod";import { Memory } from "@mastra/memory";export const AgentState = z.object({ proverbs: z.array(z.string()).default([]),});/** * Persistent SQLite URL for working-memory storage. * * Why not `file::memory:`: an in-memory store resets on every process * restart. For demos that surface user state to the UI (notes panel, agent * delegations, preferences), that is silent data loss — the user adds notes, * the dev hits save, Next.js HMR restarts the server, and the notes vanish * with no error. * * Tests can override via `MASTRA_WORKING_MEMORY_URL=file::memory:` to keep * fixture isolation. The default is a relative file path so the DB lives * next to the package and survives reloads. */export const WORKING_MEMORY_DB_URL = process.env.MASTRA_WORKING_MEMORY_URL ?? "file:./mastra-memory.db";/** * Shared-state schema for the Shared State (Read + Write) demo. * * - `preferences` is WRITTEN by the UI via `agent.setState({ preferences })`. * The AG-UI Mastra adapter merges `input.state` into the thread's * `workingMemory` metadata before each run, so the LLM sees the latest UI * preferences as part of working memory on every turn. * - `notes` is WRITTEN by the agent (via the `set_notes` tool) and READ by * the UI via `useAgent({ updates: [OnStateChanged] })`. Mastra emits a * `STATE_SNAPSHOT` after each run with the working-memory contents. */export const SharedStateRWAgentState = z.object({ preferences: z .object({ name: z.string().default(""), tone: z.enum(["formal", "casual", "playful"]).default("casual"), language: z.string().default("English"), interests: z.array(z.string()).default([]), }) .default({ name: "", tone: "casual", language: "English", interests: [], }), notes: z.array(z.string()).default([]),});/** * Shared-state schema for the Sub-Agents demo. * * `delegations` is appended to by the supervisor as it fans out work to the * research / writing / critique sub-agents. The UI subscribes via * `useAgent({ updates: [OnStateChanged] })` and renders a live delegation * log. */export const SubagentsAgentState = z.object({ delegations: z .array( z.object({ id: z.string(), sub_agent: z.enum([ "research_agent", "writing_agent", "critique_agent", ]), task: z.string(), status: z.enum(["running", "completed", "failed"]), result: z.string(), }), ) .default([]),});export const weatherAgent = new Agent({ id: "weather-agent", name: "Weather Agent", tools: { get_weather: weatherTool, query_data: queryDataTool, manage_sales_todos: manageSalesTodosTool, get_sales_todos: getSalesTodosTool, schedule_meeting: scheduleMeetingTool, search_flights: searchFlightsTool, generate_a2ui: generateA2uiTool, }, model: openai("gpt-4o"), instructions: "You are a helpful assistant.", memory: new Memory({ storage: new LibSQLStore({ id: "weather-agent-memory", url: WORKING_MEMORY_DB_URL, }), options: { workingMemory: { enabled: true, schema: AgentState, }, }, }),});// Dedicated agent for the headless-complete demo. Exercises the full// generative-UI stack when the chat UI is composed manually: two backend// tools (weather + stock price) wired through `useRenderTool`, plus a// frontend-registered `highlight_note` tool the agent can invoke via the// same tool-call channel. The system prompt nudges the model toward the// right surface per user question and falls back to plain text otherwise.//// Note: `highlight_note` is intentionally NOT declared here — it's a// frontend-only tool registered via `useComponent` in the demo's// `tool-renderers.tsx`. The agent picks it up through CopilotKit's// frontend-tool forwarding when `copilotkit.runAgent` is called.export const headlessCompleteAgent = new Agent({ id: "headless-complete-agent", name: "Headless Complete Agent", tools: { weatherTool, stockPriceTool, }, model: openai("gpt-4o-mini"), instructions: `You are a helpful, concise assistant wired into a headless chat surface that demonstrates CopilotKit's full rendering stack. Pick the right surface for each user question and fall back to plain text when none of the tools fit.Routing rules: - If the user asks about weather for a place, call \`get_weather\` with the location. - If the user asks about a stock or ticker (AAPL, TSLA, MSFT, ...), call \`get_stock_price\` with the ticker. - If the user asks you to highlight, flag, or mark a short note or phrase, call the frontend \`highlight_note\` tool with the text and a color (yellow, pink, green, or blue). Do NOT ask the user for the color — pick a sensible one if they didn't say. - Otherwise, reply in plain text.After a tool returns, write one short sentence summarizing the result. Never fabricate data a tool could provide.`, memory: new Memory({ storage: new LibSQLStore({ id: "headless-complete-agent-memory", url: WORKING_MEMORY_DB_URL, }), options: { workingMemory: { enabled: true, schema: AgentState, }, }, }),});/** * Mastra agent backing the Shared State (Read + Write) demo. * * Bidirectional shared-state pattern: * - UI -> agent: the UI writes `preferences` via `agent.setState(...)`. * The AG-UI Mastra adapter merges that into working memory before each * run, so the LLM reads it as part of its system context. * - agent -> UI: the LLM calls `set_notes` to update the `notes` array. * Mastra includes the `notes` field in its working-memory schema, so * after each run the AG-UI adapter emits a `STATE_SNAPSHOT` and the UI * re-renders. * * Note on the system prompt: rather than a static string, this is a * function so we can reaffirm — every turn — that the LLM should respect * whatever `preferences` are sitting in working memory. Mastra exposes * working memory to the LLM automatically; the prompt just nudges it to * actually USE those preferences instead of ignoring them. */export const sharedStateReadWriteAgent = new Agent({ id: "shared-state-read-write", name: "Shared State Read+Write Agent", tools: { setNotesTool }, model: openai("gpt-4o-mini"), instructions: `You are a helpful, concise assistant wired to a UI that owns the user's preferences and an agent-authored notes panel.PREFERENCES (READ from working memory every turn):The UI writes a \`preferences\` object into shared state. It contains: - name: how to address the user - tone: "formal" | "casual" | "playful" - language: the language to reply in - interests: a list of topics the user cares aboutAlways tailor your reply to these preferences. Address the user by name when one is set. Reply in their preferred language and tone. Lean on their interests when suggesting examples or topics.NOTES (WRITE via the \`set_notes\` tool):The UI also renders an "Agent notes" panel sourced from the \`notes\` array in shared state. Whenever the user asks you to remember something, OR when you make a useful observation about the user worth surfacing, call the \`set_notes\` tool with the FULL updated list of short note strings (existing notes + new ones). Always pass the entire list — never a diff. Keep each note short (< 120 chars).The \`set_notes\` tool persists the notes to working memory itself — you do NOT need to also call \`updateWorkingMemory\`. Just call \`set_notes\` and the UI will update.`, memory: new Memory({ storage: new LibSQLStore({ id: "shared-state-rw-agent-memory", url: WORKING_MEMORY_DB_URL, }), options: { workingMemory: { enabled: true, schema: SharedStateRWAgentState, }, }, }),});/** * Mastra agent backing the Sub-Agents demo. * * Supervisor pattern: this agent delegates to three specialized sub-agents * (research / writing / critique) exposed as tools. Each tool runs the * matching sub-agent under the hood and returns both its output and a * `delegation` entry the supervisor must append to working memory's * `delegations` array. The UI renders that array live as a delegation log. * * Sub-agents are defined alongside the tools in * `src/mastra/tools/subagents.ts` — they're full `Agent` instances with * their own system prompts and don't share memory with the supervisor. */export const subagentsSupervisorAgent = new Agent({ id: "subagents-supervisor", name: "Subagents Supervisor", tools: { researchAgentTool, writingAgentTool, critiqueAgentTool, }, model: openai("gpt-4o-mini"), instructions: `You are a supervisor agent that coordinates three specialized sub-agents to produce high-quality deliverables.Available sub-agents (call them as tools): - research_agent: gathers facts on a topic. - writing_agent: turns facts + a brief into a polished draft. - critique_agent: reviews a draft and suggests improvements.For most non-trivial user requests, delegate in sequence: research -> write -> critique. Pass the relevant facts/draft through the \`task\` argument of each tool. Keep your own messages short — explain the plan once, delegate, then return a concise summary once done.DELEGATION LOG (working memory):Each sub-agent tool returns a JSON payload of the form \`{ "result": <text>, "delegation": <Delegation> }\`. The tool itself appends the \`delegation\` object to the \`delegations\` array in working memory — you do NOT need to call \`updateWorkingMemory\` for delegations. Just keep delegating; the live log updates automatically.If a delegation's \`status\` field is \`"failed"\`, treat it as a real error: do not pretend the sub-agent succeeded. Decide whether to retry, fall back to a different sub-agent, or summarize the failure to the user.`, memory: new Memory({ storage: new LibSQLStore({ id: "subagents-supervisor-memory", url: WORKING_MEMORY_DB_URL, }), options: { workingMemory: { enabled: true, schema: SubagentsAgentState, }, }, }),});/** * Lightweight Mastra agent backing the MCP Apps demo. * * Defines no bespoke tools — the CopilotKit runtime is wired with * `mcpApps: { servers: [...] }` (see * `src/app/api/copilotkit-mcp-apps/route.ts`). The runtime auto-applies the * MCP Apps middleware, which injects the remote MCP server's tools into * each request and emits the activity events the built-in * `MCPAppsActivityRenderer` renders in chat as sandboxed iframes. */export const mcpAppsAgent = new Agent({ id: "mcp-apps-agent", name: "MCP Apps Agent", model: openai("gpt-4o-mini"), instructions: `You draw simple diagrams in Excalidraw via the MCP tool.SPEED MATTERS. Produce a correct-enough diagram fast; do not optimize for polish. Target: one tool call, done in seconds.When the user asks for a diagram:1. Call \`create_view\` ONCE with 3-5 elements total: shapes + arrows + an optional title text.2. Use straightforward shapes (rectangle, ellipse, diamond) with plain \`label\` fields (\`{"text": "...", "fontSize": 18}\`) on them.3. Connect with arrows. Endpoints can be element centers or simple coordinates.4. Include ONE \`cameraUpdate\` at the END of the elements array that frames the whole diagram (600x450 or 800x600).5. Reply with ONE short sentence describing what you drew.Every element needs a unique string \`id\` (e.g. \`"b1"\`, \`"a1"\`, \`"title"\`). Standard sizes: rectangles 160x70, ellipses/diamonds 120x80, 40-80px gap between shapes.Do NOT call \`read_me\`, do NOT iterate, do NOT make multiple calls. Ship on the first shot.`, memory: new Memory({ storage: new LibSQLStore({ id: "mcp-apps-agent-memory", url: WORKING_MEMORY_DB_URL, }), options: { workingMemory: { enabled: true, schema: AgentState, }, }, }),});/** * Mastra agent backing the byoc-hashbrown demo. * * The demo page wraps CopilotChat in the HashBrownDashboard provider and * overrides the assistant message slot with a renderer that consumes * hashbrown-shaped structured output via `@hashbrownai/react`'s `useUiKit` * + `useJsonParser`. * * The system prompt forces the model to emit a single JSON envelope * `{ "ui": [ { <componentName>: { "props": { ... } } }, ... ] }` matching * the schema consumed by `useSalesDashboardKit()` in the frontend renderer. * Without this prompt the default weatherAgent produces plain text, which * `useJsonParser` parses as `null` and the dashboard renders nothing. */export const byocHashbrownAgent = new Agent({ id: "byoc-hashbrown-agent", name: "BYOC Hashbrown Agent", model: openai("gpt-4o-mini"), instructions: `You are a sales analytics assistant that replies by emitting a single JSONobject consumed by a streaming JSON parser on the frontend.ALWAYS respond with a single JSON object of the form:{ "ui": [ { <componentName>: { "props": { ... } } }, ... ]}Do NOT wrap the response in code fences. Do NOT include any preface orexplanation outside the JSON object. The response MUST be valid JSON.Available components and their prop schemas:- "metric": { "props": { "label": string, "value": string } } A KPI card. \`value\` is a pre-formatted string like "$1.2M" or "248".- "pieChart": { "props": { "title": string, "data": string } } A donut chart. \`data\` is a JSON-encoded STRING (embedded JSON) of an array of {label, value} objects with at least 3 segments, e.g. "data": "[{\\"label\\":\\"Enterprise\\",\\"value\\":600000}]".- "barChart": { "props": { "title": string, "data": string } } A vertical bar chart. \`data\` is a JSON-encoded STRING of an array of {label, value} objects with at least 3 bars, typically time-ordered.- "dealCard": { "props": { "title": string, "stage": string, "value": number } } A single sales deal. \`stage\` MUST be one of: "prospect", "qualified", "proposal", "negotiation", "closed-won", "closed-lost". \`value\` is a raw number (no currency symbol or comma).- "Markdown": { "props": { "children": string } } Short explanatory text. Use for section headings and brief summaries. Standard markdown is supported in \`children\`.Rules:- Always produce plausible sample data when the user asks for a dashboard or chart — do not refuse for lack of data.- Prefer 3-6 rows of data in charts; keep labels short.- Use "Markdown" for short headings or linking sentences between visual components. Do not emit long prose.- Do not emit components that are not listed above.- \`data\` props on charts MUST be a JSON STRING — escape inner quotes.Example response (sales dashboard):{"ui":[{"Markdown":{"props":{"children":"## Q4 Sales Summary"}}},{"metric":{"props":{"label":"Total Revenue","value":"$1.2M"}}},{"metric":{"props":{"label":"New Customers","value":"248"}}},{"pieChart":{"props":{"title":"Revenue by Segment","data":"[{\\"label\\":\\"Enterprise\\",\\"value\\":600000},{\\"label\\":\\"SMB\\",\\"value\\":400000},{\\"label\\":\\"Startup\\",\\"value\\":200000}]"}}},{"barChart":{"props":{"title":"Monthly Revenue","data":"[{\\"label\\":\\"Oct\\",\\"value\\":350000},{\\"label\\":\\"Nov\\",\\"value\\":400000},{\\"label\\":\\"Dec\\",\\"value\\":450000}]"}}}]}`, memory: new Memory({ storage: new LibSQLStore({ id: "byoc-hashbrown-agent-memory", url: WORKING_MEMORY_DB_URL, }), options: { workingMemory: { enabled: true, schema: AgentState, }, }, }),});/** * Vision-capable Mastra agent backing the Multimodal Attachments demo. * * gpt-4o supports image and PDF attachments in the messages array. The * AG-UI Mastra adapter forwards user-message `content` parts (image_url / * file) verbatim to the model. Kept on a dedicated agent (and dedicated * route) so the vision-tier cost is scoped to exactly the cell that * exercises it. *//** * Scheduling agent for the interrupt-adapted demos (gen-ui-interrupt, * interrupt-headless). * * This agent powers the "Strategy B" adaptation of the LangGraph interrupt * demos. LangGraph has a native `interrupt()` primitive with * checkpoint/resume; Mastra does not. Instead, we register a frontend tool * (`schedule_meeting`) via `useFrontendTool` with an async handler. The * handler returns a Promise that only resolves once the user picks a time * slot (or cancels), producing the same UX as `interrupt()`. * * The agent defines NO backend tools — `schedule_meeting` is satisfied * entirely by the frontend. The system prompt directs the model to always * call `schedule_meeting` when asked to book/schedule. */export const interruptAgent = new Agent({ id: "interrupt-agent", name: "Interrupt Agent", tools: {}, model: openai("gpt-4o-mini"), instructions: `You are a scheduling assistant. Whenever the user asks you to book a call or schedule a meeting, you MUST call the \`schedule_meeting\` tool. Pass a short \`topic\` describing the purpose of the meeting and, if known, an \`attendee\` describing who the meeting is with.The \`schedule_meeting\` tool is implemented on the client: it surfaces a time-picker UI to the user and returns the user's selection. After the tool returns, briefly confirm whether the meeting was scheduled and at what time, or note that the user cancelled. Do NOT ask for approval yourself — always call the tool and let the picker handle the decision.Keep responses short and friendly. After you finish executing tools, always send a brief final assistant message summarizing what happened so the message persists.`, memory: new Memory({ storage: new LibSQLStore({ id: "interrupt-agent-memory", url: WORKING_MEMORY_DB_URL, }), options: { workingMemory: { enabled: true, schema: AgentState, }, }, }),});export const multimodalAgent = new Agent({ id: "multimodal-demo", name: "Multimodal Agent", model: openai("gpt-4o"), instructions: "You are a helpful assistant with vision and document capabilities. When the user shares an image or PDF, examine it carefully and answer their question about it. Be concise and specific — describe what you actually see, not what you guess might be there.", memory: new Memory({ storage: new LibSQLStore({ id: "multimodal-agent-memory", url: WORKING_MEMORY_DB_URL, }), options: { workingMemory: { enabled: true, schema: AgentState, }, }, }),});What is this?#
Sub-agents are the canonical multi-agent pattern: a top-level supervisor LLM orchestrates one or more specialized sub-agents by exposing each of them as a tool. The supervisor decides what to delegate, the sub-agents do their narrow job, and their results flow back up to the supervisor's next step.
This is fundamentally the same shape as tool-calling, but each "tool" is itself a full-blown agent with its own system prompt and (often) its own tools, memory, and model.
When should I use this?#
Reach for sub-agents when a task has distinct specialized sub-tasks that each benefit from their own focus:
- Research → Write → Critique pipelines, where each stage needs a different system prompt and temperature.
- Router + specialists, where one agent classifies the request and dispatches to the right expert.
- Divide-and-conquer — any problem that fits cleanly into parallel or sequential sub-problems.
The example below uses the Research → Write → Critique shape as the canonical example.
Setting up sub-agents#
Each sub-agent is a full create_agent(...) call with its own model,
its own system prompt, and (optionally) its own tools. They don't share
memory or tools with the supervisor; the supervisor only ever sees
what the sub-agent returns.
import { createTool } from "@mastra/core/tools";import { z } from "zod";import { openai } from "@ai-sdk/openai";import { Agent } from "@mastra/core/agent";import crypto from "node:crypto";import { writeDelegationsToWorkingMemory } from "./working-memory";// Each sub-agent is a full Mastra `Agent` with its own system prompt. They// don't share memory or tools with the supervisor — the supervisor only sees// their final text output via the tools below. Mirrors the LangGraph-Python// `subagents.py` reference where each sub-agent is a `create_agent(...)`.const SUBAGENT_MODEL = openai("gpt-4o-mini");const researchSubAgent = new Agent({ id: "research-subagent", name: "Research Subagent", model: SUBAGENT_MODEL, instructions: "You are a research sub-agent. Given a topic, produce a concise " + "bulleted list of 3-5 key facts. No preamble, no closing.",});const writingSubAgent = new Agent({ id: "writing-subagent", name: "Writing Subagent", model: SUBAGENT_MODEL, instructions: "You are a writing sub-agent. Given a brief and optional source " + "facts, produce a polished 1-paragraph draft. Be clear and concrete. " + "No preamble.",});const critiqueSubAgent = new Agent({ id: "critique-subagent", name: "Critique Subagent", model: SUBAGENT_MODEL, instructions: "You are an editorial critique sub-agent. Given a draft, give 2-3 " + "crisp, actionable critiques. No preamble.",});Keep sub-agent system prompts narrow and focused. The point of this pattern is that each one does one thing well. If a sub-agent needs to know the whole user context to do its job, that's a signal the boundary is wrong.
Exposing sub-agents as tools#
The supervisor delegates by calling tools. Each tool is a thin wrapper
around sub_agent.invoke(...) that:
- Runs the sub-agent synchronously on the supplied
taskstring. - Records the delegation into a
delegationsslot in shared agent state (so the UI can render a live log). - Returns the sub-agent's final message as a
ToolMessage, which the supervisor sees as a normal tool result on its next turn.
import { createTool } from "@mastra/core/tools";import { z } from "zod";import { openai } from "@ai-sdk/openai";import { Agent } from "@mastra/core/agent";import crypto from "node:crypto";import { writeDelegationsToWorkingMemory } from "./working-memory";// Each sub-agent is a full Mastra `Agent` with its own system prompt. They// don't share memory or tools with the supervisor — the supervisor only sees// their final text output via the tools below. Mirrors the LangGraph-Python// `subagents.py` reference where each sub-agent is a `create_agent(...)`.const SUBAGENT_MODEL = openai("gpt-4o-mini");const researchSubAgent = new Agent({ id: "research-subagent", name: "Research Subagent", model: SUBAGENT_MODEL, instructions: "You are a research sub-agent. Given a topic, produce a concise " + "bulleted list of 3-5 key facts. No preamble, no closing.",});const writingSubAgent = new Agent({ id: "writing-subagent", name: "Writing Subagent", model: SUBAGENT_MODEL, instructions: "You are a writing sub-agent. Given a brief and optional source " + "facts, produce a polished 1-paragraph draft. Be clear and concrete. " + "No preamble.",});const critiqueSubAgent = new Agent({ id: "critique-subagent", name: "Critique Subagent", model: SUBAGENT_MODEL, instructions: "You are an editorial critique sub-agent. Given a draft, give 2-3 " + "crisp, actionable critiques. No preamble.",});/** * Result of invoking a sub-agent. Discriminated so the wrapping tools can map * success/failure into the correct delegation `status` for the UI without * relying on string-matching the `result` field. We deliberately do NOT * surface raw `err.message` to the LLM/UI: error messages from upstream APIs * routinely leak api keys, file paths, and prompt contents. Mirror the * agno / claude-sdk-python pattern of redacting to the error class name. */type SubAgentResult = { ok: true; text: string } | { ok: false; error: string };async function invokeSubAgent( agent: Agent, task: string,): Promise<SubAgentResult> { // Mastra Agent.generate returns an object with a `.text` field for the // final assistant text. We catch internally so a sub-agent failure becomes // a visible failed delegation entry rather than crashing the supervisor. try { const result = await agent.generate(task); const text = (result as { text?: unknown }).text; return { ok: true, text: typeof text === "string" && text.length > 0 ? text : "", }; } catch (err) { // Redact: only the error class name reaches the LLM and the UI. The full // `err.message` (which can contain provider keys, file paths, or echoed // prompts) stays in the server log below. const errorClass = err instanceof Error ? err.constructor.name : "UnknownError"; console.error( JSON.stringify({ at: new Date().toISOString(), level: "error", component: "subagents", agentId: agent.id, errorClass, message: err instanceof Error ? err.message : String(err), stack: err instanceof Error ? err.stack : undefined, }), ); return { ok: false, error: errorClass }; }}/** * Delegate a research task to the research sub-agent. * * The supervisor LLM "calls" this tool to fan a research subtask out. The * tool synchronously runs the matching Mastra sub-agent and returns a * `Delegation` entry. The supervisor is instructed to APPEND this entry to * the `delegations` array in working memory so the UI's live delegation log * picks it up via the AG-UI state-snapshot channel. */type SubAgentName = "research_agent" | "writing_agent" | "critique_agent";/** * Build the delegation entry + tool-result payload from a SubAgentResult. * * Status mapping rules (the whole point of `SubAgentResult`): * - ok: true → status = "completed", result = sub-agent text output * - ok: false → status = "failed", result = "[sub-agent error] <ErrorClass>" * * Without this discrimination, a sub-agent that throws renders as a green * "completed" entry in the UI's delegation log — a UX lie the supervisor LLM * cannot detect either, since it sees the same green payload. Surfacing the * redacted error class through the `result` field lets the supervisor decide * to retry or fall back, without leaking provider-side internals. */function buildDelegationPayload( subAgentName: SubAgentName, task: string, result: SubAgentResult,): { delegation: Record<string, unknown>; resultText: string } { const resultText = result.ok ? result.text : `[sub-agent error] ${result.error}`; const delegation = { id: crypto.randomUUID(), sub_agent: subAgentName, task, status: result.ok ? ("completed" as const) : ("failed" as const), result: resultText, }; return { delegation, resultText };}export const researchAgentTool = createTool({ id: "research_agent", description: "Delegate a research task to the research sub-agent. Use for: " + "gathering facts, background, definitions, statistics. Returns a " + "bulleted list of key facts. The delegation is also recorded directly " + "in working memory by the tool itself — you do not need to (and " + "should not) re-emit the delegation object.", inputSchema: z.object({ task: z.string().describe("The research task / topic to investigate."), }), execute: async (inputData, executionContext) => { const task = inputData.task ?? ""; const result = await invokeSubAgent(researchSubAgent, task); const { delegation, resultText } = buildDelegationPayload( "research_agent", task, result, ); await writeDelegationsToWorkingMemory(executionContext, delegation); return JSON.stringify({ result: resultText, delegation }); },});export const writingAgentTool = createTool({ id: "writing_agent", description: "Delegate a drafting task to the writing sub-agent. Use for: producing " + "a polished paragraph, draft, or summary. Pass relevant facts from " + "prior research inside `task`. Returns the draft. The delegation is " + "also recorded directly in working memory by the tool itself.", inputSchema: z.object({ task: z .string() .describe( "The drafting brief, including any facts the writer should incorporate.", ), }), execute: async (inputData, executionContext) => { const task = inputData.task ?? ""; const result = await invokeSubAgent(writingSubAgent, task); const { delegation, resultText } = buildDelegationPayload( "writing_agent", task, result, ); await writeDelegationsToWorkingMemory(executionContext, delegation); return JSON.stringify({ result: resultText, delegation }); },});export const critiqueAgentTool = createTool({ id: "critique_agent", description: "Delegate a critique task to the critique sub-agent. Use for: " + "reviewing a draft and suggesting concrete improvements. Returns the " + "critique. The delegation is also recorded directly in working memory " + "by the tool itself.", inputSchema: z.object({ task: z .string() .describe( "The draft to critique, plus any specific critique focus you want.", ), }), execute: async (inputData, executionContext) => { const task = inputData.task ?? ""; const result = await invokeSubAgent(critiqueSubAgent, task); const { delegation, resultText } = buildDelegationPayload( "critique_agent", task, result, ); await writeDelegationsToWorkingMemory(executionContext, delegation); return JSON.stringify({ result: resultText, delegation }); },});This is where CopilotKit's shared-state channel earns its keep: the
supervisor's tool calls mutate delegations as they happen, and the
frontend renders every new entry live.
Rendering a live delegation log#
On the frontend, the delegation log is just a reactive render of the
delegations slot. Subscribe with useAgent({ updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged] }),
read agent.state.delegations, and render one card per entry.
/** * Live delegation log — renders the `delegations` slot of agent state. * * Each entry corresponds to one invocation of a sub-agent. The list * grows in real time as the supervisor fans work out to its children. * The parent header shows how many sub-agents have been called and * whether the supervisor is still running. */export function DelegationLog({ delegations, isRunning }: DelegationLogProps) { return ( <div data-testid="delegation-log" className="w-full h-full flex flex-col bg-white rounded-2xl shadow-sm border border-[#DBDBE5] overflow-hidden" > <div className="flex items-center justify-between px-6 py-3 border-b border-[#E9E9EF] bg-[#FAFAFC]"> <div className="flex items-center gap-3"> <span className="text-lg font-semibold text-[#010507]"> Sub-agent delegations </span> {isRunning && ( <span data-testid="supervisor-running" className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full border border-[#BEC2FF] bg-[#BEC2FF1A] text-[#010507] text-[10px] font-semibold uppercase tracking-[0.12em]" > <span className="w-1.5 h-1.5 rounded-full bg-[#010507] animate-pulse" /> Supervisor running </span> )} </div> <span data-testid="delegation-count" className="text-xs font-mono text-[#838389]" > {delegations.length} calls </span> </div> <div className="flex-1 overflow-y-auto p-4 space-y-3"> {delegations.length === 0 ? ( <p className="text-[#838389] italic text-sm"> Ask the supervisor to complete a task. Every sub-agent it calls will appear here. </p> ) : ( delegations.map((d, idx) => { const style = SUB_AGENT_STYLE[d.sub_agent]; return ( <div key={d.id} data-testid="delegation-entry" className="border border-[#E9E9EF] rounded-xl p-3 bg-[#FAFAFC]" > <div className="flex items-center justify-between mb-2"> <div className="flex items-center gap-2"> <span className="text-xs font-mono text-[#AFAFB7]"> #{idx + 1} </span> <span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-semibold uppercase tracking-[0.1em] border ${style.color}`} > <span>{style.emoji}</span> <span>{style.label}</span> </span> </div> <span className="text-[10px] uppercase tracking-[0.12em] font-semibold text-[#189370]"> {d.status} </span> </div> <div className="text-xs text-[#57575B] mb-2"> <span className="font-semibold text-[#010507]">Task: </span> {d.task} </div> <div className="text-sm text-[#010507] whitespace-pre-wrap bg-white rounded-lg p-2.5 border border-[#E9E9EF]"> {d.result} </div> </div> ); }) )} </div> </div> );}The result: as the supervisor fans work out to its sub-agents, the log grows in real time, giving the user visibility into a process that would otherwise be a long opaque spinner.
Related#
- Shared State — the channel that makes the delegation log live.
- State streaming — stream individual sub-agent outputs token-by-token inside each log entry.
