feat(agent-loop): M1 — policy gate + reminder channel + parallel reads

Three Claude-Code-inspired primitives for runPlannerLoop, derived from the
reverse-engineering reports in docs/reports/:

1. **Policy gate** (@mana/tool-registry) — evaluatePolicy() gates every tool
   dispatch: denies admin-scope, denies destructive tools not in the user's
   opt-in list, rate-limits per tool (30/60s default), flags prompt-injection
   markers in freetext without blocking. Wired into mana-mcp with a
   per-user rolling invocation log and POLICY_MODE env (off|log-only|enforce,
   default log-only). mana-ai uses detectInjectionMarker only — tool dispatch
   there is plan-only, so rate-limit/destructive checks don't apply yet.

2. **Reminder channel** (packages/shared-ai/src/planner/loop.ts) — new
   reminderChannel callback in PlannerLoopInput. Called once per round with
   LoopState snapshot (round, toolCallCount, usage, lastCall); returned
   strings wrap in <reminder> tags and inject as transient system messages
   into THIS LLM request only. Never pushed to messages[] — the Claude-Code
   <system-reminder> pattern that keeps the KV-cache prefix stable.

3. **Parallel reads** (loop.ts) — isParallelSafe predicate enables
   Promise.all dispatch when every tool_call in a round is parallel-safe,
   in batches of PARALLEL_TOOL_BATCH_SIZE=10. Any non-safe call downgrades
   the whole round to sequential. messages[] always appends in source
   order, never completion order, so the debug log stays linear.
   Default-off (undefined predicate) preserves pre-M1 behaviour.

Tests: 21 new in tool-registry (policy), 9 new in shared-ai (5 parallel,
4 reminder). All 74 green, type-check clean across 4 packages.

Design/plan: docs/plans/agent-loop-improvements-m1.md
Reports: docs/reports/claude-code-architecture.md,
         docs/reports/mana-agent-improvements-from-claude-code.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-23 13:56:40 +02:00
parent 493db0c3b2
commit e5d230e599
19 changed files with 2550 additions and 29 deletions

View file

@ -0,0 +1,385 @@
# 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 `{{`, `<system`, `ignore previous`
werden erkannt und als Metrik markiert. Nicht blockiert (zu viele False
Positives), aber sichtbar.
- **Ein Policy-Ort für beide Consumer.** `mana-mcp` und `mana-ai` rufen
denselben Code — keine Drift mehr.
### Heutiger Zustand (Problem)
[`services/mana-mcp/src/mcp-adapter.ts:34-37`](../../services/mana-mcp/src/mcp-adapter.ts#L34):
```ts
function isExposable(spec: AnyToolSpec): boolean {
return spec.scope === 'user-space';
}
```
Das ist der gesamte Gate. `mana-ai`s `onToolCall` hat gar nichts.
### Neuer Zustand (Lösung)
Neues Modul `packages/mana-tool-registry/src/policy.ts`:
```ts
export interface PolicyDecision {
readonly allow: boolean;
readonly reason?: string;
/** Optional hint, wird von M1 Verbesserung 2 als Reminder-Tag
* an den nächsten LLM-Turn angehängt. */
readonly reminder?: string;
}
export interface PolicyInput {
readonly spec: AnyToolSpec;
readonly ctx: ToolContext;
readonly rawInput: unknown;
readonly userSettings: {
readonly allowDestructive: readonly string[]; // Tool-Names Whitelist
readonly perToolRateLimit?: number; // default 30/min
};
readonly recentInvocations: readonly { toolName: string; at: number }[];
}
export function evaluatePolicy(input: PolicyInput): PolicyDecision;
```
Integration:
- [`services/mana-mcp/src/mcp-adapter.ts`](../../services/mana-mcp/src/mcp-adapter.ts) ruft `evaluatePolicy()` in `invoke()` **vor** `spec.handler()`.
- [`services/mana-ai/src/cron/tick.ts`](../../services/mana-ai/src/cron/tick.ts) ruft es im `onToolCall`-Callback.
- `recentInvocations` kommt aus einer In-Memory-Ringbuffer pro User (beide Services).
### Aufwand
~1 Arbeitstag (6-8h).
### Tests
- Unit-Tests in `packages/mana-tool-registry/src/policy.test.ts`: je ein
Case für allow/deny pro Policy-Regel.
- MCP-Integration-Test: Destructive-Tool-Call ohne Opt-In → 403 mit
klarer Fehlermeldung.
- Rate-Limit-Test: 31 Calls in 60s → letzter wird geblockt.
### Rollout
Flag-gated per ENV `POLICY_ENFORCE=true` (default off). Erst eine Woche
**log-only** (alle Decisions werden geloggt, nichts blockiert), dann
enforcement flippen.
---
## Verbesserung 2 — Reminder-Channel im Planner-Loop
### Was es macht
`runPlannerLoop` bekommt einen optionalen `reminderChannel`-Callback. Vor
jedem LLM-Call fragt die Loop den Channel nach aktuellen Per-Round-Hinweisen
(„du hast 80 % deines Token-Budgets verbraucht", „Mission ist in 2 min
überfällig"). Die Hinweise werden als **transiente** System-Message vor den
API-Call gesetzt und danach **wieder entfernt**. Sie leben nie in der
persistierten Message-History.
### Was es ermöglicht
- **Per-Round-Steering ohne History-Mutation.** Der Loop sieht den Zustand,
die Iteration speichert aber nur die Entscheidungen — kein KV-Cache-
Invalidation, kein Log-Rauschen.
- **Token-Budget-Awareness.** Aktuell weiß das LLM nicht, wie viele Calls
es noch hat. Künftig: „du hast 2 von 5 Rounds noch".
- **Stale-Data-Warnings.** Wenn `mana-ai` länger nicht sync'd hat, kann
das LLM warnen statt zu halluzinieren.
- **Zero-Knowledge-Hinweise.** Bei ZK-Usern: „verbotene Tabellen sind
nicht resolvable — frag nicht nach". Heute muss das im System-Prompt
stehen und bleibt dort ewig.
- **Policy-Feedback.** `evaluatePolicy()` (Verbesserung 1) kann einen
`reminder`-String zurückgeben, der dem LLM in der nächsten Runde erklärt,
warum ein Tool-Call geblockt wurde — statt nur einen Fehler zu werfen.
### Heutiger Zustand (Problem)
[`packages/shared-ai/src/planner/loop.ts:131-135`](../../packages/shared-ai/src/planner/loop.ts#L131):
```ts
const messages: ChatMessage[] = [
{ role: 'system', content: input.systemPrompt },
...(input.priorMessages ?? []),
{ role: 'user', content: input.userPrompt },
];
```
Transienter Context geht heute auf einem von zwei schlechten Wegen rein:
1. in den `systemPrompt` eingebacken → bleibt ewig stehen, veraltet schnell,
2. an den `userPrompt` per Concatenation → mutiert die History, landet in Logs.
### Neuer Zustand (Lösung)
[`packages/shared-ai/src/planner/loop.ts`](../../packages/shared-ai/src/planner/loop.ts) bekommt neuen Input-Slot:
```ts
export interface LoopState {
readonly round: number;
readonly toolCallCount: number;
readonly tokensUsed: TokenUsage;
readonly lastCall?: ExecutedCall;
}
export interface PlannerLoopInput {
// … bestehende Felder …
/** Called before each LLM request. Return an array of transient
* system-message strings to inject into THIS request only. They
* are removed from `messages` before the next iteration and never
* appear in the returned message log. */
readonly reminderChannel?: (state: LoopState) => 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: `<reminder>${text}</reminder>`,
}));
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.

View file

@ -0,0 +1,449 @@
# Claude Code — Anatomie eines Agent-Harness
**Stand:** 2026-04-23
**Quellenlage:** Reverse-Engineering / Leaks aus Community-Analysen (siehe §13)
> Technischer Bericht über die interne Architektur von Claude Code (Anthropics offiziellem CLI),
> konsolidiert aus öffentlich dokumentierten Reverse-Engineering-Analysen des
> minified `@anthropic-ai/claude-code`-Pakets sowie live mitgeschnittenen API-Roundtrips.
---
## Inhaltsverzeichnis
1. [Kontext zur Quellenlage](#1-kontext-zur-quellenlage)
2. [System-Architektur](#2-system-architektur)
3. [Prompt-System](#3-prompt-system)
4. [Tool-System](#4-tool-system)
5. [Sub-Agent-System (I2A / Task-Tool)](#5-sub-agent-system-i2a--task-tool)
6. [Context-Management](#6-context-management)
7. [Steering-Mechanismen](#7-steering-mechanismen)
8. [Security & Sandboxing](#8-security--sandboxing)
9. [Real-Time Steering: `h2A`](#9-real-time-steering-h2a)
10. [UI/Terminal-Layer](#10-uiterminal-layer)
11. [Memory & Todos](#11-memory--todos)
12. [Model-Routing](#12-model-routing)
13. [Bemerkenswerte Clever Tricks](#13-bemerkenswerte-clever-tricks)
14. [Relevanz für das Mana-Monorepo](#14-relevanz-für-das-mana-monorepo)
15. [Quellen](#15-quellen)
---
## 1. Kontext zur Quellenlage
Das ursprünglich viel zitierte Repo `shareAI-lab/analysis_claude_code` wurde inzwischen
archiviert bzw. in `shareAI-lab/learn-claude-code` überführt — der neue Fokus liegt auf
Didaktik („Harness Engineering"), nicht mehr auf den deobfuscierten Funktionsnamen. Die
ursprünglichen Funktionsnamen (`nO`, `h2A`, `wU2`, `I2A`, `UH1`, `gW5`, `tU2`, `KN5` etc.)
leben aber in mehreren Folge-Analysen weiter (BrightCoding Juli 2025, xugj520
„Efficient Coder", PromptLayer-Blog, Medium Sujay Pawar).
`Yuyz0112/claude-code-reverse` geht einen komplementären Weg: Statt statische
Code-Analyse monkey-patcht er `beta.messages.create` im installierten `cli.js` und loggt
die echten API-Roundtrips — daraus lassen sich die System-Prompts, Tool-Definitionen und
Modell-Routing-Entscheidungen direkt ablesen.
`Piebald-AI/claude-code-system-prompts` pflegt per Version ein vollständiges
Prompt-Archiv. Version 2.1.117 hat ~110 Prompt-Strings, 24 Built-in-Tool-Beschreibungen
und ~40 System-Reminder.
Die folgenden Befunde sind die konsolidierte Lesart über diese Quellen.
---
## 2. System-Architektur
Die Gesamtarchitektur ist ein vierlagiges Harness:
1. **UI Layer** — CLI (Ink/React), VSCode-Plugin, optionale Web-Frontends.
2. **Agent Core Scheduling Layer**`nO` (Master-Loop) plus `h2A` (asynchrone
Message-Queue als Steering-Bus).
3. **Execution Layer** — Streaming-Generator (`wu`), Context-Compressor (`wU2`),
Tool-Execution-Engine (`MH1`), Tool-Scheduler (`UH1`/`gW5`).
4. **Storage Layer** — Messages, komprimierte Summaries, CLAUDE.md-basierter
Langzeitspeicher, `~/.claude/todos/{session}.json`.
Der zentrale Prozessor ist `nO`: ein single-threaded Master-Loop, implementiert als
Generator-basierte async-Runtime. Das Muster ist das kanonische
```ts
while (response.stop_reason === "tool_use") {
executeTools();
appendResults();
recallModel();
}
```
das bei reinem Text terminiert. Um diesen Kern-Loop drapiert sich der gesamte Rest der
Maschinerie. `tU2` ist in den Analysen der Name für den Konversations-Flow-Wrapper, der
Messages in `nO` rein- und rausleitet und dabei System-Reminder einmischt.
### Flow-Diagramm
```
User
→ tU2 (conversation flow)
→ System-Prompt-Zusammenbau + Reminder-Injection
→ nO (master loop) ←→ h2A (steering bus)
→ Tool-Call?
→ UH1 Permission-Gate
→ gW5 Concurrent-Scheduler (max 10)
→ MH1 exec
→ Results zurück in h2A
→ nO → next iteration
```
---
## 3. Prompt-System
Jeder Turn geht mit drei Schichten an die API:
1. **System-Prompt**: Claude-Code-Identität, Kernrichtlinien, Tool-Policies,
Sandbox-Regeln, Git-Hinweise. Piebalds Archiv zeigt: mehrere Dutzend Strings werden
*konditional* injiziert je nach Mode (Auto/Learning/Plan), IDE-Integration,
aktiviertem Sandboxing etc.
2. **User-Message** mit angehängtem `<system-reminder>`-Block, der den dynamischen
Zustand trägt: Working-Directory, Git-Branch, Platform, Date, CLAUDE.md-Inhalte,
Todo-Liste, File-Freshness-Warnungen, Skills-Liste.
3. **Turn-spezifische Reminder** am Anfang und Ende einer Konversation
(`system-reminder-start`, `system-reminder-end` bei Yuyz0112) — der Start-Reminder
lädt Umgebungskontext, der End-Reminder prüft, ob Todos re-injiziert werden müssen.
### Besonderheit: CLAUDE.md-Disclaimer
Der CLAUDE.md-Block wird **mit einem expliziten Disclaimer** injiziert:
> „IMPORTANT: this context may or may not be relevant to your tasks. You should not
> respond to this context unless it is highly relevant."
Claude Code gibt dem Modell also bewusst die Freiheit, den Projektkontext zu ignorieren.
Das adressiert den klassischen Bug, dass lange System-Prompts das Modell zu rigiden
Interpretationen zwingen.
---
## 4. Tool-System
Die ursprünglichen Analysen sprechen von „15 Kern-Tools"; die aktuelle Version 2.1.117
laut Piebald-Archiv hat **24 Built-in-Tool-Beschreibungen**.
### Historische Kern-Menge
`View/Read`, `LS`, `Glob`, `Grep`, `Edit`, `Write/Replace`, `Bash`, `WebFetch`,
`WebSearch`, `NotebookRead`, `NotebookEdit`, `TodoWrite`, `Task` (Sub-Agent-Launcher),
`BatchTool` (historischer Vorläufer der heute impliziten Parallel-Tool-Calls),
`exit_plan_mode`.
### Neuere Additionen
`EnterPlanMode`/`ExitPlanMode`, `Worktree`-Management, `TaskCreate`/`TaskUpdate`,
`CronCreate`, `Computer` (Browser-Automation), `LSP`.
### Execution-Pipeline
- **`UH1` — Permission Gateway**: Statisch-strukturelle Prüfung vor Execution. Jeder
Tool-Call wird gegen die Whitelist aus `settings.json` gemappt (z. B.
`Bash(npm test:*)`, `Edit(src/**)`). Read-only Tools (`Read`, `Glob`, `Grep`, `LS`,
`WebFetch`, `WebSearch`) sind per Default auto-approved. Writes und Bash-Befehle
gehen durch Permission-Prompt, außer der Mode ist `acceptEdits`, `dontAsk` oder
`bypassPermissions`. Permission-Modes-Katalog: `plan` (read-only), `default`,
`acceptEdits`, `dontAsk`, `bypassPermissions`.
- **`gW5` — Parallel Scheduler**: Concurrent-Executor mit **Maximum 10 parallelen
Tool-Calls**. Wenn das Modell in einem Turn mehrere unabhängige Tool-Blocks
zurückgibt (z. B. gleichzeitig `Grep` + `Glob` + `Read`), werden sie in einem Pool
von maximal 10 Workern parallel abgearbeitet. Sobald ein Call ein Write ist oder
Side-Effects hat, serialisiert `gW5` für diesen Block.
- **`MH1` — Tool Execution Engine**: Der eigentliche Dispatcher mit dem Handler-Map
`toolName → handler`.
### Performance-Messwerte (Reverse-Engineering)
| Metrik | Wert | Quelle |
|---------------------------|---------------------|---------------------------|
| `gW5`/`UH1`-Overhead | ~0.8 ms pro Tool | xugj520 (M2 Max) |
| `h2A`-Throughput | >10 000 msg/s | xugj520 (M2 Max) |
| Max parallele Tool-Calls | 10 | ComeOnOliver/Analysis |
Das sind Reverse-Engineering-Schätzwerte, nicht Anthropic-offiziell.
---
## 5. Sub-Agent-System (I2A / Task-Tool)
Das `Task`-Tool ruft `KN5` als Launcher auf, der einen neuen `I2A`-SubAgent-Kontext
startet:
- **Fresh messages[]** — der Sub-Agent bekommt ein leeres History-Array, plus die aus
dem Parent extrahierte Task-Description als initiale User-Message.
- **Isolation**: eigene Tool-Permissions (restriktiver als der Parent, meist
read-only + Grep/Glob), eigener Token-Budget, eigener v8-Isolate-ähnlicher Scope.
- **Return-Contract**: Nur die finale Zusammenfassung des Sub-Agents wird als ein
einzelnes `tool_result` in die Parent-History eingefügt — das Kernziel: „dirty
context" (hunderte gescannte Files) bleibt im Kind, im Parent landet nur der
Destillat-Absatz.
- **Rekursionsgrenze**: Sub-Agents können **keine weiteren Sub-Agents** starten („One
level deep, no further"). Maximale Parallelität in einem Batch: ebenfalls 10.
- **Modell-Routing per Sub-Agent-Typ**: Der `Explore`-SubAgent läuft auf Haiku
(billig + schnell, read-only-Recherche), `Plan`-Mode auf Sonnet, General-Purpose
auf dem Parent-Modell.
---
## 6. Context-Management
### `wU2` — 92%-Threshold-Compressor
Die zentrale Komponente ist `wU2` — der **92%-Threshold-Compressor**. Sobald die
Token-Nutzung der Konversation ca. 92 % des Context-Window-Limits erreicht, feuert
`wU2` automatisch einen zusätzlichen API-Call mit einem speziellen
`system-compact`-Prompt (per Yuyz0112 belegt).
Das Modell wird instruiert, den Verlauf nach einem festen Schema zu komprimieren:
- **Task Goal**
- **Decisions Made**
- **Files Changed**
- **Current Progress**
werden explizit erhalten; verbatim-Details und intermediäre Tool-Outputs fallen weg.
Die Analysen berichten Kompressionsraten von ~6.8×.
In neueren Versionen existiert zusätzlich ein „soft"-Trigger bei ~50 % Auslastung, der
eine leichte Summary mit einbaut, ohne die Raw-Turns zu ersetzen.
### CLAUDE.md-Loading (8-Stage-Pipeline)
In der Praxis landen folgende Scopes gestapelt im System-Prompt:
1. `~/.claude/CLAUDE.md` (global)
2. Parent-Dirs hoch zur Git-Root
3. `./CLAUDE.md` (Projekt)
4. `./CLAUDE.local.md` (user-local, gitignored)
5. Sub-Directory CLAUDE.md-Dateien (beim Cd-ähnlichen Navigieren „imported")
6. Auto-Memory aus `~/.claude/projects/{project}/memory/MEMORY.md`
7. Session-spezifische Reminder
8. Turn-spezifische Reminder (siehe §7)
Größe wird empfohlen unter 200 Zeilen zu halten.
---
## 7. Steering-Mechanismen
Dies ist der unterschätzte Teil. Claude Code injiziert zur Laufzeit kontinuierlich
kleine Reminder-Blöcke ins nächste Turn, **ohne die Konversations-History zu
mutieren** — sie kommen als transiente `<system-reminder>`-Tags im nächsten
User-Message-Envelope:
| Mechanismus | Trigger | Effekt |
|--------------------------|---------------------------------------------------|------------------------------------------------------------------------|
| Todo-Re-Injection | Nach jedem `TodoWrite`-Call | Modell „sieht" seine Todo-Liste in *jedem* API-Request |
| File-Freshness-Tracking | File modifiziert nach letztem `Read` | Blockiert nächsten `Edit`; injiziert „File modified"-Warning |
| Stale-Todo-Reminder | Task-Tools zu lange nicht genutzt | Injiziert Hinweis, Planungs-Tools zu verwenden |
| Hook-Notifications | `PreToolUse`, `PostToolUse`, `SessionStart/End` | User-Hooks triggern; Output wird als `<system-reminder>` zurückgespielt |
**Kernidee:** Der Assistant-Teil der History bleibt unberührt — was kritisch ist, weil
jede Assistant-Mutation KV-Cache invalidieren würde. Cache-Stability wird explizit
priorisiert.
---
## 8. Security & Sandboxing
xugj520 beschreibt ein **6-Layer-Permission-Framework**:
1. **UI-Input-Validation**
2. **Prompt-Analyse auf Injection-Patterns**
3. **`UH1`-Policy-Matching** gegen Whitelist
4. **Pro-Tool-Arg-Validation** (z. B. Path-Canonicalization mit Blacklist außerhalb
des Workspaces)
5. **LLM-basierte Command-Injection-Detection** für Bash (ein separater Haiku-Call
prüft `rm -rf /`, `curl | sh` etc. vor Execution)
6. **Output-Filter/Redaction** (Secrets, Tokens)
Das Bash-Tool entstringt explizit Backticks und `$()`-Substitutionen in
User-gelieferten Argumenten. MCP-Server laufen in einem Bridge-Modus mit pro-Policy
entweder Docker- oder WASM-Isolation.
---
## 9. Real-Time Steering: `h2A`
`h2A` ist die namentlich auffälligste Komponente — **Dual-Buffer-Async-Message-Queue
mit Promise-basiertem Async-Iterator und Backpressure**. Funktionell:
- **Zero-Latency-Delivery**: Der Producer (Streaming-API-Response-Chunks,
Tool-Result-Events, User-Interjections) pusht in Buffer A, während der Consumer aus
Buffer B liest. Swap passiert atomar — keine Lock-Kontention, keine Drops.
- **Mid-Task-User-Interjections**: Der User kann *während* ein Tool läuft Input
senden; `h2A` merged das in die nächste Iteration, ohne den Loop neu zu starten.
Das erklärt, warum Claude Code auf Tastatureingaben reagiert, während es z. B. einen
langen Bash-Command ausführt.
- **Stream-JSON-I/O**: Externe Clients (IDE-Plugins, Remote-Control) sprechen Claude
Code über line-delimited JSON-Events über stdin/stdout, die direkt in `h2A` gepumpt
werden.
---
## 10. UI/Terminal-Layer
Ink (React-Renderer für Terminals) mit einem stark geforkten Custom-Reconciler via
`react-reconciler`. Jeder Frame:
1. React-Commit
2. In-memory DOM-Tree (DOMElement/TextNode)
3. **Yoga (Facebook's Flexbox-Engine, als yoga.wasm eingebunden)** berechnet Layout
4. `renderNodeToOutput` schreibt styled Characters in ein getyptes Screen-Buffer-Array
5. Diff gegen den vorherigen Frame
6. Minimale ANSI-Patches werden als single buffered write rausgeschrieben
**Primitive:** `ink-root`, `ink-box`, `ink-text`, `ink-raw-ansi`.
Version 2.1.x enthält laut DeepWiki-Mirrors 130+ React-Komponenten.
---
## 11. Memory & Todos
`TodoWrite` ist bewusst *nicht* in-process persistiert — es schreibt einen JSON-File
unter `~/.claude/todos/{session_id}.json`. Der Persistenz-Pfad erlaubt Session-Resume
und Inter-Session-Kontinuität.
**Felder:** `id`, `content`, `status` (pending/in_progress/completed), `priority`.
Die Re-Injection (siehe §7) macht den Todo-File zum De-facto-Arbeitsspeicher-Format.
Zusätzlich gibt's `MEMORY.md` (User-Memory, cross-session, per Projekt) und die
„Dream"-/Kairos-Features in neueren Builds, die Session-Summaries verdichtet ablegen.
---
## 12. Model-Routing
Claude Code ist in Wahrheit ein Multi-Modell-System:
| Modell | Einsatz |
|----------------|----------------------------------------------------------------------------|
| **Sonnet** 4.x/4.7 | Default für den Haupt-Agent-Loop |
| **Opus** | opt-in via `--model opus`; auto für Complex Debugging/Plan-Mode |
| **Haiku** | Hochfrequente Background-Calls (siehe unten) |
### Haiku-Aufgaben
- **(a) Quota-Check beim Startup** — ein 1-Token-Dummy-Request mit Text „quota",
gelingt nur, wenn Budget da ist
- **(b) Topic-Detection** — nach jedem User-Input (entscheidet, ob der Terminal-Title
geupdatet werden muss)
- **(c) Session-Summarization** — beim Resume
- **(d) Command-Injection-Detection** — pre-Bash
- **(e) Auto-Compact-Fallback** — wenn der Primär-Compactor teuer wird
Yuyz0112s Logs zeigen: all das läuft über `beta.messages.create` mit explizitem
`model: "claude-haiku-3.5"`.
**Quota-Handling ist proaktiv:** Die Haiku-Probe beim Start fängt 429-Fehler bevor der
User die erste echte Query tippt; bei Budget-Exhaustion wird auf einen Reduced-Mode
degradiert (kein Auto-Compact, kein Topic-Detection).
---
## 13. Bemerkenswerte Clever Tricks
Was technisch am interessantesten ist:
1. **Reminder-Injection statt History-Pollution** — Der gesamte Steuerungskanal
(Todos, File-Freshness, Plan-Mode-Hints) läuft über transiente
`<system-reminder>`-Tags im User-Turn. Der Assistant-Teil der History bleibt
unberührt — was kritisch ist, weil jede Assistant-Mutation KV-Cache invalidieren
würde. Cache-Stability wird explizit priorisiert.
2. **Der 92%-Trigger vs. hartes Limit** — Anstatt bei 100 % zu crashen, wird bei
92 % präventiv komprimiert. `wU2` ist eine Insurance-Policy, kein Notausgang.
3. **`h2A`-Dual-Buffer mit User-Interjection** — Agent-Frameworks die man sonst sieht
(LangGraph, CrewAI) sind turn-based. Claude Codes User-Interjection mitten im
Tool-Call ist architektonisch der Unterschied zwischen „Chat-Loop" und
„Interactive Shell".
4. **Sub-Agent als Context-Laundering**`I2A` ist nicht primär für Parallelisierung
da, sondern um „dreckige" Kontexte aus der Parent-History zu isolieren. Pattern
stammt wohl aus Reinforcement-Learning-Tradition: Episoden sauber halten.
5. **LLM für Security** — Die Haiku-basierte Command-Injection-Detection ist ein
bemerkenswerter Bruch mit klassischer Security-Praxis (Regex-Blacklists).
Anthropic vertraut einem Modell, Angriffsmuster zu erkennen, die Regex nicht
erwischt.
6. **Der versteckte Quota-Ping** — Mit einem einzigen „quota"-Token-Request beim
Start wird das Budget getestet, bevor der User auch nur getippt hat. Billig und
clever.
7. **yoga.wasm** — Dass ein CLI-Tool einen Flexbox-Engine aus dem
React-Native-Umfeld als WASM einbindet, um Terminal-Layout zu rendern, ist
technisch elegant overkill — und erklärt, wie Ink so robust mit Resize-Events
umgeht.
8. **Parallel-Tool-Policy ohne Write**`gW5` parallelisiert nur Read-Tools. Sobald
ein Write kommt, wird serialisiert. Das macht Consistency trivial, ohne dass das
Modell überhaupt darüber nachdenken muss.
9. **BatchTool wurde deprecated**, weil das Modell selbst gelernt hat, mehrere
`tool_use`-Blocks in einem Response-Turn zurückzugeben — das Harness brauchte
irgendwann keinen expliziten Batch-Wrapper mehr. Modell-Training hat das Feature
wegtrainiert.
10. **CLAUDE.md-Disclaimer** — Der „may or may not be relevant"-Disclaimer ist
subtile Instruktions-Entkopplung: Anthropic möchte, dass CLAUDE.md *Kontext* ist,
nicht *Befehl*.
### Kurzes Fazit
Das technisch Bemerkenswerte an Claude Code ist nicht eine einzelne Komponente,
sondern die Konsequenz, mit der **alles, was nicht der Loop ist, zu Harness degradiert
wurde**. `nO` ist 20 Zeilen. Der Rest — `h2A`, `wU2`, `UH1`, `gW5`, `I2A`, `KN5`,
`tU2`, die Reminder-Injektion, die Haiku-Nebencalls — ist Environment-Engineering um
einen minimalen, modellgetriebenen Kern herum. Die explizite Botschaft der
Community-Analysen: Wer heute „Agents" baut und stattdessen in Rule-Trees und
Prompt-Ketten denkt, hat das Grundpattern verfehlt.
---
## 14. Relevanz für das Mana-Monorepo
Direkte Ableitungen für laufende Initiativen:
- **`services/mana-mcp` (:3069)** — Der Reminder-Injection-Mechanismus aus §7 ist
direkt übertragbar: Tool-Results könnten nicht nur rohe JSON-Payloads zurückgeben,
sondern transiente `<system-reminder>`-Blöcke mit Space-Context, Tier-Status oder
stale-Data-Warnungen. Siehe
[`docs/plans/mana-mcp-and-personas.md`](../plans/mana-mcp-and-personas.md).
- **`services/mana-ai` Mission-Runner** — Das `nO`/`h2A`-Pattern (single-threaded
Master-Loop + Async-Steering-Bus) ist eine sauberere Alternative zu der
cross-tick-Statemachine, die dort aktuell Gemini Deep Research orchestriert. Siehe
`project_gemini_deep_research`-Memory.
- **Shared Tool-Registry** (`packages/mana-tool-registry`) — Das Permission-Gateway
(`UH1`) mit Whitelist-Matching ist ein brauchbares Mental-Model für die
Tool-Authorization, die wir persona-scoped einführen müssen.
- **Compression-Pattern (`wU2`)** — Für lange Sync-Logs oder Missions-Historie mit
>50k Tokens sinnvoll: präventives Komprimieren bei 92 % Budget-Auslastung nach
festem Schema (Goal / Decisions / Files / Progress).
---
## 15. Quellen
- [shareAI-lab/learn-claude-code](https://github.com/shareAI-lab/learn-claude-code)
(Nachfolger von `analysis_claude_code`)
- [Yuyz0112/claude-code-reverse](https://github.com/Yuyz0112/claude-code-reverse)
(API-Monkey-Patch-Logs)
- [Piebald-AI/claude-code-system-prompts](https://github.com/Piebald-AI/claude-code-system-prompts)
(System-Prompt-Archiv v2.1.117)
- [ComeOnOliver/claude-code-analysis](https://github.com/ComeOnOliver/claude-code-analysis)
(17-Sektion-Dokumentation)
- [Inside Claude Code: A Deep-Dive Reverse Engineering Report — BrightCoding (Juli 2025)](https://www.blog.brightcoding.dev/2025/07/17/inside-claude-code-a-deep-dive-reverse-engineering-report/)
- [Claude Code Reverse Engineering v1.0.33 — Efficient Coder (xugj520)](https://www.xugj520.cn/en/archives/claude-code-reverse-engineering.html)
- [Claude Code: Behind-the-scenes of the master agent loop — PromptLayer Blog](https://blog.promptlayer.com/claude-code-behind-the-scenes-of-the-master-agent-loop/)
- [How Claude Code Actually Works — Sujay Pawar (Medium)](https://medium.com/@sujaypawar/how-claude-code-actually-works-1f6d4f1eea82)
- [ZenML LLMOps Database: Claude Code Agent Architecture](https://www.zenml.io/llmops-database/claude-code-agent-architecture-single-threaded-master-loop-for-autonomous-coding)
- [Reverse-Engineering Claude Code — sathwick.xyz](https://sathwick.xyz/blog/claude-code.html)
- [Claude Code Architecture (Reverse Engineered) — vrungta.substack](https://vrungta.substack.com/p/claude-code-architecture-reverse)
- [How Claude Code Uses React in the Terminal — DEV.to](https://dev.to/vilvaathibanpb/how-claude-code-uses-react-in-the-terminal-2f3b)
- [Pan Xinghan: what the shareAI-lab analysis adds — Medium](https://medium.com/@sampan090611/claude-code-feels-like-a-senior-dev-heres-what-actually-makes-it-different-and-what-the-49c02b456d9c)
---
**Verwandte Berichte in diesem Repo:**
- [`docs/reports/ai-agent-architecture-comparison.md`](./ai-agent-architecture-comparison.md)
- [`docs/reports/gemini-deep-research.md`](./gemini-deep-research.md)
- [`docs/reports/web-research-capabilities.md`](./web-research-capabilities.md)

View file

@ -0,0 +1,631 @@
# Mana-Agent-Infrastruktur — Verbesserungen aus den Claude-Code-Learnings
**Stand:** 2026-04-23
**Voraussetzung:** [`claude-code-architecture.md`](./claude-code-architecture.md)
> Konkrete, priorisierte Verbesserungsvorschläge für unser Agent-Stack
> (`services/mana-ai`, `services/mana-mcp`, `packages/mana-tool-registry`,
> `packages/shared-ai`, Persona-Runner), abgeleitet aus den Patterns, die
> Claude Code durch Reverse-Engineering exponiert hat.
---
## Inhalt
1. [Zusammenfassung](#1-zusammenfassung)
2. [Ist-Stand: Wo steht unser Stack wirklich?](#2-ist-stand-wo-steht-unser-stack-wirklich)
3. [Gap-Analyse gegen Claude Code](#3-gap-analyse-gegen-claude-code)
4. [Verbesserung 1 — Permission-Gateway `UH1`-Style](#4-verbesserung-1--permission-gateway-uh1-style)
5. [Verbesserung 2 — Reminder-Injection statt History-Pollution](#5-verbesserung-2--reminder-injection-statt-history-pollution)
6. [Verbesserung 3 — Context-Compressor `wU2`-Style](#6-verbesserung-3--context-compressor-wu2-style)
7. [Verbesserung 4 — Parallel-Execution `gW5`-Style](#7-verbesserung-4--parallel-execution-gw5-style)
8. [Verbesserung 5 — Sub-Agent-Pattern `I2A`-Style](#8-verbesserung-5--sub-agent-pattern-i2a-style)
9. [Verbesserung 6 — Haiku-Tier für Background-Tasks](#9-verbesserung-6--haiku-tier-für-background-tasks)
10. [Verbesserung 7 — Async-Steering-Bus `h2A`-Style](#10-verbesserung-7--async-steering-bus-h2a-style)
11. [Verbesserung 8 — Deprecated-Tool-Training](#11-verbesserung-8--deprecated-tool-training)
12. [Roadmap und Priorisierung](#12-roadmap-und-priorisierung)
13. [Explizit nicht übernehmen](#13-explizit-nicht-übernehmen)
---
## 1. Zusammenfassung
Claude Code ist im Kern ein **minimaler Agent-Loop mit sehr viel Environment-
Engineering drumherum**. Unser Mana-Stack hat den Loop (`runPlannerLoop` in
[`packages/shared-ai/src/planner/loop.ts`](../../packages/shared-ai/src/planner/loop.ts))
und die Tool-Registry bereits sauber getrennt — aber fast das gesamte
„drumherum" fehlt: Permission-Gating, Reminder-Injection, Context-Compression,
parallele Tool-Execution, Sub-Agent-Isolation, Async-Steering.
Die gute Nachricht: unsere Architektur ist *vorbereitet*. Die Registry-
Trennung (`@mana/tool-registry`, `@mana/shared-ai`), die saubere `ToolContext`-
Abstraktion, die LWW-Projektionen — all das sind solide Fundamente, auf denen
man die Claude-Code-Patterns inkrementell nachziehen kann, ohne den Stack
umzubauen.
**Größter Impact-Hebel:** Reminder-Injection + Context-Compression.
**Größtes Sicherheitsdefizit:** fehlendes Permission-Gate auf MCP-Ebene.
**Größter Performance-Hebel:** Parallel-Tool-Execution bei Read-Tools.
---
## 2. Ist-Stand: Wo steht unser Stack wirklich?
### Die Haupt-Loop: `runPlannerLoop`
Unser Äquivalent zu Claude Codes `nO`-Master-Loop lebt in
[`packages/shared-ai/src/planner/loop.ts:117-210`](../../packages/shared-ai/src/planner/loop.ts).
Das Muster ist isomorph zu Claude Code:
```ts
while (rounds < maxRounds) { // entspricht Claude's while-loop
const response = await llm.complete(...); // entspricht stop_reason-Check
if (response.toolCalls.length === 0) break; // terminiert bei Text
for (const call of response.toolCalls) {
await onToolCall(call); // entspricht MH1-Dispatch
}
}
```
**Abweichungen:**
- `DEFAULT_MAX_ROUNDS = 5` ([loop.ts:115](../../packages/shared-ai/src/planner/loop.ts#L115)) — Claude Code hat kein hartes Round-Limit, sondern ein Token-Limit.
- Tool-Calls werden **sequenziell** abgearbeitet ([loop.ts:172-188](../../packages/shared-ai/src/planner/loop.ts#L172)) — explizit so dokumentiert: „Parallel execution is a perfectly valid optimisation for pure-read tools but we keep order here".
- Kein Permission-Gate — `onToolCall` wird einfach aufgerufen.
- Kein Reminder-Injection-Mechanismus — System + Prior + User, fertig.
### Mission-Runner: `mana-ai`
[`services/mana-ai/src/cron/tick.ts`](../../services/mana-ai/src/cron/tick.ts)
(670 Zeilen) orchestriert den Loop im Background. Besonderheiten:
- **60-Sekunden-Tick statt event-driven** ([tick.ts:102-286](../../services/mana-ai/src/cron/tick.ts#L102)) — das Polling-Modell fängt DB-Changes nur mit Lag auf.
- **Overlap-Guard** via Module-Level-Boolean `running` ([tick.ts:100](../../services/mana-ai/src/cron/tick.ts#L100)) — einfach aber funktioniert.
- **Cross-Tick-State-Machine** für Deep Research ([tick.ts](../../services/mana-ai/src/cron/tick.ts), `handleDeepResearch`) — das einzige Feature, das „länger als ein Tick" überbrückt.
- **Per-Agent-Concurrency** ([tick.ts:194-208](../../services/mana-ai/src/cron/tick.ts#L194)) — mit Budget-Gate auf Token-Ebene. Gut.
- **Key-Grants** ([tick.ts, crypto/](../../services/mana-ai/src/crypto)) — RSA-OAEP-gewrappte MDKs pro Mission, TTL-clamped. Sehr solide.
### MCP-Gateway: `mana-mcp`
[`services/mana-mcp/src/`](../../services/mana-mcp/src) ist **bereits
implementiert**, nicht nur geplant. 379 LOC total, stateless, JWT-gated.
Tool-Registrierung in
[`mcp-adapter.ts:81-124`](../../services/mana-mcp/src/mcp-adapter.ts#L81):
```ts
for (const spec of getRegistry()) {
if (!isExposable(spec)) continue; // filter admin-scoped
server.tool(spec.name, spec.description, shape, invoke);
}
```
Das ist elegant — aber **`isExposable` ist die einzige Policy-Schicht**
([mcp-adapter.ts:35-37](../../services/mana-mcp/src/mcp-adapter.ts#L35)). Es
gibt keine Rate-Limits, keine pro-Request-Policy, keine User-Whitelist pro
Tool, keine Command-Injection-Prüfung für freie Text-Felder.
### Tool-Registry: `@mana/tool-registry`
[`packages/mana-tool-registry/src/`](../../packages/mana-tool-registry/src)
(rund 400 LOC). Sehr sauber:
- `ToolSpec<I, O>` mit Zod-Schemas ([types.ts:91-122](../../packages/mana-tool-registry/src/types.ts#L91))
- `ToolContext` mit `userId`/`spaceId`/`jwt`/`invoker`/`getMasterKey` ([types.ts:58-74](../../packages/mana-tool-registry/src/types.ts#L58))
- `registerTool` + `getRegistry` Singleton ([registry.ts](../../packages/mana-tool-registry/src/registry.ts))
- `encryptedFields` als **deklaratives** Feld — nicht handler-intern. Genial für zukünftige CI-Drift-Checks gegen die web-app `crypto/registry.ts`.
**Aktuell abgedeckte Module:** `habits`, `spaces`, `todo`, `notes`, `journal`,
`calendar`, `contacts`, `articles`, `missions`, `tags` ([types.ts:18-29](../../packages/mana-tool-registry/src/types.ts#L18)).
Laut `mana-ai/CLAUDE.md`: 31 propose-Tools über 16 Module sind server-seitig
sichtbar; 28 weitere auto-Tools leben ausschließlich in der Webapp.
### Persona-Runner
Nicht implementiert. Plan in
[`docs/plans/mana-mcp-and-personas.md`](../plans/mana-mcp-and-personas.md).
Wichtig: wir haben dort die Chance, die Sub-Agent-Patterns aus §8 **direkt
richtig** zu bauen, statt nachträglich nachzurüsten.
---
## 3. Gap-Analyse gegen Claude Code
| Pattern (Claude Code) | Mana-Äquivalent | Status | Priorität |
|-------------------------------------|-------------------------------------------------|----------------------------|-----------|
| `nO` Master-Loop | `runPlannerLoop` | ✅ vorhanden, solide | — |
| `MH1` Tool-Dispatcher | `onToolCall` + Registry-Handler | ✅ vorhanden | — |
| `UH1` Permission-Gateway | nur `isExposable` Admin-Filter | ⚠️ stark lückenhaft | **hoch** |
| `gW5` Parallel-Scheduler (max 10) | sequenziell | ❌ fehlt | mittel |
| `wU2` 92%-Compressor | keinerlei Context-Kompression | ❌ fehlt | **hoch** |
| `<system-reminder>` Reminder-Injection | User-Prompt-Concat, kein transientes Channel | ❌ fehlt | **hoch** |
| `h2A` Async-Message-Queue | 60s-Tick, kein mid-task interrupt | ❌ fehlt | niedrig |
| `I2A` Sub-Agent (Fresh-Context) | Persona-Runner (extern, geplant) | 🟡 im Plan, nicht isomorph | mittel |
| File-Freshness-Tracking | n/a — wir editieren keine Files | — n/a | — |
| Haiku für Background-Tasks | alle Calls gehen an mana-llm primary model | ❌ fehlt | mittel |
| BatchTool deprecated | wir haben weder Batch noch parallel | — n/a | — |
| CLAUDE.md-Disclaimer-Pattern | Agent-Context / Memory ohne Disclaimer | 🟡 improvement-worth | niedrig |
---
## 4. Verbesserung 1 — Permission-Gateway `UH1`-Style
### Problem
[`services/mana-mcp/src/mcp-adapter.ts:34-37`](../../services/mana-mcp/src/mcp-adapter.ts#L34)
— der einzige Gate ist Scope-Filter:
```ts
function isExposable(spec: AnyToolSpec): boolean {
return spec.scope === 'user-space';
}
```
Das reicht nicht:
- Kein **pro-User-Opt-In** für gefährliche Tools (z. B. `habits.delete`).
- Kein **Rate-Limit** pro User pro Tool (MCP ist JWT-gated, aber ein entwendeter JWT kann in 10 Sekunden 1000 Calls machen).
- Kein **Path-/Content-Filter** für Freitext-Argumente (Tool `notes.create` mit `content` könnte Prompt-Injection ins Frontend tragen).
- `destructive`-Policy-Hint ist **dokumentiert** ([types.ts:48](../../packages/mana-tool-registry/src/types.ts#L48)) aber nicht **durchgesetzt** — die Registry weiß, welches Tool destructive ist, aber niemand liest das an der Grenze.
### Vorschlag
Ein zentrales `evaluatePolicy()` in `@mana/tool-registry`:
```ts
// packages/mana-tool-registry/src/policy.ts (neu)
export interface PolicyDecision {
allow: boolean;
reason?: string;
/** Optional: inject as <system-reminder> on next turn. */
reminder?: string;
}
export function evaluatePolicy(
spec: AnyToolSpec,
ctx: ToolContext,
rawInput: unknown,
opts: {
userSettings?: { allowDestructive: boolean; perToolRateLimit?: number };
recentInvocations?: readonly { toolName: string; at: Date }[];
},
): PolicyDecision;
```
Aufgerufen wird sie in `mcp-adapter.ts` **vor** `spec.handler()` und — wichtig
— auch in `mana-ai`s `onToolCall`-Callback. Damit ist die Policy an einer
Stelle und für beide Consumer gültig.
**Konkrete Regeln für M1:**
- `policyHint: 'destructive'` → Default `deny`, User muss explizit in Settings
opt-in (pro Tool oder pro Scope).
- Rolling 60-Sekunden-Window: Cap bei 30 Calls/Tool/User/Minute auf MCP.
- Für Tools mit Freitext-Argumenten (`content`, `description`, `note`): ein
Zod `.refine()` das klassische Injection-Marker (`{{`, `<system`,
`ignore previous`) erkennt und loggt — nicht blockiert, aber markiert.
### Aufwand
~1 Tag. Die Registry ist dafür gebaut.
---
## 5. Verbesserung 2 — Reminder-Injection statt History-Pollution
### Problem
In [`runPlannerLoop`](../../packages/shared-ai/src/planner/loop.ts#L131) wird
die `messages`-History pro Round durch Assistant- und Tool-Turns erweitert —
korrekt und nötig. Was **nicht** passiert: transienter Kontext (Token-Budget,
Agent-Memory-Updates, User-Interjections, Mission-Deadline-Änderungen) wird
entweder
1. in den System-Prompt eingebacken und bleibt dort ewig (veraltet), oder
2. in den User-Prompt per String-Concatenation injiziert (mutiert die
History, invalidiert KV-Cache, landet in Logs).
Die `<agent_context>`-Blöcke aus
[`mana-ai` v0.5](../../services/mana-ai/CLAUDE.md) sind schon ein Schritt in
die richtige Richtung, aber sie sind im System-Prompt und nicht transient.
### Vorschlag
**`ReminderChannel`** als neuer Input-Slot für `runPlannerLoop`:
```ts
// packages/shared-ai/src/planner/loop.ts
export interface PlannerLoopInput {
// … bestehende Felder …
/** Per-round transient hints. Called after every assistant turn;
* injected as a fresh system message at the end of `messages` before
* the next LLM call. NOT persisted in the returned message log. */
readonly reminderChannel?: (roundIndex: number, state: LoopState) => string | null;
}
```
Die Reminder-Strings werden als transiente `{ role: 'system', content: '<reminder>…</reminder>' }`
**vor jedem LLM-Call** eingefügt und **nach** dem Call wieder entfernt — sie
leben nie in `messages`, landen nicht in der Iteration-History. Genau das
Pattern von Claude Codes `<system-reminder>`-Tags.
**Use-Cases heute schon sinnvoll:**
- Token-Budget: „Du hast 80 % deines Mission-Budgets verbraucht. Plane Tool-Calls sparsam."
- Mission-Timer: „Mission ist in 2 Minuten überfällig — priorisiere."
- Zero-Knowledge-Mode: „User ist ZK — verbotene Tabellen werden nicht decrypted. Frag nicht nach."
- Nach TodoWrite: aktuellen Todo-State echoen (wie in Claude Code, §7).
- Stale-Data-Warning: „Letzter Sync vor 45 min — Daten könnten veraltet sein."
### Aufwand
~4h für die Loop-Änderung, ~2 Tage für die ersten drei Reminder-Producer.
### Warum wichtig
Das ist der **größte qualitative Hebel** — er wirkt sich auf jede einzelne
Mission-Iteration aus, nicht nur auf Edge-Cases. Genau das, was Claude Code
so feedback-sensitiv macht.
---
## 6. Verbesserung 3 — Context-Compressor `wU2`-Style
### Problem
Bei langlaufenden Missions (Deep Research, Multi-Round-Plans) wird die
Iteration-History in `Mission.iterations[]` immer länger. Heute wird sie
komplett in den `buildSystemPrompt()`-Call geschoben — irgendwann overflowed
das den Context.
[`services/mana-ai/src/cron/tick.ts:211-221`](../../services/mana-ai/src/cron/tick.ts#L211)
ruft `planOneMission`, das via `runPlannerLoop` alle Iterations durchreicht.
**Kein** Abbruch, kein Pruning, keine Summary.
### Vorschlag
Einen dedizierten `compactHistory()` pro Mission-Lifecycle:
```ts
// packages/shared-ai/src/planner/compact.ts (neu)
export async function compactIterations(
iterations: readonly MissionIteration[],
llm: LlmClient,
opts: { budgetTokens: number; maxInputTokens: number },
): Promise<{ preserved: MissionIteration[]; summary: CompactSummary }>;
```
**Trigger-Heuristik** (analog zum 92 %-Trigger):
- Wenn die kumulierte Token-Schätzung der `iterations[]` > `0.6 × maxInputTokens` → komprimieren.
- Alle Iterations älter als die letzten 3 werden in eine einzelne **Compact-Iteration** gefasst mit dem Schema `{ goal, decisions, filesChanged, currentProgress }` (genau das, was Claude Code persistiert).
- Die Compact-Iteration wird als synthetische Iteration mit `actor: { kind:'system', source:'compactor' }` in `Mission.iterations[]` geschrieben und die summierten Originale werden **archiviert** in einer neuen Tabelle `mana_ai.iteration_archive` (nicht gelöscht, nur nicht mehr Teil des Prompt-Contexts).
**Kompressionsrate** aus Claude Code: ~6.8× gemeldet. Bei uns realistisch
~3-5×, weil Iterations schon strukturiert sind.
### Aufwand
~3-5 Tage inkl. Archiv-Tabelle und Migration.
### Wann sinnvoll
**Jetzt** für Deep-Research-Missions (die schon heute Token-Explosion
riskieren), später für normale Multi-Round-Plans.
---
## 7. Verbesserung 4 — Parallel-Execution `gW5`-Style
### Problem
[`packages/shared-ai/src/planner/loop.ts:172-188`](../../packages/shared-ai/src/planner/loop.ts#L172) —
Kommentar im Code:
> „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 für Debug-Ergonomie, kostet aber bei multi-Read-
Plans linear Zeit. Mission mit 5 `read_*`-Tools: 5× LLM-Latency statt 1×.
### Vorschlag
Claude Codes `gW5`-Regel direkt übernehmen:
1. **Parallelisieren** wenn alle `toolCalls` einer Round `policyHint: 'read'` haben.
2. **Serialisieren** sobald eine davon `write`/`destructive` ist.
3. **Harte Grenze 10 parallel** — bei mehr: in Batches à 10.
```ts
// packages/shared-ai/src/planner/loop.ts (patch)
const allRead = calls.every(c => getPolicyHint(c.name) === 'read');
if (allRead && calls.length > 1) {
const results = await Promise.all(
calls.slice(0, 10).map(call => onToolCall(call))
);
// … append to messages in source order, not completion order
} else {
for (const call of calls) { /* sequential */ }
}
```
Wichtig: Reihenfolge in `messages` bleibt **Source-Order**, nicht
Completion-Order. Das erhält die Debug-Lesbarkeit, die der bisherige
Kommentar schützen wollte — wir verlieren also nichts, gewinnen aber
Wanduhr-Zeit.
### Aufwand
~2h. Die Information (`policyHint`) existiert bereits in der Registry.
### Voraussetzung
Verbesserung 1 (Policy-Gate) sollte vorher laufen, damit `policyHint` an der
Loop-Grenze autoritativ ist.
---
## 8. Verbesserung 5 — Sub-Agent-Pattern `I2A`-Style
### Problem
Der Plan sieht den Persona-Runner als **eigenes Service** auf :3070 vor
([`docs/plans/mana-mcp-and-personas.md`](../plans/mana-mcp-and-personas.md)).
Das ist für Deployment-Isolation sinnvoll, aber es **ist nicht** das
Claude-Code-Pattern.
Claude Codes `I2A` ist *in-process*:
- Fresh `messages[]` (kein Parent-History-Leak)
- eigenes Token-Budget
- eigene Tool-Permissions (restriktiver)
- Parent kriegt **nur die finale Summary** zurück, nicht die Zwischenschritte
- Rekursions-Grenze: 1 Level
### Vorschlag
**Zwei-Schichten-Modell**:
**(a) In-Process Sub-Loop** in `@mana/shared-ai`:
```ts
// packages/shared-ai/src/planner/sub-agent.ts (neu)
export async function runSubAgent(opts: {
readonly parentLoop: { messages: readonly ChatMessage[]; spec: ToolSpec };
readonly task: string;
readonly allowedTools: readonly string[]; // Whitelist, restriktiver als Parent
readonly maxRounds?: number; // Default 3
readonly llm: LlmClient;
readonly onToolCall: (call: ToolCallRequest) => Promise<ToolResult>;
}): Promise<{ summary: string; usage: TokenUsage }>;
```
Wird vom `Task`-ähnlichen Tool in der Registry aufgerufen. Rekursion wird
über einen Depth-Counter im `ToolContext` verhindert
(`ctx.subAgentDepth >= 1 → error`).
**(b) Persona-Runner als Out-of-Process Orchestrator** für Langläufer — der
bleibt, wie im Plan, ein eigener Service. Aber: er ruft intern denselben
`runSubAgent`-Code, nur mit höherem Round-Budget und Persona-spezifischen
System-Prompt.
### Warum zweistufig
In-Process-Sub-Agents sind für **Context-Laundering** da (dirty Recherche-
Kontext vom Parent fernhalten). Der Persona-Runner ist für **Langzeit-
Lifecycles** (eine Persona lebt über mehrere Wochen). Beides braucht dasselbe
primitive `runSubAgent`, aber andere Deployment-Modelle.
### Aufwand
~1 Woche.
---
## 9. Verbesserung 6 — Haiku-Tier für Background-Tasks
### Problem
Claude Code nutzt Haiku für hochfrequente Nebencalls:
- Quota-Check
- Topic-Detection
- Session-Summarization
- Command-Injection-Detection
- Auto-Compact-Fallback
Bei uns geht **jeder** Call an `mana-llm` mit dem Default-Modell — das ist
für Routing-Entscheidungen ("ist dieser User-Input eine Frage oder eine
Mission?") overkill und teuer.
### Vorschlag
`@mana/shared-ai` bekommt einen `TieredLlmClient`:
```ts
// packages/shared-ai/src/planner/tiered-client.ts (neu)
export function createTieredLlmClient(baseUrl: string): {
primary: LlmClient; // für runPlannerLoop
fast: LlmClient; // für Classification, Summarization, Guard
};
```
Konkrete Einsätze:
- **`compactIterations`** (§6) → `fast` statt `primary`. Spart 80 % Kosten
beim Kompressor.
- **Mission-Trigger-Klassifikation** statt Regex (heute
[`tick.ts:73-82`](../../services/mana-ai/src/cron/tick.ts#L73)): statt
`DEEP_RESEARCH_TRIGGER` als Regex ein Haiku-Call „Ist dieses Mission-
Objective Deep Research?" — robuster und überrascht nicht bei neuen
Formulierungen.
- **Reminder-Producer** (§5): Der Token-Budget-Reminder wird via `fast`
formuliert statt hartkodiert — variiert die Phrase pro Runde (weniger
Prompt-Staleness).
- **Command-Injection-Check** für Freitext-Tool-Args (in §4 erwähnt) →
`fast`.
### Modell-Mapping in mana-llm
Wir müssen `mana-llm` einen `tier: 'primary' | 'fast'` Request-Parameter geben,
der dann intern auf ein billigeres Modell routet (z. B. Ollama `llama3.1:8b`
lokal für `fast`, Claude/Gemini-primary über Cloud für `primary`).
### Aufwand
~3 Tage, fast alles in `mana-llm`.
---
## 10. Verbesserung 7 — Async-Steering-Bus `h2A`-Style
### Problem
Unser Mission-Runner tickt alle 60 Sekunden
([`tick.ts:102`](../../services/mana-ai/src/cron/tick.ts#L102)). Wenn der
User mid-Mission etwas ändert (neues Objective, Mission pausieren, neuen
Kontext hinzufügen), wird das erst im **nächsten** Tick sichtbar.
Claude Codes `h2A` ermöglicht User-Interjections *während* ein Tool läuft.
Das ist für uns **nur teilweise** relevant — Missions sind explizit als
Background-Jobs konzipiert — aber es gibt einen konkreten Use-Case:
### Konkreter Use-Case: Companion-Chat im Frontend
Die Webapp hat einen Companion-Chat (unified mana app). Der läuft interaktiv.
Heute nutzt er vermutlich
([`packages/shared-ai/src/planner/loop.ts`](../../packages/shared-ai/src/planner/loop.ts))
direkt — also dieselbe sequenzielle Loop.
**Vorschlag:** `runPlannerLoop` bekommt einen optionalen `AbortSignal` und
einen `InterruptChannel`:
```ts
export async function runPlannerLoop(opts: {
// … bestehend …
readonly signal?: AbortSignal;
readonly interruptChannel?: {
readonly take: () => ChatMessage | null; // non-blocking pull
};
}): Promise<PlannerLoopResult>;
```
Vor jedem nächsten LLM-Call: `const msg = interruptChannel?.take()` — falls
vorhanden, als `user`-Message einfügen statt die Loop stumpf weiterlaufen
zu lassen.
### Aufwand
~1 Tag.
### Ausdrücklich nicht tun
`h2A` **nicht** für `mana-ai`-Background-Missions einbauen. Der Tick-
Ansatz ist für Server-side-Missions korrekt — User-Interjections kommen dort
über den normalen Sync-Flow (Mission-Update → nächster Tick sieht es).
---
## 11. Verbesserung 8 — Deprecated-Tool-Training
### Problem
Wir haben aktuell 59+ Tools in der Registry/Shared-AI. Nicht alle sind
gleich sinnvoll für LLMs zum Planen — manche sind redundant (`notes.create`
vs. `notes.append_to_note` vs. `notes.update_note`), manche werden praktisch
nie genutzt.
Claude Codes **BatchTool-Deprecation** ist instruktiv: Anthropic hat das
Tool rausgenommen, weil das Modell selbst gelernt hat, mehrere `tool_use`-
Blocks pro Turn zu senden — das Feature war wegtrainiert.
### Vorschlag
Einen monatlichen **Tool-Usage-Audit**:
- Metrik `mana_ai_tool_invocations_total{tool}` aus bereits existierenden Metrics
- Report aller Tools unter Top-50-Percentile-Calls → Kandidat für Deprecation
- Alternative: `mcp-adapter.ts` loggt den **vom Modell geforderten aber
erfolglosen** Tool-Call — daraus wird sichtbar, welche Tools das Modell
erfindet, weil der Name „intuitiv" wäre (z. B. `notes.delete` wenn wir nur
`notes.archive` haben).
Das ist weniger eine Code-Änderung und mehr ein **Prozess**: alle 6 Wochen
einen 1-Stunden-Review, Tools konsolidieren.
### Aufwand
0 für die Infra (Metrics sind da). 1h pro Audit-Zyklus.
---
## 12. Roadmap und Priorisierung
### M1 (2 Wochen)
| # | Verbesserung | Aufwand | Abhängigkeit |
|---|------------------------------------|---------|--------------------|
| 1 | Permission-Gateway (§4) | 1 Tag | — |
| 2 | Reminder-Injection Loop-API (§5) | 4 h | — |
| 3 | Parallel-Execution für Reads (§7) | 2 h | §4 (policyHint) |
| 4 | Async-Steering im Companion (§10) | 1 Tag | — |
**M1-Outcome:** Sicherer MCP-Gateway, qualitativ bessere Mission-Planung durch
Reminders, schnellere Multi-Read-Plans, Companion-Chat abbruchbar.
### M2 (3-4 Wochen)
| # | Verbesserung | Aufwand | Abhängigkeit |
|---|--------------------------------------|----------|---------------|
| 5 | Context-Compressor `wU2` (§6) | 3-5 Tage | §9 |
| 6 | Haiku-Tier in `mana-llm` (§9) | 3 Tage | — |
| 7 | Reminder-Producer Library (§5) | 2 Tage | M1 #2 |
**M2-Outcome:** Deep-Research-Missions skalieren, Background-Calls 80 %
billiger, Reminder-Channel in Produktion.
### M3 (Persona-Runner)
| # | Verbesserung | Aufwand | Abhängigkeit |
|---|--------------------------------------|-----------|---------------|
| 8 | In-Process `runSubAgent` (§8) | 1 Woche | M1 #1, M2 #5 |
| 9 | Persona-Runner nutzt `runSubAgent` | 2 Wochen | M3 #8 |
**M3-Outcome:** Sub-Agent-Pattern einheitlich, Persona-Runner kann
komplexe Multi-Step-Personas orchestrieren, ohne Parent-Context zu
verseuchen.
### Ongoing
| # | Verbesserung | Aufwand |
|---|--------------------------------------|---------------------|
| 10 | Tool-Usage-Audit (§11) | 1h alle 6 Wochen |
---
## 13. Explizit nicht übernehmen
Nicht jedes Claude-Code-Pattern macht für uns Sinn:
- **File-Freshness-Tracking** — wir editieren keine Dateien im
Agent-Kontext. Das Äquivalent wäre „Sync-Freshness-Tracking", das aber
schon durch `mana_sync` LWW-Semantik adressiert ist.
- **`BatchTool` einführen** — das Claude-Code-Pattern ist, `BatchTool` zu
*deprecaten*, weil das Modell nativ parallele `tool_use`-Blocks sendet.
Wir sollten das direkt als Endzustand adoptieren (§7), nicht über ein
Batch-Zwischenstadium gehen.
- **yoga.wasm / Ink** — die Mana-Webapp ist SvelteKit, kein Terminal-UI.
Das UI-Layer-Muster ist für uns irrelevant.
- **`--bypassPermissions`-Mode** — für ein Multi-User-Produkt mit
Zero-Knowledge-Option darf es kein Opt-Out aus der Policy geben.
- **Der Haiku-Quota-Ping** — unser Billing läuft über `mana-credits`, wir
sehen Quota deterministisch vor dem Call, nicht probabilistisch.
---
## Related
- [`claude-code-architecture.md`](./claude-code-architecture.md) — Technische Grundlage dieses Berichts
- [`docs/plans/mana-mcp-and-personas.md`](../plans/mana-mcp-and-personas.md) — Ongoing-Plan für mana-mcp + persona-runner
- [`docs/architecture/COMPANION_BRAIN_ARCHITECTURE.md`](../architecture/COMPANION_BRAIN_ARCHITECTURE.md) §20-22 — Agent-Design der Webapp
- [`docs/reports/ai-agent-architecture-comparison.md`](./ai-agent-architecture-comparison.md) — Weiterer externer Vergleich