{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-3",
  "title": "Agent 3: runs and traces",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "AI agent runs with status, time, tokens and cost, filtered and searched, and a trace waterfall for the selected run: model calls, tool calls and retrieval, each span with input, output and error.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/json-viewer.json",
    "https://ui.ballmac.com/r/segmented-control.json",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/agent-3/agent-3.tsx",
      "content": "// Ballmac UI: Agent 3. https://ui.ballmac.com/blocks/agent-3\n\"use client\"\n\nimport * as React from \"react\"\nimport { Activity, Brain, CircleAlert, CircleCheck, Database, LoaderCircle, Search, Workflow, Wrench } from \"lucide-react\"\n\nimport { JsonViewer } from \"@/components/ballmac/json-viewer\"\nimport { SegmentedControl, SegmentedControlItem } from \"@/components/ballmac/segmented-control\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { cn } from \"@/lib/utils\"\n\ntype Agent3SpanKind = \"llm\" | \"tool\" | \"retrieval\" | \"step\"\n\ntype Agent3Span = {\n  id: string\n  name: string\n  kind: Agent3SpanKind\n  /** Milliseconds from the start of the run. */\n  start: number\n  /** Milliseconds. */\n  duration: number\n  status?: \"ok\" | \"error\"\n  /** Nesting level: 0 for top-level steps, 1 for what runs inside them, and so on. */\n  depth?: number\n  tokens?: number\n  input?: unknown\n  output?: unknown\n  error?: string\n}\n\ntype Agent3Run = {\n  id: string\n  agent: string\n  task: string\n  status: \"success\" | \"error\" | \"running\"\n  /** When it started, already written for people (\"Today 09:14\"). */\n  started: string\n  /** Milliseconds. */\n  duration: number\n  tokens: number\n  /** Already formatted (\"$0.12\"). */\n  cost?: string\n  spans: Agent3Span[]\n}\n\ntype Agent3Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    /** Runs, newest first. */\n    runs?: Agent3Run[]\n    title?: string\n    /** The run selected at first. Defaults to the first one. */\n    defaultRunId?: string\n    /** Locale for numbers. */\n    locale?: string\n  }\n\nconst sample: Agent3Run[] = [\n  {\n    id: \"run_9f2\",\n    agent: \"Support agent\",\n    task: \"Refund the duplicate charge on order 4821\",\n    status: \"success\",\n    started: \"Today 09:14\",\n    duration: 8420,\n    tokens: 18420,\n    cost: \"$0.06\",\n    spans: [\n      { id: \"s1\", name: \"plan\", kind: \"llm\", start: 0, duration: 1900, tokens: 2210, input: { messages: 3 }, output: { steps: 4 } },\n      { id: \"s2\", name: \"Find the order\", kind: \"step\", start: 1900, duration: 1300 },\n      { id: \"s3\", name: \"orders.get\", kind: \"tool\", start: 1950, duration: 420, depth: 1, input: { id: 4821 }, output: { total: \"$129.00\", payments: 2 } },\n      { id: \"s4\", name: \"payments.list\", kind: \"tool\", start: 2400, duration: 610, depth: 1, input: { order: 4821 }, output: [{ id: \"ch_1\" }, { id: \"ch_2\" }] },\n      { id: \"s5\", name: \"Search refund policy\", kind: \"retrieval\", start: 3200, duration: 880, depth: 0, input: { query: \"duplicate charge refund\" }, output: { documents: 3 } },\n      { id: \"s6\", name: \"decide\", kind: \"llm\", start: 4100, duration: 2600, tokens: 9820, input: { messages: 9 }, output: { action: \"refund\", charge: \"ch_2\" } },\n      { id: \"s7\", name: \"payments.refund\", kind: \"tool\", start: 6800, duration: 1100, input: { charge: \"ch_2\" }, output: { status: \"succeeded\" } },\n      { id: \"s8\", name: \"email.send\", kind: \"tool\", start: 7950, duration: 470, input: { to: \"jordan@acme.com\", template: \"refund\" }, output: { id: \"msg_31\" } },\n    ],\n  },\n  {\n    id: \"run_9e7\",\n    agent: \"Ops agent\",\n    task: \"Summarize failed deploys this week\",\n    status: \"error\",\n    started: \"Today 08:02\",\n    duration: 5120,\n    tokens: 12904,\n    cost: \"$0.04\",\n    spans: [\n      { id: \"t1\", name: \"plan\", kind: \"llm\", start: 0, duration: 1400, tokens: 1804 },\n      { id: \"t2\", name: \"deploys.list\", kind: \"tool\", start: 1400, duration: 1240, output: { deploys: 142 } },\n      { id: \"t3\", name: \"logs.search\", kind: \"tool\", start: 2700, duration: 2420, status: \"error\", error: \"Timed out after 2.4 s: the log service did not answer.\", input: { status: \"failed\", since: \"7d\" } },\n    ],\n  },\n  {\n    id: \"run_9d1\",\n    agent: \"Research agent\",\n    task: \"Compare pricing pages of five competitors\",\n    status: \"running\",\n    started: \"Today 07:40\",\n    duration: 21300,\n    tokens: 40210,\n    spans: [\n      { id: \"u1\", name: \"plan\", kind: \"llm\", start: 0, duration: 2100, tokens: 2400 },\n      { id: \"u2\", name: \"web.fetch\", kind: \"tool\", start: 2100, duration: 9200 },\n      { id: \"u3\", name: \"extract\", kind: \"llm\", start: 11300, duration: 10000, tokens: 21800 },\n    ],\n  },\n]\n\nconst kindMeta: Record<Agent3SpanKind, { label: string; icon: typeof Brain; bar: string }> = {\n  llm: { label: \"Model call\", icon: Brain, bar: \"bg-chart-1\" },\n  tool: { label: \"Tool call\", icon: Wrench, bar: \"bg-chart-2\" },\n  retrieval: { label: \"Retrieval\", icon: Database, bar: \"bg-chart-4\" },\n  step: { label: \"Step\", icon: Workflow, bar: \"bg-muted-foreground/50\" },\n}\n\nconst runStatus = {\n  success: { label: \"Succeeded\", icon: CircleCheck, tone: \"text-chart-2\" },\n  error: { label: \"Failed\", icon: CircleAlert, tone: \"text-destructive\" },\n  running: { label: \"Running\", icon: LoaderCircle, tone: \"text-muted-foreground\" },\n} as const\n\nconst seconds = (ms: number) => (ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFixed(ms < 10000 ? 2 : 1)} s`)\n\nfunction RunsSkeleton() {\n  return (\n    <div className=\"grid gap-4\" aria-hidden=\"true\">\n      <div className=\"grid gap-2 rounded-xl border p-3\">\n        {[0, 1, 2].map((i) => (\n          <Skeleton key={i} className=\"h-9\" />\n        ))}\n      </div>\n      <Skeleton className=\"h-64 rounded-xl\" />\n    </div>\n  )\n}\n\n/** The waterfall: one row per span, a bar placed by its start and sized by its duration, against the run's total time. */\nfunction Trace({ run, locale }: { run: Agent3Run; locale: string }) {\n  const nf = new Intl.NumberFormat(locale)\n  const total = Math.max(run.duration, ...run.spans.map((s) => s.start + s.duration), 1)\n  const [spanId, setSpanId] = React.useState(run.spans.find((s) => s.status === \"error\")?.id ?? run.spans[0]?.id)\n  const span = run.spans.find((s) => s.id === spanId)\n  return (\n    <div className=\"grid gap-4 lg:grid-cols-[1fr_20rem]\">\n      <div className=\"bg-card min-w-0 rounded-xl border\">\n        <div className=\"flex flex-wrap items-center justify-between gap-2 border-b px-4 py-3\">\n          <h3 className=\"text-sm font-semibold\">Trace</h3>\n          <ul aria-label=\"Legend\" className=\"text-muted-foreground flex flex-wrap gap-3 text-xs\">\n            {(Object.keys(kindMeta) as Agent3SpanKind[]).map((k) => (\n              <li key={k} className=\"flex items-center gap-1.5\">\n                <span aria-hidden=\"true\" className={cn(\"h-2 w-3 rounded-sm\", kindMeta[k].bar)} />\n                {kindMeta[k].label}\n              </li>\n            ))}\n          </ul>\n        </div>\n        <ul aria-label={`Spans in ${run.task}`} className=\"grid p-2\">\n          {run.spans.map((s) => {\n            const meta = kindMeta[s.kind]\n            const Icon = meta.icon\n            const failed = s.status === \"error\"\n            return (\n              <li key={s.id}>\n                <button\n                  type=\"button\"\n                  aria-pressed={s.id === spanId}\n                  onClick={() => setSpanId(s.id)}\n                  className={cn(\n                    \"grid w-full grid-cols-[minmax(7rem,12rem)_1fr] items-center gap-3 rounded-md px-2 py-1.5 text-start outline-none\",\n                    \"hover:bg-accent/60 focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-pressed:bg-accent\"\n                  )}\n                >\n                  <span className=\"flex min-w-0 items-center gap-1.5 text-xs\" style={{ paddingInlineStart: `${(s.depth ?? 0) * 0.875}rem` }}>\n                    <Icon aria-hidden=\"true\" className={cn(\"size-3.5 shrink-0\", failed ? \"text-destructive\" : \"text-muted-foreground\")} />\n                    <span dir=\"ltr\" className=\"truncate font-mono\">{s.name}</span>\n                    <span className=\"sr-only\">\n                      , {meta.label}, starts at {seconds(s.start)}, takes {seconds(s.duration)}\n                      {failed ? \", failed\" : \"\"}\n                    </span>\n                  </span>\n                  <span aria-hidden=\"true\" className=\"bg-muted/60 relative h-4 rounded-sm\">\n                    <span\n                      className={cn(\"absolute inset-y-0.5 min-w-1 rounded-sm\", failed ? \"bg-destructive\" : meta.bar)}\n                      style={{ insetInlineStart: `${(s.start / total) * 100}%`, width: `${(s.duration / total) * 100}%` }}\n                    />\n                  </span>\n                </button>\n              </li>\n            )\n          })}\n        </ul>\n        <div aria-hidden=\"true\" className=\"text-muted-foreground flex justify-between border-t px-4 py-2 font-mono text-[10px] tabular-nums\">\n          <span>0</span>\n          <span>{seconds(total / 2)}</span>\n          <span>{seconds(total)}</span>\n        </div>\n      </div>\n\n      {span ? (\n        <aside aria-label=\"Span details\" className=\"bg-card grid h-fit min-w-0 gap-3 rounded-xl border p-4\">\n          <div>\n            <p className=\"text-muted-foreground text-xs\">{kindMeta[span.kind].label}</p>\n            <h4 dir=\"ltr\" className=\"mt-0.5 text-start font-mono text-sm font-semibold break-all\">{span.name}</h4>\n          </div>\n          <dl className=\"grid grid-cols-2 gap-2 text-xs\">\n            <div>\n              <dt className=\"text-muted-foreground\">Duration</dt>\n              <dd className=\"font-medium tabular-nums\">{seconds(span.duration)}</dd>\n            </div>\n            <div>\n              <dt className=\"text-muted-foreground\">Starts at</dt>\n              <dd className=\"font-medium tabular-nums\">{seconds(span.start)}</dd>\n            </div>\n            {span.tokens ? (\n              <div>\n                <dt className=\"text-muted-foreground\">Tokens</dt>\n                <dd className=\"font-medium tabular-nums\">{nf.format(span.tokens)}</dd>\n              </div>\n            ) : null}\n            <div>\n              <dt className=\"text-muted-foreground\">Status</dt>\n              <dd className={cn(\"font-medium\", span.status === \"error\" && \"text-destructive\")}>{span.status === \"error\" ? \"Failed\" : \"OK\"}</dd>\n            </div>\n          </dl>\n          {span.error ? (\n            <p role=\"note\" className=\"border-destructive/30 bg-destructive/5 rounded-md border p-2 text-xs text-pretty\">\n              {span.error}\n            </p>\n          ) : null}\n          {span.input !== undefined ? (\n            <div className=\"grid gap-1.5\">\n              <h5 className=\"text-muted-foreground text-xs font-medium\">Input</h5>\n              <JsonViewer value={span.input} label=\"Input\" defaultExpandedDepth={1} />\n            </div>\n          ) : null}\n          {span.output !== undefined ? (\n            <div className=\"grid gap-1.5\">\n              <h5 className=\"text-muted-foreground text-xs font-medium\">Output</h5>\n              <JsonViewer value={span.output} label=\"Output\" defaultExpandedDepth={1} />\n            </div>\n          ) : null}\n        </aside>\n      ) : null}\n    </div>\n  )\n}\n\n/**\n * Agent runs and their traces: every run with status, time, tokens and cost, filtered by status or searched, and for the selected\n * run a waterfall of model calls, tool calls and retrieval with each span's input, output and error.\n */\nfunction Agent3({\n  runs = sample,\n  title = \"Runs\",\n  defaultRunId,\n  locale = \"en-US\",\n  state = \"ready\",\n  onRetry,\n  emptyState,\n  errorState,\n  className,\n  ...props\n}: Agent3Props) {\n  const id = React.useId()\n  const nf = new Intl.NumberFormat(locale)\n  const [filter, setFilter] = React.useState<\"all\" | \"error\" | \"running\">(\"all\")\n  const [query, setQuery] = React.useState(\"\")\n  const q = query.trim().toLowerCase()\n  const visible = runs.filter((r) => (filter === \"all\" || r.status === filter) && (!q || `${r.task} ${r.agent} ${r.id}`.toLowerCase().includes(q)))\n  const [runId, setRunId] = React.useState(defaultRunId ?? runs[0]?.id)\n  const run = visible.find((r) => r.id === runId) ?? visible[0]\n  const ready = state === \"ready\"\n\n  return (\n    <section data-slot=\"agent-3\" data-state={state} aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-6xl px-4 py-8 sm:px-6\", className)} {...frameProps(props)}>\n      <header className=\"flex flex-wrap items-center gap-3\">\n        <h2 id={`${id}-title`} className=\"flex-1 text-2xl font-semibold tracking-[-0.03em]\">\n          {title}\n        </h2>\n        <div className=\"bg-background focus-within:ring-ring/50 flex h-9 w-full items-center gap-2 rounded-md border px-2.5 focus-within:ring-[3px] sm:w-56\">\n          <Search aria-hidden=\"true\" className=\"text-muted-foreground size-4\" />\n          <input type=\"search\" aria-label=\"Search runs\" placeholder=\"Search runs\" value={query} onChange={(e) => setQuery(e.target.value)} disabled={!ready} className=\"placeholder:text-muted-foreground min-w-0 flex-1 bg-transparent text-sm outline-none\" />\n        </div>\n        <SegmentedControl value={filter} onValueChange={(v) => setFilter(v as typeof filter)} aria-label=\"Status\" size=\"sm\" disabled={!ready}>\n          <SegmentedControlItem value=\"all\">All</SegmentedControlItem>\n          <SegmentedControlItem value=\"error\">Failed</SegmentedControlItem>\n          <SegmentedControlItem value=\"running\">Running</SegmentedControlItem>\n        </SegmentedControl>\n      </header>\n\n      <StateView\n        state={ready && !runs.length ? \"empty\" : state}\n        onRetry={onRetry}\n        label=\"Loading runs\"\n        loading={<RunsSkeleton />}\n        empty={{ title: emptyState?.title ?? \"No runs yet\", description: emptyState?.description ?? \"Each time an agent works on a task, its run and trace appear here.\", icon: emptyState?.icon ?? <Activity />, action: emptyState?.action }}\n        error={{ title: errorState?.title ?? \"Runs could not be loaded\", ...errorState }}\n        className=\"mt-6\"\n      >\n        <div className=\"grid gap-4\">\n          <div tabIndex={0} role=\"region\" aria-label=\"Runs table\" className=\"bg-card overflow-x-auto rounded-xl border outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\">\n            <table className=\"w-full min-w-[40rem] text-sm\">\n              <thead>\n                <tr className=\"text-muted-foreground border-b text-start text-xs\">\n                  <th scope=\"col\" className=\"px-4 py-2.5 text-start font-medium\">Task</th>\n                  <th scope=\"col\" className=\"px-4 py-2.5 text-start font-medium\">Status</th>\n                  <th scope=\"col\" className=\"px-4 py-2.5 text-start font-medium\">Started</th>\n                  <th scope=\"col\" className=\"px-4 py-2.5 text-end font-medium\">Duration</th>\n                  <th scope=\"col\" className=\"px-4 py-2.5 text-end font-medium\">Tokens</th>\n                  <th scope=\"col\" className=\"px-4 py-2.5 text-end font-medium\">Cost</th>\n                </tr>\n              </thead>\n              <tbody>\n                {visible.map((r) => {\n                  const s = runStatus[r.status]\n                  const Icon = s.icon\n                  const sel = r.id === run?.id\n                  return (\n                    <tr key={r.id} data-selected={sel || undefined} className=\"data-[selected]:bg-accent/60 border-b last:border-0\">\n                      <td className=\"max-w-80 px-4 py-2.5\">\n                        <button type=\"button\" aria-pressed={sel} onClick={() => setRunId(r.id)} className=\"block w-full rounded-sm text-start outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\">\n                          <span className=\"block truncate font-medium\">{r.task}</span>\n                          <span className=\"text-muted-foreground block truncate text-xs\">{r.agent}</span>\n                        </button>\n                      </td>\n                      <td className=\"px-4 py-2.5\">\n                        <span className=\"inline-flex items-center gap-1.5 text-xs whitespace-nowrap\">\n                          <Icon aria-hidden=\"true\" className={cn(\"size-3.5\", s.tone)} />\n                          {s.label}\n                        </span>\n                      </td>\n                      <td className=\"text-muted-foreground px-4 py-2.5 text-xs whitespace-nowrap\">{r.started}</td>\n                      <td className=\"px-4 py-2.5 text-end tabular-nums\">{seconds(r.duration)}</td>\n                      <td className=\"px-4 py-2.5 text-end tabular-nums\">{nf.format(r.tokens)}</td>\n                      <td className=\"px-4 py-2.5 text-end tabular-nums\">{r.cost ?? \"—\"}</td>\n                    </tr>\n                  )\n                })}\n                {!visible.length ? (\n                  <tr>\n                    <td colSpan={6} className=\"text-muted-foreground px-4 py-8 text-center text-sm\">\n                      No runs match.\n                    </td>\n                  </tr>\n                ) : null}\n              </tbody>\n            </table>\n          </div>\n          {run ? <Trace key={run.id} run={run} locale={locale} /> : null}\n        </div>\n      </StateView>\n    </section>\n  )\n}\n\nexport { Agent3, type Agent3Props, type Agent3Run, type Agent3Span, type Agent3SpanKind }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/agent-3/agent-3.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "ai",
      "agent",
      "observability",
      "trace",
      "waterfall",
      "spans",
      "runs",
      "llm",
      "tokens",
      "cost",
      "debugging"
    ],
    "version": "1.0.0",
    "updated": "2026-10-17",
    "tests": true,
    "ai": {
      "summary": "Agent observability. <Agent3 runs={[{ id, agent, task, status: 'success' | 'error' | 'running', started, duration (ms), tokens, cost, spans: [{ id, name, kind: 'llm' | 'tool' | 'retrieval' | 'step', start (ms from run start), duration (ms), depth, status, tokens, input, output, error }] }]} />. Map OpenTelemetry GenAI spans, Langfuse or AI SDK telemetry onto spans; a failed span is selected first.",
      "whenToUse": [
        "Debugging what an agent did and where time and tokens went",
        "An admin page of recent agent runs"
      ],
      "whenNotToUse": [
        "Watching one run live (use agent-1)",
        "General request tracing for web services (use log-stream or a data-table)"
      ],
      "composesWith": [
        "agent-1",
        "agent-2",
        "app-shell-1"
      ],
      "a11y": [
        {
          "keys": "Tab",
          "action": "Each run's task is a toggle button; each span is a button that opens its details"
        },
        {
          "keys": "Screen readers",
          "action": "Each span names its kind, start, duration and whether it failed, since the bars are visual only"
        },
        {
          "keys": "Arrow keys in the table region",
          "action": "Scroll it sideways on narrow screens"
        }
      ],
      "customization": [
        "title, defaultRunId, locale",
        "Bars use insetInlineStart, so the waterfall mirrors in right-to-left"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "agent-3-loading",
        "trigger": "state=\"loading\"",
        "note": "A skeleton table and trace; search and the status filter are disabled."
      },
      {
        "id": "empty",
        "title": "No runs yet",
        "example": "agent-3-empty",
        "trigger": "state=\"empty\" (or runs={[]})",
        "note": "Explains that runs and traces appear once agents work; a filter with no matches says so inside the table instead."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "agent-3-error",
        "trigger": "state=\"error\"",
        "note": "An alert with Try again (onRetry)."
      }
    ],
    "examples": [
      "agent-3-demo",
      "agent-3-failed",
      "agent-3-loading",
      "agent-3-empty",
      "agent-3-error"
    ],
    "url": "https://ui.ballmac.com/components/agent-3"
  },
  "categories": [
    "blocks",
    "agents"
  ],
  "type": "registry:block"
}