melchizedek-agents 1.0.2 → 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 +333 -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 +1979 -0
  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 +11 -7
  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
@@ -26,11 +26,16 @@
26
26
  * be logged, traced, and streamed to a waiting user as progress. In DELEGATE
27
27
  * mode the choice is only ever implicit in a tool call.
28
28
  *
29
- * ── Why the router MUST be tool-less ──────────────────────────────────────
30
- * The classifier holds an `outputSchema`, and an agent holding one answers
31
- * with that JSON and nothing else (see config/agents/examples/critic.yaml; ADK
32
- * refused the combination outright). So the classifier is a leaf with no
33
- * subagent tools, and dispatch happens in code where it belongs.
29
+ * ── Why the router is tool-less ────────────────────────────────────────────
30
+ * The classifier holds an `outputSchema`, and an agent holding one ends its
31
+ * turn on that JSON. Its JSON names a route for code to run, so it holds no
32
+ * subagent tools: dispatch happens in code where it belongs. This is a choice
33
+ * of method, not a constraint of the engine: an orchestrator may hold an
34
+ * `outputSchema` beside its subagent tools, delegate, and answer in that
35
+ * schema itself (ADR 0109; ADK refused the combination, the engine does not).
36
+ * Plan-dispatch is for a turn whose answer IS the specialist's; a schema on a
37
+ * delegating orchestrator is for a turn whose answer is the orchestrator's
38
+ * own structured judgment of what its team returned.
34
39
  *
35
40
  * ── Fail-static ───────────────────────────────────────────────────────────
36
41
  * Every failure mode here resolves to `default_route` and answers the user.
@@ -26,11 +26,16 @@
26
26
  * be logged, traced, and streamed to a waiting user as progress. In DELEGATE
27
27
  * mode the choice is only ever implicit in a tool call.
28
28
  *
29
- * ── Why the router MUST be tool-less ──────────────────────────────────────
30
- * The classifier holds an `outputSchema`, and an agent holding one answers
31
- * with that JSON and nothing else (see config/agents/examples/critic.yaml; ADK
32
- * refused the combination outright). So the classifier is a leaf with no
33
- * subagent tools, and dispatch happens in code where it belongs.
29
+ * ── Why the router is tool-less ────────────────────────────────────────────
30
+ * The classifier holds an `outputSchema`, and an agent holding one ends its
31
+ * turn on that JSON. Its JSON names a route for code to run, so it holds no
32
+ * subagent tools: dispatch happens in code where it belongs. This is a choice
33
+ * of method, not a constraint of the engine: an orchestrator may hold an
34
+ * `outputSchema` beside its subagent tools, delegate, and answer in that
35
+ * schema itself (ADR 0109; ADK refused the combination, the engine does not).
36
+ * Plan-dispatch is for a turn whose answer IS the specialist's; a schema on a
37
+ * delegating orchestrator is for a turn whose answer is the orchestrator's
38
+ * own structured judgment of what its team returned.
34
39
  *
35
40
  * ── Fail-static ───────────────────────────────────────────────────────────
36
41
  * Every failure mode here resolves to `default_route` and answers the user.
@@ -7,7 +7,8 @@
7
7
  * from the YAMLs and the environment: every syndicate the loader can see,
8
8
  * every agent's model, the provider that id routes to, whether that path
9
9
  * is funded (direct key, gateway, or local), and which declared
10
- * server-side tools the path will drop. Then it says exactly what to set,
10
+ * server-side tools the path will drop, and which tools need an OAuth
11
+ * grant before they work (ADR 0112). Then it says exactly what to set,
11
12
  * grouped by how much each variable unlocks.
12
13
  *
13
14
  * WHAT IT NEVER DOES:
@@ -46,6 +47,34 @@ export interface DoctorRow {
46
47
  gaps: CapabilityGap[];
47
48
  }
48
49
  export type VerdictState = 'ready' | 'ready-local' | 'via-gateway' | 'blocked';
50
+ /**
51
+ * A tool that sends an OAuth token (ADR 0112): which agent holds it, the
52
+ * provider, and the grant. An authorization_code grant needs each user's
53
+ * consent before the tool works for them; a client_credentials grant needs
54
+ * its variables set. Variable names only, never a value.
55
+ */
56
+ export interface DoctorGrant {
57
+ agent: string;
58
+ /** `openapi <spec>` or `mcp <url>`. */
59
+ tools: string;
60
+ provider: string;
61
+ grant: 'authorization_code' | 'client_credentials';
62
+ scopes: string[];
63
+ /** The variables the grant reads that are not set here. */
64
+ missingEnv: string[];
65
+ /** Why the operator's OAuth host allowlist refuses this grant here (ADR 0114); empty when it is permitted. */
66
+ hostProblems: string[];
67
+ }
68
+ /**
69
+ * The server's OAuth setup under this environment (ADR 0114): which of its
70
+ * variables are set, and what is wrong, names only. Present when a
71
+ * syndicate declares a grant or any of the variables is set.
72
+ */
73
+ export interface DoctorOAuth {
74
+ /** Which of MELCHIZEDEK_CREDENTIAL_KEY, OAUTH_REDIRECT_URI and MELCHIZEDEK_OAUTH_HOSTS are set. */
75
+ set: Record<string, boolean>;
76
+ problems: string[];
77
+ }
49
78
  export interface DoctorSyndicate {
50
79
  /** Path relative to the agents dir, e.g. "examples/council.yaml". */
51
80
  file: string;
@@ -75,6 +104,8 @@ export interface DoctorSyndicate {
75
104
  declared: boolean;
76
105
  issue?: string;
77
106
  };
107
+ /** The tools that need an OAuth grant (ADR 0112), when any do. */
108
+ grants?: DoctorGrant[];
78
109
  }
79
110
  export interface Unlock {
80
111
  env: string;
@@ -108,6 +139,8 @@ export interface DoctorResult {
108
139
  /** Every provider not on its default endpoint: a cloud platform or a proxy (ADR 0023). */
109
140
  endpoints: DoctorEndpoint[];
110
141
  counts: Record<VerdictState, number>;
142
+ /** The OAuth setup, when any syndicate declares a grant or its variables are set. */
143
+ oauth?: DoctorOAuth;
111
144
  }
112
145
  export interface DoctorEndpoint {
113
146
  provider: ProviderId;
@@ -7,7 +7,8 @@
7
7
  * from the YAMLs and the environment: every syndicate the loader can see,
8
8
  * every agent's model, the provider that id routes to, whether that path
9
9
  * is funded (direct key, gateway, or local), and which declared
10
- * server-side tools the path will drop. Then it says exactly what to set,
10
+ * server-side tools the path will drop, and which tools need an OAuth
11
+ * grant before they work (ADR 0112). Then it says exactly what to set,
11
12
  * grouped by how much each variable unlocks.
12
13
  *
13
14
  * WHAT IT NEVER DOES:
@@ -33,6 +34,10 @@ import { credentialSource, endpointFromEnv, endpointLabel, endpointProblems, PLA
33
34
  import { GATEWAY_ENV, GATEWAY_KEY_ENV, GATEWAYS, gatewayConfig, gatewayProblem, gatewayUsable, } from './models/gateway.js';
34
35
  import { PROVIDERS } from './models/providerMap.js';
35
36
  import { describeRuntime } from './runtime/runtimeFlag.js';
37
+ import { agentOAuthGrants, oauthEnvNames } from './tools/oauthTools.js';
38
+ import { grantHostProblems, OAUTH_REDIRECT_URI_ENV, oauthEnvProblems } from './a2a/oauthSetup.js';
39
+ import { CREDENTIAL_KEY_ENV } from './tools/credentialCipher.js';
40
+ import { OAUTH_HOSTS_ENV } from './tools/oauthHosts.js';
36
41
  function resolvable(module) {
37
42
  try {
38
43
  createRequire(import.meta.url).resolve(module);
@@ -160,6 +165,18 @@ export function tierOf(rows) {
160
165
  return 'multi-provider';
161
166
  return providers[0] === 'ollama' ? 'keyless' : providers[0];
162
167
  }
168
+ /** The OAuth grants one agent's tools declare, with the variables not set here. */
169
+ function grantsOf(agent, prefix) {
170
+ return agentOAuthGrants(agent, prefix).map((use) => ({
171
+ agent: use.agent,
172
+ tools: use.tools,
173
+ provider: use.oauth2.provider,
174
+ grant: use.oauth2.grant,
175
+ scopes: [...(use.oauth2.scopes ?? [])],
176
+ missingEnv: oauthEnvNames(use.oauth2).filter((name) => !process.env[name]?.trim()),
177
+ hostProblems: grantHostProblems(use),
178
+ }));
179
+ }
163
180
  /** The first problem, without the file prefix the doctor already shows. */
164
181
  function firstProblem(err) {
165
182
  if (err instanceof SyndicateValidationError)
@@ -179,6 +196,8 @@ function needsOf(agent, delegates = false) {
179
196
  function walk(config, prefix, opts, rows) {
180
197
  const orch = config.orchestrator;
181
198
  const orchModel = orch?.model;
199
+ if (orch)
200
+ opts.grants.push(...grantsOf(orch, prefix));
182
201
  if (orch && orchModel) {
183
202
  // In DELEGATE mode the orchestrator calls its subagents as tools; under
184
203
  // plan-dispatch it is a tool-less classifier and code runs the route; in
@@ -218,6 +237,7 @@ function walk(config, prefix, opts, rows) {
218
237
  }
219
238
  continue;
220
239
  }
240
+ opts.grants.push(...grantsOf(sub, prefix));
221
241
  // A subagent without its own model inherits the orchestrator's.
222
242
  const model = sub.model ?? orchModel;
223
243
  if (!model)
@@ -283,7 +303,8 @@ export function diagnoseSyndicate(file, agentsDir, scripts) {
283
303
  const config = load(file);
284
304
  const rows = [];
285
305
  const nestedErrors = [];
286
- walk(config, '', { load, seen: new Set([file, path.basename(file)]), nestedErrors }, rows);
306
+ const grants = [];
307
+ walk(config, '', { load, seen: new Set([file, path.basename(file)]), nestedErrors, grants }, rows);
287
308
  const memory = config.memory_system === 'long-term'
288
309
  ? {
289
310
  namespace: config.memory_namespace ?? LEGACY_MEMORY_APP_NAME,
@@ -302,6 +323,7 @@ export function diagnoseSyndicate(file, agentsDir, scripts) {
302
323
  ...(runCommand ? { runCommand } : {}),
303
324
  ...(nestedErrors.length ? { error: nestedErrors.join('\n') } : {}),
304
325
  ...(memory ? { memory } : {}),
326
+ ...(grants.length ? { grants } : {}),
305
327
  };
306
328
  }
307
329
  catch (err) {
@@ -398,7 +420,11 @@ export function runDoctor(options = {}) {
398
420
  for (const s of syndicates)
399
421
  counts[s.verdict.state]++;
400
422
  flagMemoryNamespaces(syndicates);
401
- return { agentsDir, runtime: runtimeReport(), syndicates, unlocks, gateway, endpoints: endpointRows(), counts };
423
+ // The server's OAuth setup (ADR 0114): the key, the redirect URI and the allowlist, names only.
424
+ const grants = syndicates.flatMap((s) => s.grants ?? []).map((g) => ({ agent: g.agent, tools: g.tools, oauth2: { provider: g.provider, grant: g.grant, token_url: '' } }));
425
+ const set = Object.fromEntries([CREDENTIAL_KEY_ENV, OAUTH_REDIRECT_URI_ENV, OAUTH_HOSTS_ENV].map((name) => [name, !!process.env[name]?.trim()]));
426
+ const oauth = grants.length || Object.values(set).some(Boolean) ? { set, problems: oauthEnvProblems(grants) } : undefined;
427
+ return { agentsDir, runtime: runtimeReport(), syndicates, unlocks, gateway, endpoints: endpointRows(), counts, ...(oauth ? { oauth } : {}) };
402
428
  }
403
429
  // ── Rendering ────────────────────────────────────────────────────────────────
404
430
  const ANSI = {
@@ -464,6 +490,14 @@ export function renderDoctor(result, opts = {}) {
464
490
  ? `gateway ${c.red}✗${c.reset} ${result.gateway.problem}`
465
491
  : `gateway ${c.yellow}◇${c.reset} ${result.gateway.label} fills in for any provider whose direct key is absent ${c.dim}(native search is lost on that path)${c.reset}`);
466
492
  }
493
+ if (result.oauth) {
494
+ // Names only: which variables are set, never their values (ADR 0114).
495
+ const set = Object.entries(result.oauth.set).map(([name, on]) => `${name} ${on ? 'set' : 'unset'}`).join(' · ');
496
+ const mark = result.oauth.problems.length ? `${c.red}✗${c.reset}` : `${c.green}✓${c.reset}`;
497
+ lines.push(`oauth ${mark} ${c.dim}${set}${c.reset}`);
498
+ for (const p of result.oauth.problems)
499
+ lines.push(` ${c.red}✗ ${p}${c.reset}`);
500
+ }
467
501
  lines.push('');
468
502
  // The table: one line per syndicate (name, verdict, run command), then
469
503
  // one indented line per agent.
@@ -490,6 +524,16 @@ export function renderDoctor(result, opts = {}) {
490
524
  if (s.memory?.issue) {
491
525
  lines.push(` ${c.yellow}⚠ memory: ${s.memory.issue}${c.reset}`);
492
526
  }
527
+ for (const g of s.grants ?? []) {
528
+ const scopes = g.scopes.length ? ` ${c.dim}[${g.scopes.join(' ')}]${c.reset}` : '';
529
+ const what = g.grant === 'authorization_code'
530
+ ? `needs a grant: each user connects ${g.provider} (consent) before ${g.tools} works for them`
531
+ : `needs a grant: the server's own ${g.provider} token (client_credentials) for ${g.tools}`;
532
+ const missing = g.missingEnv.length ? ` ${c.red}✗ ${g.missingEnv.join(', ')} not set${c.reset}` : '';
533
+ lines.push(` ${c.cyan}⚿ ${g.agent}${c.reset} ${what}${scopes}${missing}`);
534
+ for (const p of g.hostProblems ?? [])
535
+ lines.push(` ${c.red}✗ ${p}${c.reset}`);
536
+ }
493
537
  if (s.declaredTier && s.declaredTier !== s.tier) {
494
538
  lines.push(` ${c.yellow}⚠ header says "tier: ${s.declaredTier}" but the models say ${s.tier}${c.reset}`);
495
539
  }
@@ -2,6 +2,7 @@ import type { DispatchConfig } from './dispatch.ts';
2
2
  export type { DispatchConfig } from './dispatch.ts';
3
3
  import type { SkillsConfig } from './tools/skillToolset.ts';
4
4
  import type { OpenApiConfig } from './tools/openapiTools.ts';
5
+ import type { McpAuthConfig } from './tools/oauthTools.ts';
5
6
  import type { ContextConfig, ExampleConfig } from './compile.ts';
6
7
  export type { OpenApiConfig } from './tools/openapiTools.ts';
7
8
  export type { SkillsConfig } from './tools/skillToolset.ts';
@@ -91,7 +92,9 @@ export interface AgentYamlConfig {
91
92
  outputKey?: string;
92
93
  /**
93
94
  * Model generation config (temperature, topP, safety, thinking, etc.)
94
- * Maps to LlmAgentConfig.generateContentConfig.
95
+ * Maps to LlmAgentConfig.generateContentConfig. In YAML a v1 spelling,
96
+ * deprecated (ADR 0115): write `sampling`, `output` and `reasoning`. It
97
+ * stays the engine form the loader hands downstream.
95
98
  */
96
99
  generateContentConfig?: GenerateContentConfig;
97
100
  /**
@@ -102,12 +105,45 @@ export interface AgentYamlConfig {
102
105
  reasoning?: ReasoningSetting;
103
106
  /**
104
107
  * Output schema for structured JSON responses.
105
- * Maps to LlmAgentConfig.outputSchema.
108
+ * Maps to LlmAgentConfig.outputSchema. In YAML a v1 spelling, deprecated
109
+ * (ADR 0115): write `output.schema`. It stays the engine form.
106
110
  */
107
111
  outputSchema?: Record<string, unknown>;
112
+ /**
113
+ * Sampling, provider-neutral (v2): replaces generateContentConfig's
114
+ * temperature, topP, maxOutputTokens and stopSequences. The loader folds it
115
+ * into generateContentConfig (lib/agentDialect.ts toEngineAgent), so a
116
+ * loaded config never carries it. Cannot be combined with the v1 key it replaces.
117
+ */
118
+ sampling?: {
119
+ temperature?: number;
120
+ top_p?: number;
121
+ max_output_tokens?: number;
122
+ stop?: string[];
123
+ };
124
+ /**
125
+ * Structured output (v2): `schema` replaces outputSchema, `mime` replaces
126
+ * generateContentConfig.responseMimeType. Folded into those by the loader.
127
+ */
128
+ output?: {
129
+ schema?: Record<string, unknown>;
130
+ mime?: 'application/json' | 'text/plain';
131
+ };
132
+ /**
133
+ * Per-provider instruction (v2), keyed by provider id (lib/models/providerMap.ts):
134
+ * `instruction` replaces the agent's instruction, `instruction_append` is
135
+ * appended after a blank line, for the provider of the model the agent runs
136
+ * on. Applied at compile time, before the skills index; not for fallback_model.
137
+ */
138
+ model_overrides?: Partial<Record<'gemini' | 'anthropic' | 'openai' | 'xai' | 'moonshot' | 'ollama', {
139
+ instruction?: string;
140
+ instruction_append?: string;
141
+ }>>;
108
142
  mcp_server_url?: string;
109
143
  /** The MCP server's tools this agent may use; the rest are not exposed. */
110
144
  mcp_tools?: string[];
145
+ /** The OAuth grant the MCP server takes (ADR 0112): each user's, or the server's own. */
146
+ mcp_auth?: McpAuthConfig;
111
147
  /** Answers when `model` fails provider-side, or its provider's circuit is open (ADR 0044). */
112
148
  fallback_model?: string;
113
149
  /**
@@ -216,6 +252,8 @@ export interface SyndicateYamlConfig {
216
252
  memory_retention_days?: number;
217
253
  /** Optional hard limit on the turn's model calls, subagents included (LLM -> Tool cycles). */
218
254
  max_steps?: number;
255
+ /** Delegated calls from one model step that run at once (ADR 0116). Default 4. */
256
+ max_concurrency?: number;
219
257
  /**
220
258
  * The nested `yaml_reference:` syndicates this one reaches, raw, keyed by
221
259
  * reference: written by the registry publisher (`bundleReferences`) so a
@@ -251,6 +289,11 @@ export interface LoadSyndicateOptions {
251
289
  * with the public example of the same name (ADR 0018).
252
290
  */
253
291
  shippedFallback?: boolean;
292
+ /**
293
+ * Where load-time warnings go (the deprecated v1 spellings of ADR 0115).
294
+ * Default console.warn. A warning names the file and key paths, never a value.
295
+ */
296
+ onWarning?: (message: string) => void;
254
297
  }
255
298
  /**
256
299
  * Parse CLI variable bindings into a VariableMap.
@@ -4,6 +4,7 @@ import * as path from 'path';
4
4
  import { createClient } from '@supabase/supabase-js';
5
5
  import { hasSupabaseCredentials } from './persistence/supabaseProvider.js';
6
6
  import { validateSyndicateConfig } from './syndicateSchema.js';
7
+ import { v1SpellingsOf } from './agentDialect.js';
7
8
  // ── Variable Injection (private) ──────────────────────────
8
9
  const TOKEN_RE = /\{\{(\w+)\}\}/g;
9
10
  const FULL_TOKEN_RE = /^\{\{(\w+)\}\}$/;
@@ -87,6 +88,7 @@ function resolveAndValidate(raw, label, options) {
87
88
  ...bindings,
88
89
  };
89
90
  const interpolated = isPlainObject(raw) ? interpolateDeep(raw, merged) : raw;
91
+ warnV1Spellings(interpolated, label, options.onWarning);
90
92
  const unresolved = findUnresolvedTokens(interpolated);
91
93
  if (unresolved.length > 0) {
92
94
  console.warn(`⚠ Unresolved template variables: ${unresolved.map((t) => `{{${t}}}`).join(', ')}`);
@@ -99,6 +101,24 @@ function resolveAndValidate(raw, label, options) {
99
101
  }
100
102
  return config;
101
103
  }
104
+ /** Labels already warned about, so a server loading one file per request warns once per process. */
105
+ const warnedV1 = new Set();
106
+ /**
107
+ * One line per label when a file still uses the v1 spellings ADR 0115
108
+ * deprecates. Key paths and the label only: never a value (a prompt, a key).
109
+ */
110
+ function warnV1Spellings(raw, label, onWarning = console.warn) {
111
+ if (warnedV1.has(label))
112
+ return;
113
+ const paths = v1SpellingsOf(raw);
114
+ if (!paths.length)
115
+ return;
116
+ warnedV1.add(label);
117
+ const fix = label.startsWith('registry:') || label.startsWith('bundled:')
118
+ ? 'Run npx melchizedek-codemod on the file it was published from, and republish'
119
+ : `Run npx melchizedek-codemod ${label} to rewrite them`;
120
+ onWarning(`⚠ ${label}: generateContentConfig and outputSchema are v1 spellings, deprecated (ADR 0115): ${paths.join(', ')}. ${fix}.`);
121
+ }
102
122
  function applyOverrides(config, overrides) {
103
123
  return { ...config, ...overrides };
104
124
  }
@@ -227,9 +247,9 @@ export function collectGuards(config, loadNested = nestedLoader(config), seen =
227
247
  return [...names];
228
248
  }
229
249
  export function loadSyndicate(filename, options = {}) {
230
- const { bindings = {}, overrides } = options;
250
+ const { bindings = {}, overrides, onWarning } = options;
231
251
  const { raw, label, filePath } = readSyndicateFile(filename, options);
232
- return anchorSpecPaths(resolveAndValidate(raw, label, { bindings, overrides }), path.dirname(filePath));
252
+ return anchorSpecPaths(resolveAndValidate(raw, label, { bindings, overrides, onWarning }), path.dirname(filePath));
233
253
  }
234
254
  /**
235
255
  * An `openapi:` spec path is written relative to the syndicate file, so a
@@ -2,11 +2,12 @@
2
2
  * lib/memory/namespace.ts — memory always resolves to the ROOT syndicate's
3
3
  * namespace (ADR 0020 item 3).
4
4
  *
5
- * WHY: a DELEGATE subagent runs on its own session row under
6
- * `appName = <the subagent's name>`, and its memory tools search with the
7
- * session's app name. Facts are written under the root syndicate's
8
- * namespace, so a `load_memory` declared on a subagent would search
9
- * `<SubAgent>/<user>`, find nothing, and say so without error. Wrapping the
5
+ * WHY: a DELEGATE subagent runs on its own session row, filed under its
6
+ * agent path (`appName = <app>/<caller>/<subagent>`, ADR 0111; the
7
+ * subagent's name alone in a conversation stored before), and its memory
8
+ * tools search with the session's app name. Facts are written under the
9
+ * root syndicate's namespace, so a `load_memory` declared on a subagent
10
+ * would search the child's own key, find nothing, and say so without error. Wrapping the
10
11
  * service the runtime receives pins every search and ingestion to one
11
12
  * namespace, whatever app name the caller carries.
12
13
  *
@@ -2,11 +2,12 @@
2
2
  * lib/memory/namespace.ts — memory always resolves to the ROOT syndicate's
3
3
  * namespace (ADR 0020 item 3).
4
4
  *
5
- * WHY: a DELEGATE subagent runs on its own session row under
6
- * `appName = <the subagent's name>`, and its memory tools search with the
7
- * session's app name. Facts are written under the root syndicate's
8
- * namespace, so a `load_memory` declared on a subagent would search
9
- * `<SubAgent>/<user>`, find nothing, and say so without error. Wrapping the
5
+ * WHY: a DELEGATE subagent runs on its own session row, filed under its
6
+ * agent path (`appName = <app>/<caller>/<subagent>`, ADR 0111; the
7
+ * subagent's name alone in a conversation stored before), and its memory
8
+ * tools search with the session's app name. Facts are written under the
9
+ * root syndicate's namespace, so a `load_memory` declared on a subagent
10
+ * would search the child's own key, find nothing, and say so without error. Wrapping the
10
11
  * service the runtime receives pins every search and ingestion to one
11
12
  * namespace, whatever app name the caller carries.
12
13
  *
@@ -30,12 +30,12 @@
30
30
  */
31
31
  import { endpointFromEnv } from '../models/endpoints.js';
32
32
  import { GoogleGenAI } from '@google/genai';
33
- import { trace, SpanStatusCode } from '@opentelemetry/api';
33
+ import { SpanStatusCode } from '@opentelemetry/api';
34
34
  import { llmRequestToModelRequest, modelResponseToLlmResponse } from '../models/genaiMapping.js';
35
35
  import { EMBEDDING_DIMENSIONS, EMBEDDING_MODEL, MEMORY_EXTRACTION_MODEL } from '../config.js';
36
36
  import { providerForModel } from '../models/providerMap.js';
37
37
  import { resolveModel } from '../models/registry.js';
38
- import { initializeTracing, traceLlmGeneration } from '../observability/tracer.js';
38
+ import { startEngineSpan, traceLlmGeneration } from '../observability/tracer.js';
39
39
  import { trimTrailingSlashes } from '../models/urls.js';
40
40
  /**
41
41
  * Extraction on any model id. The key passed here reaches only a model of
@@ -86,8 +86,7 @@ export function modelExtractor(opts = {}) {
86
86
  * a reasoning step.
87
87
  */
88
88
  async function traced(provider, model, count, fn) {
89
- initializeTracing();
90
- const span = trace.getTracer('melchizedek-tracer').startSpan('llm.request');
89
+ const span = startEngineSpan('melchizedek-tracer', 'llm.request');
91
90
  span.setAttribute('llm.provider', provider);
92
91
  span.setAttribute('llm.model', model);
93
92
  span.setAttribute('llm.purpose', 'memory_embedding');
@@ -55,7 +55,7 @@ export declare function describeCapabilities(model: string, tools?: readonly str
55
55
  * a funded direct path with no dropped tool is the quiet default.
56
56
  */
57
57
  export declare function capabilitySummary(agentName: string, r: CapabilityReport): string | undefined;
58
- export declare const CAPABILITIES: readonly ['delegation', 'memory_tools', 'structured_output', 'thinking_with_tools', 'streaming', 'vision', 'native_search'];
58
+ export declare const CAPABILITIES: readonly ['delegation', 'memory_tools', 'structured_output', 'structured_output_with_tools', 'thinking_with_tools', 'streaming', 'vision', 'native_search'];
59
59
  export type Capability = (typeof CAPABILITIES)[number];
60
60
  export declare const CAPABILITY_LABELS: Record<Capability, string>;
61
61
  export type Support = 'supported' | 'degraded' | 'unsupported';
@@ -74,6 +74,21 @@ export declare function capabilityOf(model: string, capability: Capability, opts
74
74
  }): CapabilityCell & {
75
75
  row: MatrixRow;
76
76
  };
77
+ /**
78
+ * Whether an outputSchema goes in the same request as the agent's tools, as
79
+ * the provider's own structured-output field, on the path `model` takes
80
+ * (ADR 0109). True only where the `structured_output_with_tools` cell is
81
+ * supported and the model's generation takes it: Claude from Opus 4.8,
82
+ * Sonnet 5 and Haiku 5.5 on (`output_config.format`), every OpenAI id
83
+ * (`text.format`), Gemini 2 and later on Vertex AI (`responseJsonSchema`).
84
+ * Elsewhere the native loop declares set_model_response beside the tools
85
+ * (lib/runtime/native/request.ts), which works on every path. A path that
86
+ * cannot be planned (a malformed gateway setting) answers false, the form
87
+ * that works everywhere.
88
+ */
89
+ export declare function outputSchemaBesideTools(model: string, opts?: {
90
+ callerKey?: boolean;
91
+ }): boolean;
77
92
  /** What one agent's YAML asks of its model. */
78
93
  export interface AgentNeedsInput {
79
94
  tools?: readonly string[];
@@ -20,7 +20,7 @@
20
20
  import { PROVIDERS } from './providerMap.js';
21
21
  import { planTransport } from './gateway.js';
22
22
  import { nativeSearchOn, PLATFORMS_FOR, platformFromEnv } from './endpoints.js';
23
- import { claudeUrlImagesOn } from './claudeModels.js';
23
+ import { claudeGeneration, claudeUrlImagesOn } from './claudeModels.js';
24
24
  /** The platform a direct path uses (ADR 0023); a misconfigured one reads as direct here, the doctor reports it. */
25
25
  function directPlatform(provider) {
26
26
  try {
@@ -117,6 +117,7 @@ export const CAPABILITIES = [
117
117
  'delegation',
118
118
  'memory_tools',
119
119
  'structured_output',
120
+ 'structured_output_with_tools',
120
121
  'thinking_with_tools',
121
122
  'streaming',
122
123
  'vision',
@@ -126,6 +127,7 @@ export const CAPABILITY_LABELS = {
126
127
  delegation: 'delegation (subagents as tools)',
127
128
  memory_tools: 'memory tools (load_memory)',
128
129
  structured_output: 'structured output (outputSchema)',
130
+ structured_output_with_tools: 'structured output beside tools (an outputSchema on an agent that calls tools or delegates)',
129
131
  thinking_with_tools: 'thinking with tool use',
130
132
  streaming: 'token streaming',
131
133
  vision: 'image input',
@@ -138,12 +140,14 @@ const nativeSearch = (row) => SERVER_SIDE_TOOLS.web_search.includes(row)
138
140
  ? ok()
139
141
  : unsupported('no native search on this path; the web_search sentinel is dropped (use web_extract)');
140
142
  const responsesReasoningNote = (ids) => `encrypted reasoning items are replayed verbatim within the turn's tool loop, with store: false (ADR 0050); ${ids}`;
143
+ const setModelResponseNote = (why) => `the schema travels as a set_model_response tool beside the agent's tools, so the model is asked, not held, to answer in it: the turn ends when it calls the tool, and its arguments are the answer (${why}; ADR 0109)`;
141
144
  const CHAT_THINKING_NOTE = "reasoning: is the lever, sent as reasoning_effort in the model's word, a thinking budget as the level that covers it (ADR 0047); the reasoning is not carried between tool steps, so the model re-reasons each step";
142
145
  export const CAPABILITY_MATRIX = {
143
146
  gemini: {
144
147
  delegation: ok(),
145
148
  memory_tools: ok(),
146
149
  structured_output: ok(),
150
+ structured_output_with_tools: degraded(setModelResponseNote('on the Gemini API; Gemini 2 and later on Vertex AI take responseJsonSchema beside the tools in one request')),
147
151
  thinking_with_tools: ok(),
148
152
  streaming: ok(),
149
153
  vision: ok(),
@@ -153,6 +157,7 @@ export const CAPABILITY_MATRIX = {
153
157
  delegation: ok(),
154
158
  memory_tools: ok(),
155
159
  structured_output: ok('test', 'output_config.format (json_schema) from Opus 4.8, Sonnet 5 and Haiku 5.5 on; a forced tool call on Claude 4.6 and earlier and Opus 4.7, offered under tool_choice auto when thinking is on (ADR 0049)'),
160
+ structured_output_with_tools: ok('test', 'output_config.format beside the tools in one request from Opus 4.8, Sonnet 5 and Haiku 5.5 on; Claude 4.6 and earlier and Opus 4.7 get a set_model_response tool beside them (ADR 0109)'),
156
161
  thinking_with_tools: ok('test', "a thinking budget on Claude 4.6 and earlier, adaptive thinking with output_config.effort after (ADR 0049); signed thinking blocks are replayed verbatim within the turn's tool loop (ADR 0046), and where the model binds them to the conversation (Fable 5.1, Opus 5.5, Sonnet 5.5, Haiku 5.5) under drop_block, so a block whose history changed is dropped rather than rejected; with a budget, a step answering another model's tool call runs without thinking"),
157
162
  streaming: ok(),
158
163
  vision: ok('test', 'user-turn images only'),
@@ -162,6 +167,7 @@ export const CAPABILITY_MATRIX = {
162
167
  delegation: ok(),
163
168
  memory_tools: ok(),
164
169
  structured_output: ok(),
170
+ structured_output_with_tools: ok('test', 'text.format (strict json_schema) beside the tools in one request (ADR 0109)'),
165
171
  thinking_with_tools: ok('test', responsesReasoningNote('reasoning ids only (o-series, gpt-5*)')),
166
172
  streaming: ok(),
167
173
  vision: ok('test', 'user-turn images only'),
@@ -171,6 +177,7 @@ export const CAPABILITY_MATRIX = {
171
177
  delegation: ok(),
172
178
  memory_tools: ok(),
173
179
  structured_output: ok(),
180
+ structured_output_with_tools: degraded(setModelResponseNote('the schema is not yet sent beside tools on xAI')),
174
181
  thinking_with_tools: ok('test', responsesReasoningNote('grok-4.5, grok-4.6 and grok-4.7; other grok ids re-reason each step')),
175
182
  streaming: ok(),
176
183
  vision: ok('test', 'user-turn images only'),
@@ -180,6 +187,7 @@ export const CAPABILITY_MATRIX = {
180
187
  delegation: ok(),
181
188
  memory_tools: ok(),
182
189
  structured_output: ok('test', 'strict json_schema; kimi-k2.6 is documented as unstable on complex schemas ($ref, oneOf)'),
190
+ structured_output_with_tools: degraded(setModelResponseNote('the schema is not yet sent beside tools on Moonshot')),
183
191
  thinking_with_tools: ok('test', "reasoning_content is sent back on the turn's tool-loop assistant messages for the same model (ADR 0046) on kimi-k3, kimi-k2.6 and kimi-k2.7-code; earlier turns' reasoning is not, which K3 and K2.7 Code also ask for; effort travels as reasoning_effort (K3) or a thinking switch (K2.x)"),
184
192
  streaming: ok(),
185
193
  vision: ok('test', 'user-turn images only, sent as base64 (Moonshot takes no public image URLs)'),
@@ -189,6 +197,7 @@ export const CAPABILITY_MATRIX = {
189
197
  delegation: ok(),
190
198
  memory_tools: ok(),
191
199
  structured_output: ok('test', 'json_schema, enforced by grammar-constrained decoding from Ollama 0.5.0 (an older server ignores it); not enforced on Ollama Cloud (ADR 0096)'),
200
+ structured_output_with_tools: degraded(setModelResponseNote('grammar-constrained decoding to the schema would hold back the tool calls')),
192
201
  thinking_with_tools: degraded(CHAT_THINKING_NOTE),
193
202
  streaming: ok(),
194
203
  vision: ok('test', 'needs a vision model, e.g. ollama/qwen3-vl:8b'),
@@ -198,6 +207,7 @@ export const CAPABILITY_MATRIX = {
198
207
  delegation: ok(),
199
208
  memory_tools: ok(),
200
209
  structured_output: ok('test', 'strict json_schema; upstream support varies by model'),
210
+ structured_output_with_tools: degraded(setModelResponseNote('upstream support for a schema beside tools varies by model')),
201
211
  thinking_with_tools: degraded(CHAT_THINKING_NOTE),
202
212
  streaming: ok(),
203
213
  vision: ok('test', 'upstream model must accept images'),
@@ -215,6 +225,45 @@ export function capabilityOf(model, capability, opts = {}) {
215
225
  }
216
226
  return { row, ...CAPABILITY_MATRIX[row][capability] };
217
227
  }
228
+ /**
229
+ * Whether an outputSchema goes in the same request as the agent's tools, as
230
+ * the provider's own structured-output field, on the path `model` takes
231
+ * (ADR 0109). True only where the `structured_output_with_tools` cell is
232
+ * supported and the model's generation takes it: Claude from Opus 4.8,
233
+ * Sonnet 5 and Haiku 5.5 on (`output_config.format`), every OpenAI id
234
+ * (`text.format`), Gemini 2 and later on Vertex AI (`responseJsonSchema`).
235
+ * Elsewhere the native loop declares set_model_response beside the tools
236
+ * (lib/runtime/native/request.ts), which works on every path. A path that
237
+ * cannot be planned (a malformed gateway setting) answers false, the form
238
+ * that works everywhere.
239
+ */
240
+ export function outputSchemaBesideTools(model, opts = {}) {
241
+ let cell;
242
+ try {
243
+ cell = capabilityOf(model, 'structured_output_with_tools', opts);
244
+ }
245
+ catch {
246
+ return false;
247
+ }
248
+ if (cell.support !== 'supported')
249
+ return false;
250
+ switch (cell.row) {
251
+ case 'anthropic':
252
+ return claudeGeneration(model).structuredOutput === 'output_format';
253
+ case 'gemini':
254
+ return geminiMajorVersion(model) >= 2;
255
+ default:
256
+ return true;
257
+ }
258
+ }
259
+ /** A Gemini id's major version (a Vertex AI resource name's last segment), or 0 when it is not a `gemini-<n>` id. Read without a pattern. */
260
+ function geminiMajorVersion(model) {
261
+ const name = model.slice(model.lastIndexOf('/') + 1);
262
+ if (!name.startsWith('gemini-'))
263
+ return 0;
264
+ const major = Number.parseInt(name.slice('gemini-'.length), 10);
265
+ return Number.isFinite(major) ? major : 0;
266
+ }
218
267
  export function requiredCapabilities(agent) {
219
268
  const tools = agent.tools ?? [];
220
269
  const needs = new Set();
@@ -224,6 +273,8 @@ export function requiredCapabilities(agent) {
224
273
  needs.add('memory_tools');
225
274
  if (agent.outputSchema)
226
275
  needs.add('structured_output');
276
+ if (agent.outputSchema && (tools.length > 0 || agent.delegates))
277
+ needs.add('structured_output_with_tools');
227
278
  const thinking = agent.generateContentConfig?.thinkingConfig;
228
279
  const thinks = agent.reasoning !== undefined
229
280
  ? agent.reasoning !== 'none' && !(typeof agent.reasoning === 'object' && agent.reasoning.budget_tokens === 0)
@@ -256,6 +307,9 @@ export function capabilityGaps(model, agent, opts = {}) {
256
307
  export function platformCell(provider, platform, capability) {
257
308
  if (platform === 'direct')
258
309
  return undefined;
310
+ if (capability === 'structured_output_with_tools' && provider === 'gemini' && platform === 'vertex') {
311
+ return ok('test', 'responseJsonSchema beside the tools in one request on Gemini 2 and later; an earlier id gets set_model_response (ADR 0109)');
312
+ }
259
313
  if (capability === 'vision' && provider === 'anthropic' && !claudeUrlImagesOn(platform)) {
260
314
  return degraded(`user-turn images inline (base64) only; an image given by URL is dropped, since ${PLATFORM_LABEL[platform]} takes no URL image source`);
261
315
  }
@@ -1,4 +1,4 @@
1
- import type { SpanContext } from '@opentelemetry/api';
1
+ import type { Span, SpanContext, SpanOptions } from '@opentelemetry/api';
2
2
  import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
3
3
  import type { LlmResponse } from '../models/genaiMapping.ts';
4
4
  import type { TurnEvent as Event } from '../runtime/events.ts';
@@ -43,6 +43,13 @@ export declare function takeTraceStats(traceId: string): TraceStats | undefined;
43
43
  /** Name of the agent of the nearest enclosing agent span (ADK's `invoke_agent`, the loop's `agent.invoke`), or null. */
44
44
  export declare function agentForSpan(spanId: string | undefined): string | null;
45
45
  export declare function initializeTracing(): void;
46
+ /**
47
+ * Starts a span on the named tracer, starting the tracer first; in a turn
48
+ * that opted out of tracing, a non-recording span and no tracer. Every span
49
+ * the engine opens inside a turn goes through here or checks turnUntraced
50
+ * itself.
51
+ */
52
+ export declare function startEngineSpan(tracerName: string, name: string, options?: SpanOptions): Span;
46
53
  export interface TraceMetadata {
47
54
  syndicateName: string;
48
55
  bindings?: Record<string, any>;