docs(giellie): add agent design spec and implementation plan

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Dave 2026-09-30 23:31:57 +02:00
parent 7555196166
commit 662610ca28
2 changed files with 3637 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,190 @@
# Giellie Agent — design
**Date:** 2026-09-30 · **Status:** draft, awaiting review
## Goal
Replace the sidebar "Giellie" web view (`/webview?src=hermes`) with a chat
agent that can see all WFDASH data, answer questions, do its own
calculations, and perform any action the app supports — except delete —
with the user's approval for changes. Powered by MiniMax (M3, multimodal)
using the key already stored in `/settings/ai`.
## Decisions (agreed with Dave)
| Topic | Decision |
|---|---|
| Name | **Giellie** (two l's). Sidebar label "Giellie Agent", route `/giellie`. |
| Tool access | In-process call to the existing MCP handler (approach A). No tool logic is duplicated. |
| Writes | Approve/Reject card per pending write batch. Per-chat "trust this chat" toggle auto-approves ordinary writes. |
| Deletes | Never. `*.delete` tools are removed from Giellie's catalogue. |
| Who can use it | Any logged-in user (Dave's call; reps/drivers can therefore ask for costs and margins). |
| Persistence | Chats saved per user in **one** PB collection, `giellie_chats`. |
| Input | Typed/pasted text, file upload, camera/photo button. |
| Calculations | Done by the model itself; no calculator tool. |
| Python | Not used. Production is `node:20-alpine`; the TS extractor already covers the formats. |
## Architecture
```
Browser (/giellie)
│ POST /api/v1/giellie/chat (SSE stream)
│ POST /api/v1/giellie/chat/[id]/decision
▼
giellie-service (src/lib/server/giellie-service.ts)
├─ auth: verify PB token → user ── reject 401 if invalid
├─ load/save chat in PB (giellie_chats)
├─ agent loop: MiniMax ⇄ tools
└─ tool bridge → POST handler of src/app/api/mcp/route.ts
(synthetic JSON-RPC request, Bearer AGENT_API_KEY, actor = user)
```
### Auth (important)
The app has **no server-side route auth** — the PB token is forwarded as
`x-wfdash-auth` for audit attribution only. Giellie's routes hold
`AGENT_API_KEY` and can write, so they MUST verify the token themselves:
`authRefresh()` against PB with the `x-wfdash-auth` token; 401 on failure.
The verified user id/name becomes the `actor` passed to every MCP call, so
the `audit_log` hook credits the real person.
### Tool bridge
- Catalogue: `tools/list` via the MCP handler, minus the denylist below,
mapped to Anthropic `tools` (`name` → sanitised, `.` → `__`).
- Calls: `tools/call` through the same handler; the MCP text envelope is
unwrapped to a string for the model. Large results are truncated for
the model (with a "truncated, narrow your query" note), never silently.
- Denylist (never offered, never executable): `companies.delete`,
`customers.delete`, `suppliers.delete`, `ai_settings.get`,
`ai_settings.update` (the latter two would expose/alter the API key).
### Read vs write classification
Default-deny: a tool is **read** only if it is in an explicit `READ_TOOLS`
allowlist (`*.list`, `*.get`, `*.find_*`, `*.lookup`, `*.stats`,
`*.daily_summary`, `sprint.*` reads, `audit_log.get`, `files.fetch`,
`tools.discovery`, `users.lookup`, `stock_quick_draft.search|suppliers`,
…). The allowlist is reviewed against the handlers during implementation —
notably `*.next_number` may reserve a counter and is treated as write.
Everything else is a **write**.
**Always-confirm** (ignore the trust toggle): `quotes.send_email`,
`quotes.retry_email`, `purchase_orders.send_email`,
`purchase_orders.retry_email`, `stock_purchases.send`,
`stock_purchases.send_email`, `stock_purchases.retry_email`. Anything that
emails a customer or supplier always needs a click.
### Agent loop and approvals
1. Client posts the user message (+ attachments, `trust` flag). Server
appends it to the transcript and calls MiniMax (streaming).
2. Text and tool activity stream to the client as SSE events
(`text`, `tool_start`, `tool_result`, `approval_required`, `done`, `error`).
3. Read tool calls run immediately and loop back to the model. Loop cap:
25 model turns per request.
4. If the model emits write calls, the server stores them in
`pending_action`, ends the stream with `approval_required`, and waits.
Reads in the same turn still run first.
5. `POST …/decision { approve }` executes the batch (approve) or returns
"rejected by user" as the tool results (reject), then resumes the loop
and streams again. A page reload mid-approval re-renders the card from
`pending_action`.
6. Trust toggle is sent per request, never persisted; it covers non-
always-confirm writes only.
A batch is approved or rejected as a whole. The card shows tool name and
readable arguments.
### Prompt injection
Email bodies, attachments and supplier text are untrusted data. The system
prompt says so explicitly, and writes still pass through the approval card
(the trust toggle is the residual risk and never covers sends).
### Model / config
Reuses `getActiveAiConfig()` from `ai-settings-service` (provider, key,
base URL, model, `enabled`). Giellie gets its own system prompt constant in
`src/lib/server/giellie-system-prompt.ts` (role, SA business context,
"verify by reading before answering", "do your own arithmetic, show
working for money", "never delete; cancel/archive instead", today's date).
If AI is disabled or no key is set, the page shows a link to `/settings/ai`.
**Open verification (first implementation step):** MiniMax's
Anthropic-compatible endpoint has historically been text-only. A probe
sends one image block through the configured endpoint. If rejected, image
turns use MiniMax's native vision endpoint; nothing else changes.
### Files and photos
- Upload button (xlsx, csv, docx, txt, pdf, images) and a camera button
(`<input accept="image/*" capture="environment">`).
- Server extraction via the existing `attachment-extractor`: text formats
and text-layer PDFs become text; images and scanned PDFs (rendered
pages) go to the model as image blocks.
- Originals are stored in the chat's `attachments` file field so the
transcript only holds filenames. Limits: 10 files/message, 10 MB each
(matches `maxSelect: 10` on existing attachment fields).
### Persistence — `giellie_chats` (single collection)
| Field | Type | Notes |
|---|---|---|
| `user` | relation → users, required | Owner. List/view/create/update rules: `user = @request.auth.id`. |
| `title` | text | From the first message. |
| `messages` | json (maxSize ≈ 5 MB) | Full transcript; tool results truncated when stored. |
| `attachments` | file, multi (`maxSelect` 50) | Uploads and photos. |
| `pending_action` | json | Write batch awaiting approval, else null. |
Created in PB admin **by hand first** (per AGENTS.md: the live instance is
the source of truth); a migration script
`scripts/migrations/2026-09-30-create-giellie-chats.ts` and a
`pb:migrate:giellie-chats` npm script accompany it. The server writes via
`getAdminPocketBase()` and filters by the verified user id itself.
## UI
- `src/app/(app)/giellie/page.tsx`: two panes — chat list (left), chat
(right). Message bubbles, tool-activity lines (collapsed by default,
expandable to arguments/result), approval card, composer with
attach/camera/send, "Trust this chat" toggle in the header, new-chat button.
- Sidebar: `Giellie` → `Giellie Agent`, href `/giellie`, icon `Bot`
([sidebar.tsx:141](../../../src/components/sidebar.tsx), plus the
`/webview` breadcrumb entry at ~482 → `/giellie`).
- `/webview` route is left in place (unlinked) unless Dave wants it removed.
- Next.js here is 16.2.x with breaking changes: read
`node_modules/next/dist/docs/` for route handlers/streaming before coding.
## Data-layer rule
Per AGENTS.md: business logic in `src/lib/server/giellie-service.ts`,
exposed through thin `src/app/api/v1/giellie/**` route handlers. No
`pb-helpers.ts`.
## Testing
- Unit (vitest): tool classification (read/write/always-confirm/denied,
default-deny for unknown names); denylist filtering; MCP envelope
unwrapping; result truncation; auth rejection; decision flow
(approve/reject/reload); loop cap.
- Agent loop tested with a stubbed model client (scripted tool calls).
- Manual/browser check on `:4001` (per memory, dev port): text Q&A, a
write with approval, a rejection, photo ingest.
- **No test runs MCP write tools against live PB** — memory records three
prior incidents of test fixtures polluting the real quotes board.
Write-path tests stub the bridge.
## Out of scope
Voice input; per-role tool restrictions; editing tool arguments inside the
approval card; multi-user shared chats; token/cost accounting.
## Risks
- Everyone-logged-in access exposes costs and margins to reps/drivers
(accepted; revisit with per-role restrictions).
- Trust toggle plus prompt injection could cause unwanted non-email writes;
mitigated by audit trail and always-confirm sends.
- MiniMax vision over the Anthropic-compatible endpoint is unverified (probe first).
- Manual deploy takes 5–10 min; the PB collection must exist before deploy.