Skip to content

Tool-Renderer-Registry

源码版本v0.73.1

pi-web-ui entkoppelt das UI-Rendering von Tool-Aufrufen: Das execute des Tools liegt im AgentTool, das Rendering in einer ToolRenderer-Implementierung; die beiden werden über den Tool-Namen in der toolRenderers-Map gepaart. renderTool ist der einheitliche Einstiegspunkt, der in der Registry nach dem Tool-Namen nachschlägt — bei Treffer wird der Custom-Renderer genutzt, sonst fällt es auf DefaultRenderer zurück. Eingebaute Renderer für bash, extract_document, javascript_repl usw. sind enthalten; der Host kann über registerToolRenderer fachliche Renderer injizieren.

Zuständigkeiten

  1. Globale Registry: Die Map toolRenderers speichert ToolRenderer-Instanzen nach toolName. Siehe packages/web-ui/src/tools/renderer-registry.ts:9-16.
  2. Einheitlicher Einstieg: renderTool(toolName, params, result, isStreaming) schlägt zuerst in der Registry nach; Treffer nutzt sie, sonst Fallback auf DefaultRenderer. Außerdem erzwingt der globale Schalter showJsonMode das Default-JSON-Rendering. Siehe packages/web-ui/src/tools/index.ts:28-44.
  3. Header-Hilfsfunktionen: renderHeader und renderCollapsibleHeader vereinheitlichen Icon, Text und Interaktion für die drei Zustände (inprogress/complete/error). Siehe packages/web-ui/src/tools/renderer-registry.ts:29-130.
  4. Inline-Rendering: ToolMessage.render ruft renderTool und entscheidet nach isCustom, ob eine Karte drumherum gelegt wird. Siehe packages/web-ui/src/components/Messages.ts:258-276.
  5. Eingebaute Renderer: extract-document.ts und javascript-repl.ts registrieren am Dateiende per registerToolRenderer(name, renderer) automatisch. Siehe packages/web-ui/src/tools/extract-document.ts:275 und packages/web-ui/src/tools/javascript-repl.ts:293.

Designmotivation

Warum kommt nicht einfach eine render-Methode auf AgentTool? Weil das execute des Tools auf der pi-agent-core-Ebene definiert ist (Backend-Logik), das Rendering auf der pi-web-ui-Ebene (Frontend-Logik). Die beiden Schichten dürfen nicht gekoppelt werden — dasselbe bash-Tool kann in CLI, TUI und Web genutzt werden; execute wird wiederverwendet, das Rendering wird pro UI neu geschrieben. Das Registry-Muster erlaubt web-ui, für beliebige Tool-Namen einen Renderer zu registrieren, auch wenn das Tool in einem anderen Package liegt.

Warum liefert renderTool {content, isCustom} statt eines TemplateResult? Weil manche Renderer ihr Layout selbst kontrollieren wollen (etwa das artifacts-Tool, das inline auf die Leinwand zeichnet und keine Karten-Umhüllung will): isCustom: true signalisiert ToolMessage, keine Karten-Wrapper drumherum zu legen, sondern nur den content zurückzugeben; isCustom: false wickelt den content in eine border rounded-md-Karte. Diese „Custom-Inhalt vs. Default-Hülle"-Entscheidung gibt dem Renderer einerseits Freiheit und sorgt andererseits dafür, dass die meisten Tools ein einheitliches Aussehen behalten.

Warum liegen in extract-document und javascript-repl der Renderer und das Tool in derselben Datei? Weil sie gepaart sind: Die Form von tool.result.details wird durch das Tool definiert und vom Renderer direkt konsumiert. Die gemeinsame Datei verringert das Risiko von Inkonsistenz; das registerToolRenderer am Dateiende ist „automatische Registrierung" — tools/index.ts löst sie per import "./javascript-repl.js" als Side-Effect aus, der Host muss nicht manuell aufrufen.

Wichtige Dateien

renderTool ist der einheitliche Einstieg, die Logik ist kurz:

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

Die automatische Registrierung am Ende von javascript-repl.ts ist das „import als Registrierung"-Muster:

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

Datenfluss

Der Render-Pfad eines Tool-Aufrufs vom AssistantMessage bis zum zurückgegebenen Renderer-Ergebnis:

Randbedingungen und Fehler

  • Tool ohne registrierten Renderer: renderTool fällt auf DefaultRenderer zurück, der params und result als JSON anzeigt; kein leeres Ergebnis. Siehe packages/web-ui/src/tools/index.ts:39-43.
  • Während Streaming ist result noch nicht da: Der Renderer bekommt result === undefined und zeichnet über renderHeader(state="inprogress", ...) Spinner und Text. Siehe packages/web-ui/src/tools/extract-document.ts:249-264.
  • aborted-Tool-Aufruf: ToolMessage synthetisiert im aborted-Fall ein isError-ToolResultMessage als Platzhalter; der Renderer rendert den Fehlerzustand. Siehe packages/web-ui/src/components/Messages.ts:248-258.
  • CORS-Fehler-Degradation: Wenn bei extract_document der direkte fetch scheitert, isCorsError greift und corsProxyUrl gesetzt ist, wird über den Proxy erneut versucht. Siehe packages/web-ui/src/tools/extract-document.ts:103-135.
  • showJsonMode-Debug-Schalter: Nach setShowJsonMode(true) geht jeder Tool-Aufruf erzwungen über die JSON-Ansicht des DefaultRenderer, was das Debuggen von Tool-Rückgaben erleichtert. Siehe packages/web-ui/src/tools/index.ts:21-23.
  • Renderer mehrfach registriert: registerToolRenderer überschreibt per Map.set ohne Fehler; ein später registrierter Renderer gewinnt. Der Host kann so einen eingebauten Renderer überschreiben.

Zusammenfassung

renderer-registry entkoppelt das execute eines Tools vom Rendering: renderTool schlägt nach toolName in der Map nach, bei Treffer wird der Custom-Renderer genutzt, sonst fällt es auf DefaultRenderer zurück. extract_document und javascript_repl registrieren sich per „import als Registrierung" selbst. Der Render-Einstieg liegt im ToolMessage der Nachrichten-Render-Komponenten, die Verdrahtung der Tool-Instanzen in ChatPanel Top-Level-Element.