Skip to content

Agent-Klasse und Lebenszyklus

源码版本v0.73.1

Agent ist der zustandsbehaftete Einstiegspunkt von @mariozechner/pi-agent-core. Darunter liegt die nackte runAgentLoop-Schleife, darüber eine Orchestrierungsschicht wie AgentSession. Dazwischen hält Agent: Transcript und Werkzeugtabelle, verwaltet den activeRun-Lebenszyklus, pflegt zwei Warteschlangen (steering/followUp), übersetzt AgentOptions in AgentLoopConfig, wickelt den eigentlichen Schleifenaufruf in runWithLifecycle ein und leitet Events über subscribe an externe Listener weiter. Man kann es als "Zustandshülle über der Schleife" (state shell) verstehen - die Schleife selbst speichert keinen Zustand.

Verantwortung

  1. Zustand halten: AgentState enthält messages/tools/model/thinkingLevel/isStreaming/streamingMessage/pendingToolCalls/errorMessage, beim Konstruieren durch createMutableAgentState initialisiert. Siehe packages/agent/src/agent.ts:158-188.
  2. Warteschlangenverwaltung: steeringQueue und followUpQueue jeweils eine PendingMessageQueue, mode ist "one-at-a-time" oder "all". Siehe packages/agent/src/agent.ts:113-144 und packages/agent/src/agent.ts:200-280.
  3. Lebenszyklus-Hooks: beforeToolCall/afterToolCall werden beim Konstruieren gespeichert, in createLoopConfig gepackt und an die untere Schleife übergeben. Siehe packages/agent/src/agent.ts:410-436.
  4. Laufzeit-Wrapper: runWithLifecycle erzeugt einen AbortController, setzt isStreaming, fängt Exceptions über handleRunFailure ab und ruft in finally finishRun auf. Siehe packages/agent/src/agent.ts:438-486.
  5. Drei Eingänge: prompt nimmt neue Eingaben, continue setzt vom Ende aus fort, steer/followUp reihen ein. Siehe packages/agent/src/agent.ts:312-353 und packages/agent/src/agent.ts:252-259.

Entwurfsmotivation

Warum nicht die obere Schicht direkt runAgentLoop aufrufen lassen? Weil die Schleife selbst rein funktional ist - man gibt einen context hinein, sie läuft durch und gibt newMessages zurück, ohne etwas zu speichern. Ein echtes Coding-Assistent braucht aber "transcript über Runden hinweg, weiteres Tippen während des Streamings muss in eine Warteschlange, abort braucht eine einzige Signalquelle, Fehler müssen errorMessage in den state schreiben". Das alles in runLoop zu stopfen, würde die Schleife schwer und nicht wiederverwendbar machen; oben aufzuhängen würde in print/rpc/TUI drei Modi dupliziert. Agent wird als einzige Zustandshülle herausgezogen - die Schleife läuft nur, der Zustand wird nur gespeichert.

Zwei Warteschlangen statt einer, weil steering (Einwurf in dieser Runde, der vor der nächsten Assistant-Antwort injiziert werden soll) und followUp (eine weitere Runde starten, wenn eigentlich Schluss wäre) unterschiedliche Semantiken haben - steering wird im inneren while über getSteeringMessages gezogen, followUp am Ende des äußeren while über getFollowUpMessages, siehe Doppelte while-Hauptschleife. Der "all"/"one-at-a-time"-Mode der PendingMessageQueue lässt den Aufrufer entscheiden, ob auf einmal geleert oder nur ein Eintrag geholt wird.

Wichtige Dateien

Der Konstruktor setzt alle austauschbaren Teile ein, streamFn default zeigt auf streamSimple:

typescript
// packages/agent/src/agent.ts:190-207
constructor(options: AgentOptions = {}) {
  this._state = createMutableAgentState(options.initialState);
  this.convertToLlm = options.convertToLlm ?? defaultConvertToLlm;
  this.transformContext = options.transformContext;
  this.streamFn = options.streamFn ?? streamSimple;
  // ... getApiKey / onPayload / onResponse / beforeToolCall / afterToolCall ...
  this.steeringQueue = new PendingMessageQueue(options.steeringMode ?? "one-at-a-time");
  this.followUpQueue = new PendingMessageQueue(options.followUpMode ?? "one-at-a-time");
  this.transport = options.transport ?? "auto";
  this.toolExecution = options.toolExecution ?? "parallel";
}

createLoopConfig schließt drain der Warteschlange zu asynchronen Funktionen, die Schleife zieht über diese beiden Callbacks Nachrichten:

typescript
// packages/agent/src/agent.ts:427-434
getSteeringMessages: async () => {
  if (skipInitialSteeringPoll) {
    skipInitialSteeringPoll = false;
    return [];
  }
  return this.steeringQueue.drain();
},
getFollowUpMessages: async () => this.followUpQueue.drain(),

runWithLifecycle ist die gemeinsame Hülle aller Lauf-Eingänge, bei Fehler geht es über handleRunFailure, der Fehler in den state geschrieben und agent_end gesendet:

typescript
// packages/agent/src/agent.ts:454-461
try {
  await executor(abortController.signal);
} catch (error) {
  await this.handleRunFailure(error, abortController.signal.aborted);
} finally {
  this.finishRun();
}

Datenfluss

Lebenszyklus von prompt(text):

Grenzen und Fehler

  • Gleichzeitiges prompt abgelehnt: prompt/continue werfen direkt, wenn activeRun schon existiert, ohne still einzureihen. Aufrufer soll auf steer/followUp ausweichen, siehe packages/agent/src/agent.ts:316-320.
  • continue Rollenprüfung: Letzte Nachricht ist assistant, dann werden zuerst steering/followUp-Warteschlangen verbraucht; beide leer wirft "Cannot continue from message role: assistant", siehe packages/agent/src/agent.ts:336-350.
  • Fehler verliert keine Nachricht: handleRunFailure wickelt den Fehler als eine assistant-AgentMessage mit stopReason: "aborted"|"error" ein und hängt sie ans Transcript an, UI kann direkt rendern, siehe packages/agent/src/agent.ts:463-478.
  • reset loescht sauber: reset leert messages, Streaming-Zustand, pendingToolCalls, errorMessage und beide Warteschlangen, siehe packages/agent/src/agent.ts:301-310.
  • abort einzelne Quelle: abort() ruft nur activeRun.abortController.abort() auf, Schleife und stream lauschen auf dasselbe Signal, kein Multipfad-Koordinationsaufwand, siehe packages/agent/src/agent.ts:287-290.

Zusammenfassung

Agent wickelt runAgentLoop als zustandsbehaftetes Objekt ein: hält Transcript, verwaltet zwei Warteschlangen, setzt Lebenszyklus-Hooks ein, vereinheitlicht das abort-Signal. Nach oben hin ist das die Orchestrierungsschicht AgentSession, nach unten hin die Doppelte while-Hauptschleife. Wie die Lebenszyklus-Hooks vom Werkzeugausführungspfad konsumiert werden, siehe Werkzeugausführung sequential/parallel; die Zustandsfelder und die Form von AgentMessage/AgentEvent siehe Typvertrag.