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.
- package/.env.example +19 -2
- package/CHANGELOG.md +333 -0
- package/DOCUMENTATION.md +99 -39
- package/README.md +1 -0
- package/config/agents/README.md +2 -2
- package/config/agents/examples/ares.yaml +6 -10
- package/config/agents/examples/assistant.yaml +6 -6
- package/config/agents/examples/augustin.yaml +9 -15
- package/config/agents/examples/cartographers.yaml +6 -6
- package/config/agents/examples/council.yaml +6 -6
- package/config/agents/examples/critic.yaml +16 -16
- package/config/agents/examples/harness.yaml +4 -4
- package/config/agents/examples/image_production.yaml +33 -32
- package/config/agents/examples/librarian.yaml +4 -4
- package/config/agents/examples/patient_advocate.yaml +6 -10
- package/config/agents/examples/pipeline.yaml +20 -19
- package/config/agents/examples/research.yaml +36 -43
- package/config/agents/examples/scribe.yaml +31 -32
- package/config/agents/examples/scriptorium.yaml +6 -6
- package/config/agents/examples/structured_critic.yaml +82 -0
- package/config/agents/examples/style_council.yaml +9 -11
- package/config/agents/examples/syndicate.yaml +3 -5
- package/config/agents/examples/tutor.yaml +2 -2
- package/config/agents/examples/weather.yaml +2 -2
- package/config/agents/syndicate.schema.json +1979 -0
- package/config/agents/syndicateSchema.yaml +109 -72
- package/config/agents/templates/README.md +1 -1
- package/config/agents/templates/account_memory.yaml +4 -4
- package/config/agents/templates/case_desk.yaml +6 -6
- package/config/agents/templates/conversational.yaml +2 -2
- package/config/agents/templates/draft_review.yaml +6 -6
- package/config/agents/templates/intake_extractor.yaml +61 -60
- package/config/agents/templates/research_brief.yaml +6 -6
- package/config/agents/templates/research_desk.yaml +6 -6
- package/config/agents/templates/review_panel.yaml +8 -8
- package/config/agents/templates/support_triage.yaml +30 -29
- package/config/agents/templates/systems_operator.yaml +21 -5
- package/db/migrations/0014_durable_runs.sql +31 -0
- package/dist/lib/a2a/app.d.ts +15 -0
- package/dist/lib/a2a/app.js +53 -5
- package/dist/lib/a2a/executor.d.ts +7 -1
- package/dist/lib/a2a/executor.js +34 -4
- package/dist/lib/a2a/oauthSetup.d.ts +90 -0
- package/dist/lib/a2a/oauthSetup.js +205 -0
- package/dist/lib/agentDialect.d.ts +66 -0
- package/dist/lib/agentDialect.js +134 -0
- package/dist/lib/compile.d.ts +10 -5
- package/dist/lib/compile.js +69 -37
- package/dist/lib/compileNative.js +2 -0
- package/dist/lib/dispatch.d.ts +10 -5
- package/dist/lib/dispatch.js +10 -5
- package/dist/lib/doctor.d.ts +34 -1
- package/dist/lib/doctor.js +47 -3
- package/dist/lib/loadSyndicate.d.ts +45 -2
- package/dist/lib/loadSyndicate.js +22 -2
- package/dist/lib/memory/namespace.d.ts +6 -5
- package/dist/lib/memory/namespace.js +6 -5
- package/dist/lib/memory/providers.js +3 -4
- package/dist/lib/models/capabilities.d.ts +16 -1
- package/dist/lib/models/capabilities.js +55 -1
- package/dist/lib/observability/tracer.d.ts +8 -1
- package/dist/lib/observability/tracer.js +33 -5
- package/dist/lib/runtime/approvals.d.ts +6 -0
- package/dist/lib/runtime/native/agentLoop.d.ts +18 -1
- package/dist/lib/runtime/native/agentLoop.js +105 -11
- package/dist/lib/runtime/native/checkpoint.d.ts +164 -0
- package/dist/lib/runtime/native/checkpoint.js +365 -0
- package/dist/lib/runtime/native/delegate.d.ts +141 -18
- package/dist/lib/runtime/native/delegate.js +235 -39
- package/dist/lib/runtime/native/interrupts.d.ts +101 -2
- package/dist/lib/runtime/native/interrupts.js +201 -1
- package/dist/lib/runtime/native/request.d.ts +13 -3
- package/dist/lib/runtime/native/request.js +28 -4
- package/dist/lib/runtime/native/telemetry.js +10 -15
- package/dist/lib/runtime/questions.d.ts +7 -4
- package/dist/lib/runtime/sessions.d.ts +1 -1
- package/dist/lib/runtime/syndicateTurn.d.ts +7 -1
- package/dist/lib/runtime/syndicateTurn.js +74 -22
- package/dist/lib/runtime/turnControl.d.ts +15 -0
- package/dist/lib/runtime/turnControl.js +10 -0
- package/dist/lib/storage/postgres/index.d.ts +6 -1
- package/dist/lib/storage/postgres/index.js +7 -2
- package/dist/lib/storage/postgres/taskQueue.d.ts +14 -2
- package/dist/lib/storage/postgres/taskQueue.js +20 -3
- package/dist/lib/storage/postgres/taskStore.d.ts +26 -2
- package/dist/lib/storage/postgres/taskStore.js +74 -8
- package/dist/lib/syndicateSchema.d.ts +278 -0
- package/dist/lib/syndicateSchema.js +182 -15
- package/dist/lib/tools/auth.d.ts +7 -5
- package/dist/lib/tools/auth.js +12 -4
- package/dist/lib/tools/credentialEnv.d.ts +28 -0
- package/dist/lib/tools/credentialEnv.js +52 -0
- package/dist/lib/tools/credentialStore.js +9 -2
- package/dist/lib/tools/mcpToolFactory.d.ts +25 -2
- package/dist/lib/tools/mcpToolFactory.js +179 -21
- package/dist/lib/tools/oauthHosts.d.ts +71 -0
- package/dist/lib/tools/oauthHosts.js +154 -0
- package/dist/lib/tools/oauthTools.d.ts +233 -0
- package/dist/lib/tools/oauthTools.js +421 -0
- package/dist/lib/tools/openapiTools.d.ts +4 -6
- package/dist/lib/tools/openapiTools.js +40 -36
- package/dist/lib/tools/taskTools.d.ts +30 -5
- package/dist/lib/tools/taskTools.js +74 -10
- package/dist/lib/workflow.d.ts +8 -4
- package/dist/lib/workflow.js +8 -4
- package/dist/lib/workflowConfig.d.ts +6 -0
- package/dist/scripts/a2a_server.d.ts +36 -0
- package/dist/scripts/a2a_server.js +122 -1
- package/dist/scripts/assistant_worker.d.ts +10 -0
- package/dist/scripts/assistant_worker.js +69 -19
- package/dist/scripts/doctor.d.ts +4 -1
- package/dist/scripts/doctor.js +5 -2
- package/dist/scripts/yaml_codemod.d.ts +52 -0
- package/dist/scripts/yaml_codemod.js +570 -0
- package/package.json +11 -7
- package/skills/briefs/melchizedek-author.md +7 -5
- package/skills/briefs/melchizedek-models.md +4 -3
- package/skills/briefs/melchizedek.md +1 -0
- package/skills/melchizedek/SKILL.md +1 -0
- package/skills/melchizedek-author/SKILL.md +7 -4
- package/skills/melchizedek-author/assets/minimal.yaml +13 -13
- 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
|
+
}
|
package/dist/lib/compile.d.ts
CHANGED
|
@@ -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
|
|
188
|
-
*
|
|
189
|
-
* `ask_user` node is refused
|
|
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
|
package/dist/lib/compile.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
205
|
-
const resolvedModel = (opts.resolveModel ?? ((m) => m))(
|
|
206
|
-
|
|
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
|
-
|
|
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 (
|
|
272
|
-
|
|
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
|
|
283
|
-
*
|
|
284
|
-
* `ask_user` node is refused
|
|
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
|
|
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
|
-
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
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
|
|
408
|
-
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
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
|
/**
|