docs(ask): agent actions

docs/modules.md §7.1: threads, memory, files, free queries/virtual
datasets, the propose/confirm/apply/undo gateway, both PDF export paths
(manual answer export and Ask-made reports via make_pdf_report),
images, and change requests — with the actor/x-ask-user model and the
three new PocketBase collections.

docs/data-model.md §4.17: field-by-field ask_threads/ask_messages/
ask_log reference, cross-linked from modules.md via explicit anchors.

docs/api-reference.md §12/§12b: /api/chat's new body shape and its
thread/action/chart/image/pdf SSE events, plus every new agent-actions
route (picker, threads, attachments, confirm-card actions, memories,
change requests) and their auth-matrix entries; the pb-files proxy
entry now notes its ask_messages admin-token path.

README.md: the intro, the modules table (adds Change requests), and
the API surface summary now describe an agent that acts, not just a
read-only chat.

Also marks Tasks 1–12 and 9b's implementation checkbox done in the plan
doc (docs/superpowers/plans/2026-09-26-ask-agent-actions.md) — the
remaining unchecked box is 9b's live check, skipped because
ask_threads/ask_messages/ask_log aren't applied on the target
PocketBase yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DEls 2026-09-26 21:32:53 +02:00
parent 88011eb5b8
commit fecc885d3e
5 changed files with 244 additions and 15 deletions

View file

@ -5,8 +5,11 @@ South African civil construction company, backed by **PocketBase**.
Tracks, per construction site: contracts, employees & timesheets,
vehicles (fuel + plant), fuel logs, plant hour meters, fixed costs,
income claims, purchase orders + line items, and delivery notes with
OCR-driven receipting. Includes an LLM chat ("Ask") with a read-only
tool layer over the same data, and a read-only MCP server.
OCR-driven receipting. Includes an LLM agent ("Ask") that reads the same
data the dashboard uses, measures anything with free queries and its own
charts, reads any file you attach, remembers lasting facts, changes data
through confirm-first proposed actions with undo, and files its own PDF
reports and app change requests — plus a read-only MCP server.
> **Default login PIN is `2468`.** Change `NEXT_PUBLIC_APP_PIN` in
> production. There is no PIN bypass — the old `?demo=1` shortcut was
@ -43,7 +46,7 @@ npm run mcp # uses the PocketBase creds in .env.local
## Modules
The sidebar (`src/components/layout/sidebar.tsx`) has four top-level
The sidebar (`src/components/layout/sidebar.tsx`) has five top-level
pages and three groups. Each is documented in
[`docs/modules.md`](./docs/modules.md).
@ -64,7 +67,8 @@ pages and three groups. Each is documented in
| `/dashboard/financial/purchase-orders[/[poId]]` | Purchase Orders | Every PO, fulfilment %, line editing, verify |
| `/dashboard/financial/delivery-notes` (+ `/inbox`, `/work-queue`, `/document/[id]`) | Delivery Notes | Upload → OCR → match → post pipeline and its review queues |
| `/dashboard/financial/suppliers[/[supplierId]]` | Suppliers | Ordered / delivered / outstanding per supplier |
| `/dashboard/chat` | Ask | Read-only LLM chat with the same metrics the dashboard uses |
| `/dashboard/chat` | Ask | Agent: free queries + charts, files, memory, proposed data changes with undo, PDF reports, images |
| `/dashboard/change-requests` | Change requests | App changes ("can we add this page/column/button?") Ask has logged for the developer |
| `/clock` | MEGA Clock | Installable phone PWA: worker badge scan in/out + vehicle hour-meter scan |
Old paths (`/dashboard/employees`, `/dashboard/fuel[/id]`,
@ -131,7 +135,10 @@ historical, not authoritative.
| `POST /api/vehicles/import-summary` | server-only | Sync the Buildsite Vehicle Fuel Summary CSV into `vehicles` |
| `POST /api/vehicles/[id]/merge` | server-only | Merge a duplicate vehicle into another (dry run unless `confirm`) |
| `GET /api/pb-files/[collection]/[recordId]/[...filename]` | server-only | Same-origin proxy for PocketBase files (PDF slips in iframes) |
| `POST /api/chat` | `ENABLE_CHAT=true` (no user auth) | SSE chat stream |
| `POST /api/chat` | `ENABLE_CHAT=true`, `x-ask-user` identifies (no real auth yet) | SSE chat stream — `thread`/`action`/`chart`/`image`/`pdf` events |
| `GET /api/ask/users`, `GET/POST /api/chat/threads[/[id]]`, `POST /api/chat/attachments`, `GET/POST /api/ask/memories[/[id]]` | `ENABLE_CHAT=true` + `x-ask-user` | Ask's picker, threads, attachments, memory |
| `GET /api/agent/actions/[id]`, `POST …/{apply,reject,undo}` | `ENABLE_CHAT=true` + `x-ask-user`; confirm routes are same-origin only | Propose → confirm → apply/undo, with a before/after snapshot |
| `GET/PATCH /api/ask/change-requests[/[id]]` | server-only | App change requests Ask filed for the developer |
| `GET /api/clock/employee-lookup`, `POST /api/clock/scan`, `POST /api/clock/vehicle-lookup`, `POST /api/clock/vehicle-scan` | server-only | MEGA Clock PWA: badge clock-in/out + vehicle hour-meter scan |
| `POST /api/clock/events/[id]/correct` | same-origin + `x-app-pin` = `APP_PIN` | Record the real leaving time for a missed scan-out |

View file

@ -596,20 +596,41 @@ but valid "last scanned at site X" is more useful than nulling it.
**File:** `src/app/api/chat/route.ts`
**Caching:** `dynamic = "force-dynamic"`
**Auth:** none — gated by `ENABLE_CHAT=true`. **No user authentication.**
**Auth:** none — gated by `ENABLE_CHAT=true`. Identification only, via
`x-ask-user` (a `users` record id from the picker — see
[modules.md §7.1](./modules.md#ask-agent-actions-modules)). **No real user
authentication yet.**
**Protocol:** Server-Sent Events
**Body — new:** `{ threadId?, message, display?, attachmentIds?, images? }`
with `x-ask-user`. No `threadId` creates one, titled from `message`, owned
by the actor; a thread owned by someone else 404s.
**Body — legacy** (kept working for one release, unpersisted): `{ history }`
with no `threadId`.
`Content-Type: text/event-stream; charset=utf-8`. Events emitted:
- `meta` — `{ provider, model }` (first)
- `thread` — `{ id, title }` — only when a new thread was just created
- `delta` — `{ text }` (repeated, as the model streams)
- `tool` — `{ name }` (when a tool is called)
- `action` — the full `PlannedAction` preview from `propose_changes`
(`{ actionId, summary, preview[{ collection, kind, id?, before?, after }], problems, status: "pending" }`).
Sent **instead of** handing the plan to the model: the tool result the
model sees is `{ actionId, status, problems }` only.
- `chart` — a `ChartSpec` (`src/lib/reports/chart-spec.ts`:
`{ id, kind, title, subtitle?, xKey, xLabel?, series[{ key, label, format }], stacked?, rows }`),
once per chart a report tool returned. Sent **instead of** handing the
chart to the model: the tool result the model sees has `charts` replaced
by `chartsAvailable: { id, title }[]`, and the model places a chart by
writing `[[chart:<id>]]` on its own line. `id` matches
once per chart a report tool or `make_chart` returned. Sent **instead
of** handing the chart to the model: the tool result the model sees has
`charts` replaced by `chartsAvailable: { id, title }[]`, and the model
places a chart by writing `[[chart:<id>]]` on its own line. `id` matches
`/^[a-z0-9-]{1,64}$/`; specs with any other id are dropped.
- `image` — `{ id, url }`, once per `generate_image` call. Same split as
`chart`: the model keeps `imagesAvailable: [{ id }]` and places it with
`[[image:<id>]]`.
- `pdf` — `{ title, subtitle?, filename? }`, once when `make_pdf_report`
fires (the model calls it last, after building the report itself). The
browser renders a report cover and a Download PDF button for that turn;
the model's own tool result is just `{ ok: true }`.
- `done` — `{ text, iterations, stoppedOnLimit }` (final)
- `error` — `{ message }` (on failure)
@ -623,6 +644,32 @@ tool layer (`src/lib/agent/`).
---
## 12b. Ask agent actions
Detailed field-by-field contracts live in each route's own doc comment
(same convention as §12a); see
[modules.md §7.1](./modules.md#ask-agent-actions-modules) for how these
fit together and [data-model.md §4.17](./data-model.md#ask-agent-actions)
for the collections. All gated the same way as §12 (`ENABLE_CHAT=true`);
all identify the actor via `x-ask-user` except where noted.
| Method + path | Purpose |
| --- | --- |
| `GET /api/ask/users` | `{ users: { id, name }[] }` — `users` records with a non-empty `name`, for the "Who's using Ask?" picker. No `x-ask-user` needed. |
| `GET` / `POST /api/chat/threads` | List the actor's threads (newest first) / create an empty one. |
| `GET` / `PATCH` / `DELETE /api/chat/threads/[id]` | A thread's rows / rename+archive / delete. 404 if not owned by the actor. |
| `POST /api/chat/attachments` | Multipart `file` (+ optional `threadId`). Parses and stores as an `ask_messages` row (`kind: "attachment"`); returns `{ threadId, attachmentId, kind, summary, images? }` (`images` only for a scanned PDF's rasterised pages). |
| `GET /api/agent/actions/[id]` | An action's current state, reconstructed as `{ actionId, summary, preview, problems: [], status, error }` — used to re-render a card on thread reopen or refresh. |
| `POST /api/agent/actions/[id]/apply` | Confirm: applies a pending action. Same-origin only (`Origin` must match). |
| `POST /api/agent/actions/[id]/reject` | Cancel: marks a pending action rejected without applying anything. Same-origin only. |
| `POST /api/agent/actions/[id]/undo` | Reverses an applied action once. Same-origin only. |
| `GET` / `POST /api/ask/memories` | List the actor's active memories + active shared ones / add one manually (`{ text, site?, shared? }`). |
| `DELETE /api/ask/memories/[id]` | Forget one (kept as `status: "undone"`, not deleted). 400 if someone else tries to forget a personal memory. |
| `GET /api/ask/change-requests` | `{ requests: [...] }`, optional `?status=`. No `x-ask-user` needed — a plain dashboard route, not actor-scoped. |
| `PATCH /api/ask/change-requests/[id]` | `{ status }` — `open`/`planned`/`done`/`declined`. No `x-ask-user` needed. |
---
## 12a. Routes added after the 2026-09-06 pass
These are summarised rather than documented field-by-field; each route
@ -668,7 +715,7 @@ the queue item (see [`n8n/purchase-order-ingest-implementation.md`](./n8n/purcha
| `PATCH /api/suppliers/[id]` | Rename, same normalisation + uniqueness. |
| `POST /api/vehicles/[id]/merge` | Body `{ intoId, confirm? }`. Without `confirm: true` returns the plan (dry run). With it: moves the duplicate `[id]`'s fuel logs and plant-hours rows onto `intoId`, fills blank fields, deletes `[id]`. 409 on conflicts (different site/type, same plant-hours week). Batches of ≤ 50; safe to re-run if interrupted. |
| `POST /api/vehicles/import-summary` | Sync the Buildsite "Vehicle Fuel Summary" CSV: which vehicles exist per site and whether each is fuel (km/L) or plant (L/h). |
| `GET /api/pb-files/[collection]/[recordId]/[...filename]` | Same-origin proxy for PB files, so PDF slips render in an iframe without PB's sandbox CSP. |
| `GET /api/pb-files/[collection]/[recordId]/[...filename]` | Same-origin proxy for PB files, so PDF slips render in an iframe without PB's sandbox CSP. Collections: `delivery_notes`/`purchase_orders` (public, forwarded as-is) and `ask_messages` (its rules are all null by design — this route mints a short-lived admin file token server-side rather than exposing that rule). |
### MEGA Clock additions
@ -722,7 +769,11 @@ searched for:
| `GET /api/data-import-batches/[id]/status` | server-only | UI poll |
| `POST /api/data-import-batches/[id]/retry` | server-only | |
| `POST /api/records/[collection]/[id]/verify` | server-only | Allowlist: POs + DNs |
| `POST /api/chat` | gated by `ENABLE_CHAT=true` | **No user auth** — the gate is the env flag, not identity |
| `POST /api/chat` | gated by `ENABLE_CHAT=true` | `x-ask-user` **identifies**, doesn't authenticate — anyone with the PIN can pick any name |
| `GET /api/ask/users` · `GET/POST /api/chat/threads` · `.../[id]` · `POST /api/chat/attachments` · `GET/POST /api/ask/memories` · `DELETE …/[id]` | gated by `ENABLE_CHAT=true` | `x-ask-user` identifies the actor; threads/memories 404/400 if not owned |
| `GET /api/agent/actions/[id]` | gated by `ENABLE_CHAT=true` | `x-ask-user` only |
| `POST /api/agent/actions/[id]/{apply,reject,undo}` | same-origin only + `x-ask-user` | Confirm-card buttons; in the test phase anyone may confirm any pending action |
| `GET/PATCH /api/ask/change-requests` · `.../[id]` | server-only | Plain dashboard route, not actor-scoped |
| `POST /api/clock/scan` | server-only | Behind the /clock PIN gate |
| `POST /api/clock/vehicle-lookup` | server-only | Behind the /clock PIN gate |
| `POST /api/clock/vehicle-scan` | server-only | Behind the /clock PIN gate |
@ -740,7 +791,7 @@ searched for:
| `POST /api/suppliers` · `PATCH /api/suppliers/[id]` | server-only | |
| `POST /api/vehicles/import-summary` | server-only | Buildsite CSV sync |
| `POST /api/vehicles/[id]/merge` | server-only | Dry run unless `confirm: true`; deletes the duplicate |
| `GET /api/pb-files/[collection]/[recordId]/[...filename]` | server-only | Same-origin PB file proxy |
| `GET /api/pb-files/[collection]/[recordId]/[...filename]` | server-only | Same-origin PB file proxy; mints an admin file token for `ask_messages` |
**Routes with no protection at all:** the chat route (when enabled) and
any route that runs purely server-side. The browser-facing routes

View file

@ -370,6 +370,62 @@ Joins DNs to PO line items. `cascadeDelete: true` on the parent DN.
| `qty_received` | number | |
| `condition_notes` | text | Free-text audit trail (e.g. `"OCR 92% · desc: Cement 50kg · unit: bag"`) |
<a id="ask-agent-actions"></a>
### 4.17 `ask_threads`, `ask_messages`, `ask_log` (Ask agent actions)
Additive, created by `scripts/pb-add-ask-collections.py` (`--apply` is a
human step). All rules `null` — superuser only; the Next.js server is the
only client. See [modules.md §7.1](./modules.md#ask-agent-actions-modules)
for how these back threads, memory, actions and change requests.
**`ask_threads`** — one row per conversation.
| Field | Type | Notes |
| --- | --- | --- |
| `title` | text | `autoTitle()` of the first message, or renamed by the user |
| `owner` | relation → `users`, required | The picked actor (`x-ask-user`) |
| `archived` | bool | |
| `last_message_at` | date | Bumped on every `appendMessage`; a lost race here is harmless |
**`ask_messages`** — one row per message, attachment or generated image.
**Append-only**: created once, never rewritten, so concurrent tabs/users
can't overwrite each other.
| Field | Type | Notes |
| --- | --- | --- |
| `thread` | relation → `ask_threads`, required, **cascade delete** | |
| `owner` | relation → `users` | |
| `kind` | select | `user` / `assistant` / `attachment` / `image` |
| `text` | text (max 100 000) | The message; for an attachment, its summary; for an image, the prompt |
| `display` | text | The slash command as typed, when applicable |
| `charts` | json | `ChartSpec[]` the answer placed or left unplaced |
| `action_ids`, `attachment_ids`, `tools` | json | Arrays of ids / tool names |
| `file` | file (≤25 MB; xlsx/xls/csv/pdf/png/jpg/webp/txt) | Attachment or generated image |
| `parsed` | json (≤5 MB) | Attachment preview (capped 2 000 rows / 50 000 chars — `read_attachment` re-parses the stored file beyond that), or `{ pdf: {...} }` for a report turn |
| `parse_error` | text | |
**`ask_log`** — one row per thing Ask keeps track of, by `type`.
| Field | Type | Notes |
| --- | --- | --- |
| `type` | select | `change` (proposed data edit, for Undo) / `memory` (lasting fact) / `request` (app change request) |
| `owner` | relation → `users` | |
| `shared` | bool | Memories only — visible to everyone when `true` |
| `thread` | relation → `ask_threads` (optional, **no** cascade) | The log outlives a deleted thread |
| `status` | select | `change`: `pending`/`applied`/`rejected`/`failed`/`undone`. `memory`: `applied` (active) / `undone` (forgotten). `request`: `open`/`planned`/`done`/`declined` |
| `text` | text (max 2 000) | Change summary / memory text / request title |
| `detail` | text | Request detail |
| `site` | relation → `sites` (optional) | A memory about one site |
| `ops` | json | The planned `AgentOp[]`, `$ref`s intact |
| `before`, `after` | json | Snapshots for Undo. `before`: one entry per op. `after`: while `pending`, the proposed values per op; once `applied`/`failed`, `{ results, confirmed_by, undone_by? }` |
| `error` | text | Set on `applied_action` → `failed` |
| `applied_at`, `undone_at` | date | |
Indexes: `ask_threads(owner, archived, last_message_at)`,
`ask_messages(thread, created)`, `ask_log(type, status)`,
`ask_log(owner, type)`, `ask_log(thread)`.
---
## 5. Indexes and constraints summary

View file

@ -437,6 +437,121 @@ real authentication exists — the chat reads every site's financials and
spends LLM credits for anyone who can reach it." See
[multi-site-assessment §7](./multi-site-assessment.md#7-the-chatagent-layer).
<a id="ask-agent-actions-modules"></a>
### 7.1 Agent actions — threads, memory, files, free queries, proposed changes, PDF reports, images, change requests
Ask went from a report reader to an agent that reads any file, measures
anything with its own charts, changes data through confirm-first actions
with undo, and files its own PDF reports and app-change requests. Three
PocketBase collections back all of it (`scripts/pb-add-ask-collections.py`,
additive, `--apply` is a human step): `ask_threads`, `ask_messages`
(append-only), `ask_log` (`type`: `change` | `memory` | `request`). Schema
details: [data-model.md](./data-model.md#ask-agent-actions).
**Per-user, no logins yet.** `src/lib/agent/context.ts`'s `resolveActor`
trusts an `x-ask-user` header — a `users` record id the "Who's using
Ask?" picker (`GET /api/ask/users`, only records with a `name`) writes to
`localStorage`. This is identification, not security, until real logins
land — see "When logins land" in the plan doc. `AgentContext { actor,
threadId }` is threaded through every tool and the gateway.
**Threads.** `src/lib/chat/threads.ts` loads/appends `ask_messages` rows
(`autoTitle`, `loadThreadEntries`, `appendMessage` — creates only, never
rewrites, so concurrent tabs/users can't clobber each other) under
`/api/chat/threads` (list/create) and `/api/chat/threads/[id]`
(get/rename/archive/delete). `/api/chat` accepts
`{ threadId?, message, display?, attachmentIds?, images? }` with
`x-ask-user`; no `threadId` creates one titled from the first message. The
old `{ history }` body (no `threadId`) still works, unpersisted, for one
release.
**Memory.** `src/lib/agent/memory.ts`: `remember`/`forget`/`listMemories`
against `ask_log` (`type: "memory"`), deduped by normalised text. Personal
memories are private to their owner; `shared: true` ones are visible to
everyone. `formatMemoriesForPrompt` feeds the prompt's "What you remember"
section. A Memory drawer in `chat-view.tsx` lists Mine / Shared with
delete buttons and a manual add.
**Files.** `src/lib/agent/attachments.ts`'s `parseAttachment` reads
xlsx/xls/csv (SheetJS/PapaParse), PDF (`pdftotext -layout`; under ~50
chars/page marks it `scanned`), images and plain text.
`POST /api/chat/attachments` stores the result as an `ask_messages` row
(`kind: "attachment"`); a scanned PDF's first pages are rasterised and
returned as vision images the same way a photographed slip is. The
`read_attachment` tool re-parses on demand and pages a sheet 200 rows at a
time. `send_to_po_pipeline` posts a stored attachment into the existing
purchase-order pipeline (hash-dedupe, same as `/api/purchase-orders/upload`)
once the user confirms it's a PO/DN. `src/lib/agent/people-match.ts`'s
`matchPeople` (employee number → normalised full name → surname+initial
"likely") backs the "load workers from a list" recipe: `match_workers` →
`propose_changes`, never re-creating an exact match.
**Free queries and charts.** `src/lib/agent/query.ts`'s `runQuery` reads
either a live PocketBase collection (fields validated against
`schema-cache.ts`, filters bound with `pb.filter()`) or one of the seven
virtual datasets in `src/lib/agent/datasets.ts` (`site_summary`,
`fuel_log_status`, `plant_hours`, `payroll_hours`, `attendance`,
`supplier_balance`, `po_fulfilment` — each built from the same
`src/lib/metrics` functions the dashboard pages use, never re-derived).
Grouping, Monday-start week/day/month bucketing, and
sum/avg/min/max/count/count_distinct aggregation happen in JS on rows
already fetched — the model never computes. Results are cached under a
`resultId` (2h TTL); `make_chart` turns one into a `ChartSpec` by column
name only, so the model designs its own chart without ever holding a
number.
**Proposed changes (the gateway).** `src/lib/agent/gateway.ts` is the one
seam every write goes through: `authorize()` (a stub today — rejects
`NEVER_WRITABLE` collections and `dn_batches` lock/stage fields; the seam
where logins will add real permission checks) → `planOps` (schema
validation, `before` snapshots, relation-id existence, the
sites/employees duplicate rule, capped at 500 ops) → `createPendingAction`
(a pending `ask_log` row) → `applyAction`/`undoAction`. `propose_changes`
is the tool; `/api/agent/actions/[id]/{apply,reject,undo}` are the
same-origin-only confirm-card routes. The full plan streams to the
browser as an `action` SSE event; the model itself only ever sees
`{ actionId, status, problems }` — never the preview — the same split
`extractCharts` already does for chart numbers. `action-card.tsx` renders
it (created rows in green, updated fields old→new, deleted in red) with
Confirm/Cancel, then Undo once applied.
**PDF export.** Two independent paths. (1) Any answer can be exported
as-is: the toolbar's **PDF** button (`src/app/dashboard/chat/answer-pdf.ts`)
clones the rendered answer at a fixed A4 width, renders it with
`html-to-image`, and slices it into pages that never cut through a chart
or table row (`slicePages`), embedding each with `pdf-lib`. (2) Ask can
build a report on request ("give me a PDF on fuel at The Hills for
September"): it writes the report itself (headings, narrative, KPI
tables quoting tool display strings, charts via `make_chart`), then calls
`make_pdf_report({ title, subtitle? })` **once, last** — a pure metadata
signal (`src/lib/agent/pdf-report.ts`), split out of the tool result and
streamed as a `pdf` SSE event the same way an action card is. The turn
then renders a report cover (wordmark, title, date, "Prepared by Ask for
{name}") and reuses path (1)'s `answerToPdf`/`slicePages` for a Download
button that also fires once automatically
(`src/app/dashboard/chat/pdf-auto-download.ts`'s `nextPdfTurnToDownload`,
a pure "once per turn" decision). The report meta persists in the
assistant row's existing `parsed` json field, so reopening the thread
still shows the cover and button.
**Images.** `generate_image` (`src/lib/agent/images.ts`) calls MiniMax
`image-01` and stores the result as its own `ask_messages` row
(`kind: "image"`), streamed as an `image` SSE event and placed with
`[[image:<id>]]` — the same split/placement mechanism as a chart, but an
illustration only: never a chart, a number, or anything that looks like
data. A `base_resp.status_code` in `{1028, 1030, 2061}` (the MiniMax Token
Plan tier gate) surfaces as "Image generation isn't enabled on this
MiniMax plan" rather than retrying.
**Change requests.** "Can we add this to the app?" can't be done by Ask
itself — `file_change_request` logs an `ask_log` row (`type: "request"`,
`status: "open"`) instead of `propose_changes`, which is for data.
`/dashboard/change-requests` lists and re-statuses them
(open/planned/done/declined); `list_change_requests` (read-only) is also
served by the MCP server (`src/mcp/server.ts`) so Claude Code can pick
them up.
---
## 8. Financial landing, Delivery Notes, Suppliers

View file

@ -379,13 +379,13 @@ Added mid-plan: the user wants to ask for any custom report and get a finished P
- The Claude provider still points at `claude-opus-5`. Read the `claude-api` skill and update it to the current model (a configurable `ANTHROPIC_MODEL` env with a current default).
- Add `docs/operations.md` guidance: `CHAT_PROVIDER=anthropic` + `ANTHROPIC_API_KEY` switches Ask to Claude, which is more reliable at long multi-step tool use (reading a sheet → matching → proposing 40 creates). Run the same 5 test prompts on both providers and compare.
- [ ] Commit — `chore(ask): configurable Claude model`.
- [x] Commit — `chore(ask): configurable Claude model`.
---
### Task 13: Docs
- [ ] `docs/modules.md` §7 (threads, memory, files, query/datasets, actions + undo, PDF, images, change requests), `docs/data-model.md` (`ask_threads`, `ask_messages`, `ask_log`), `docs/api-reference.md` (new routes + `thread`/`action`/`image` SSE events), `README.md` summary. Commit — `docs(ask): agent actions`.
- [x] `docs/modules.md` §7 (threads, memory, files, query/datasets, actions + undo, PDF, images, change requests), `docs/data-model.md` (`ask_threads`, `ask_messages`, `ask_log`), `docs/api-reference.md` (new routes + `thread`/`action`/`image`/`pdf` SSE events), `README.md` summary. Commit — `docs(ask): agent actions`.
---