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.
- package/.env.example +40 -3
- package/AGENT_SETUP.md +4 -2
- package/CHANGELOG.md +246 -0
- package/DOCUMENTATION.md +311 -15
- package/QUICKSTART.md +2 -2
- package/README.md +125 -149
- package/config/agents/examples/harness.yaml +135 -0
- package/config/agents/examples/model_zoo.yaml +23 -9
- package/config/agents/examples/pipeline.yaml +120 -0
- package/config/agents/examples/specs/open-meteo-forecast.json +77 -0
- package/config/agents/examples/specs/open-meteo-geocoding.json +53 -0
- package/config/agents/examples/weather.yaml +59 -0
- package/config/agents/syndicateSchema.yaml +125 -1
- package/db/hardening.sql +4 -3
- package/db/migrations/0011_erase_expired.sql +198 -0
- package/db/migrations/0012_audit_log.sql +98 -0
- package/dist/lib/a2a/app.d.ts +27 -0
- package/dist/lib/a2a/app.js +101 -5
- package/dist/lib/a2a/executor.d.ts +34 -4
- package/dist/lib/a2a/executor.js +109 -9
- package/dist/lib/compile.d.ts +41 -3
- package/dist/lib/compile.js +162 -17
- package/dist/lib/config.d.ts +24 -0
- package/dist/lib/config.js +24 -0
- package/dist/lib/doctor.js +17 -4
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.js +1 -0
- package/dist/lib/loadEnv.d.ts +5 -0
- package/dist/lib/loadEnv.js +7 -0
- package/dist/lib/loadSyndicate.d.ts +50 -0
- package/dist/lib/loadSyndicate.js +19 -3
- package/dist/lib/memory/erase.d.ts +5 -0
- package/dist/lib/memory/erase.js +5 -0
- package/dist/lib/memory/providers.d.ts +27 -0
- package/dist/lib/memory/providers.js +28 -0
- package/dist/lib/models/capabilities.js +10 -0
- package/dist/lib/models/endpoints.d.ts +1 -1
- package/dist/lib/models/endpoints.js +4 -1
- package/dist/lib/models/fallback.d.ts +41 -0
- package/dist/lib/models/fallback.js +105 -0
- package/dist/lib/models/gateway.d.ts +2 -2
- package/dist/lib/models/gateway.js +4 -4
- package/dist/lib/models/kimiLlm.d.ts +123 -0
- package/dist/lib/models/kimiLlm.js +171 -0
- package/dist/lib/models/providerMap.d.ts +2 -2
- package/dist/lib/models/providerMap.js +4 -1
- package/dist/lib/models/registry.d.ts +1 -1
- package/dist/lib/models/registry.js +11 -3
- package/dist/lib/net/redirects.d.ts +14 -0
- package/dist/lib/net/redirects.js +94 -0
- package/dist/lib/observability/audit.d.ts +21 -0
- package/dist/lib/observability/audit.js +39 -0
- package/dist/lib/observability/metrics.d.ts +6 -0
- package/dist/lib/observability/tracer.d.ts +14 -1
- package/dist/lib/observability/tracer.js +16 -2
- package/dist/lib/persistence/supabaseProvider.d.ts +2 -6
- package/dist/lib/persistence/supabaseProvider.js +2 -31
- package/dist/lib/runtime/questions.d.ts +55 -0
- package/dist/lib/runtime/questions.js +107 -0
- package/dist/lib/runtime/syndicateTurn.d.ts +45 -5
- package/dist/lib/runtime/syndicateTurn.js +165 -17
- package/dist/lib/storage/postgres/index.d.ts +9 -0
- package/dist/lib/storage/postgres/index.js +13 -0
- package/dist/lib/storage/rlsStatus.d.ts +45 -0
- package/dist/lib/storage/rlsStatus.js +69 -0
- package/dist/lib/syndicateSchema.d.ts +177 -0
- package/dist/lib/syndicateSchema.js +343 -3
- package/dist/lib/toolRegistry.js +9 -0
- package/dist/lib/tools/mcpToolFactory.d.ts +26 -0
- package/dist/lib/tools/mcpToolFactory.js +61 -4
- package/dist/lib/tools/openapiTools.d.ts +52 -0
- package/dist/lib/tools/openapiTools.js +225 -0
- package/dist/lib/tools/skillToolset.d.ts +135 -0
- package/dist/lib/tools/skillToolset.js +247 -0
- package/dist/lib/tools/urlContextTool.d.ts +29 -0
- package/dist/lib/tools/urlContextTool.js +39 -0
- package/dist/lib/workflow.d.ts +72 -0
- package/dist/lib/workflow.js +180 -0
- package/dist/lib/workflowConfig.d.ts +95 -0
- package/dist/lib/workflowConfig.js +93 -0
- package/dist/scripts/a2a_server.js +51 -8
- package/dist/scripts/db.d.ts +9 -0
- package/dist/scripts/db.js +30 -15
- package/dist/scripts/syndicate_chat.js +39 -5
- package/package.json +6 -3
- package/skills/briefs/_shared.md +1 -1
- package/skills/briefs/melchizedek-author.md +8 -4
- package/skills/briefs/melchizedek-memory.md +1 -1
- package/skills/briefs/melchizedek-models.md +1 -1
- package/skills/briefs/melchizedek-scribe.md +4 -4
- package/skills/briefs/melchizedek-serve.md +1 -1
- package/skills/briefs/melchizedek.md +4 -1
- package/skills/melchizedek/SKILL.md +4 -1
- package/skills/melchizedek-author/SKILL.md +7 -3
- package/skills/melchizedek-memory/SKILL.md +1 -1
- package/skills/melchizedek-models/SKILL.md +1 -1
- package/skills/melchizedek-scribe/SKILL.md +3 -3
- package/skills/melchizedek-serve/SKILL.md +1 -1
- /package/skills/melchizedek-author/{templates → assets}/minimal.yaml +0 -0
- /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
|
|
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
|
|
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`):
|
|
465
|
-
scope, status, reason, duration, model calls and
|
|
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
|
-
| `
|
|
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`)
|
|
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`
|
|
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
|
|
856
|
-
|
|
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`. |
|