docs(adr): ADR-0029 agent runtime architecture #324

Merged
james merged 1 commit from 48-adr-agent-runtime into main 2026-06-28 19:31:16 +00:00
Owner

Resolves the load-bearing runtime decisions for epic #47 before any code lands (#48). Design only — no code, per the ticket's acceptance criteria. Linked from docs/adr/README.md.

Numbered 0029 (0028 is the export/import archive ADR).

Decisions (one per scope bullet)

  1. Provider — pluggable LlmProvider abstraction: Anthropic native (default, best agentic tool-use) + OpenAI-compatible (OpenAI/OpenRouter/local Ollama for zero third-party). Per-user provider/model/base-URL; default model configurable (claude-sonnet-4-6 out of box, opt-up to claude-opus-4-8). Applies to the built-in agent only — MCP runs the external client's own model.
  2. Loop locationserver-side for both surfaces; a shared apps/api/lib/agent/ core (tool registry + repo-scoped execution + audit) driven by the PWA chat route and POST /api/mcp (PAT-authed). Browser never holds the loop or keys.
  3. StreamingSSE over the existing auth middleware; SW bypasses it so the offline shell is untouched. Not the Vercel AI SDK (couples client; rough on Expo). MCP uses its own streamable-HTTP framing.
  4. Conversation schemaconversations + messages, per-user FK, normalized provider-agnostic message model (assistant tool_calls[], tool-role results by tool_call_id); JSON-as-text for both engines.
  5. API keysper-user, AES-256-GCM encrypted at rest with an instance secret (CAROL_ENCRYPTION_KEY); new reversible-crypto helper (PATs are one-way hashed). Local Ollama needs none; key is write-only, never logged/returned.
  6. Write-confirmation — write tools return a proposal; turn ends with a persisted pending_action + conversation awaiting_confirmation (no HTTP held across the click). PWA confirm → apply via repo layer + audit, then resume the loop; MCP returns the proposal as the tool result and the external client confirms.

Consequences (new crypto surface, adapter-translation cost, one-way SSE deferring voice, resume state-machine complexity, no shared instance key) and the rejected alternatives (Anthropic-only, local-only, per-instance env key, Vercel AI SDK, WebSocket, raw provider-blob storage) are documented in the ADR.

Model IDs grounded against the current Claude model reference (Opus 4.8 / Sonnet 4.6 / Haiku 4.5).

Closes #48

🤖 Generated with Claude Code

Resolves the load-bearing runtime decisions for epic #47 before any code lands (#48). Design only — no code, per the ticket's acceptance criteria. Linked from `docs/adr/README.md`. Numbered **0029** (0028 is the export/import archive ADR). ## Decisions (one per scope bullet) 1. **Provider** — pluggable `LlmProvider` abstraction: Anthropic native (default, best agentic tool-use) + OpenAI-compatible (OpenAI/OpenRouter/**local Ollama** for zero third-party). Per-user provider/model/base-URL; default model configurable (`claude-sonnet-4-6` out of box, opt-up to `claude-opus-4-8`). Applies to the **built-in agent only** — MCP runs the external client's own model. 2. **Loop location** — **server-side for both surfaces**; a shared `apps/api/lib/agent/` core (tool registry + repo-scoped execution + audit) driven by the PWA chat route and `POST /api/mcp` (PAT-authed). Browser never holds the loop or keys. 3. **Streaming** — **SSE** over the existing auth middleware; SW bypasses it so the offline shell is untouched. Not the Vercel AI SDK (couples client; rough on Expo). MCP uses its own streamable-HTTP framing. 4. **Conversation schema** — `conversations` + `messages`, per-user FK, **normalized provider-agnostic** message model (assistant `tool_calls[]`, `tool`-role results by `tool_call_id`); JSON-as-text for both engines. 5. **API keys** — **per-user, AES-256-GCM encrypted at rest** with an instance secret (`CAROL_ENCRYPTION_KEY`); new reversible-crypto helper (PATs are one-way hashed). Local Ollama needs none; key is write-only, never logged/returned. 6. **Write-confirmation** — write tools return a **proposal**; turn ends with a persisted `pending_action` + conversation `awaiting_confirmation` (no HTTP held across the click). PWA confirm → apply via repo layer + audit, then resume the loop; MCP returns the proposal as the tool result and the external client confirms. Consequences (new crypto surface, adapter-translation cost, one-way SSE deferring voice, resume state-machine complexity, no shared instance key) and the rejected alternatives (Anthropic-only, local-only, per-instance env key, Vercel AI SDK, WebSocket, raw provider-blob storage) are documented in the ADR. Model IDs grounded against the current Claude model reference (Opus 4.8 / Sonnet 4.6 / Haiku 4.5). Closes #48 🤖 Generated with [Claude Code](https://claude.com/claude-code)
docs(adr): ADR-0029 agent runtime architecture
All checks were successful
Commits / Conventional Commits (pull_request) Successful in 5s
PR / OSV-Scanner (pull_request) Successful in 1m38s
PR / OpenAPI (pull_request) Successful in 1m52s
PR / Static analysis (pull_request) Successful in 1m56s
PR / pnpm audit (pull_request) Successful in 2m16s
PR / Lint (pull_request) Successful in 2m39s
PR / Client (web export smoke) (pull_request) Successful in 3m4s
PR / Package age policy (soft) (pull_request) Successful in 1m6s
PR / Typecheck (pull_request) Successful in 3m10s
PR / Test (postgres) (pull_request) Successful in 3m18s
PR / Trivy (image) (pull_request) Successful in 1m37s
PR / Build (pull_request) Successful in 3m21s
Secrets / gitleaks (pull_request) Successful in 1m10s
PR / Test (sqlite) (pull_request) Successful in 3m28s
PR / Coverage (soft) (pull_request) Successful in 1m52s
fcccf01512
Resolves the load-bearing runtime decisions for epic #47 before any code
lands: pluggable LLM provider (Anthropic native default + OpenAI-
compatible incl. local Ollama) for the built-in agent only; server-side
agent loop shared by the PWA chat and the /api/mcp endpoint; SSE
streaming to the PWA; normalized provider-agnostic conversations/messages
schema; per-user provider keys encrypted at rest; and a propose-then-
confirm write handoff backed by a persisted pending_action + audit trail.

No code — design only. Linked from docs/adr/README.md.

Closes #48

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 82.3% ≥ 50%
Branches 73.4% ⚠️ ≥ 75%
Functions 91.6% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 82.3% ✅ | ≥ 50% | | Branches | 73.4% ⚠️ | ≥ 75% | | Functions | 91.6% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.
james merged commit 15d3866e40 into main 2026-06-28 19:31:16 +00:00
james deleted branch 48-adr-agent-runtime 2026-06-28 19:31:16 +00:00
Sign in to join this conversation.
No description provided.