Agent-Hauptschleife: embedded-runner
Verantwortung
embedded-agent-runner ist die Kernschicht, die bei OpenClaw „Modell + Werkzeuge + Sitzungshistorie" zu einer vollständigen Agent-Interaktion zusammenspinnt. Es nimmt einen bereits vorbereiteten Nutzer-Prompt und ruft in einer while (true)-Schleife (loop) wiederholt das Modell auf, parst Werkzeugaufrufe, trägt Werkzeugergebnisse nach, löst bei Bedarf Kontextkomprimierung (compaction) aus — bis das Modell eine Endantwort liefert oder das Budget (budget) erschöpft ist. Diese Schicht spricht nicht direkt mit Kanälen — wenn eine Kanalnachricht über das Gateway hier ankommt, ist nur noch „sessionId + prompt + Konfiguration" übrig.
run.ts ist der Träger dieser Schleife. Es orchestriert nicht nur den Einzelversuch (attempt), sondern entscheidet bei Versuch-Fehlern, ob mit demselben Modell wiederholt (retry) wird, ein Auth-Profil gewechselt, ein Fallback-Modell aktiviert oder nach Komprimierung fortgefahren wird. Der embedded-runner ist also keine dünne Hülle „einmal Modell aufrufen und fertig", sondern ein zustandsbehafteter Retry-Orchestrator.
Designmotivation
Warum die Agent-Hauptschleife als residente while (true) auslegen, statt jeden Modellaufruf der oberen Kanalschicht selbst wiederholen zu lassen? Drei Kerngründe.
Erstens: Der Wiederholungszustand muss über Runden hinweg kumulieren. Derselbe Prompt kann aus vielen Gründen wiederholt werden müssen — Ratenbegrenzung, Idle-Timeout, leere Antwort, unvollständiges Reasoning — und jede Wiederholung hat eine eigene Obergrenze (z. B. MAX_SAME_MODEL_RATE_LIMIT_RETRIES, MAX_EMPTY_ERROR_RETRIES, MAX_MISSING_ASSISTANT_RETRIES). Würde Wiederholung oben angesiedelt, müsste jeder Kanal diese Zustandsmaschine neu implementieren und könnte Profil-Rotation und Fallback-Kette nicht teilen.
Zweitens: Kontextkomprimierung muss mit der Wiederholung koordiniert werden. Wenn der Modellkontext überläuft, wirft der embedded-runner nicht einfach einen Fehler, sondern versucht zuerst eine Komprimierung in-place (overflowCompactionAttempts, maximal 3 Mal) und setzt mit demselben Prompt fort. Diese „Komprimiere-Wiederhole"-Kopplung lässt sich in einer zustandslosen Oberschicht nicht abbilden.
Drittens: Der Kosten-Ausfallschutz muss in der Schleifenschicht verankert sein. idleTimeoutBreakerState ist der für #76293 entworfene Kosten-Leitungsschutz: Bei aufeinanderfolgenden Idle-Timeouts ohne jeglichen Modellfortschritt werden weitere Attempts zwangsweise gestoppt, um unnötige Kosten zu vermeiden. Dieser Breacher-Zustand gehört naturgemäß zur Schleife selbst.
Die Schleife in der embedded-Schicht hat noch einen Nebeneffekt: CLI-Runner und Gateway-Runner teilen dieselbe Wiederholungssemantik, nur der Einstiegspunkt unterscheidet sich.
Schlüsseldateien
run.ts— Hauptschleifendatei, 4200+ Zeilen, enthält diewhile (true)-Turn-Schleife, alle Wiederholungszweige, Komprimierungs-Koordination, Failover-Entscheidungen.run.ts iteration caps:1560-1567— Drei Kern-Obergrenzen:MAX_TIMEOUT_COMPACTION_ATTEMPTS=2,MAX_OVERFLOW_COMPACTION_ATTEMPTS=3,MAX_RUN_LOOP_ITERATIONS.run.ts while(true) Kopf:1885-1921— Schleifenkopf: Überlaufprüfung +runLoopIterations-Inkrement.run.ts Attempt-Dispatch:2044-2231— RuftrunEmbeddedAttemptWithBackendauf, um einen Einzelversuch zu dispatchen.run.ts Idle-Breaker-Schritt:2278-2307— Idle-Timeout-Leitungsschutz-Beurteilung.run.ts Retry-Zweige:3746-3793— Drei Retry-Zweige: reasoning-only / missing-assistant / empty-response.cli-runner.ts runCliAgent:391-401— Dünner CLI-Einstieg, bindet Lifecycle-Generation.cli-runner.ts runPreparedCliAgent:508-528— Bereits vorbereiteter Kontext tritt in die Ausführung ein, hängtbefore_agent_run/llm_input/llm_output/agent_end-Hooks an.session-manager-init.ts prepareSessionManagerForRun:47-117— Normalisiert den Session-File-Header vor dem Eintritt in die Schleife.
Datenfluss
Die Kernschleife des embedded-runner (run.ts:1885) ist eine while (true), die zuerst das Budget prüft:
while (true) {
if (runLoopIterations >= MAX_RUN_LOOP_ITERATIONS) {
const message =
`Exceeded retry limit after ${runLoopIterations} attempts ` +
`(max=${MAX_RUN_LOOP_ITERATIONS}).`;
log.error(
`[run-retry-limit] sessionKey=${params.sessionKey ?? params.sessionId} ` +
`provider=${provider}/${modelId} attempts=${runLoopIterations} ` +
`maxAttempts=${MAX_RUN_LOOP_ITERATIONS}`,
);
const retryLimitDecision = resolveRunFailoverDecision({
stage: "retry_limit",
fallbackConfigured,
failoverReason: lastRetryFailoverReason,
});
return handleRetryLimitExhaustion({ message, decision: retryLimitDecision, ... });
}
runLoopIterations += 1;MAX_RUN_LOOP_ITERATIONS wird von resolveMaxRunRetryIterations(profileCandidates.length, config, agentId) berechnet — je mehr Profil-Kandidaten, je aggressiver die Agent-Konfiguration, desto mehr Iterationen sind erlaubt. Bei Erreichen der Obergrenze wird nicht einfach ein Fehler geworfen, sondern resolveRunFailoverDecision gefragt, ob ein Fallback-Modell übernehmen kann; erst wenn auch Fallback erschöpft ist, endet der Run mit livenessState: "blocked".
Nach bestandenem Budget montiert die Schleife den Prompt für diesen Attempt. Der Prompt wird nicht einfach weitergereicht, sondern mit mehreren „Fortsetzungsanweisungen" kombiniert:
const basePrompt =
nextAttemptPromptOverride ??
(provider === "anthropic" ? scrubAnthropicRefusalMagic(params.prompt) : params.prompt);
nextAttemptPromptOverride = null;
const promptAdditions = [
reasoningOnlyRetryInstruction,
emptyResponseRetryInstruction,
compactionContinuationRetryInstruction,
].filter((value): value is string => typeof value === "string" && value.trim().length > 0);
const prompt =
promptAdditions.length > 0
? `${basePrompt}\n\n${promptAdditions.join("\n\n")}`
: basePrompt;Die drei Additions entsprechen jeweils einem Retry-Szenario: reasoningOnlyRetryInstruction setzt an, wenn das Modell nur Thinking liefert ohne sichtbare Antwort; emptyResponseRetryInstruction fordert bei einer Null-Token-Antwort eine sichtbare Antwort; compactionContinuationRetryInstruction teilt dem Modell nach Komprimierung mit „Fahre aus dem komprimierten Transcript fort, fange nicht von vorne an". Retry ist also kein einfaches Wiederabsenden desselben Prompts, sondern ein kontextbewusstes „gerichtetes Fortsetzen".
Ist der Prompt vorbereitet, dispatcht die Schleife an runEmbeddedAttemptWithBackend (run.ts:2044), den eigentlichen Einstieg in den Einzelversuch (attempt):
const rawAttempt = await runEmbeddedAttemptWithBackend({
sessionId: activeSessionId,
sessionKey: resolvedSessionKey,
promptCacheKey: params.promptCacheKey,
...
sessionFile: activeSessionFile,
workspaceDir: resolvedWorkspace,
cwd: params.cwd,
...
beforeAgentFinalizeRevisionAttempts,
maxBeforeAgentFinalizeRevisions: MAX_BEFORE_AGENT_FINALIZE_REVISIONS,
...
}).catch((err: unknown): never => {
throw postCompactionAbortError ?? err;
}).finally(() => {
clearAttemptTimeoutRelease();
stopLaneProgressHeartbeat();
parentAbortSignal?.removeEventListener?.("abort", relayParentAbort);
if (postCompactionAbortController === attemptAbortController) {
postCompactionAbortController = undefined;
}
});Beachten Sie die drei Aufräumungen in .finally: clearAttemptTimeoutRelease ist der Lane-Timeout-Release-Timer, stopLaneProgressHeartbeat stoppt den Heartbeat, parentAbortSignal wird abgehängt — das hinterlässt einen sauberen Zustand für die nächste Iteration und verhindert Watchdog-Reste vom vorherigen Attempt.
Nach Rückkehr des Attempts folgt zuerst der Kosten-Leitungsschutz (run.ts:2278):
const breakerStep = stepIdleTimeoutBreaker(idleTimeoutBreakerState, {
idleTimedOut,
completedModelProgress: hasCompletedModelProgressForIdleBreaker(attempt),
outputTokens: attemptUsage?.output,
});
if (breakerStep.tripped) {
const breakerMessage =
`Idle-timeout cost-runaway breaker tripped: ` +
`${breakerStep.consecutive} consecutive idle timeouts ` +
`without completed model progress ` +
`(cap=${MAX_CONSECUTIVE_IDLE_TIMEOUTS_BEFORE_OUTPUT}). ` +
`Halting further attempts to bound paid model calls. ` +
`See issue #76293.`;
...
return handleRetryLimitExhaustion({ message: breakerMessage, ... });
}Der Breacher ist eine reine Funktion stepIdleTimeoutBreaker, der Zustand idleTimeoutBreakerState wird außerhalb der Schleife erzeugt und über Attempts hinweg kumuliert — so setzen Profil-Rotation oder Retry mit demselben Modell den Zähler nicht zurück, sondern wirken tatsächlich als „kostenbremse über Attempts hinweg".
Anschließend kommen Retry-Zweige (run.ts:3746), jede Retry-Art hat einen eigenen Zähler:
if (
nextReasoningOnlyRetryInstruction &&
reasoningOnlyRetryAttempts < maxReasoningOnlyRetryAttempts
) {
reasoningOnlyRetryAttempts += 1;
reasoningOnlyRetryInstruction = nextReasoningOnlyRetryInstruction;
log.warn(`reasoning-only assistant turn detected: ... retrying ${reasoningOnlyRetryAttempts}/${maxReasoningOnlyRetryAttempts} ...`);
continue;
}
...
if (
!nextReasoningOnlyRetryInstruction &&
nextEmptyResponseRetryInstruction &&
emptyResponseRetryAttempts < maxEmptyResponseRetryAttempts
) {
emptyResponseRetryAttempts += 1;
emptyResponseRetryInstruction = nextEmptyResponseRetryInstruction;
log.warn(`empty response detected: ... retrying ${emptyResponseRetryAttempts}/${maxEmptyResponseRetryAttempts} ...`);
continue;
}Vorteil dieser Struktur: Wenn eine Retry-Art erschöpft ist, betrifft das nicht die anderen — nach Erschöpfung von reasoning-only kann eine Empty-Response-Wiederholung noch ausgelöst werden und umgekehrt. Alle continue geben die Kontrolle an den Schleifenkopf zurück, sodass Budget-Prüfung und Prompt-Neuaufbau erneut greifen, anstatt die Prüfung zu überspringen.
Grenzen und Fehler
- Schleifengrenze ist keine hartcodierte Konstante:
MAX_RUN_LOOP_ITERATIONSwird vonresolveMaxRunRetryIterations(profileCandidates.length, params.config, sessionAgentId)aufgelöst (run.ts:1562). Das bedeutet: derselbe Agent hat bei unterschiedlicher Anzahl Profil-Kandidaten ein unterschiedliches Retry-Budget — wird ein Fallback-Profil hinzugefügt, weitet sich die Schleifengrenze automatisch aus. - Overflow-Komprimierung hat eigenen Zähler:
MAX_OVERFLOW_COMPACTION_ATTEMPTS=3(run.ts:1561) ist nur für Kontextüberlauf, getrennt von der Timeout-KomprimierungMAX_TIMEOUT_COMPACTION_ATTEMPTS=2, damit eine Fehlerart nicht das Budget der anderen aufbraucht. - Post-Compaction-Schleifenwächter:
createPostCompactionLoopGuard(run.ts:1599) zielt auf #77474 — wenn das Modell nach Komprimierung sofort in eine Werkzeug-Endlosschleife eintritt, bricht der Wächter den aktuellen Attempt ab, anstatt auf Timeout zu warten. - Lane-Timeout-Release-Grace-Timer:
armAttemptTimeoutRelease(run.ts:2034) gibt der Lane bei nicht reagierendem nativen Transport eineEMBEDDED_RUN_LANE_TIMEOUT_GRACE_MS-Frist, bevor sie freigegeben wird, damit ein einzelner schlechter Transport nicht die gesamte Lane-Warteschlange blockiert. - before_agent_run-Hook kann blockieren:
runPreparedCliAgent(cli-runner.ts:508) führt vor dem eigentlichen Eintritt in die Schleife denbefore_agent_run-Hook aus; wählt der Hook „block", startet die Schleife gar nicht — das ist der „Ausführung verweigern"-Ausstieg der Plugin-Schicht. - CLI-Einstieg und Gateway-Einstieg teilen sich die Schleife:
runCliAgent(cli-runner.ts:392) macht nur Lifecycle-Generation-Bindung undbefore_agent_reply-Cron-Hook, die eigentliche Arbeit läuft überrunPreparedCliAgent→executePreparedCliRun→ embedded runner. Daher sind die Wiederholungssemantiken von CLI- und Gateway-Modus konsistent — es gibt kein „CLI wiederholt 3 Mal, Gateway nur 1 Mal".
Zusammenfassung
Der embedded-runner ist eine zustandsbehaftete while (true): Er nimmt nicht an, dass ein einzelner Modellaufruf gelingt, sondern internalisiert „Retry, Komprimierung, Failover, Kosten-Leitungsschutz". Alle Zähler kumulieren über Attempts, alle Retry-Arten haben unabhängige Budgets, alle Fehler haben Ausgänge.
Wie der Einzelversuch (attempt) intern das Modell aufruft, tool_use parst und tool_result nachträgt, siehe Einzelner Attempt: Modellaufruf und Werkzeugpaarung; wie die Sitzungsdatei (session) verwaltet wird und Werkzeuge am SessionManager registriert werden, siehe Sitzungsverwaltung: SessionManager; Werkzeugdefinitionen und -strategien siehe Werkzeugsystem; Streaming-Integration der Provider siehe Provider-Anbindung.
Vergleich mit offiziellen Ressourcen: Agent runtime Doku · README.