melchizedek-agents 1.1.0 → 1.3.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 (146) hide show
  1. package/.env.example +19 -0
  2. package/AGENT_SETUP.md +3 -1
  3. package/CHANGELOG.md +373 -0
  4. package/DOCUMENTATION.md +193 -21
  5. package/ONBOARDING.md +378 -0
  6. package/QUICKSTART.md +15 -2
  7. package/README.md +26 -3
  8. package/config/agents/examples/connectors.yaml +82 -0
  9. package/config/agents/examples/council.yaml +1 -1
  10. package/config/agents/examples/critic.yaml +8 -8
  11. package/config/agents/examples/image_production.yaml +4 -3
  12. package/config/agents/syndicate.schema.json +389 -33
  13. package/config/agents/syndicateSchema.yaml +18 -6
  14. package/config/agents/templates/intake_extractor.yaml +3 -2
  15. package/config/agents/templates/systems_operator.yaml +5 -2
  16. package/dist/lib/a2a/app.d.ts +12 -0
  17. package/dist/lib/a2a/app.js +25 -11
  18. package/dist/lib/a2a/executor.js +14 -6
  19. package/dist/lib/a2a/oauthSetup.d.ts +16 -0
  20. package/dist/lib/a2a/oauthSetup.js +77 -8
  21. package/dist/lib/agentDialect.d.ts +18 -8
  22. package/dist/lib/agentDialect.js +36 -19
  23. package/dist/lib/chatgpt/adapter.d.ts +50 -0
  24. package/dist/lib/chatgpt/adapter.js +107 -0
  25. package/dist/lib/chatgpt/cli.d.ts +23 -0
  26. package/dist/lib/chatgpt/cli.js +77 -0
  27. package/dist/lib/chatgpt/oauth.d.ts +103 -0
  28. package/dist/lib/chatgpt/oauth.js +448 -0
  29. package/dist/lib/chatgpt/state.d.ts +98 -0
  30. package/dist/lib/chatgpt/state.js +145 -0
  31. package/dist/lib/chatgpt/store.d.ts +24 -0
  32. package/dist/lib/chatgpt/store.js +94 -0
  33. package/dist/lib/compile.d.ts +38 -7
  34. package/dist/lib/compile.js +97 -62
  35. package/dist/lib/compileNative.d.ts +11 -3
  36. package/dist/lib/compileNative.js +18 -1
  37. package/dist/lib/dispatch.js +6 -2
  38. package/dist/lib/doctor.d.ts +107 -0
  39. package/dist/lib/doctor.js +217 -7
  40. package/dist/lib/loadSyndicate.d.ts +15 -0
  41. package/dist/lib/mcp/server.d.ts +195 -0
  42. package/dist/lib/mcp/server.js +564 -0
  43. package/dist/lib/memory/supabaseMemoryService.js +2 -2
  44. package/dist/lib/models/adapterResolver.js +6 -1
  45. package/dist/lib/models/capabilities.d.ts +9 -0
  46. package/dist/lib/models/capabilities.js +24 -0
  47. package/dist/lib/models/chatCompletionsAdapter.d.ts +11 -3
  48. package/dist/lib/models/chatCompletionsAdapter.js +19 -5
  49. package/dist/lib/models/claudeAdapter.js +4 -0
  50. package/dist/lib/models/contract.d.ts +5 -2
  51. package/dist/lib/models/gateway.d.ts +3 -2
  52. package/dist/lib/models/gateway.js +8 -4
  53. package/dist/lib/models/gatewayAdapter.d.ts +7 -1
  54. package/dist/lib/models/gatewayAdapter.js +8 -0
  55. package/dist/lib/models/geminiAdapter.js +8 -2
  56. package/dist/lib/models/genaiMapping.d.ts +8 -4
  57. package/dist/lib/models/genaiMapping.js +23 -9
  58. package/dist/lib/models/gptAdapter.d.ts +4 -2
  59. package/dist/lib/models/gptAdapter.js +11 -4
  60. package/dist/lib/models/grokAdapter.d.ts +3 -1
  61. package/dist/lib/models/grokAdapter.js +11 -3
  62. package/dist/lib/models/kimiAdapter.d.ts +2 -1
  63. package/dist/lib/models/kimiAdapter.js +3 -3
  64. package/dist/lib/models/reasoning.d.ts +30 -2
  65. package/dist/lib/models/reasoning.js +56 -5
  66. package/dist/lib/onboarding.d.ts +135 -0
  67. package/dist/lib/onboarding.js +578 -0
  68. package/dist/lib/runtime/approvals.d.ts +2 -0
  69. package/dist/lib/runtime/approvals.js +4 -3
  70. package/dist/lib/runtime/credentials.d.ts +6 -0
  71. package/dist/lib/runtime/native/agentLoop.d.ts +5 -2
  72. package/dist/lib/runtime/native/agentLoop.js +2 -0
  73. package/dist/lib/runtime/native/checkpoint.d.ts +12 -5
  74. package/dist/lib/runtime/native/checkpoint.js +13 -19
  75. package/dist/lib/runtime/native/delegate.d.ts +82 -1
  76. package/dist/lib/runtime/native/delegate.js +157 -2
  77. package/dist/lib/runtime/native/interrupts.d.ts +88 -5
  78. package/dist/lib/runtime/native/interrupts.js +220 -30
  79. package/dist/lib/runtime/syndicateTurn.d.ts +4 -2
  80. package/dist/lib/runtime/syndicateTurn.js +324 -77
  81. package/dist/lib/runtime/turnControl.d.ts +41 -0
  82. package/dist/lib/session/transcript.d.ts +9 -0
  83. package/dist/lib/session/transcript.js +20 -1
  84. package/dist/lib/skills.d.ts +18 -0
  85. package/dist/lib/skills.js +73 -1
  86. package/dist/lib/storage/postgres/index.d.ts +6 -1
  87. package/dist/lib/storage/postgres/index.js +1 -1
  88. package/dist/lib/storage/postgres/taskQueue.d.ts +7 -3
  89. package/dist/lib/storage/postgres/taskQueue.js +10 -3
  90. package/dist/lib/syndicateSchema.d.ts +184 -9
  91. package/dist/lib/syndicateSchema.js +143 -29
  92. package/dist/lib/tools/auth.d.ts +13 -1
  93. package/dist/lib/tools/auth.js +10 -2
  94. package/dist/lib/tools/credentialHosts.d.ts +88 -0
  95. package/dist/lib/tools/credentialHosts.js +165 -0
  96. package/dist/lib/tools/credentialUses.d.ts +98 -0
  97. package/dist/lib/tools/credentialUses.js +120 -0
  98. package/dist/lib/tools/generateImageTool.js +2 -1
  99. package/dist/lib/tools/inspectImageTool.js +1 -1
  100. package/dist/lib/tools/mcpToolFactory.d.ts +29 -1
  101. package/dist/lib/tools/mcpToolFactory.js +72 -26
  102. package/dist/lib/tools/mcpTransports.d.ts +12 -0
  103. package/dist/lib/tools/mcpTransports.js +11 -0
  104. package/dist/lib/tools/oauthConsent.d.ts +18 -3
  105. package/dist/lib/tools/oauthConsent.js +57 -15
  106. package/dist/lib/tools/oauthDiscovery.d.ts +202 -0
  107. package/dist/lib/tools/oauthDiscovery.js +616 -0
  108. package/dist/lib/tools/oauthHosts.d.ts +2 -0
  109. package/dist/lib/tools/oauthHosts.js +2 -2
  110. package/dist/lib/tools/oauthTools.d.ts +54 -13
  111. package/dist/lib/tools/oauthTools.js +106 -23
  112. package/dist/lib/tools/openapiTools.d.ts +7 -0
  113. package/dist/lib/tools/openapiTools.js +49 -3
  114. package/dist/lib/tools/taskTools.d.ts +32 -2
  115. package/dist/lib/tools/taskTools.js +93 -33
  116. package/dist/lib/tools/xApiSearchTool.js +2 -2
  117. package/dist/lib/workflow/turn.d.ts +22 -3
  118. package/dist/lib/workflow/turn.js +151 -31
  119. package/dist/lib/workflow.d.ts +10 -7
  120. package/dist/lib/workflow.js +10 -7
  121. package/dist/scripts/a2a_server.d.ts +2 -2
  122. package/dist/scripts/a2a_server.js +19 -2
  123. package/dist/scripts/assistant_worker.d.ts +2 -0
  124. package/dist/scripts/assistant_worker.js +24 -5
  125. package/dist/scripts/doctor.d.ts +4 -2
  126. package/dist/scripts/doctor.js +6 -4
  127. package/dist/scripts/mcp_server.d.ts +12 -0
  128. package/dist/scripts/mcp_server.js +345 -0
  129. package/dist/scripts/setup.d.ts +25 -0
  130. package/dist/scripts/setup.js +181 -0
  131. package/dist/scripts/skills_install.d.ts +3 -1
  132. package/dist/scripts/skills_install.js +17 -2
  133. package/dist/scripts/syndicate_chat.js +12 -3
  134. package/dist/scripts/yaml_codemod.d.ts +12 -2
  135. package/dist/scripts/yaml_codemod.js +115 -33
  136. package/package.json +8 -2
  137. package/skills/README.md +11 -2
  138. package/skills/briefs/_shared.md +1 -1
  139. package/skills/briefs/melchizedek-models.md +4 -2
  140. package/skills/briefs/skills-readme.md +3 -1
  141. package/skills/melchizedek-models/SKILL.md +6 -2
  142. package/skills/melchizedek-onboard/SKILL.md +71 -0
  143. package/skills/melchizedek-onboard-cloud/SKILL.md +38 -0
  144. package/skills/melchizedek-onboard-keys/SKILL.md +47 -0
  145. package/skills/melchizedek-onboard-local/SKILL.md +41 -0
  146. package/skills/melchizedek-onboard-serve/SKILL.md +43 -0
@@ -0,0 +1,145 @@
1
+ /**
2
+ * lib/chatgpt/state.ts — where a Sign in with ChatGPT credential lives, whether
3
+ * it is the OpenAI path, and the served-surface refusal (ADR 0126).
4
+ *
5
+ * WHY a leaf (node:fs, node:os, node:path only):
6
+ * The gateway's transport rule (lib/models/gateway.ts), the doctor, the
7
+ * resolver and every served bin ask the same two questions — is a sign-in
8
+ * stored, and does it carry OpenAI ids here — so the answer lives in one
9
+ * module with no adapter or SDK in its import graph.
10
+ *
11
+ * LOCAL ONLY:
12
+ * OpenAI's plan-usage flow is for open-source apps that run on the user's
13
+ * own machine (developers.openai.com/siwc/token-sharing-open-source). The
14
+ * A2A server, the worker and any other served surface call
15
+ * `refuseChatGptSignInOnServedSurface` at startup and stop while the
16
+ * sign-in would carry OpenAI ids; `markServedSurface` also makes the
17
+ * adapter refuse at call time, so a surface that skipped the startup check
18
+ * still never spends a person's ChatGPT plan on someone else's request.
19
+ *
20
+ * WHAT IT NEVER DOES: return or print a token. The status carries presence,
21
+ * times and the file's path.
22
+ */
23
+ import { existsSync, readFileSync, statSync } from 'node:fs';
24
+ import { homedir } from 'node:os';
25
+ import { dirname, join, resolve } from 'node:path';
26
+ /** Where the credential file lives, when not the default (a path; never a value). */
27
+ export const CHATGPT_SIGNIN_FILE_ENV = 'MELCHIZEDEK_CHATGPT_SIGNIN_FILE';
28
+ /** `off` ignores a stored sign-in (a served process on a machine that also signed in). */
29
+ export const CHATGPT_SIGNIN_ENV = 'MELCHIZEDEK_CHATGPT_SIGNIN';
30
+ /** The scope that lets the token use the person's ChatGPT plan. */
31
+ export const PLAN_SCOPE = 'chatgpt.tokens.use.direct';
32
+ /** The credential file: MELCHIZEDEK_CHATGPT_SIGNIN_FILE, else ~/.melchizedek/chatgpt-signin.json. */
33
+ export function signInFile(env = process.env) {
34
+ const set = env[CHATGPT_SIGNIN_FILE_ENV]?.trim();
35
+ return set ? resolve(set) : join(homedir(), '.melchizedek', 'chatgpt-signin.json');
36
+ }
37
+ /** The git work tree a path sits inside, if any (an ancestor holding `.git`). */
38
+ export function insideGitWorkTree(file) {
39
+ let dir = dirname(resolve(file));
40
+ for (;;) {
41
+ if (existsSync(join(dir, '.git')))
42
+ return dir;
43
+ const parent = dirname(dir);
44
+ if (parent === dir)
45
+ return undefined;
46
+ dir = parent;
47
+ }
48
+ }
49
+ /** Whether a parsed value has the stored shape. */
50
+ export function isStoredSignIn(v) {
51
+ if (!v || typeof v !== 'object')
52
+ return false;
53
+ const o = v;
54
+ return o.version === 1 && typeof o.issuer === 'string' && typeof o.hostId === 'string';
55
+ }
56
+ /**
57
+ * Read the credential file: undefined when absent; throws, naming the file
58
+ * and never its content, when it is readable by others or malformed.
59
+ */
60
+ export function readStoredSignIn(file) {
61
+ if (!existsSync(file))
62
+ return undefined;
63
+ if (process.platform !== 'win32' && (statSync(file).mode & 0o077) !== 0) {
64
+ throw new Error(`${file} is readable by other users; run \`chmod 600 ${file}\` (or sign in again).`);
65
+ }
66
+ let parsed;
67
+ try {
68
+ parsed = JSON.parse(readFileSync(file, 'utf-8'));
69
+ }
70
+ catch {
71
+ throw new Error(`${file} is not a Sign in with ChatGPT credential file; delete it and sign in again.`);
72
+ }
73
+ if (!isStoredSignIn(parsed))
74
+ throw new Error(`${file} is not a Sign in with ChatGPT credential file; delete it and sign in again.`);
75
+ return parsed;
76
+ }
77
+ /** Signed in: a refresh token, and the plan scope granted. */
78
+ export function isSignedIn(s) {
79
+ return !!s?.refreshToken && !!s.accessToken && (s.scopes ?? []).includes(PLAN_SCOPE);
80
+ }
81
+ /** The stored sign-in's status under this environment. */
82
+ export function chatGptSignInStatus(env = process.env) {
83
+ const file = signInFile(env);
84
+ const enabled = env[CHATGPT_SIGNIN_ENV]?.trim().toLowerCase() !== 'off';
85
+ const present = existsSync(file);
86
+ if (!present)
87
+ return { enabled, file, present, signedIn: false };
88
+ try {
89
+ const s = readStoredSignIn(file);
90
+ const signedIn = isSignedIn(s);
91
+ return { enabled, file, present, signedIn, ...(signedIn && s?.expiresAt ? { expiresAt: s.expiresAt } : {}) };
92
+ }
93
+ catch (err) {
94
+ return { enabled, file, present, signedIn: false, problem: err instanceof Error ? err.message : String(err) };
95
+ }
96
+ }
97
+ /**
98
+ * Whether OpenAI ids would run on the sign-in here: it is enabled and signed
99
+ * in, no OPENAI_API_KEY is set, and OpenAI is on its own API (no
100
+ * OPENAI_PLATFORM other than direct, no OPENAI_BASE_URL proxy, which must
101
+ * never receive the token).
102
+ */
103
+ export function chatGptSignInRoutesOpenAi(env = process.env) {
104
+ if (env.OPENAI_API_KEY?.trim())
105
+ return false;
106
+ const platform = env.OPENAI_PLATFORM?.trim().toLowerCase();
107
+ if (platform && platform !== 'direct')
108
+ return false;
109
+ if (env.OPENAI_BASE_URL?.trim())
110
+ return false;
111
+ const status = chatGptSignInStatus(env);
112
+ return status.enabled && status.signedIn;
113
+ }
114
+ // ── Local only ───────────────────────────────────────────────────────────────
115
+ let served;
116
+ /** Mark this process a served surface: the sign-in adapter refuses every call from here on. */
117
+ export function markServedSurface(surface) {
118
+ served ??= surface;
119
+ }
120
+ /** The served surface this process was marked as, if any. */
121
+ export function servedSurface() {
122
+ return served;
123
+ }
124
+ /** Test seam: forget the mark. */
125
+ export function resetServedSurfaceForTests() {
126
+ served = undefined;
127
+ }
128
+ /** The message a served surface stops with. */
129
+ export function servedSurfaceMessage(surface, env = process.env) {
130
+ return (`${surface} will not start: Sign in with ChatGPT is local only, and it is the OpenAI path here ` +
131
+ `(a sign-in is stored at ${signInFile(env)} and OPENAI_API_KEY is not set). ` +
132
+ `A served surface answers other people, and a ChatGPT plan pays only for its owner's own use on their own machine. ` +
133
+ `Set OPENAI_API_KEY (or Azure OpenAI), or set ${CHATGPT_SIGNIN_ENV}=off for this process, ` +
134
+ `or sign out with \`melchizedek-setup --chatgpt-signout\`.`);
135
+ }
136
+ /**
137
+ * Called by every served surface at startup: marks the process served, and
138
+ * throws when the sign-in would carry OpenAI ids here. The A2A app, the A2A
139
+ * server bin and the worker call it; a new served surface calls it too.
140
+ */
141
+ export function refuseChatGptSignInOnServedSurface(surface, env = process.env) {
142
+ markServedSurface(surface);
143
+ if (chatGptSignInRoutesOpenAi(env))
144
+ throw new Error(servedSurfaceMessage(surface, env));
145
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * lib/chatgpt/store.ts — writing the Sign in with ChatGPT credential file and
3
+ * serializing its refreshes (ADR 0126).
4
+ *
5
+ * The file is the person's own: mode 600 in a mode-700 directory, outside any
6
+ * git work tree, replaced atomically (a new mode-600 file renamed over the
7
+ * old one). OpenAI's refresh tokens rotate, so two processes must not refresh
8
+ * the same session at once (developers.openai.com/siwc/token-sharing-open-source/
9
+ * profiles-and-sessions): a lock file beside the credential serializes them.
10
+ */
11
+ import type { StoredSignIn } from './state.ts';
12
+ /** Write the credential file: mode 600, atomically, never inside a git work tree. */
13
+ export declare function writeStoredSignIn(file: string, value: StoredSignIn): void;
14
+ /** The stored sign-in with its tokens removed; the host id and the issued client id stay (OpenAI asks apps to reuse them). */
15
+ export declare function withoutTokens(s: StoredSignIn): StoredSignIn;
16
+ /** Remove the tokens from the file, keeping the registration. */
17
+ export declare function clearTokens(file: string): void;
18
+ /**
19
+ * Run `fn` holding the credential's lock file (created exclusively). A lock
20
+ * older than a minute is a crashed holder's and is taken over.
21
+ */
22
+ export declare function withSignInLock<T>(file: string, fn: () => Promise<T>, opts?: {
23
+ waitMs?: number;
24
+ }): Promise<T>;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * lib/chatgpt/store.ts — writing the Sign in with ChatGPT credential file and
3
+ * serializing its refreshes (ADR 0126).
4
+ *
5
+ * The file is the person's own: mode 600 in a mode-700 directory, outside any
6
+ * git work tree, replaced atomically (a new mode-600 file renamed over the
7
+ * old one). OpenAI's refresh tokens rotate, so two processes must not refresh
8
+ * the same session at once (developers.openai.com/siwc/token-sharing-open-source/
9
+ * profiles-and-sessions): a lock file beside the credential serializes them.
10
+ */
11
+ import { randomUUID } from 'node:crypto';
12
+ import { chmodSync, closeSync, existsSync, mkdirSync, openSync, renameSync, rmSync, statSync, writeSync } from 'node:fs';
13
+ import { dirname } from 'node:path';
14
+ import { setTimeout as delay } from 'node:timers/promises';
15
+ import { insideGitWorkTree, readStoredSignIn } from './state.js';
16
+ /** Write the credential file: mode 600, atomically, never inside a git work tree. */
17
+ export function writeStoredSignIn(file, value) {
18
+ const repo = insideGitWorkTree(file);
19
+ if (repo) {
20
+ throw new Error(`refusing to store a ChatGPT sign-in inside the git work tree ${repo}; set MELCHIZEDEK_CHATGPT_SIGNIN_FILE to a path outside it.`);
21
+ }
22
+ const dir = dirname(file);
23
+ if (!existsSync(dir))
24
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
25
+ const tmp = `${file}.${randomUUID()}.tmp`;
26
+ const fd = openSync(tmp, 'wx', 0o600);
27
+ try {
28
+ writeSync(fd, `${JSON.stringify(value, null, 2)}\n`);
29
+ }
30
+ finally {
31
+ closeSync(fd);
32
+ }
33
+ try {
34
+ if (process.platform !== 'win32')
35
+ chmodSync(tmp, 0o600);
36
+ renameSync(tmp, file);
37
+ }
38
+ catch (err) {
39
+ rmSync(tmp, { force: true });
40
+ throw err;
41
+ }
42
+ }
43
+ /** The stored sign-in with its tokens removed; the host id and the issued client id stay (OpenAI asks apps to reuse them). */
44
+ export function withoutTokens(s) {
45
+ const { accessToken: _a, refreshToken: _r, expiresAt: _e, earliestRefreshAt: _n, scopes: _s, ...kept } = s;
46
+ return { ...kept, savedAt: new Date().toISOString() };
47
+ }
48
+ /** Remove the tokens from the file, keeping the registration. */
49
+ export function clearTokens(file) {
50
+ const s = readStoredSignIn(file);
51
+ if (s)
52
+ writeStoredSignIn(file, withoutTokens(s));
53
+ }
54
+ const LOCK_STALE_MS = 60_000;
55
+ const LOCK_WAIT_MS = 20_000;
56
+ /**
57
+ * Run `fn` holding the credential's lock file (created exclusively). A lock
58
+ * older than a minute is a crashed holder's and is taken over.
59
+ */
60
+ export async function withSignInLock(file, fn, opts = {}) {
61
+ const lock = `${file}.lock`;
62
+ const dir = dirname(file);
63
+ if (!existsSync(dir))
64
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
65
+ const deadline = Date.now() + (opts.waitMs ?? LOCK_WAIT_MS);
66
+ for (;;) {
67
+ try {
68
+ closeSync(openSync(lock, 'wx', 0o600));
69
+ break;
70
+ }
71
+ catch (err) {
72
+ if (err.code !== 'EEXIST')
73
+ throw err;
74
+ try {
75
+ if (Date.now() - statSync(lock).mtimeMs > LOCK_STALE_MS) {
76
+ rmSync(lock, { force: true });
77
+ continue;
78
+ }
79
+ }
80
+ catch {
81
+ continue;
82
+ }
83
+ if (Date.now() > deadline)
84
+ throw new Error(`another process holds ${lock}; try again, or delete it if no melchizedek process is running.`);
85
+ await delay(100);
86
+ }
87
+ }
88
+ try {
89
+ return await fn();
90
+ }
91
+ finally {
92
+ rmSync(lock, { force: true });
93
+ }
94
+ }
@@ -110,7 +110,30 @@ export type SpecTool = {
110
110
  } | {
111
111
  kind: 'workflow';
112
112
  workflow: WorkflowSpec;
113
+ } | {
114
+ kind: 'dispatch';
115
+ dispatch: DispatchSpec;
113
116
  };
117
+ /**
118
+ * A nested dispatch (plan-dispatch) syndicate (ADR 0120): it runs as it
119
+ * runs at the top, as a turn of its own on its own conversation (its
120
+ * classifier picks a route, the route answers), so what it compiles to is
121
+ * the file and the options its turn loads its routes with. The turn runner
122
+ * runs it (lib/runtime/syndicateTurn.ts, TurnControl.nestedDispatch), as a
123
+ * delegated call, a dispatch route or a workflow node.
124
+ */
125
+ export interface DispatchSpec {
126
+ /** The entry's name, under which its caller lists it. */
127
+ name: string;
128
+ /** The entry's description: the tool's, when delegated to. */
129
+ description: string;
130
+ /** The nested syndicate, as loaded. */
131
+ config: SyndicateYamlConfig;
132
+ /** The file it came from. */
133
+ ref: string;
134
+ /** The options its routes compile with: the caller's, its nesting chain extended by `ref`. */
135
+ compile: CompileOptions;
136
+ }
114
137
  /**
115
138
  * A workflow syndicate, compiled: lib/compileNative.ts builds the walk's
116
139
  * graph and agents from it. Nested as a subagent (a `yaml_reference` in
@@ -138,6 +161,11 @@ export interface WorkflowSpec {
138
161
  yaml: SubagentYamlConfig;
139
162
  workflow: WorkflowSpec;
140
163
  }>;
164
+ /** Every node that is a `yaml_reference` to a dispatch syndicate, with its YAML entry: the walk runs it as the node, as its own turn (ADR 0120). */
165
+ dispatches?: Array<{
166
+ yaml: SubagentYamlConfig;
167
+ dispatch: DispatchSpec;
168
+ }>;
141
169
  /** The registry entry for a tool node's tool name, or undefined (CompileOptions.onUnknownTool applies). */
142
170
  resolveTool: (name: string) => unknown;
143
171
  }
@@ -186,21 +214,23 @@ export interface AgentSpec {
186
214
  * lookup for its tool nodes. `name` and `description` name a nested one
187
215
  * after its subagent entry (ADR 0098); `ref` is the file it came from.
188
216
  *
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.
217
+ * A nested workflow's pauses (an `ask_user` node, a gate) reach the turn
218
+ * wherever it runs (ADR 0111, ADR 0119). `delegated` is accepted for the
219
+ * callers that pass it and no longer changes what compiles.
193
220
  */
194
221
  export declare function compileWorkflowSpec(config: SyndicateYamlConfig, opts?: CompileOptions, name?: string, description?: string, ref?: string, delegated?: boolean): Promise<WorkflowSpec>;
195
222
  /** Every agent spec a workflow runs, its nested workflow nodes' agents included, for one adapter lookup (nativeAdapterFor). */
196
223
  export declare function workflowAgentSpecs(spec: WorkflowSpec): AgentSpec[];
197
- /** A subagent entry, compiled: one agent, or a nested workflow syndicate's whole graph (ADR 0098, ADR 0106). */
224
+ /** A subagent entry, compiled: one agent, a nested workflow syndicate's whole graph (ADR 0098, ADR 0106), or a nested dispatch syndicate's own turn (ADR 0120). */
198
225
  export type EntrySpec = {
199
226
  kind: 'agent';
200
227
  spec: AgentSpec;
201
228
  } | {
202
229
  kind: 'workflow';
203
230
  workflow: WorkflowSpec;
231
+ } | {
232
+ kind: 'dispatch';
233
+ dispatch: DispatchSpec;
204
234
  };
205
235
  /**
206
236
  * A subagent entry as what it runs: an inline agent or a nested syndicate's
@@ -208,8 +238,9 @@ export type EntrySpec = {
208
238
  * as its whole graph under the entry's name and description. A delegated
209
239
  * subagent, a dispatch route and a workflow node all compile through here,
210
240
  * 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).
241
+ * A nested workflow pauses wherever it runs (ADR 0111, ADR 0119);
242
+ * `delegated` is accepted for the callers that pass it and no longer
243
+ * changes what compiles.
213
244
  */
214
245
  export declare function compileEntrySpec(subCfg: SubagentYamlConfig, opts?: CompileOptions, delegated?: boolean): Promise<EntrySpec>;
215
246
  /**
@@ -145,10 +145,10 @@ export function examplesTool(examples) {
145
145
  const tool = examplesInstructionTool(examples);
146
146
  return tool ? [tool] : [];
147
147
  }
148
- async function resolveAgentTools(toolNames, mcpServerUrl, opts, openapi, examples, mcpAllowed, mcpAuth) {
149
- const tools = [...resolveNamedTools(toolNames, opts.onUnknownTool), ...examplesTool(examples)];
148
+ async function resolveAgentTools(agent, opts) {
149
+ const tools = [...resolveNamedTools(agent.tools, opts.onUnknownTool), ...examplesTool(agent.examples)];
150
150
  // OpenAPI operations become tools here, so require_approval can name them.
151
- for (const entry of openapi ?? []) {
151
+ for (const entry of agent.openapi ?? []) {
152
152
  const built = await buildOpenApiTools(entry);
153
153
  opts.log?.(`openapi · ${relative(process.cwd(), entry.spec) || entry.spec}: ${built.map((t) => t.name).join(', ') || '(no operations)'}`);
154
154
  for (const t of built) {
@@ -157,28 +157,43 @@ async function resolveAgentTools(toolNames, mcpServerUrl, opts, openapi, example
157
157
  tools.push(t);
158
158
  }
159
159
  }
160
- if (mcpServerUrl) {
161
- opts.log?.(`Loading MCP tools: ${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 } : {}) });
164
- // mcp_tools: only the named tools are exposed (a server's list is its
165
- // own to change); a name the server does not offer is reported, and a
166
- // gate on it fails the compile in gateTools.
167
- const allowed = mcpAllowed ? new Set(mcpAllowed) : undefined;
168
- const mcpTools = allowed ? offered.filter((t) => allowed.has(t.name)) : offered;
169
- if (allowed) {
170
- const missing = [...allowed].filter((n) => !offered.some((t) => t.name === n));
171
- if (missing.length)
172
- opts.log?.(`MCP ${mcpServerUrl} does not offer ${missing.map((n) => `'${n}'`).join(', ')} (mcp_tools)`);
173
- const hidden = offered.length - mcpTools.length;
174
- if (hidden)
175
- opts.log?.(`MCP ${mcpServerUrl}: ${hidden} tool(s) not in mcp_tools are not exposed`);
176
- }
177
- for (const mcpTool of mcpTools) {
160
+ /** One server's tools, narrowed to the names the YAML allows (a server's list is its own to change). */
161
+ const offeredBy = async (label, url, allowedNames, oauth2, transport) => {
162
+ opts.log?.(`Loading MCP tools: ${label}`);
163
+ // An authorization_code server lists nothing at startup: its tools are the allowed names, each on its user's own connection (ADR 0112).
164
+ const offered = await createMcpTools(url, { ...(allowedNames ? { tools: allowedNames } : {}), ...(oauth2 ? { oauth2 } : {}), ...(transport ? { transport } : {}) });
165
+ if (!allowedNames)
166
+ return offered;
167
+ // A name the server does not offer is reported, and a gate on it fails the compile in gateTools.
168
+ const allowed = new Set(allowedNames);
169
+ const kept = offered.filter((t) => allowed.has(t.name));
170
+ const missing = [...allowed].filter((n) => !offered.some((t) => t.name === n));
171
+ if (missing.length)
172
+ opts.log?.(`MCP ${label} does not offer ${missing.map((n) => `'${n}'`).join(', ')}`);
173
+ const hidden = offered.length - kept.length;
174
+ if (hidden)
175
+ opts.log?.(`MCP ${label}: ${hidden} tool(s) not named for this agent are not exposed`);
176
+ return kept;
177
+ };
178
+ if (agent.mcp_server_url) {
179
+ // The single-server form: a name the agent already has keeps the agent's tool.
180
+ for (const mcpTool of await offeredBy(agent.mcp_server_url, agent.mcp_server_url, agent.mcp_tools, agent.mcp_auth?.oauth2, agent.mcp_transport)) {
178
181
  if (!tools.some((t) => t.name === mcpTool.name))
179
182
  tools.push(mcpTool);
180
183
  }
181
184
  }
185
+ // mcp_servers (ADR 0124): the loader refused a declared name twice; a
186
+ // server that answers with a name already taken is refused here too, so
187
+ // no server's tool can stand in for another's or the agent's own.
188
+ const servers = await Promise.all((agent.mcp_servers ?? []).map((server) => offeredBy(`${server.name} (${server.url})`, server.url, server.tools, server.auth?.oauth2, server.transport)));
189
+ for (const [i, offered] of servers.entries()) {
190
+ const server = agent.mcp_servers[i];
191
+ for (const mcpTool of offered) {
192
+ if (tools.some((t) => t?.name === mcpTool.name))
193
+ throw new Error(`mcp_servers ${server.name}: tool '${mcpTool.name}' collides with another of this agent's tools (ADR 0124)`);
194
+ tools.push(mcpTool);
195
+ }
196
+ }
182
197
  return tools;
183
198
  }
184
199
  /**
@@ -264,31 +279,44 @@ function nestedOptions(ref, opts) {
264
279
  return { ...opts, [NESTING]: [...chain, ref] };
265
280
  }
266
281
  /**
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.
282
+ * A nested `yaml_reference:` syndicate, loaded. Every pause inside one
283
+ * reaches the turn wherever it runs (ADR 0110, ADR 0111, ADR 0119,
284
+ * ADR 0120): a nested delegate syndicate's through the open call, a nested
285
+ * workflow's through its walk, a nested dispatch syndicate's through its
286
+ * own turn, whose routes run as they run at the top. Nothing is refused
287
+ * here any more.
278
288
  */
279
- function loadNestedSyndicate(ref, opts, delegated = false) {
289
+ function loadNestedSyndicate(ref, opts) {
280
290
  opts.log?.(`Loading nested syndicate: ${ref}`);
281
- const nested = (opts.loadNested ?? loadSyndicate)(ref);
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.`);
291
+ return (opts.loadNested ?? loadSyndicate)(ref);
292
+ }
293
+ /**
294
+ * A nested dispatch syndicate as its entry runs it (ADR 0120). Its routes
295
+ * compile when its turn routes to one, as at the top, so a route that
296
+ * cannot compile fails that turn; the references below it are followed
297
+ * here (loaded, not compiled: no tool is resolved and no MCP server is
298
+ * reached), so a reference cycle or a depth past the limit is refused at
299
+ * load, by name, before any model call. A route may not carry the entry's
300
+ * name: the pause walk tells the nested syndicate from its routes by name.
301
+ */
302
+ function compileDispatchEntry(subCfg, nested, nestedOpts) {
303
+ const ref = subCfg.yaml_reference;
304
+ for (const route of nested.subagents ?? []) {
305
+ if (route.name === subCfg.name) {
306
+ throw new Error(`${ref}: the route '${route.name}' has the name its caller gives the nested syndicate; rename the route or the entry.`);
286
307
  }
287
308
  }
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.`);
309
+ followReferences(nested, nestedOpts);
310
+ return { name: subCfg.name, description: subCfg.description ?? '', config: nested, ref, compile: nestedOpts };
311
+ }
312
+ /** Loads every `yaml_reference` below `config`, depth first, through nestedOptions: a cycle or a chain past the limit throws by name. */
313
+ function followReferences(config, opts) {
314
+ for (const entry of config.subagents ?? []) {
315
+ if (!entry.yaml_reference || entry.a2a_agent_url)
316
+ continue;
317
+ const below = nestedOptions(entry.yaml_reference, opts);
318
+ followReferences(loadNestedSyndicate(entry.yaml_reference, below), below);
290
319
  }
291
- return nested;
292
320
  }
293
321
  /**
294
322
  * The spec of a workflow syndicate: every agent through compileSubagentSpec
@@ -296,35 +324,34 @@ function loadNestedSyndicate(ref, opts, delegated = false) {
296
324
  * lookup for its tool nodes. `name` and `description` name a nested one
297
325
  * after its subagent entry (ADR 0098); `ref` is the file it came from.
298
326
  *
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.
327
+ * A nested workflow's pauses (an `ask_user` node, a gate) reach the turn
328
+ * wherever it runs (ADR 0111, ADR 0119). `delegated` is accepted for the
329
+ * callers that pass it and no longer changes what compiles.
303
330
  */
304
331
  export async function compileWorkflowSpec(config, opts = {}, name, description, ref, delegated = false) {
305
332
  if (!isWorkflowSyndicate(config))
306
333
  throw new Error(`${config.syndicate_name}: no workflow block`);
307
- if (name && !delegated) {
308
- const asking = Object.entries(config.workflow.nodes ?? {}).find(([, node]) => nodeKind(node) === 'ask_user');
309
- if (asking) {
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.`);
311
- }
312
- }
313
334
  const workflowName = name || config.syndicate_name;
314
335
  const agents = [];
315
336
  const workflows = [];
337
+ const dispatches = [];
316
338
  for (const yaml of [{ description: '', ...config.orchestrator }, ...(config.subagents ?? [])]) {
317
339
  const entry = await compileEntrySpec(yaml, opts);
318
340
  if (entry.kind === 'workflow')
319
341
  workflows.push({ yaml, workflow: entry.workflow });
342
+ else if (entry.kind === 'dispatch')
343
+ dispatches.push({ yaml, dispatch: entry.dispatch });
320
344
  else
321
345
  agents.push({ yaml, spec: entry.spec });
322
346
  }
323
- // A map runs one agent per item; a nested graph is not an agent a map item can be (ADR 0106).
347
+ // A map runs one agent per item; a nested graph or a nested dispatch syndicate is not an agent a map item can be (ADR 0106, ADR 0120).
324
348
  for (const [node, entry] of Object.entries(config.workflow.nodes ?? {})) {
325
- const mapped = nodeKind(entry) === 'map' ? workflows.find((w) => w.yaml.name === entry.map) : undefined;
349
+ if (nodeKind(entry) !== 'map')
350
+ continue;
351
+ const mapped = workflows.find((w) => w.yaml.name === entry.map) ?? dispatches.find((d) => d.yaml.name === entry.map);
326
352
  if (mapped) {
327
- throw new Error(`${ref ?? config.syndicate_name}: the map node '${node}' runs '${mapped.yaml.name}', a workflow syndicate (${mapped.yaml.yaml_reference}); a map runs one agent per item, so make the workflow a node of its own.`);
353
+ const what = 'workflow' in mapped ? 'a workflow syndicate' : 'a dispatch syndicate';
354
+ throw new Error(`${ref ?? config.syndicate_name}: the map node '${node}' runs '${mapped.yaml.name}', ${what} (${mapped.yaml.yaml_reference}); a map runs one agent per item, so make it a node of its own.`);
328
355
  }
329
356
  }
330
357
  return {
@@ -333,6 +360,7 @@ export async function compileWorkflowSpec(config, opts = {}, name, description,
333
360
  config: { ...config, syndicate_name: workflowName },
334
361
  agents,
335
362
  workflows,
363
+ dispatches,
336
364
  resolveTool: (tool) => resolveNamedTools([tool], opts.onUnknownTool)[0],
337
365
  };
338
366
  }
@@ -346,16 +374,20 @@ export function workflowAgentSpecs(spec) {
346
374
  * as its whole graph under the entry's name and description. A delegated
347
375
  * subagent, a dispatch route and a workflow node all compile through here,
348
376
  * 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).
377
+ * A nested workflow pauses wherever it runs (ADR 0111, ADR 0119);
378
+ * `delegated` is accepted for the callers that pass it and no longer
379
+ * changes what compiles.
351
380
  */
352
381
  export async function compileEntrySpec(subCfg, opts = {}, delegated = false) {
353
382
  if (subCfg.yaml_reference && !subCfg.a2a_agent_url) {
354
383
  const nestedOpts = nestedOptions(subCfg.yaml_reference, opts);
355
- const nested = loadNestedSyndicate(subCfg.yaml_reference, nestedOpts, delegated);
384
+ const nested = loadNestedSyndicate(subCfg.yaml_reference, nestedOpts);
356
385
  if (isWorkflowSyndicate(nested)) {
357
386
  return { kind: 'workflow', workflow: await compileWorkflowSpec(nested, nestedOpts, subCfg.name, subCfg.description, subCfg.yaml_reference, delegated) };
358
387
  }
388
+ // A nested dispatch syndicate classifies and routes as at the top (ADR 0120), wherever it runs.
389
+ if (isDispatchSyndicate(nested))
390
+ return { kind: 'dispatch', dispatch: compileDispatchEntry(subCfg, nested, nestedOpts) };
359
391
  return { kind: 'agent', spec: await compileSpec(nested, nestedOpts, subCfg.name, subCfg.description) };
360
392
  }
361
393
  return { kind: 'agent', spec: await compileSubagentSpec(subCfg, opts) };
@@ -389,11 +421,14 @@ export async function compileSubagentSpec(subCfg, opts = {}) {
389
421
  if (isWorkflowSyndicate(nested)) {
390
422
  throw new Error(`${subCfg.yaml_reference}: '${subCfg.name}' is a workflow syndicate, which runs as its whole graph, not as one agent; compile the entry with compileEntrySpec (ADR 0106).`);
391
423
  }
424
+ if (isDispatchSyndicate(nested)) {
425
+ throw new Error(`${subCfg.yaml_reference}: '${subCfg.name}' is a dispatch syndicate, which runs as its own turn (its classifier, then a route), not as one agent; compile the entry with compileEntrySpec (ADR 0120).`);
426
+ }
392
427
  return compileSpec(nested, nestedOpts, subCfg.name, subCfg.description);
393
428
  }
394
429
  // YAML v2 (ADR 0115): the engine form, for a config built in code (a loaded one already is).
395
430
  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);
431
+ const gated = gateTools(await resolveAgentTools(agent, opts), agent.require_approval, agent.name);
397
432
  const model = resolveAgentModel(agent.model, opts);
398
433
  // model_overrides for the provider the agent runs on, before the skills index is appended.
399
434
  const { instruction, tools } = await withSkills(instructionFor(agent, model.modelId), gated, agent.skills, agent.name, opts);
@@ -427,14 +462,14 @@ export async function compileSpec(config, opts = {}, overrideName, overrideDescr
427
462
  }
428
463
  // A delegated subagent: a pause inside it, a nested workflow's included, reaches the caller (ADR 0110, ADR 0111).
429
464
  const entry = await compileEntrySpec(subCfg, opts, true);
430
- return entry.kind === 'workflow' ? entry : { kind: 'agent', agent: entry.spec };
465
+ return entry.kind === 'agent' ? { kind: 'agent', agent: entry.spec } : entry;
431
466
  }));
432
467
  const orchestrator = toEngineAgent(config.orchestrator); // YAML v2 (ADR 0115), for a config built in code
433
468
  const name = overrideName || orchestrator.name;
434
469
  // 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);
470
+ // names, OpenAPI operations, and the MCP servers' tools (narrowed by
471
+ // mcp_tools or each mcp_servers entry's tools, under its grant), so a one-agent syndicate can reach an MCP server.
472
+ const own = gateTools(await resolveAgentTools(orchestrator, opts), orchestrator.require_approval, name);
438
473
  const model = resolveAgentModel(orchestrator.model, opts);
439
474
  // The skills toolset goes last, after the delegations and the agent's own tools; the
440
475
  // skills index goes after the instruction with its model_overrides entry applied.
@@ -32,13 +32,14 @@
32
32
  * caller's. ADK classes are told apart by ADK's own Symbol.for marks, so
33
33
  * this file imports nothing from ADK.
34
34
  */
35
- import type { AgentSpec, CompileOptions, WorkflowSpec } from './compile.ts';
35
+ import type { AgentSpec, CompileOptions, DispatchSpec, WorkflowSpec } from './compile.ts';
36
36
  import type { SubagentYamlConfig, SyndicateYamlConfig } from './loadSyndicate.ts';
37
37
  import type { ModelAdapter } from './models/contract.ts';
38
38
  import type { WorkflowSubagent } from './runtime/native/delegate.ts';
39
39
  import type { NativeAgent } from './runtime/native/request.ts';
40
40
  export { UnsupportedOnRuntimeError, unsupportedOnNative } from './runtime/runtimeFlag.ts';
41
41
  import type { WorkflowGraph } from './workflow/graph.ts';
42
+ import type { NestedRun } from './workflow/turn.ts';
42
43
  /**
43
44
  * The class name of an ADK model the runtime cannot run, else undefined:
44
45
  * any ADK BaseLlm (ADR 0107; the ADK runtime that ran one left in 1.0.0).
@@ -67,8 +68,8 @@ export interface NativeWorkflow {
67
68
  graph: WorkflowGraph;
68
69
  agents: Map<string, NativeAgent>;
69
70
  resolveTool: (name: string) => unknown;
70
- /** The nodes that are a nested workflow syndicate, each compiled the same way, by YAML name (ADR 0106). */
71
- workflows: Map<string, NativeWorkflow>;
71
+ /** The nodes that are a nested workflow syndicate, each compiled the same way (ADR 0106), or a nested dispatch syndicate, its own turn (ADR 0120), by YAML name. */
72
+ workflows: Map<string, NativeWorkflow | NestedRun>;
72
73
  }
73
74
  /**
74
75
  * A workflow spec for the walk: the graph, every agent compiled from its
@@ -82,6 +83,13 @@ export declare function compileNativeWorkflow(spec: WorkflowSpec): NativeWorkflo
82
83
  * session lib/runtime/native/delegate.ts opened.
83
84
  */
84
85
  export declare function workflowSubagentOf(workflow: NativeWorkflow): WorkflowSubagent;
86
+ /**
87
+ * A nested dispatch syndicate as the delegated subagent the native loop
88
+ * calls, or the node a walk runs (ADR 0120): one call runs it as its own
89
+ * turn (nestedDispatchWalk) on the child session lib/runtime/native/delegate.ts
90
+ * (or lib/workflow/turn.ts) opened.
91
+ */
92
+ export declare function dispatchSubagentOf(dispatch: DispatchSpec): WorkflowSubagent & NestedRun;
85
93
  /** One subagent entry (a dispatch route) for the native loop: compileSubagentSpec, then compileNative. */
86
94
  export declare function compileNativeSubagent(subCfg: SubagentYamlConfig, opts?: CompileOptions): Promise<NativeAgent>;
87
95
  /** What nativeAdapterFor reads from a spec: its model resolution, the other ids it calls, and its delegated subagents. */