Subagents

Muestra el trabajo que un agente delega en otros agentes, una línea por ejecución. Cada ejecución dice qué está haciendo y se pliega en un resumen breve al terminar. Las ejecuciones en paralelo comparten una cabecera con progreso en vivo, y cada una se puede detener, reintentar y abrir para ver sus llamadas a herramientas y su informe.

Los agentes de programación como Claude Code, Codex y Cursor reparten partes de un trabajo entre otros agentes, a menudo varios a la vez. Subagents da a cada una de esas ejecuciones una línea que se lee como alguien delegando: el agente, su tarea en palabras sencillas, una etiqueta en vivo de lo que está haciendo («Reading auth/session.ts»), un temporizador y cuántas herramientas ha usado.

Las ejecuciones empiezan plegadas, para que una respuesta con mucho trabajo siga siendo corta. Cada línea sigue diciendo qué hace su ejecución, y al abrirla se ven las llamadas a herramientas llegando en una ventana corta que sigue el paso más reciente. Cuando una ejecución termina, su línea pasa a un resumen en pasado como “Mapped the token refresh flow · 14 tool uses · 32s”, con la primera frase del informe debajo y el informe completo dentro.

Las ejecuciones en paralelo comparten una cabecera que dice «Running 3 agents» con una barra de progreso y luego «3 agents finished in 48s». Una ejecución puede estar en cola, en marcha, terminada, fallida con su motivo y Retry, detenida o seguir en segundo plano. getSubagentFromPart lee una herramienta del AI SDK cuya salida transmite los mensajes del propio subagente, y los pasos anidados se muestran con Tool Calls y el informe con Markdown.

  1. Obtén HextaUI Pro

    Un solo pago desbloquea todos los bloques, incluidos los futuros. Ver precios.

  2. Añade el registro Pro a components.json

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

    Crea un token en tu página de cuenta y colócalo en .env.local como HEXTAUI_PRO_TOKEN.

  4. Añade el bloque

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

Con el AI SDK

Convierte el subagente en una herramienta cuyo execute es un generador asíncrono que entrega los mensajes de UI del subagente, para que cada uno llegue como salida preliminar. getSubagentFromPart convierte esa parte en una ejecución, las partes de herramientas anidadas en pasos de Tool Calls y el texto tras la última llamada en el informe. Deja la herramienta en el servidor y el renderizado en un componente de cliente.

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

Ejecuciones en paralelo

Pasa varias ejecuciones a Subagents para tener una cabecera con progreso en vivo y Stop all. Conecta onStop y onRetry a lo que ejecute los agentes, y el botón de la fila cambia entre ambos según el estado de la ejecución.

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

Desde datos guardados

Muestra una ejecución terminada del historial con props sencillas. Una duración guardada sustituye al temporizador en vivo y el informe queda a 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}
    />
  )
}

Anatomía

Las partes que compones, de fuera hacia dentro.

ParteDescripción
SubagentsUn grupo de ejecuciones con una cabecera de resumen, progreso, Stop all y anuncios.
SubagentUna ejecución: marca de estado, agente, tarea, etiqueta en vivo, número de herramientas, temporizador y su botón Stop o Retry.
SubagentMarkLa marca de estado por separado: un orbe mientras trabaja y luego un check, una alerta o un icono de parada.
getSubagentFromPartConvierte una parte de herramienta del AI SDK con mensajes de subagente en streaming en una ejecución.

Subagents

También acepta cualquier prop de section excepto children. Una sola ejecución se muestra sin cabecera.

PropTipoPredeterminado
runsEjecuciones en el orden en que empezaron. Mantienen su sitio al terminar.
SubagentProps[]–
labelDa nombre a la región y a su barra de progreso.
string"Subagents"
durationTotal de segundos guardados, para el historial. Por defecto, el tiempo medido mientras trabajaban las ejecuciones.
number–
defaultOpenEmpieza con las ejecuciones listadas bajo la cabecera.
booleantrue
onStopAllSe llama desde Stop all. Sin ella, Stop all llama al onStop de cada ejecución en marcha.
() => void–

Subagent

También acepta cualquier prop de div excepto children.

PropTipoPredeterminado
idId estable, normalmente el toolCallId.
string–
taskLo que se le pidió al agente, en pocas palabras: «Map the token refresh flow».
string–
statusqueued, running, done, failed o stopped.
SubagentStatus–
agentEl tipo o nombre del agente, como Explore o Plan.
string–
titleSustituye la tarea en la línea mientras la ejecución trabaja.
string–
doneTitleLa línea al terminar. Por defecto, la tarea en pasado cuando su primera palabra es un verbo conocido.
string–
promptLas instrucciones completas, que se muestran arriba al abrirla.
string–
callsLos pasos de la ejecución, mostrados con Tool Calls.
ToolCallProps[]–
activitySustituye la etiqueta en vivo, que si no sale del paso activo más reciente.
string–
resultEl informe final, en Markdown.
string–
resultStreamingEl informe aún está llegando.
boolean–
previewUna línea para la ejecución plegada. Por defecto, la primera frase del informe.
string–
errorPor qué falló la ejecución; se muestra en la línea y completo al abrirla.
string–
toolUsesSustituye el recuento, que por defecto es el número de llamadas.
number–
modelSe muestra como insignia en pantallas más anchas.
string–
durationSegundos guardados, para el historial.
number–
startedAtCuándo empezó una ejecución que ya está trabajando, en milisegundos, para que el temporizador no se reinicie al montarse.
number–
backgroundLa ejecución sigue trabajando mientras continúa el chat. Muestra una insignia Background y se queda plegada.
boolean–
defaultOpenEmpieza la ejecución abierta, con sus llamadas a herramientas y su informe.
booleanfalse
onStopMuestra Stop mientras la ejecución está en cola o en marcha.
() => void–
onRetryMuestra Retry cuando la ejecución ha fallado o se ha detenido.
() => void–

getSubagentFromPart(part, options)

Devuelve una ejecución a partir de una parte de herramienta. Pasa el resultado directamente a Subagent o a las runs de Subagents.

PropTipoPredeterminado
partLa parte de herramienta del subagente. Su salida es un mensaje de UI cuyas partes contienen razonamiento, llamadas a herramientas y texto.
ToolUIPart | DynamicToolUIPart–
options.stoppedEl chat se detuvo, así que una ejecución sin terminar muestra Stopped.
booleanfalse
options.describeToolAñade un tipo, un objetivo o una vista por cada herramienta anidada. Los nombres habituales como readFile, grep y bash se deducen solos.
(part) => Partial<ToolCallProps>–
options.agentNombre del agente. Por defecto, subagent_type de la entrada y, si no, el nombre de la herramienta.
string–
options.modelInsignia del modelo. Por defecto, model de los metadatos del mensaje.
string–
KeyAcción
EnterSpaceAbre o cierra una ejecución, o la lista bajo la cabecera del grupo.
TabPasa de una ejecución a su botón Stop o Retry, y de la cabecera a Stop all.
  • La cabecera de cada ejecución es un botón con aria-expanded cuyo nombre lee el agente, la tarea, la etiqueta en vivo, el número de herramientas y el tiempo.
  • El grupo es una región con nombre y aria-busy mientras los agentes trabajan. Una región en vivo educada anuncia cuando una ejecución vuelve a empezar, termina, falla o se detiene, y después el resumen del grupo. Los pasos de herramientas y los ticks del temporizador no se anuncian.
  • Stop y Retry son botones reales con el nombre de su ejecución, como «Stop Explore: Map the token refresh flow». Cuando un botón desaparece, el foco vuelve a la cabecera de la ejecución.
  • El estado nunca depende solo del color: cada estado tiene su propia marca y su texto. Con movimiento reducido, el orbe se queda quieto y las líneas cambian sin deslizarse.

Construido con

Los componentes gratuitos de HextaUI con los que está hecho Subagents. Cada uno se instala por separado.

Código

6 archivos, añadidos a components/blocks/subagents.