Subagents

Affichez le travail qu’un agent confie à d’autres agents, une ligne par exécution. Chaque exécution dit ce qu’elle fait et se replie en un court résumé une fois terminée. Les exécutions parallèles partagent un en-tête avec la progression en direct, et chacune peut être arrêtée, relancée et ouverte sur ses appels d’outils et son rapport.

Les agents de code comme Claude Code, Codex et Cursor confient des parties d’un travail à d’autres agents, souvent plusieurs à la fois. Subagents donne à chacune de ces exécutions une ligne qui se lit comme une délégation : l’agent, sa tâche en mots simples, un libellé en direct de ce qu’il fait (« Reading auth/session.ts »), un chronomètre et le nombre d’outils utilisés.

Les exécutions démarrent repliées, pour qu’une réponse chargée reste courte. Chaque ligne continue de dire ce que fait son exécution, et l’ouvrir montre les appels d’outils qui arrivent dans une courte fenêtre qui suit la dernière étape. Quand une exécution se termine, sa ligne devient un résumé au passé comme « Mapped the token refresh flow · 14 tool uses · 32s », avec la première phrase du rapport en dessous et le rapport complet à l’intérieur.

Les exécutions en parallèle partagent un en-tête qui affiche « Running 3 agents » avec une barre de progression, puis « 3 agents finished in 48s ». Une exécution peut être en attente, en cours, terminée, en échec avec sa raison et Retry, arrêtée ou laissée en arrière-plan. getSubagentFromPart lit un outil de l’AI SDK dont la sortie diffuse les propres messages du sous-agent ; les étapes imbriquées s’affichent avec Tool Calls et le rapport avec Markdown.

  1. Procurez-vous HextaUI Pro

    Un seul paiement débloque tous les blocs, y compris les prochains. Voir les tarifs.

  2. Ajouter le registre Pro à components.json

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  3. Ajouter votre token

    Créez un token sur votre page de compte et placez-le dans .env.local sous le nom HEXTAUI_PRO_TOKEN.

  4. Ajouter le block

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

Avec l’AI SDK

Faites du sous-agent un outil dont execute est un générateur asynchrone qui renvoie les messages d’UI du sous-agent, pour que chacun arrive comme une sortie préliminaire. getSubagentFromPart transforme cette part en exécution, les parts d’outils imbriquées en étapes Tool Calls et le texte après le dernier appel d’outil en rapport. Gardez l’outil sur le serveur et l’affichage dans un composant client.

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
  )
}

Exécutions en parallèle

Passez plusieurs exécutions à Subagents pour un seul en-tête avec progression en direct et Stop all. Reliez onStop et onRetry à ce qui exécute les agents : le bouton de la ligne passe de l’un à l’autre selon l’état de l’exécution.

"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"),
      }))}
    />
  )
}

À partir de données enregistrées

Affichez une exécution terminée depuis l’historique avec de simples props. Une durée enregistrée remplace le chronomètre en direct, et le rapport reste à un clic.

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}
    />
  )
}

Anatomie

Les parties à composer, de l’extérieur vers l’intérieur.

PartieDescription
SubagentsUn groupe d’exécutions avec un en-tête récapitulatif, la progression, Stop all et des annonces.
SubagentUne exécution : marque d’état, agent, tâche, libellé en direct, nombre d’outils, chronomètre et son bouton Stop ou Retry.
SubagentMarkLa marque d’état seule : un orbe pendant l’exécution, puis une coche, une alerte ou une icône d’arrêt.
getSubagentFromPartTransforme une part d’outil de l’AI SDK avec des messages de sous-agent en streaming en exécution.

Subagents

Accepte aussi toutes les props de section sauf children. Une seule exécution s’affiche sans en-tête.

PropTypePar défaut
runsExécutions dans l’ordre de leur lancement. Elles gardent leur place en se terminant.
SubagentProps[]–
labelNomme la région et sa barre de progression.
string"Subagents"
durationTotal de secondes enregistré, pour l’historique. Par défaut le temps mesuré pendant que les exécutions travaillaient.
number–
defaultOpenDémarrer avec les exécutions listées sous l’en-tête.
booleantrue
onStopAllAppelée par Stop all. Sans elle, Stop all appelle le onStop de chaque exécution en cours.
() => void–

Subagent

Accepte aussi toutes les props de div sauf children.

PropTypePar défaut
idIdentifiant stable, généralement le toolCallId.
string–
taskCe qu’on a demandé à l’agent, en quelques mots : « Map the token refresh flow ».
string–
statusqueued, running, done, failed ou stopped.
SubagentStatus–
agentLe type ou le nom de l’agent, comme Explore ou Plan.
string–
titleRemplace la tâche sur la ligne pendant que l’exécution travaille.
string–
doneTitleLa ligne une fois terminée. Par défaut la tâche au passé quand son premier mot est un verbe connu.
string–
promptLes instructions complètes, affichées en haut à l’ouverture.
string–
callsLes étapes de l’exécution, affichées avec Tool Calls.
ToolCallProps[]–
activityRemplace le libellé en direct, qui vient sinon de l’étape active la plus récente.
string–
resultLe rapport final, en Markdown.
string–
resultStreamingLe rapport est encore en cours d’arrivée.
boolean–
previewUne ligne pour l’exécution repliée. Par défaut la première phrase du rapport.
string–
errorPourquoi l’exécution a échoué, affiché sur la ligne et en entier à l’ouverture.
string–
toolUsesRemplace le compte, qui vaut par défaut le nombre d’appels.
number–
modelAffiché comme badge sur les écrans plus larges.
string–
durationSecondes enregistrées, pour l’historique.
number–
startedAtQuand une exécution déjà en cours a démarré, en millisecondes, pour que le chronomètre ne reparte pas de zéro au montage.
number–
backgroundL’exécution continue pendant que la conversation avance. Elle affiche un badge Background et reste repliée.
boolean–
defaultOpenDémarre l’exécution ouverte sur ses appels d’outils et son rapport.
booleanfalse
onStopAffiche Stop tant que l’exécution est en attente ou en cours.
() => void–
onRetryAffiche Retry quand l’exécution a échoué ou a été arrêtée.
() => void–

getSubagentFromPart(part, options)

Renvoie une exécution à partir d’une part d’outil. Passez le résultat directement à Subagent ou dans les runs de Subagents.

PropTypePar défaut
partLa part d’outil du sous-agent. Sa sortie est un message d’UI dont les parts contiennent le raisonnement, les appels d’outils et le texte.
ToolUIPart | DynamicToolUIPart–
options.stoppedLe chat s’est arrêté, donc une exécution inachevée affiche Stopped.
booleanfalse
options.describeToolAjoute un type, une cible ou une vue pour chaque outil imbriqué. Les noms courants comme readFile, grep et bash sont devinés pour vous.
(part) => Partial<ToolCallProps>–
options.agentNom de l’agent. Par défaut subagent_type dans l’entrée, puis le nom de l’outil.
string–
options.modelBadge du modèle. Par défaut model dans les métadonnées du message.
string–
ToucheAction
EnterSpaceOuvre ou ferme une exécution, ou la liste sous l’en-tête du groupe.
TabPasse d’une exécution à son bouton Stop ou Retry, et de l’en-tête à Stop all.
  • L’en-tête de chaque exécution est un bouton avec aria-expanded dont le nom donne l’agent, la tâche, le libellé en direct, le nombre d’outils et la durée.
  • Le groupe est une région nommée avec aria-busy pendant que les agents travaillent. Une région live polie annonce quand une exécution redémarre, se termine, échoue ou s’arrête, puis le résumé du groupe. Les étapes d’outils et les tics du chronomètre restent silencieux.
  • Stop et Retry sont de vrais boutons nommés d’après leur exécution, comme « Stop Explore: Map the token refresh flow ». Quand un bouton disparaît, le focus revient à l’en-tête de l’exécution.
  • L’état ne repose jamais sur la seule couleur : chaque état a sa propre marque et son libellé. Avec les animations réduites, l’orbe reste immobile et les lignes changent sans glisser.

Construit avec

Les composants HextaUI gratuits dont Subagents est constitué. Chacun s’installe séparément.

Code

6 fichiers, ajoutés à components/blocks/subagents.