melchizedek-agents 1.7.0 → 1.10.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 (57) hide show
  1. package/CHANGELOG.md +161 -1
  2. package/DOCUMENTATION.md +77 -10
  3. package/README.md +2 -0
  4. package/config/agents/syndicate.schema.json +69 -0
  5. package/config/agents/syndicateSchema.yaml +17 -0
  6. package/db/hardening.sql +3 -1
  7. package/db/migrations/0016_span_tree.sql +52 -0
  8. package/db/migrations/0017_cache_pricing.sql +146 -0
  9. package/db/migrations/0018_queue_view.sql +82 -0
  10. package/db/telemetry.sql +36 -8
  11. package/dist/lib/a2a/app.d.ts +26 -4
  12. package/dist/lib/a2a/app.js +41 -6
  13. package/dist/lib/a2a/executor.d.ts +28 -1
  14. package/dist/lib/a2a/executor.js +65 -2
  15. package/dist/lib/a2a/feedback.d.ts +121 -0
  16. package/dist/lib/a2a/feedback.js +219 -0
  17. package/dist/lib/a2a/policy.d.ts +55 -5
  18. package/dist/lib/a2a/policy.js +56 -12
  19. package/dist/lib/compile.d.ts +2 -0
  20. package/dist/lib/compile.js +2 -0
  21. package/dist/lib/compileNative.js +2 -0
  22. package/dist/lib/doctor.d.ts +10 -0
  23. package/dist/lib/doctor.js +33 -4
  24. package/dist/lib/index.d.ts +6 -2
  25. package/dist/lib/index.js +3 -0
  26. package/dist/lib/loadSyndicate.d.ts +14 -0
  27. package/dist/lib/loadSyndicate.js +22 -5
  28. package/dist/lib/memory/providers.js +7 -3
  29. package/dist/lib/observability/lineage.d.ts +19 -0
  30. package/dist/lib/observability/lineage.js +31 -0
  31. package/dist/lib/observability/metadata.d.ts +69 -0
  32. package/dist/lib/observability/metadata.js +153 -0
  33. package/dist/lib/observability/otlpFilter.js +1 -0
  34. package/dist/lib/observability/supabaseSpanExporter.d.ts +31 -2
  35. package/dist/lib/observability/supabaseSpanExporter.js +45 -6
  36. package/dist/lib/observability/tracer.d.ts +17 -1
  37. package/dist/lib/observability/tracer.js +61 -3
  38. package/dist/lib/project.d.ts +116 -0
  39. package/dist/lib/project.js +274 -0
  40. package/dist/lib/runtime/native/compaction.js +7 -3
  41. package/dist/lib/runtime/native/request.d.ts +2 -0
  42. package/dist/lib/runtime/native/step.js +1 -1
  43. package/dist/lib/runtime/native/telemetry.d.ts +10 -3
  44. package/dist/lib/runtime/native/telemetry.js +49 -4
  45. package/dist/lib/runtime/syndicateTurn.js +5 -1
  46. package/dist/lib/storage/postgres/index.d.ts +10 -0
  47. package/dist/lib/storage/postgres/index.js +6 -1
  48. package/dist/lib/syndicateSchema.d.ts +5 -0
  49. package/dist/lib/syndicateSchema.js +13 -0
  50. package/dist/lib/tools/taskTools.d.ts +6 -0
  51. package/dist/lib/tools/taskTools.js +24 -5
  52. package/dist/lib/workflow/toolNode.d.ts +1 -0
  53. package/dist/lib/workflow/toolNode.js +1 -1
  54. package/dist/scripts/a2a_server.js +3 -3
  55. package/dist/scripts/doctor.js +2 -1
  56. package/dist/scripts/mcp_server.js +2 -1
  57. package/package.json +6 -4
package/CHANGELOG.md CHANGED
@@ -4,7 +4,167 @@ Consumers of the package read this file; it records changes to the
4
4
  **published API surface** (the exports map in `package.json`, the bins,
5
5
  the starter pack and the templates), not the repo's full history.
6
6
 
7
- ## Unreleased
7
+ ## 1.10.0 — 2026-10-20
8
+
9
+ A minor release for deployments: a served syndicate takes end-user
10
+ feedback at POST /feedback into the ledger (ADR 0140), budgets report
11
+ their state and can stop on a caller's own rule, and the job queue has a
12
+ read model with a retry.
13
+
14
+ ### Added
15
+
16
+ - **End-user feedback, `POST /feedback` and `POST /<agentId>/feedback`
17
+ (ADR 0140).** A product's thumbs or 1–5 rating of a served turn, named by
18
+ its A2A `taskId` or its `traceId`, with an optional `comment` (≤ 4,000
19
+ characters) and `userId`, lands in the ledger as one `adk_labels` row:
20
+ `ref = trace_id = <trace id>`, `labeler = 'end-user'`,
21
+ `fields = { score, userId }`, `note` = the comment through the telemetry
22
+ redactor. Behind the bearer, the authenticator and the task rate limit,
23
+ with a 32 KB body limit, the caller's own turns only, and one label per
24
+ (turn, user) per minute. New option `storage.feedback`; `postgresStorage`
25
+ supplies it when `ledger` is on. New subpath
26
+ `melchizedek-agents/a2a/feedback` and barrel exports: `parseFeedback`,
27
+ `feedbackRow`, `postgresFeedbackStore`, `END_USER_LABELER`, types
28
+ `FeedbackStore`, `FeedbackRow`, `FeedbackScore`, `FeedbackTurn`.
29
+ - **Budget hooks.** `budgets(config, opts)` takes `onBudgetState(state)`,
30
+ called with each subject's day totals, limits and level (`ok`, `warn`,
31
+ `over`) after every task and when a task arrives over a limit; `warnAt`
32
+ (default 0.8); and `hardStop`: `true` (default, as before), `false`
33
+ (limits only report), or a function returning a refusal reason, asked at
34
+ admission (a local server's own spend check). New types `BudgetState`,
35
+ `BudgetLevel`, `BudgetOptions`, `HardStop`.
36
+ - **The job queue's read model.** Migration `0018_queue_view` adds the view
37
+ `melchizedek_task_queue`: one row per background job with `attempts`,
38
+ `retries`, `max_attempts`, `dead_lettered`, `last_error`, the lease and
39
+ the checkpoint. `postgresStorage().retryTask(owner, id)` queues a failed
40
+ or cancelled job again with its attempts reset (`applyRetry` in
41
+ `lib/tools/taskTools.ts`, the rule `task_update`'s `queued` applies).
42
+ The view is revoked from the API roles in the migration and in
43
+ `db/hardening.sql`.
44
+
45
+ ## 1.9.0 — 2026-10-20
46
+
47
+ Not published on its own: these changes ship in 1.10.0.
48
+
49
+ A minor release for observability: the ledger keeps a turn's span tree
50
+ with cache tokens and their prices (ADR 0139), and a syndicate's YAML
51
+ and an A2A request can label a turn with metadata, with a hook on every
52
+ task state (ADR 0141).
53
+
54
+ The ledger keeps a turn's span tree, counts prompt-cache tokens, and
55
+ prices them at their own rates (ADR 0139). **Apply the schema before
56
+ upgrading the engine:** `npx melchizedek-db apply --telemetry` runs the
57
+ new migrations 0016 and 0017 and `db/telemetry.sql`; the sinks write the
58
+ new columns, so a ledger without them refuses the rows (they are spooled
59
+ to the dead-letter file, and `telemetry:replay` re-sends them once the
60
+ schema is applied). Rows written before keep NULL in every new column.
61
+
62
+ ### Added
63
+
64
+ - **The span tree in `adk_telemetry` (migration `0016_span_tree`).** Every
65
+ span of a turn gets a row: the root, `agent.invoke <name>`, `model.call`,
66
+ `llm.request`, `tool.execute <name>`, `script.invoke` / `script.call`,
67
+ `workflow.invoke` / `node.execute`. New columns `parent_span_id` (the
68
+ nearest ancestor that has a row, read from the tracer's lineage when the
69
+ span starts and recorded as `ledger.parent_span_id` where it differs
70
+ from the direct parent), `kind` (`turn | agent | model | tool | script |
71
+ node`) and `error`; index `idx_adk_telemetry_parent`. A `model.call` row
72
+ carries no tokens and no payload (its `llm.request` rows and
73
+ `adk_payloads` hold them). New exports on `./observability/*`:
74
+ `ledgerSpanKind`, `LedgerSpanKind`, `LEDGER_PARENT_ATTRIBUTE`
75
+ (`lineage`); `isLedgerSpan`, `ledgerParentOf`, `spanFailed`, and the
76
+ new `TelemetryRow` / `TurnRow` fields (`supabaseSpanExporter`).
77
+ - **Tool arguments and results on tool spans.** `tool.args` and
78
+ `tool.result`, JSON, run through the telemetry redactor and then cut at
79
+ 8,192 characters (`TOOL_ATTRIBUTE_MAX_CHARS`, with
80
+ `tool.args_truncated` / `tool.result_truncated`), on the loop's tool
81
+ calls and a tool node's. `OTEL_EXPORT_CONTENT=off` drops `tool.result`
82
+ as it drops `tool.args`.
83
+ - **Cache tokens and finish reasons.** `llm.request` records
84
+ `llm.tokens.cache_read` and `llm.tokens.cache_write` (parts of
85
+ `llm.tokens.input`, set when the provider reports them) and
86
+ `llm.finish_reason`; `model.call` records `llm.finish_reason` too.
87
+ `adk_telemetry` and `adk_turns` gain `cache_read_tokens` and
88
+ `cache_write_tokens` (a turn's summed over its calls; the root span's
89
+ `syndicate.tokens.cache_read` / `.cache_write`), and `syndicate.usage`
90
+ carries `cache_read` / `cache_write` per model. `LlmCallMeta.source`
91
+ lets a caller of `traceLlmGeneration` hand the contract response each
92
+ `LlmResponse` came from, which is where a cache write is read.
93
+ - **Cache pricing (migration `0017_cache_pricing`).** `model_pricing`
94
+ gains `cache_read_per_mtok` and `cache_write_per_mtok` (NULL prices the
95
+ token as input, so every existing row prices as before);
96
+ `melchizedek_cost_usd(model, input, output, thinking, cache_read,
97
+ cache_write)` prices them (the four-argument form stays);
98
+ `melchizedek_usage_cost` and the ledger trigger read them. Seeded from
99
+ the vendors' lists, checked 2026-10-20: the Anthropic rows 0015 seeded
100
+ gain their cache rates (5-minute write) while they still hold 0015's
101
+ rates, `claude-opus-5-5` and `claude-sonnet-5-5` are added, and OpenAI's
102
+ `gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-sol`, `gpt-6-luna`, `gpt-5.6-sol` /
103
+ `-terra` / `-luna`, `gpt-5.5`, `gpt-5.4` (under-272K rates), `gpt-5.2`,
104
+ `gpt-5.1`, `gpt-5`, `gpt-5-mini`, `gpt-5-nano` get rows. Gemini, Grok and
105
+ Kimi rows get no cache rates.
106
+ - **`metadata:` in the syndicate YAML (ADR 0141).** `{ key: value }` on
107
+ the syndicate and on any agent: strings (at most 256 characters),
108
+ numbers or booleans, at most 32 keys, each key
109
+ `[A-Za-z0-9_][A-Za-z0-9_.:-]{0,63}`. The syndicate's become
110
+ `meta.<key>` on every root span of a turn, so on
111
+ `adk_turns.attributes`; an agent's become `meta.agent.<key>` on its
112
+ `agent.invoke` span. A value shaped like a key or token is dropped at
113
+ load with a warning (`LoadSyndicateOptions.onWarning`) naming the key,
114
+ never the value; string values pass through the telemetry redactor. An
115
+ attribute an in-process caller passes in `trace.attributes` wins a key
116
+ both name. `config/agents/syndicate.schema.json` is regenerated.
117
+ - **Per-request metadata over A2A (ADR 0141).** `X-Melch-Meta-<key>`
118
+ headers and the scalar entries of the A2A message's `metadata` object
119
+ become `meta.<key>` on the turn's root span, with the YAML's limits and
120
+ redaction; at most 32 keys a turn, the YAML's first. A header wins a key
121
+ both name; a key the YAML declares, or one starting `agent.`, is never
122
+ taken from a request; a credential-shaped value is dropped with a
123
+ warning. A malformed `X-Melch-Meta-*` header is refused with 400; a
124
+ message entry that is not metadata is skipped. `A2AContext.meta` holds
125
+ the checked headers.
126
+ - **`A2AAppOptions.onTaskState(record)` (ADR 0141).** Called after each
127
+ new state the executor publishes, with
128
+ `{ taskId, contextId, state, syndicate, agentId, traceId?, at }`:
129
+ `submitted`, `working` (with the first progress line), then the state
130
+ the task ends in. Barrel types `TaskStateRecord`, `TaskStateName`,
131
+ `Metadata`, `MetadataValue`; the helpers are in
132
+ `melchizedek-agents/observability/metadata`.
133
+
134
+ ### Docs
135
+
136
+ - `tasks/get` and the states a polling caller reads, in DOCUMENTATION.md
137
+ §6 and the A2A wiki doc.
138
+
139
+ ## 1.8.0 — 2026-10-18
140
+
141
+ A minor release that gives a repository a manifest (ADR 0138). A
142
+ `melchizedek.json` at the root names the agents folder, the exact engine
143
+ version the project is written for, and how each syndicate ships; the
144
+ bins find it on their own, `melchizedek-doctor` checks the pin, and the
145
+ desktop app opens a repository through it. Nothing changes for a
146
+ deployment that sets `MELCHIZEDEK_AGENTS_DIR`.
147
+
148
+ ### Added
149
+
150
+ - **The project manifest, `melchizedek.json` (ADR 0138).** A JSON file at
151
+ a repository's root naming its agents folder (`agents`), the engine
152
+ version it is written for (`engine`, an exact pin), an `ignore` list,
153
+ a `tests` folder, the env variable *names* it needs, and per-file
154
+ shipping records (`syndicates.<file>.deliverable`,
155
+ `publish: { kind: "registry", id }`). New subpath
156
+ `melchizedek-agents/project` and barrel exports: `findProject`,
157
+ `loadProject`, `parseProjectManifest`, `resolveAgentsDir`,
158
+ `listProjectSyndicates`, `checkProject`, `projectManifestSchema`.
159
+ - **The agents directory reads the manifest.** Every loader and bin
160
+ resolves it as: explicit option, `MELCHIZEDEK_AGENTS_DIR`, the nearest
161
+ manifest's `agents` (not above the repository root), then
162
+ `<cwd>/config/agents`. A deployment that sets the variable is
163
+ unchanged.
164
+ - **`melchizedek-doctor` reports the project**: the engine pin against
165
+ the running engine, a missing agents or tests folder, `ignore`
166
+ patterns that match nothing, unset env names, and `syndicates` keys
167
+ with no file. A pin mismatch or a missing folder fails `--check`.
8
168
 
9
169
  ## 1.7.0 — 2026-10-17
10
170
 
package/DOCUMENTATION.md CHANGED
@@ -126,6 +126,7 @@ Field reference:
126
126
  |---|---|---|
127
127
  | `syndicate_name` | root | Display name. On the CLI it also namespaces memory user keys; on the A2A server memory is keyed by the caller's silo across every long-term syndicate that server serves. |
128
128
  | `memory_system` | root | `internal-only` (nothing persists — on the server too: its transcripts stay in process memory), `session-only` (transcript persists in Supabase), `long-term` (adds fact distillation + vector recall). |
129
+ | `metadata` | root, any agent | Static labels for the ledger ([ADR 0141](./wiki/decisions/0141-metadata-and-the-task-state-hook.md)): `{ key: value }` with string (at most 256 characters), number or boolean values, at most 32 keys, each key `[A-Za-z0-9_][A-Za-z0-9_.:-]{0,63}`. The syndicate's become `meta.<key>` on every root span of a turn, so on its `adk_turns.attributes` (`meta.tags: "a,b"` gives the turn tags); an agent's become `meta.agent.<key>` on its `agent.invoke` span. A value shaped like a key or token is dropped at load with a warning naming the key; string values pass through the telemetry redactor. An A2A caller can add keys of its own (§6, Headers) but never set one named here. |
129
130
  | `variables` | root | Key/values bound into `{{placeholders}}` anywhere in instructions. `current_date` is always injected; CLI `--bind key=value` overrides. |
130
131
  | `memory_extraction_rules` | root | Domain rules appended to the shared fact-extraction prompt for THIS syndicate only (requires `memory_system: "long-term"`). The extraction prompt is global — anything domain-specific belongs here, never edited into it. Unset, the prompt renders byte-identical to before the slot existed (§4). |
131
132
  | `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`. |
@@ -192,7 +193,7 @@ function, and every adapter recognises it by marker
192
193
  | `inspect_image` | Contract | **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`). |
193
194
  | `task_add` / `task_list` / `task_get` / `task_update` | Contract | 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. |
194
195
  | `ask_user` | Contract, long-running | 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. Any agent but a workflow node's; inside a delegated subagent the question reaches the person with the agent path (§6, Questions). |
195
- | `task_queue` | Contract | 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. A run is durable (ADR 0113): it is checkpointed beside the job at every step boundary (migration 0014 on Postgres), a job claimed again resumes from its last checkpoint (inside a dispatch route too, ADR 0121), a checkpoint above `MELCHIZEDEK_CHECKPOINT_MAX_BYTES` (default 5 MiB of JSON; `TaskBackendOptions.checkpointMaxBytes`, `postgresStorage({ taskQueue })`) is skipped and the previous one kept, SIGTERM re-queues the job in hand, and `task_update` can cancel a running job. |
196
+ | `task_queue` | Contract | 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. A run is durable (ADR 0113): it is checkpointed beside the job at every step boundary (migration 0014 on Postgres), a job claimed again resumes from its last checkpoint (inside a dispatch route too, ADR 0121), a checkpoint above `MELCHIZEDEK_CHECKPOINT_MAX_BYTES` (default 5 MiB of JSON; `TaskBackendOptions.checkpointMaxBytes`, `postgresStorage({ taskQueue })`) is skipped and the previous one kept, SIGTERM re-queues the job in hand, and `task_update` can cancel a running job. On Postgres the view `melchizedek_task_queue` (migration 0018) lists every job with `attempts`, `retries`, `dead_lettered`, `last_error`, its lease and checkpoint, and `postgresStorage().retryTask(owner, id)` queues a failed or cancelled job again. |
196
197
 
197
198
  **MCP tools** are the exception to the registry: a subagent with
198
199
  `mcp_server_url:` (or several servers under `mcp_servers:`) in its YAML
@@ -626,8 +627,14 @@ row per turn with input, output, the responding agent, the plan-dispatch
626
627
  route, errors, tokens, model-vs-tool latency, the tool calls with their
627
628
  responses, the ids that join it to `adk_sessions` (`session_id`,
628
629
  `invocation_id`), provenance (`config_hash`, `engine_version`) and a
629
- full-text `search` column; `adk_telemetry` — one row per `llm.request` /
630
- root span; and `adk_payloads` — full prompts and responses per model call,
630
+ full-text `search` column; `adk_telemetry` — one row per span of the
631
+ turn's tree (the root, `agent.invoke`, `model.call`, `llm.request`,
632
+ `tool.execute`, script and workflow spans), each with `parent_span_id`
633
+ (the nearest ancestor that has a row), `kind` (`turn | agent | model |
634
+ tool | script | node`) and `error` (migration 0016, ADR 0139); a tool
635
+ span carries `tool.args` and `tool.result` (redacted, then cut at 8 KB),
636
+ and tokens sit on the `llm.request` rows and the root, never on a
637
+ `model.call` row; and `adk_payloads` — full prompts and responses per model call,
631
638
  kept by policy (`TELEMETRY_PAYLOADS=off|errors|sample|all`,
632
639
  `TELEMETRY_PAYLOAD_SAMPLE`, `TELEMETRY_PAYLOAD_TTL_DAYS`) and expired by
633
640
  `melchizedek_prune_telemetry()`. A row from a failed call's `llm.request`
@@ -648,6 +655,14 @@ input summed per call), thinking at the output rate unless the row sets
648
655
  never a partial sum; `ollama/*` costs zero. Add or change a rate with an
649
656
  `INSERT … ON CONFLICT (model) DO UPDATE` on `model_pricing`; rows already
650
657
  written keep the rate in force when they were written (ADR 0136).
658
+ Prompt-cache tokens are parts of the input: each `llm.request` records
659
+ `llm.tokens.cache_read` / `llm.tokens.cache_write` (and
660
+ `llm.finish_reason`), `adk_telemetry` and `adk_turns` carry
661
+ `cache_read_tokens` / `cache_write_tokens` (the turn's summed over its
662
+ calls), and `syndicate.usage` carries `cache_read` / `cache_write` per
663
+ model. `model_pricing.cache_read_per_mtok` / `cache_write_per_mtok` price
664
+ them (migration 0017; NULL prices them as input), seeded for Anthropic and
665
+ OpenAI from their price lists (ADR 0139).
651
666
  Operate it with `npm run telemetry:stats`,
652
667
  `telemetry:prune` and `telemetry:replay` (the exporter spools failed
653
668
  batches to `outputs/telemetry-deadletter.ndjson`).
@@ -700,6 +715,7 @@ README has the commands.
700
715
  | `GET /<agentId>/.well-known/agent-card.json` | bearer | another syndicate's card; its URLs point at `/<agentId>/a2a/...` |
701
716
  | `POST /<agentId>/a2a/jsonrpc`, `/<agentId>/a2a/rest` | bearer | another syndicate (`A2A_SERVED_AGENTS` restricts which) |
702
717
  | `DELETE /memory` | bearer | erase everything stored for the calling scope: facts, sessions (with subagent rows), ledger rows, A2A tasks and the third-party tokens held for its tools, including conversations whose sessions have expired, with per-store counts; `?all=1` covers every memory namespace |
718
+ | `POST /feedback`, `/<agentId>/feedback` | bearer | an end user's rating of a turn (ADR 0140): `{ "taskId" or "traceId", "score": "up" \| "down" \| 1–5, "comment"?, "userId"? }` → `201 { ok, traceId }`; one `adk_labels` row (below). 501 without the ledger |
703
719
 
704
720
  "bearer" means the credential `A2A_AUTH` asks for (below); with no
705
721
  `A2A_SERVER_SECRET` and no authenticator the server binds `127.0.0.1` only.
@@ -745,6 +761,38 @@ or pass your own `resolveRequest`.
745
761
  The counts live in the `melchizedek_usage` table (`db/migrations/0004_usage.sql`)
746
762
  when Postgres or Supabase is configured, else in process memory. A store
747
763
  that cannot be read refuses the task.
764
+ `budgets(config, { onBudgetState, warnAt, hardStop })` adds two hooks.
765
+ `onBudgetState(state)` is called with each subject's standing after every
766
+ task (`cause: 'record'`) and when a task arrives over a limit
767
+ (`cause: 'admit'`, `stopped` saying whether it was refused):
768
+ `{ subject: 'caller:<name>' | 'scope:<hash>', day, usage, limits?,
769
+ level: 'ok' | 'warn' | 'over', reached?, agentId }`, `warn` from `warnAt`
770
+ (default 0.8) of any limit, so a consumer can mirror a deployment's
771
+ spend. `hardStop: false` makes the limits advisory (reported, not
772
+ enforced); a function `(subject) => reason | undefined` is an outside
773
+ stop asked at admission after the limits, whose reason refuses the task
774
+ (a desktop app serving locally passes its monthly spend check, with
775
+ `budgets({}, { hardStop })`). It may throw; that refuses too.
776
+ - **End-user feedback** (`POST /feedback`, `lib/a2a/feedback.ts`, ADR 0140):
777
+ a product's thumbs or 1–5 rating of a turn, named by its A2A `taskId` (a
778
+ dispatch task's answer, not its classifier turn) or its `traceId`, with an
779
+ optional `comment` (at most 4,000 characters) and the product's own
780
+ `userId` ([A-Za-z0-9._@:+/-], at most 128; a credential-shaped one is
781
+ refused). It writes one `adk_labels` row: `ref` and `trace_id` the turn's
782
+ trace id, `labeler = 'end-user'`, `fields = { score, userId }` (`score`
783
+ the string or the number as sent, `userId` null when none), `note` the
784
+ comment through the telemetry redactor (`TELEMETRY_REDACT`), null when
785
+ none. The route is behind the bearer, the authenticator and the task rate
786
+ limit like a task, with its own 32 KB body limit. A caller rates only
787
+ turns whose `user_id` is its scope (or beneath a scope that owns nested
788
+ ones) unless it holds an operator credential; another's turn answers the
789
+ same 404 as a missing one, and a turn reaches the ledger a few seconds
790
+ after it ends. One label per turn per `userId` (else per scope) per
791
+ minute, per process: 429 with `Retry-After`. Answers never repeat what
792
+ was sent. It needs the ledger in Postgres: `postgresStorage({ ledger:
793
+ true })` supplies `storage.feedback`, and `postgresFeedbackStore(pool)`
794
+ builds one on any pool with `db/telemetry.sql` applied; the supabase-js
795
+ path has none.
748
796
  - **One record per task** (option `onTaskEnd`): task id, trace id, agent,
749
797
  caller, a hash of the scope, status, reason, duration, model calls and
750
798
  tokens. The trace id is the one the turn's spans and its ledger row carry,
@@ -755,6 +803,14 @@ or pass your own `resolveRequest`.
755
803
  record carries `callerTraceId`. The turn keeps a trace id of its own, since
756
804
  ledger attribution and erasure key on it and a caller must not choose it.
757
805
  `A2A_LOG_FORMAT=json` prints every server line as JSON, this record included.
806
+ - **Every state a task enters** (option `onTaskState`): `{ taskId, contextId,
807
+ state, syndicate, agentId, traceId?, at }` after each new state the server
808
+ publishes: `submitted`, `working` (with the first progress line), then
809
+ `completed`, `failed`, `canceled`, `rejected` or `input-required`. A repeat
810
+ of the state is not reported again; `traceId` is there once the turn's root
811
+ span started; `at` is ms since the epoch; a throwing hook is ignored. For a
812
+ live view of work in flight. A caller polling instead reads the same state
813
+ with `tasks/get` (below).
758
814
  - **The audit trail** (`melchizedek_audit`, `db/migrations/0012_audit_log.sql`;
759
815
  option `audit`, supplied by `postgresStorage`): one row per failed
760
816
  authentication (`auth.failure`), task outcome (`task.end`), erasure
@@ -825,6 +881,7 @@ sending it regulated data.
825
881
  | `X-API-Key` | BYOK mode only: the caller's model key (see above) |
826
882
  | `X-Provider` | BYOK mode only: which provider `X-API-Key` belongs to (default `google`) |
827
883
  | `X-Surface`, `X-Surface-Guild`, `-Channel`, `-User` | optional, telemetry only |
884
+ | `X-Melch-Meta-<key>` | optional, telemetry only: `meta.<key>` (the key lowercased) on the turn's root span, with the limits of the YAML's `metadata:`. A header outside them is refused with 400. The A2A message's own `metadata` object adds its scalar entries the same way (a header wins a key both name; other entries are skipped). At most 32 keys a turn, the YAML's first; a key the YAML declares, or one starting `agent.`, is never taken from a request; a credential-shaped value is dropped ([ADR 0141](./wiki/decisions/0141-metadata-and-the-task-state-hook.md)). |
828
885
 
829
886
  #### Sessions
830
887
 
@@ -840,7 +897,12 @@ transcripts in process memory even when Supabase is configured.
840
897
  (Method names below are 0.3's; 1.0 clients use `SendMessage`,
841
898
  `GetTask`, `SendStreamingMessage`, `CancelTask` and the 1.0 enum spellings.)
842
899
  `message/send` blocks until the turn finishes unless the request sets
843
- `configuration.blocking: false`; then poll `tasks/get`. `message/stream`
900
+ `configuration.blocking: false`; then poll `tasks/get` with the returned
901
+ task id (`{"jsonrpc":"2.0","id":2,"method":"tasks/get","params":{"id":"<task id>"}}`):
902
+ `result.status.state` is `submitted`, `working`, `input-required` (an
903
+ approval, a question or a consent waits; the status message says which),
904
+ `completed`, `failed`, `canceled` or `rejected`, and once it is final,
905
+ `result.status.message` carries the answer or the reason. `message/stream`
844
906
  emits `[STATUS]` progress updates (tool calls, the chosen route, guard
845
907
  notes) and the answer as the final status message. With
846
908
  `A2A_STREAM_TEXT=true` (`streamText` in code) it also streams the answer as
@@ -1610,22 +1672,27 @@ subclass), and register it behind a model-id prefix.
1610
1672
  subagent. `scripts/demo_mcp_server.ts` is a complete server to copy —
1611
1673
  tool definitions, SSE wiring, and persistent state in ~250 lines.
1612
1674
 
1613
- **Teach your coding agent the framework**: `skills/` holds six Agent
1675
+ **Teach your coding agent the framework**: `skills/` holds eleven Agent
1614
1676
  Skills (the open SKILL.md standard — a directory per skill, `name` and
1615
1677
  `description` frontmatter) that give Claude Code, Codex, Cursor,
1616
1678
  OpenCode or Gemini CLI the catalog of syndicates and the procedures in
1617
1679
  this document: `melchizedek` (find and run, delegate a task),
1618
1680
  `melchizedek-author`, `melchizedek-serve`, `melchizedek-memory`,
1619
- `melchizedek-models`, `melchizedek-scribe`. `npx melchizedek-skills
1681
+ `melchizedek-models`, `melchizedek-scribe`, and five onboarding skills
1682
+ that route a person by the credentials they have — `melchizedek-onboard`
1683
+ (the triage) and `melchizedek-onboard-local`, `-keys`, `-cloud` and
1684
+ `-serve` (the authentication levels each owns). `npx melchizedek-skills
1620
1685
  install` copies them into `.claude/skills/` and `.agents/skills/` of the
1621
1686
  current project (`--for claude,codex,cursor,opencode,gemini,agents,all`,
1622
1687
  `--global`, `--dir <path>`, `--only <names>`, `--force`, `--dry-run`;
1623
1688
  `npm run skills:install` in a clone). The installer (`lib/skills.ts`)
1624
1689
  copies only from the package's own `skills/` directory, follows no
1625
- symlinks, and never overwrites a differing file without `--force`. Each
1626
- SKILL.md body was written by the Scribe syndicate
1627
- (`config/agents/examples/scribe.yaml`) from a brief of facts and reviewed
1628
- by a person; `skills/README.md` records the procedure.
1690
+ symlinks, and never overwrites a differing file without `--force`. The
1691
+ onboarding skills are short procedures written by hand that print their
1692
+ facts from `melchizedek-setup --level <id>`; every other SKILL.md body was
1693
+ written by the Scribe syndicate (`config/agents/examples/scribe.yaml`)
1694
+ from a brief of facts and reviewed by a person; `skills/README.md`
1695
+ records the procedure.
1629
1696
 
1630
1697
  ## 8. Security notes
1631
1698
 
package/README.md CHANGED
@@ -106,6 +106,8 @@ console.log(result.status, result.text);
106
106
 
107
107
  `runSyndicateTurn` is the same runtime the server, CLI, and eval harness use. It runs every syndicate on the engine's own agent loop and, for a workflow, its own scheduler (the `native` runtime, the only one). `MELCHIZEDEK_RUNTIME=adk` is an error that names 1.0.0; `npx melchizedek-doctor` prints the runtime in use (ADR 0107). To read syndicates from somewhere else, pass `loadSyndicate(file, { agentsDir })` or set `MELCHIZEDEK_AGENTS_DIR`. To serve inside your own Express app, mount `(await createA2AApp({ defaultSyndicate: 'mine.yaml', serverSecret })).app`. `events.onEvent` and `events.onTextDelta` follow the answering agent; `events.onAgentEvent(event, branch)` follows every delegated subagent live, its thinking, text and tool calls, with `branch.path` and `branch.callId` saying where it runs (ADR 0135).
108
108
 
109
+ A served syndicate also takes end-user ratings at `POST /feedback` (`{ traceId | taskId, score: 'up' | 'down' | 1–5, comment?, userId? }`, into the ledger's `adk_labels`; `melchizedek-agents/a2a/feedback`, ADR 0140), and `budgets(config, { onBudgetState, hardStop })` from `melchizedek-agents/a2a/policy` mirrors each caller's spend and takes an outside stop. On Postgres, `melchizedek_task_queue` (migration 0018) is the job queue's read model and `postgresStorage().retryTask(owner, id)` its Retry.
110
+
109
111
  The example syndicates ship inside the package at `node_modules/melchizedek-agents/config/agents/examples/`; copy one out as a starting point.
110
112
 
111
113
  ### Custom Tools and Guards
@@ -1028,6 +1028,29 @@
1028
1028
  }
1029
1029
  },
1030
1030
  "additionalProperties": false
1031
+ },
1032
+ "metadata": {
1033
+ "type": "object",
1034
+ "propertyNames": {
1035
+ "type": "string",
1036
+ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,63}$"
1037
+ },
1038
+ "additionalProperties": {
1039
+ "anyOf": [
1040
+ {
1041
+ "type": "string",
1042
+ "maxLength": 256
1043
+ },
1044
+ {
1045
+ "type": "number"
1046
+ },
1047
+ {
1048
+ "type": "boolean"
1049
+ }
1050
+ ]
1051
+ },
1052
+ "description": "Labels for the ledger: strings (at most 256 characters), numbers or booleans, at most 32 keys. Each becomes `meta.agent.<key>` on this agent's agent.invoke span. A value shaped like a key or token is dropped at load with a warning; string values pass through the telemetry redactor (ADR 0141).",
1053
+ "maxProperties": 32
1031
1054
  }
1032
1055
  },
1033
1056
  "required": [
@@ -2064,6 +2087,29 @@
2064
2087
  },
2065
2088
  "additionalProperties": false
2066
2089
  },
2090
+ "metadata": {
2091
+ "type": "object",
2092
+ "propertyNames": {
2093
+ "type": "string",
2094
+ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,63}$"
2095
+ },
2096
+ "additionalProperties": {
2097
+ "anyOf": [
2098
+ {
2099
+ "type": "string",
2100
+ "maxLength": 256
2101
+ },
2102
+ {
2103
+ "type": "number"
2104
+ },
2105
+ {
2106
+ "type": "boolean"
2107
+ }
2108
+ ]
2109
+ },
2110
+ "description": "Labels for the ledger: strings (at most 256 characters), numbers or booleans, at most 32 keys. Each becomes `meta.agent.<key>` on this agent's agent.invoke span. A value shaped like a key or token is dropped at load with a warning; string values pass through the telemetry redactor (ADR 0141).",
2111
+ "maxProperties": 32
2112
+ },
2067
2113
  "yaml_reference": {
2068
2114
  "description": "A whole nested syndicate (filename under the agents dir) used as this subagent. An approval request or ask_user question raised inside it pauses the turn with the agent path (ADR 0110, ADR 0111): a delegate syndicate's gates anywhere, a dispatch syndicate's on its routes, which it classifies and runs as at the top, its route's answer being its own (ADR 0120), and a workflow's ask_user nodes and gates, wherever each runs: delegated to as a subagent, as a dispatch route or as a workflow node (ADR 0119).",
2069
2115
  "type": "string",
@@ -2384,6 +2430,29 @@
2384
2430
  },
2385
2431
  "additionalProperties": false
2386
2432
  },
2433
+ "metadata": {
2434
+ "type": "object",
2435
+ "propertyNames": {
2436
+ "type": "string",
2437
+ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,63}$"
2438
+ },
2439
+ "additionalProperties": {
2440
+ "anyOf": [
2441
+ {
2442
+ "type": "string",
2443
+ "maxLength": 256
2444
+ },
2445
+ {
2446
+ "type": "number"
2447
+ },
2448
+ {
2449
+ "type": "boolean"
2450
+ }
2451
+ ]
2452
+ },
2453
+ "description": "Labels for the ledger: strings (at most 256 characters), numbers or booleans, at most 32 keys. Each becomes `meta.<key>` on every turn's root span, so on the ledger row (adk_turns.attributes); a key the YAML names cannot be set by an A2A caller. A value shaped like a key or token is dropped at load with a warning; string values pass through the telemetry redactor (ADR 0141).",
2454
+ "maxProperties": 32
2455
+ },
2387
2456
  "variables": {
2388
2457
  "description": "Defaults for {{token}} interpolation. {{current_date}} is built in.",
2389
2458
  "type": "object",
@@ -53,6 +53,18 @@ syndicate_name: "My Syndicate"
53
53
  variables:
54
54
  my_variable: "default_value"
55
55
 
56
+ # [optional] Static labels for the ledger (ADR 0141): each becomes
57
+ # `meta.<key>` on the turn's root span, so on its adk_turns row; `tags`
58
+ # (comma-separated) gives the turn tags. Strings (at most 256 characters),
59
+ # numbers or booleans; keys [A-Za-z0-9_][A-Za-z0-9_.:-]{0,63}; at most 32.
60
+ # ⚠ A value shaped like a key or token is dropped at load with a warning.
61
+ # An A2A caller's own metadata (X-Melch-Meta-*, Message.metadata) never
62
+ # overrides a key named here.
63
+ # metadata:
64
+ # environment: "prod"
65
+ # team: "growth"
66
+ # tags: "nightly,eu"
67
+
56
68
  # [optional] Self-correction, ON by default: the engine's reflect-and-retry,
57
69
  # ported from ADK's plugins (ADR 0034, ADR 0075). model_errors: retries of a model
58
70
  # reply the engine marks malformed (default 2). tool_errors: retries of a tool that threw, with
@@ -323,6 +335,11 @@ orchestrator:
323
335
  # stderr are each cut at 20,000 characters, with a line saying so.
324
336
  # Maps to: tools[] (a toolset) and instruction (the appended index)
325
337
 
338
+ # [optional] Labels for this agent's agent.invoke span, each as
339
+ # `meta.agent.<key>`; the same rules as the syndicate's metadata.
340
+ # metadata:
341
+ # role: "researcher"
342
+
326
343
  # [optional] Few-shot exchanges the engine adds to every request's
327
344
  # instruction (lib/tools/examples.ts), in the block ADK's ExampleTool
328
345
  # wrote; the model never calls it.
package/db/hardening.sql CHANGED
@@ -84,7 +84,9 @@ BEGIN
84
84
  END LOOP;
85
85
  -- The production view and the retention function read/delete those
86
86
  -- tables on behalf of the caller; neither belongs on the public API.
87
- FOREACH t IN ARRAY ARRAY['adk_turns_production', 'adk_kpi_daily', 'adk_kpi_routes_daily', 'adk_kpi_judges_daily', 'adk_kpi_hourly'] LOOP
87
+ -- So does the job queue's read model (migration 0018).
88
+ FOREACH t IN ARRAY ARRAY['adk_turns_production', 'adk_kpi_daily', 'adk_kpi_routes_daily', 'adk_kpi_judges_daily', 'adk_kpi_hourly',
89
+ 'melchizedek_task_queue'] LOOP
88
90
  IF to_regclass('public.' || t) IS NOT NULL THEN
89
91
  PERFORM pg_temp.melchizedek_revoke_api(format('%I', t));
90
92
  END IF;
@@ -0,0 +1,52 @@
1
+ -- ============================================================================
2
+ -- 0016_span_tree — the ledger keeps a turn's span tree and its cache tokens
3
+ -- (ADR 0139)
4
+ -- ============================================================================
5
+ -- adk_telemetry holds one row per span the ledger keeps: the turn's root,
6
+ -- each agent run, model step and provider call, tool call, script run and
7
+ -- call, workflow walk and node run (lib/observability/lineage.ts
8
+ -- ledgerSpanKind). Each row now says where it sits:
9
+ -- parent_span_id the nearest ancestor span the ledger keeps a row for;
10
+ -- NULL for a root, and for every row written before 0016
11
+ -- kind turn | agent | model | tool | script | node; NULL for a
12
+ -- row written before 0016
13
+ -- error the span failed (an error status or code, a tool's error
14
+ -- response); NULL for a row written before 0016
15
+ -- The full span (attributes, events, status) stays in `span`.
16
+ --
17
+ -- Cache tokens, the parts of input_tokens a provider read from or wrote to
18
+ -- its prompt cache (lib/models/contract.ts Usage), priced at their own rates
19
+ -- by 0017_cache_pricing:
20
+ -- adk_telemetry.cache_read_tokens / cache_write_tokens one call's (an
21
+ -- llm.request row), or the turn's sum (a root row)
22
+ -- adk_turns.cache_read_tokens / cache_write_tokens summed over every
23
+ -- call of the turn, as a provider bills them
24
+ -- NULL where nothing was reported, and on every row written before 0016.
25
+ --
26
+ -- The ledger tables are optional: db/telemetry.sql creates them, after the
27
+ -- migrations, and adds the same columns for a fresh install. This migration
28
+ -- upgrades a ledger that already exists in place, and does nothing where
29
+ -- there is none. Numbers and span ids only: nothing personal, nothing secret.
30
+ -- Idempotent; safe to re-run.
31
+ -- ============================================================================
32
+
33
+ DO $span_tree$
34
+ BEGIN
35
+ IF to_regclass('public.adk_telemetry') IS NOT NULL THEN
36
+ EXECUTE 'ALTER TABLE adk_telemetry ADD COLUMN IF NOT EXISTS parent_span_id TEXT';
37
+ EXECUTE 'ALTER TABLE adk_telemetry ADD COLUMN IF NOT EXISTS kind TEXT';
38
+ EXECUTE 'ALTER TABLE adk_telemetry ADD COLUMN IF NOT EXISTS error BOOLEAN';
39
+ EXECUTE 'ALTER TABLE adk_telemetry ADD COLUMN IF NOT EXISTS cache_read_tokens INTEGER';
40
+ EXECUTE 'ALTER TABLE adk_telemetry ADD COLUMN IF NOT EXISTS cache_write_tokens INTEGER';
41
+ EXECUTE 'CREATE INDEX IF NOT EXISTS idx_adk_telemetry_parent ON adk_telemetry (trace_id, parent_span_id)';
42
+ END IF;
43
+ IF to_regclass('public.adk_turns') IS NOT NULL THEN
44
+ EXECUTE 'ALTER TABLE adk_turns ADD COLUMN IF NOT EXISTS cache_read_tokens INTEGER';
45
+ EXECUTE 'ALTER TABLE adk_turns ADD COLUMN IF NOT EXISTS cache_write_tokens INTEGER';
46
+ END IF;
47
+ END
48
+ $span_tree$;
49
+
50
+ INSERT INTO melchizedek_schema_version (version, name)
51
+ VALUES (16, '0016_span_tree')
52
+ ON CONFLICT (version) DO NOTHING;