Skip to content

feat(ToolInvocation): tarjeta plegable para tool calls de IA + ToolInvocationGroup - #43

Merged
A-PachecoT merged 2 commits into
mainfrom
feat/tool-invocation
Oct 13, 2026
Merged

A-PachecoT merged 2 commits into
mainfrom
feat/tool-invocation

Conversation

@A-PachecoT

Copy link
Copy Markdown
Contributor

Qué es

ToolInvocation: tarjeta plegable que muestra qué hizo una llamada a tool dentro de un turno de IA (nombre legible, resumen de una línea, pill de estado y, plegados, input y output). ToolInvocationGroup pliega varias de un mismo turno detrás de «AI ran N actions». Pedido de André (2026-10-12) para montarlo en Fovente bajo los mensajes de IA con metadata.ai_trace.tool_calls.

API

<ToolInvocationGroup labels={{ title: (n) => `La IA hizo ${n} acciones` }} summary="Tarifario · Calculadora">
  <ToolInvocation
    name="Consultar tarifario"                  // ReactNode, legible
    summary="Consultó tarifario MSL · Ningbo"   // una línea en el header (se trunca)
    state="output-available"                    // input-streaming | input-available | output-available | output-error
    input={{ carrier: "MSL", origin: "Ningbo" }} // objeto plano → lista clave/valor; anidado → JSON
    output='{"success": true, "rate": 95}'      // texto | string JSON (se formatea; si está truncado con «…» queda como texto) | objeto | ReactNode
    errorText="…"                               // con state="output-error": bloque role="alert"
    defaultOpen={false}                         // plegado por default; también open/onOpenChange
    icon={<FileSearch className="size-3.5" />}  // opcional, default una llave
    density="default"                           // | "compact"
    labels={{ completed: "Listo", input: "Entrada" }} // todos los strings, defaults en inglés
  />
</ToolInvocationGroup>

También se exportan ToolInvocationStatus, ToolValue, defaultToolInvocationLabels, defaultToolInvocationGroupLabels, toolInvocationVariants, toolInvocationStatusVariants y los tipos.

Lo que mapea de Fovente: cada entrada de ai_trace.tool_calls es {tool, args, result_preview, refused_by?} (backend/src/services/tools/base.py::build_trace_entry), y result_preview es ToolResult.to_json() truncado a 200 chars. Eso queda como name/input={args}/output={result_preview}; un preview truncado no rompe nada, se muestra como texto. El nombre legible y el resumen los pone quien consume.

Referencia de mercado

Vercel AI Elements «Tool», https://elements.ai-sdk.dev/components/tool (fetcheado el 2026-10-12): Tool/ToolHeader/ToolContent/ToolInput/ToolOutput, con estados input-streaming(Pending) · input-available(Running) · output-available(Completed) · output-error(Error). Lo que cambia acá:

  • un solo componente con props en vez de cinco subcomponentes (en Fovente se monta desde datos, no se compone a mano);
  • está plegado por default (AI Elements abre las que terminaron), porque bajo un mensaje es ruido para quien opera;
  • el resumen en el header y el grupo por turno no existen en AI Elements;
  • no se implementan los estados de aprobación (approval-*, output-denied) porque hoy no los necesitamos.

Diseño, a11y y tokens

  • Solo tokens: --chat-card, --chat-border, --chat-muted, --cf-code-bg y --status-{info,success,error}. No hay hex. Lo revisé en light y en dark.
  • El header es un <button> (Radix Collapsible) con aria-expanded/aria-controls, y se abre y cierra con Enter/Space. El estado se lee como texto, no solo por el color.
  • El objetivo táctil mide al menos 44 px. Si la tarjeta mide menos de 20rem (container query), la pill queda en solo ícono y el texto sigue disponible para lectores de pantalla.
  • Los radios van explícitos en px: rounded-xl cambia según la app (en Storybook vale 32px).
  • La animación de altura es la de Collapsible, y el spin y el pulse llevan motion-safe.

Story (Messaging/ToolInvocation)

default (plegado) · open · running · streaming · error-state · truncated-preview (la forma real de prod) · all-states · localized (labels en español) · compact · group (un turno con 3 acciones, 1 con error) · mobile-baseline (375 px).

Lo capturé en un Storybook estático local a 720 px en light y dark, y a 375 px en light y dark:

  • group (dark): el header dice «La IA hizo 3 acciones», con la línea de resumen y la pill «1 error». Debajo, con un borde a la izquierda, van 3 tarjetas: ícono de color, nombre en negrita, resumen gris y la pill verde «Completed» o roja «Error».
  • all-states: las cuatro pills (gris Pending, azul Running que gira, verde Completed, roja Error) se leen bien en los dos temas.
  • truncated-preview (light): el input aparece como name cotizaciones y el output es un bloque mono con el JSON truncado que termina en «…».
  • mobile-baseline (375 px): no hay scroll horizontal (scrollWidth igual al viewport), los nombres largos se truncan y la pill queda en solo ícono.

Verificación

  • npx vitest run src/__tests__/components/ToolInvocation.test.tsx termina con exit 0: 9 tests, que cubren el plegado por default, el teclado, la lista clave/valor, el JSON formateado contra el truncado, el error con role=alert, los labels, el ReactNode y el grupo (conteo, errores, título localizado).
  • npm test da 947 verdes y 3 rojos. Los 3 rojos son los mismos en origin/main antes de este PR (ChatInput › should render send button, chat-sim session-lifecycle, chat-sim types-contract.typecheck) y no tienen relación con este cambio.
  • tsc --noEmit da los mismos 3 errores de base de main (ShaderHeroCanvas y los 2 imports a products/… de chat-sim), ninguno nuevo.
  • npm run build:iife termina con exit 0 y npx storybook build también.

…n turno de IA + ToolInvocationGroup

Referencia: Vercel AI Elements «Tool» (https://elements.ai-sdk.dev/components/tool), adaptado a
@cofoundy/ui: tokens (light/dark), un componente por props, plegado por default, botón Radix con
aria-expanded, labels sobreescribibles con defaults en inglés. Acepta la forma real de Fovente
(ai_trace.tool_calls[]: tool/args/result_preview, este último JSON truncado a 200 chars).

Co-Authored-By: Claude <noreply@anthropic.com>
@A-PachecoT
A-PachecoT enabled auto-merge (squash) October 13, 2026 04:28
@A-PachecoT
A-PachecoT merged commit 8e4f349 into main Oct 13, 2026
1 check passed
@A-PachecoT
A-PachecoT deleted the feat/tool-invocation branch October 13, 2026 04:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant