# Agent-Loop Improvements — M1 _Started 2026-04-23._ Drei kleine, voneinander unabhängige Verbesserungen am Mana-Agent-Stack, abgeleitet aus der Claude-Code-Architektur-Analyse. Alle drei zusammen ~1.5 Arbeitstage, mit hohem qualitativem und Sicherheits-Impact. **Hintergrund:** - [`docs/reports/claude-code-architecture.md`](../reports/claude-code-architecture.md) — wie Claude Code intern aufgebaut ist - [`docs/reports/mana-agent-improvements-from-claude-code.md`](../reports/mana-agent-improvements-from-claude-code.md) — vollständige Gap-Analyse mit 8 Verbesserungen; dies hier ist die priorisierte M1-Teilmenge ## Ziel in einem Satz Den `runPlannerLoop` um drei Primitive erweitern, die Claude Code hat und wir nicht haben: einen **Permission-Gate vor Tool-Execution**, einen **transienten Reminder-Channel** für Per-Round-Hinweise, und **Parallelisierung für reine Read-Tools**. ## Nicht-Ziele - **Kein** Umbau von `runPlannerLoop`s Grundstruktur — nur Erweiterungen. - **Keine** Änderung am Message-Log-Format — Iterations bleiben binärkompatibel. - **Keine** neue LLM-Route, kein neues Modell, kein Haiku-Tier (das ist M2). - **Kein** Context-Compressor (das ist M2, braucht eigene Archiv-Tabelle). - **Kein** Sub-Agent-Pattern (das ist M3, zusammen mit dem Persona-Runner). ## Wer profitiert | Konsument | Nutzen | |-----------------------|----------------------------------------------------------| | `services/mana-ai` | bessere Mission-Pläne + schnellere Multi-Read-Ticks | | `services/mana-mcp` | Schutz gegen missbräuchliche MCP-Clients | | Webapp Companion-Chat | bessere Antworten durch Per-Round-Context-Hinweise | | Persona-Runner (M3) | Fundament — braucht Permission-Gate bevor es live darf | --- ## Verbesserung 1 — Permission-Gate vor Tool-Execution ### Was es macht Bevor ein Tool-Handler aufgerufen wird, läuft ein zentrales `evaluatePolicy()` aus `@mana/tool-registry`. Das Gate entscheidet anhand von Tool-Scope, Policy-Hint, Usage-History und User-Settings, ob die Ausführung erlaubt ist. ### Was es ermöglicht - **Destructive-Tools werden per Default blockiert.** Heute ist `policyHint: 'destructive'` nur dokumentiert ([types.ts:48](../../packages/mana-tool-registry/src/types.ts#L48)), nicht durchgesetzt. Künftig: User muss in Settings explizit opt-in pro Tool oder Scope. - **Rate-Limiting pro User pro Tool.** Heute kann ein entwendeter JWT in 10 Sekunden hunderte Calls machen. Künftig: Cap 30 Calls/Tool/Minute (konfigurierbar pro Tool). - **Freitext-Input-Inspektion.** Für Tools mit String-Feldern (`content`, `description`, `note`): Marker wie `{{`, ` readonly string[]; } ``` Implementation skizziert (in der Loop): ```ts while (rounds < maxRounds) { rounds++; const reminders = input.reminderChannel?.({ round: rounds, /* … */ }) ?? []; const reminderMessages: ChatMessage[] = reminders.map(text => ({ role: 'system', content: `${text}`, })); const response = await llm.complete({ messages: [...messages, ...reminderMessages], // transient, nicht an messages push // … }); // … bestehende Logik (messages.push für assistant/tool, NICHT für reminder) … } ``` ### Erste Producer (Beispiele, nicht Scope von M1) Die Channel-API kommt in M1; die konkreten Reminder-Producer können inkrementell danach entstehen. Niedrig hängende Früchte: ```ts // services/mana-ai/src/planner/reminders.ts (später) export function tokenBudgetReminder(agent: ServerAgent, usage24h: number) { if (!agent.maxTokensPerDay) return null; const pct = usage24h / agent.maxTokensPerDay; if (pct < 0.75) return null; return `Agent ${agent.name} hat ${Math.round(pct * 100)}% des Tagesbudgets verbraucht. Plane sparsam.`; } ``` ### Aufwand 4h für die Loop-Änderung + Test. Producer sind eigene kleine PRs danach. ### Tests - `loop.test.ts`: Reminder wird injiziert, erscheint im LLM-Call, **nicht** im `result.messages`. - `loop.test.ts`: Reminder ist pro Round unabhängig — Round 2 kriegt nicht Round 1's Reminder zurück. ### Rollout Keine Flag-Gating nötig — Channel ist optional. Bestehende Caller, die ihn nicht setzen, verhalten sich identisch zu heute. --- ## Verbesserung 3 — Parallel-Execution für Read-Tools ### Was es macht Wenn das LLM in einer Runde mehrere Tool-Calls zurückgibt und **alle** davon `policyHint: 'read'` sind, führt `runPlannerLoop` sie mit `Promise.all` parallel aus, Cap bei 10 gleichzeitigen Calls. Sobald ein Write oder Destructive im Batch ist: wie heute sequenziell. Die Reihenfolge in `messages` bleibt **Source-Order** (wie das LLM sie gesendet hat), nicht Completion-Order. Debug-Log bleibt linear lesbar. ### Was es ermöglicht - **Schnellere Multi-Read-Missions.** Eine Research-Mission mit 5 Read- Tools: heute 5× Read-Latenz sequenziell, künftig ~1× Latenz parallel. Realer Gewinn: Wall-Clock-Zeit pro Tick halbiert sich in den Fällen, wo es zählt. - **Freie Kapazität für Compactor und Policy-Gate.** Beide Verbesserungen von M1/M2 kosten Latenz; der Parallel-Gain gleicht das aus. - **Kein Risiko bei Writes.** Die Regel „Read-only parallel, Writes seriell" ist dieselbe wie in Claude Codes `gW5` — sie macht Consistency trivial, ohne dass das Modell darüber nachdenken muss. ### Heutiger Zustand (Problem) [`packages/shared-ai/src/planner/loop.ts:172-188`](../../packages/shared-ai/src/planner/loop.ts#L172) — expliziter Code-Kommentar: > „Parallel execution is a perfectly valid optimisation for pure-read tools > but we keep order here so the message log tells a linear story when the > user debugs a failure." Das Argument ist legitim, aber der Message-Log kann Source-Order behalten, auch wenn die Calls parallel laufen. Wir verlieren nichts an Debug-Ergonomie. ### Neuer Zustand (Lösung) In [`loop.ts`](../../packages/shared-ai/src/planner/loop.ts) wird der Tool-Exec-Block ersetzt: ```ts // Bestimme Parallel-Eligibility aus der Registry const policyHints = response.toolCalls.map(c => getPolicyHintByName(c.name)); const allRead = policyHints.every(h => h === 'read'); if (allRead && response.toolCalls.length > 1) { // Cap 10: bei mehr Tools in Batches à 10 const BATCH_SIZE = 10; const allResults: ExecutedCall[] = []; for (let i = 0; i < response.toolCalls.length; i += BATCH_SIZE) { const batch = response.toolCalls.slice(i, i + BATCH_SIZE); const results = await Promise.all( batch.map(async (call) => ({ round: rounds, call, result: await onToolCall(call), })), ); allResults.push(...results); } // Append in Source-Order (nicht Completion-Order) for (const ex of allResults) { executedCalls.push(ex); messages.push({ role: 'tool', toolCallId: ex.call.id, content: JSON.stringify({ /* … */ }), }); } } else { // Sequenziell wie heute for (const call of response.toolCalls) { /* bestehend */ } } ``` Helper `getPolicyHintByName` kommt aus der Registry (lesbar, da in M1 eh integriert — Verbesserung 1 zieht die Policy-Information schon an die Loop-Grenze). ### Abhängigkeit Braucht **Verbesserung 1** vorher, damit `policyHint` autoritativ verfügbar ist. Ohne Policy-Gate müsste die Loop die Hints aus der Registry direkt nachschlagen — nicht schlimm, aber die Abfolge ist sauberer. ### Aufwand ~2h Code + Test. ### Tests - `loop.test.ts`: 3 Read-Calls → `Promise.all` wird aufgerufen, Wall-Clock ~= max(read) statt sum(reads). - `loop.test.ts`: 2 Read + 1 Write → sequenzielle Abarbeitung. - `loop.test.ts`: 11 Read-Calls → 2 Batches (10 + 1), aber Source-Order in `messages` erhalten. ### Rollout Keine Flag-Gating nötig. Verhalten ist strikt additiv (sequenzieller Pfad bleibt unverändert für gemischte Batches und für bestehende Caller, die keine Registry haben). --- ## Reihenfolge & Zeitplan | Reihenfolge | Verbesserung | Aufwand | Voraussetzung | |-------------|---------------------------|-------------|---------------------| | 1. | Permission-Gate (§1) | 1 Tag | — | | 2. | Reminder-Channel (§2) | 4 h | — (parallel zu §1) | | 3. | Parallel-Reads (§3) | 2 h | §1 (für policyHint) | **Gesamt: ~1.5 Arbeitstage.** Die drei Verbesserungen sind bewusst *klein*. Der Plan ist: 1. Alle drei in einem Sprint zusammen mergen (eine PR pro Verbesserung, drei PRs gesamt). 2. `POLICY_ENFORCE=false` starten (log-only), eine Woche beobachten. 3. Im gleichen Zeitraum die ersten Reminder-Producer in `mana-ai` nachziehen (eigene kleine PRs, nicht Teil von M1). 4. Flag flippen, Metriken prüfen (`policy_deny_total`, `parallel_read_batches_total`). ## Exit-Kriterien für M1 - [ ] `evaluatePolicy()` existiert in `@mana/tool-registry`, wird von beiden Consumern aufgerufen. - [ ] `POLICY_ENFORCE=true` läuft eine Woche in Staging ohne False-Positive-Rate > 1 %. - [ ] `runPlannerLoop` hat `reminderChannel`-API, Tests grün, mindestens ein Real-Producer live (z. B. Token-Budget-Reminder in `mana-ai`). - [ ] Multi-Read-Mission in `mana-ai` zeigt messbare Wall-Clock-Verkürzung in der Metrik `mana_ai_tick_duration_seconds` (Ziel: -30 % p95 bei Research-Missions). ## Danach M2 (Context-Compressor + Haiku-Tier) und M3 (In-Process Sub-Agents + Persona-Runner) bauen auf allen drei M1-Primitiven auf — besonders der Reminder-Channel ist das Vehikel, über das M2's Compactor dem LLM mitteilen kann, dass komprimiert wurde. Details: siehe [`docs/reports/mana-agent-improvements-from-claude-code.md`](../reports/mana-agent-improvements-from-claude-code.md) §12 Roadmap.