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>
Replaces the single-line summary ("Planner failed: fetch …") with
full diagnostic detail: error name + message + last-active phase +
stack trace, all persisted onto the iteration itself. UI expands a
collapsed details block next to each failed iteration, so the user
can see *where* it broke ("TypeError in calling-llm") without opening
DevTools.
Paired with a one-click Retry button that re-runs the mission under
the same config — useful while debugging a flaky backend (GPU server
down, Gemini quota, etc.).
- `packages/shared-ai/src/missions/types.ts` — new
`MissionIteration.errorDetails: { name, message, phase?, stack? }`
- `finishIteration` accepts the field, deep-clones it, and also now
clears the transient phase markers (currentPhase/phaseStartedAt/
phaseDetail/cancelRequested) whenever an iteration finalises — keeps
the schema honest (phases are sub-state of \`running\` only).
- `runMission` tracks \`lastPhase\` via a new \`enterPhase\` helper that
wraps setIterationPhase. The catch handler populates errorDetails
with lastPhase + message + stack.
- ListView: \`<details>\` block under each failed iteration + Retry
button (disabled while another run is in-flight).
77/77 webapp tests still green; svelte-check clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes the "iteration is running, no feedback" black hole. The user now
sees, per running iteration:
⏳ Frage Planner · frage Planner an ⏱ 23s
[Abbrechen]
Phases (\`IterationPhase\`):
resolving-inputs → calling-llm → parsing-response →
staging-proposals → finalizing
The runner advances through these via \`setIterationPhase\` between each
await, writing currentPhase + phaseDetail + phaseStartedAt onto the
iteration. UI reads them via Dexie liveQuery — no polling.
Cancel:
- \`requestIterationCancel\` writes cancelRequested=true on the iteration
- runner polls \`isCancelRequested\` between every phase + per stage step
- cancellation finalises as \`failed\` with summary \`'cancelled by user'\`
- UI button is disabled + relabelled "Wird abgebrochen…" until the next
poll picks it up
Hard timeout: 90 s wall-clock per iteration via Promise.race against a
CancelledError. Wedged backends (e.g. flaky mana-llm) fail fast with
"timeout after 90s" instead of sitting in \`running\` forever.
Elapsed counter is a \$state variable ticking once a second, scoped to
the ListView component — Dexie isn't touched. Auto-cleaned on
component destroy.
shared-ai re-exports \`IterationPhase\` so server-side mana-ai can
inspect the same phase enum (no consumer there yet, but the type is
ready for the run-status endpoint planned in HEALTH page).
77/77 webapp tests still green; svelte-check clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Foundation for Phase 2+ of the Mission Key-Grant flow: lets mana-ai
execute missions that depend on encrypted inputs (notes/tasks/events/
journal/kontext) without needing an open browser tab. Opt-in per
mission, Zero-Knowledge users excluded.
- Canonical HKDF-SHA256 derivation (scope-bound via tables + recordIds
in the HKDF info string → scope changes invalidate the grant
cryptographically, not just via a runtime check)
- Mission.grant field on the shared Mission type
- Golden snapshot + drift-guard test so webapp wrap path and mana-auth
wrap endpoint can't silently diverge
- Ideas backlog at docs/future/AI_AGENTS_IDEAS.md
- Full rollout plan at docs/plans/ai-mission-key-grant.md
- COMPANION_BRAIN_ARCHITECTURE.md §21 captures the flow + privacy
guarantees + non-goals
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Single source of truth for AI Workbench types shared between the webapp
(Vite/SvelteKit) and the server-side mana-ai Bun service. Prevents the
two runtimes from drifting on prompt shape or mission structure.
- `@mana/shared-ai` package:
- `actor.ts` — Actor union (user | ai | system) + helpers, mirrors the
webapp's runtime type so server-side consumers parse incoming actors
without re-declaring
- `missions/types.ts` — Mission, MissionCadence, MissionInputRef,
MissionIteration, PlanStep, MissionState. Adds optional
`iteration.source: 'browser' | 'server'` to distinguish foreground
vs server-produced iterations (groundwork for proposal write-back)
- `planner/prompt.ts` — `buildPlannerPrompt` pure function
- `planner/parser.ts` — `parsePlannerResponse` strict JSON validator
- Vitest smoke tests (2) cover prompt → parse round-trip + unknown-
tool rejection
- Webapp:
- `missions/types.ts` re-exports from shared-ai, keeps webapp-local
`MISSIONS_TABLE` constant + `planStepStatusFromProposal` bridge
- `missions/planner/{types,prompt,parser}.ts` become re-export stubs
so existing imports keep working unchanged
- Existing webapp tests (60) continue to pass — the wire code didn't
move, just its home
Next: mana-ai service imports buildPlannerPrompt/parsePlannerResponse
from shared-ai + wires mana-llm + writes iteration back as a
'source=server' row (tracked in services/mana-ai/CLAUDE.md).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>