# How "Ask" works (MEGA) — brief for building the same thing in Healy ## Correction first The in-app chat does NOT use MCP. It calls tool functions directly inside the Next.js server. MCP (`src/mcp/server.ts`) is a separate, optional, read-only wrapper over the same tools, run locally over stdio for Claude Desktop/Code. Build the tool layer first; treat MCP as a thin extra. ## Stack - Next.js 16 (App Router), React 19, TypeScript, Tailwind 4, vitest - PocketBase via `pocketbase` SDK, superuser client on the server only (`pbAdmin()`) - Model: MiniMax M3 via OpenAI-compatible endpoint (`https://api.minimax.io/v1`); Anthropic `claude-opus-5` behind the same interface. `CHAT_PROVIDER` selects. - Streaming: SSE from `POST /api/chat` (plain `ReadableStream`; browser reads with `fetch`) - Tool schemas: `zod` -> `zod-to-json-schema` - UI markdown: `react-markdown` + `remark-gfm` mapped onto the app's own table styles ## Architecture 1. `src/lib/chat/` — provider-neutral core: `types.ts` (no SDK imports), `loop.ts` (model -> tools -> model, with max-iterations valve), `provider-anthropic.ts`, `provider-openai.ts` (one SDK each), `system-prompt.ts` (built per request: date + user memories), `think-filter.ts`. 2. `src/lib/agent/` — tools: `tools.ts` (read tools), `tools-actions.ts` (write tools), `load.ts` (fetching), `format.ts` (shaping + `display` strings), `resolve.ts` (name -> id), `query.ts` (generic `query_records`), `gateway.ts` (the write seam), `schema-cache.ts`. 3. `src/lib/metrics/` — ALL business calculations as pure tested functions, shared by dashboard and agent. 4. `/api/chat` — env gate, identify user, persist thread, run loop, stream events. 5. `/dashboard/chat` — client SSE reader, threads, attachments, confirm cards, charts, PDF. ## Rules that make it trustworthy - The model never does arithmetic. Tools return finished numbers + preformatted `display`. Prompt bans sum/average/percent/"combined" and says what to do instead (list rows, say aggregate not available). The first wording failed a live probe — keep "combined value of top three" as a regression probe. - Name resolution returns ambiguity lists instead of guessing. - `query_records`: typed generic read, filters bound with `pb.filter()`, grouping/aggregation in JS. - Tool failures go back to the model as error results and are logged server-side. ## Writes Model calls `propose_changes` -> pending row in `ask_log` -> confirm card -> human presses Confirm -> `applyAction` writes -> each op keeps a `before` snapshot -> Undo. Ops validated against live schema. `authorize()` is the single permission seam with a `NEVER_WRITABLE` set (`_superusers`, `users`, chat tables). Ops run sequentially so `$ref:` can reference records created earlier in the batch. **For Healy: remove the `delete` op kind entirely** (union type, planner, apply). Decide deliberately whether Undo may delete records the agent itself just created (recommended: yes, and nothing else). ## Gotchas MiniMax - M2/M2.7 are text-only. M3 is the first accepting BOTH `tools` and images. Test both together. - Use the OpenAI-compatible route; the `/anthropic` shim mangles tool-call args. - Reasoning streams inline in `…`; strip it with a stateful filter (tags split across chunks). - Tool-call args arrive as string fragments per index; concatenate before `JSON.parse`. - Key must be an Open Platform API key (not Token Plan subscription key, not Hailuo/MiniMax Agent). - Image endpoint: `{base minus /v1}/v1/image_generation` (appending /v1 to base gave /v1/v1). Anthropic - `budget_tokens` and assistant prefill are removed (400). Use `thinking: {type: "adaptive"}`. PocketBase - `sort=-created` / `-updated` can 400 with an empty body. Sort a real field or use autodate fields. - `getList(1, 500)` silently truncates; use `getFullList`. - `fields:` that omits a date field makes later date logic silently skip rows. - A detail-less 400 is usually a bad filter/sort; reproduce with a direct `getFullList`. Chat plumbing - History crosses JSON; types guarantee nothing. Normalise in the route (assistant turns arrive without `toolCalls`). - Compare `Origin` to the `Host` header, not `request.url`. - Agent loop needs more than 8 iterations for "read file -> match -> propose". ## Brief to give the Healy agent > Build a chat agent for Healy shaped like MEGA's "Ask". Next.js + PocketBase, SSE from `POST /api/chat`. > Provider-neutral loop with two adapters (MiniMax M3 via OpenAI-compatible endpoint; Anthropic fallback). > Tools are zod-schema'd functions; all business maths lives in pure tested functions shared with the UI. > Reads: typed tools per domain + generic `query_records`; model never does arithmetic; tools return `display`. > Writes: create and update only, NO delete. Propose -> pending log row -> human confirm -> apply, with > `before` snapshot and undo; validate against live schema; keep a never-writable list. > API access: expose the same handlers as REST and as an MCP server, both authenticated. > Also: strip ``, reassemble fragmented tool args, normalise history at the route, log tool failures. ## Decide deliberately (API + MCP + writes) - Auth: Ask used an `x-ask-user` header + shared PIN during testing. A create/update API or MCP surface needs real per-user authentication. - MCP is headless (no human to confirm). Either keep MCP read-only, or let it only create PENDING actions a person confirms in the app (recommended).