Skip to content

Registro de renderers de herramientas

源码版本v0.73.1

pi-web-ui desacopla el render UI de las llamadas a herramientas: el execute se define en AgentTool, el render lo implementa la interfaz ToolRenderer, y se asocian por nombre de herramienta en el Map toolRenderers. renderTool es la entrada unificada: busca por nombre en la registry, y si hay hit usa el renderer custom, si no va a DefaultRenderer. Incluye renderers integrados para bash, extract_document, javascript_repl, etc., y el host puede inyectar renderers de negocio vía registerToolRenderer.

Responsabilidades

  1. Registry global: el Map toolRenderers guarda instancias de ToolRenderer por toolName. Ver packages/web-ui/src/tools/renderer-registry.ts:9-16.
  2. Entrada unificada: renderTool(toolName, params, result, isStreaming) primero consulta la registry, si hay lo usa, si no hace fallback a DefaultRenderer; también soporta showJsonMode como flag global para forzar el render JSON por defecto. Ver packages/web-ui/src/tools/index.ts:28-44.
  3. Helper de cabecera: renderHeader y renderCollapsibleHeader unifican el icono + texto + interacción de plegado en los tres estados (inprogress/complete/error). Ver packages/web-ui/src/tools/renderer-registry.ts:29-130.
  4. Render inline: ToolMessage.render invoca renderTool y, según isCustom, decide si envolver en tarjeta. Ver packages/web-ui/src/components/Messages.ts:258-276.
  5. Renderers integrados: extract-document.ts y javascript-repl.ts se auto-registran al final del archivo con registerToolRenderer(name, renderer). Ver packages/web-ui/src/tools/extract-document.ts:275 y packages/web-ui/src/tools/javascript-repl.ts:293.

Motivación de diseño

¿Por qué no añadir un método render directamente a AgentTool? Porque el execute de la herramienta se define en la capa pi-agent-core, lógica de backend; el render se define en la capa pi-web-ui, lógica de frontend. Las dos capas no se pueden acoplar: la misma herramienta bash se puede usar en CLI, TUI y web, el execute se reutiliza y el render se escribe aparte para cada una. El patrón registry permite que web-ui registre un renderer para cualquier nombre de herramienta, aunque la herramienta viva en otro paquete.

¿Por qué renderTool devuelve {content, isCustom} en vez de un TemplateResult? Porque algunos renderers quieren controlar todo el layout (por ejemplo la herramienta de artifacts se inlinea en el canvas, sin tarjeta); isCustom: true le dice a ToolMessage que no envuelva en wrapper de tarjeta y devuelva el content directo; isCustom: false envuelve en una tarjeta border rounded-md. Esta alternativa "contenido custom + envoltorio por defecto" da libertad al renderer y a la vez mantiene una apariencia uniforme para la mayoría de las herramientas.

¿Por qué extract-document y javascript-repl ponen renderer y tool en el mismo archivo? Porque son pareados: la forma de result.details la define la tool, y el renderer la consume. Mantenerlos en el mismo archivo reduce el riesgo de inconsistencia; el registerToolRenderer al final es "auto-registro": tools/index.ts hace import "./javascript-repl.js" para disparar el efecto secundario y el host no tiene que llamar a nada a mano.

Archivos clave

renderTool es la entrada unificada, con lógica muy corta:

typescript
// packages/web-ui/src/tools/index.ts:28-44
export function renderTool(
    toolName: string,
    params: any | undefined,
    result: ToolResultMessage | undefined,
    isStreaming?: boolean,
): ToolRenderResult {
    if (showJsonMode) {
        return defaultRenderer.render(params, result, isStreaming);
    }

    const renderer = getToolRenderer(toolName);
    if (renderer) {
        return renderer.render(params, result, isStreaming);
    }
    return defaultRenderer.render(params, result, isStreaming);
}

El auto-registro al final de javascript-repl.ts es el patrón "import es registro":

typescript
// packages/web-ui/src/tools/javascript-repl.ts:292-293
// Auto-register the renderer
registerToolRenderer(javascriptReplTool.name, javascriptReplRenderer);

Flujo de datos

Ruta de render de una llamada a herramienta, desde AssistantMessage hasta que el renderer devuelve:

Límites y fallos

  • Herramienta sin renderer registrado: renderTool cae a DefaultRenderer, que muestra params y result como JSON, sin dejar la UI en blanco. Ver packages/web-ui/src/tools/index.ts:39-43.
  • En streaming aún no llega result: el renderer recibe result === undefined y muestra renderHeader(state="inprogress", ...) con spinner + texto. Ver packages/web-ui/src/tools/extract-document.ts:249-264.
  • Llamada a herramienta abortada: ToolMessage sintetiza un ToolResultMessage isError placeholder en modo aborted, y el renderer lo muestra como error. Ver packages/web-ui/src/components/Messages.ts:248-258.
  • Degradación ante CORS: si extract_document falla en fetch directo y isCorsError lo detecta y hay corsProxyUrl configurado, reintenta por proxy. Ver packages/web-ui/src/tools/extract-document.ts:103-135.
  • Toggle de depuración showJsonMode: con setShowJsonMode(true), todas las herramientas se fuerzan al JSON view de DefaultRenderer, útil para inspeccionar resultados. Ver packages/web-ui/src/tools/index.ts:21-23.
  • Registro duplicado de renderer: registerToolRenderer usa Map.set y sobrescribe; el último gana, sin error. El host puede inyectar un renderer custom para sobrescribir el integrado.

Resumen

renderer-registry desacopla execute y render de la herramienta: renderTool consulta el Map por toolName, si hay hit usa el renderer custom, si no va a DefaultRenderer. extract_document y javascript_repl se auto-registran con el patrón "import es registro". La entrada de render en componentes de render de mensajes ToolMessage, y el ensamblaje de instancias de herramienta en elemento top-level ChatPanel.