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
@@ -0,0 +1,205 @@
1
+ /**
2
+ * lib/a2a/oauthSetup.ts — tool credentials and the OAuth consent step from
3
+ * the environment, for the server binary (WS6-3d, ADR 0114).
4
+ *
5
+ * WHY this file exists:
6
+ * `createA2AApp({ toolCredentials })` takes a credential store and a
7
+ * consent step, and the YAML declares the grants (ADR 0112), but the
8
+ * `melchizedek-serve` binary built neither: a declared authorization-code
9
+ * grant worked only for an embedder who wrote the wiring. This module is
10
+ * that wiring, from four variables, so the binary and an embedder build it
11
+ * the same way:
12
+ *
13
+ * MELCHIZEDEK_CREDENTIAL_KEY the key that seals each user's tokens (ADR 0072)
14
+ * OAUTH_REDIRECT_URI the consent callback, as registered at every provider (ADR 0085)
15
+ * MELCHIZEDEK_OAUTH_HOSTS which hosts each provider's tokens may go to (ADR 0114)
16
+ * the client id and secret variables each YAML grant names
17
+ *
18
+ * The consent clients are built from the syndicates the server serves from
19
+ * files (`oauthClientsFor`), never from a registry row, so a stored YAML
20
+ * can name a provider the files declare but cannot define one.
21
+ *
22
+ * WHAT IT NEVER DOES: print, return or log a value. Problems name variables,
23
+ * providers and hosts only (secrets-hygiene).
24
+ */
25
+ import { CREDENTIAL_KEY_ENV, credentialCipherFromEnv } from '../tools/credentialCipher.js';
26
+ import { credentialStore, memoryCredentialRows } from '../tools/credentialStore.js';
27
+ import { oauthConsent } from '../tools/oauthConsent.js';
28
+ import { OAUTH_HOSTS_ENV, oauthHosts } from '../tools/oauthHosts.js';
29
+ import { oauthClientsFor, oauthGrantHostProblems, oauthRefreshProviders, syndicateOAuthGrants, syndicateOAuthHostProblems } from '../tools/oauthTools.js';
30
+ /** The consent callback's URL, exactly as registered at every provider. */
31
+ export const OAUTH_REDIRECT_URI_ENV = 'OAUTH_REDIRECT_URI';
32
+ /** Who may complete a consent callback: `required` (the browser carries the user's identity) or `state` (the nonce alone). */
33
+ export const OAUTH_CALLBACK_IDENTITY_ENV = 'OAUTH_CALLBACK_IDENTITY';
34
+ /** OAUTH_CALLBACK_IDENTITY, checked: `required` (the default) or `state`. Throws on anything else. */
35
+ export function callbackIdentity(env = process.env) {
36
+ const raw = (env[OAUTH_CALLBACK_IDENTITY_ENV] ?? '').trim().toLowerCase() || 'required';
37
+ if (raw !== 'required' && raw !== 'state')
38
+ throw new Error(`${OAUTH_CALLBACK_IDENTITY_ENV} must be 'required' or 'state' (got '${raw.slice(0, 40)}')`);
39
+ return raw;
40
+ }
41
+ /**
42
+ * Why consent callbacks would all be refused under this environment, or
43
+ * null: a required caller identity that the configured authenticator cannot
44
+ * give a browser (only a gateway's header can: A2A_AUTH=header).
45
+ */
46
+ export function callbackIdentityProblem(env = process.env) {
47
+ const authKind = (env.A2A_AUTH ?? 'secret').trim().toLowerCase() || 'secret';
48
+ if (callbackIdentity(env) !== 'required' || authKind === 'header')
49
+ return null;
50
+ return `the consent callback requires the browser to carry the user's identity, which A2A_AUTH=${authKind} cannot give a browser, so every callback is refused: serve behind a gateway (A2A_AUTH=header), or set ${OAUTH_CALLBACK_IDENTITY_ENV}=state to let the single-use state alone bind it`;
51
+ }
52
+ const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);
53
+ /** Why `raw` cannot be the consent redirect URI, or null (the rule oauthConsent enforces). */
54
+ export function redirectUriProblem(raw) {
55
+ let url;
56
+ try {
57
+ url = new URL(raw);
58
+ }
59
+ catch {
60
+ return `${OAUTH_REDIRECT_URI_ENV} is not a URL`;
61
+ }
62
+ if (!(url.protocol === 'https:' || (url.protocol === 'http:' && LOOPBACK.has(url.hostname))))
63
+ return `${OAUTH_REDIRECT_URI_ENV} must use https (http only on a loopback host)`;
64
+ if (url.username || url.password)
65
+ return `${OAUTH_REDIRECT_URI_ENV} must not carry credentials`;
66
+ if (url.search || url.hash)
67
+ return `${OAUTH_REDIRECT_URI_ENV} must not carry a query or a fragment`;
68
+ return null;
69
+ }
70
+ /** The MCP server a grant's tools call, when the grant is an MCP one. */
71
+ const serversOf = (use) => (use.tools.startsWith('mcp ') && use.tools.length > 4 ? [use.tools.slice(4)] : []);
72
+ /**
73
+ * Why this grant's hosts are refused by the allowlist in `env`, one line
74
+ * each (the MCP server, the token and authorization endpoints; an OpenAPI
75
+ * entry's servers are checked when it compiles). Names and hosts only.
76
+ */
77
+ export function grantHostProblems(use, env = process.env) {
78
+ let allowlist;
79
+ try {
80
+ allowlist = oauthHosts(env) ?? null;
81
+ }
82
+ catch {
83
+ return [`${OAUTH_HOSTS_ENV} is malformed`];
84
+ }
85
+ return oauthGrantHostProblems(use.oauth2, serversOf(use), allowlist);
86
+ }
87
+ /**
88
+ * What is wrong with this environment for these declared grants, one line
89
+ * each, names only: the credential key, the redirect URI and the allowlist
90
+ * (each grant's own hosts: grantHostProblems). The doctor prints these; the
91
+ * server refuses to start on the ones that would make it unsafe.
92
+ */
93
+ export function oauthEnvProblems(grants, env = process.env) {
94
+ const problems = [];
95
+ const authCode = [...new Set(grants.filter((g) => g.oauth2.grant === 'authorization_code').map((g) => g.oauth2.provider))];
96
+ const keySet = !!env[CREDENTIAL_KEY_ENV]?.trim();
97
+ const redirect = env[OAUTH_REDIRECT_URI_ENV]?.trim();
98
+ if (keySet) {
99
+ try {
100
+ credentialCipherFromEnv(env);
101
+ }
102
+ catch (err) {
103
+ // parseCredentialKey's message names the variable and the shape, never the value.
104
+ problems.push(err instanceof Error ? err.message : `${CREDENTIAL_KEY_ENV} is malformed`);
105
+ }
106
+ }
107
+ if (redirect) {
108
+ const problem = redirectUriProblem(redirect);
109
+ if (problem)
110
+ problems.push(problem);
111
+ if (!keySet)
112
+ problems.push(`${OAUTH_REDIRECT_URI_ENV} is set but ${CREDENTIAL_KEY_ENV} is not: the consent step has nowhere to keep a grant`);
113
+ try {
114
+ const identity = callbackIdentityProblem(env);
115
+ if (identity && authCode.length)
116
+ problems.push(identity);
117
+ }
118
+ catch (err) {
119
+ problems.push(err instanceof Error ? err.message : `${OAUTH_CALLBACK_IDENTITY_ENV} is malformed`);
120
+ }
121
+ }
122
+ if (authCode.length) {
123
+ const who = authCode.join(', ');
124
+ if (!keySet)
125
+ problems.push(`authorization_code grants (${who}) need ${CREDENTIAL_KEY_ENV}: without it no user's token can be held, and their tools answer unavailable`);
126
+ if (!redirect)
127
+ problems.push(`authorization_code grants (${who}) need ${OAUTH_REDIRECT_URI_ENV}: without it a user who has not connected cannot be asked to`);
128
+ }
129
+ let allowlist;
130
+ try {
131
+ allowlist = oauthHosts(env);
132
+ }
133
+ catch (err) {
134
+ problems.push(err instanceof Error ? err.message : `${OAUTH_HOSTS_ENV} is malformed`);
135
+ }
136
+ if (authCode.length && allowlist === undefined && !problems.some((p) => p.startsWith(OAUTH_HOSTS_ENV))) {
137
+ problems.push(`authorization_code grants (${authCode.join(', ')}) need ${OAUTH_HOSTS_ENV}: without it the operator has bound no provider to its hosts, so they are refused`);
138
+ }
139
+ return problems;
140
+ }
141
+ /**
142
+ * The credential store and consent step from the environment. Throws, naming
143
+ * variables only, when the configuration would be unsafe or cannot work: a
144
+ * malformed key, a redirect URI without a key, a malformed redirect URI or
145
+ * allowlist, an authorization-code grant the allowlist does not permit, a
146
+ * grant whose client variables are unset.
147
+ */
148
+ export function oauthServerSetup(options) {
149
+ const env = options.env ?? process.env;
150
+ const warnings = [];
151
+ const grants = syndicateOAuthGrants(options.configs, options.load);
152
+ const authCode = [...new Set(grants.filter((g) => g.oauth2.grant === 'authorization_code').map((g) => g.oauth2.provider))];
153
+ const redirect = env[OAUTH_REDIRECT_URI_ENV]?.trim() || undefined;
154
+ const cipher = credentialCipherFromEnv(env); // a malformed key throws here, at boot
155
+ const allowlist = oauthHosts(env); // a malformed allowlist throws here, at boot
156
+ const hostsLabel = allowlist ? `host allowlist for ${Object.keys(allowlist).join(', ')}` : `no host allowlist (${OAUTH_HOSTS_ENV}): authorization_code grants refused`;
157
+ if (redirect) {
158
+ const problem = redirectUriProblem(redirect);
159
+ if (problem)
160
+ throw new Error(problem);
161
+ }
162
+ // A served syndicate whose grant the allowlist refuses is refused here, at boot, as its route would refuse it.
163
+ const refused = syndicateOAuthHostProblems(options.configs, { ...(options.load ? { load: options.load } : {}), allowlist: allowlist ?? null });
164
+ if (refused.length)
165
+ throw new Error(`refusing an OAuth grant the operator's host allowlist does not permit:\n - ${refused.join('\n - ')}`);
166
+ if (!cipher) {
167
+ if (redirect)
168
+ throw new Error(`${OAUTH_REDIRECT_URI_ENV} is set but ${CREDENTIAL_KEY_ENV} is not: the consent step has nowhere to keep a grant. Set ${CREDENTIAL_KEY_ENV} (openssl rand -base64 32).`);
169
+ if (authCode.length)
170
+ warnings.push(`authorization_code grants (${authCode.join(', ')}) are declared but ${CREDENTIAL_KEY_ENV} is not set: their tools answer unavailable.`);
171
+ return { summary: `off (${CREDENTIAL_KEY_ENV} unset) · ${hostsLabel}`, warnings };
172
+ }
173
+ const clients = oauthClientsFor(options.configs, { env, ...(options.load ? { load: options.load } : {}), allowlist: allowlist ?? null });
174
+ const providers = Object.keys(clients);
175
+ const store = credentialStore({
176
+ rows: options.rows ?? memoryCredentialRows(),
177
+ cipher,
178
+ providers: oauthRefreshProviders(clients, { ...(options.allowPrivate ? { allowPrivate: true } : {}), ...(options.fetch ? { fetch: options.fetch } : {}) }),
179
+ ...(options.audit ? { audit: options.audit } : {}),
180
+ });
181
+ if (!options.rows)
182
+ warnings.push(`Tool credentials are held in process memory: lost on restart, one instance only. Set DATABASE_URL to keep them sealed in Postgres.`);
183
+ const where = options.rows ? 'postgres' : 'process memory';
184
+ if (!redirect) {
185
+ if (providers.length)
186
+ warnings.push(`${OAUTH_REDIRECT_URI_ENV} is not set: a user who has not connected ${providers.join(', ')} cannot be asked to.`);
187
+ return { toolCredentials: { store }, summary: `sealed (key ${cipher.keyId}) in ${where}; no consent step · ${hostsLabel}`, warnings };
188
+ }
189
+ if (!providers.length) {
190
+ warnings.push(`${OAUTH_REDIRECT_URI_ENV} is set but no served syndicate declares an authorization_code grant: no consent callback is mounted.`);
191
+ return { toolCredentials: { store }, summary: `sealed (key ${cipher.keyId}) in ${where}; no consent step · ${hostsLabel}`, warnings };
192
+ }
193
+ const consent = oauthConsent({
194
+ providers: clients,
195
+ redirectUri: redirect,
196
+ credentials: store,
197
+ ...(options.audit ? { audit: options.audit } : {}),
198
+ ...(options.fetch ? { fetch: options.fetch } : {}),
199
+ });
200
+ return {
201
+ toolCredentials: { store, consent },
202
+ summary: `sealed (key ${cipher.keyId}) in ${where}; consent at ${new URL(redirect).pathname} for ${providers.join(', ')} · ${hostsLabel}`,
203
+ warnings,
204
+ };
205
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * lib/agentDialect.ts — the YAML v2 agent keys and the engine form they map to
3
+ * (ADR 0115).
4
+ *
5
+ * WHY this file exists:
6
+ * An agent's sampling and structured-output settings were spelled in
7
+ * Gemini's dialect (`generateContentConfig.temperature`, `outputSchema`),
8
+ * which is what the engine still reads (lib/runtime/native/request.ts
9
+ * buildModelRequest). YAML v2 spells them provider-neutrally (`sampling`,
10
+ * `output`) and adds per-provider instruction overrides (`model_overrides`).
11
+ * This module is the one place the two spellings meet:
12
+ *
13
+ * - `V2_SPELLINGS`: each v2 key path and the v1 path it replaces. The
14
+ * loader (lib/syndicateSchema.ts crossFieldProblems) refuses a file that
15
+ * sets both; `toEngineAgent` refuses a config built in code that does.
16
+ * - `toEngineAgent`: folds `sampling` and `output` into the engine form.
17
+ * validateSyndicateConfig applies it to every agent it returns, so every
18
+ * loader hands downstream the engine form; lib/compile.ts applies it
19
+ * again (idempotent) for configs built in code.
20
+ * - `v1SpellingsOf`: the deprecated v1 key paths a raw file still uses, for
21
+ * the loader's one-line deprecation warning (paths only, never values).
22
+ * - `instructionFor`: an agent's instruction with its `model_overrides`
23
+ * entry for the provider of the model it runs on applied.
24
+ *
25
+ * Internal: not in the lib/index.ts barrel nor the package exports map.
26
+ */
27
+ import type { AgentYamlConfig } from './loadSyndicate.ts';
28
+ /** One v2 key and the v1 (engine) path it replaces. Both paths are relative to the agent. */
29
+ export interface V2Spelling {
30
+ readonly v2: readonly [string, string];
31
+ readonly v1: readonly string[];
32
+ }
33
+ /** Every v2 key with a v1 equivalent; the mapper writes the v1 path. */
34
+ export declare const V2_SPELLINGS: readonly V2Spelling[];
35
+ /** The v2 agent keys, which an inline agent alone may carry (not a yaml_reference or a2a_agent_url subagent). */
36
+ export declare const V2_AGENT_KEYS: readonly ['sampling', 'output', 'model_overrides'];
37
+ /** The v1 spellings ADR 0115 deprecates, as agent keys. */
38
+ export declare const V1_AGENT_KEYS: readonly ['generateContentConfig', 'outputSchema'];
39
+ /** The v2/v1 pairs an agent sets both of: each is a conflict. */
40
+ export declare function conflictsOf(agent: unknown): V2Spelling[];
41
+ /** The loader's message for one conflict, reported against the v2 key path. */
42
+ export declare function conflictMessage(s: V2Spelling): string;
43
+ /**
44
+ * An agent in the engine form: `sampling` and `output` folded into
45
+ * `generateContentConfig` and `outputSchema` (the table above) and removed;
46
+ * `model_overrides` kept (lib/compile.ts applies it). The same object when
47
+ * the agent has neither `sampling` nor `output`, so it is idempotent. Throws
48
+ * when a v2 key and the v1 key it replaces are both set.
49
+ */
50
+ export declare function toEngineAgent<T extends object>(agent: T): T;
51
+ /**
52
+ * The deprecated v1 key paths a raw syndicate uses, in file order:
53
+ * `orchestrator.generateContentConfig`, `subagents[2].outputSchema`, ...
54
+ * Paths only: the loader logs them, never the values under them.
55
+ */
56
+ export declare function v1SpellingsOf(raw: unknown): string[];
57
+ /**
58
+ * An agent's base instruction for the model it runs on: its
59
+ * `model_overrides` entry for that model's provider replaces it
60
+ * (`instruction`) or is appended after a blank line (`instruction_append`).
61
+ * The base instruction when there is no entry or no model id. The skills
62
+ * index (lib/compile.ts withSkills) is appended after this.
63
+ */
64
+ export declare function instructionFor(agent: Pick<AgentYamlConfig, 'model_overrides'> & {
65
+ instruction?: string;
66
+ }, modelId: string | undefined): string;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * lib/agentDialect.ts — the YAML v2 agent keys and the engine form they map to
3
+ * (ADR 0115).
4
+ *
5
+ * WHY this file exists:
6
+ * An agent's sampling and structured-output settings were spelled in
7
+ * Gemini's dialect (`generateContentConfig.temperature`, `outputSchema`),
8
+ * which is what the engine still reads (lib/runtime/native/request.ts
9
+ * buildModelRequest). YAML v2 spells them provider-neutrally (`sampling`,
10
+ * `output`) and adds per-provider instruction overrides (`model_overrides`).
11
+ * This module is the one place the two spellings meet:
12
+ *
13
+ * - `V2_SPELLINGS`: each v2 key path and the v1 path it replaces. The
14
+ * loader (lib/syndicateSchema.ts crossFieldProblems) refuses a file that
15
+ * sets both; `toEngineAgent` refuses a config built in code that does.
16
+ * - `toEngineAgent`: folds `sampling` and `output` into the engine form.
17
+ * validateSyndicateConfig applies it to every agent it returns, so every
18
+ * loader hands downstream the engine form; lib/compile.ts applies it
19
+ * again (idempotent) for configs built in code.
20
+ * - `v1SpellingsOf`: the deprecated v1 key paths a raw file still uses, for
21
+ * the loader's one-line deprecation warning (paths only, never values).
22
+ * - `instructionFor`: an agent's instruction with its `model_overrides`
23
+ * entry for the provider of the model it runs on applied.
24
+ *
25
+ * Internal: not in the lib/index.ts barrel nor the package exports map.
26
+ */
27
+ import { providerForModel } from './models/providerMap.js';
28
+ const isObj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
29
+ /** Every v2 key with a v1 equivalent; the mapper writes the v1 path. */
30
+ export const V2_SPELLINGS = [
31
+ { v2: ['sampling', 'temperature'], v1: ['generateContentConfig', 'temperature'] },
32
+ { v2: ['sampling', 'top_p'], v1: ['generateContentConfig', 'topP'] },
33
+ { v2: ['sampling', 'max_output_tokens'], v1: ['generateContentConfig', 'maxOutputTokens'] },
34
+ { v2: ['sampling', 'stop'], v1: ['generateContentConfig', 'stopSequences'] },
35
+ { v2: ['output', 'schema'], v1: ['outputSchema'] },
36
+ { v2: ['output', 'mime'], v1: ['generateContentConfig', 'responseMimeType'] },
37
+ ];
38
+ /** The v2 agent keys, which an inline agent alone may carry (not a yaml_reference or a2a_agent_url subagent). */
39
+ export const V2_AGENT_KEYS = ['sampling', 'output', 'model_overrides'];
40
+ /** The v1 spellings ADR 0115 deprecates, as agent keys. */
41
+ export const V1_AGENT_KEYS = ['generateContentConfig', 'outputSchema'];
42
+ /** The value at `path` under `root`, or undefined. */
43
+ function at(root, path) {
44
+ let cur = root;
45
+ for (const seg of path) {
46
+ if (!isObj(cur))
47
+ return undefined;
48
+ cur = cur[seg];
49
+ }
50
+ return cur;
51
+ }
52
+ /** The v2/v1 pairs an agent sets both of: each is a conflict. */
53
+ export function conflictsOf(agent) {
54
+ return V2_SPELLINGS.filter((s) => at(agent, s.v2) !== undefined && at(agent, s.v1) !== undefined);
55
+ }
56
+ /** The loader's message for one conflict, reported against the v2 key path. */
57
+ export function conflictMessage(s) {
58
+ return `cannot be combined with ${s.v1.join('.')}; ${s.v2.join('.')} replaces it, so keep one (ADR 0115)`;
59
+ }
60
+ /**
61
+ * An agent in the engine form: `sampling` and `output` folded into
62
+ * `generateContentConfig` and `outputSchema` (the table above) and removed;
63
+ * `model_overrides` kept (lib/compile.ts applies it). The same object when
64
+ * the agent has neither `sampling` nor `output`, so it is idempotent. Throws
65
+ * when a v2 key and the v1 key it replaces are both set.
66
+ */
67
+ export function toEngineAgent(agent) {
68
+ const a = agent;
69
+ if (a.sampling === undefined && a.output === undefined)
70
+ return agent;
71
+ const clash = conflictsOf(a);
72
+ if (clash.length) {
73
+ const name = typeof a.name === 'string' ? a.name : 'agent';
74
+ throw new Error(`${name}: ${clash.map((s) => `${s.v2.join('.')} cannot be combined with ${s.v1.join('.')}`).join('; ')}; the v2 key replaces it, so keep one (ADR 0115)`);
75
+ }
76
+ const { sampling: _sampling, output: _output, ...rest } = a;
77
+ const out = { ...rest };
78
+ let gcc;
79
+ for (const s of V2_SPELLINGS) {
80
+ const value = at(a, s.v2);
81
+ if (value === undefined)
82
+ continue;
83
+ if (s.v1.length === 1) {
84
+ out[s.v1[0]] = value;
85
+ continue;
86
+ }
87
+ gcc ??= { ...(isObj(a.generateContentConfig) ? a.generateContentConfig : {}) };
88
+ gcc[s.v1[1]] = value;
89
+ }
90
+ if (gcc)
91
+ out.generateContentConfig = gcc;
92
+ return out;
93
+ }
94
+ /**
95
+ * The deprecated v1 key paths a raw syndicate uses, in file order:
96
+ * `orchestrator.generateContentConfig`, `subagents[2].outputSchema`, ...
97
+ * Paths only: the loader logs them, never the values under them.
98
+ */
99
+ export function v1SpellingsOf(raw) {
100
+ if (!isObj(raw))
101
+ return [];
102
+ const agents = [['orchestrator', raw.orchestrator]];
103
+ if (Array.isArray(raw.subagents))
104
+ raw.subagents.forEach((sub, i) => agents.push([`subagents[${i}]`, sub]));
105
+ const out = [];
106
+ for (const [path, agent] of agents) {
107
+ if (!isObj(agent))
108
+ continue;
109
+ for (const key of V1_AGENT_KEYS)
110
+ if (agent[key] !== undefined)
111
+ out.push(`${path}.${key}`);
112
+ }
113
+ return out;
114
+ }
115
+ /**
116
+ * An agent's base instruction for the model it runs on: its
117
+ * `model_overrides` entry for that model's provider replaces it
118
+ * (`instruction`) or is appended after a blank line (`instruction_append`).
119
+ * The base instruction when there is no entry or no model id. The skills
120
+ * index (lib/compile.ts withSkills) is appended after this.
121
+ */
122
+ export function instructionFor(agent, modelId) {
123
+ const base = agent.instruction ?? '';
124
+ if (!agent.model_overrides || modelId === undefined)
125
+ return base;
126
+ const entry = agent.model_overrides[providerForModel(modelId)];
127
+ if (!entry)
128
+ return base;
129
+ if (entry.instruction !== undefined)
130
+ return entry.instruction;
131
+ if (entry.instruction_append !== undefined)
132
+ return base ? `${base}\n\n${entry.instruction_append}` : entry.instruction_append;
133
+ return base;
134
+ }
@@ -177,6 +177,8 @@ export interface AgentSpec {
177
177
  context?: ContextConfig;
178
178
  /** `mode: task`. */
179
179
  mode?: 'task';
180
+ /** The syndicate's `max_concurrency`, on its orchestrator (ADR 0116): delegated calls one step runs at once. */
181
+ maxConcurrency?: number;
180
182
  }
181
183
  /**
182
184
  * The spec of a workflow syndicate: every agent through compileSubagentSpec
@@ -184,11 +186,12 @@ export interface AgentSpec {
184
186
  * lookup for its tool nodes. `name` and `description` name a nested one
185
187
  * after its subagent entry (ADR 0098); `ref` is the file it came from.
186
188
  *
187
- * A nested workflow runs inside its caller's tool call, so a pause inside it
188
- * could not reach the caller (ADR 0028, as for any nested syndicate): an
189
- * `ask_user` node is refused by name here.
189
+ * A nested workflow delegated to as a subagent (`delegated`) pauses through
190
+ * its caller's open call (ADR 0111). Run as a dispatch route or a workflow
191
+ * node, its pause cannot reach the turn yet: an `ask_user` node is refused
192
+ * by name there.
190
193
  */
191
- export declare function compileWorkflowSpec(config: SyndicateYamlConfig, opts?: CompileOptions, name?: string, description?: string, ref?: string): Promise<WorkflowSpec>;
194
+ export declare function compileWorkflowSpec(config: SyndicateYamlConfig, opts?: CompileOptions, name?: string, description?: string, ref?: string, delegated?: boolean): Promise<WorkflowSpec>;
192
195
  /** Every agent spec a workflow runs, its nested workflow nodes' agents included, for one adapter lookup (nativeAdapterFor). */
193
196
  export declare function workflowAgentSpecs(spec: WorkflowSpec): AgentSpec[];
194
197
  /** A subagent entry, compiled: one agent, or a nested workflow syndicate's whole graph (ADR 0098, ADR 0106). */
@@ -205,8 +208,10 @@ export type EntrySpec = {
205
208
  * as its whole graph under the entry's name and description. A delegated
206
209
  * subagent, a dispatch route and a workflow node all compile through here,
207
210
  * so the graph a file describes is the graph that runs (ADR 0106).
211
+ * `delegated` marks a delegated subagent's entry, whose nested workflow may
212
+ * pause (ADR 0111).
208
213
  */
209
- export declare function compileEntrySpec(subCfg: SubagentYamlConfig, opts?: CompileOptions): Promise<EntrySpec>;
214
+ export declare function compileEntrySpec(subCfg: SubagentYamlConfig, opts?: CompileOptions, delegated?: boolean): Promise<EntrySpec>;
210
215
  /**
211
216
  * The names of a syndicate's subagent entries that are `yaml_reference`s to
212
217
  * a workflow syndicate. Loads each reference once and compiles nothing, so a
@@ -40,6 +40,7 @@ import { isWorkflowSyndicate, nodeKind } from './workflowConfig.js';
40
40
  import { loadSyndicate, nestedLoader } from './loadSyndicate.js';
41
41
  import { REASONING_OLDER_SPELLING } from './syndicateSchema.js';
42
42
  import { reasoningConfig } from './models/reasoning.js';
43
+ import { instructionFor, toEngineAgent } from './agentDialect.js';
43
44
  import { resolveTools as resolveNamedTools } from './toolRegistry.js';
44
45
  import { createMcpTools } from './tools/mcpToolFactory.js';
45
46
  import { requireApproval, toolOf } from './tools/tool.js';
@@ -144,7 +145,7 @@ export function examplesTool(examples) {
144
145
  const tool = examplesInstructionTool(examples);
145
146
  return tool ? [tool] : [];
146
147
  }
147
- async function resolveAgentTools(toolNames, mcpServerUrl, opts, openapi, examples, mcpAllowed) {
148
+ async function resolveAgentTools(toolNames, mcpServerUrl, opts, openapi, examples, mcpAllowed, mcpAuth) {
148
149
  const tools = [...resolveNamedTools(toolNames, opts.onUnknownTool), ...examplesTool(examples)];
149
150
  // OpenAPI operations become tools here, so require_approval can name them.
150
151
  for (const entry of openapi ?? []) {
@@ -158,7 +159,8 @@ async function resolveAgentTools(toolNames, mcpServerUrl, opts, openapi, example
158
159
  }
159
160
  if (mcpServerUrl) {
160
161
  opts.log?.(`Loading MCP tools: ${mcpServerUrl}`);
161
- const offered = await createMcpTools(mcpServerUrl);
162
+ // An authorization_code server lists nothing at startup: its tools are the mcp_tools names, each on its user's own connection (ADR 0112).
163
+ const offered = await createMcpTools(mcpServerUrl, { ...(mcpAllowed ? { tools: mcpAllowed } : {}), ...(mcpAuth?.oauth2 ? { oauth2: mcpAuth.oauth2 } : {}) });
162
164
  // mcp_tools: only the named tools are exposed (a server's list is its
163
165
  // own to change); a name the server does not offer is reported, and a
164
166
  // gate on it fails the compile in gateTools.
@@ -201,9 +203,11 @@ async function withSkills(instruction, tools, skills, agentName, opts) {
201
203
  export function agentGates(agent) {
202
204
  return !!agent?.require_approval?.length || agent?.skills?.scripts === 'local';
203
205
  }
204
- function specOf(yaml, name, description, instruction, tools, opts) {
205
- const resolvedModel = (opts.resolveModel ?? ((m) => m))(yaml.model);
206
- const modelId = modelIdOf(yaml.model, resolvedModel);
206
+ function resolveAgentModel(model, opts) {
207
+ const resolvedModel = (opts.resolveModel ?? ((m) => m))(model);
208
+ return { resolvedModel, modelId: modelIdOf(model, resolvedModel) };
209
+ }
210
+ function specOf(yaml, name, description, instruction, tools, { resolvedModel, modelId }) {
207
211
  const spec = {
208
212
  name,
209
213
  instruction,
@@ -240,12 +244,6 @@ function specOf(yaml, name, description, instruction, tools, opts) {
240
244
  return spec;
241
245
  }
242
246
  const asTools = (tools) => tools.map((tool) => ({ kind: 'tool', tool }));
243
- /**
244
- * A nested `yaml_reference:` syndicate, loaded. A nested syndicate runs
245
- * inside a tool call (or as a route whose own subagents do): a pause there
246
- * cannot reach the caller (ADR 0028), so one that declares approval gates is
247
- * refused.
248
- */
249
247
  /** Where the yaml_reference chain a compile is inside is kept: an own symbol on the options, so a spread copy keeps it. */
250
248
  const NESTING = Symbol('melchizedek.compile.nesting');
251
249
  /** The deepest a yaml_reference chain may go: well past any syndicate the engine ships, short of the stack. */
@@ -265,11 +263,30 @@ function nestedOptions(ref, opts) {
265
263
  throw new Error(`${ref}: nested syndicates go deeper than ${MAX_NESTING_DEPTH} levels (${[...chain, ref].join(' → ')}).`);
266
264
  return { ...opts, [NESTING]: [...chain, ref] };
267
265
  }
268
- function loadNestedSyndicate(ref, opts) {
266
+ /**
267
+ * A nested `yaml_reference:` syndicate, loaded, with the approval gates that
268
+ * cannot pause where it runs refused by name (ADR 0110, ADR 0111):
269
+ * - a nested delegate syndicate's gates pause the turn through the open
270
+ * call, wherever it runs;
271
+ * - a nested dispatch syndicate runs its classifier alone (compileSpec
272
+ * lists no route on it), so its orchestrator may gate, and a gate on a
273
+ * route, which never runs nested, is refused;
274
+ * - a nested workflow's gates pause the walk, which reaches the caller
275
+ * only through a delegated call's open call (`delegated`): as a
276
+ * dispatch route or a workflow node, its pause cannot reach the turn
277
+ * yet, so they are refused there.
278
+ */
279
+ function loadNestedSyndicate(ref, opts, delegated = false) {
269
280
  opts.log?.(`Loading nested syndicate: ${ref}`);
270
281
  const nested = (opts.loadNested ?? loadSyndicate)(ref);
271
- if (declaresApprovals(nested)) {
272
- throw new Error(`${ref}: approval gates (require_approval) are not supported inside a nested syndicate.`);
282
+ if (isDispatchSyndicate(nested)) {
283
+ const gated = (nested.subagents ?? []).find((s) => agentGates(s));
284
+ if (gated) {
285
+ throw new Error(`${ref}: approval gates (require_approval, or skill scripts) on the route '${gated.name}' never run: a nested dispatch syndicate runs its classifier alone. Gate the classifier, or run the syndicate as its own.`);
286
+ }
287
+ }
288
+ else if (isWorkflowSyndicate(nested) && !delegated && declaresApprovals(nested)) {
289
+ throw new Error(`${ref}: approval gates (require_approval, or skill scripts) in a workflow run as a dispatch route or a workflow node cannot pause the turn yet; delegate to the workflow as a subagent, or run it as its own syndicate.`);
273
290
  }
274
291
  return nested;
275
292
  }
@@ -279,17 +296,18 @@ function loadNestedSyndicate(ref, opts) {
279
296
  * lookup for its tool nodes. `name` and `description` name a nested one
280
297
  * after its subagent entry (ADR 0098); `ref` is the file it came from.
281
298
  *
282
- * A nested workflow runs inside its caller's tool call, so a pause inside it
283
- * could not reach the caller (ADR 0028, as for any nested syndicate): an
284
- * `ask_user` node is refused by name here.
299
+ * A nested workflow delegated to as a subagent (`delegated`) pauses through
300
+ * its caller's open call (ADR 0111). Run as a dispatch route or a workflow
301
+ * node, its pause cannot reach the turn yet: an `ask_user` node is refused
302
+ * by name there.
285
303
  */
286
- export async function compileWorkflowSpec(config, opts = {}, name, description, ref) {
304
+ export async function compileWorkflowSpec(config, opts = {}, name, description, ref, delegated = false) {
287
305
  if (!isWorkflowSyndicate(config))
288
306
  throw new Error(`${config.syndicate_name}: no workflow block`);
289
- if (name) {
307
+ if (name && !delegated) {
290
308
  const asking = Object.entries(config.workflow.nodes ?? {}).find(([, node]) => nodeKind(node) === 'ask_user');
291
309
  if (asking) {
292
- throw new Error(`${ref ?? config.syndicate_name}: the ask_user node '${asking[0]}' pauses for a person, which a workflow nested in another syndicate (${name}) cannot carry to its caller; run the workflow as its own syndicate, or remove the node.`);
310
+ throw new Error(`${ref ?? config.syndicate_name}: the ask_user node '${asking[0]}' pauses for a person, which a workflow run as a dispatch route or a workflow node (${name}) cannot carry to the turn yet; delegate to the workflow as a subagent, run it as its own syndicate, or remove the node.`);
293
311
  }
294
312
  }
295
313
  const workflowName = name || config.syndicate_name;
@@ -328,13 +346,15 @@ export function workflowAgentSpecs(spec) {
328
346
  * as its whole graph under the entry's name and description. A delegated
329
347
  * subagent, a dispatch route and a workflow node all compile through here,
330
348
  * so the graph a file describes is the graph that runs (ADR 0106).
349
+ * `delegated` marks a delegated subagent's entry, whose nested workflow may
350
+ * pause (ADR 0111).
331
351
  */
332
- export async function compileEntrySpec(subCfg, opts = {}) {
352
+ export async function compileEntrySpec(subCfg, opts = {}, delegated = false) {
333
353
  if (subCfg.yaml_reference && !subCfg.a2a_agent_url) {
334
354
  const nestedOpts = nestedOptions(subCfg.yaml_reference, opts);
335
- const nested = loadNestedSyndicate(subCfg.yaml_reference, nestedOpts);
355
+ const nested = loadNestedSyndicate(subCfg.yaml_reference, nestedOpts, delegated);
336
356
  if (isWorkflowSyndicate(nested)) {
337
- return { kind: 'workflow', workflow: await compileWorkflowSpec(nested, nestedOpts, subCfg.name, subCfg.description, subCfg.yaml_reference) };
357
+ return { kind: 'workflow', workflow: await compileWorkflowSpec(nested, nestedOpts, subCfg.name, subCfg.description, subCfg.yaml_reference, delegated) };
338
358
  }
339
359
  return { kind: 'agent', spec: await compileSpec(nested, nestedOpts, subCfg.name, subCfg.description) };
340
360
  }
@@ -371,10 +391,14 @@ export async function compileSubagentSpec(subCfg, opts = {}) {
371
391
  }
372
392
  return compileSpec(nested, nestedOpts, subCfg.name, subCfg.description);
373
393
  }
374
- const gated = gateTools(await resolveAgentTools(subCfg.tools, subCfg.mcp_server_url, opts, subCfg.openapi, subCfg.examples, subCfg.mcp_tools), subCfg.require_approval, subCfg.name);
375
- const { instruction, tools } = await withSkills(subCfg.instruction ?? '', gated, subCfg.skills, subCfg.name, opts);
376
- logCapabilities(opts, subCfg.name, subCfg.model, subCfg.tools);
377
- return specOf(subCfg, subCfg.name, subCfg.description, instruction, asTools(tools), opts);
394
+ // YAML v2 (ADR 0115): the engine form, for a config built in code (a loaded one already is).
395
+ const agent = toEngineAgent(subCfg);
396
+ const gated = gateTools(await resolveAgentTools(agent.tools, agent.mcp_server_url, opts, agent.openapi, agent.examples, agent.mcp_tools, agent.mcp_auth), agent.require_approval, agent.name);
397
+ const model = resolveAgentModel(agent.model, opts);
398
+ // model_overrides for the provider the agent runs on, before the skills index is appended.
399
+ const { instruction, tools } = await withSkills(instructionFor(agent, model.modelId), gated, agent.skills, agent.name, opts);
400
+ logCapabilities(opts, agent.name, agent.model, agent.tools);
401
+ return specOf(agent, agent.name, agent.description, instruction, asTools(tools), model);
378
402
  }
379
403
  /**
380
404
  * The spec of a syndicate's orchestrator. In DELEGATE mode its subagents
@@ -401,16 +425,24 @@ export async function compileSpec(config, opts = {}, overrideName, overrideDescr
401
425
  opts.log?.(`Remote A2A agent: ${subCfg.name} → ${subCfg.a2a_agent_url}`);
402
426
  return { kind: 'remote', name: subCfg.name, description: subCfg.description, url: subCfg.a2a_agent_url };
403
427
  }
404
- const entry = await compileEntrySpec(subCfg, opts);
428
+ // A delegated subagent: a pause inside it, a nested workflow's included, reaches the caller (ADR 0110, ADR 0111).
429
+ const entry = await compileEntrySpec(subCfg, opts, true);
405
430
  return entry.kind === 'workflow' ? entry : { kind: 'agent', agent: entry.spec };
406
431
  }));
407
- const name = overrideName || config.orchestrator.name;
408
- // Orchestrator tools are registry names only — no entrypoint has ever
409
- // attached an MCP server to an orchestrator, and this compiler preserves
410
- // that exactly rather than widening the contract in passing.
411
- const own = gateTools(await resolveAgentTools(config.orchestrator.tools, undefined, opts, config.orchestrator.openapi, config.orchestrator.examples), config.orchestrator.require_approval, name);
412
- // The skills toolset goes last, after the delegations and the agent's own tools.
413
- const { instruction, tools } = await withSkills(config.orchestrator.instruction, own, config.orchestrator.skills, name, opts);
414
- logCapabilities(opts, name, config.orchestrator.model, config.orchestrator.tools);
415
- return specOf(config.orchestrator, name, overrideDescription || config.orchestrator.description, instruction, [...delegated, ...asTools(tools)], opts);
432
+ const orchestrator = toEngineAgent(config.orchestrator); // YAML v2 (ADR 0115), for a config built in code
433
+ const name = overrideName || orchestrator.name;
434
+ // The orchestrator's own tools resolve as a subagent's do: registry
435
+ // names, OpenAPI operations, and the MCP server's tools (narrowed by
436
+ // mcp_tools, under its mcp_auth grant), so a one-agent syndicate can reach an MCP server.
437
+ const own = gateTools(await resolveAgentTools(orchestrator.tools, orchestrator.mcp_server_url, opts, orchestrator.openapi, orchestrator.examples, orchestrator.mcp_tools, orchestrator.mcp_auth), orchestrator.require_approval, name);
438
+ const model = resolveAgentModel(orchestrator.model, opts);
439
+ // The skills toolset goes last, after the delegations and the agent's own tools; the
440
+ // skills index goes after the instruction with its model_overrides entry applied.
441
+ const { instruction, tools } = await withSkills(instructionFor(orchestrator, model.modelId), own, orchestrator.skills, name, opts);
442
+ logCapabilities(opts, name, orchestrator.model, orchestrator.tools);
443
+ const spec = specOf(orchestrator, name, overrideDescription || orchestrator.description, instruction, [...delegated, ...asTools(tools)], model);
444
+ // The syndicate's own key: a nested syndicate's orchestrator runs its delegations under its own file's (ADR 0116).
445
+ if (config.max_concurrency !== undefined)
446
+ spec.maxConcurrency = config.max_concurrency;
447
+ return spec;
416
448
  }
@@ -145,6 +145,8 @@ export function compileNative(spec) {
145
145
  agent.fallbackModel = spec.fallbackModel;
146
146
  if (spec.context)
147
147
  agent.context = spec.context;
148
+ if (spec.maxConcurrency !== undefined)
149
+ agent.maxConcurrency = spec.maxConcurrency;
148
150
  return agent;
149
151
  }
150
152
  /**