mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 12:41:09 +02:00
Second phase of the Multi-Agent Workbench rollout (docs/plans/
multi-agent-workbench.md). Builds on Phase 1's identity-aware Actor.
Adds the Agent primitive — a named AI persona that owns Missions,
carries its own policy + memory, and (from Phase 3 on) drives the
Workbench lens. Everything is wired; a single user currently has one
"Mana" default agent until the UI (Phase 5) lets them create more.
Shared types (@mana/shared-ai):
- agents/types.ts: Agent, AgentState, DEFAULT_AGENT_ID/NAME constants
- policy/types.ts: AiPolicy + PolicyDecision (moved from webapp so
Agent.policy can reference it without a runtime dep on the web app)
- missions/types.ts: new optional Mission.agentId field
Webapp data layer:
- data/ai/agents/{types,store,queries,bootstrap}.ts
- Dexie schema v19 adds `agents` table (indexes on state, name,
[state+name]); sync registered under the existing ai app-id
- Encryption registry: agents.systemPrompt + agents.memory encrypted;
name/role/avatar/policy stay plaintext for search + UI rendering
- DuplicateAgentNameError thrown at write time (not a Dexie unique
index — bootstrap races between tabs would otherwise hit
ConstraintError; store now resolves via getOrCreateAgent)
- bootstrap.ts: ensureDefaultAgent + backfillMissionsAgentId. The
backfill runs once per device (localStorage sentinel) so missions
that pre-date the rollout get stamped with the default agent's id.
Called fire-and-forget from startMissionTick() during layout init.
Runner threading (already merged into d5c351d63 via Till's debug-log
commit that picked up my uncommitted edits):
- runner.ts + server-iteration-staging.ts now resolve mission.agentId
to the real Agent and build makeAgentActor with agent.name as
displayName. Missing-agent fallback keeps using LEGACY_AI_PRINCIPAL
so historical writes still attribute cleanly.
Tests: shared-ai 26/26, mana-ai 35/35, svelte-check 0 errors.
Agent store vitest suite is present but blocked by a pre-existing
\$lib alias resolution issue in the webapp vitest config that
predates this phase (proposals/store.test.ts is broken the same way
on HEAD). Will address separately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
239 lines
16 KiB
Markdown
239 lines
16 KiB
Markdown
# Plan: Multi-Agent Workbench — benannte KI-Agenten als erstklassige Bürger
|
||
|
||
**Status:** Draft, 2026-04-15
|
||
**Scope:** Upgrade vom Single-User-Workbench zum "Orchestration-Cockpit" mit mehreren benannten AI-Agenten, die autonom auf den Daten des einen Users arbeiten. Keine Team-Features (anderer User) in dieser Iteration.
|
||
**Motivation:** Heute sind Missionen "nackte Arbeitsaufträge" ohne Identität. Bei 10 laufenden Missionen fehlt die ordnende Identität. Agenten geben jedem Bündel Missionen + Persönlichkeit + Memory ein Zuhause und machen die Workbench zu einem echten Control-Room.
|
||
**Verwandte Docs:** [`docs/future/AI_AGENTS_IDEAS.md`](../future/AI_AGENTS_IDEAS.md), [`docs/architecture/COMPANION_BRAIN_ARCHITECTURE.md`](../architecture/COMPANION_BRAIN_ARCHITECTURE.md) §20, [`docs/plans/ai-mission-key-grant.md`](./ai-mission-key-grant.md).
|
||
|
||
---
|
||
|
||
## Entscheidungen (baked in)
|
||
|
||
| Frage | Entscheidung | Begründung |
|
||
|---|---|---|
|
||
| **Konzept eines Agents** | Option C (Hybrid): Metadaten + Policy + **persistente Memory** die in jede Planner-Prompt injiziert wird | 80% der "echten Agent"-UX für 20% Mehraufwand gegenüber rein additivem Modell. Memory ist kein eigener Denk-Loop, bleibt upgrade-bar. |
|
||
| **Refactor-Tiefe** | **L3** — `Actor` wird identitätsbewusst (`{kind, principalId, displayName}`) | Wir sind nicht live. Der Actor-Layer ist frisch. Der Cutover ist *jetzt* billig, später teuer. Alle Follow-ups (Per-Agent-Policy, Timeline-Filter, zukünftige Team-Features) setzen darauf auf. |
|
||
| **Scene↔Agent-Beziehung** | **Orthogonal** (Option Y). Scenes können optional `viewingAsAgentId` setzen. | "Agents sind Bürger, Scenes sind Fenster". Keine 1:1-Zwangsbindung — User kann mehrere Scenes auf denselben Agent zeigen. |
|
||
| **Agent-Memory** | **Feld auf Agent** (`memory: string`), manuell durch User editierbar. Keine Versionierung, keine Self-Modification. | Simpel. Versionierung + self-modifying ist ein eigenes Projekt (evals, drift, loops). |
|
||
| **Default-Agent-Migration** | Auto-Anlage eines "Mana"-Default-Agents bei erster Mission-Sichtung. Alle Legacy-Missions ohne `agentId` werden auf diesen migriert. User-Level-AiPolicy wird seine Policy. | Keine User-Action nötig für Bestandsdaten. User kann ihn später umbenennen / aufteilen. |
|
||
| **Agent-Löschung** | Soft-Delete (`deletedAt`). Aktive Missionen des gelöschten Agents werden nicht abgebrochen, laufen orphan-active weiter. Workbench zeigt sie grau. | Kein Daten-Verlust durch Klick. User kann bewusst Missions separat archivieren. |
|
||
| **Budget** | `maxTokensPerDay: number` pro Agent. Globaler Default ableitbar. Rollender 24h-Counter in Prometheus + Dexie-Side. | 10 Agents × paralleler Ticks können sonst schnell teuer. Harte Stopp-Semantik wenn Budget überschritten. |
|
||
| **Concurrency** | `maxConcurrentMissions` Feld pro Agent (Default: 1). `mana-ai` Tick respektiert das pro Agent. | Verhindert dass 10 Agents × N Missionen den LLM-Pool + PG-Pool überlasten. |
|
||
| **Mission Key-Grants** | Bleiben **per-Mission**, kein Redesign. UI zeigt zusätzlich Agent-Avatar + -Name im Consent-Dialog. | Crypto-Modell unverändert. Nur Display erweitert. |
|
||
| **Policy-Scope** | AiPolicy wandert von User-global auf Agent-Level. Default-Agent erbt die heutige User-Policy. | Konsistent mit "jede Mission gehört einem Agent". Verschiedene Agents können verschiedene Tool-Zugriffe haben. |
|
||
| **System-Prompt & Role** | Optional `systemPrompt: string` (technisch) + `role: string` (UI-Beschreibung). Nur `role` ist Pflicht. | Beide sind separat — Role erklärt dem User, systemPrompt erklärt dem LLM. |
|
||
| **Encryption von Agent-Feldern** | `name`, `role`, `avatar`, `policy`, `state` plaintext. `systemPrompt` + `memory` encrypted (Registry-Eintrag). | Name ist Display-Key (Search, Index). Prompt + Memory enthalten oft Kontext über den User → sensibel. |
|
||
|
||
---
|
||
|
||
## Datenmodell
|
||
|
||
### Neuer Record-Typ
|
||
|
||
```ts
|
||
// packages/shared-ai/src/agents/types.ts
|
||
export interface Agent {
|
||
readonly id: string;
|
||
readonly createdAt: string;
|
||
readonly updatedAt: string;
|
||
|
||
/** Display name, e.g. "Cashflow Watcher". Indexed (lookup key). */
|
||
name: string;
|
||
/** Emoji or media ID for the avatar. */
|
||
avatar?: string;
|
||
/** Short user-facing description: what is this agent for? */
|
||
role: string;
|
||
|
||
/** Optional prepend to every Planner prompt for missions owned by
|
||
* this agent. Encrypted at rest. */
|
||
systemPrompt?: string;
|
||
/** Persistent, user-curated context markdown. Injected into every
|
||
* Planner prompt. Encrypted at rest. */
|
||
memory?: string;
|
||
|
||
/** Per-tool allowlist/propose/deny — the heart of what the agent is
|
||
* allowed to do autonomously. Replaces the user-level AiPolicy. */
|
||
policy: AiPolicy;
|
||
|
||
/** Budget — rolling 24h window, enforced by mana-ai. */
|
||
maxTokensPerDay?: number;
|
||
/** How many missions this agent may run in parallel. Default 1. */
|
||
maxConcurrentMissions: number;
|
||
|
||
state: 'active' | 'paused' | 'archived';
|
||
deletedAt?: string;
|
||
}
|
||
```
|
||
|
||
### Erweiterte bestehende Typen
|
||
|
||
```ts
|
||
// Mission gets an owner:
|
||
export interface Mission {
|
||
// ...existing fields
|
||
/** Owning agent. Missing on legacy records; migration creates a
|
||
* "Default Mana" agent and assigns them to it. */
|
||
agentId?: string;
|
||
}
|
||
|
||
// Scene gets an optional lens:
|
||
export interface WorkbenchScene {
|
||
// ...existing fields
|
||
/** When set, this scene "belongs to" this agent — its Workbench
|
||
* timeline + proposal filters default to scope the agent. Does NOT
|
||
* restrict which apps see data; purely a lens. */
|
||
viewingAsAgentId?: string;
|
||
}
|
||
|
||
// Actor becomes identity-aware:
|
||
export interface Actor {
|
||
readonly kind: 'user' | 'ai' | 'system';
|
||
/** UUID of the principal. For 'user' that's the userId; for 'ai'
|
||
* that's the agentId; for 'system' that's a sentinel like
|
||
* 'system:projection' or 'system:mission-runner'. */
|
||
readonly principalId: string;
|
||
/** Display name cached on the record — so historic events still
|
||
* show "Cashflow Watcher" even after the agent is renamed. */
|
||
readonly displayName: string;
|
||
/** Only for kind='ai'. */
|
||
readonly missionId?: string;
|
||
readonly iterationId?: string;
|
||
readonly rationale?: string;
|
||
}
|
||
```
|
||
|
||
**Migration-Semantik:** Alte Events / Records mit `Actor {kind: 'ai', ...}` ohne `principalId` werden bei Read-Time auf den Default-Agent gemappt (Compat-Layer in `data/events/actor.ts`).
|
||
|
||
---
|
||
|
||
## Phasen
|
||
|
||
### Phase 0 — RFC + Datenmodell fixieren (0.5 Tag)
|
||
|
||
- [ ] Dieses Dokument durchsprechen, Decision-Table ist Einsatzpunkt.
|
||
- [ ] Datenmodell in `packages/shared-ai/src/agents/types.ts` anlegen.
|
||
- [ ] Encryption-Registry-Eintrag vorbereiten: `agents: { enabled: true, fields: ['systemPrompt', 'memory'] }`.
|
||
|
||
### Phase 1 — Actor-Identität (L3-Cutover) (2 Tage)
|
||
|
||
Der zentrale Refactor. Alles andere hängt davon ab.
|
||
|
||
- [ ] `Actor` in `@mana/shared-ai/src/actor.ts` erweitern um `principalId` + `displayName`. Compat-Layer: bei Read, alte Events ohne Felder → `principalId = 'legacy:user'` / `'legacy:ai-default'`, `displayName = 'Unbekannt'`.
|
||
- [ ] `USER_ACTOR` Helper: `makeUserActor(userId, displayName)`.
|
||
- [ ] Neue Helpers: `makeAgentActor(agent, mission, iteration, rationale)` und `SYSTEM_ACTOR` mit definierten `principalId`-Strings (`system:projection`, `system:mission-runner`, `system:stream`).
|
||
- [ ] **Touch-Points im Webapp** — `data/events/`, `data/ai/proposals/`, `data/ai/missions/runner.ts`, `data/ai/revert/`, alle Module-Stores die `USER_ACTOR` nutzen. Grep-Lauf, dann systematischer Rewrite.
|
||
- [ ] **Touch-Points im mana-ai** — `iteration-writer.ts` schreibt heute `{kind: 'system', source: 'mission-runner'}` → wird zu `{kind: 'ai', principalId: agentId, displayName: agent.name, missionId, iterationId}`.
|
||
- [ ] **Touch-Points in mana-sync** — keine. `sync_changes.actor` ist JSONB, akzeptiert neues Schema transparent.
|
||
- [ ] Tests anpassen: `packages/shared-ai/src/actor.test.ts`, alle Event-bezogenen Webapp-Tests.
|
||
|
||
### Phase 2 — Agent CRUD + Daten-Layer (1.5 Tage)
|
||
|
||
- [ ] Neue Dexie-Tabelle `agents` in `apps/mana/apps/web/src/lib/data/database.ts`. Indizes: `by-userId`, `by-name`, `by-state`.
|
||
- [ ] `apps/mana/apps/web/src/lib/data/ai/agents/store.ts` — CRUD: `createAgent`, `updateAgent`, `archiveAgent`, `deleteAgent`, `useAgents()` liveQuery-Hook, `useAgent(id)`.
|
||
- [ ] Encryption Registry + Dexie-Hooks fürs `systemPrompt` + `memory` Feld.
|
||
- [ ] Sync-Appregistry: `appId='ai-agents'` für die Tabelle.
|
||
- [ ] **Default-Agent-Bootstrap** — Layout-Effect beim Login: wenn 0 Agents existieren, lege "Mana" (Emoji `🤖`) an mit der aktuellen User-Level-AiPolicy.
|
||
- [ ] **Mission-Migration** — beim ersten Boot nach Rollout: alle `missions.agentId === undefined` kriegen `agentId = defaultAgent.id` (einmaliger Backfill, idempotent).
|
||
|
||
### Phase 3 — mana-ai runner verstehhagent-bewusst (1 Tag)
|
||
|
||
- [ ] `ServerMission` bekommt `agentId`. Projektion liest das Feld aus.
|
||
- [ ] **Agent-Projektion** serverseitig — analog zu `mission_snapshots` bauen wir `agent_snapshots` (LWW über `sync_changes` für `table='agents'`), scoped auf `mana_ai` Schema.
|
||
- [ ] `planOneMission` lädt den Agent, injiziert `systemPrompt + memory` in die Planner-Messages vor der Mission-Instruction. Budget-Check: wenn Agent-Budget überschritten → Mission skip mit `state='budget-exceeded'`, Metrik `mana_ai_budget_exceeded_total{agent=}`.
|
||
- [ ] **Per-Agent Concurrency-Guard** — der Tick tracked `activeMissionsByAgent` in memory, weiter nur wenn unter `maxConcurrentMissions`.
|
||
- [ ] **Audit + Metriken** — `mana_ai_agent_decisions_total{agent, decision}` (decision = `ran | skipped-budget | skipped-concurrency | skipped-paused`).
|
||
- [ ] Server-iteration-writer: Actor-JSON bekommt `principalId = agentId`, `displayName = agent.name`.
|
||
|
||
### Phase 4 — Policy pro Agent (1 Tag)
|
||
|
||
- [ ] `AiPolicy` wandert von `$lib/data/ai/policy.ts` (user-scoped Store) auf ein Feld am Agent. Store bleibt als Helper, nimmt aber Agent als Argument.
|
||
- [ ] `pendingProposals` Writer: liest Policy vom auslösenden Agent, nicht mehr global.
|
||
- [ ] `mana-ai`s tools.ts filtert die Tool-Allowlist per Agent-Policy vor jedem Tick.
|
||
- [ ] Settings-Page "Automatisierungs-Einstellungen" wandert zur Agent-Detail-Seite (jeder Agent hat seine eigene Policy-Tabelle). Legacy-Settings-Route redirected zum Default-Agent.
|
||
|
||
### Phase 5 — UI: Agents-Modul + Scene-Binding (2 Tage)
|
||
|
||
- [ ] Neues Modul `apps/mana/apps/web/src/lib/modules/ai-agents/ListView.svelte` — `/companion/agents` oder als App-Tab "Agents". CRUD + Policy-Editor + Memory-Editor + Budget/Concurrency-Felder.
|
||
- [ ] `AgentPicker.svelte` Komponente — Dropdown mit Avatar + Name, einsetzbar in Mission-Create-Flow + Scene-Settings.
|
||
- [ ] Mission-Create-Flow (`ai-missions/ListView.svelte`): neuer Schritt "Welcher Agent führt das aus?". Default: letzter-verwendeter oder "Mana".
|
||
- [ ] `SceneAppBar.svelte` — wenn `scene.viewingAsAgentId` gesetzt: Agent-Avatar-Dot auf dem Tab, Tooltip mit Name.
|
||
- [ ] Scene-Settings-Dialog: "An Agent binden" (optional) + "Bindung lösen".
|
||
|
||
### Phase 6 — Observability (0.5 Tag)
|
||
|
||
- [ ] AI-Workbench-Timeline (`ai-workbench/ListView.svelte`): Filter-Dropdown "Alle Agents | [Agent1] | [Agent2] …". Bucket-Header zeigt Agent-Avatar + -Name statt nur `missionId`.
|
||
- [ ] `AiProposalInbox`-Card: Agent-Avatar + -Name oben links, Tooltip mit Mission-Titel + Rationale.
|
||
- [ ] Budget-Anzeige: mini-Fortschrittsbalken im Agent-Tile ("23% Budget heute").
|
||
|
||
### Phase 7 — Rollout (0.5 Tag)
|
||
|
||
- [ ] Feature-Flag `PUBLIC_MULTI_AGENT_WORKBENCH=true` default (sind wir pre-live). Setting kann genutzt werden falls wir graduelle Migration im Webapp wollen — aktuell voll an.
|
||
- [ ] Docs-Update: [`apps/mana/CLAUDE.md`](../../apps/mana/CLAUDE.md) — AI-Workbench-Abschnitt erweitern. `services/mana-ai/CLAUDE.md` → Agent-Projektion + per-Agent-Metriken.
|
||
- [ ] User-Doc in `apps/docs/src/content/docs/architecture/security.mdx` — Abschnitt zu Agenten-Scope (ein Agent sieht deine Daten genau wie du; Mission-Key-Grants pro Agent sichtbar).
|
||
|
||
**Gesamtaufwand:** ~8–9 Arbeitstage.
|
||
|
||
---
|
||
|
||
## Dateien (neu / modifiziert)
|
||
|
||
**Neu:**
|
||
- `packages/shared-ai/src/agents/types.ts` + `index.ts`
|
||
- `packages/shared-ai/src/agents/default-agent.ts` (Bootstrap-Konstanten)
|
||
- `apps/mana/apps/web/src/lib/data/ai/agents/store.ts` + `queries.ts`
|
||
- `apps/mana/apps/web/src/lib/modules/ai-agents/ListView.svelte` + `module.config.ts`
|
||
- `apps/mana/apps/web/src/lib/components/ai/AgentPicker.svelte`
|
||
- `services/mana-ai/src/db/agents-projection.ts`
|
||
- `docs/plans/multi-agent-workbench.md` (dieses Dokument)
|
||
|
||
**Modifiziert:**
|
||
- `packages/shared-ai/src/actor.ts` — Identity-erweitert
|
||
- `packages/shared-ai/src/missions/types.ts` — `agentId`
|
||
- `packages/shared-ai/src/policy.ts` — Policy-Shape bleibt, Owner wandert auf Agent
|
||
- `apps/mana/apps/web/src/lib/types/workbench-scenes.ts` — `viewingAsAgentId?`
|
||
- `apps/mana/apps/web/src/lib/data/crypto/registry.ts` — `agents` Eintrag
|
||
- `apps/mana/apps/web/src/lib/data/database.ts` — Tabelle + Indizes
|
||
- `apps/mana/apps/web/src/lib/data/events/actor.ts` — Compat-Layer
|
||
- `apps/mana/apps/web/src/lib/data/ai/missions/runner.ts` — Agent-bewusst
|
||
- `apps/mana/apps/web/src/lib/data/ai/policy.ts` — Agent-scoped
|
||
- `apps/mana/apps/web/src/lib/components/workbench/SceneAppBar.svelte` — Agent-Avatar
|
||
- `apps/mana/apps/web/src/lib/modules/ai-missions/ListView.svelte` — AgentPicker
|
||
- `apps/mana/apps/web/src/lib/modules/ai-workbench/ListView.svelte` — Agent-Filter
|
||
- `services/mana-ai/src/db/missions-projection.ts` — `agentId` durchreichen
|
||
- `services/mana-ai/src/db/iteration-writer.ts` — Agent-Actor
|
||
- `services/mana-ai/src/cron/tick.ts` — Budget + Concurrency
|
||
- `services/mana-ai/src/metrics.ts` — Per-Agent-Metriken
|
||
|
||
---
|
||
|
||
## Risiken & Gegenmassnahmen
|
||
|
||
| Risiko | Mitigation |
|
||
|---|---|
|
||
| **Actor-Cutover bricht alle historischen Events** | Compat-Layer in `actor.ts` bei Read. Alte Events fallen auf `'legacy:*'` principalIds zurück, Timeline zeigt "Unbekannt". Kein Data-Loss. |
|
||
| **Default-Agent-Bootstrap-Race** beim Login (zwei Tabs starten parallel) | Bootstrap via `getOrCreate`-Pattern mit Dexie-Transaction. Idempotent: zweiter Call findet existierenden Agent. |
|
||
| **Agent-Memory wird zu lang → LLM-Prompt explodiert** | Harte 4000-char Warnung in der Memory-Editor-UI. Runner trunkiert auf 8000 chars mit Log-Warnung. |
|
||
| **Systemprompt-Injection über Memory-Feld** | Memory + systemPrompt werden in `<agent_context>...</agent_context>` Delimiter gewrappt. Output weiterhin via `parsePlannerResponse` validiert. |
|
||
| **Budget-Exhaustion während laufender Mission** | Check vor neuem Planner-Call, nicht mid-mission. Laufende Iteration darf fertig werden. Nächste Iteration der gleichen Mission im nächsten Tick wartet bis Counter-Reset. |
|
||
| **Concurrency-Guard im Single-Process-Runner ist race-free**, beim Multi-Instance-Deploy nicht | Advisory-Locks auf `mana_ai.agent_concurrency` bei Multi-Instance-Rollout (nicht in dieser Phase). |
|
||
| **Soft-deleted Agent hat laufende Mission → UI zeigt Ghost-Agent** | Ghost-Agent-Marker: greyer Avatar + "archiviert" Tooltip. Missions laufen fertig, Revert bleibt möglich. |
|
||
|
||
---
|
||
|
||
## Nicht-Ziele
|
||
|
||
- **Kein Agent-to-Agent Messaging.** Agents laufen unabhängig. Wenn später nötig, ist das ein eigenes Projekt.
|
||
- **Kein Meta-Planner über Agents.** Agents erzeugen sich keine Missionen selbst. User bleibt Mission-Creator (optional: Templates als Hilfsmittel).
|
||
- **Keine Team-Features.** Andere User oder geteilte Daten kommen in einem separaten Plan nach dieser Iteration.
|
||
- **Keine Agent-Memory-Self-Modification.** Memory wird nur vom User editiert. Evals + Drift-Kontrolle + Safe-Updates sind ein eigenes Thema.
|
||
- **Keine Per-Agent-Encryption-Domains.** Alle Agents sehen alle Daten des einen Users. Mission-Key-Grants bleiben per-Mission.
|
||
- **Keine neuen UI-Konzepte jenseits Modul-Tab + Picker.** Wir bauen nichts neu, was sich nicht im bestehenden Scene/App-Modell abbilden lässt.
|
||
|
||
---
|
||
|
||
## Offene Fragen (vor Phase 1)
|
||
|
||
1. **Agent-Name-Uniqueness:** erzwingen oder erlauben? → Empfehlung: erzwingen (Dexie-Unique-Index), UI-Error bei Duplikat.
|
||
2. **"system"-Actor-Renaming:** heutige `{kind:'system', source:'projection'}` Actors — kriegen `principalId = 'system:projection'`? Oder je-System-Source eigener principalId? → Empfehlung: je Source (`system:projection`, `system:stream`, `system:mission-runner`, `system:migration`). Einfacher filterbar.
|
||
3. **Legacy-User-Policy-Migration:** eine einmalige Wanderung zur Default-Agent-Policy, und danach ist die User-Setting-UI weg? Oder behalten wir einen "User-wide override"? → Empfehlung: wandern lassen, UI weg. Sauber.
|
||
4. **Scene-Agent-Binding-Default:** wenn User eine neue Scene anlegt, bind sie an den "aktuellen Agent" oder explicit leer? → Empfehlung: explicit leer. User bindet manuell wenn er will.
|