Agent-Klasse und Lebenszyklus
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
- Zustand halten:
AgentStateenthältmessages/tools/model/thinkingLevel/isStreaming/streamingMessage/pendingToolCalls/errorMessage, beim Konstruieren durchcreateMutableAgentStateinitialisiert. Siehepackages/agent/src/agent.ts:158-188. - Warteschlangenverwaltung:
steeringQueueundfollowUpQueuejeweils einePendingMessageQueue, mode ist"one-at-a-time"oder"all". Siehepackages/agent/src/agent.ts:113-144undpackages/agent/src/agent.ts:200-280. - Lebenszyklus-Hooks:
beforeToolCall/afterToolCallwerden beim Konstruieren gespeichert, increateLoopConfiggepackt und an die untere Schleife übergeben. Siehepackages/agent/src/agent.ts:410-436. - Laufzeit-Wrapper:
runWithLifecycleerzeugt einenAbortController, setztisStreaming, fängt Exceptions überhandleRunFailureab und ruft infinallyfinishRunauf. Siehepackages/agent/src/agent.ts:438-486. - Drei Eingänge:
promptnimmt neue Eingaben,continuesetzt vom Ende aus fort,steer/followUpreihen ein. Siehepackages/agent/src/agent.ts:312-353undpackages/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
packages/agent/src/agent.ts:113-144—PendingMessageQueue:enqueue/drain/clear,drainholt bei"all"alle, sonst nur den ersten.packages/agent/src/agent.ts:158-207—class AgentFelder und Konstruktor, defaultstreamFnzeigt aufstreamSimple,toolExecutiondefault"parallel".packages/agent/src/agent.ts:312-323—prompt-Überladungen: unterstütztAgentMessage,AgentMessage[],string + images, intern übernormalizePromptInput→runPromptMessages.packages/agent/src/agent.ts:355-372—normalizePromptInput: String+Bild wird zu einer user-AgentMessagemittimestamp.packages/agent/src/agent.ts:374-400—runPromptMessages/runContinuationrufenrunAgentLoop/runAgentLoopContinueauf, beide inrunWithLifecycleeingewickelt.packages/agent/src/agent.ts:410-436—createLoopConfig: baut aus InstanzfeldernAgentLoopConfig,getSteeringMessages/getFollowUpMessagesschließen überdrainder Warteschlange.packages/agent/src/agent.ts:438-486—runWithLifecycle+handleRunFailure+finishRun.
Der Konstruktor setzt alle austauschbaren Teile ein, streamFn default zeigt auf streamSimple:
// 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:
// 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:
// 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/continuewerfen direkt, wennactiveRunschon existiert, ohne still einzureihen. Aufrufer soll aufsteer/followUpausweichen, siehepackages/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", siehepackages/agent/src/agent.ts:336-350. - Fehler verliert keine Nachricht:
handleRunFailurewickelt den Fehler als eine assistant-AgentMessagemitstopReason: "aborted"|"error"ein und hängt sie ans Transcript an, UI kann direkt rendern, siehepackages/agent/src/agent.ts:463-478. - reset loescht sauber:
resetleert messages, Streaming-Zustand, pendingToolCalls, errorMessage und beide Warteschlangen, siehepackages/agent/src/agent.ts:301-310. - abort einzelne Quelle:
abort()ruft nuractiveRun.abortController.abort()auf, Schleife und stream lauschen auf dasselbe Signal, kein Multipfad-Koordinationsaufwand, siehepackages/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.