Der autonome Completion-Loop — Personal Agent vs. Hermes, Codex CLI, OpenHands, SWE-agent
Fokussierter Mechanik-Vergleich einer einzigen Frage: Woher weiß ein unbeaufsichtigter Agent, dass er fertig ist — und was hält ihn auf, wenn er es nie wird? Verglichen werden Terminationssignal, Verifikation, Loop-Driver, Schranken und ihre Einheit, Stall-Detection, Failure-Handoff und Kontextführung über Schritte hinweg.
Quellen: geklonte Repos
NousResearch/hermes-agent(hermes_cli/goals.py,agent/conversation_loop.py,agent/verification_evidence.py,agent/tool_guardrails.py,hermes_cli/kanban_db.py),openai/codex(codex-rs/core/src/session/turn.rs,codex-rs/ext/goal/,codex-rs/hooks/src/events/stop.rs),All-Hands-AI/OpenHands@4470813(src/types/agent-server/core/,src/hooks/mutation/,src/api/agent-server-adapter.ts),SWE-agent/SWE-agent(sweagent/agent/agents.py,sweagent/agent/models.py,sweagent/agent/reviewer.py) — und PA (agent/goal_toolset.py,worker/goal_workflow.py,worker/goal_activities.py,realtime/producers/inline.py,agent/kanban_context.py,agent/interaction_toolset.py,frontend/src/stores/chat.ts).Scope-Vorbehalt OpenHands: der geklonte HEAD ist nicht mehr der Python-Agent, sondern
@openhands/agent-canvas(React/TS, 4.py-Dateien, keine davon ein Agent —docs/architecture.md:8-24). Der beobachtbare Kontrakt (jedes Event, jeder Status, jeder Default) ist dort vollständig; die Mechanik (Controller, Stuck-Detector-Muster) lebt inOpenHands/software-agent-sdk. Wo der Mechanismus fehlt, steht das hier als Lücke und nicht als Behauptung.
Legende: ✓ = vorhanden / first-class · ◑ = teilweise / nur per Opt-in / nicht first-class · ✗ = nicht vorhanden · — = nicht zutreffend.
1. TL;DR — die sieben Lehren
- Die EINHEIT einer Schranke ist wichtiger als ihr Wert. PAs
GOAL_MAX_ITERATIONS = 12(worker/goal_activities.py:24) zählt Turns. Der beobachtete Runaway spielte sich innerhalb eines Turns ab — 19 Tool-Calls, 3 Verifier-Calls auf dem stärksten Modell, ein Sub-Agent und eine Websuche, während der Zähler auf 1 stand (tests/api/test_goal_toolset.py:139-171). Eine Schranke, die die falsche Achse zählt, ist schlimmer als keine, weil sie sich wie Sicherheit liest. Hermes’IterationBudget(agent/iteration_budget.py:32) zählt API-Calls, seinGoalState.turns_used(hermes_cli/goals.py:1741) zählt Judge-Turns — beide Achsen sind dort getrennt gedeckelt. - Evidenz gehört aus dem Speicher neu gelesen, nicht aus dem Kontext
geglaubt. Hermes’
agent/verification_evidence.py:629liest exit codes aus einer SQLite-Ledger, die der Terminal-Tool als Seiteneffekt schreibt (tools/terminal_tool.py:3245) — inklusive der Regelif state["last_edit_at"] > evidence["created_at"]: status = "stale"(:688). Codex reicht dem Stop-Hook dentranscript_pathstatt des Transkripts (codex-rs/hooks/src/events/stop.rs:113). SWE-agent rechnet den Diff im Moment des Submits frisch aus dem Repo (tools/submit/bin/submit:10). PA liest beicomplete_goalbewusst die livectx.messages(agent/goal_toolset.py:338) — richtig für den Bug, den es behebt, aber es verliert alles, was die Kompaktierung wegsummiert hat (§9). - Der billige deterministische Check gehört VOR den LLM-Judge. Hermes
formuliert das explizit: „Quality gates run BEFORE the LLM judge: a failing
gate is deterministic evidence the goal is not done, so the judge call is
skipped entirely" (
hermes_cli/goals.py:1745). PA hat die Reihenfolge, aber nicht die Determinismus-Eigenschaft: die billige Stufe ist selbst ein LLM-Probe (worker/goal_activities.py:52-99). - Prosa ist keine Schranke. Codex’
continuation.md:43-49verlangt, dass derselbe Blocker über drei aufeinanderfolgende Goal-Turns auftreten muss, bevorblockedgesetzt wird — und nirgends im Code existiert ein Goal-Turn-Zähler, gegen den man das prüfen könnte (codex-rs/ext/goal/src/spec.rs:77-79sind die einzigen Fundstellen von „consecutive"). Die Guardian-Circuit-Breaker im gleichen Repo setzt seine 3-Strikes-Regel dagegen in Code durch (codex-rs/core/src/guardian/mod.rs:106-137) — das Haus kann es, hat es für Task-Konvergenz nur nicht getan. - Stall-Detection ist die am häufigsten fehlende Zutat. Von fünf Systemen
hat genau eines echte Detektoren (Hermes:
agent/tool_guardrails.py:276Result-Hash-Wiederholung +hermes_cli/goals.py:1498Workspace-Fingerprint), eines hat sie als Always-on-Feature ohne auffindbare Logik (OpenHandsstuck_detection: true,src/api/agent-server-adapter.ts:1114), und drei — Codex, SWE-agent, PA — haben gar nichts. - Zwei geschachtelte Loops brauchen zwei Terminationssignale UND zwei
Cancel-Pfade. OpenHands’
stopGoalbricht die äußere Schleife ab, lässt den laufenden Turn aber „deliberately" weiterlaufen (src/hooks/mutation/conversation-mutation-utils.ts:95-99) — jeder Caller muss zusätzlich pausieren. PA hat die gleiche Formklasse als echten Defekt: dasask_user-Tool existiert und ist in Goal-Turns aktiv (agent/interaction_toolset.py:23,assembler/assembler.py:216-219), aber der Goal-Loop schaut nie hin und plant sofort den nächsten Turn (§8.3). - Das Endzustands-Vokabular muss bis zum Nutzer durchkommen. OpenHands
trennt sauber
complete/capped/interrupted(src/types/agent-server/core/events/conversation-state-event.ts:87) und rendert die Judge-Notizmissingbeim Abbruch ausgeklappt (src/components/features/chat/goal-status-content.tsx:29-44). PA hat das Vokabular serverseitig (done/capped/failed/budget/paused/cleared,worker/goal_workflow.py:48-66) und wirft es im Frontend weg: das Banner überlebt nuractiveundpaused(frontend/src/stores/chat.ts:986-1000).
2. Die fünf Systeme auf einen Blick
| Dimension | Personal Agent | Hermes Agent | Codex CLI | OpenHands | SWE-agent |
|---|---|---|---|---|---|
| Terminationssignal (innen) | kein — „keine Tool-Calls" endet nur den Turn | strukturell (conversation_loop.py:7398) |
strukturell (turn.rs:389) |
Tool FinishAction (base/action.ts:13) |
stdout-Sentinel (tools.py:372) |
| Terminationssignal (außen) | Tool complete_goal + Verifier (goal_toolset.py:290) |
LLM-Judge-Verdikt (goals.py:1822) |
Tool update_goal (spec.rs:60) |
Judge-Verdikt (conversation-state-event.ts:67) |
— (ein Lauf, kein äußerer Loop) |
| Verifikation deterministisch | ✗ | ✓ (Evidence-Ledger + Shell-Gates) | ◑ (nur user-supplied Stop-Hook) | ◑ (nur user-supplied Stop-Hook) | ◑ (Self-Review-Gate, kein Test) |
| Loop-Driver | ✓ durable (Temporal) | ✗ in-process (+ durable Kanban) | ✗ in-process, event-driven | ◑ server-side, nicht durable | ✗ in-process while |
| Zähl-Schranke auf der äußeren Schleife | Turns (12) | Judge-Turns (20) | ✗ keine | Audit-Runden (/goal --max N) |
✗ (Attempts im RetryAgent) |
| Geld-Schranke | ◑ Monat/User, per Turn gesampelt | ✗ | ✗ | ✓ max_budget_per_task |
✓ primär ($3/Instance) |
| Stall-Detection | ✗ | ✓ (3 Mechanismen) | ✗ | ◑ (an, Logik extern) | ✗ |
| Failure-Handoff strukturiert | ◑ serverseitig ja, UI nein | ✓ | ✓ | ✓ | ◑ (Exit-Status-Enum + Diff) |
| Kontext über Schritte | volle History, komprimiert | volle History, komprimiert | volle History, komprimiert | volle History, condensed | volle History, kein Summary |
In einem Satz: PA hat als einziges System einen durable Loop und als einziges einen fail-closed Verifier — und als einziges keine einzige Schranke auf der Achse, auf der es tatsächlich weggelaufen ist.
3. Terminationssignal — wie endet ein Lauf?
| Aspekt | PA | Hermes | Codex | OpenHands | SWE-agent |
|—||||||
| „Keine Tool-Calls mehr" beendet den Lauf | ✗ | ✓ (Turn) | ✓ (Turn) | ✗ | ✗ |
| Explizites Completion-Tool | ✓ | ◑ (nur Kanban-Layer) | ✓ | ✓ | ✓ |
| Tool-Call ist ein Antrag, kein Abschluss | ✓ | ✓ (Kanban prüft Artefakte) | ✗ | ◑ (nur wenn Critic an) | ✓ (Stage-Gate) |
| Zweiter, modell-unabhängiger Terminationspfad | ✓ (_auto_readiness_check) | ◑ (Gates) | ✗ | ✗ | ✗ |
| Sentinel-String statt Tool-Identität | ✗ | ✗ | ✗ | ✗ | ✓ |
| Stop-Gates können den Abschluss vetoen | ✗ | ✓ (3 Gates) | ✓ (Stop-Hook) | ✓ (Stop-Hook) | ✓ (Review-Stage) |
PA. complete_goal(summary) ist das einzige agent-seitige Signal — und
ausdrücklich ein Antrag: „The goal is NOT automatically considered complete. An
independent VERIFIER evaluates your work" (agent/goal_toolset.py:294-297). Erst
flip_goal_done setzt run_config.goal.status = "done" (:146-160, gerufen bei
:371-373). Wichtig zur zeitlichen Einordnung: der Flip passiert mitten im
laufenden Turn in einer eigenen Session; derselbe run_goal_turn-Aufruf
liest den Status danach neu (worker/goal_activities.py:216-219), berechnet
cont = False (:242) und feuert den Nudge (:243-245) — es wird kein
zusätzlicher Turn verbraucht. Strukturell endet „keine Tool-Calls" bei PA nur den
Turn; der Workflow plant sofort den nächsten (worker/goal_workflow.py:42-56).
Der zweite Pfad ist PA-spezifisch und in keinem der Vergleichssysteme zu finden:
_auto_readiness_check läuft nach jedem Turn, in dem der Agent nicht
abgeschlossen hat, prüft mit einem billigen Probe über die persistierte Evidenz
und flippt bei Bestätigung durch den strikten Verifier trotzdem auf done
(worker/goal_activities.py:238-241, :52-117). Das fängt ein Ziel ab, das
verifizierbar erreicht, aber nie deklariert wurde.
SWE-agent ist der Gegenpol und die Warnung: die Terminierung ist ein
Substring-Match auf Tool-stdout. ToolHandler.check_for_submission_cmd scannt
jede Observation nach <<SWE_AGENT_SUBMISSION>> (sweagent/tools/tools.py:372-376);
der Tool-Name wird nie geprüft. Jedes Kommando, dessen Ausgabe diesen String
enthält, beendet den Lauf — und umgekehrt endet er nicht, wenn
/root/model.patch nicht lesbar ist (sweagent/agent/agents.py:886-890).
Codex trennt sauber: der innere Loop braucht gar kein Finish-Tool
(„If the model sends only an assistant message, we record it … and consider the
turn complete", codex-rs/core/src/session/turn.rs:138-151), der äußere ist ein
update_goal{status}-Tool (codex-rs/ext/goal/src/spec.rs:60-93) — dessen
Handler jedoch null prüft und nur eine SQLite-Zeile schreibt
(codex-rs/ext/goal/src/tool.rs:221-280).
Hermes stapelt drei Loops mit drei Signalen: strukturell innen
(agent/conversation_loop.py:7398-7403), LLM-Judge im /goal-Loop
(hermes_cli/goals.py:1822-1832), und im durablen Kanban-Layer ein echtes
Completion-Tool, das eine Text-Antwort als protocol_violation behandelt und
bis zu zweimal synthetisch nachfasst (agent/kanban_stop.py:69-101).
4. Verifikation — wird „ich bin fertig" geglaubt?
| Aspekt | PA | Hermes | Codex | OpenHands | SWE-agent |
|—||||||
| Selbstauskunft wird ungeprüft geglaubt | ✗ | ◑ (ohne Contract ja) | ✓ | ✓ (Critic default aus) | ◑ (Stage-Gate, kein Inhalt) |
| Separater LLM-Verifier | ✓ | ✓ | ✗ | ◑ (opt-in) | ◑ (nur RetryAgent) |
| Verifier auf anderem Modell | ◑ (nur Auto-Pfad, bedingt) | ✓ (auxiliary.goal_judge) | — | ◑ | ◑ |
| Evidenz aus dem Speicher re-gelesen | ◑ (nur Auto-Pfad) | ✓ (SQLite-Ledger) | ✓ (Stop-Hook transcript_path) | ◑ (event_ids) | ✓ (git diff --cached) |
| Evidenz = gerenderte Tool-Traces statt Prosa | ✓ | ✓ | ✗ | ✗ | ✓ (Diff) |
| Reasoning/Thinking bewusst ausgeschlossen | ✓ | — | ✗ | ✗ | — |
| Stale-Regel (Edit invalidiert grünen Lauf) | ✗ | ✓ | ✗ | ✗ | ✗ |
| Tests/Lint/Assertions laufen wirklich | ✗ | ✓ (/goal gate add) | ◑ (nur eigener Stop-Hook) | ◑ (nur eigener Stop-Hook) | ✗ |
| Strukturierter Verdict-Typ | ✗ (Substring) | ◑ (JSON + Parse-Counter) | — | ✓ (GoalVerdict) | ✗ (letzte Zahl der letzten Zeile) |
| Kritik fließt als Arbeitsauftrag zurück | ✓ | ✓ | — | ✓ (missing) | ✗ (Attempts sind i.i.d.) |
4.1 PA: gut geerdet, schwach unabhängig
Die Evidenz-Aufbereitung ist PAs stärkster Baustein. _msg_evidence
(agent/goal_toolset.py:59-82) rendert Prosa plus echte
[called tool(args)]- und [result tool: …]-Zeilen und schließt die
Thinking-Parts explizit aus („self-justification, not evidence"); der
Verifier-Prompt hämmert „judge from the tool actions/results, not the agent’s
prose claims" (:244-251). Das Budget ist newest-first über 40.000 Zeichen und
wird danach re-chronologisiert (:118-143), damit bei Truncation die jüngste
echte Arbeit überlebt. Die Log-Zeile tool_lines=work.count("[called ")
(:353-359) ist die billige Kanarie, die den Blindheits-Bug überhaupt sichtbar
gemacht hat.
Drei Schwächen bleiben:
- Die „unabhängige" Prüfung läuft auf demselben Modell.
complete_goalbekommtparent_modeldurchgereicht (assembler/assembler.py:431) — anderer Kontext und andere Instruktionen, aber nicht anderes Urteil. Der Auto-Pfad löst zwarPurpose.STRICT_VERIFIERauf, fällt aber auf das Probe-Modell zurück, wenn nichts konfiguriert ist:verifier_model = strong.model if (strong and strong.model is not None) else cheap.model(worker/goal_activities.py:104). Dann ist die zweistufige Prüfung ein Modell, zweimal gefragt — schwächer alscomplete_goals Variante. - Verdikt-Parsing per Substring auf der ersten Zeile
(
agent/goal_toolset.py:268). Bemerkenswert: die gröbere Kopie im Codebase macht es richtig —run_workernutzt ein pydantic-_Verdictmitachieved: bool+next_step: strundoutput_type=_Verdict(agent/kanban_context.py:101-104). - Kein einziger deterministischer Check. Keine Tests, kein Diff-Review, keine Assertion, kein Schema. Zwei LLM-Meinungen sind die ganze Prüfung.
Die Fail-Richtungen sind dagegen sauber durchdacht und asymmetrisch: kein
Verifier-Modell → fail open (agent/goal_toolset.py:241), Verifier-Fehler →
fail closed („keep working", :265-267), Probe-Fehler → fail closed, damit
ein Flake nie ein Ziel beenden kann (:219-223). Jede Richtung ist im Kommentar
begründet und per Test festgenagelt (tests/api/test_goal_toolset.py:52). Hermes
entscheidet hier anders herum — „A broken judge must not wedge progress; the turn
budget is the backstop" (hermes_cli/goals.py:18-19), jeder Transportfehler gibt
("continue", …, True) zurück (:1112) — und braucht deshalb zwei zusätzliche
Zähler gegen den festgefahrenen Judge (:71, :76).
4.2 Hermes: der Evidence-Ledger ist die beste Idee im Feld
agent/verification_stop.py ist ausdrücklich „policy-only: it never runs checks
itself". Es liest verification_status(session_id, cwd) aus einer persistenten
SQLite-DB unter ~/.hermes/verification_evidence.db
(agent/verification_evidence.py:59-60, Tabellen :112-138), die als
Seiteneffekt der Tool-Ausführung befüllt wird: jedes Shell-Kommando ruft
record_terminal_result(command, cwd, session_id, exit_code, output)
(tools/terminal_tool.py:3245-3253), jeder File-Edit ruft mark_workspace_edited
(tools/file_tools.py:1986). Die Invalidierungsregel macht den Ledger
vertrauenswürdig: if state["last_edit_at"] > evidence["created_at"]: status = "stale"
(:688-691) — „ich habe die Tests schon laufen lassen" ist nach dem nächsten Edit
wertlos. Doku-Pfade (.md, LICENSE) werden gefiltert, damit ein README-Edit nie
auslöst (verification_stop.py:24-72). Kosten auf dem Happy Path: null
zusätzliche LLM-Calls.
Der LLM-Judge daneben ist bewusst amnesisch und entsprechend schwach: er sieht
Zieltext (≤2000 Zeichen), die letzte Assistant-Nachricht auf
_JUDGE_RESPONSE_SNIPPET_CHARS = 4000 gekürzt (hermes_cli/goals.py:63), die
Live-Prozessliste und die Uhrzeit — nie das Transkript, nie den Diff, nie den
Ledger (:1070-1094). Ohne Completion-Contract akzeptiert der Prompt schlicht
„The response explicitly confirms the goal was completed" (:156); mit
Contract verlangt er „a command result, file contents excerpt, test/benchmark
output — not a claim like ‘done’ or ‘all tests pass’ without evidence" (:238-251).
4.3 Codex und OpenHands: die Prüfung ist optional oder Prosa
Codex’ gesamte Verifikation ist der Text in
codex-rs/ext/goal/templates/goals/continuation.md:30-42 — ein wirklich guter
12-Zeilen-„Completion audit" („Treat uncertain or indirect evidence as not
achieved"), den kein Codepfad prüft. Der einzige echte Hebel ist der
Stop-Hook, der StopOutcome{should_block, block_reason, continuation_fragments}
zurückgeben und den Abschluss vetoen darf
(codex-rs/hooks/src/events/stop.rs:63-69, Wiedereintritt bei
codex-rs/core/src/session/turn.rs:470-501) — Hooks sind Stable und default-on
(codex-rs/features/src/lib.rs:1055-1059), aber es wird keiner mitgeliefert.
OpenHands hat den reichsten Kontrakt und die schwächsten Defaults:
critic_enabled = false, critic_mode = finish_and_message,
critic_threshold = 0.6, enable_iterative_refinement = false
(src/mocks/settings-handlers.ts:174-300, src/services/settings.ts:46-48). Ist
der Critic an, reitet sein CriticResult{score, message, metadata} auf dem
Action-Event selbst mit („Optional critic evaluation of this action and preceding
history", src/types/agent-server/core/events/action-event.ts:63-66) und rendert
direkt unter der Finish-Nachricht
(.../finish-event-message.tsx:29-31). Er sagt aber eine
Nutzerzufriedenheits-Wahrscheinlichkeit voraus, er führt keine Tests aus — seine
Feature-Taxonomie ist verhaltensbasiert („insufficient testing, loop behavior",
src/types/agent-server/core/base/critic.ts:13-25).
4.4 SWE-agent: die billigste gute Idee im ganzen Feld
Das review_on_submit_m-Bundle ersetzt submit: der erste Aufruf berechnet
den Diff frisch (git add -A && git diff --cached), rendert eine Checkliste mit
{{diff}} und {{problem_statement}}, erhöht einen Registry-Zähler und beendet
ohne Sentinel (tools/review_on_submit_m/bin/submit:23-42). Der Lauf geht
also weiter, und das Modell bekommt seinen eigenen Diff als ganz normale
Observation zurück, dazu „re-run your reproduction script, delete it,
git checkout -- any test files you touched, then submit again"
(config/default.yaml:47-63). Erst der zweite submit terminiert. Kosten: ein
Tool-Roundtrip, null zusätzliche Infrastruktur, deterministisch (ein Zähler, kein
Urteil) — und die Evidenz ist aus der Welt neu berechnet, nicht aus dem
Kontext erinnert.
5. Wer treibt die Schleife — und was passiert beim Prozesstod?
| Aspekt | PA | Hermes | Codex | OpenHands | SWE-agent |
|—||||||
| Loop-Konstrukt | durable while im Workflow | selbstfütternde Queue | Idle-Event (Rekursion) | server-side Loop | while in-process |
| Überlebt Prozesstod | ✓ | ✗ (Goal) / ✓ (Kanban) | ✗ | ✗ | ✗ |
| Automatischer Neustart nach Crash | ✓ | ◑ (nur Kanban-Dispatcher) | ✗ | ✗ | ◑ (Instanz-Neustart ab Schritt 0) |
| Turn-Level-Idempotenz | ✗ | — | — | — | — |
| History-Wachstum begrenzt | ✓ (Continue-as-new alle 4) | ✓ (Kompaktierung) | ✓ (Auto-Compaction) | ✓ (Condenser) | ✗ |
PA ist hier konkurrenzlos. GoalWorkflow.run ist ein Temporal-Workflow mit
einer Activity pro Turn (worker/goal_workflow.py:37-67); der Start verweigert
sich ohne Temporal („autonomous goals need the durable backend",
api/routers/chats.py:799-833), Workflow-ID ist goal-{chat_id} mit
TERMINATE_IF_RUNNING (temporal/client.py:54-71). Stirbt der Worker, spielt
Temporal den Workflow woanders wieder ein; die laufende Activity wird bis zu
viermal retried (goal_workflow.py:20-28).
Zwei Einschränkungen, die man kennen muss:
- Der Turn ist nicht idempotent. Ein Retry startet das Modell neu gegen die
persistierte History und kann Seiteneffekte wiederholen. Darunter liegt noch
eine zweite, in der ursprünglichen Analyse fehlende Retry-Ebene:
MAX_TURN_RETRIES = 2überAUTO_RETRY_CATEGORIES = {rate_limit, server_error, network_transient}(agent/retry_backoff.py:24-28), angewandt im Inline-Producer (realtime/producers/inline.py:394-405) — also bis zu drei Modell-Versuche innerhalb eines Activity-Versuchs, unter Temporals vier Activity-Versuchen. Dazu ein eigener_context_overflow_retry, der die History trimmt und einmal neu versucht (inline.py:146-176). - Der Startup-Reaper greift dem Worker ins Lenkrad. Goal-Turns werden mit
execution_mode='inline'angelegt (agent/service.py:181), obwohl sie im Worker laufen.reap_orphaned_inline_runsfiltert genau darauf (db/repositories/run_repo.py:116-119) mit der Begründung „inline runs execute in the API process, so a restart orphans them" — für Goal-Turns falsch. Der periodische Watchdog ist harmlos, weilAgentService._run_heartbeatupdated_atvom Worker aus auffrischt (agent/service.py:356-357); der Startup-Sweep aber läuft mitolder_than=Noneund markiert unbedingt jeden laufenden Inline-Run als failed (main.py:216-218) — inklusive einer Meldung „Der Lauf wurde durch einen Neustart unterbrochen" an den Nutzer. Der Worker setzt den Run später selbst auf completed; bleibt ein falscher Fehlerhinweis plus Buchhaltungs-Race.
Hermes’ /goal-Loop hat gar kein Loop-Konstrukt — er ist eine selbstfütternde
Queue: evaluate_after_turn gibt {"should_continue": True, "continuation_prompt": …}
zurück, und der Driver schiebt den Prompt als ganz normale User-Nachricht nach
(cli.py:10855-10861; im Gateway als synthetisches MessageEvent über
_enqueue_fifo, „so any real user message that arrives simultaneously … takes
priority naturally", gateway/run.py:19265-19285). Der GoalState ist zwar
durabel (hermes_cli/goals.py:707-717), der Continuation-Prompt lebte aber nur in
einer In-Memory-Queue — ein Crash hinterlässt ein active-Ziel, das erst wieder
läuft, wenn ein Mensch die Session öffnet. Nur der Kanban-Layer heilt sich selbst:
dispatch_once reklamiert TTL-abgelaufene Claims, Claims ohne Heartbeat und Tasks
mit totem Worker-PID, bevor es neu vergibt (hermes_cli/kanban_db.py:8386-8400,
alle 60 s getriggert von gateway/kanban_watchers.py:970).
Codex ist die eleganteste Nicht-Lösung: kein Loop, sondern Rekursion über den
Event-Bus. Der Turn-Teardown emittiert Thread-Idle
(codex-rs/core/src/tasks/mod.rs:825-832), die Goal-Extension reagiert darauf
(codex-rs/ext/goal/src/extension.rs:155-166) und startet über den einen
bewachten Eingang try_start_turn_if_idle (codex-rs/core/src/session/inject.rs:47)
einen neuen Turn — der ablehnt, wenn gerade gearbeitet wird, Plan-Modus aktiv ist
oder ein User-Turn ansteht. Das ist genau die Wächter-Stelle, die PA fehlt (§8.3).
6. Schranken und ihre EINHEIT — die schärfste Achse
Der zentrale Befund dieses Vergleichs. Nicht ob gedeckelt wird, sondern was gezählt wird.
| System | Schranke | Einheit | Default | Greift wo |
|---|---|---|---|---|
| PA | GOAL_MAX_ITERATIONS |
Turns (ganze Agent-Läufe) | 12 | goal_workflow.py:43 + goal_activities.py:242 |
| PA | _MAX_VERIFY_PER_RUN |
Verifier-Calls pro Turn | 2 | goal_toolset.py:33, :305-317 |
| PA | INLINE_REQUEST_LIMIT |
Modell-Requests pro Turn | 400 | inline.py:38 |
| PA | MAX_TURN_RETRIES |
Modell-Versuche pro Activity-Versuch | 2 | retry_backoff.py:28 |
| PA | Temporal maximum_attempts |
Activity-Versuche pro Turn | 4 | goal_workflow.py:24 |
| PA | BudgetRepo |
USD, Monat, pro User, alle Aktivität | konfigurierbar | goal_activities.py:154-162 |
| PA | start_to_close_timeout |
Sekunden pro Activity-Versuch | 900 | goal_workflow.py:23 |
| PA | Stream-/Tool-Idle | Sekunden ohne Event | 180 / 3900 | inline.py:45, :51 |
| PA | APPROVAL_TIMEOUT |
Sekunden Wartezeit pro genehmigungspflichtigem Call | 600 | device_policy.py:31, :78 |
| Hermes | IterationBudget |
API-Calls (refundbar für execute_code) |
90 / 500 / 45–50 | iteration_budget.py:32-49 |
| Hermes | GoalState.turns_used |
Judge-bewertete Turns | 20 | goals.py:50, :1894 |
| Hermes | Guardrail-Zähler | Tool-Calls (sha256 über Name+Args) | warn@2 / block@5 | tool_guardrails.py:72-82 |
| Hermes | loop_caps |
Tool-Calls pro Turn, harte Sperre | 50 web_search / 50 delegate | tool_guardrails.py:135-136 |
| Hermes | consecutive_failures |
Worker-Attempts (Prozess-Spawns) | 2 | kanban_db.py:6738-6742 |
| Hermes | max_runtime_seconds |
Wall-Clock pro Worker-Prozess | konfigurierbar | kanban_db.py:7196-7296 |
| Codex | ThreadGoal.token_budget |
Tokens (input − cached + output) |
None |
accounting.rs:332-338, goals.rs:499 |
| Codex | Guardian-Breaker | Ablehnungen (3 in Folge / 10 von 50) | an | guardian/mod.rs:48-54 |
| OpenHands | max_iterations |
Agent-Steps | 500 | agent-server-adapter.ts:1110-1113 |
| OpenHands | GoalStatus.max_iterations |
Audit-Runden | per /goal --max N |
use-goal-interceptor.ts:11 |
| OpenHands | max_budget_per_task |
USD pro Conversation | konfigurierbar | conversation-state-event.ts:25-49 |
| OpenHands | max_refinement_iterations |
Retry-Versuche nach Critic | 3 | settings-handlers.ts:262-282 |
| SWE-agent | per_instance_cost_limit |
USD pro Instanz | 3.00 | models.py:73-76 |
| SWE-agent | per_instance_call_limit |
API-Calls | 0 = aus | models.py:78 |
| SWE-agent | total_execution_timeout |
Sekunden im Container (ohne LLM-Zeit) | 1800 | tools/tools.py:145-148 |
| SWE-agent | max_requeries |
Parse-Fehler in Folge | 3 | agents.py:158-161 |
6.1 Was daraus folgt
Niemand außer Hermes deckelt Tool-Calls pro Turn. loop_caps (50 web_search,
50 delegate_task, agent/tool_guardrails.py:447) ist die einzige Schranke im
ganzen Feld, die innerhalb eines Turns auf der Tool-Achse hart blockt — und
Hermes’ Kommentar schreibt die Idee explizit Claude Codes Runaway-Loop-Caps zu
(:129-149).
PA hat die Zutaten, aber an der falschen Stelle. Echte Tool-Call-Caps
existieren im Codebase: MAX_TOOL_CALLS = 40 (agent/code_execution/monty.py:36)
und MAX_TOOL_CALLS = 200 (agent/code_execution/agents_script.py:98) — beide
begrenzen aber die sandboxed Script-Executor, nicht einen Agent-Turn. Das Muster
war verfügbar und wurde auf genau die Achse nicht angewandt, die weggelaufen ist.
Eine Korrektur zur eigenen Codebase-Erzählung: Der Kommentar in
worker/goal_activities.py:213-214 behauptet, der Iterations-Cap habe bis zum
Persistieren von g["iterations"] „nichts begrenzt (Zähler stand auf 0)". Das ist
falsch. Der Zähler des Workflows ist workflow-lokal und wurde immer erhöht:
GoalWorkflow.run(self, params, iteration=0), iteration += 1 bei
worker/goal_workflow.py:53, weitergereicht über continue_as_new(args=[params, iteration])
bei :58, getestet bei :43; die Activity bekommt ihn aus params
(goal_activities.py:125) und leitet dieselbe Schranke bei :242 erneut ab.
run_config.goal["iterations"] wird nirgends in die Schleife zurückgelesen —
der Key wird nur geschrieben (goal_activities.py:224, chats.py:816, :874,
agent/tasks/service.py:162) und vom Frontend-Banner gelesen. Der DB-Write ist
Telemetrie, kein Bound. Damit fällt eine der drei erzählten Einheiten-Lücken weg;
die beiden echten bleiben: der Turn-Zähler sieht nicht in den Turn hinein, und
das Geld-Gate wird nur an der Turn-Grenze gesampelt.
Zum Verifier-Cap, präzise: verify_calls[rkey] += 1 steht vor jeder
frühen Rückkehr (agent/goal_toolset.py:305-307 — vor dem chat_id-Parse bei
:319-321 und vor dem „no active goal"-Check bei :330-333). Ein Turn, der zwei
dieser harmlosen Fehler trifft, ist danach für seinen ersten echten
Verifikationsversuch gesperrt. Das Dict ist per run_id gekeyt und wird nie
geleert; per-Turn ist es nur, weil goal_toolset() pro Assembly neu gebaut wird
(assembler/assembler.py:428-431) — der Test fixiert die per-run_id-Semantik
(tests/api/test_goal_toolset.py:174-178), nicht die Lebensdauer.
Zum Geld-Gate, präzise: Der Goal-Loop war der letzte unbeaufsichtigte
Pfad, der das Monats-Gate bekam, nicht der einzige ohne. Dasselbe
BudgetRepo.resolve_limit/spend_this_month-Paar sitzt auch in
workflows/executor.py:232 und agent/code_execution/agents_script.py:196, neben
dem interaktiven Gate in api/routers/runs.py:308-313. Ein Tages-Gate gibt es
nicht: BudgetRepo.spend_today existiert, wird aber nur von admin.py:164 und
usage.py:66 zur Anzeige gelesen.
Die unmodellierte Wall-Clock-Schranke: Läuft ein Goal-Turn im
Security-Modus judge/approve_each (assembler/assembler.py:715-723), pollt
request_tool_approval bis zu APPROVAL_TIMEOUT = 600.0 Sekunden auf eine
menschliche Entscheidung (agent/device_policy.py:31, :78), bevor es
„No approval (timed out)" zurückgibt. Bei einem unbeaufsichtigten Lauf schaut
niemand hin: ein genehmigungspflichtiger Call frisst 600 s des
900-s-Activity-Budgets, zwei sprengen es und lösen einen Temporal-Retry aus,
der wieder wartet. Das ist dieselbe Fehlerklasse wie §8.3 — der Loop nimmt an, ein
Mensch sei anwesend — nur dass sie hier reale Zeit kostet.
7. Stall-Detection — merkt jemand, dass sich nichts bewegt?
| Mechanismus | PA | Hermes | Codex | OpenHands | SWE-agent |
|—||||||
| Wiederholte identische Tool-Calls erkannt | ✗ | ✓ | ✗ | ? (extern) | ✗ |
| Identisches Tool-Ergebnis N-mal erkannt | ✗ | ✓ | ✗ | ? | ✗ |
| „Workspace unverändert" erkannt | ✗ | ✓ | ✗ | ? | ✗ |
| Semantische / Embedding-Ähnlichkeit | ✗ | ✗ | ✗ | ◑ (Critic-Feature) | ✗ |
| Turn-übergreifendes Kreisen erkannt | ✗ | ✗ | ✗ | ? | ✗ |
| Dedizierter Terminalzustand dafür | ✗ | ✗ | ✗ | ✓ (stuck) | ✗ |
| Park-/Wait-Barriere gegen Busy-Work | ✗ | ✓ | ✗ | ✗ | ✗ |
Hermes ist das einzige System mit echter Erkennung, und der _no_progress-Teil
ist das schärfste Signal des ganzen Felds: eine Map von ToolCallSignature
(sha256 aus Tool-Name + kanonisierten Args) auf (sha256 des RESULTATS,
Wiederholungszahl) (agent/tool_guardrails.py:276-507, :225-236). Ein
Read-only-Tool, das N-mal byte-identisch antwortet, ist ein Agent, der sich
dreht — die Meldung sagt es wörtlich: „this read-only call returned the same
result N times" (:337), „this looks like a loop; inspect the error and change
strategy instead of retrying it unchanged" (:392). Zweitens der
Workspace-Fingerprint: vor dem Re-Run eines zuvor gescheiterten Quality-Gates
hasht Hermes git rev-parse HEAD + git status --porcelain; ist der Fingerprint
unverändert, wird das Gate nicht neu gefahren — der gespeicherte Fehlschlag
wird abgespielt und attempts zählt trotzdem hoch, annotiert mit „(workspace
unchanged since last failure — not re-run)" (hermes_cli/goals.py:1498-1518).
Das übersetzt „der Agent hat nichts geändert" in Fortschritt auf dem
Retry-Zähler statt in eine Gratis-Runde. Drittens das wait-Verdikt mit
wait_on_pid/wait_for_seconds (:160-176, :1720-1739) — es parkt den Loop, ohne
einen Turn zu verbrennen, mit der Prompt-Regel „Do NOT pick WAIT just because work
remains — only when re-poking now would be pure busy-work."
Wichtige Ehrlichkeit zur Grenze: alle Guardrail-Zähler sind Exact-Hash und
werden per reset_for_turn an der Turn-Grenze geleert
(agent/tool_guardrails.py:280) — ein Agent, der über Goal-Turns hinweg
oszilliert (edit → test → revert → edit), wird nur vom Turn-Budget und der
Fingerprint-Regel gefangen. Und der Default ist beratend:
hard_stop_enabled: bool = False (:73), out of the box wird dem Agent also
gesagt, dass er kreist, und er darf weiterkreisen.
PA hat nichts davon. Die Suche über agent/, worker/ und realtime/ fördert
nur Nachbarschaftsphänomene zutage: die Byte-identische Tool-Result-Dedup in der
Kompression (agent/compression.py:36, dedup_tool_returns :225-273) loggt
tool_returns_deduped und schlägt keinen Alarm; kanban_heartbeat.py ist ein
Liveness-Puls, kein Fortschrittsmaß; die beiden Watchdogs erkennen einen toten
Producer (main.py:224-245) bzw. Stille (inline.py:45), nicht Wiederholung.
_auto_readiness_check erkennt Fertigkeit, nicht Kreisen
(worker/goal_activities.py:52-117). Ein Agent, der zwölf Turns lang denselben
fehlschlagenden Befehl absetzt, wird ausschließlich von GOAL_MAX_ITERATIONS oder
dem Monatsbudget gestoppt — bemerkt wird er nie. Der Verifier-Cap
(agent/goal_toolset.py:33) ist das einzige Stück Code im Repo, das auf „hat
aufgehört zu konvergieren" reagiert, und er sieht genau eine Achse innerhalb genau
eines Turns.
Codex und SWE-agent haben es ebenfalls nicht, und beide auf lehrreiche Weise.
Codex delegiert es vollständig an das Modell (Blocked-Audit-Prosa, §1.4); die
einzige automatische Unterbrechung ist ein nicht-retrybarer Turn-Fehler, der das
Ziel gezielt blockt, „to prevent automatic continuation from looping and consuming
tokens" (codex-rs/ext/goal/src/extension.rs:311-315). SWE-agent hat zwei Zähler,
die wie Stall-Detection aussehen und keine sind: _n_consecutive_timeouts zählt
Kommando-Timeouts (sweagent/agent/agents.py:969-975), und
ScoreRetryLoop._n_consec_exit_cost wird inkrementiert und zurückgesetzt, aber
nie gelesen (sweagent/agent/reviewer.py:612-614) — toter Code, ebenso wie die
offensichtlich dafür gedachten Hooks on_model_query/on_attempt_started
(:103-122, null Aufrufstellen).
OpenHands ist der interessante Zwischenfall: stuck_detection: true ist in
jeden Start-Request hart einkodiert, nicht konfigurierbar
(src/api/agent-server-adapter.ts:1000, :1114), und es gibt einen eigenen
Terminalzustand ExecutionStatus.STUCK (base/common.ts:74), den der Client wie
einen Fehler behandelt (src/utils/status.ts:25-29, „Map STUCK to ERROR for now",
src/hooks/use-agent-state.ts:30-31). Welche Muster erkannt werden, ist in diesem
Repo nicht auffindbar — die Logik liegt im SDK. Der einzige Hinweis im
Repo-Vokabular ist das Critic-Feature loop behavior neben insufficient testing
(src/types/agent-server/core/base/critic.ts:13-25).
8. Failure-Handoff — was bekommt der Mensch, wenn es nicht geklappt hat?
| Aspekt | PA | Hermes | Codex | OpenHands | SWE-agent |
|—||||||
| Unterscheidbare Endzustände serverseitig | ✓ (6) | ✓ | ✓ (6) | ✓ (4) | ✓ (Exit-Status-Enum) |
| Endzustände beim Nutzer unterscheidbar | ✗ | ✓ | ✓ | ✓ | ◑ |
| Maschinenlesbares „was ist offen" | ✗ | ◑ (Reason-String) | ✗ | ✓ (missing) | ✗ |
| Modell schreibt eine Abschluss-Zusammenfassung | ◑ (nur Prompt-Hoffnung) | ✓ (erzwungen) | ✓ (Steering) | ✓ | ✗ |
| Teilergebnis wird gerettet | — | ✓ | ✓ | ✓ | ✓ (Autosubmit) |
| Weiterarbeiten möglich | ✓ (Resume) | ✓ (/goal resume) | ✓ | ✓ (Resume-Button) | ✗ |
| Eskalation an den Menschen vor dem Aufgeben | ◑ (existiert, wird ignoriert) | ✗ | ✗ | ✗ | ✗ |
8.1 PA: das Vokabular ist da, es kommt nur nicht an
end_goal flippt den Status und feuert einen Chat-Nudge — mehr nicht
(worker/goal_activities.py:249-273). Serverseitig ist die Unterscheidung
sauber: capped (12 Turns erreicht), failed (Turn dauerhaft gescheitert oder
nicht-retrybarer Fehler, klassifiziert über classify_model_error, damit ein
Context-Overflow nicht viermal pro Turn gegen die Wand fährt, :194-207),
budget, paused, cleared, done — und der Workflow routet jeden
Nicht-Abschluss durch end_goal, damit kein eingefrorenes „active"-Banner
zurückbleibt (worker/goal_workflow.py:48-51, :62-66).
Das Frontend wirft die Unterscheidung weg: das Banner wird nur für active und
paused gerendert und für alles andere schlicht fallengelassen
(frontend/src/stores/chat.ts:986-1000, :1011-1021; ChatPage.vue:362-408).
„Verifiziert fertig", „Turns aufgebraucht", „Geld aufgebraucht" und
„unrecoverable gecrasht" sehen für den Nutzer identisch aus: Banner weg,
Ungelesen-Marke da. Ein summary wird nur auf dem Erfolgspfad geschrieben
(agent/goal_toolset.py:154-155), im Auto-Pfad als Platzhalter
„Automatically verified as achieved." (worker/goal_activities.py:116); auf den
Fehlerpfaden gar nichts.
Was der Nutzer tatsächlich bekommt, ist die Prosa, die der Agent zufällig in
seiner letzten Nachricht hinterlassen hat. Die Instruktionen bitten darum
(„if you get stuck or need a decision from the user, say so clearly and do NOT
call complete_goal", agent/run_instructions.py:176-177) und der
Verifier-Cap-Text auch („state plainly what is still open and why",
agent/goal_toolset.py:310-315) — beides Prompt-Hoffnung, keine Garantie. Genau
der Fehler aus Lehre 4, nur auf der Ausgabeseite.
8.2 Was die anderen besser machen
OpenHands liefert die beste Struktur: der Judge gibt kein Boolean zurück,
sondern GoalVerdict{score, complete, missing}
(src/types/.../conversation-state-event.ts:67-77), und missing („Concise
description of what remains") tut dreifachen Dienst — es treibt die nächste Runde
als eingespeiste User-Nachricht, es rendert als Live-Fortschrittsbanner, und wenn
die Runden-Schranke reißt, ist es der Fehlerbericht, ausgeklappt per
initiallyExpanded={!active} (goal-status-content.tsx:29-44, :65-68, :130-166).
Dazu die Vokabel-Disziplin: capped ist explizit weder complete noch
interrupted (:22-27). Und der Input bleibt bewusst aktiv: „we intentionally do
NOT disable the input when the conversation is in an ERROR/STUCK execution
state" (custom-chat-input.tsx:52-55).
Hermes pausiert statt zu sterben, mit maschinenlesbarem Grund und einer
Zeile für den Menschen: „⏸ Goal paused — 20/20 turns used. Use /goal resume to
keep going" (hermes_cli/goals.py:1894-1908); bei kaputtem Judge druckt es die
exakte config.yaml-Stanza zum Reparieren (:1839-1892). Auf der inneren Ebene
erzwingt handle_max_iterations eine zusätzliche, tool-lose API-Runde mit
„provide a final response summarizing what you’ve found and accomplished so far"
(agent/chat_completion_helpers.py:2292-2325) — der Nutzer bekommt eine
Teilfortschritts-Zusammenfassung statt Stille. Im Kanban-Layer landet ein
Budget-Abbruch als sticky blocked „(NOT a silent exit)" (goals.py:2009-2011),
und zwar bewusst über _record_task_failure(outcome="timed_out") statt
kanban_block, damit es dem Circuit-Breaker des Dispatchers zählt
(agent/turn_finalizer.py:144-191).
SWE-agent rettet als einziges immer ein Artefakt: jeder Fehlerpfad läuft durch
attempt_autosubmission_after_error, das selbst git add -A && git diff --cached
ausführt (sweagent/agent/agents.py:823-868), und der Exit-Status trägt den Grund
(submitted (exit_cost), exit_context, exit_format, …, :1154-1218).
Natürliche Sprache gibt es dafür nie — der Bericht ist ein Enum plus ein Diff.
8.3 Der PA-Defekt, den es sonst nirgends gibt: ask_user wird überrollt
Das Eskalations-Primitiv existiert und ist in Goal-Turns aktiv:
ASK_USER_TOOL = "ask_user" (agent/interaction_toolset.py:23), das Tool wirft
CallDeferred (:90), agent/service.py:780-803 fängt die Deferral,
DeferredRunCoordinator.register_questions (agent/deferred.py:100-146) legt
eine Question-Row an und pusht eine Karte, und die Antwort setzt denselben
logischen Turn fort (service.py:1477, deferred.py:160). Es ist vom
Security-Guard ausgenommen (agent/tool_guard.py:15-17), es gibt einen stehenden
Nudge (agent/run_instructions.py:68-70, injiziert bei service.py:1288-1291),
und assembler/assembler.py:216-219 hängt interaction_toolset() an jeden
Nicht-Subagent-Run — ein Goal-Turn ist keiner.
Der Goal-Loop ignoriert das vollständig:
execute_inline_runruftrun_repo.mark_completed(run)beiagent/service.py:659— vor dem Deferral-Zweig bei:780. Ein deferrter Turn sieht damit aus wie ein normal beendeter.run_goal_turninspiziert nie den Deferral-Zustand; es liest ausschließlichrun_config.goal.statusneu (worker/goal_activities.py:216-219), siehtactiveund liefertcontinue: True(:242).- Nichts irgendwo gattet einen neuen Turn auf eine unbeantwortete Frage:
AgentQuestionRepo.pending_for_chatwird ausschließlich vonapi/routers/questions.py:51gerufen. - Der nächste Turn lädt die History mit
sanitize=True(service.py:360).
Netto: Der Agent stellt dem Nutzer eine blockierende Frage, die Schleife marschiert
daran vorbei, und die Karte bleibt verwaist stehen. Codex hat für exakt diese
Klasse den Wächter try_start_turn_if_idle, der einen Extension-getriebenen Turn
ablehnt, wenn ein User-Turn aussteht (codex-rs/core/src/session/inject.rs:47).
OpenHands hat die spiegelbildliche Variante als bekannten Stolperstein — „any
inbound message cancels the goal loop", weshalb dort ein explizites
if (goalStatus?.active) return; nötig wurde
(src/services/child-conversation-launch.ts:483-488).
9. Kontext über Schritte hinweg
| Aspekt | PA | Hermes | Codex | OpenHands | SWE-agent |
|—||||||
| Schritt N+1 sieht die History von 1…N | ✓ | ✓ | ✓ | ✓ | ✓ |
| Frischer Kontext pro Schritt | ✗ | ✗ | ✗ | ✗ | ✓ (RetryAgent: total) |
| Kompaktierung / Summarization | ✓ | ✓ | ✓ | ✓ | ✗ (nur mechanisch) |
| Fortsetzung = normale User-Nachricht | ✓ | ✓ | ✓ | ✓ | — |
| Fortsetzung im Transkript markiert | ✓ (Zero-Width-Marker) | ✗ | ◑ (interner Item-Typ) | ✗ (Prefix-Matching!) | — |
| Durabler Scratchpad außerhalb der History | ✗ | ◑ (todo, re-injiziert) | ✗ | ✓ (TaskTracker + PLAN.md) | ✗ |
| Verifier sieht dieselbe History wie der Agent | ✗ | ✗ (bewusst) | — | ◑ | ✗ |
PA lädt jeden Turn die volle History frisch aus der Messages-Tabelle und
komprimiert modellabhängig (agent/service.py:360-385; Schwelle 80 % des echten
Context-Windows, agent/model_limits.py:13-14). Die Kompaktierung ist
goal-bewusst — das Summary-Template führt mit „## Active task (MOST IMPORTANT
field: the last unfinished user intent)" (agent/compression.py:45-51) — und der
Tail ist paar-sicher (nie ein Tool-Call/Return getrennt, :152-180). Das Modell
wird nach Turn 0 gepinnt, damit der Loop nicht mitten im Ziel auf ein schwaches
lokales Modell umrollt (worker/goal_activities.py:145-147).
Die scharfe Kante: Kompaktierung und Verifikation sind sich uneinig darüber,
was History ist. Das Summary wandert in die Instructions, nicht in den
Message-Graph (agent/service.py:1313-1317), während evidence_from_messages nur
parts abläuft (agent/goal_toolset.py:100-114, :66-82). Nach der ersten
Kompaktierung eines langen Ziels verliert der complete_goal-Verifier also
lautlos die gesamte wegsummierte Arbeit und muss allein aus dem Tail urteilen. Der
DB-lesende Auto-Pfad hat dieses Loch nicht (er liest rohe Rows), ist dafür bei 120
Nachrichten gedeckelt (:118-143). Zwei Pfade, zwei Evidenzfenster, zwei
Fehlermodi — sie können am selben Ziel unterschiedlich urteilen.
Zur Präzision an einer Stelle, die die Bug-Erzählung trägt:
persist_new_messages bei agent/service.py:657 ist der Erfolgspfad; Teilstände
werden auf Fehlerpfaden früher geschrieben (:601-604 bei Cancellation, :627 bei
Exception). „Messages landen erst am Run-Ende" gilt für den Bug, den es erklärt,
ist aber nicht der einzige Write.
Zum Vergleich: Hermes macht die Amnesie explizit zum Design — der Judge
bekommt pro Call frischen Minimalkontext, der Verification-Checker ignoriert den
Live-Kontext komplett und liest SQLite (agent/verification_evidence.py:629-698),
und ephemere Nudges werden vor der Persistenz als _verification_stop_synthetic
etc. wieder herausgefiltert (agent/conversation_loop.py:7190-7225). OpenHands
schreibt die Kompaktierung als eigenes Event mit forgotten_event_ids und
summary_offset statt als stilles Rewrite
(.../condensation-event.ts:5-25) und hat als einziges einen echten durablen
Scratchpad (TaskTrackerAction, PlanningFileEditorAction auf PLAN.md,
base/action.ts:127-136, :218-247) — PAs todowrite überlebt nur als Tool-Call
in der History (agent/todo_toolset.py:1-7), die Kompaktierung kann also den Plan
fressen. SWE-agent ist das Extrem in beide Richtungen: der Default-History-
Processor ist die Identitätsfunktion (sweagent/agent/history_processors.py:74-82),
und zwischen Retry-Attempts wird env.hard_reset() gefahren mit einem brandneuen
Agent (sweagent/agent/agents.py:303-326) — Attempt N+1 sieht nichts von
Attempt N, weder Patch noch Trajektorie noch Reviewer-Kritik. Das ist i.i.d.
Resampling mit Selektor, keine iterative Verbesserung.
10. Adopt / Adapt / Skip
ADOPT
| # | Maßnahme | Warum (eine Zeile) | Aufwand |
|—|—|—||
| A1 | Tool-Call-Zähler pro Turn im Goal-Loop, hart, analog loop_caps (hermes/agent/tool_guardrails.py:447) — plus Token-Zähler pro Ziel | Die Achse, auf der der Runaway stattfand (19 Calls bei Zählerstand 1, tests/api/test_goal_toolset.py:139-171), ist die einzige völlig ungezählte; das Muster existiert im Repo bereits für Script-Executors (code_execution/monty.py:36) | S |
| A2 | ask_user gattet den nächsten Turn: run_goal_turn prüft AgentQuestionRepo.pending_for_chat (heute nur api/routers/questions.py:51) und pausiert das Ziel statt weiterzulaufen | Das Eskalations-Primitiv ist gebaut und in Goal-Turns aktiv (interaction_toolset.py:23, assembler.py:216-219), wird aber überrollt, weil mark_completed (service.py:659) vor dem Deferral-Zweig (:780) läuft — Codex’ try_start_turn_if_idle ist genau dieser Wächter | S |
| A3 | Strukturierter Verdict-Typ statt Substring-Match: _Verdict{achieved: bool, next_step: str} aus agent/kanban_context.py:101-104 in goal_toolset.verify_goal ziehen | Die gröbere Kopie im eigenen Repo macht es bereits robuster als das Original (goal_toolset.py:268 parst die erste Zeile per in), und next_step ist zugleich der Rohstoff für A4 | S |
| A4 | Strukturierter Failure-Handoff: summary + open_items auf allen Endzuständen schreiben (heute nur Erfolgspfad, goal_toolset.py:154-155) und das Frontend-Banner für done/capped/failed/budget rendern statt zu droppen (frontend/src/stores/chat.ts:986-1000) | Serverseitig existieren sechs unterscheidbare Endzustände (goal_workflow.py:48-66) und der Nutzer sieht viermal dasselbe Nichts; OpenHands’ {score, complete, missing} zeigt, dass eine Struktur Fortsetzung, Fortschrittsanzeige und Fehlerbericht zugleich bedient | S–M |
| A5 | Verifier-Cap zählt erst ab dem echten Versuch — Inkrement hinter die Frühausstiege bei goal_toolset.py:319-321 und :330-333 schieben | Heute sperren zwei harmlose Parse-/Kein-Ziel-Fehler den Turn für seinen ersten echten Verifikationsversuch aus | S |
ADAPT
| # | Maßnahme | Warum (eine Zeile) | Aufwand |
|—|—|—||
| B1 | Ein Evidenzfenster statt zwei: _auto_readiness_check und complete_goal müssen dieselbe Quelle lesen; das Kompaktierungs-Summary aus den Instructions (service.py:1313-1317) zusätzlich in die Evidenz einspeisen | Die beiden Pfade können am selben Ziel unterschiedlich urteilen, und der Live-Pfad verliert nach der ersten Kompaktierung lautlos die gesamte wegsummierte Arbeit (goal_toolset.py:100-114) | M |
| B2 | Hermes’ Evidence-Ledger-Idee, PA-gerecht: statt Shell-exit-codes ein persistiertes Prüf-Artefakt pro Ziel (Tool-Ergebnis-Hashes + Zeitstempel) mit der Stale-Regel — jede spätere schreibende Aktion invalidiert ein grünes Urteil (hermes/agent/verification_evidence.py:688) | „Ich hab’s geprüft" darf über eine nachfolgende Änderung hinweg nicht wiederverwendbar sein; PA hat die Traces bereits, nur nicht die Zeit-Invalidierung | M–L |
| B3 | Stall-Detection minimal: Hash der Tool-Result-Payloads pro Turn (Hermes’ _no_progress, tool_guardrails.py:276-507) plus ein turn-übergreifender „nichts hat sich geändert"-Fingerprint (Hermes’ workspace_fingerprint, goals.py:1498), der den Turn-Zähler trotzdem vorrücken lässt | PA erkennt Kreisen gar nicht (compression.py:36 dedupliziert bereits identische Tool-Returns, ohne Alarm zu schlagen) — die Dedup-Stelle ist der billigste Ort, das Signal abzugreifen | M |
| B4 | Erzwungene Konfrontation vor dem Abschluss nach SWE-agents Zwei-Stufen-Submit (tools/review_on_submit_m/bin/submit:33-46): der erste complete_goal liefert dem Agenten seine neu aus dem Speicher gelesene Evidenz plus Checkliste zurück, ohne zu terminieren | Ein Tool-Roundtrip, null zusätzliche LLM-Infrastruktur, deterministisch — und die Evidenz stammt aus der Welt statt aus der Erinnerung; passt exakt auf PAs bestehende _MAX_VERIFY_PER_RUN-Mechanik | M |
| B5 | Wall-Clock pro Ziel + Kopplung an APPROVAL_TIMEOUT: Goal-Turns dürfen nicht 600 s pro genehmigungspflichtigem Call auf einen abwesenden Menschen warten (device_policy.py:31, :78 gegen start_to_close_timeout=900, goal_workflow.py:23) | Zwei genehmigungspflichtige Calls sprengen das Activity-Budget und lösen einen Retry aus, der wieder wartet — im unbeaufsichtigten Modus muss der Timeout kurz sein oder der Call direkt als „braucht Freigabe" eskalieren (→ A2) | S |
| B6 | Ein Verifier-Konzept statt drei: agent/kanban_context.py:70-129 (run_worker, in-process, Prosa-Evidenz, fail-open bei Judge-Fehler :124, Default 4 Turns mit Clamp 8 bei :106) auf den geprüften Goal-Pfad zusammenführen; das „Verify:"-Card-Konzept (kanban_toolset.py:712) bleibt als menschliche Review-Stufe daneben bestehen | Dieselbe Frage wird im selben Codebase dreifach und widersprüchlich beantwortet — nur eine der Kopien hat die Fixes bekommen, und ausgerechnet die ungefixte hat den besseren Verdict-Typ (A3) | M |
SKIP — bewusst nicht
| Maßnahme | Warum nicht |
|---|---|
Fail-open-Judge à la Hermes (goals.py:18-19, jeder Transportfehler → continue) |
PA failt bei Verifier-Fehlern bewusst closed (goal_toolset.py:265-267), begründet im Kommentar und per Test fixiert — ein Rate-Limit darf niemals Arbeit als erledigt markieren. Hermes braucht wegen der Gegenentscheidung zwei zusätzliche Anti-Wedge-Zähler (:71, :76); PA braucht sie nicht. |
Codex’ unbegrenzter Default-Goal-Loop (spec.rs:36-40: „Omit [token_budget] unless explicitly requested") |
Kein Iterations-Cap, kein Timeout, keine Wiederholungserkennung, nur ein nicht-retrybarer Fehler als Notbremse — genau das Gegenteil dessen, was PA aus dem eigenen Runaway gelernt hat. |
Termination per stdout-Sentinel (SWE-agent, tools/tools.py:372-376) |
Jedes Kommando, dessen Ausgabe den String enthält, beendet den Lauf. PAs Tool-plus-Verifier ist strikt besser; nicht zurückrüsten. |
Kontextfreie Retry-Attempts (SWE-agents RetryAgent, agents.py:303-326: hard_reset + neuer Agent, Attempt N+1 sieht nichts von N) |
PAs Ziele sind lange, zustandsbehaftete Vorhaben, keine i.i.d. Benchmark-Instanzen — die durchgehende History ist hier die richtige Wahl. |
Probabilistischer Zufriedenheits-Critic als Gate (OpenHands, critic.ts:43-49) |
Er sagt P(Nutzer zufrieden) voraus, nicht ob die Arbeit getan ist; PAs Tool-Trace-Verifier ist geerdeter. Das Feature-Vokabular („loop behavior") ist als Anregung für B3 interessant, das Gate nicht. |
Judge-Ensemble / n-Sample-Scoring (SWE-agent, reviewer.py:400-414: 5 Calls, Mittelwert, letzte Zahl der letzten Zeile) |
Multipliziert Kosten auf der teuersten Achse und kollidiert mit PAs Per-Response-Usage-Modell; das Geld ist in B2 (deterministische Evidenz) besser angelegt als in mehr Meinungen. |
Geld als einzige Schranke (SWE-agent, models.py:73-76) |
Ein kreisender Agent wird dort mehrere hundert Schritte später von der Kreditkarte gestoppt — und der Lauf gilt danach als submitted (exit_cost). PA braucht zählbasierte Schranken zusätzlich zum Monatsbudget, nicht statt seiner. |
11. Empfohlene Reihenfolge
- A2 + A5 (S) — zwei kleine, klar begrenzte Korrekturen an bereits gebauten Mechanismen; A2 schließt einen echten Defekt.
- A1 (S) — die fehlende Einheit; ohne sie bleibt jede weitere Schranke Kosmetik.
- A3 → A4 (S / S–M) — der strukturierte Verdict liefert
next_stepals Rohstoff für den Failure-Handoff; zusammen bauen. - B1 (M) — ein Evidenzfenster, bevor B2/B4 darauf aufsetzen.
- B3 + B5 (M / S) — Kreis-Erkennung und die unmodellierte Wartezeit.
- B4 → B2 (M / M–L) — erst die billige erzwungene Konfrontation, dann der persistente Ledger mit Stale-Regel.
- B6 (M) — Konsolidierung, sobald der Goal-Pfad stabil ist.
Offene Produktfrage vor A1: Zählt der Tool-Call-Cap pro Turn (wie Hermes’
loop_caps) oder kumulativ pro Ziel? Pro Turn ist replay-sicher und deterministisch für Temporal; pro Ziel bräuchte einen persistierten Zähler und würde damit dieselbe Telemetrie-vs-Bound-Verwechslung riskieren, dieg["iterations"]bereits einmal produziert hat (§6.1).
Erstellt durch direkte Quellcode-Analyse von vier geklonten Repos und einem
IST-Audit des Personal-Agent-Goal-Pfads. Die OpenHands-Seite beschreibt den
beobachtbaren Kontrakt aus @openhands/agent-canvas @ 4470813; die Mechanik
liegt dort in OpenHands/software-agent-sdk und ist als solche gekennzeichnet.
Wo ein System einen Mechanismus nicht hat, steht ✗ und nicht ◑ — eine ehrliche
Lücke ist nützlicher als ein gedehntes Teilweise. Empfehlungen respektieren die
Frozen Contracts (insb. #6 RunSpec-Snapshot, #13 Untrusted-Gating, #14
fail-closed Klassifikation, #1/#2 Usage-Modell).