Files
Void-Homelab/docs/superpowers/specs/2026-06-01-void-v2-plan5-companion-chat-design.md
root 1cc2abf95c docs(plan5): companion chat design spec
Scope B (knowledge assistant + drafting via pending_changes approval chain),
lean Anthropic-SDK runtime (supersedes the top-level spec's Mastra wording),
extensible shared tool registry (search/read/propose_change/context), per-Space
ambient companion, SSE turn lifecycle, inline draft card synced with the Inbox,
structural prompt-injection containment. Ignore .superpowers/ brainstorm dir.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 17:49:08 +10:00

179 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Void 2.0 — Plan 5: Companion Chat (Design Spec)
**Date:** 2026-06-01
**Status:** Approved for planning
**Builds on:** Plans 14 (foundation, API + shell, capture, workers). Version baseline `2.0.0-alpha.4`.
## 1. Purpose
Turn the right-rail stub (`public/components/rightrail.js`) into a working, always-visible
**companion chat**: an AI assistant scoped to the current Space that can
1. answer questions about the Void's knowledge (search + read), and
2. **propose changes** (create task, edit page, add ref, …) that flow through the
existing `pending_changes` approval chain — never direct writes.
This is **scope B** of the agent model: knowledge assistant **+** drafting through approval.
It is deliberately a vertical slice that exercises the locked agent model end-to-end
(suggest tier → `pending_changes` → audit) while leaving multi-persona/local-model work
(**scope C**) as a clean follow-on.
### Spec reconciliation
The top-level design spec (`2026-05-31-void-v2-design.md`) describes the agent runtime as
*"Claude subprocess + Ollama via Mastra."* That is **superseded** by this plan: the Plan 5
runtime is a **lean runtime built directly on the Anthropic SDK — no Mastra, no subprocess.**
The per-agent `model` field preserves the option to route specific agents to local Ollama
models later (scope C).
## 2. Scope
### In scope (v1)
- Lean agent runtime on the Anthropic SDK with a tool-use loop and token streaming.
- A shared, extensible tool registry with four tools: `search`, `read`, `propose_change`, `context`.
- Chat API: persist user message + SSE streaming response endpoint, over the existing
`conversations`/`messages` repos.
- One **ambient companion conversation per Space**; current view passed as ephemeral context.
- Right-rail chat UI: the approved turn rendering, live tool-activity chips, streamed answer,
inline draft card (a second view onto a `pending_changes` row), collapse-to-tab + drag-resize.
- Anthropic API key resolved via the existing `vault_path` env/file resolver.
- Tests: tool handlers, runtime loop (mocked Anthropic client), SSE/API integration, security.
### Out of scope (deferred)
- **Scope C — multi-agent personas** and per-persona local (Ollama) models.
- The **MCP server** surface (the tool registry is designed so MCP re-exposes it later).
- First-class, attachable **`Conversation` entities** bound to a Task/Project/Resource.
- **Vaultwarden** secrets swap (tracked separately; env/file stopgap is intentional here).
- Adding tools beyond the four (the registry makes this a drop-in later).
## 3. Architecture
```
Browser (right rail)
│ POST /api/conversations/:id/messages (persist user turn)
│ GET /api/conversations/:id/stream (SSE) (run turn, stream tokens + tool events)
void-server (Node / Express)
├─ lib/api/routes/chat.js SSE endpoint + message persistence
├─ lib/ai/agent/runtime.js Anthropic SDK tool-use loop, streaming
├─ lib/ai/agent/registry.js shared {name, schema, handler} tool registry
├─ lib/ai/agent/tools/*.js search · read · propose_change · context
└─ existing: repos (conversations, messages, pending_changes, agents),
capability check, audit log, hybrid search, vault_path resolver
Anthropic API (default) · Postgres (CT 310) · Ollama (embeddings/search, existing)
```
### 3.1 Agent runtime (`lib/ai/agent/`)
- **Lean loop, Anthropic SDK directly.** Build the message list (system prompt + prior turns
+ new user turn), call Claude with the registered tool schemas, execute any returned
`tool_use` blocks via the registry, append `tool_result`s, and loop until Claude returns a
final text answer. Stream text deltas to the caller as they arrive.
- **Model resolution.** The model id comes from the agent record's `model` field; defaults to a
current Claude model when unset. (This is the seam for scope-C local models.)
- **API key.** Resolved through the existing `vault_path` resolver (`env:ANTHROPIC_API_KEY` or
`file:/path`) — never hard-coded, never placed in the prompt.
- **Cost guardrails.** Per-turn `max_tokens` cap; context assembled from *relevant* material
(recent turns + tool results) rather than the whole Space; prompt caching applied at
implementation time (use the `claude-api` skill).
### 3.2 Shared tool registry (`lib/ai/agent/registry.js`)
Each tool is `{ name, description, input_schema (JSON Schema), handler(args, ctx) }`. The runtime
consumes the registry now; a future MCP server re-exposes the same definitions verbatim. The
registry is **extensible** — adding a tool is registering a new entry, no runtime changes.
`ctx` carries the acting agent, the current Space/view, and an actor for audit. Handlers enforce
capability tier; this is the single place mutation policy lives.
**v1 tools**
| Tool | Purpose | Mutates? |
|------|---------|----------|
| `search` | Hybrid FTS+vector search (wraps existing `/api/search` logic), Space-scoped | no |
| `read` | Fetch a page/ref/task/conversation by id for grounding | no |
| `context` | Resolve the current view's entity (type + id → summary) | no |
| `propose_change` | Emit a structured draft → one `pending_changes` row | **only via approval** |
`propose_change` **never applies** a change. It writes a `pending_changes` row (reusing
`applyPendingChange`'s entity/op vocabulary) and returns its id. Application happens only on
explicit user approval, through the existing dispatch + audit path.
### 3.3 Chat API (`lib/api/routes/chat.js`)
- `POST /api/conversations/:id/messages` — persist the `user` message (role=`user`), validate the
Space scope, return the message id.
- `GET /api/conversations/:id/stream?messageId=…`**SSE**. Runs the runtime for the latest user
turn and emits events:
- `tool``{ tool, args_summary, status }` for the activity chips
- `delta` — streamed assistant text
- `draft``{ pending_change_id, summary }` when `propose_change` fires
- `done``{ assistant_message_id, usage }`
- `error``{ message }`
- On completion, persist **one** `assistant` message whose `metadata` holds the tool-call trace,
any draft ids, the model id, and token usage. No `role=tool` rows.
- Conversation resolution: one ambient conversation per Space (find-or-create by `space_id` +
default agent). The current view (`{entityType, entityId}`) is passed per request and exposed to
the agent via the `context` tool / system prompt — it is **not** persisted per message beyond the
trace.
### 3.4 Right-rail UI (`public/components/rightrail.js`)
- **Turn rendering (approved):** label-led (`YOU` / agent name) with conversational left/right
alignment and a thin colored accent edge per side; tool steps render as dim, monospace,
left-aligned ledger lines (the live activity chips).
- **Streaming:** consume SSE; append `delta` text into the in-progress assistant turn; render
`tool` events as chips as they arrive.
- **Draft card:** on a `draft` event (or when rendering history with draft ids), show an inline
approve/reject card bound to the `pending_changes` row. Approving/rejecting hits the existing
pending-changes endpoint; the same row also appears in the Inbox view, and state stays in sync.
- **Chrome:** collapse to a slim tab; drag-handle resize; per-Space default agent shown in the
header.
- **Safety:** all DOM via `dom.js` (`el()`/`mount()`/`safeHref()`); assistant markdown rendered
only through the sanitized `html:` path (marked + DOMPurify). Never `innerHTML` from API data.
## 4. Security
- **Prompt-injection containment is structural, not heuristic.** The companion reads untrusted
content (Karakeep imports, fetched URLs, PDFs/OCR). Defense: `propose_change` cannot auto-apply;
tool handlers enforce the agent's capability tier (default `suggest``pending_changes`);
read/search tools cannot mutate. Worst case from a poisoned document is a draft the owner must
explicitly approve.
- **Secrets** never enter the system prompt or tool output; the API key lives only in the
resolver-backed config.
- **Capability enforcement** is centralized in the registry handlers and reuses the existing
capability check + audit emission (`actor_kind`, `agent_id`, diff).
- **Cost/abuse:** per-turn token cap and bounded tool-call iterations (loop guard) to prevent
runaway loops.
## 5. Error handling
| Failure | Behavior |
|---------|----------|
| Anthropic API error/timeout | `error` SSE event, clean message in rail, **no partial mutation**; user turn already persisted, turn retryable |
| Tool handler error | failed-status `tool` chip; loop continues or ends gracefully with an explanatory answer |
| SSE disconnect mid-turn | user message persisted; assistant turn can be re-requested |
| Ollama down (search/embeddings) | existing FTS-only fallback in hybrid search |
| Capability denied | tool returns a denial result; agent explains it can only suggest |
## 6. Testing
- **Tool handlers:** unit tests per tool; `propose_change` writes the correct `pending_changes`
row and **never** applies; capability tier enforced (suggest-tier agent cannot direct-write).
- **Runtime loop:** tests against a **mocked Anthropic client** returning deterministic
`tool_use` / text fixtures — verify multi-step tool loops, loop guard, final message
persistence + metadata shape.
- **API/SSE:** integration test that posts a user message and consumes the SSE stream
(`tool``delta``draft``done`), asserting persisted assistant message + draft row.
- **Security:** injected-content test proving a "delete everything" instruction yields only a
draft requiring approval.
- **UI smoke:** per the existing matrix — render a turn, stream, approve a draft, confirm Inbox
sync, collapse/resize.
## 7. Future upgrades (explicit hooks)
- **Scope C personas** — distinct Cradle personas with their own `model` (local Ollama via the
same runtime seam) and capability scopes, switchable per view.
- **More tools** — the extensible registry accepts new `{name, schema, handler}` entries with no
runtime changes.
- **MCP server** — re-exposes the shared tool registry to external agents (Open WebUI, OpenClaw).
- **Vaultwarden** — swap the env/file `vault_path` for Vaultwarden item ids (pointer change only).
- **First-class `Conversation` entities** — explicit threads attachable to Task/Project/Resource,
alongside the ambient per-Space companion.