Skip to content

Typvertrag

源码版本v0.73.1

packages/agent/src/types.ts ist der gesamte Typvertrag, den pi-agent-core nach außen freigibt. Es definiert das StreamFn des Schleifeneingangs, den ToolExecutionMode der Werkzeugverteilung, das AgentLoopConfig zum Zusammenbauen der Schleife, das AgentState für Laufzeit-Zustand, das AgentTool der Werkzeugprotokolle, das AgentContext für Kontext-Snapshots, das AgentEvent des Event-Streams und die erweiterbare Nachrichten-Union AgentMessage. Diese Schicht ist absichtlich nur interface/type ohne Laufzeit-Code, damit AgentSession/Agent/runLoop von Abstraktionen abhängen statt von Konkretem.

Verantwortung

  1. Stream-Funktion-Vertrag: StreamFn übernimmt direkt die Signatur von streamSimple, verlangt von der Implementierung, bei Request-/Modell-Fehlern nicht zu werfen, sondern den Fehler im error-Event und stopReason: "error" im zurückgegebenen Stream zu codieren, siehe packages/agent/src/types.ts:15-26.
  2. Werkzeugausführungsmodus: ToolExecutionMode = "sequential" | "parallel", erscheint sowohl in AgentLoopConfig.toolExecution als auch in AgentTool.executionMode (pro Werkzeug überschreibbar), siehe packages/agent/src/types.ts:28-36.
  3. Schleifenkonfiguration: AgentLoopConfig extends SimpleStreamOptions, enthält model/convertToLlm/transformContext/getApiKey/shouldStopAfterTurn/getSteeringMessages/getFollowUpMessages/toolExecution/beforeToolCall/afterToolCall. Siehe packages/agent/src/types.ts:115-248.
  4. Laufzeit-Zustand: AgentState deklariert tools und messages als setter/getter, erlaubt der Implementierung, das Top-Level-Array zu kopieren; isStreaming/streamingMessage/pendingToolCalls/errorMessage sind alle readonly. Siehe packages/agent/src/types.ts:288-313.
  5. Werkzeugprotokoll: AgentTool<TParameters, TDetails> erbt von pi-ais Tool, fügt label/prepareArguments/execute/executionMode hinzu; execute nimmt vier Parameter: toolCallId/params/signal/onUpdate. Siehe packages/agent/src/types.ts:332-355.
  6. Event-Union: AgentEvent ist eine discriminated union aus 9 Event-Typen, organisiert nach den vier Lebenszyklus-Schichten agent/turn/message/tool execution. Siehe packages/agent/src/types.ts:374-389.

Entwurfsmotivation

Warum ist AgentMessage ein Message | CustomAgentMessages[keyof CustomAgentMessages]? Weil ein Coding-Assistent in das Transcript "UI-spezifische Nachrichten" (artifact, notification, Gedanken-Zusammenfassung) einmischen will, die nicht an den LLM gehen dürfen. CustomAgentMessages ist standardmäßig ein leeres interface, die Applikationsschicht erweitert es per declaration merging um eigene Rollen, convertToLlm ist dafür zuständig, diese zu filtern oder in user/assistant/toolResult umzuwandeln, die der LLM versteht. Dieses Design erlaubt es, den Transcript-Typ zur Compile-Zeit zu erweitern, statt zu any zu degenerieren.

Warum betont der Hook-Vertrag immer wieder "darf nicht werfen"? Weil runLoop ein single-threaded Event-Stream ist und ein throw in irgendeinem Callback die Schleife abbricht, ohne eine normale agent_end-Event-Sequenz zu erzeugen - die UI bleibt bei isStreaming=true hängen. Deshalb sagen die Docs von convertToLlm/transformContext/getApiKey/shouldStopAfterTurn/getSteeringMessages/getFollowUpMessages ausdrücklich "must not throw or reject, gibt einen fallback zurück". Hook-Würfe werden an der Aufrufstelle per try/catch aufgefangen und zu error result konvertiert, aber die Callbacks der Schleife selbst können nicht abgesichert werden - das ist die harte Bedingung des Typvertrags.

Warum hat BeforeToolCallResult nur block/reason als optionale Felder und kein vollständiges Ergebnis? Weil die before-Phase nur entscheidet, "darf es laufen", nicht "was das Ergebnis ist". Ein geblocktes Werkzeug bekommt von der Schleife ein error result, der reason landet im content. So muss der before-Hook die genaue result-Form des Werkzeugs nicht kennen, erst der after-Hook (AfterToolCallResult) darf feldweise content/details/isError/terminate überschreiben, weil dann das result schon existiert.

Warum nutzt execute von AgentTool die Generics TParameters/TDetails? TParameters ist auf TSchema (typebox) beschränkt, Static<TParameters> ist der zur Compile-Zeit abgeleitete Parametertyp; TDetails ist die vom Werkzeug selbst definierte details-Form. So sind die params in der Werkzeugimplementierung stark typisiert, und die Schleifenseite speichert einheitlich als AgentTool<any>, sowohl typsicher als auch in einer heterogenen Liste verwendbar.

Wichtige Dateien

Kernfelder von AgentLoopConfig, convertToLlm ist Pflicht, der Rest optional:

typescript
// packages/agent/src/types.ts:115-144
export interface AgentLoopConfig extends SimpleStreamOptions {
  model: Model<any>;
  /** Converts AgentMessage[] to LLM-compatible Message[] before each LLM call. */
  convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
  // ...
}

AgentMessage wird per declaration merging erweitert, default ist es die Message-Union von pi-ai:

typescript
// packages/agent/src/types.ts:271-280
export interface CustomAgentMessages {
  // Empty by default - apps extend via declaration merging
}

export type AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessages];

AgentTool erbt von pi-ais Tool und fügt das Ausführungsprotokoll hinzu:

typescript
// packages/agent/src/types.ts:332-346
export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any> extends Tool<TParameters> {
  label: string;
  prepareArguments?: (args: unknown) => Static<TParameters>;
  execute: (
    toolCallId: string,
    params: Static<TParameters>,
    signal?: AbortSignal,
    onUpdate?: AgentToolUpdateCallback<TDetails>,
  ) => Promise<AgentToolResult<TDetails>>;
  // ...
}

Datenfluss

Typenfluss über die Schichten:

Grenzen und Fehler

  • StreamFn-Fehler-Vertrag: Bei Request-/Modell-Fehlern muss der Fehler im Stream codiert werden, nicht geworfen werden, sonst bricht runLoops for await ab ohne agent_end, siehe packages/agent/src/types.ts:16-23.
  • convertToLlm-Fehler-Vertrag: Darf nicht werfen, muss ein fallback Message[] zurückgeben; der Typ-Kommentar sagt ausdrücklich, dass Werfen die Schleife ohne Event-Sequenz abbricht, siehe packages/agent/src/types.ts:119-127.
  • AgentState setter kopiert: tools/messages sind setter/getter, die Implementierung kann das Top-Level-Array kopieren (die default-Implementierung createMutableAgentState macht das), um direktes Mutaten von außen zu verhindern, siehe packages/agent/src/types.ts:295-300.
  • afterToolCall ohne tiefes merge: Feldweises Überschreiben von content/details ist "ganz ersetzen", kein deep merge; isError/terminate werden einzeln ersetzt. Die Doku sagt ausdrücklich "No deep merge is performed", siehe packages/agent/src/types.ts:52-73.
  • AgentContext ist ein Snapshot: Agent kopiert in createContextSnapshot mit messages.slice()/tools.slice() und übergibt das an die Schleife, die Schleife mutiert diese Kopie intern (push partial, push toolResult) ohne Agent._state zu beeinflussen, siehe packages/agent/src/agent.ts:402-408.

Zusammenfassung

types.ts ist die Vertragsschicht von pi-agent-core: StreamFn nagelt die Form der Stream-Funktion fest, AgentLoopConfig baut die Schleife zusammen, AgentState gibt readonly Laufzeit-Zustand frei, AgentTool definiert das Werkzeugprotokoll, AgentMessage wird per declaration merging erweitert, AgentEvent ist die Union aus 9 Events. Der Vertrag betont immer wieder "darf nicht werfen", um die Event-Sequenz von runLoop intakt zu halten. Wie diese Typen konsumiert werden, siehe Agent-Klasse und Lebenszyklus und Doppelte while-Hauptschleife; die konkreten Hook-Aufrufe in der Werkzeugausführung siehe Werkzeugausführung sequential/parallel.