melchizedek-agents 0.16.2 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/.env.example +40 -3
  2. package/AGENT_SETUP.md +4 -2
  3. package/CHANGELOG.md +246 -0
  4. package/DOCUMENTATION.md +311 -15
  5. package/QUICKSTART.md +2 -2
  6. package/README.md +125 -149
  7. package/config/agents/examples/harness.yaml +135 -0
  8. package/config/agents/examples/model_zoo.yaml +23 -9
  9. package/config/agents/examples/pipeline.yaml +120 -0
  10. package/config/agents/examples/specs/open-meteo-forecast.json +77 -0
  11. package/config/agents/examples/specs/open-meteo-geocoding.json +53 -0
  12. package/config/agents/examples/weather.yaml +59 -0
  13. package/config/agents/syndicateSchema.yaml +125 -1
  14. package/db/hardening.sql +4 -3
  15. package/db/migrations/0011_erase_expired.sql +198 -0
  16. package/db/migrations/0012_audit_log.sql +98 -0
  17. package/dist/lib/a2a/app.d.ts +27 -0
  18. package/dist/lib/a2a/app.js +101 -5
  19. package/dist/lib/a2a/executor.d.ts +34 -4
  20. package/dist/lib/a2a/executor.js +109 -9
  21. package/dist/lib/compile.d.ts +41 -3
  22. package/dist/lib/compile.js +162 -17
  23. package/dist/lib/config.d.ts +24 -0
  24. package/dist/lib/config.js +24 -0
  25. package/dist/lib/doctor.js +17 -4
  26. package/dist/lib/index.d.ts +2 -0
  27. package/dist/lib/index.js +1 -0
  28. package/dist/lib/loadEnv.d.ts +5 -0
  29. package/dist/lib/loadEnv.js +7 -0
  30. package/dist/lib/loadSyndicate.d.ts +50 -0
  31. package/dist/lib/loadSyndicate.js +19 -3
  32. package/dist/lib/memory/erase.d.ts +5 -0
  33. package/dist/lib/memory/erase.js +5 -0
  34. package/dist/lib/memory/providers.d.ts +27 -0
  35. package/dist/lib/memory/providers.js +28 -0
  36. package/dist/lib/models/capabilities.js +10 -0
  37. package/dist/lib/models/endpoints.d.ts +1 -1
  38. package/dist/lib/models/endpoints.js +4 -1
  39. package/dist/lib/models/fallback.d.ts +41 -0
  40. package/dist/lib/models/fallback.js +105 -0
  41. package/dist/lib/models/gateway.d.ts +2 -2
  42. package/dist/lib/models/gateway.js +4 -4
  43. package/dist/lib/models/kimiLlm.d.ts +123 -0
  44. package/dist/lib/models/kimiLlm.js +171 -0
  45. package/dist/lib/models/providerMap.d.ts +2 -2
  46. package/dist/lib/models/providerMap.js +4 -1
  47. package/dist/lib/models/registry.d.ts +1 -1
  48. package/dist/lib/models/registry.js +11 -3
  49. package/dist/lib/net/redirects.d.ts +14 -0
  50. package/dist/lib/net/redirects.js +94 -0
  51. package/dist/lib/observability/audit.d.ts +21 -0
  52. package/dist/lib/observability/audit.js +39 -0
  53. package/dist/lib/observability/metrics.d.ts +6 -0
  54. package/dist/lib/observability/tracer.d.ts +14 -1
  55. package/dist/lib/observability/tracer.js +16 -2
  56. package/dist/lib/persistence/supabaseProvider.d.ts +2 -6
  57. package/dist/lib/persistence/supabaseProvider.js +2 -31
  58. package/dist/lib/runtime/questions.d.ts +55 -0
  59. package/dist/lib/runtime/questions.js +107 -0
  60. package/dist/lib/runtime/syndicateTurn.d.ts +45 -5
  61. package/dist/lib/runtime/syndicateTurn.js +165 -17
  62. package/dist/lib/storage/postgres/index.d.ts +9 -0
  63. package/dist/lib/storage/postgres/index.js +13 -0
  64. package/dist/lib/storage/rlsStatus.d.ts +45 -0
  65. package/dist/lib/storage/rlsStatus.js +69 -0
  66. package/dist/lib/syndicateSchema.d.ts +177 -0
  67. package/dist/lib/syndicateSchema.js +343 -3
  68. package/dist/lib/toolRegistry.js +9 -0
  69. package/dist/lib/tools/mcpToolFactory.d.ts +26 -0
  70. package/dist/lib/tools/mcpToolFactory.js +61 -4
  71. package/dist/lib/tools/openapiTools.d.ts +52 -0
  72. package/dist/lib/tools/openapiTools.js +225 -0
  73. package/dist/lib/tools/skillToolset.d.ts +135 -0
  74. package/dist/lib/tools/skillToolset.js +247 -0
  75. package/dist/lib/tools/urlContextTool.d.ts +29 -0
  76. package/dist/lib/tools/urlContextTool.js +39 -0
  77. package/dist/lib/workflow.d.ts +72 -0
  78. package/dist/lib/workflow.js +180 -0
  79. package/dist/lib/workflowConfig.d.ts +95 -0
  80. package/dist/lib/workflowConfig.js +93 -0
  81. package/dist/scripts/a2a_server.js +51 -8
  82. package/dist/scripts/db.d.ts +9 -0
  83. package/dist/scripts/db.js +30 -15
  84. package/dist/scripts/syndicate_chat.js +39 -5
  85. package/package.json +6 -3
  86. package/skills/briefs/_shared.md +1 -1
  87. package/skills/briefs/melchizedek-author.md +8 -4
  88. package/skills/briefs/melchizedek-memory.md +1 -1
  89. package/skills/briefs/melchizedek-models.md +1 -1
  90. package/skills/briefs/melchizedek-scribe.md +4 -4
  91. package/skills/briefs/melchizedek-serve.md +1 -1
  92. package/skills/briefs/melchizedek.md +4 -1
  93. package/skills/melchizedek/SKILL.md +4 -1
  94. package/skills/melchizedek-author/SKILL.md +7 -3
  95. package/skills/melchizedek-memory/SKILL.md +1 -1
  96. package/skills/melchizedek-models/SKILL.md +1 -1
  97. package/skills/melchizedek-scribe/SKILL.md +3 -3
  98. package/skills/melchizedek-serve/SKILL.md +1 -1
  99. /package/skills/melchizedek-author/{templates → assets}/minimal.yaml +0 -0
  100. /package/skills/melchizedek-scribe/{templates → assets}/brief.md +0 -0
package/DOCUMENTATION.md CHANGED
@@ -98,6 +98,8 @@ Field reference:
98
98
  | `memory_extraction_model` | root | The model that distils this syndicate's turns into memory records (long-term only); any model id. Default: the deployment's `MEMORY_EXTRACTION_MODEL`. |
99
99
  | `memory_retention_days` | root | Days a fact in this syndicate's namespace is kept; the server deletes older facts when it loads the syndicate and daily after. Requires `memory_namespace` (never applied to the shared default namespace). |
100
100
  | `dispatch` | root | Switches the syndicate from DELEGATE to PLAN-DISPATCH routing (§6). `default_route` (required) names the fail-static subagent; `route_key` / `reason_key` name the router's JSON properties (defaults `route` / `reason`). Honoured by every surface (the CLI, the server, the worker, evals). |
101
+ | `workflow` | root | Switches the syndicate to a WORKFLOW (§6): a graph whose nodes are its agents plus declared `join`, `map`, `tool` and `ask_user` nodes, and whose `edges` say what runs after what and on which route. The orchestrator is a node like any other; nothing delegates. Cannot be combined with `dispatch`. Contract: `lib/workflow.ts`; worked example: `examples/pipeline.yaml`. |
102
+ | `retries` | root | Self-correction, on by default ([ADR 0034](./wiki/decisions/0034-self-correction.md)): `model_errors` (default 2) retries a model reply ADK marks malformed (`MALFORMED_FUNCTION_CALL`) instead of failing the turn; `tool_errors` (default 3) answers a tool that threw with structured reflection guidance and caps its retries. `0` turns either off. Every retry is a model call under `max_steps`. |
101
103
  | `guards` | root | Optional list of post-answer guard NAMES (`lib/guards/index.ts`). Each runs after the answering turn and before the reply publishes, receiving the final text plus every tool-result text of that turn, and rewrites in place rather than re-asking the model; its notes land in the `[STATUS]` stream. Guards declared by a syndicate reached through `yaml_reference:` count too — the server resolves the union via `collectGuards()`. Resolved by name, never by module path, so adding one is a deliberate act in code; **the published registry ships one, `science`** (citation checks for `research.yaml`); `registerGuard()` adds your own from code, and an unregistered name is warned about and skipped. |
102
104
  | `name` / `model` / `instruction` | agent | The agent triple. Any Gemini id, `claude-*`, or `ollama/*` for open-weight local models (see §5). |
103
105
  | `description` | subagent | **The delegation API.** The orchestrator reads this when deciding to hand off — write it like a function signature ("Use this subagent to…, pass it…"). |
@@ -108,7 +110,15 @@ Field reference:
108
110
  | `a2a_agent_url` | subagent | A REMOTE agent over A2A (§6): the orchestrator delegates to it with one `request` argument; in plan-dispatch it can be a route. No `model`/`instruction` — the remote agent has its own. Credentials come from `A2A_AGENT_TOKENS`, never YAML. |
109
111
  | `max_steps` | root | Cap on model calls per turn, counted across every agent the turn reaches (orchestrator, subagents, nested syndicates). Exceeding it fails the turn with `STEP_LIMIT`. |
110
112
  | `includeContents` / `outputKey` / `globalInstruction` / `disallowTransferToParent` / `disallowTransferToPeers` | agent | Passed through to ADK's LlmAgent. `includeContents: none` makes an agent see only the current message. |
113
+ | `fallback_model` | any agent | A model, ideally on another provider, that answers when this agent's model fails provider-side (5xx, 429, a connection reset, after its own retries) before producing any output, or while that provider's circuit is open: after `MODEL_BREAKER_THRESHOLD` consecutive provider failures (default 5; 0 disables) the provider is skipped for `MODEL_BREAKER_COOLDOWN_MS` (default 30 s). A 4xx and a canceled turn are never redirected, and a stream that already produced text is never replayed elsewhere ([ADR 0044](./wiki/decisions/0044-fallback-model-and-circuit-breaker.md)). |
114
+ | `mcp_tools` | subagent | The MCP server's tools this agent may use; any other tool the server lists is not exposed. On a dispatch route `require_approval` may name them ([ADR 0041](./wiki/decisions/0041-tool-vendors-get-least-privilege.md)). |
111
115
  | `mcp_server_url` | subagent | Discover this subagent's tools from a remote MCP server at load time (§3). SSRF-guarded; `ALLOW_PRIVATE_MCP=true` permits localhost for development. |
116
+ | `openapi` | any agent | HTTP APIs as tools, each from an OpenAPI 3 spec file (§3, OpenAPI tools): `spec`, and optionally `operations` (default: the GET operations only), `auth` (from environment variables), `base_url`, `prefix`. |
117
+ | `code_execution` | any Gemini agent | `"gemini"`: the model writes Python and Gemini runs it in Google's server-side sandbox, returning the output to the model; nothing runs on this host. For arithmetic, data and checks a model gets wrong in its head. Gemini models only ([ADR 0033](./wiki/decisions/0033-context-task-code.md)). |
118
+ | `context` | orchestrator | Compacts a long conversation: when the last request's prompt passed `compact_after_tokens`, earlier turns become one summary (written by `summary_model`, default the agent's own) and the last `keep_recent_events` stay verbatim. The full history stays stored; only what the model reads shrinks. The orchestrator of a delegate syndicate only: a dispatch route already reads a bounded projection, a workflow node sees only its input. |
119
+ | `mode` | workflow node | `"task"`: the agent works with its tools until it calls `finish_task`, whose arguments (matching its `outputSchema`) become the node's output. Workflow nodes only. |
120
+ | `examples` | any agent | Few-shot exchanges, `[{ input, output }]` (up to 20), added to every request's instruction by ADK's `ExampleTool`; the model never calls it. Keeps worked examples out of the prose of `instruction`. |
121
+ | `skills` | any agent | Agent Skills (a directory of SKILL.md folders) the agent holds the way a coding harness does: every skill's name and description is appended to its instruction at compile time; `load_skill` reads one in full with the names of its files, `load_skill_resource` reads one file. `scripts: local` adds `run_skill_script`, which runs a skill's own scripts on this machine, each after a person approves (the `require_approval` pause); `tools:` names registry tools a skill's `allowed-tools` may unlock once loaded. Worked example: `examples/harness.yaml`; engine: `lib/tools/skillToolset.ts`. |
112
122
 
113
123
  Validation happens at load: missing names, legacy option blocks, and
114
124
  malformed agents fail with pointed errors before any model is called.
@@ -122,16 +132,18 @@ instance:
122
132
 
123
133
  | Name | Kind | Does |
124
134
  |---|---|---|
125
- | `web_search` | Provider-agnostic | Live web search via the agent model's NATIVE search: Gemini grounding, Anthropic `web_search` server tool, OpenAI Responses `web_search`, xAI Agent Tools `web_search`. On local `ollama/*` models the tool is omitted with a one-time warning (keyless stays keyless). On grok-* agents, optional server-side domain filters via `XAI_WEB_SEARCH_ALLOWED_DOMAINS` / `_EXCLUDED_DOMAINS` in `.env` (max 5, mutually exclusive; xAI accepts no date bounds — those are `x_search`-only). Prefer this in new YAMLs. |
135
+ | `web_search` | Provider-agnostic | Live web search via the agent model's NATIVE search: Gemini grounding, Anthropic `web_search` server tool, OpenAI Responses `web_search`, xAI Agent Tools `web_search`. On local `ollama/*` models, and on `kimi-*` (Moonshot's model-side search retires 2026-10-20; its successor is a REST API, not a request field), the tool is omitted with a one-time warning (keyless stays keyless). On grok-* agents, optional server-side domain filters via `XAI_WEB_SEARCH_ALLOWED_DOMAINS` / `_EXCLUDED_DOMAINS` in `.env` (max 5, mutually exclusive; xAI accepts no date bounds — those are `x_search`-only). Prefer this in new YAMLs. |
126
136
  | `web_extract` | FunctionTool | Deterministic page reading: fetches 1–5 agent-chosen URLs and returns clean page text (no LLM summarization). Keyless — works on every provider including local `ollama/*`. Per-page char budget (default 15k, `WEB_EXTRACT_CHAR_LIMIT`); long pages return a head+tail window with an `offset` continuation call served from a 15-minute cache. SSRF-guarded (http(s) only, private/link-local hosts refused, redirects re-checked). Block pages (bot checks, paywall stubs, JS shells) are detected code-side and returned as labeled `Error:` blocks, never as content. Pair with `web_search`: search to find, extract to read past the headline. |
127
137
  | `x_search` | xAI-only | Live search over X (Twitter) posts via xAI Agent Tools. Self-gates to `grok-*` agents; a silent no-op on every other provider, so mixed-provider YAMLs stay safe. Optional server-side constraints in `.env`: `XAI_X_SEARCH_FROM_DATE`/`_TO_DATE` (inclusive `YYYY-MM-DD`) and `_ALLOWED_HANDLES`/`_EXCLUDED_HANDLES` (max 20, mutually exclusive — allowlist wins). |
128
138
  | `collections_search` | xAI-only | Semantic search over xAI **Collections** — hosted document stores (PDFs/text/CSVs) uploaded at console.x.ai — server-side RAG with `collections://…` citations. Which collections: `XAI_COLLECTION_IDS` in `.env` (optional `XAI_COLLECTIONS_MAX_RESULTS`). Declared with no ids → omitted with a warning; non-xAI providers → silent no-op. |
139
+ | `url_context` | Gemini built-in | Gemini reads the pages at URLs in the conversation, server-side (Google fetches them, not this host). On any other provider it is a no-op the doctor reports as dropped; use `web_extract` there. |
129
140
  | `google_search` | ADK built-in | Live web search — Gemini agents only (legacy alias; use `web_search`). |
130
141
  | `preload_memory` | ADK built-in | Silently injects similarity-matched facts into every request (ambient recall). |
131
142
  | `load_memory` | ADK built-in | Explicit tool call to search the fact store (deliberate recall). |
132
143
  | `generate_image` | FunctionTool | Calls the Gemini image model directly, saves the result under `outputs/`, returns the path. A FunctionTool because binary `inlineData` cannot survive the AgentTool text boundary. |
133
144
  | `inspect_image` | FunctionTool | **Blind visual inventory** of a file under `outputs/`: subjects with exact counts, composition, light, palette, medium cues, artifacts — zero quality judgments. Its signature accepts *only* a file path, so an orchestrator cannot leak expectations into the observation (see `image_production.yaml`). |
134
145
  | `task_add` / `task_list` / `task_get` / `task_update` | FunctionTool | A to-do list and job queue. Default: a single-user JSON file (`MELCHIZEDEK_TASKS_FILE`, default `outputs/tasks.json`), so every caller of a shared endpoint shares one list. With `DATABASE_URL` (migration 0009) each caller has its own list, scoped by the caller's scope key, and any number of workers take jobs safely. |
146
+ | `ask_user` | LongRunningFunctionTool | Asks the person one question (optionally with `options`) and ends the turn `input-required`; the next message on the conversation is the call's result. Orchestrator or plan-dispatch route only (§6, Questions). |
135
147
  | `task_queue` | FunctionTool | Queues a background job (a self-contained instruction). The tool only writes the queue; `npm run assistant:worker` (`melchizedek-worker`) claims each job, runs it through one agent compiled from YAML (default: the Assistant's Worker), and writes the result back for `task_get`. `--once` drains and exits, for cron. |
136
148
 
137
149
  **MCP tools** are the exception to the registry: a subagent with
@@ -147,12 +159,50 @@ loopback/private hosts unless `ALLOW_PRIVATE_MCP=true` (SSRF guard);
147
159
  treat any remote MCP server as an untrusted tool vendor whose results
148
160
  are data, never instructions.
149
161
 
162
+ **OpenAPI tools** turn any HTTP API with an OpenAPI 3 spec into an
163
+ agent's tools, with no tool code (`lib/tools/openapiTools.ts`, on ADK's
164
+ `OpenAPIToolset`; [ADR 0032](./wiki/decisions/0032-openapi-tools.md)):
165
+
166
+ ```yaml
167
+ orchestrator:
168
+ name: Forecaster
169
+ openapi:
170
+ - spec: "specs/open-meteo-forecast.json" # relative to this YAML file
171
+ operations: [getForecast] # omitted: every GET operation
172
+ auth: { api_key: { env: "WEATHER_KEY", in: "header", name: "X-Api-Key" } }
173
+ # base_url: overrides servers[0].url · prefix: keeps two APIs apart
174
+ ```
175
+
176
+ One tool per operation, named from its `operationId` in snake_case
177
+ (`getForecast` → `get_forecast`; no `operationId`: path and method), with
178
+ the parameters and request body as its arguments and the operation's
179
+ `summary` as the description the model reads. Exposure is deliberate:
180
+ without `operations`, only GET operations become tools, so anything that
181
+ writes is exposed only by naming it, and a named operation can be listed
182
+ under `require_approval` (as written under `operations`) so a person
183
+ approves each call. `auth` names an environment variable, never a value
184
+ (`bearer_env`, or `api_key` with `in: header | query` and `name`); an unset
185
+ variable fails the compile, and so does one of the framework's own settings
186
+ (the database URL, a provider key, an `A2A_` secret: anything `.env.example`
187
+ documents), since the YAML chooses the host it goes to.
188
+ `OPENAPI_CREDENTIAL_ENVS`, when set, is the exact list of variables an `auth`
189
+ may name. A refused or unset variable fails the compile, and a static token is applied to the request,
190
+ never stored in session state. Every server must be http(s) and pass the
191
+ SSRF guard: its literal rules when the agent compiles, the full check with
192
+ DNS before each call; `ALLOW_PRIVATE_OPENAPI=true` permits private hosts for
193
+ local development. A response over 20,000 characters is cut and says so; a
194
+ network failure comes back to the model as an error. Specs are files, never
195
+ URLs: save the spec beside the YAML and review it like code (trim it to the
196
+ operations the agent needs, and write each `summary` for the model). Worked
197
+ example: `config/agents/examples/weather.yaml`, two keyless Open-Meteo specs
198
+ in `examples/specs/`.
199
+
150
200
  > **Schema dialects, handled for you.** The factory emits Gemini-style
151
201
  > UPPERCASE schema types (`'OBJECT'`, `'STRING'`, …) because the ADK is
152
202
  > Gemini-native; every non-Gemini adapter normalizes them back to
153
203
  > standard lowercase JSON-Schema at request-build time
154
204
  > (`lib/models/schemaNormalize.ts`). MCP tools therefore work on any
155
- > provider's agents — Gemini, Claude, GPT, or Grok.
205
+ > provider's agents — Gemini, Claude, GPT, Grok, or Kimi.
156
206
 
157
207
  ## 4. Sessions & long-term memory
158
208
 
@@ -179,7 +229,11 @@ npx melchizedek-db status # schema version, hardening, session counts
179
229
 
180
230
  That runs the migrations in [`db/migrations/`](./db/migrations/) and then
181
231
  [`db/hardening.sql`](./db/hardening.sql). Both are idempotent, so re-running
182
- them is also the upgrade path from any earlier layout.
232
+ them is also the upgrade path from any earlier layout. `apply` runs the whole
233
+ install as one transaction under an advisory lock: if any statement fails,
234
+ nothing is changed, and two applies started together run one after the other.
235
+ Rolling back a migration that succeeded is a restore from backup (§6), since
236
+ the migrations only move forward.
183
237
 
184
238
  The tables go in the `public` schema by default. To keep them out of reach of
185
239
  a REST layer (Supabase exposes `public`), install into a private schema and
@@ -260,6 +314,7 @@ accordingly — model optionality is a single YAML line per agent:
260
314
  | `claude-*` | Anthropic | `lib/models/claudeLlm.ts` | `ANTHROPIC_API_KEY` | ✅ server tool |
261
315
  | `gpt-*`, o-series | OpenAI | `lib/models/gptLlm.ts` (Responses API) | `OPENAI_API_KEY` | ✅ web_search tool |
262
316
  | `grok-*` | xAI | `lib/models/grokLlm.ts` (Responses API) | `XAI_API_KEY` | ✅ Agent Tools search |
317
+ | `kimi-*` | Moonshot AI (Kimi) | `lib/models/kimiLlm.ts` (chat completions) | `MOONSHOT_API_KEY` | ⚠ omitted + warning |
263
318
  | `ollama/*` | Local Ollama | `lib/models/ollamaLlm.ts` | none | ⚠ omitted + warning |
264
319
  | *any cloud id whose direct key is absent* | the id's own provider, via a gateway | `lib/models/gatewayLlm.ts` (chat completions) | `MODEL_GATEWAY` + `MODEL_GATEWAY_API_KEY` | ⚠ omitted + reported |
265
320
 
@@ -271,6 +326,45 @@ structured outputs ride `outputSchema` → `text.format`, and two
271
326
  xAI-only tools — `x_search` and `collections_search` (§3) — turn on
272
327
  live X search and hosted-document RAG. All verified live on grok-4.5.
273
328
 
329
+ **Moonshot AI (Kimi).** `kimi-*` ids route to `lib/models/kimiLlm.ts`, a
330
+ subclass of the chat-completions base, against `https://api.moonshot.ai/v1`
331
+ (`MOONSHOT_BASE_URL` for a proxy). Get a key at platform.moonshot.ai; the
332
+ `.cn` console serves mainland China. The family (USD per 1M tokens, October
333
+ 2026): `kimi-k3`, the flagship — 2.8T-parameter open-weight MoE, 1M context,
334
+ vision, always-on thinking, $3 in / $0.30 cache hit / $15 out; `kimi-k2.6`,
335
+ the cheaper general tier (256K, vision, thinking switchable, $0.95 / $4);
336
+ `kimi-k2.7-code` and `kimi-k2.7-code-highspeed` (256K, coding, thinking
337
+ cannot be turned off; the highspeed variant streams at ~180 tokens/s for
338
+ twice the price). There is no small or "flash" K3. On price, K3 is a mid-tier
339
+ closed model, not a bargain: Claude Sonnet 4.6's rate, and Claude Sonnet 5.5
340
+ ($2 / $10) undercuts it per token while leading it on the coding boards;
341
+ thinking is always on and billed as output, which is why the adapter pins
342
+ effort below Moonshot's `max`. Choose K3 for the strongest open weights
343
+ (self-hostable) and the flat-rate 1M context; choose `kimi-k2.6` for cost;
344
+ or serve K3 through `MODEL_GATEWAY` (OpenRouter's endpoints run from about
345
+ $0.88 to $3.45 per 1M input, the cheap ones quantized). What works: tool calling
346
+ and delegation, strict `json_schema` structured output (K3 and K2.7 Code
347
+ document it; K2.6 is unstable on `$ref`/`oneOf`), SSE streaming, images as
348
+ base64 (Moonshot takes no public image URLs), MCP tools (they are function
349
+ tools to the model), `reasoning_content` surfaced as THINKING. Reasoning:
350
+ K3 takes a top-level `reasoning_effort` (`low` | `high` | `max`); the adapter
351
+ pins `DEFAULT_KIMI_REASONING_EFFORT` (`high`, `lib/config.ts`) unless
352
+ `generateContentConfig.reasoningEffort` says otherwise, and maps `none` to
353
+ `low` because K3 cannot stop thinking. K2.x models take a `thinking:
354
+ { type }` switch instead: `reasoningEffort: "none"` or `thinkingBudget: 0`
355
+ sends `disabled`, and any effort value is dropped from the body. What does
356
+ not: native `web_search` (dropped with a warning; `web_extract` works, and
357
+ Moonshot's standalone `POST /v1/tools/search`, billed per call, could back
358
+ a client-side tool), and thinking carried across a tool loop (Moonshot asks
359
+ for K3's `reasoning_content` back on the assistant message; this adapter
360
+ keeps scratchpads out of history, so K3 re-reasons each step). K3's open
361
+ weights mean the same id is served by OpenRouter, Together, Fireworks and
362
+ Vercel AI Gateway; with no direct key, `MODEL_GATEWAY` maps it to
363
+ `moonshotai/kimi-k3`. The row in the capability matrix is asserted against
364
+ the request body the adapter sends; one plain turn was verified live on
365
+ 2026-10-03 (`npm run demo:models`, kimi-k3 at low effort, 17.9 s), tool
366
+ loops and search were not.
367
+
274
368
  **Which keys do I need?** `npm run doctor` (the `melchizedek-doctor` bin)
275
369
  reads every syndicate YAML, resolves each agent's model under your `.env`,
276
370
  and prints one table — agent, model, provider, which declared server-side
@@ -340,7 +434,7 @@ line each. `config/agents/examples/claude.yaml` is the minimal Claude example;
340
434
  provider, and `npm run demo:models` proves the whole surface: one
341
435
  prompt to every available provider, printing input, thinking (qwen3
342
436
  `<think>` blocks, Claude extended thinking, GPT reasoning summaries,
343
- Grok reasoning), output, and a per-request token/latency trace; add
437
+ Grok reasoning, Kimi reasoning_content), output, and a per-request token/latency trace; add
344
438
  `-- --search` to watch four native web searches plus the local
345
439
  omission. Providers without keys are skipped, never fatal.
346
440
 
@@ -415,7 +509,7 @@ options. `demo/a2a_demo.mjs` is a complete client.
415
509
  | `POST /a2a/jsonrpc`, `/a2a/rest` | bearer | the default syndicate |
416
510
  | `GET /<agentId>/.well-known/agent-card.json` | bearer | another syndicate's card; its URLs point at `/<agentId>/a2a/...` |
417
511
  | `POST /<agentId>/a2a/jsonrpc`, `/<agentId>/a2a/rest` | bearer | another syndicate (`A2A_SERVED_AGENTS` restricts which) |
418
- | `DELETE /memory` | bearer | erase everything stored for the calling scope: facts, sessions (with subagent rows) and ledger rows, with per-store counts; `?all=1` covers every memory namespace |
512
+ | `DELETE /memory` | bearer | erase everything stored for the calling scope: facts, sessions (with subagent rows), ledger rows and A2A tasks, including conversations whose sessions have expired, with per-store counts; `?all=1` covers every memory namespace |
419
513
 
420
514
  "bearer" means the credential `A2A_AUTH` asks for (below); with no
421
515
  `A2A_SERVER_SECRET` and no authenticator the server binds `127.0.0.1` only.
@@ -461,9 +555,28 @@ or pass your own `resolveRequest`.
461
555
  The counts live in the `melchizedek_usage` table (`db/migrations/0004_usage.sql`)
462
556
  when Postgres or Supabase is configured, else in process memory. A store
463
557
  that cannot be read refuses the task.
464
- - **One record per task** (option `onTaskEnd`): agent, caller, a hash of the
465
- scope, status, reason, duration, model calls and tokens.
558
+ - **One record per task** (option `onTaskEnd`): task id, trace id, agent,
559
+ caller, a hash of the scope, status, reason, duration, model calls and
560
+ tokens. The trace id is the one the turn's spans and its ledger row carry,
561
+ and the audit trail's `task.end` row names both ids, so one id joins the
562
+ log, the ledger, the audit trail and your tracing backend. A request
563
+ carrying a W3C `traceparent` header is linked, not joined: the turn's root
564
+ span links to the caller's span and records `caller.trace_id`, and the
565
+ record carries `callerTraceId`. The turn keeps a trace id of its own, since
566
+ ledger attribution and erasure key on it and a caller must not choose it.
466
567
  `A2A_LOG_FORMAT=json` prints every server line as JSON, this record included.
568
+ - **The audit trail** (`melchizedek_audit`, `db/migrations/0012_audit_log.sql`;
569
+ option `audit`, supplied by `postgresStorage`): one row per failed
570
+ authentication (`auth.failure`), task outcome (`task.end`) and erasure
571
+ (`memory.erase`), with the caller's name, the source address, the agent and
572
+ task ids, and a hash of the scope. Never a scope key and never conversation
573
+ content. A trigger refuses UPDATE and DELETE; rows leave only through
574
+ `SELECT melchizedek_prune_audit(<days>)`, which you schedule (pg_cron, or a
575
+ job) for the retention your evidence needs, since the source address is
576
+ personal data. A failed write is logged once and each event is printed to
577
+ stderr as JSON until writes recover. A database owner can still alter the
578
+ table; ship the rows to a store you do not administer when the evidence must
579
+ survive that.
467
580
  - **Rate limit**: with an authenticator it counts per caller (an operator's
468
581
  backend) or per scope (an end user); under the shared secret, per IP.
469
582
  - **The telemetry ledger is redacted before it is written**:
@@ -472,6 +585,30 @@ or pass your own `resolveRequest`.
472
585
  `setTelemetryRedactor(fn)`. Sessions and memory are not redacted; use
473
586
  `DELETE /memory` there.
474
587
 
588
+ #### Where your data goes
589
+
590
+ What a deployment sends where, how long it is kept, and the setting that
591
+ changes it. "Your Postgres" is the database `DATABASE_URL` (or Supabase)
592
+ names; nothing is kept by the framework anywhere else.
593
+
594
+ | Data | Goes to | Kept | Change it with |
595
+ |---|---|---|---|
596
+ | The prompt: the user's message, the conversation, tool results | The model provider of each agent that runs (the model-id prefix) | The provider's policy | The `model:` lines in the YAML; `ollama/` ids stay on your machine |
597
+ | Tool calls (`web_search`, `web_extract`, OpenAPI, MCP) | The tool's own host | That host's policy | The agent's `tools:`, `openapi:`, `mcp_server_url` / `mcp_tools` |
598
+ | Sessions (conversation history) | Your Postgres, `adk_sessions` | 7 days after the last message | `ttlDays` (`postgresStorage`) |
599
+ | Long-term memory facts | Your Postgres, `adk_memory_facts`; **the transcript is also sent to the extraction and embedding providers, Gemini by default whatever the agents run on** (the server warns at boot when they differ) | Until erased, or `memory_retention_days` | `MEMORY_EXTRACTION_MODEL`, `MEMORY_EMBEDDING_PROVIDER`, `memory_retention_days` |
600
+ | The ledger: each turn's input, output and tool results (key-shaped secrets redacted) | Your Postgres, `adk_turns`, `adk_telemetry` | **Until you prune it**: schedule `melchizedek_prune_telemetry(<days>)` | `TELEMETRY_REDACT`, the prune's `turn_days` |
601
+ | Sampled full model requests | Your Postgres, `adk_payloads` | 30 days | `TELEMETRY_PAYLOADS`, `TELEMETRY_PAYLOAD_TTL_DAYS` |
602
+ | Traces, when `OTEL_EXPORTER_OTLP_ENDPOINT` is set | Your OTLP collector, conversation content included with key-shaped secrets redacted (`off` drops it) | Your collector's policy | `OTEL_EXPORT_CONTENT` (`redacted`, `off`, `raw`) |
603
+ | The audit trail (no content) | Your Postgres, `melchizedek_audit` | Until `melchizedek_prune_audit(<days>)` | The prune schedule |
604
+ | The task record (no content) | stdout | Your log platform's policy | `A2A_LOG_FORMAT` |
605
+
606
+ `DELETE /memory` erases a user from every Postgres store above except the
607
+ audit trail, which holds no content, only a hash of the scope. It does not
608
+ reach a provider's own retention: read each provider's data terms, and the
609
+ business associate or data processing agreement you need with it, before
610
+ sending it regulated data.
611
+
475
612
  #### Who pays
476
613
 
477
614
  `A2A_KEY_MODE` (option `keyMode`), whatever the authenticator:
@@ -533,6 +670,8 @@ JSON); `file` parts are refused.
533
670
 
534
671
  #### Approval gates (`require_approval`)
535
672
 
673
+ A skill script run (`skills.scripts: local`) pauses the same way, without being listed: every `run_skill_script` call waits for the person's answer.
674
+
536
675
  A tool can run only after a person approves the exact call
537
676
  ([ADR 0028](./wiki/decisions/0028-approval-gates.md)). The agent lists it:
538
677
 
@@ -564,6 +703,35 @@ or native-search sentinels. In code, `runSyndicateTurn` returns
564
703
  `status: 'input-required'` with `approval`, and the next turn's part
565
704
  `approvalResponsePart(approval.id, approved)` answers it.
566
705
 
706
+ #### Questions (`ask_user`)
707
+
708
+ An agent can ask the person something mid-turn and wait for the answer
709
+ (`lib/runtime/questions.ts`, [ADR 0031](./wiki/decisions/0031-ask-user.md)).
710
+ It lists the tool:
711
+
712
+ ```yaml
713
+ orchestrator:
714
+ name: Desk
715
+ tools: [ask_user] # ask_user(question, options?)
716
+ ```
717
+
718
+ When the model calls `ask_user`, the turn ends `input-required`, final. The
719
+ status message is the question ("Desk asks: Which account? (personal /
720
+ work)") with a data part
721
+ `{ type: 'input_request', interrupt_id, node, message, payload }`, where
722
+ `payload.options` lists the choices when the agent gave some. The caller's
723
+ next message on the same conversation is the answer: its text becomes the
724
+ call's result, and the agent resumes its own tool loop where it asked. A
725
+ workflow's `ask_user` node (§6, Workflows) publishes the same data part and
726
+ is answered the same way, so a client handles both alike.
727
+
728
+ `ask_user` is allowed where an approval gate is: the orchestrator and the
729
+ subagents of a plan-dispatch syndicate (a dispatch turn that answers goes
730
+ straight back to the route that asked, without the classifier). A delegated
731
+ subagent or a workflow node listing it is a load error. In code,
732
+ `runSyndicateTurn` returns `status: 'input-required'` with `input`
733
+ (`node`, `message`, `payload`), and the next message's text answers it.
734
+
567
735
  #### Limits
568
736
 
569
737
  | Setting | Default |
@@ -572,9 +740,13 @@ or native-search sentinels. In code, `runSyndicateTurn` returns
572
740
  | `A2A_AUTH_FAILURE_MAX` failed logins per IP per 15 min, then blocked | 30 |
573
741
  | `A2A_TASK_TIMEOUT_MS` per task | 15 min |
574
742
  | `A2A_MAX_CONCURRENT_TASKS` | unlimited |
575
- | `max_steps` (YAML): model calls per turn, subagents included | none |
743
+ | `A2A_MAX_CONCURRENT_PER_SCOPE`: tasks at once for one end user | 4 |
744
+ | `A2A_MAX_CONCURRENT_PER_CALLER`: tasks at once for one caller | unlimited |
745
+ | `max_steps` (YAML): model calls per turn, subagents included | 50 |
746
+ | `A2A_TRUST_PROXY`: proxies in front (required with `PUBLIC_URL`) | none trusted |
576
747
  | `A2A_BODY_LIMIT` | 1 MB |
577
748
  | `A2A_SHUTDOWN_GRACE_MS`: SIGTERM waits for running tasks | 25 s |
749
+ | `A2A_SHUTDOWN_DELAY_MS`: of that, `/readyz` fails while requests are still served | 5 s |
578
750
 
579
751
  The rate limit and the failed-login limit count in the process by default,
580
752
  so each replica keeps its own window. With `A2A_REDIS_URL` set (and the
@@ -604,7 +776,11 @@ Without `A2A_SERVER_SECRET` the server binds `127.0.0.1` only; binding
604
776
  another `HOST` requires the secret or `ALLOW_UNAUTHENTICATED=true`. With
605
777
  `PUBLIC_URL` set it refuses to start without the secret, with the
606
778
  `.env.example` placeholder as the secret, or against an unhardened
607
- database (unless `ALLOW_UNHARDENED_DB=true`). With durable storage it reads
779
+ database (unless `ALLOW_UNHARDENED_DB=true`), and until the deployment
780
+ states its posture: `A2A_AUTH` (prefer `callers` or `jwt`; `secret` lets any
781
+ holder of the secret act as any user), `A2A_SERVED_AGENTS` (a list, or `*`
782
+ for every agent) and `A2A_TRUST_PROXY` (the number of proxies in front, or
783
+ `false`). The refusal names each one missing. With durable storage it reads
608
784
  `melchizedek_schema_version` and refuses to start on a database behind the
609
785
  migrations it ships, naming `melchizedek-db apply` (or
610
786
  `ALLOW_SCHEMA_MISMATCH=true` to start anyway); a database ahead of it only
@@ -630,9 +806,57 @@ local conversation keeps talking to the same remote conversation.
630
806
  `Dockerfile` builds the compiled server and runs it as a non-root user with
631
807
  a health check; `compose.yaml` adds optional Ollama and Phoenix (traces).
632
808
  `npx melchizedek-db print|apply|status` installs and checks the database.
633
- Set `PUBLIC_URL`, `A2A_SERVER_SECRET` and the provider keys from your
634
- secret manager; give the orchestrator's stop timeout at least
635
- `A2A_SHUTDOWN_GRACE_MS`.
809
+ Set `PUBLIC_URL`, `A2A_SERVER_SECRET`, `A2A_AUTH`, `A2A_SERVED_AGENTS`,
810
+ `A2A_TRUST_PROXY` and the provider keys from your secret manager; give the orchestrator's stop timeout at least
811
+ `A2A_SHUTDOWN_GRACE_MS`. On SIGTERM the server first fails `/readyz` and
812
+ keeps serving for `A2A_SHUTDOWN_DELAY_MS` so the load balancer deregisters
813
+ it, then closes the listener and drains running tasks for the rest of the
814
+ grace period; point the readiness probe at `/readyz`, which also fails while
815
+ durable storage does not answer. `MELCHIZEDEK_DOTENV=off` makes every bin
816
+ ignore `.env` files and run on its environment alone.
817
+
818
+ #### Backups
819
+
820
+ Everything durable is in the one Postgres you point the server at:
821
+ conversations (`adk_sessions`, `adk_session_events`), long-term memory
822
+ (`adk_memory_facts`), the telemetry ledger, the agent registry and its
823
+ versions, A2A tasks, the task tools' lists and the budget counters. The
824
+ engine takes no backups of it; back it up as you would any production
825
+ database, with your provider's scheduled backups or point-in-time recovery
826
+ (RDS and Cloud SQL automated backups; on Supabase, a paid plan, since the
827
+ Free plan has none).
828
+
829
+ What a backup protects is the history. The rest can be rebuilt: the schema
830
+ with `npx melchizedek-db apply`, and registry agents by republishing their
831
+ YAML files. Without a backup, lost conversations, memory and ledger rows
832
+ stay lost.
833
+
834
+ A logical backup works on any provider and is worth having beside its
835
+ snapshots. Use a `pg_dump` at least as new as the server, and the
836
+ schema the tables live in (`public`, or `MELCHIZEDEK_DB_SCHEMA`):
837
+
838
+ ```bash
839
+ pg_dump --schema=public --no-owner --no-privileges -Fc -f melch.dump "$DATABASE_URL"
840
+ # restore into a new database that has pgvector:
841
+ psql "$TARGET_URL" -c 'CREATE EXTENSION IF NOT EXISTS vector'
842
+ pg_restore --no-owner --no-privileges -d "$TARGET_URL" melch.dump
843
+ npx melchizedek-db status # with DATABASE_URL=$TARGET_URL: schema version and counts
844
+ ```
845
+
846
+ Test the restore, not just the dump: restore into a scratch database and
847
+ compare row counts per table with the source. Two things a backup changes:
848
+
849
+ - **Erasure.** `DELETE /memory` removes a scope from the live database, not
850
+ from backups taken before it. Keep backup retention as short as your
851
+ recovery needs allow. After a restore, re-run any erasures made since the
852
+ backup was taken. The server logs each one with its scope (`Erasure for
853
+ scope …`); keep those lines for as long as you keep backups, since a
854
+ platform's log retention is usually shorter. The same holds for the
855
+ telemetry dead-letter spool (`outputs/telemetry-deadletter.ndjson`), which
856
+ holds ledger rows only while the database was unreachable: replay it
857
+ (`npm run telemetry:replay`) before honouring an erasure, then delete it.
858
+ - **Secrets.** A dump holds every conversation in clear text. Encrypt it at
859
+ rest and give it the same access rules as the database.
636
860
 
637
861
  #### The agent registry
638
862
 
@@ -793,6 +1017,74 @@ Implemented in `scripts/a2a_server.ts`; like `yaml_reference`, it is
793
1017
  A2A-only — the CLI runner (`scripts/syndicate_chat.ts`) still compiles
794
1018
  every syndicate in DELEGATE mode.
795
1019
 
1020
+ ### Workflows (`workflow:`) — the third orchestration method
1021
+
1022
+ A syndicate that declares a `workflow:` block is a **graph**. Its agents
1023
+ are the nodes; `edges` says what runs after what, and on which route;
1024
+ nothing delegates and nothing classifies. It is for the shape the other
1025
+ two methods cannot express: run these two at once, join what they
1026
+ produce, edit it, ask the person, then publish.
1027
+
1028
+ ```yaml
1029
+ workflow:
1030
+ edges:
1031
+ - [START, Planner, { article: [Writer, Checker], default: Answerer }]
1032
+ - [[Writer, Checker], Both, Editor, Confirm, Publisher]
1033
+ nodes:
1034
+ Planner: { route_key: "kind" } # an agent: modifiers only
1035
+ Both: { join: true } # waits for every predecessor
1036
+ Editor: { retry: { max_attempts: 2 } }
1037
+ Confirm: { ask_user: "Publish this draft? Reply yes, or say what to change." }
1038
+ ```
1039
+
1040
+ **Edges.** Each entry is a chain. `START` opens at least one chain. A
1041
+ name is an agent or a declared node; a list of names fans out (after one
1042
+ node) or fans in (before one node); a map `{ <route>: <node or nodes>,
1043
+ default: <node> }` ends a chain and routes on the output of the node
1044
+ before it — the `route_key` property of a JSON output (an agent with an
1045
+ `outputSchema`), else the trimmed text — with `default` catching what no
1046
+ key matched. Every node receives the previous node's output as its
1047
+ message; a join hands on `{ <predecessor>: <output>, … }`.
1048
+
1049
+ **Nodes.** `workflow.nodes` declares the nodes that are not agents, each
1050
+ exactly one kind: `join: true`; `map: <Agent>` (runs the agent once per
1051
+ item of a list input, concurrently, `max_parallel` at a time, and outputs
1052
+ the list of results); `tool: <registry name>` (runs the tool with the
1053
+ node input as its arguments); `ask_user: "<question>"` (below). An entry
1054
+ named after an agent carries modifiers only: `route_key`, `retry`
1055
+ (`max_attempts`, `initial_delay`, `max_delay`, `backoff_factor`, in
1056
+ seconds) and `timeout` (seconds), which any node may carry.
1057
+
1058
+ **The pause.** An `ask_user` node ends the turn `input-required`
1059
+ (`result.input`: the node, the question, and what it was asked about).
1060
+ The next message on the conversation is the answer: the node's output
1061
+ becomes `{ "reply": <the answer>, "input": <what it received> }` and the
1062
+ graph resumes where it waited. `melchizedek-chat` prints the question
1063
+ and takes the next line; the A2A server ends the task `input-required`
1064
+ with a data part `{ type: 'input_request', interrupt_id, node, message,
1065
+ payload, schema }`, and the client's next message on the same
1066
+ conversation resumes it. A later message after the graph has finished
1067
+ starts it again from `START`.
1068
+
1069
+ **What a node sees.** Unless its YAML sets `includeContents`, a node
1070
+ agent sees only its input — not the conversation, not the other nodes —
1071
+ which is what makes a graph legible: each agent's input is one value a
1072
+ person can read in the trace. Every node runs in the shared session, so
1073
+ `memory_system: long-term` would ingest node inputs as user turns;
1074
+ `internal-only` is the sensible default for a workflow.
1075
+
1076
+ **Failure.** A node's error is not the turn's: a node with `retry` tries
1077
+ again (the attempt is recorded in `answer.nodeErrors`), and a node that
1078
+ gives up fails the turn with `NODE_FAILED` naming it. `max_steps` and
1079
+ the deadline cap the whole graph as they cap any turn.
1080
+
1081
+ **Not yet.** Approval gates (`require_approval`, `skills.scripts: local`)
1082
+ and remote `a2a_agent_url` subagents are refused inside a workflow by
1083
+ the schema, and a workflow cannot be another syndicate's `yaml_reference`
1084
+ (ADK cannot yet make a `Workflow` a subagent; nested, only its
1085
+ orchestrator runs). The record is [ADR 0030](./wiki/decisions/0030-workflow-graphs.md);
1086
+ the contract is `lib/workflow.ts`, on ADK's `Workflow`.
1087
+
796
1088
  ## 7. Extending the framework
797
1089
 
798
1090
  **Call ADK directly (no syndicate)**: the YAML layer is a convenience,
@@ -801,7 +1093,7 @@ the canonical minimal block — `LlmAgent` + `Runner` +
801
1093
  `InMemorySessionService` straight from `@google/adk`, ~30 lines you can
802
1094
  copy into any repo that has `@google/adk` installed. Add
803
1095
  `registerAvailableProviders()` from `lib/models/registry.ts` and the
804
- same block runs `claude-*` / `gpt-*` / `grok-*` / `ollama/*` ids too.
1096
+ same block runs `claude-*` / `gpt-*` / `grok-*` / `kimi-*` / `ollama/*` ids too.
805
1097
 
806
1098
  **Add a syndicate**: create `config/agents/<name>.yaml` — copy the
807
1099
  closest starter-pack file from `config/agents/examples/` or start from
@@ -852,8 +1144,12 @@ by a person; `skills/README.md` records the procedure.
852
1144
  optional `adk_telemetry` sink and `adk_agent_registry` with its version
853
1145
  history (an unprotected registry is worst of all: agent definitions writable with the anon key
854
1146
  means anyone can rewrite the instructions your server boots). The A2A
855
- server verifies hardening at boot and is fatal on public deployments
856
- without it. Note `service_role` bypasses RLS by design — the hardening
1147
+ server verifies hardening at boot on both storage paths (`DATABASE_URL`
1148
+ and supabase-js) and is fatal on public deployments without it
1149
+ (`ALLOW_UNHARDENED_DB=true` accepts the risk). Over `DATABASE_URL` the check
1150
+ passes on its own when the database has no `anon` or `authenticated` role
1151
+ (plain Postgres: no API serves the tables) or when the tables live in a
1152
+ schema other than `public` (`MELCHIZEDEK_DB_SCHEMA`). Note `service_role` bypasses RLS by design — the hardening
857
1153
  constrains the API surface, not the trusted server.
858
1154
  - **A2A**: bearer auth, a failed-login limiter and a request rate limit
859
1155
  are built in; without `A2A_SERVER_SECRET` the server binds loopback only.
package/QUICKSTART.md CHANGED
@@ -14,7 +14,7 @@ fifteen. The full reference is [`DOCUMENTATION.md`](./DOCUMENTATION.md).
14
14
  - Optional: **Ollama** ([ollama.com](https://ollama.com)) for the
15
15
  open-weight local syndicates; provider keys only for the models you
16
16
  actually declare — **Anthropic** (`claude-*`), **OpenAI** (`gpt-*`),
17
- **xAI** (`grok-*`) — or one `MODEL_GATEWAY` key as the fallback for
17
+ **xAI** (`grok-*`), **Moonshot** (`kimi-*`) — or one `MODEL_GATEWAY` key as the fallback for
18
18
  whatever direct key is missing (native search is lost on that path);
19
19
  and a free **Supabase project** (only for persistent sessions / memory).
20
20
  Not sure which you need? `npm run doctor` tells you, read-only.
@@ -243,7 +243,7 @@ console.log(result.text);
243
243
  |---|---|
244
244
  | `Gemini API Key is not configured` | `.env` missing or key not set — step 2. |
245
245
  | `[400] Tool call context circulation is not enabled` | The agent's `model:` is too old for agent transfer. Use `gemini-3.8-flash` or newer (all shipped configs already do). |
246
- | `Model not found` for `claude-*` / `gpt-*` / `grok-*` | The matching provider key (`ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `XAI_API_KEY`) is not set in `.env`, so the provider wasn't registered — `npm run doctor` names the variable. Or set `MODEL_GATEWAY` + `MODEL_GATEWAY_API_KEY` once for every cloud provider (native search is lost on that path). |
246
+ | `Model not found` for `claude-*` / `gpt-*` / `grok-*` / `kimi-*` | The matching provider key (`ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `XAI_API_KEY` / `MOONSHOT_API_KEY`) is not set in `.env`, so the provider wasn't registered — `npm run doctor` names the variable. Or set `MODEL_GATEWAY` + `MODEL_GATEWAY_API_KEY` once for every cloud provider (native search is lost on that path). |
247
247
  | `GATEWAY_HTTP_ERROR … 404/400` | The gateway rejected the mapped model id. Fix the name once in `.env`: `MODEL_GATEWAY_MODEL_MAP=<your id>=<the gateway's id>`. `GATEWAY_KEY_MISSING` means `MODEL_GATEWAY` is set without `MODEL_GATEWAY_API_KEY`. |
248
248
  | `OLLAMA_UNREACHABLE` for `ollama/*` | Ollama isn't running — start the app or `ollama serve`; then check the model is pulled (`ollama list`). |
249
249
  | `Refusing to connect to private/loopback MCP host` | The SSRF guard is on (correctly). For the local demo set `ALLOW_PRIVATE_MCP=true` in `.env`. |