{"slug":"multi-agent-trace","category":"traces","job":"oversee","added":"2026-09-12T06:58Z","title":"Multi-Agent Trace","description":"A tree of parallel sub-agent tasks, each with its own status and step list.","url":"https://ai-patterns.app/patterns/traces/multi-agent-trace","markdown":"https://ai-patterns.app/patterns/traces/multi-agent-trace.md","json":"https://ai-patterns.app/api/patterns/traces/multi-agent-trace","seenIn":[{"team":"Claude","source":"https://claude.com/blog/how-and-when-to-use-subagents-in-claude-code"}],"spec":"# Multi-Agent Trace\n\n## Summary\nA tree/list view of an orchestrator's parallel sub-agent tasks — each with its own name, live status, current-step label, and collapsible step list — so the user can watch several agents work at once instead of inferring parallel progress from one linear trace. The multi-agent counterpart to Expandable Trace.\n\n## When to use\n- An orchestrating agent has dispatched more than one sub-agent or task concurrently, and the user benefits from seeing each one's independent progress and outcome.\n- Any workflow where sub-agents can finish in a different order than they started, or one can fail while others keep going — a single linear trace can't represent that.\n\n## When not to use\n- For a single agent working through one sequence of steps — use Expandable Trace; wrapping one linear sequence in this pattern adds structure with nothing to show for it.\n- When \"sub-agents\" are actually just sequential steps of one process dressed up as separate agents — if they run one after another, they read better as a single trace.\n\n## Anatomy\n- Root header: an aggregate status line (\"3 agents working\" while running, \"Done — 3/3 complete\" once finished) with a count.\n- Per-agent row: a status icon (queued/running/done/error), the agent's name or role, a short current-step label, elapsed time, and an expand chevron.\n- Expanded body: that agent's own step list, in the same step-row language as Expandable Trace.\n\n## Behavior\n- Each row updates independently — one agent completing, failing, or still running has no effect on any other row's state or label.\n- The root header aggregates live: it reflects \"N agents working\" while any are active, and collapses to a static summary once all have resolved.\n- Expanding one row never collapses another — several can be open simultaneously, since the agents themselves are independent.\n- A failed sub-agent's row gets a distinct error tint and icon and stays visible rather than disappearing, so the user knows it needs attention.\n\n## Content guidelines\n- Agent names are short role labels (\"Research agent\", \"Code agent\"), not full descriptions of what they're doing — the current-step label carries that.\n- Current-step labels follow the same present-tense, short-phrase convention as Thinking Loader and Tool Call Chip (\"Reading docs\", not \"It is currently reading the documentation\").\n\n## Accessibility\n- Each row's expand toggle exposes `aria-expanded`.\n- Status changes are announced through a single `aria-live=\"polite\"` region on the root header, not per row, so simultaneous updates from several agents don't spam assistive tech.\n- Status is conveyed by icon and text together, never color alone, since red/green distinctions must survive a color-blind or grayscale viewing.\n\n## Related patterns\n- **Expandable Trace** — Use it instead when one agent did the work in a single linear sequence.\n- **Tool Call Chip** — Use it as well inside an agent's row when a step is a specific tool call.\n- **Background Tasks Drawer** — Use it instead when the parallel work outlives the response and needs an app-level view.\n","code":"\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { Check, ChevronDown, Loader2, X } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface AgentStep {\n  label: string;\n  meta?: string;\n}\n\nexport type AgentStatus = \"queued\" | \"running\" | \"done\" | \"error\";\n\nexport interface Agent {\n  id: string;\n  name: string;\n  icon?: React.ComponentType<{ className?: string }>;\n  status: AgentStatus;\n  currentStep?: string;\n  elapsedSeconds?: number;\n  steps: AgentStep[];\n  subAgents?: Agent[];\n}\n\nexport interface MultiAgentTraceProps {\n  agents: Agent[];\n  className?: string;\n}\n\nfunction countActive(agents: Agent[]): number {\n  return agents.reduce((n, a) => {\n    const self = a.status === \"running\" || a.status === \"queued\" ? 1 : 0;\n    return n + self + (a.subAgents ? countActive(a.subAgents) : 0);\n  }, 0);\n}\n\nfunction countFailed(agents: Agent[]): number {\n  return agents.reduce((n, a) => {\n    return n + (a.status === \"error\" ? 1 : 0) + (a.subAgents ? countFailed(a.subAgents) : 0);\n  }, 0);\n}\n\nfunction countTotal(agents: Agent[]): number {\n  return agents.reduce((n, a) => n + 1 + (a.subAgents ? countTotal(a.subAgents) : 0), 0);\n}\n\nexport function MultiAgentTrace({ agents, className }: MultiAgentTraceProps) {\n  const active = countActive(agents);\n  const failed = countFailed(agents);\n  const total = countTotal(agents);\n  const allResolved = active === 0;\n\n  return (\n    <div className={cn(\"w-full overflow-hidden rounded-2xl border bg-card\", className)}>\n      <div aria-live=\"polite\" className=\"flex items-center gap-2 border-b px-4 py-3 text-xs font-medium\">\n        {!allResolved ? (\n          <Loader2 className=\"size-3.5 animate-spin text-muted-foreground\" aria-hidden />\n        ) : failed > 0 ? (\n          <X className=\"size-3.5 text-destructive\" aria-hidden />\n        ) : (\n          <Check className=\"size-3.5 text-emerald-600 dark:text-emerald-400\" aria-hidden />\n        )}\n        <span>\n          {!allResolved\n            ? `${active} agent${active === 1 ? \"\" : \"s\"} working`\n            : `Done — ${total - failed}/${total} complete`}\n        </span>\n      </div>\n\n      <ul className=\"divide-y\">\n        {agents.map((agent) => (\n          <AgentRow key={agent.id} agent={agent} />\n        ))}\n      </ul>\n    </div>\n  );\n}\n\nfunction AgentRow({ agent, depth = 0 }: { agent: Agent; depth?: number }) {\n  const [open, setOpen] = React.useState(false);\n  const hasContent = agent.steps.length > 0 || (agent.subAgents?.length ?? 0) > 0;\n\n  return (\n    <li>\n      <button\n        type=\"button\"\n        onClick={() => setOpen((v) => !v)}\n        aria-expanded={open}\n        disabled={!hasContent}\n        className=\"flex w-full items-center gap-3 px-4 py-3 text-left disabled:cursor-default\"\n        style={{ paddingLeft: depth > 0 ? `${1 + depth * 1.5}rem` : undefined }}\n      >\n        <StatusIcon status={agent.status} icon={agent.icon} />\n        <div className=\"min-w-0 flex-1\">\n          <p className=\"text-xs font-medium\">{agent.name}</p>\n          {agent.currentStep && (\n            <p className=\"truncate text-[11px] text-muted-foreground\">{agent.currentStep}</p>\n          )}\n        </div>\n        {typeof agent.elapsedSeconds === \"number\" && (\n          <span className=\"shrink-0 font-mono text-xs text-muted-foreground\">\n            {agent.elapsedSeconds}s\n          </span>\n        )}\n        {hasContent && (\n          <ChevronDown\n            className={cn(\n              \"size-4 shrink-0 text-muted-foreground transition-transform\",\n              open && \"rotate-180\"\n            )}\n          />\n        )}\n      </button>\n\n      <AnimatePresence initial={false}>\n        {open && hasContent && (\n          <motion.div\n            initial={{ height: 0, opacity: 0 }}\n            animate={{ height: \"auto\", opacity: 1 }}\n            exit={{ height: 0, opacity: 0 }}\n            transition={{ duration: 0.2, ease: \"easeInOut\" }}\n            className=\"overflow-hidden\"\n          >\n            {agent.steps.length > 0 && (\n              <ul\n                className=\"px-4 pb-3 pl-11\"\n                style={{ paddingLeft: depth > 0 ? `${2.75 + depth * 1.5}rem` : undefined }}\n              >\n                {agent.steps.map((step, i) => (\n                  <li key={i} className=\"flex gap-3\">\n                    <div className=\"flex flex-col items-center\">\n                      <span className=\"flex size-4 shrink-0 items-center justify-center rounded-full bg-muted\">\n                        <Check className=\"size-2.5 text-muted-foreground\" />\n                      </span>\n                      {(i < agent.steps.length - 1 || (agent.subAgents?.length ?? 0) > 0) && (\n                        <span className=\"my-1 w-px flex-1 bg-border\" />\n                      )}\n                    </div>\n                    <p className=\"pb-3 text-xs text-foreground/90\">\n                      {step.label}\n                      {step.meta && (\n                        <span className=\"ml-1.5 text-muted-foreground\">{step.meta}</span>\n                      )}\n                    </p>\n                  </li>\n                ))}\n              </ul>\n            )}\n\n            {agent.subAgents && agent.subAgents.length > 0 && (\n              <ul className=\"border-t divide-y\">\n                {agent.subAgents.map((sub) => (\n                  <AgentRow key={sub.id} agent={sub} depth={depth + 1} />\n                ))}\n              </ul>\n            )}\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </li>\n  );\n}\n\nfunction StatusIcon({\n  status,\n  icon: Icon,\n}: {\n  status: AgentStatus;\n  icon?: React.ComponentType<{ className?: string }>;\n}) {\n  if (status === \"running\") {\n    return (\n      <span className=\"relative flex size-6 shrink-0 items-center justify-center rounded-full bg-muted\">\n        {Icon ? (\n          <>\n            <Icon className=\"size-3.5 text-muted-foreground\" aria-label=\"Running\" />\n            <span className=\"absolute -bottom-0.5 -right-0.5 flex size-2.5 items-center justify-center rounded-full bg-card ring-1 ring-border\">\n              <Loader2 className=\"size-1.5 animate-spin text-muted-foreground\" />\n            </span>\n          </>\n        ) : (\n          <Loader2 className=\"size-3.5 animate-spin text-muted-foreground\" aria-label=\"Running\" />\n        )}\n      </span>\n    );\n  }\n  if (status === \"error\") {\n    return (\n      <span className=\"flex size-6 shrink-0 items-center justify-center rounded-full bg-destructive/10 text-destructive\">\n        {Icon ? <Icon className=\"size-3.5\" aria-label=\"Error\" /> : <X className=\"size-3.5\" aria-label=\"Failed\" />}\n      </span>\n    );\n  }\n  if (status === \"done\") {\n    return (\n      <span className=\"relative flex size-6 shrink-0 items-center justify-center rounded-full bg-emerald-500/15 text-emerald-600 dark:text-emerald-400\">\n        {Icon ? (\n          <>\n            <Icon className=\"size-3.5\" aria-label=\"Done\" />\n            <span className=\"absolute -bottom-0.5 -right-0.5 flex size-2.5 items-center justify-center rounded-full bg-card ring-1 ring-border\">\n              <Check className=\"size-1.5 text-emerald-600 dark:text-emerald-400\" />\n            </span>\n          </>\n        ) : (\n          <Check className=\"size-3.5\" aria-label=\"Done\" />\n        )}\n      </span>\n    );\n  }\n  return (\n    <span className=\"flex size-6 shrink-0 items-center justify-center rounded-full bg-muted\" aria-label=\"Queued\">\n      {Icon ? (\n        <Icon className=\"size-3.5 text-muted-foreground/50\" />\n      ) : (\n        <span className=\"size-1.5 rounded-full bg-muted-foreground\" />\n      )}\n    </span>\n  );\n}\n","dependencies":["lucide-react","motion"],"localImports":["@/lib/utils"]}