Subagents

Zeig die Arbeit, die ein Agent an andere Agents abgibt, eine Zeile pro Lauf. Jeder Lauf sagt, was er gerade tut, und klappt fertig zu einer kurzen Zusammenfassung ein. Parallele Läufe teilen sich eine Kopfzeile mit Live-Fortschritt, und jeder Lauf lässt sich stoppen, wiederholen und bis zu seinen Tool-Aufrufen und seinem Bericht öffnen.

Coding-Agenten wie Claude Code, Codex und Cursor geben Teile einer Aufgabe an andere Agenten ab, oft an mehrere gleichzeitig. Subagents gibt jedem dieser Läufe eine Zeile, die sich wie Delegieren liest: der Agent, seine Aufgabe in einfachen Worten, ein Live-Label dafür, was er gerade tut („Reading auth/session.ts“), ein Timer und wie viele Tools er benutzt hat.

Läufe starten eingeklappt, damit eine volle Antwort kurz bleibt. Jede Zeile sagt weiter, was ihr Lauf gerade tut, und beim Öffnen streamen die Tool-Aufrufe in ein kurzes Fenster, das dem neuesten Schritt folgt. Ist ein Lauf fertig, wird seine Zeile zu einer Zusammenfassung in der Vergangenheit wie „Mapped the token refresh flow · 14 tool uses · 32s“, darunter der erste Satz seines Berichts und darin der ganze Bericht.

Parallele Läufe teilen sich einen Kopf, der „Running 3 agents“ mit Fortschrittsbalken zeigt und dann „3 agents finished in 48s“. Läufe können wartend, laufend, fertig, mit Grund und Retry fehlgeschlagen, gestoppt oder im Hintergrund weiterlaufend sein. getSubagentFromPart liest ein AI-SDK-Tool, dessen Ausgabe die eigenen Nachrichten des Subagenten streamt, und die verschachtelten Schritte werden mit Tool Calls, der Bericht mit Markdown gerendert.

  1. HextaUI Pro holen

    Eine einmalige Zahlung schaltet jeden Block frei, auch künftige. Preise ansehen.

  2. Die Pro-Registry zu components.json hinzufügen

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  3. Token hinzufügen

    Erstelle auf deiner Kontoseite einen Token und trage ihn in .env.local als HEXTAUI_PRO_TOKEN ein.

  4. Den Block hinzufügen

    pnpm dlx shadcn@latest add @hextaui-pro/subagents

Mit dem AI SDK

Mach den Subagenten zu einem Tool, dessen execute ein asynchroner Generator ist, der die UI-Nachrichten des Subagenten liefert, sodass jede als vorläufige Ausgabe ankommt. getSubagentFromPart macht aus diesem Part einen Lauf, aus verschachtelten Tool-Parts Schritte für Tool Calls und aus dem Text nach dem letzten Tool-Aufruf den Bericht. Halte das Tool auf dem Server und das Rendern in einer Client-Komponente.

import type * as React from "react"

import { execFile } from "node:child_process"
import { readFile } from "node:fs/promises"
import { promisify } from "node:util"
import { useChat } from "@ai-sdk/react"
import {
  isToolUIPart,
  readUIMessageStream,
  stepCountIs,
  tool,
  ToolLoopAgent,
  type UIMessage,
} from "ai"
import { z } from "zod"

import { CodeBlock } from "../code-block/code-block"
import type { SubagentProps } from "@/components/blocks/subagents/subagent"
import { getSubagentFromPart } from "@/components/blocks/subagents/subagent-part"
import { Subagents } from "@/components/blocks/subagents/subagents"

const run = promisify(execFile)

const explorer = new ToolLoopAgent({
  model: "anthropic/claude-haiku-4.5",
  instructions:
    "You explore a codebase to answer one question. Read only what you need, then finish with a short report that names the files.",
  stopWhen: stepCountIs(40),
  tools: {
    grep: tool({
      inputSchema: z.object({ pattern: z.string() }),
      execute: async ({ pattern }) =>
        (await run("rg", ["-n", "--max-count", "20", pattern])).stdout,
    }),
    readFile: tool({
      inputSchema: z.object({ path: z.string() }),
      execute: async ({ path }) => ({ content: await readFile(path, "utf8") }),
    }),
  },
})

export const explore = tool({
  description:
    "Hand a self-contained question about the codebase to an explorer agent. Call it several times in one step to explore in parallel.",
  inputSchema: z.object({
    description: z.string().describe("The task in 3 to 6 words"),
    prompt: z.string().describe("Everything the agent needs to know"),
  }),
  execute: async function* ({ prompt }, { abortSignal }) {
    const result = await explorer.stream({ prompt, abortSignal })
    yield* readUIMessageStream({
      stream: result.toUIMessageStream({
        messageMetadata: () => ({ model: "Haiku 4.5" }),
      }),
    })
  },
  toModelOutput: ({ output }) => {
    const report = output?.parts.findLast((part) => part.type === "text")
    return { type: "text", value: report?.text ?? "The agent found nothing." }
  },
})

function AssistantMessage({
  message,
  streaming,
  onStopAll,
}: {
  message: UIMessage
  streaming: boolean
  onStopAll: () => void
}) {
  const blocks: (React.ReactNode | SubagentProps[])[] = []

  message.parts.forEach((part, index) => {
    if (isToolUIPart(part) && part.type === "tool-explore") {
      const run: SubagentProps = getSubagentFromPart(part, {
        stopped: !streaming,
        describeTool: (child) =>
          child.state === "output-available" && child.type === "tool-readFile"
            ? {
                content: (
                  <CodeBlock
                    code={String((child.output as { content: string }).content)}
                    filename={String((child.input as { path: string }).path)}
                    lineNumbers
                    collapseAfter={10}
                  />
                ),
              }
            : undefined,
      })
      const last = blocks.at(-1)
      if (Array.isArray(last)) last.push(run)
      else blocks.push([run])
    } else if (part.type === "text") {
      blocks.push(<p key={index}>{part.text}</p>)
    }
  })

  return (
    <div className="flex flex-col gap-4">
      {blocks.map((block, index) =>
        Array.isArray(block) ? (
          <Subagents key={block[0].id} runs={block} onStopAll={onStopAll} />
        ) : (
          <div key={index}>{block}</div>
        )
      )}
    </div>
  )
}

export function Chat() {
  const { messages, status, stop } = useChat()
  const busy = status === "submitted" || status === "streaming"

  return messages.map((message, index) =>
    message.role === "assistant" ? (
      <AssistantMessage
        key={message.id}
        message={message}
        streaming={busy && index === messages.length - 1}
        onStopAll={stop}
      />
    ) : null
  )
}

Parallele Läufe

Übergib mehrere Läufe an Subagents für einen gemeinsamen Kopf mit Live-Fortschritt und Stop all. Verbinde onStop und onRetry mit dem, was die Agenten ausführt, und der Button der Zeile wechselt zwischen beiden, wenn sich der Status des Laufs ändert.

"use client"

import * as React from "react"

import type { SubagentProps } from "@/components/blocks/subagents/subagent"
import { Subagents } from "@/components/blocks/subagents/subagents"

const initialRuns: SubagentProps[] = [
  {
    id: "sessions",
    agent: "Explore",
    task: "Trace how sessions are created",
    status: "done",
    duration: 14,
    toolUses: 5,
    preview: "Sessions live in the sid cookie for 7 days.",
  },
  {
    id: "refresh",
    agent: "Explore",
    task: "Map the token refresh flow",
    status: "running",
    calls: [
      { id: "a", kind: "read", status: "done", subject: "middleware.ts" },
      { id: "b", kind: "read", status: "running", subject: "lib/auth/refresh.ts" },
    ],
  },
  {
    id: "changes",
    agent: "General",
    task: "Check recent auth changes",
    status: "failed",
    error: "The model was overloaded and stopped responding.",
  },
]

export function Delegation() {
  const [runs, setRuns] = React.useState(initialRuns)
  const update = (id: string, status: SubagentProps["status"]) =>
    setRuns((current) =>
      current.map((run) =>
        run.id === id ? { ...run, status, error: undefined } : run
      )
    )

  return (
    <Subagents
      runs={runs.map((run) => ({
        ...run,
        onStop: () => update(run.id, "stopped"),
        onRetry: () => update(run.id, "running"),
      }))}
    />
  )
}

Aus gespeicherten Daten

Rendere einen fertigen Lauf aus dem Verlauf mit einfachen Props. Eine gespeicherte Dauer ersetzt den Live-Timer, und der Bericht ist einen Klick entfernt.

import { Subagent } from "@/components/blocks/subagents/subagent"

const report = `Sessions are created by \`createSession()\` in \`lib/auth/session.ts\`.

- The \`sid\` cookie lasts **7 days**.
- The token inside it expires after **15 minutes**, so refresh keeps people signed in.`

export function SessionsRun() {
  return (
    <Subagent
      id="sessions"
      agent="Explore"
      task="Trace how sessions are created"
      model="Haiku 4.5"
      status="done"
      duration={14}
      calls={[
        { id: "1", kind: "search", status: "done", subject: "createSession", meta: "4 results" },
        { id: "2", kind: "read", status: "done", subject: "lib/auth/session.ts" },
        { id: "3", kind: "read", status: "done", subject: "lib/auth/cookies.ts" },
      ]}
      result={report}
    />
  )
}

Aufbau

Die Teile, die du zusammensetzt, von außen nach innen.

PartBeschreibung
SubagentsEine Gruppe von Läufen mit Zusammenfassungskopf, Fortschritt, Stop all und Ansagen.
SubagentEin Lauf: Statuszeichen, Agent, Aufgabe, Live-Label, Tool-Anzahl, Timer und sein Stop- oder Retry-Button.
SubagentMarkDas Statuszeichen allein: ein Orb während des Laufs, danach ein Haken, eine Warnung oder ein Stopp-Symbol.
getSubagentFromPartWandelt einen AI-SDK-Tool-Part mit gestreamten Subagent-Nachrichten in einen Lauf um.

Subagents

Akzeptiert außerdem jedes section-Prop außer children. Ein einzelner Lauf wird ohne Kopf gerendert.

PropTypStandard
runsLäufe in der Reihenfolge, in der sie gestartet wurden. Sie behalten ihren Platz, wenn sie fertig werden.
SubagentProps[]–
labelBenennt die Region und ihren Fortschrittsbalken.
string"Subagents"
durationGespeicherte Gesamtsekunden, für den Verlauf. Standardmäßig die Zeit, die gemessen wurde, während die Läufe arbeiteten.
number–
defaultOpenMit den Läufen unter dem Kopf aufgelistet starten.
booleantrue
onStopAllWird von Stop all aufgerufen. Ohne diese Funktion ruft Stop all das onStop jedes laufenden Laufs auf.
() => void–

Subagent

Akzeptiert außerdem jedes div-Prop außer children.

PropTypStandard
idStabile ID, meist die toolCallId.
string–
taskWas der Agent tun sollte, in wenigen Worten: „Map the token refresh flow“.
string–
statusqueued, running, done, failed oder stopped.
SubagentStatus–
agentTyp oder Name des Agenten, etwa Explore oder Plan.
string–
titleErsetzt die Aufgabe in der Zeile, solange der Lauf arbeitet.
string–
doneTitleDie Zeile, wenn er fertig ist. Standardmäßig die Aufgabe in der Vergangenheit, wenn ihr erstes Wort ein bekanntes Verb ist.
string–
promptDie vollständigen Anweisungen, oben angezeigt, wenn der Lauf geöffnet ist.
string–
callsDie Schritte des Laufs, gerendert mit Tool Calls.
ToolCallProps[]–
activityErsetzt das Live-Label, das sonst vom neuesten aktiven Schritt kommt.
string–
resultDer abschließende Bericht, als Markdown.
string–
resultStreamingDer Bericht kommt noch an.
boolean–
previewEine Zeile für den eingeklappten Lauf. Standardmäßig der erste Satz des Berichts.
string–
errorWarum der Lauf fehlgeschlagen ist, in der Zeile und vollständig beim Öffnen gezeigt.
string–
toolUsesÜberschreibt die Anzahl, die standardmäßig der Zahl der Aufrufe entspricht.
number–
modelWird auf breiteren Bildschirmen als Badge gezeigt.
string–
durationGespeicherte Sekunden, für den Verlauf.
number–
startedAtWann ein bereits arbeitender Lauf gestartet ist, in Millisekunden, damit der Timer beim Mounten nicht neu startet.
number–
backgroundDer Lauf arbeitet weiter, während der Chat weitergeht. Er zeigt ein Background-Badge und bleibt eingeklappt.
boolean–
defaultOpenStartet den Lauf geöffnet, mit seinen Tool-Aufrufen und dem Bericht.
booleanfalse
onStopZeigt Stop, solange der Lauf wartet oder läuft.
() => void–
onRetryZeigt Retry, sobald der Lauf fehlgeschlagen ist oder gestoppt wurde.
() => void–

getSubagentFromPart(part, options)

Gibt einen Lauf aus einem Tool-Part zurück. Übergib das Ergebnis direkt an Subagent oder in die runs von Subagents.

PropTypStandard
partDer Tool-Part des Subagenten. Seine Ausgabe ist eine UI-Nachricht, deren Parts Reasoning, Tool-Aufrufe und Text enthalten.
ToolUIPart | DynamicToolUIPart–
options.stoppedDer Chat wurde gestoppt, daher zeigt ein unfertiger Lauf Stopped.
booleanfalse
options.describeToolErgänzt pro verschachteltem Tool eine Art, ein Ziel oder eine Ansicht. Gängige Namen wie readFile, grep und bash werden für dich erraten.
(part) => Partial<ToolCallProps>–
options.agentName des Agenten. Standardmäßig subagent_type aus der Eingabe, danach der Tool-Name.
string–
options.modelModell-Badge. Standardmäßig model aus den Nachrichten-Metadaten.
string–
TasteAktion
EnterSpaceÖffnet oder schließt einen Lauf oder die Liste unter dem Gruppenkopf.
TabSpringt von einem Lauf zu seinem Stop- oder Retry-Button und vom Kopf zu Stop all.
  • Der Kopf jedes Laufs ist ein Button mit aria-expanded, dessen Name Agent, Aufgabe, Live-Label, Tool-Anzahl und Zeit nennt.
  • Die Gruppe ist eine benannte Region mit aria-busy, solange Agenten arbeiten. Eine höfliche Live-Region sagt an, wenn ein Lauf neu startet, fertig wird, fehlschlägt oder stoppt, und danach die Zusammenfassung der Gruppe. Tool-Schritte und Timer-Ticks bleiben still.
  • Stop und Retry sind echte Buttons, benannt nach ihrem Lauf, etwa „Stop Explore: Map the token refresh flow“. Verschwindet ein Button, springt der Fokus zurück zum Kopf des Laufs.
  • Status ist nie nur Farbe: Jeder Zustand hat sein eigenes Zeichen und eigenen Wortlaut. Bei reduzierter Bewegung bleibt der Orb still, und Zeilen wechseln ohne zu gleiten.

Gebaut mit

Die kostenlosen HextaUI-Komponenten, aus denen Subagents besteht. Jede lässt sich einzeln installieren.

Code

6 Dateien, hinzugefügt zu components/blocks/subagents.