melchizedek-agents 1.0.3 → 1.1.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 (122) hide show
  1. package/.env.example +19 -2
  2. package/CHANGELOG.md +292 -0
  3. package/DOCUMENTATION.md +99 -39
  4. package/README.md +1 -0
  5. package/config/agents/README.md +2 -2
  6. package/config/agents/examples/ares.yaml +6 -10
  7. package/config/agents/examples/assistant.yaml +6 -6
  8. package/config/agents/examples/augustin.yaml +9 -15
  9. package/config/agents/examples/cartographers.yaml +6 -6
  10. package/config/agents/examples/council.yaml +6 -6
  11. package/config/agents/examples/critic.yaml +16 -16
  12. package/config/agents/examples/harness.yaml +4 -4
  13. package/config/agents/examples/image_production.yaml +33 -32
  14. package/config/agents/examples/librarian.yaml +4 -4
  15. package/config/agents/examples/patient_advocate.yaml +6 -10
  16. package/config/agents/examples/pipeline.yaml +20 -19
  17. package/config/agents/examples/research.yaml +36 -43
  18. package/config/agents/examples/scribe.yaml +31 -32
  19. package/config/agents/examples/scriptorium.yaml +6 -6
  20. package/config/agents/examples/structured_critic.yaml +82 -0
  21. package/config/agents/examples/style_council.yaml +9 -11
  22. package/config/agents/examples/syndicate.yaml +3 -5
  23. package/config/agents/examples/tutor.yaml +2 -2
  24. package/config/agents/examples/weather.yaml +2 -2
  25. package/config/agents/syndicate.schema.json +797 -9
  26. package/config/agents/syndicateSchema.yaml +109 -72
  27. package/config/agents/templates/README.md +1 -1
  28. package/config/agents/templates/account_memory.yaml +4 -4
  29. package/config/agents/templates/case_desk.yaml +6 -6
  30. package/config/agents/templates/conversational.yaml +2 -2
  31. package/config/agents/templates/draft_review.yaml +6 -6
  32. package/config/agents/templates/intake_extractor.yaml +61 -60
  33. package/config/agents/templates/research_brief.yaml +6 -6
  34. package/config/agents/templates/research_desk.yaml +6 -6
  35. package/config/agents/templates/review_panel.yaml +8 -8
  36. package/config/agents/templates/support_triage.yaml +30 -29
  37. package/config/agents/templates/systems_operator.yaml +21 -5
  38. package/db/migrations/0014_durable_runs.sql +31 -0
  39. package/dist/lib/a2a/app.d.ts +15 -0
  40. package/dist/lib/a2a/app.js +53 -5
  41. package/dist/lib/a2a/executor.d.ts +7 -1
  42. package/dist/lib/a2a/executor.js +34 -4
  43. package/dist/lib/a2a/oauthSetup.d.ts +90 -0
  44. package/dist/lib/a2a/oauthSetup.js +205 -0
  45. package/dist/lib/agentDialect.d.ts +66 -0
  46. package/dist/lib/agentDialect.js +134 -0
  47. package/dist/lib/compile.d.ts +10 -5
  48. package/dist/lib/compile.js +69 -37
  49. package/dist/lib/compileNative.js +2 -0
  50. package/dist/lib/dispatch.d.ts +10 -5
  51. package/dist/lib/dispatch.js +10 -5
  52. package/dist/lib/doctor.d.ts +34 -1
  53. package/dist/lib/doctor.js +47 -3
  54. package/dist/lib/loadSyndicate.d.ts +45 -2
  55. package/dist/lib/loadSyndicate.js +22 -2
  56. package/dist/lib/memory/namespace.d.ts +6 -5
  57. package/dist/lib/memory/namespace.js +6 -5
  58. package/dist/lib/memory/providers.js +3 -4
  59. package/dist/lib/models/capabilities.d.ts +16 -1
  60. package/dist/lib/models/capabilities.js +55 -1
  61. package/dist/lib/observability/tracer.d.ts +8 -1
  62. package/dist/lib/observability/tracer.js +33 -5
  63. package/dist/lib/runtime/approvals.d.ts +6 -0
  64. package/dist/lib/runtime/native/agentLoop.d.ts +18 -1
  65. package/dist/lib/runtime/native/agentLoop.js +105 -11
  66. package/dist/lib/runtime/native/checkpoint.d.ts +164 -0
  67. package/dist/lib/runtime/native/checkpoint.js +365 -0
  68. package/dist/lib/runtime/native/delegate.d.ts +141 -18
  69. package/dist/lib/runtime/native/delegate.js +235 -39
  70. package/dist/lib/runtime/native/interrupts.d.ts +101 -2
  71. package/dist/lib/runtime/native/interrupts.js +201 -1
  72. package/dist/lib/runtime/native/request.d.ts +13 -3
  73. package/dist/lib/runtime/native/request.js +28 -4
  74. package/dist/lib/runtime/native/telemetry.js +10 -15
  75. package/dist/lib/runtime/questions.d.ts +7 -4
  76. package/dist/lib/runtime/sessions.d.ts +1 -1
  77. package/dist/lib/runtime/syndicateTurn.d.ts +7 -1
  78. package/dist/lib/runtime/syndicateTurn.js +74 -22
  79. package/dist/lib/runtime/turnControl.d.ts +15 -0
  80. package/dist/lib/runtime/turnControl.js +10 -0
  81. package/dist/lib/storage/postgres/index.d.ts +6 -1
  82. package/dist/lib/storage/postgres/index.js +7 -2
  83. package/dist/lib/storage/postgres/taskQueue.d.ts +14 -2
  84. package/dist/lib/storage/postgres/taskQueue.js +20 -3
  85. package/dist/lib/storage/postgres/taskStore.d.ts +26 -2
  86. package/dist/lib/storage/postgres/taskStore.js +74 -8
  87. package/dist/lib/syndicateSchema.d.ts +278 -0
  88. package/dist/lib/syndicateSchema.js +182 -15
  89. package/dist/lib/tools/auth.d.ts +7 -5
  90. package/dist/lib/tools/auth.js +12 -4
  91. package/dist/lib/tools/credentialEnv.d.ts +28 -0
  92. package/dist/lib/tools/credentialEnv.js +52 -0
  93. package/dist/lib/tools/credentialStore.js +9 -2
  94. package/dist/lib/tools/mcpToolFactory.d.ts +25 -2
  95. package/dist/lib/tools/mcpToolFactory.js +179 -21
  96. package/dist/lib/tools/oauthHosts.d.ts +71 -0
  97. package/dist/lib/tools/oauthHosts.js +154 -0
  98. package/dist/lib/tools/oauthTools.d.ts +233 -0
  99. package/dist/lib/tools/oauthTools.js +421 -0
  100. package/dist/lib/tools/openapiTools.d.ts +4 -6
  101. package/dist/lib/tools/openapiTools.js +40 -36
  102. package/dist/lib/tools/taskTools.d.ts +30 -5
  103. package/dist/lib/tools/taskTools.js +74 -10
  104. package/dist/lib/workflow.d.ts +8 -4
  105. package/dist/lib/workflow.js +8 -4
  106. package/dist/lib/workflowConfig.d.ts +6 -0
  107. package/dist/scripts/a2a_server.d.ts +36 -0
  108. package/dist/scripts/a2a_server.js +122 -1
  109. package/dist/scripts/assistant_worker.d.ts +10 -0
  110. package/dist/scripts/assistant_worker.js +69 -19
  111. package/dist/scripts/doctor.d.ts +4 -1
  112. package/dist/scripts/doctor.js +5 -2
  113. package/dist/scripts/yaml_codemod.d.ts +52 -0
  114. package/dist/scripts/yaml_codemod.js +570 -0
  115. package/package.json +5 -2
  116. package/skills/briefs/melchizedek-author.md +7 -5
  117. package/skills/briefs/melchizedek-models.md +4 -3
  118. package/skills/briefs/melchizedek.md +1 -0
  119. package/skills/melchizedek/SKILL.md +1 -0
  120. package/skills/melchizedek-author/SKILL.md +7 -4
  121. package/skills/melchizedek-author/assets/minimal.yaml +13 -13
  122. package/skills/melchizedek-models/SKILL.md +6 -3
package/.env.example CHANGED
@@ -33,8 +33,11 @@ ALLOW_PRIVATE_MCP=false
33
33
  # ── Optional: OpenAPI tools (an agent's `openapi:` list) ──────────────────────
34
34
  # ALLOW_PRIVATE_OPENAPI: set to "true" to let an OpenAPI tool call a localhost /
35
35
  # private / link-local server. Off by default as an SSRF guard. Credentials an
36
- # `openapi:` entry names (auth.bearer_env, auth.api_key.env) are read from this
37
- # file or the environment, never written in YAML.
36
+ # `openapi:` entry names (auth.bearer_env, auth.api_key.env, and an OAuth
37
+ # grant's auth.oauth2.client_id_env / client_secret_env, which an MCP server's
38
+ # mcp_auth.oauth2 uses too) are read from this file or the environment, never
39
+ # written in YAML. Give each API its own variable: the framework's own
40
+ # settings below are refused (ADR 0112).
38
41
  # ALLOW_PRIVATE_OPENAPI=false
39
42
 
40
43
  # ── Optional: Anthropic / Claude ──────────────────────────────────────────────
@@ -245,6 +248,20 @@ PUBLIC_URL=
245
248
  # out of the database and its backups. Unset = no credential store. A row
246
249
  # sealed with another key stays unreadable (the user reconnects).
247
250
  # MELCHIZEDEK_CREDENTIAL_KEY=
251
+ # The OAuth consent callback (ADR 0085), exactly as registered at every
252
+ # provider a YAML grant names: https, no query. Mounts the callback and lets a
253
+ # run ask a user to connect a provider. Needs MELCHIZEDEK_CREDENTIAL_KEY.
254
+ # OAUTH_REDIRECT_URI=https://agents.example.com/oauth/callback
255
+ # Who may complete a callback (ADR 0114): required (default) — the browser
256
+ # carries the user's identity, which only A2A_AUTH=header behind a gateway
257
+ # gives it; state — the single-use state alone binds the flow to its user.
258
+ # OAUTH_CALLBACK_IDENTITY=required
259
+ # The hosts each provider's tokens may be sent to (ADR 0114), the operator's
260
+ # binding, never the YAML's: provider=host,host;provider=host. A host is a
261
+ # hostname, *.domain (subdomains only), IPv4 or [IPv6]; no scheme or port.
262
+ # Unset: authorization_code grants are refused, client_credentials allowed.
263
+ # Set: every grant's provider is listed and every host it calls is its own.
264
+ # MELCHIZEDEK_OAUTH_HOSTS=tracker=api.tracker.example.com,auth.tracker.example.com
248
265
 
249
266
  # ── Optional: Knowledge Bundle (wiki) ─────────────────────────────────────────
250
267
  # WIKI_ROOT: filesystem path of the OKF knowledge bundle the wiki tools serve.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,298 @@ the starter pack and the templates), not the repo's full history.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 1.1.0 — 2026-10-09
10
+
11
+ Release 1.1.0 closes workstream 6 of the ADK-independence plan (ADRs
12
+ 0109–0116): the first features built on the engine's own loop. An
13
+ orchestrator can delegate and answer in its own JSON schema; a pause inside
14
+ a delegated subagent or a nested syndicate reaches the turn; tools can hold
15
+ OAuth grants declared in YAML, sent only to hosts the operator binds;
16
+ background jobs checkpoint and resume, and cancel works across replicas; a
17
+ step's subagent calls run at once; and the YAML gains provider-neutral v2
18
+ keys, with a codemod. Two fixes: `trace: false` now records nothing, and
19
+ MCP settings on an orchestrator are honoured.
20
+
21
+ ### Upgrade
22
+
23
+ Read this list before deploying 1.1.0.
24
+
25
+ 1. **Apply migration `0014_durable_runs` before you deploy**
26
+ (`db/migrations/0014_durable_runs.sql`, through `npx melchizedek-db
27
+ apply`) on any server or worker that uses `DATABASE_URL`. Every save
28
+ of the Postgres task stores now writes the new columns
29
+ (`adk_a2a_tasks.cancel_requested_at`, `melchizedek_tasks.checkpoint`),
30
+ so without the migration every Postgres task-store save fails. The A2A
31
+ server refuses to start against a database behind 0014 unless
32
+ `ALLOW_SCHEMA_MISMATCH=true` is set, and with that override its saves
33
+ fail. The migration adds nullable columns only and is safe to re-run.
34
+ 2. **New OAuth environment variables** for `melchizedek-serve`, needed
35
+ only when a served syndicate declares an `oauth2` grant:
36
+ `MELCHIZEDEK_OAUTH_HOSTS` (which hosts each provider's tokens may go to),
37
+ `OAUTH_REDIRECT_URI` (the consent callback: https, or http on a loopback
38
+ host) and `OAUTH_CALLBACK_IDENTITY` (`required`, the default, or
39
+ `state`). `MELCHIZEDEK_CREDENTIAL_KEY` keys the credential store.
40
+ 3. **`authorization_code` grants are refused until an allowlist is set.**
41
+ With no `MELCHIZEDEK_OAUTH_HOSTS` (or `createA2AApp({ oauthHosts })`), a
42
+ syndicate holding an `authorization_code` grant is refused when served;
43
+ `client_credentials` grants are allowed.
44
+ 4. **`max_concurrency` now runs a step's subagent calls at once**, at most
45
+ four by default. Set `max_concurrency: 1` at the root of a delegate
46
+ syndicate to restore the old one-after-another order.
47
+ 5. **YAML v2 keys.** `sampling:`, `output:` and `model_overrides:` replace
48
+ `generateContentConfig` and `outputSchema`. The old spellings are
49
+ deprecated, not removed: a v1 file loads and behaves as before, with one
50
+ deprecation line per file per process. `npx melchizedek-codemod
51
+ [--check] <file|dir>…` rewrites v1 files to v2.
52
+ 6. **`trace: false` now records nothing.** A turn run with
53
+ `runSyndicateTurn({ trace: false })` writes no telemetry row and exports
54
+ no span. If you relied on its agent, model or tool spans, trace the turn.
55
+ 7. **Child sessions are keyed by agent path.** A delegated subagent's
56
+ session is filed under `<app>/<caller>/<subagent>`; conversations stored
57
+ under the old key still resume. Code that reads a subagent's session by
58
+ its name alone reads the path instead.
59
+ 8. **The capability matrix has a new member, `structured_output_with_tools`.**
60
+ Code that builds a `Record<Capability, …>` must add it.
61
+ 9. **An A2A task cancelled while waiting for the turn lock now ends
62
+ `canceled`**, not `rejected`.
63
+
64
+ ### Breaking — read before upgrading
65
+
66
+ - **Breaking: migration 0014 is required** on a Postgres deployment
67
+ (Upgrade item 1).
68
+ - **Breaking: `Capability`, `CAPABILITIES` and `CAPABILITY_MATRIX`
69
+ (`melchizedek-agents/models/capabilities`) gain
70
+ `structured_output_with_tools`**, so an exhaustive
71
+ `Record<Capability, …>` no longer type-checks without it.
72
+ - **Breaking: a step's calls to subagents run concurrently by default**
73
+ (ADR 0116). See Changed; `max_concurrency: 1` keeps the old order.
74
+ - **Breaking: a delegated subagent's own session is filed under its agent
75
+ path** (ADR 0111). See Changed; stored conversations still resume.
76
+ - **Breaking: `runSyndicateTurn({ trace: false })` records nothing**: no
77
+ span of any kind, no ledger row, no export. See Fixed.
78
+ - **Breaking: an A2A task cancelled while it waits for its conversation's
79
+ turn lock ends `canceled`**, not `rejected` (ADR 0113).
80
+
81
+ ### Added
82
+
83
+ - **An orchestrator that delegates can answer in its own `outputSchema`**
84
+ (ADR 0109). An agent holding an `outputSchema` beside subagents or tools
85
+ calls them first, then ends its turn on one JSON object matching the
86
+ schema; no relay leaf or `dispatch:` block is needed (plan-dispatch stays
87
+ available). On Claude from Opus 4.8, Sonnet 5 and Haiku 5.5, on every
88
+ OpenAI id, and on Gemini 2 and later through Vertex AI, the schema now
89
+ travels in the provider's own structured-output field in the same request
90
+ as the tools (`output_config.format`, `text.format`, `responseJsonSchema`)
91
+ instead of as a `set_model_response` tool, on the agent's `fallback_model`
92
+ too; every other path keeps `set_model_response`.
93
+ - **The capability matrix gains `structured_output_with_tools`**
94
+ (`melchizedek-agents/models/capabilities`): supported on Anthropic and
95
+ OpenAI (and Gemini on Vertex AI), degraded (`set_model_response`) on the
96
+ Gemini API, xAI, Moonshot, Ollama and the gateway. `npm run doctor` names
97
+ it as a gap for an agent with a schema beside tools on a degraded path.
98
+ `outputSchemaBesideTools(model)` says whether a model's path takes both.
99
+ - **New starter-pack example `config/agents/examples/structured_critic.yaml`**
100
+ (`npm run syndicate:structured-critic`): the critic as one agent, an
101
+ orchestrator on `claude-opus-5-5` that asks a Drafter on
102
+ `claude-haiku-5-5` for a draft and grades it in its own `output.schema`.
103
+ It needs only `ANTHROPIC_API_KEY`.
104
+ - **A pause inside a delegated subagent reaches the turn** (WS6-2a,
105
+ [ADR 0110](./wiki/decisions/0110-pauses-inside-delegated-subagents-reach-the-turn.md)).
106
+ `require_approval` and `ask_user` are allowed on a delegated subagent, and
107
+ gates inside a nested delegate syndicate (`yaml_reference`) no longer fail
108
+ to load. The turn ends `input-required` with the request or question; the
109
+ decision or answer goes back down to the subagent, which finishes before
110
+ its caller continues. Before, a gate there was a load error and a pause
111
+ answered the call with an empty text.
112
+ - `PendingApproval.path` and `PendingInput.path` (optional): the agents from
113
+ the turn's own agent down to the one that asked, set only for a pause
114
+ inside a delegated subagent. The A2A `approval_request` and
115
+ `input_request` data parts carry `path` beside their fields when it is set.
116
+ - **Pauses inside nested syndicates reach the turn** (WS6-2b,
117
+ [ADR 0111](./wiki/decisions/0111-pauses-inside-nested-syndicates.md)). An
118
+ approval request or `ask_user` question raised inside a `yaml_reference`
119
+ subagent ends the turn `input-required` with the agent path, over
120
+ `runSyndicateTurn` and A2A, and the answer resumes it. A nested dispatch
121
+ syndicate may gate its classifier (a gate on one of its routes, which never
122
+ run nested, is refused by name). A nested workflow delegated to as a
123
+ subagent may hold `ask_user` nodes and gated agent nodes; the path ends at
124
+ the node. As a dispatch route or a workflow node, a nested workflow's
125
+ pauses stay refused, with a message that says so.
126
+ - **OAuth grants in YAML (`auth: { oauth2 }`, `mcp_auth: { oauth2 }`;
127
+ ADR 0112).** An `openapi:` entry's `auth` takes a third form, `oauth2`,
128
+ and an agent with `mcp_server_url` may declare `mcp_auth: { oauth2 }`.
129
+ The block names a `provider`, a `grant` (`authorization_code` or
130
+ `client_credentials`), `authorization_url`, `token_url`, `client_id` or
131
+ `client_id_env`, `client_secret_env` and `scopes`; secrets are
132
+ environment variable names, never values, under the same rule as
133
+ `bearer_env`. `authorization_code` sends the run's user's own token
134
+ (`ctx.accessToken`, so the consent pause asks a user who has not granted
135
+ it); `client_credentials` sends the server's own token from the token
136
+ endpoint, held in memory until shortly before it expires. A token goes
137
+ only over https (or http to a loopback host). An authorization-code MCP
138
+ server needs `mcp_tools` and runs each user on their own connection.
139
+ `npm run doctor` lists every tool that needs a grant. New module
140
+ `melchizedek-agents/tools/oauthTools` (`oauthClientsFor`, which builds
141
+ `oauthConsent({ providers })` from the YAML, `oauthRefreshProviders`,
142
+ which gives `credentialStore({ providers })` the matching refresh hooks,
143
+ `clientCredentialsGrant`, `oauthTokenSource`, `tokenTransportProblem`)
144
+ and `melchizedek-agents/tools/credentialEnv` (`credentialEnvProblem`,
145
+ still exported from `tools/openapiTools`), both under the existing
146
+ `./tools/*` export. `OAuthTokenSource` takes the call's destination
147
+ (`(ctx, destination)`), and `oauthTokenSource` accepts several servers.
148
+ `ToolCredentialError` gains the codes `unavailable` and `grant_failed`,
149
+ and `createMcpTools` / `loadMcpTools` an optional second argument. The
150
+ systems_operator template carries a commented `mcp_auth` block.
151
+ - **The OAuth host allowlist (`MELCHIZEDEK_OAUTH_HOSTS`,
152
+ `createA2AApp({ oauthHosts })`; ADR 0114).** The operator binds each
153
+ provider to the hosts its tokens and client secret may be sent to
154
+ (`provider=host,host;…`; `*.domain` for subdomains). With none, an
155
+ `authorization_code` grant is refused and `client_credentials` allowed;
156
+ with one, every grant's provider must be listed and its server, token
157
+ endpoint and authorization endpoint must be that provider's hosts. A
158
+ syndicate that breaks it is refused when served, when compiled and at
159
+ each call (`ToolCredentialError` code `host_refused`). New module
160
+ `melchizedek-agents/tools/oauthHosts` (`parseOAuthHosts`,
161
+ `checkOAuthHosts`, `setOAuthHosts`, `oauthHosts`, `hostAllowed`,
162
+ `oauthHostProblem`) under the existing `./tools/*` export;
163
+ `tools/oauthTools` gains `oauthGrantHostProblems`, `oauthCallProblem`,
164
+ `syndicateOAuthGrants` and `syndicateOAuthHostProblems`, and
165
+ `oauthClientsFor` an `allowlist` option.
166
+ - **`melchizedek-serve` wires tool credentials and the consent step**
167
+ from `MELCHIZEDEK_CREDENTIAL_KEY` (rows in Postgres with `DATABASE_URL`,
168
+ else process memory), `OAUTH_REDIRECT_URI`, `OAUTH_CALLBACK_IDENTITY`
169
+ (`required`, the default, or `state`) and `MELCHIZEDEK_OAUTH_HOSTS`, with
170
+ the consent clients and refresh hooks built from the served syndicate
171
+ files. `melchizedek-agents/server` exports `serverOAuth` and
172
+ `servedFileSyndicates`. `npm run doctor` prints an `oauth` line (which
173
+ variables are set, and each problem by name) and each grant's host
174
+ refusals; `--check` fails on an OAuth problem.
175
+ - **Durable long-running runs** (WS6-5, ADR 0113):
176
+ - **The worker resumes a job instead of starting over.**
177
+ `melchizedek-worker` checkpoints each job's sessions beside the job at
178
+ every step boundary (Postgres: the `checkpoint` column; the JSON store:
179
+ `<store>.checkpoints.json`). A job claimed again after its worker
180
+ stopped resumes from its last checkpoint. A dispatch syndicate
181
+ checkpointed inside an agent route starts over, and its classifier runs
182
+ again.
183
+ - **SIGTERM re-queues the job in hand** with its checkpoint, so the next
184
+ worker finishes it. Before, the job was recorded as failed.
185
+ - **A running background job can be cancelled.** `task_update` with
186
+ `status: cancelled` now accepts a running job. The worker stops at its
187
+ next lease renewal or checkpoint save and writes no result.
188
+ - **A2A cancel works across replicas**
189
+ (`melchizedek-agents/storage/postgres`): a `tasks/cancel` that reaches a
190
+ replica not running the task answers `canceled` and records the
191
+ request; the replica holding the task's lease aborts the run on its
192
+ lease heartbeat; the stored task stays `canceled`; the reaper keeps a
193
+ cancel that was already reported. New:
194
+ `PostgresTaskStore.requestCancel(taskId, context)`, the optional
195
+ `leases.cancelRequested` on `postgresStorage()`, and the optional
196
+ `storage.leases.cancelRequested` option of `createA2AApp`.
197
+ - **`TaskBackend` (`melchizedek-agents/tools/taskTools`), additive only:**
198
+ new optional members `saveCheckpoint` and `loadCheckpoint`; `renew` may
199
+ resolve `false` when the claim is gone; new export `taskCheckpointPath`.
200
+ A backend you wrote yourself still type-checks. It just does not
201
+ checkpoint.
202
+ - **YAML schema v2: provider-neutral keys** (WS6-4, ADR 0115).
203
+ `sampling: { temperature, top_p, max_output_tokens, stop }`,
204
+ `output: { schema, mime }` (`application/json` or `text/plain`) and
205
+ `reasoning:` (ADR 0047) replace `generateContentConfig` and
206
+ `outputSchema`. `model_overrides.<provider>` (`gemini`, `anthropic`,
207
+ `openai`, `xai`, `moonshot`, `ollama`) gives one provider its own
208
+ `instruction` or an `instruction_append`, applied for the provider the
209
+ agent's model routes to.
210
+ - **New bin `melchizedek-codemod [--check] <file|dir>…`** rewrites v1 YAML
211
+ into v2, keeping comments and layout; idempotent; `--check` exits
212
+ non-zero when a file would change. A registry row is migrated by running
213
+ it on the file it was published from and republishing.
214
+ - **`LoadSyndicateOptions.onWarning`** receives the v1 deprecation line
215
+ (default `console.warn`). The agent YAML types (`AgentYamlConfig`,
216
+ `SubagentYamlConfig`) gain `sampling`, `output` and `model_overrides`.
217
+ - **`max_concurrency`**, a new optional root key of a delegate syndicate:
218
+ a positive integer up to 32, default 4 (ADR 0116). See Changed.
219
+ - **`turnUntraced()`** (`melchizedek-agents/runtime/turnControl`) says
220
+ whether the running code belongs to a turn that opted out of tracing;
221
+ `createTurnControl` takes `untraced`, and `TurnControl` carries it.
222
+ **`startEngineSpan(tracerName, name, options?)`**
223
+ (`melchizedek-agents/observability/tracer`) starts a span the way the
224
+ engine does: a non-recording one, without starting the tracer, inside an
225
+ untraced turn.
226
+ - Engine internals with no exports map entry: `delegatedPauses`,
227
+ `openCalls`, `resumedDelegations` in `lib/runtime/native/interrupts.ts`;
228
+ `resumeSubagent`, `SubagentPause`, `resumeWorkflowSubagent`,
229
+ `childAppName`, `legacyChild` in `lib/runtime/native/delegate.ts`;
230
+ `runDurableTurn`, `checkpointingSessions` in
231
+ `lib/runtime/native/checkpoint.ts` (the worker's runner).
232
+
233
+ ### Changed
234
+
235
+ - **A step's calls to subagents run at once** (ADR 0116). When an
236
+ orchestrator's model calls several subagents in one step (the council's
237
+ Advocate and Skeptic, say), they now run concurrently instead of one after
238
+ another, at most `max_concurrency` at a time (default 4).
239
+ `max_concurrency: 1` keeps the old order. Two calls to the same subagent
240
+ in one step still run one after the other on its one session. The stored
241
+ responses stay in call order whatever order the subagents finish in, so
242
+ stored history does not change; a pause, a cancel, `max_steps` and durable
243
+ checkpoints keep their rules. The key is refused beside `workflow:` (use
244
+ `workflow.max_concurrency`) and `dispatch:`. Other tool calls are
245
+ unchanged.
246
+ - **A delegated subagent's own session is filed under its agent path**
247
+ (WS6-2b, ADR 0111): `<app>/<caller>/<subagent>`, and below a nested
248
+ syndicate `<app>/<caller>/<subagent>/<inner>`, instead of the subagent's
249
+ name alone, so two syndicates with a same-named subagent on one
250
+ conversation no longer share it. A conversation stored before keeps its
251
+ subagents' sessions under the old key: they are still read, continued and
252
+ resumed. Code that read a subagent's session directly by its name reads
253
+ the path instead. The stored events do not change.
254
+ - **`generateContentConfig` and `outputSchema` are deprecated, not
255
+ removed** (ADR 0115). A v1 file loads and behaves exactly as before; each
256
+ load that finds them prints one line, once per file or registry id per
257
+ process, naming the key paths (never their values). A v2 key beside its
258
+ v1 spelling on one agent (`sampling.temperature` with
259
+ `generateContentConfig.temperature`, `output.schema` with
260
+ `outputSchema`, …) is a load error. `toolConfig` and the effort words
261
+ `xhigh` and `max` have no v2 key yet and stay readable there.
262
+ `syndicate.schema.json` marks both keys `deprecated`. The loader (and
263
+ `validateSyndicateConfig`) returns the engine form: `sampling` and
264
+ `output` arrive folded into `generateContentConfig` and `outputSchema`.
265
+ - **Every shipped example, template, the annotated `syndicateSchema.yaml`
266
+ and the author skill's `minimal.yaml` are in the v2 spelling.** Copies of
267
+ them keep working either way. The `ares` example's search agent now says
268
+ `reasoning: none` (`thinkingLevel: MINIMAL` on Gemini 3) where it said
269
+ `thinkingBudget: 0`.
270
+ - **An A2A task cancelled during the turn-lock wait** now ends `canceled`,
271
+ not `rejected` (ADR 0113).
272
+ - **An OpenAPI `auth` that sets two forms** now reads "exactly one of
273
+ bearer_env, api_key or oauth2" (was "exactly one of bearer_env or
274
+ api_key").
275
+ - **The authoring skill (`skills/melchizedek-author`) allows an
276
+ `output: { schema }` on an orchestrator that delegates** when the answer
277
+ is the orchestrator's own structured judgment (ADR 0109), and points at
278
+ `structured_critic.yaml`; a schema on a leaf stays the choice when the
279
+ user should receive a specialist's answer.
280
+
281
+ ### Fixed
282
+
283
+ - **`runSyndicateTurn({ trace: false })` records nothing.** It used to drop
284
+ only the turn's root span: the agent, model-step, `llm.request` and tool
285
+ spans still started the global tracer, so with `TELEMETRY_SUPABASE=true`
286
+ an untraced turn still wrote `adk_telemetry` rows (and could write
287
+ `adk_payloads` rows), and with `OTEL_EXPORTER_OTLP_ENDPOINT` set it still
288
+ exported. Now a turn with `trace: false` opens no span of any kind, sends
289
+ nothing to the ledger, the console exporter, `onSpanEnd` listeners or an
290
+ OTLP endpoint, and does not start the tracer. Its step budget and its
291
+ `usage` are unchanged. The default (traced) is unchanged, and the A2A
292
+ server and the chat bin trace as before.
293
+ - **`mcp_server_url` and `mcp_tools` on the orchestrator are honoured.**
294
+ The schema accepted them there, but only subagents read them, so a
295
+ one-agent syndicate with an MCP server on its orchestrator got no MCP
296
+ tools and no warning. The orchestrator now resolves them as a subagent
297
+ does (narrowed by `mcp_tools`, gateable by its `require_approval`). On a
298
+ plan-dispatch syndicate the orchestrator is the classifier, and an MCP
299
+ server declared there is now dialled at compile time.
300
+
9
301
  ## 1.0.3 — 2026-10-09
10
302
 
11
303
  Dependency updates and one packaging fix. No export, bin, YAML key or
package/DOCUMENTATION.md CHANGED
@@ -106,8 +106,8 @@ orchestrator:
106
106
  tools:
107
107
  - "google_search" # names resolved via lib/toolRegistry.ts
108
108
  reasoning: medium # none | low | medium | high, or { budget_tokens: 4096 }
109
- generateContentConfig:
110
- maxOutputTokens: 4096
109
+ sampling:
110
+ max_output_tokens: 4096
111
111
 
112
112
  subagents:
113
113
  - name: "Researcher"
@@ -136,19 +136,22 @@ Field reference:
136
136
  | `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…"). |
137
137
  | `tools` | agent | Names resolved by the tool registry (§3). Long-term memory agents add `preload_memory` / `load_memory`. |
138
138
  | `reasoning` | agent | How hard the agent reasons, on any provider: `none`, `low`, `medium`, `high`, or `{ budget_tokens: <int> }`. The compiler sends each provider the field it reads: a thinking level on Gemini 3, a thinking budget on Claude 4.6 and earlier (2,048 / 8,192 / 16,384 tokens for low / medium / high), adaptive thinking with that effort on later Claude models ([ADR 0049](./wiki/decisions/0049-claude-requests-by-model-generation.md)), an effort word on GPT, Grok, Kimi, Ollama and the gateway ([ADR 0047](./wiki/decisions/0047-provider-neutral-reasoning-key.md)). Unset, each adapter keeps its own default. |
139
- | `generateContentConfig` | agent | Temperature, output caps. Its `thinkingConfig` and `reasoningEffort` are the older, provider-specific spelling of `reasoning`; setting either next to `reasoning` is a load error. |
140
- | `outputSchema` | agent | Structured-JSON contract. **Constraint:** an agent holding `outputSchema` cannot also hold transfer powers — structured output does not combine with delegation on one agent. Keep schema-holders as leaf agents (see `critic.yaml`'s header comment for the war story). |
139
+ | `sampling` | agent | `{ temperature, top_p, max_output_tokens, stop }`, the fields the engine sends to every provider; each adapter sends the ones its API takes ([ADR 0115](./wiki/decisions/0115-yaml-schema-v2-provider-neutral-keys.md)). |
140
+ | `output` | agent | `{ schema, mime }`. `mime` is `application/json` (JSON mode, with or without a schema) or `text/plain` (the default). `schema` is the structured-JSON contract: the agent ends its turn on one JSON object matching it. An orchestrator may hold one beside its subagents: it delegates first, then answers in the schema itself. The schema travels in the provider's own structured-output field beside the tools on Claude from Opus 4.8, Sonnet 5 and Haiku 5.5, on OpenAI, and on Gemini 2+ through Vertex AI, and as a `set_model_response` tool elsewhere (the capability matrix's `structured_output_with_tools`, [ADR 0109](./wiki/decisions/0109-structured-output-beside-tools.md)). Worked example: `examples/structured_critic.yaml`. A leaf that holds the schema (`critic.yaml`) and plan-dispatch remain the choices when the answer should be a specialist's. |
141
+ | `model_overrides` | agent | A prompt nuance for one provider: keyed by `gemini`, `anthropic`, `openai`, `xai`, `moonshot` or `ollama`, each entry holds `instruction` (replaces the agent's instruction on that provider) or `instruction_append` (added after a blank line). The entry for the provider of the agent's `model` applies, before the skills index is appended; a `fallback_model` gets the primary's instruction. |
142
+ | `generateContentConfig` / `outputSchema` | agent | The deprecated v1 spelling of `sampling`, `output.mime`, `reasoning` (as `thinkingConfig` / `reasoningEffort`) and `output.schema`. Both still load and behave the same; a load that finds either prints one deprecation line per file per process (`LoadSyndicateOptions.onWarning`, default `console.warn`), and a v2 key beside its v1 spelling on one agent is a load error. Only `toolConfig` and the effort words `xhigh` / `max` have no v2 form yet; `topK`, `seed`, the penalties, `candidateCount`, `safetySettings` and `includeThoughts` reach no provider. `npx melchizedek-codemod [--check] <file|dir>` rewrites a v1 file to v2, comments kept ([ADR 0115](./wiki/decisions/0115-yaml-schema-v2-provider-neutral-keys.md)). |
141
143
  | `yaml_reference` | subagent | Mount another syndicate file as a nested subagent. |
142
144
  | `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. |
143
145
  | `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`. |
144
146
  | `includeContents` / `outputKey` / `globalInstruction` / `disallowTransferToParent` / `disallowTransferToPeers` | agent | Passed through to the agent (spelled after ADK's `LlmAgent` fields). `includeContents: none` makes an agent see only the current message. |
145
147
  | `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)). |
146
148
  | `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)). |
149
+ | `mcp_auth` | subagent | `{ oauth2 }`: the OAuth grant the MCP server takes, the same block as an OpenAPI `auth.oauth2`. `client_credentials` sends the server's own token; `authorization_code` sends each user's own token on their own connection (the consent pause asks for it) and needs `mcp_tools` ([ADR 0112](./wiki/decisions/0112-oauth-grants-declared-beside-the-tool.md)). |
147
150
  | `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. |
148
151
  | `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`. |
149
152
  | `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)). |
150
153
  | `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. |
151
- | `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. |
154
+ | `mode` | workflow node | `"task"`: the agent works with its tools until it calls `finish_task`, whose arguments (matching its `output.schema`) become the node's output. Workflow nodes only. |
152
155
  | `examples` | any agent | Few-shot exchanges, `[{ input, output }]` (up to 20), added to every request's instruction as a few-shot block, the one ADK's `ExampleTool` wrote (an Instruction tool, `lib/tools/examples.ts`); the model never calls it. Keeps worked examples out of the prose of `instruction`. |
153
156
  | `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), with PATH, HOME, the temp directory and the locale but none of the server's keys, plus the variable names listed under `env:` (or `secret_env:` for a secret-shaped name, ADR 0086), and stdout and stderr each cut at 20,000 characters; `tools:` names registry tools a skill's `allowed-tools` may unlock once loaded. Worked example: `examples/harness.yaml`; engine: `lib/tools/skillToolset.ts` over `lib/tools/skills/`. |
154
157
 
@@ -181,8 +184,8 @@ function, and every adapter recognises it by marker
181
184
  | `generate_image` | Contract | Calls the Gemini image model directly, saves the result under `outputs/`, returns the path. A function tool because binary `inlineData` cannot survive delegation, where a subagent returns only its final text. |
182
185
  | `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`). |
183
186
  | `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. |
184
- | `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. Orchestrator or plan-dispatch route only (§6, Questions). |
185
- | `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. |
187
+ | `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). |
188
+ | `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, SIGTERM re-queues the job in hand, and `task_update` can cancel a running job. |
186
189
 
187
190
  **MCP tools** are the exception to the registry: a subagent with
188
191
  `mcp_server_url:` in its YAML gets its tools from a remote MCP server at
@@ -221,8 +224,17 @@ without `operations`, only GET operations become tools, so anything that
221
224
  writes is exposed only by naming it, and a named operation can be listed
222
225
  under `require_approval` (as written under `operations`) so a person
223
226
  approves each call. `auth` names an environment variable, never a value
224
- (`bearer_env`, or `api_key` with `in: header | query` and `name`); an unset
225
- variable fails the compile, and so does one of the framework's own settings
227
+ (`bearer_env`, or `api_key` with `in: header | query` and `name`), or
228
+ declares an OAuth grant (`oauth2`: `provider`, `grant: authorization_code |
229
+ client_credentials`, `authorization_url`, `token_url`, `client_id` or
230
+ `client_id_env`, `client_secret_env`, `scopes`), whose token each call
231
+ fetches: the run's user's own through the consent pause, or the server's own
232
+ from the token endpoint ([ADR 0112](./wiki/decisions/0112-oauth-grants-declared-beside-the-tool.md)).
233
+ `oauthClientsFor(configs)` (`melchizedek-agents/tools/oauthTools`) builds the
234
+ consent step's clients from the same YAML, and `npm run doctor` lists every
235
+ tool that needs a grant. The hosts a grant's token goes to must be ones the
236
+ operator binds to its provider (`MELCHIZEDEK_OAUTH_HOSTS`, [ADR 0114](./wiki/decisions/0114-oauth-tokens-go-only-to-hosts-the-operator-binds.md));
237
+ with no allowlist, `authorization_code` is refused. An unset variable fails the compile, and so does one of the framework's own settings
226
238
  (the database URL, a provider key, an `A2A_` secret: anything `.env.example`
227
239
  documents), since the YAML chooses the host it goes to.
228
240
  `OPENAPI_CREDENTIAL_ENVS`, when set, is the exact list of variables an `auth`
@@ -365,7 +377,7 @@ The xAI adapter carries the deepest capability surface: `grok-4.5`
365
377
  requests pin `reasoning.effort: "medium"` (`lib/config.ts`), SSE
366
378
  streaming works end-to-end (`runConfig: { streamingMode: 'sse' }` —
367
379
  partial delta events stream, one aggregated event persists with usage),
368
- structured outputs ride `outputSchema` → `text.format`, and two
380
+ structured outputs ride `output.schema` → `text.format`, and two
369
381
  xAI-only tools — `x_search` and `collections_search` (§3) — turn on
370
382
  live X search and hosted-document RAG. All verified live on grok-4.5.
371
383
 
@@ -486,7 +498,7 @@ runs as a one-agent syndicate through `runSyndicateTurn`.
486
498
 
487
499
  Reasoning/thinking: scratchpads from every provider are surfaced as
488
500
  dimmed THINKING output and kept out of session history. On Claude, any
489
- `reasoning:` other than `none` (or the older
501
+ `reasoning:` other than `none` (or the deprecated
490
502
  `generateContentConfig.thinkingConfig.thinkingBudget`) enables Anthropic
491
503
  extended thinking, tools included: the signed thinking blocks ride on the
492
504
  response's parts as `providerState` and are replayed verbatim within the
@@ -787,12 +799,24 @@ without it. A message that is not an answer gets the same request back,
787
799
  without a model call. The paused call and its arguments are pinned, so an
788
800
  approval cannot run a different call.
789
801
 
790
- Gates are allowed on the orchestrator and on the subagents of a
791
- plan-dispatch syndicate, which run as the turn's own agent, and on the
792
- agent nodes of a workflow (§6). A delegated
793
- subagent runs inside a tool call, where a pause cannot reach the caller, so a
794
- gate there is a load error, as is any gate inside a nested `yaml_reference`
795
- syndicate. Only function tools from the registry can be gated, not MCP tools
802
+ Gates are allowed on the orchestrator, on the subagents of a plan-dispatch
803
+ syndicate, on the agent nodes of a workflow (§6), and on a delegated
804
+ subagent, at any depth of nested delegate syndicates
805
+ ([ADR 0110](./wiki/decisions/0110-pauses-inside-delegated-subagents-reach-the-turn.md)).
806
+ A delegated subagent's gate pauses the whole turn: the call that reached it
807
+ stays open, the request carries `path` (the agents from the turn's own down
808
+ to the one that asked, e.g. `["Desk", "Mailer"]`, in the data part and in
809
+ `approval.path`), and the decision goes back down to that subagent, which
810
+ runs or refuses the call and finishes before its caller continues. The same
811
+ holds inside nested syndicates
812
+ ([ADR 0111](./wiki/decisions/0111-pauses-inside-nested-syndicates.md)): a
813
+ nested delegate syndicate's gates anywhere in it, a nested dispatch
814
+ syndicate's on its classifier (its routes never run when it is nested, so a
815
+ gate on one is a load error), and a nested workflow's gated agent nodes and
816
+ `ask_user` nodes when the workflow is a delegated subagent; the path then
817
+ ends at the node, e.g. `["Desk", "Writer", "Send"]`. A gate or `ask_user`
818
+ node in a workflow run as a dispatch route or as another workflow's node is
819
+ still a load error, as are skill scripts on a delegated subagent. Only function tools from the registry can be gated, not MCP tools
796
820
  or native-search sentinels. In code, `runSyndicateTurn` returns
797
821
  `status: 'input-required'` with `approval`, and the next turn's part
798
822
  `approvalResponsePart(approval.id, approved)` answers it.
@@ -819,12 +843,14 @@ call's result, and the agent resumes its own tool loop where it asked. A
819
843
  workflow's `ask_user` node (§6, Workflows) publishes the same data part and
820
844
  is answered the same way, so a client handles both alike.
821
845
 
822
- `ask_user` is allowed where an approval gate is: the orchestrator and the
846
+ `ask_user` is allowed where an approval gate is: the orchestrator, the
823
847
  subagents of a plan-dispatch syndicate (a dispatch turn that answers goes
824
- straight back to the route that asked, without the classifier). A delegated
825
- subagent or a workflow node listing it is a load error. In code,
848
+ straight back to the route that asked, without the classifier), and a
849
+ delegated subagent, whose question carries `path` and whose answer goes back
850
+ down to it (ADR 0110). A workflow node listing it is a load error. In code,
826
851
  `runSyndicateTurn` returns `status: 'input-required'` with `input`
827
- (`node`, `message`, `payload`), and the next message's text answers it.
852
+ (`node`, `message`, `payload`, and `path` for a delegated subagent's
853
+ question), and the next message's text answers it.
828
854
 
829
855
  #### OAuth consent for tools
830
856
 
@@ -877,6 +903,32 @@ one replica, or route the callback to the instance that paused the call. In
877
903
  code, `runSyndicateTurn({ …, toolCredentials })` returns
878
904
  `status: 'input-required'` with `consent`.
879
905
 
906
+ **From the environment (`melchizedek-serve`).** The server binary builds the
907
+ store and the consent step itself, with the clients taken from the grants the
908
+ served syndicate files declare (`auth.oauth2`, `mcp_auth`; a registry row may
909
+ name a provider they declare, never define one):
910
+
911
+ | Variable | Shape | What it does |
912
+ |---|---|---|
913
+ | `MELCHIZEDEK_CREDENTIAL_KEY` | 32 random bytes, base64 or 64 hex (`openssl rand -base64 32`) | Seals each user's tokens. Unset: no store, and an authorization-code tool answers `unavailable`. Rows live in Postgres with `DATABASE_URL`, else in process memory |
914
+ | `OAUTH_REDIRECT_URI` | `https://agents.example.com/oauth/callback` (no query) | The callback, as registered at every provider. Mounts it and lets a run ask a user to connect. Needs the key |
915
+ | `OAUTH_CALLBACK_IDENTITY` | `required` (default) or `state` | `required`: the browser that completes a grant carries the flow's user's identity, which only `A2A_AUTH=header` behind a gateway gives it. `state`: the single-use state alone binds the flow (a forwarded link can connect the wrong account) |
916
+ | `MELCHIZEDEK_OAUTH_HOSTS` | `provider=host,host;provider=host` | Which hosts each provider's tokens and client secret may be sent to. A host is a hostname, `*.domain` (subdomains only), IPv4 or `[IPv6]` |
917
+
918
+ **The host allowlist** ([ADR 0114](./wiki/decisions/0114-oauth-tokens-go-only-to-hosts-the-operator-binds.md))
919
+ is the operator's binding, never the YAML's: `MELCHIZEDEK_OAUTH_HOSTS`, or
920
+ `createA2AApp({ oauthHosts: { tracker: ['api.tracker.example.com'] } })`,
921
+ which wins. Unset, an `authorization_code` grant is refused (a user's token
922
+ never goes to a host nobody bound) and a `client_credentials` grant is
923
+ allowed (its secret is the operator's, as `bearer_env`'s is). Set, it is the
924
+ whole list: every grant's provider must be on it, and its API or MCP server,
925
+ token endpoint and authorization endpoint must be that provider's hosts. A
926
+ syndicate that breaks the rule is refused when the server loads it to serve,
927
+ again when its tools compile, and every call checks the host it sends a
928
+ token to. The server refuses to start on a malformed key, allowlist or
929
+ redirect URI, or on a redirect URI without a key; `npm run doctor` reports
930
+ each of these, and a declared grant with no key or redirect URI, by name.
931
+
880
932
  #### Limits
881
933
 
882
934
  | Setting | Default |
@@ -1056,7 +1108,7 @@ user:
1056
1108
  | | DELEGATE (default) | PLAN-DISPATCH (`dispatch:` present) |
1057
1109
  |---|---|---|
1058
1110
  | Subagents are | tools on the orchestrator | plain configs the server selects from |
1059
- | Orchestrator holds | subagent tools, no `outputSchema` | an `outputSchema`, no subagent tools |
1111
+ | Orchestrator holds | subagent tools, no `output.schema` | an `output.schema`, no subagent tools |
1060
1112
  | Routing decision is | implicit in which tool it calls | an explicit value in code |
1061
1113
  | The final answer comes from | the orchestrator re-emitting the answer | **the specialist itself** |
1062
1114
  | LLM calls per request | classify + specialist + relay | classify + specialist |
@@ -1070,14 +1122,14 @@ orchestrator:
1070
1122
  model: "gemini-3.5-flash-lite"
1071
1123
  instruction: |
1072
1124
  Name exactly ONE specialist for this message. ...
1073
- outputSchema: # a leaf holding a schema — no subagent tools
1074
- type: "OBJECT"
1075
- properties:
1076
- route: { type: "STRING", description: "Exact specialist name" }
1077
- reason: { type: "STRING", description: "≤8 plain words for the waiting user" }
1078
- required: ["route"]
1079
- generateContentConfig:
1080
- responseMimeType: "application/json"
1125
+ output:
1126
+ mime: "application/json"
1127
+ schema: # the router's schema; dispatch gives it no subagent tools
1128
+ type: "OBJECT"
1129
+ properties:
1130
+ route: { type: "STRING", description: "Exact specialist name" }
1131
+ reason: { type: "STRING", description: "≤8 plain words for the waiting user" }
1132
+ required: ["route"]
1081
1133
  ```
1082
1134
 
1083
1135
  **Why it exists.** In DELEGATE mode the orchestrator receives the
@@ -1090,13 +1142,18 @@ Plan-dispatch has no relay turn to fail, and the classifier's output
1090
1142
  shrinks from a whole relayed answer to ~15 tokens of JSON.
1091
1143
 
1092
1144
  **Why the classifier is tool-less.** An agent that holds an
1093
- `outputSchema` answers with that JSON and nothing else, so the schema sits
1094
- on a leaf that delegates nothing (see `config/agents/examples/critic.yaml`,
1095
- whose header also tells how an orchestrator holding both deadlocked on
1096
- ADK before 1.0.0).
1097
- That constraint shapes the method: the classifier is a leaf, and the
1145
+ `output.schema` ends its turn on that JSON. The router's JSON names a
1146
+ route for code to run, so the router holds no subagent tools, and the
1098
1147
  hand-off happens in code, where it can be logged, traced, and streamed
1099
- to the user as progress.
1148
+ to the user as progress. This is a choice of method, not a limit of the
1149
+ engine: an orchestrator may hold an `output.schema` beside its subagents,
1150
+ delegate, and answer in the schema itself
1151
+ ([ADR 0109](./wiki/decisions/0109-structured-output-beside-tools.md));
1152
+ under ADK, before 1.0.0, an orchestrator holding both deadlocked (see
1153
+ `config/agents/examples/critic.yaml`'s header). Choose plan-dispatch when
1154
+ the answer is the specialist's, and a schema on the orchestrator when the
1155
+ answer is its own structured judgment of what its team returned
1156
+ (`config/agents/examples/structured_critic.yaml`).
1100
1157
 
1101
1158
  **Sessions.** Every *route* runs in the shared `<contextId>` session, so
1102
1159
  one transcript accumulates across routes and long-term memory ingests
@@ -1190,7 +1247,7 @@ name is an agent or a declared node; a list of names fans out (after one
1190
1247
  node) or fans in (before one node); a map `{ <route>: <node or nodes>,
1191
1248
  default: <node> }` ends a chain and routes on the output of the node
1192
1249
  before it — the `route_key` property of a JSON output (an agent with an
1193
- `outputSchema`), else the trimmed text — with `default` catching what no
1250
+ `output.schema`), else the trimmed text — with `default` catching what no
1194
1251
  key matched. Every node receives the previous node's output as its
1195
1252
  message; a join hands on `{ <predecessor>: <output>, … }`.
1196
1253
 
@@ -1246,8 +1303,11 @@ DELEGATE subagent its last output is the tool's answer; as a plan-dispatch
1246
1303
  route it is the turn's answer, and the conversation keeps the message and
1247
1304
  that answer; as a node of another workflow it is the node's output. The
1248
1305
  graph's events are kept in the entry's own session, as for any subagent.
1249
- A nested workflow may not have an `ask_user` node (a pause inside it
1250
- cannot reach the caller); it is refused by name, as is a `map` over one.
1306
+ As a DELEGATE subagent, an `ask_user` node or a gated agent node inside it
1307
+ pauses the whole turn, and the answer resumes the graph
1308
+ ([ADR 0111](./wiki/decisions/0111-pauses-inside-nested-syndicates.md)). As a
1309
+ route or a node, a pause inside it cannot reach the turn yet, so an
1310
+ `ask_user` node or a gate there is refused by name, as is a `map` over one.
1251
1311
 
1252
1312
  **Not yet.** Remote `a2a_agent_url` subagents are refused inside a
1253
1313
  workflow by the schema. An `ask_user` tool on a node agent is refused
package/README.md CHANGED
@@ -173,6 +173,7 @@ Configurations live in `config/agents/examples/` and each demonstrates a differe
173
173
  | `tutor.yaml` | Single local agent (Ollama); the anatomy of an instruction block | [1.03 · Agent Design](https://lyceumagents.com/curriculum/agent-design/) |
174
174
  | `council.yaml` | Advocate/skeptic council weighed by an orchestrator (local) | [1.05 · Workflows & Voice](https://lyceumagents.com/curriculum/workflows-and-voice/) |
175
175
  | `critic.yaml` | Drafter → Critic loop that re-drafts until a parsed confidence score clears the bar | [1.04 · Testing & Refinement](https://lyceumagents.com/curriculum/testing-and-refinement/) |
176
+ | `structured_critic.yaml` | The critic as one agent: it delegates to a Drafter, then answers in its own JSON schema (Claude) | — |
176
177
  | `delegation.yaml` | Intent routing to specialists via agent descriptions | [1.05 · Workflows & Voice](https://lyceumagents.com/curriculum/workflows-and-voice/) |
177
178
  | `hierarchical.yaml` | Goal decomposition into sequential research and writing stages, then a checked merge | [1.05 · Workflows & Voice](https://lyceumagents.com/curriculum/workflows-and-voice/) |
178
179
  | `style_council.yaml` | Three stylists with identical knowledge and different voices, behind one router | [1.05 · Workflows & Voice](https://lyceumagents.com/curriculum/workflows-and-voice/) |
@@ -17,8 +17,8 @@ and it is loadable by filename everywhere (`npm run chat:syndicate --
17
17
 
18
18
  Everything under `examples/` is a **starter pack**: working, tested
19
19
  syndicates that demonstrate the engine's patterns (synthesis, delegation,
20
- critic loops, hierarchies, MCP tools, local open-weight models, the wiki
21
- gardeners). They are teaching material, not product — copy them, gut them,
20
+ critic loops, an orchestrator answering in its own JSON schema,
21
+ hierarchies, MCP tools, local open-weight models, the wiki gardeners). They are teaching material, not product — copy them, gut them,
22
22
  delete the whole directory; the engine runs fine without them.
23
23
 
24
24
  ## `templates/` — the production templates