melchizedek-agents 0.19.0 → 0.20.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 (106) hide show
  1. package/CHANGELOG.md +252 -76
  2. package/DOCUMENTATION.md +44 -15
  3. package/QUICKSTART.md +9 -6
  4. package/README.md +6 -9
  5. package/config/agents/syndicateSchema.yaml +3 -0
  6. package/dist/lib/adkPeer.d.ts +63 -0
  7. package/dist/lib/adkPeer.js +240 -0
  8. package/dist/lib/compile.d.ts +40 -2
  9. package/dist/lib/compile.js +80 -12
  10. package/dist/lib/compileAdk.d.ts +7 -4
  11. package/dist/lib/compileAdk.js +22 -12
  12. package/dist/lib/compileNative.d.ts +28 -2
  13. package/dist/lib/compileNative.js +60 -4
  14. package/dist/lib/doctor.d.ts +28 -1
  15. package/dist/lib/doctor.js +63 -2
  16. package/dist/lib/index.d.ts +4 -0
  17. package/dist/lib/index.js +5 -0
  18. package/dist/lib/models/adapterResolver.d.ts +13 -11
  19. package/dist/lib/models/adapterResolver.js +11 -10
  20. package/dist/lib/models/adkGeminiAdapter.js +2 -0
  21. package/dist/lib/models/adkShim.d.ts +1 -1
  22. package/dist/lib/models/adkShim.js +1 -1
  23. package/dist/lib/models/capabilities.js +13 -10
  24. package/dist/lib/models/claudeLlm.js +2 -2
  25. package/dist/lib/models/fallback.d.ts +1 -1
  26. package/dist/lib/models/fallback.js +1 -1
  27. package/dist/lib/models/gatewayLlm.js +2 -2
  28. package/dist/lib/models/geminiAdapter.d.ts +6 -4
  29. package/dist/lib/models/geminiAdapter.js +28 -5
  30. package/dist/lib/models/geminiState.d.ts +21 -0
  31. package/dist/lib/models/geminiState.js +26 -0
  32. package/dist/lib/models/genaiMapping.d.ts +24 -4
  33. package/dist/lib/models/genaiMapping.js +109 -6
  34. package/dist/lib/models/gptLlm.js +2 -2
  35. package/dist/lib/models/grokLlm.js +2 -2
  36. package/dist/lib/models/kimiLlm.js +2 -2
  37. package/dist/lib/models/ollamaLlm.js +2 -2
  38. package/dist/lib/models/registry.d.ts +5 -1
  39. package/dist/lib/models/registry.js +17 -12
  40. package/dist/lib/models/tracedGemini.d.ts +1 -1
  41. package/dist/lib/models/tracedGemini.js +1 -1
  42. package/dist/lib/runtime/adkSessionBridge.d.ts +1 -1
  43. package/dist/lib/runtime/adkSessionBridge.js +1 -1
  44. package/dist/lib/runtime/approvals.d.ts +5 -0
  45. package/dist/lib/runtime/approvals.js +39 -8
  46. package/dist/lib/runtime/credentials.d.ts +3 -1
  47. package/dist/lib/runtime/credentials.js +6 -1
  48. package/dist/lib/runtime/native/agentLoop.d.ts +7 -1
  49. package/dist/lib/runtime/native/agentLoop.js +31 -13
  50. package/dist/lib/runtime/native/delegate.d.ts +53 -0
  51. package/dist/lib/runtime/native/delegate.js +91 -8
  52. package/dist/lib/runtime/native/interrupts.d.ts +6 -3
  53. package/dist/lib/runtime/native/interrupts.js +23 -4
  54. package/dist/lib/runtime/native/request.d.ts +1 -1
  55. package/dist/lib/runtime/native/request.js +1 -1
  56. package/dist/lib/runtime/native/selfCorrection.d.ts +64 -6
  57. package/dist/lib/runtime/native/selfCorrection.js +82 -8
  58. package/dist/lib/runtime/native/step.d.ts +14 -6
  59. package/dist/lib/runtime/native/step.js +34 -10
  60. package/dist/lib/runtime/runtimeFlag.d.ts +13 -5
  61. package/dist/lib/runtime/runtimeFlag.js +14 -6
  62. package/dist/lib/runtime/syndicateTurn.d.ts +15 -10
  63. package/dist/lib/runtime/syndicateTurn.js +59 -32
  64. package/dist/lib/runtime/valueDepth.d.ts +23 -0
  65. package/dist/lib/runtime/valueDepth.js +41 -0
  66. package/dist/lib/session/supabaseSessionService.d.ts +1 -1
  67. package/dist/lib/session/supabaseSessionService.js +1 -1
  68. package/dist/lib/session/transcript.d.ts +1 -1
  69. package/dist/lib/session/transcript.js +5 -3
  70. package/dist/lib/storage/postgres/sessionService.d.ts +1 -1
  71. package/dist/lib/storage/postgres/sessionService.js +1 -1
  72. package/dist/lib/syndicateSchema.d.ts +2 -0
  73. package/dist/lib/syndicateSchema.js +27 -6
  74. package/dist/lib/toolRegistry.d.ts +12 -1
  75. package/dist/lib/toolRegistry.js +42 -2
  76. package/dist/lib/tools/adkTool.d.ts +1 -1
  77. package/dist/lib/tools/adkTool.js +1 -1
  78. package/dist/lib/tools/collectionsSearchTool.d.ts +1 -1
  79. package/dist/lib/tools/collectionsSearchTool.js +1 -1
  80. package/dist/lib/tools/generateImageTool.d.ts +1 -1
  81. package/dist/lib/tools/inspectImageTool.d.ts +1 -1
  82. package/dist/lib/tools/urlContextTool.d.ts +1 -1
  83. package/dist/lib/tools/urlContextTool.js +1 -1
  84. package/dist/lib/tools/webExtractTool.d.ts +1 -1
  85. package/dist/lib/tools/webSearchTool.d.ts +1 -1
  86. package/dist/lib/tools/webSearchTool.js +1 -1
  87. package/dist/lib/tools/xApiSearchTool.d.ts +1 -1
  88. package/dist/lib/tools/xSearchTool.d.ts +1 -1
  89. package/dist/lib/tools/xSearchTool.js +1 -1
  90. package/dist/lib/wiki/agentRun.d.ts +1 -1
  91. package/dist/lib/wiki/agentRun.js +2 -1
  92. package/dist/lib/workflow/agentNode.d.ts +21 -4
  93. package/dist/lib/workflow/agentNode.js +96 -5
  94. package/dist/lib/workflow/graph.d.ts +5 -4
  95. package/dist/lib/workflow/graph.js +14 -4
  96. package/dist/lib/workflow/resume.d.ts +10 -9
  97. package/dist/lib/workflow/resume.js +13 -12
  98. package/dist/lib/workflow/scheduler.d.ts +10 -7
  99. package/dist/lib/workflow/scheduler.js +15 -2
  100. package/dist/lib/workflow.d.ts +33 -11
  101. package/dist/lib/workflow.js +49 -27
  102. package/dist/lib/workflowConfig.d.ts +4 -0
  103. package/dist/lib/workflowConfig.js +4 -0
  104. package/dist/scripts/doctor.d.ts +3 -1
  105. package/dist/scripts/doctor.js +4 -2
  106. package/package.json +6 -3
@@ -43,8 +43,25 @@
43
43
  * error; a run that ends with one and no output returns it as the
44
44
  * run's `error`, and the scheduler fails the attempt with
45
45
  * NodeReportedError, ADK's message, retried or stopping the walk as
46
- * ADK's does, and reported once (by the node's own event). A run the turn stopped throws. A run that pauses on a person
47
- * (an ask_user tool, an approval) is an interrupt, not run here yet.
46
+ * ADK's does, and reported once (by the node's own event). A run the turn stopped throws. A run that pauses on
47
+ * an ask_user tool call is refused by name.
48
+ * 8. APPROVALS (ADR 0098). A run that pauses on approval requests (a tool
49
+ * in the agent's `require_approval`) resolves with their ids as the
50
+ * node's interrupts: the node waits and the walk ends paused, as an
51
+ * ask_user node's does. The resumed walk reruns the node with
52
+ * `resumedInterruptIds`: until every request has a decision among the
53
+ * answers (a confirmation, never a plain-text message) it waits again
54
+ * without running, its open requests raised again on one event of the
55
+ * new run (`waitAgain`), so the run after still resumes them; then the
56
+ * agent's run continues instead of starting afresh, unless it already
57
+ * did on an earlier resume, whose stored output is then the node's
58
+ * (`outputAfter`). No input turn is stored, so the person's answer stays the
59
+ * latest user event, and the loop's approval resume
60
+ * (lib/runtime/native/interrupts.ts, ADR 0077) runs or refuses the
61
+ * pinned call before the next model step, which reads the node's turn
62
+ * from its input on. ADK's runLlmAgentAsNode stores the input again and
63
+ * starts the agent afresh, so the pinned call never runs there; ADK
64
+ * turns refuse gates on workflow nodes instead.
48
65
  *
49
66
  * `agentNodeRuntime` puts it together for the scheduler: a `runNode` for
50
67
  * agent nodes and map items, and an `onEvent` that stores the event ADK
@@ -52,10 +69,11 @@
52
69
  * (lib/workflow/nodeEvents.ts), so a session the native walk writes holds
53
70
  * what ADK's holds, in the same order.
54
71
  *
55
- * NOT HERE: interrupts inside a node (an ask_user tool call, an approval),
72
+ * NOT HERE: an ask_user tool call inside a node, a pause inside a map item,
56
73
  * tool nodes (lib/workflow/toolNode.ts) and ask_user nodes
57
74
  * (lib/workflow/pause.ts). It imports nothing from ADK.
58
75
  */
76
+ import { APPROVAL_REQUEST } from '../runtime/approvals.js';
59
77
  import { createTurnEvent, getFunctionCalls } from '../runtime/events.js';
60
78
  import { runAgentLoop } from '../runtime/native/agentLoop.js';
61
79
  import { predecessorOutputs } from '../runtime/native/request.js';
@@ -130,12 +148,71 @@ function nodeStamp(agent, run, invocationId, state) {
130
148
  }
131
149
  }
132
150
  enrichNodeEvent(event, run, { invocationId });
151
+ // ADK's node runner (consume): an event that raises an interrupt records the node's input for the resume.
152
+ if (event.longRunningToolIds?.length)
153
+ event.actions = { ...event.actions, agentState: { ...(event.actions?.agentState ?? {}), input: run.input } };
133
154
  if (event.output !== undefined)
134
155
  state.output = event.output;
135
156
  if (event.errorCode !== undefined)
136
157
  state.reported = { errorCode: event.errorCode, ...(event.errorMessage !== undefined ? { errorMessage: event.errorMessage } : {}) };
137
158
  };
138
159
  }
160
+ /** Whether `event` carries the approval request `id` (an `adk_request_confirmation` call). */
161
+ function isApprovalRequest(event, id) {
162
+ return !!event && getFunctionCalls(event).some((call) => call.id === id && call.name === APPROVAL_REQUEST);
163
+ }
164
+ /**
165
+ * Whether a resumed walk's answer to an approval request is a decision: a
166
+ * ToolConfirmation (`{ confirmed }`), or ADK's `{ response: <json> }` form.
167
+ * A plain-text message the resume read as the answer is not one, and the
168
+ * node keeps waiting.
169
+ */
170
+ function isApprovalAnswer(value) {
171
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
172
+ return false;
173
+ const answer = value;
174
+ return typeof answer.confirmed === 'boolean' || typeof answer.response === 'string';
175
+ }
176
+ /**
177
+ * Rule 8, a node whose decided requests an earlier resume already acted on
178
+ * (another node's approval was the one answered since): the output its run
179
+ * stored after the last of those requests, else undefined. The node does
180
+ * not run again.
181
+ */
182
+ function outputAfter(events, path, ids) {
183
+ const open = new Set(ids);
184
+ let start = -1;
185
+ events.forEach((event, i) => {
186
+ if (getFunctionCalls(event).some((call) => call.name === APPROVAL_REQUEST && call.id !== undefined && open.has(call.id)))
187
+ start = i;
188
+ });
189
+ if (start === -1)
190
+ return undefined;
191
+ for (let i = events.length - 1; i > start; i--) {
192
+ const event = events[i];
193
+ if (event.nodeInfo?.path === path && event.output !== undefined && !event.partial)
194
+ return event.output;
195
+ }
196
+ return undefined;
197
+ }
198
+ /**
199
+ * Rule 8, a node still waiting: its open requests raised again, as one event
200
+ * of this run (a copy of each request call, stamped as the node's), so the
201
+ * next message's resume finds the walk paused in this invocation as in the
202
+ * one before; nothing runs. Resolves with the ids it waits on.
203
+ */
204
+ async function waitAgain(agent, run, ctx, waiting) {
205
+ const open = new Set(waiting);
206
+ const calls = ctx.session.events.flatMap((event) => getFunctionCalls(event).filter((call) => call.name === APPROVAL_REQUEST && call.id !== undefined && open.has(call.id)));
207
+ const seen = new Set();
208
+ const parts = calls.filter((call) => !seen.has(call.id) && seen.add(call.id)).map((call) => ({ functionCall: structuredClone(call) }));
209
+ const event = createTurnEvent({ author: agent.name, invocationId: ctx.invocationId, content: { role: 'model', parts } });
210
+ event.longRunningToolIds = [...seen];
211
+ nodeStamp(agent, run, ctx.invocationId, { output: undefined })(event);
212
+ const stored = await (ctx.appendInput ?? ((e) => ctx.sessions.append(ctx.session, e)))(event);
213
+ ctx.onEvent?.(stored);
214
+ return { interruptIds: [...seen] };
215
+ }
139
216
  /**
140
217
  * Runs `agent` as the node `run` names, on the native loop, and resolves
141
218
  * with the node's output, or with the error it reported when it ended on
@@ -146,7 +223,17 @@ export async function runAgentNode(agent, run, ctx) {
146
223
  const taskMode = agent.mode === 'task';
147
224
  const nodeAgent = asNodeAgent(agent);
148
225
  const { session, sessions, invocationId } = ctx;
149
- if (!taskMode && run.input !== undefined && run.input !== null) {
226
+ // Rule 8: a node that waited on an approval resumes its own run once every request it raised is answered.
227
+ const resumed = run.resumedInterruptIds ?? [];
228
+ if (resumed.length > 0) {
229
+ const waiting = resumed.filter((id) => !isApprovalAnswer(run.resumeInputs?.[id]));
230
+ if (waiting.length > 0)
231
+ return waitAgain(agent, run, ctx, waiting);
232
+ const finished = outputAfter(ctx.session.events, run.path, resumed);
233
+ if (finished !== undefined)
234
+ return { output: finished };
235
+ }
236
+ if (resumed.length === 0 && !taskMode && run.input !== undefined && run.input !== null) {
150
237
  const userEvent = createTurnEvent({ author: 'user', invocationId, branch: run.branch, content: nodeInputContent(run.input) });
151
238
  const stored = await (ctx.appendInput ?? ((e) => sessions.append(session, e)))(userEvent);
152
239
  ctx.onEvent?.(stored);
@@ -179,7 +266,11 @@ export async function runAgentNode(agent, run, ctx) {
179
266
  ctx.onEvent?.(next.value);
180
267
  }
181
268
  if (end.reason === 'paused') {
182
- throw new Error(`Node '${agent.name}' paused on ${(end.pending ?? []).join(', ')}: a pause inside an agent node does not run on the native runtime yet.`);
269
+ // Rule 8: an approval request pauses the node, and the walk with it; any other pause (an ask_user call) is refused.
270
+ const pending = end.pending ?? [];
271
+ if (pending.length > 0 && pending.every((id) => isApprovalRequest(end.lastEvent, id)))
272
+ return { interruptIds: [...pending] };
273
+ throw new Error(`Node '${agent.name}' paused on ${pending.join(', ')}: a pause inside an agent node other than an approval does not run on the native runtime yet.`);
183
274
  }
184
275
  if (end.reason === 'stopped')
185
276
  throw new NodeStoppedError(agent.name, end.stop);
@@ -19,8 +19,8 @@
19
19
  * 1. The block's cross-field rules — every name an agent or a declared
20
20
  * node, `START` opening a chain, a routing map after the node it
21
21
  * routes, a declared node exactly one kind, the reserved `__route`
22
- * suffix, `workflow` without `dispatch`, and the pauses and remotes a
23
- * node cannot carry yet. Same paths and messages as the schema's
22
+ * suffix, `workflow` without `dispatch`, `retry` and `timeout` refused on
23
+ * a map entry, and the pauses and remotes a node cannot carry yet. Same paths and messages as the schema's
24
24
  * (lib/syndicateSchema.ts, `workflowProblems`), thrown together as a
25
25
  * `WorkflowGraphError`.
26
26
  * 2. The graph's own rules, on the built edges — no empty routing map,
@@ -74,8 +74,9 @@ export interface MapNode {
74
74
  /** Concurrency of the map; absent means the runtime's default (8). */
75
75
  maxParallel?: number;
76
76
  /**
77
- * The map entry's own modifiers. ADK's compile does not hand them to its
78
- * ParallelWorker, so neither runtime applies them; they are kept as written.
77
+ * The map entry's own modifiers: always empty, since the schema refuses
78
+ * `retry` and `timeout` on a map entry (ADR 0103). ADK's compile hands
79
+ * neither to its ParallelWorker.
79
80
  */
80
81
  settings: GraphNodeSettings;
81
82
  /** The mapped agent's own node modifiers (`nodes.<agent>`), which ADK applies to each item's run. */
@@ -19,8 +19,8 @@
19
19
  * 1. The block's cross-field rules — every name an agent or a declared
20
20
  * node, `START` opening a chain, a routing map after the node it
21
21
  * routes, a declared node exactly one kind, the reserved `__route`
22
- * suffix, `workflow` without `dispatch`, and the pauses and remotes a
23
- * node cannot carry yet. Same paths and messages as the schema's
22
+ * suffix, `workflow` without `dispatch`, `retry` and `timeout` refused on
23
+ * a map entry, and the pauses and remotes a node cannot carry yet. Same paths and messages as the schema's
24
24
  * (lib/syndicateSchema.ts, `workflowProblems`), thrown together as a
25
25
  * `WorkflowGraphError`.
26
26
  * 2. The graph's own rules, on the built edges — no empty routing map,
@@ -254,6 +254,15 @@ export function workflowConfigProblems(raw, subs) {
254
254
  if (kind !== 'map' && entry.max_parallel !== undefined) {
255
255
  out.push({ path: ['workflow', 'nodes', name, 'max_parallel'], message: 'max_parallel applies to map only' });
256
256
  }
257
+ // A map item runs under its agent's own modifiers, as on ADK (ADR 0089, ADR 0103): the map entry's would be applied by neither runtime.
258
+ if (kind === 'map') {
259
+ for (const key of ['retry', 'timeout']) {
260
+ if (entry[key] === undefined)
261
+ continue;
262
+ const target = entry.map;
263
+ out.push({ path: ['workflow', 'nodes', name, key], message: `${key} on a map node is not applied: each item runs under its agent's own ${key}; set it on nodes.${target}` });
264
+ }
265
+ }
257
266
  }
258
267
  for (const name of agentNames) {
259
268
  if (name.endsWith(ROUTE_STEP_SUFFIX))
@@ -324,8 +333,9 @@ export function workflowConfigProblems(raw, subs) {
324
333
  for (const [path, agent] of agents) {
325
334
  if (!isObj(agent))
326
335
  continue;
327
- if (Array.isArray(agent.require_approval) && agent.require_approval.length) {
328
- out.push({ path: [...path, 'require_approval'], message: 'approval gates are not supported inside a workflow yet' });
336
+ // A map item cannot pause: the walk resumes a paused agent node, not an item of a map (ADR 0094, ADR 0098).
337
+ if (typeof agent.name === 'string' && mapped.has(agent.name) && Array.isArray(agent.require_approval) && agent.require_approval.length) {
338
+ out.push({ path: [...path, 'require_approval'], message: 'approval gates are not supported on an agent a map node runs: a map item cannot pause the walk' });
329
339
  }
330
340
  if (isObj(agent.skills) && agent.skills.scripts === 'local') {
331
341
  out.push({ path: [...path, 'skills', 'scripts'], message: 'skill scripts (an approval pause) are not supported inside a workflow yet' });
@@ -119,13 +119,13 @@ export declare function workflowNodeInput(content: TurnContent | undefined): unk
119
119
  */
120
120
  export declare function resumeInputsFromPlainText(content: TurnContent | undefined, events: readonly StoredEvent[], invocationId: string): Record<string, unknown>;
121
121
  /**
122
- * A pause the native walk cannot resume: one raised inside an agent node (an
123
- * `ask_user` tool call, an approval, a credential request) or inside a map
124
- * item. ADK resumes those inside the node (the agent's own history, the
125
- * ParallelWorker's item); the native agent node refuses to pause at all
126
- * (lib/workflow/agentNode.ts), so it cannot pick one up either. Resuming the
127
- * walk anyway would run the node again from its input and ask again, so the
128
- * resume refuses by name instead.
122
+ * A pause the native walk cannot resume: one raised inside an agent node
123
+ * other than an approval (an `ask_user` tool call, a credential request) or
124
+ * inside a map item. ADK resumes those inside the node (the agent's own
125
+ * history, the ParallelWorker's item); the native agent node pauses only on
126
+ * an approval (lib/workflow/agentNode.ts, ADR 0098), so it cannot pick the
127
+ * others up. Resuming the walk anyway would run the node again from its
128
+ * input and ask again, so the resume refuses by name instead.
129
129
  */
130
130
  export declare class UnsupportedWorkflowResumeError extends Error {
131
131
  /** The node path the pause was raised at. */
@@ -134,8 +134,9 @@ export declare class UnsupportedWorkflowResumeError extends Error {
134
134
  }
135
135
  /**
136
136
  * Throws UnsupportedWorkflowResumeError for the first interrupt in the run's
137
- * events that is not an ask_user node's input request at a direct child of
138
- * the workflow (the workflow's own record aside).
137
+ * events that is neither an ask_user node's input request nor an agent
138
+ * node's approval request at a direct child of the workflow (the workflow's
139
+ * own record aside).
139
140
  */
140
141
  export declare function assertResumable(runEvents: readonly StoredEvent[], workflowPath: string): void;
141
142
  export interface WorkflowResumeRequest {
@@ -384,27 +384,28 @@ export function resumeInputsFromPlainText(content, events, invocationId) {
384
384
  }
385
385
  // ── What the native walk cannot resume yet ───────────────────────────────────
386
386
  /**
387
- * A pause the native walk cannot resume: one raised inside an agent node (an
388
- * `ask_user` tool call, an approval, a credential request) or inside a map
389
- * item. ADK resumes those inside the node (the agent's own history, the
390
- * ParallelWorker's item); the native agent node refuses to pause at all
391
- * (lib/workflow/agentNode.ts), so it cannot pick one up either. Resuming the
392
- * walk anyway would run the node again from its input and ask again, so the
393
- * resume refuses by name instead.
387
+ * A pause the native walk cannot resume: one raised inside an agent node
388
+ * other than an approval (an `ask_user` tool call, a credential request) or
389
+ * inside a map item. ADK resumes those inside the node (the agent's own
390
+ * history, the ParallelWorker's item); the native agent node pauses only on
391
+ * an approval (lib/workflow/agentNode.ts, ADR 0098), so it cannot pick the
392
+ * others up. Resuming the walk anyway would run the node again from its
393
+ * input and ask again, so the resume refuses by name instead.
394
394
  */
395
395
  export class UnsupportedWorkflowResumeError extends Error {
396
396
  /** The node path the pause was raised at. */
397
397
  nodePath;
398
398
  constructor(nodePath, why) {
399
- super(`Cannot resume the workflow on the native runtime: the pause at '${nodePath}' was raised ${why}, which only an ask_user node's pause supports here.`);
399
+ super(`Cannot resume the workflow on the native runtime: the pause at '${nodePath}' was raised ${why}, which only an ask_user node's pause or an agent node's approval supports here.`);
400
400
  this.name = 'UnsupportedWorkflowResumeError';
401
401
  this.nodePath = nodePath;
402
402
  }
403
403
  }
404
404
  /**
405
405
  * Throws UnsupportedWorkflowResumeError for the first interrupt in the run's
406
- * events that is not an ask_user node's input request at a direct child of
407
- * the workflow (the workflow's own record aside).
406
+ * events that is neither an ask_user node's input request nor an agent
407
+ * node's approval request at a direct child of the workflow (the workflow's
408
+ * own record aside).
408
409
  */
409
410
  export function assertResumable(runEvents, workflowPath) {
410
411
  for (const event of runEvents) {
@@ -415,8 +416,8 @@ export function assertResumable(runEvents, workflowPath) {
415
416
  continue;
416
417
  if (directChildName(path, workflowPath) === undefined)
417
418
  throw new UnsupportedWorkflowResumeError(path || String(event.author ?? ''), 'inside a nested node (a map item)');
418
- const asksForInput = partsOf(event).some((p) => p.functionCall?.name === INPUT_REQUEST);
419
- if (!asksForInput)
419
+ const resumable = partsOf(event).some((p) => p.functionCall?.name === INPUT_REQUEST || p.functionCall?.name === APPROVAL_REQUEST);
420
+ if (!resumable)
420
421
  throw new UnsupportedWorkflowResumeError(path, 'inside an agent node');
421
422
  }
422
423
  }
@@ -147,6 +147,12 @@ export interface NodeRun {
147
147
  * Absent in a walk that is not a resume.
148
148
  */
149
149
  resumeInputs?: Readonly<Record<string, unknown>>;
150
+ /**
151
+ * The interrupts this node's prior run paused on, when this run resumes it
152
+ * (a resumed walk's first activation of a node that waited). An agent node
153
+ * resumes its own run on them instead of starting afresh (ADR 0098).
154
+ */
155
+ resumedInterruptIds?: readonly string[];
150
156
  }
151
157
  /** An error a node reports instead of throwing, as an ADK event's errorCode and errorMessage. */
152
158
  export interface ReportedNodeError {
@@ -361,14 +367,11 @@ export declare const RETRY_DEFAULTS: {
361
367
  readonly jitter: 1;
362
368
  };
363
369
  /**
364
- * A node's retry: the YAML's fields, plus ADK's two the YAML does not spell
365
- * yet (`exceptions`: the error names that may be retried, all when absent;
366
- * `jitter`: the random spread, default 1).
370
+ * A node's retry, as the YAML spells it: ADK's fields, `exceptions` (the
371
+ * error names that may be retried, all when absent) and `jitter` (the
372
+ * random spread, default 1) included.
367
373
  */
368
- export type RetrySettings = RetryYaml & {
369
- exceptions?: string[];
370
- jitter?: number;
371
- };
374
+ export type RetrySettings = RetryYaml;
372
375
  /** The names an error answers to: its class's and its `name`, as ADK matches `exceptions`. */
373
376
  export declare function errorNames(error: unknown): string[];
374
377
  /** ADK's errorType: the class name, or the assigned name of a plain Error. */
@@ -292,7 +292,12 @@ export async function runWorkflowGraph(graph, options) {
292
292
  }
293
293
  }
294
294
  const resuming = prior !== undefined && prior.interruptIds.size > 0 && prior.input !== undefined;
295
- return { ...(resuming ? { input: prior.input } : {}), resumeInputs: repeat ? {} : resume.resumeInputs };
295
+ const waited = prior !== undefined && prior.interruptIds.size > 0 && !repeat;
296
+ return {
297
+ ...(resuming ? { input: prior.input } : {}),
298
+ resumeInputs: repeat ? {} : resume.resumeInputs,
299
+ ...(waited ? { resumedInterruptIds: [...prior.interruptIds] } : {}),
300
+ };
296
301
  };
297
302
  // Seed: one trigger per START edge, each on its own branch when there are several.
298
303
  const startEdges = graph.edges.filter((e) => e.from === START_NODE);
@@ -329,7 +334,15 @@ export async function runWorkflowGraph(graph, options) {
329
334
  const path = `${workflowPath}.${name}`;
330
335
  const input = start.input !== undefined ? start.input : trigger.input;
331
336
  emit({ type: 'node_start', node: name, kind: node.kind, runId, path, branch, input });
332
- const ctx = { input, runId, path, branch, signal: controller.signal, ...(start.resumeInputs ? { resumeInputs: start.resumeInputs } : {}) };
337
+ const ctx = {
338
+ input,
339
+ runId,
340
+ path,
341
+ branch,
342
+ signal: controller.signal,
343
+ ...(start.resumeInputs ? { resumeInputs: start.resumeInputs } : {}),
344
+ ...(start.resumedInterruptIds ? { resumedInterruptIds: start.resumedInterruptIds } : {}),
345
+ };
333
346
  const traced = { name, kind: node.kind, path, runId, attempts: state.attempts };
334
347
  const run = walk.traceNode(traced, () => executeNode(node, ctx, walk, state.attempts)).then((result) => ({ name, result: { ...result, branch } }), (error) => ({ name, error }));
335
348
  pending.set(name, run);
@@ -41,18 +41,32 @@
41
41
  * the wiki), including a `map` node, which ADK would otherwise name after
42
42
  * the agent it wraps.
43
43
  *
44
+ * ── As a subagent (ADR 0098) ─────────────────────────────────────────────
45
+ * A delegated `yaml_reference` to a workflow syndicate is the whole graph:
46
+ * `assembleWorkflow` builds its Workflow under the subagent entry's name
47
+ * and description, and lib/compileAdk.ts wraps it in ADK's AgentTool, whose
48
+ * answer is the graph's last event's text. A nested workflow with an
49
+ * `ask_user` node is refused by name (lib/compile.ts compileWorkflowSpec): a
50
+ * pause inside a tool call cannot reach the caller (ADR 0028). As a dispatch
51
+ * route or a workflow node, a workflow syndicate is still its orchestrator
52
+ * alone.
53
+ *
54
+ * ── Approval gates (ADR 0098) ────────────────────────────────────────────
55
+ * A tool in a node agent's `require_approval` pauses the node on ADK's
56
+ * `adk_request_confirmation`, and the walk with it. Only the native walk
57
+ * resumes it (lib/workflow/agentNode.ts): ADK's runLlmAgentAsNode reruns the
58
+ * node from its input and never runs the pinned call, so runSyndicateTurn
59
+ * refuses a gated workflow on ADK. The schema refuses a gate on an agent a
60
+ * map runs (an item cannot pause the walk).
61
+ *
44
62
  * ── Not in this version ───────────────────────────────────────────────────
45
- * Approval gates (`require_approval`, `skills.scripts: local`) and remote
46
- * `a2a_agent_url` subagents are refused inside a workflow by the schema: a
47
- * gated call pauses a node through ADK's interrupt path, whose resume is not
48
- * the one ADR 0028 built, and a remote agent is reachable only as a tool.
49
- * Both are open for a later record. A `Workflow` cannot yet be another
50
- * agent's subagent (an ADK limit), so `yaml_reference` to a workflow
51
- * syndicate compiles its orchestrator alone.
63
+ * Skill scripts (`skills.scripts: local`, an approval pause) and remote
64
+ * `a2a_agent_url` subagents are refused inside a workflow by the schema; a
65
+ * remote agent is reachable only as a tool. Both are open for a later
66
+ * record.
52
67
  */
53
- import { Workflow } from '@google/adk';
54
- import type { BaseNode, LlmAgent } from '@google/adk';
55
- import type { CompileOptions } from './compile.ts';
68
+ import type { BaseNode, LlmAgent, Workflow } from '@google/adk';
69
+ import type { CompileOptions, WorkflowSpec } from './compile.ts';
56
70
  import type { SyndicateYamlConfig } from './loadSyndicate.ts';
57
71
  export * from './workflowConfig.ts';
58
72
  export interface CompiledWorkflow {
@@ -63,10 +77,18 @@ export interface CompiledWorkflow {
63
77
  nodes: Map<string, BaseNode>;
64
78
  }
65
79
  /**
66
- * Compile a workflow syndicate: every agent through `compileSubagent` (its
80
+ * Compile a workflow syndicate: every agent through `compileSubagentSpec` (its
67
81
  * tools, skills and MCP server as in any mode), the declared nodes, the
68
82
  * hidden route steps, and the edge chains, into one ADK `Workflow` named
69
83
  * after the syndicate. Throws on a name the schema let through, a tool that
70
84
  * is not registered, or a graph ADK rejects (unreachable node, cycle).
71
85
  */
72
86
  export declare function compileWorkflow(config: SyndicateYamlConfig, opts?: CompileOptions, transformAgent?: (agent: LlmAgent) => LlmAgent): Promise<CompiledWorkflow>;
87
+ /**
88
+ * ADK's Workflow from a compiled workflow spec, synchronously: each agent
89
+ * built by compileAdk with its node modifiers (retry, timeout) at
90
+ * construction, then the declared nodes, route steps and edges. A nested
91
+ * workflow (ADR 0098) is this Workflow under its subagent entry's name and
92
+ * description, which lib/compileAdk.ts wraps in ADK's AgentTool.
93
+ */
94
+ export declare function assembleWorkflow(spec: WorkflowSpec, opts?: CompileOptions, transformAgent?: (agent: LlmAgent) => LlmAgent): CompiledWorkflow;
@@ -41,22 +41,37 @@
41
41
  * the wiki), including a `map` node, which ADK would otherwise name after
42
42
  * the agent it wraps.
43
43
  *
44
+ * ── As a subagent (ADR 0098) ─────────────────────────────────────────────
45
+ * A delegated `yaml_reference` to a workflow syndicate is the whole graph:
46
+ * `assembleWorkflow` builds its Workflow under the subagent entry's name
47
+ * and description, and lib/compileAdk.ts wraps it in ADK's AgentTool, whose
48
+ * answer is the graph's last event's text. A nested workflow with an
49
+ * `ask_user` node is refused by name (lib/compile.ts compileWorkflowSpec): a
50
+ * pause inside a tool call cannot reach the caller (ADR 0028). As a dispatch
51
+ * route or a workflow node, a workflow syndicate is still its orchestrator
52
+ * alone.
53
+ *
54
+ * ── Approval gates (ADR 0098) ────────────────────────────────────────────
55
+ * A tool in a node agent's `require_approval` pauses the node on ADK's
56
+ * `adk_request_confirmation`, and the walk with it. Only the native walk
57
+ * resumes it (lib/workflow/agentNode.ts): ADK's runLlmAgentAsNode reruns the
58
+ * node from its input and never runs the pinned call, so runSyndicateTurn
59
+ * refuses a gated workflow on ADK. The schema refuses a gate on an agent a
60
+ * map runs (an item cannot pause the walk).
61
+ *
44
62
  * ── Not in this version ───────────────────────────────────────────────────
45
- * Approval gates (`require_approval`, `skills.scripts: local`) and remote
46
- * `a2a_agent_url` subagents are refused inside a workflow by the schema: a
47
- * gated call pauses a node through ADK's interrupt path, whose resume is not
48
- * the one ADR 0028 built, and a remote agent is reachable only as a tool.
49
- * Both are open for a later record. A `Workflow` cannot yet be another
50
- * agent's subagent (an ADK limit), so `yaml_reference` to a workflow
51
- * syndicate compiles its orchestrator alone.
63
+ * Skill scripts (`skills.scripts: local`, an approval pause) and remote
64
+ * `a2a_agent_url` subagents are refused inside a workflow by the schema; a
65
+ * remote agent is reachable only as a tool. Both are open for a later
66
+ * record.
52
67
  */
53
- import { DEFAULT_ROUTE, FunctionNode, JoinNode, ParallelWorker, RequestInput, ToolNode, Workflow, createEvent, } from '@google/adk';
54
- import { compileSubagent } from './compile.js';
55
- import { resolveTools as resolveNamedTools } from './toolRegistry.js';
68
+ import { requireAdk } from './adkPeer.js';
69
+ import { compileWorkflowSpec } from './compile.js';
70
+ import { compileAdk } from './compileAdk.js';
56
71
  import { DEFAULT_ROUTE_KEY, ROUTE_STEP_SUFFIX, START_NAME, isWorkflowSyndicate, nodeKind, nodeSettings, routeOf } from './workflowConfig.js';
57
72
  export * from './workflowConfig.js';
58
73
  /**
59
- * Compile a workflow syndicate: every agent through `compileSubagent` (its
74
+ * Compile a workflow syndicate: every agent through `compileSubagentSpec` (its
60
75
  * tools, skills and MCP server as in any mode), the declared nodes, the
61
76
  * hidden route steps, and the edge chains, into one ADK `Workflow` named
62
77
  * after the syndicate. Throws on a name the schema let through, a tool that
@@ -65,23 +80,29 @@ export * from './workflowConfig.js';
65
80
  export async function compileWorkflow(config, opts = {}, transformAgent = (a) => a) {
66
81
  if (!isWorkflowSyndicate(config))
67
82
  throw new Error(`${config.syndicate_name}: no workflow block`);
83
+ return assembleWorkflow(await compileWorkflowSpec(config, opts), opts, transformAgent);
84
+ }
85
+ /**
86
+ * ADK's Workflow from a compiled workflow spec, synchronously: each agent
87
+ * built by compileAdk with its node modifiers (retry, timeout) at
88
+ * construction, then the declared nodes, route steps and edges. A nested
89
+ * workflow (ADR 0098) is this Workflow under its subagent entry's name and
90
+ * description, which lib/compileAdk.ts wraps in ADK's AgentTool.
91
+ */
92
+ export function assembleWorkflow(spec, opts = {}, transformAgent = (a) => a) {
93
+ const config = spec.config;
94
+ if (!isWorkflowSyndicate(config))
95
+ throw new Error(`${config.syndicate_name}: no workflow block`);
96
+ // ADK's Workflow is the adk runtime's; the native walk (lib/workflow/turn.ts) needs none of this (ADR 0102).
97
+ const { DEFAULT_ROUTE, FunctionNode, JoinNode, ParallelWorker, RequestInput, ToolNode, Workflow, createEvent } = requireAdk("ADK's Workflow (a workflow syndicate on the adk runtime, compileWorkflow)");
68
98
  const wf = config.workflow;
69
99
  const nodeYaml = wf.nodes ?? {};
70
- const agentConfigs = new Map();
71
- agentConfigs.set(config.orchestrator.name, { description: '', ...config.orchestrator });
72
- for (const sub of config.subagents ?? [])
73
- agentConfigs.set(sub.name, sub);
74
- // Agents, with their node modifiers (retry, timeout) at construction.
75
- const compileOpts = {
76
- ...opts,
77
- nodeConfig: (name) => {
78
- const settings = nodeSettings(nodeYaml[name]);
79
- return Object.keys(settings).length ? settings : undefined;
80
- },
81
- };
100
+ // Agents, with their node modifiers (retry, timeout) at construction; not on a nested syndicate's orchestrator.
82
101
  const agents = new Map();
83
- for (const [name, sub] of agentConfigs)
84
- agents.set(name, transformAgent(await compileSubagent(sub, compileOpts)));
102
+ for (const { yaml, spec: agentSpec } of spec.agents) {
103
+ const settings = yaml.yaml_reference ? {} : nodeSettings(nodeYaml[yaml.name]);
104
+ agents.set(yaml.name, transformAgent(compileAdk(agentSpec, opts, settings)));
105
+ }
85
106
  // Declared nodes.
86
107
  const nodes = new Map(agents);
87
108
  for (const [name, entry] of Object.entries(nodeYaml)) {
@@ -118,7 +139,7 @@ export async function compileWorkflow(config, opts = {}, transformAgent = (a) =>
118
139
  nodes.set(name, worker);
119
140
  }
120
141
  else if (kind === 'tool') {
121
- const [tool] = resolveNamedTools([entry.tool], opts.onUnknownTool);
142
+ const tool = spec.resolveTool(entry.tool);
122
143
  if (!tool)
123
144
  throw new Error(`workflow node '${name}': tool '${entry.tool}' is not registered`);
124
145
  nodes.set(name, new ToolNode(tool, { name, ...settings }));
@@ -172,7 +193,8 @@ export async function compileWorkflow(config, opts = {}, transformAgent = (a) =>
172
193
  return out;
173
194
  });
174
195
  const workflow = new Workflow({
175
- name: config.syndicate_name,
196
+ name: spec.name,
197
+ ...(spec.description ? { description: spec.description } : {}),
176
198
  edges,
177
199
  ...(wf.max_concurrency !== undefined ? { maxConcurrency: wf.max_concurrency } : {}),
178
200
  });
@@ -20,6 +20,10 @@ export interface RetryYaml {
20
20
  initial_delay?: number;
21
21
  max_delay?: number;
22
22
  backoff_factor?: number;
23
+ /** Randomness of the backoff: 0 none, default 1 (ADK's). */
24
+ jitter?: number;
25
+ /** Error names a failure is retried on (its class's or its `name`); every error when absent. */
26
+ exceptions?: string[];
23
27
  }
24
28
  /** Per-node settings. A node is an agent (modifiers only) or exactly one of the kinds. */
25
29
  export interface WorkflowNodeYaml {
@@ -42,6 +42,10 @@ export function toRetryConfig(retry) {
42
42
  out.maxDelay = retry.max_delay;
43
43
  if (retry.backoff_factor !== undefined)
44
44
  out.backoffFactor = retry.backoff_factor;
45
+ if (retry.jitter !== undefined)
46
+ out.jitter = retry.jitter;
47
+ if (retry.exceptions !== undefined)
48
+ out.exceptions = [...retry.exceptions];
45
49
  return out;
46
50
  }
47
51
  /** The BaseNode fields a node entry's modifiers map to. */
@@ -6,7 +6,9 @@
6
6
  * examples/), resolves each agent's model to its provider under the current
7
7
  * .env, and prints: the provider, whether that path is funded (direct key,
8
8
  * gateway stand-in, or local), which declared server-side tools it keeps or
9
- * drops, and one verdict per syndicate. Closes with the env vars that would
9
+ * drops, and one verdict per syndicate. Says first which runtime a turn runs
10
+ * on (MELCHIZEDEK_RUNTIME, else native) and whether @google/adk, needed only
11
+ * for the adk runtime, is installed. Closes with the env vars that would
10
12
  * unlock the most, where to get each, and the first command to try.
11
13
  *
12
14
  * Read-only: never edits .env, never sends a request, never prints a key
@@ -6,7 +6,9 @@
6
6
  * examples/), resolves each agent's model to its provider under the current
7
7
  * .env, and prints: the provider, whether that path is funded (direct key,
8
8
  * gateway stand-in, or local), which declared server-side tools it keeps or
9
- * drops, and one verdict per syndicate. Closes with the env vars that would
9
+ * drops, and one verdict per syndicate. Says first which runtime a turn runs
10
+ * on (MELCHIZEDEK_RUNTIME, else native) and whether @google/adk, needed only
11
+ * for the adk runtime, is installed. Closes with the env vars that would
10
12
  * unlock the most, where to get each, and the first command to try.
11
13
  *
12
14
  * Read-only: never edits .env, never sends a request, never prints a key
@@ -86,5 +88,5 @@ if (json) {
86
88
  else {
87
89
  console.log(renderDoctor(result, { color: !noColor }));
88
90
  }
89
- if (check && result.counts.blocked > 0)
91
+ if (check && (result.counts.blocked > 0 || result.runtime.problem))
90
92
  process.exit(1);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "melchizedek-agents",
3
- "version": "0.19.0",
4
- "description": "A multi-model, multi-agent orchestration framework on Google ADK — YAML-defined syndicates, Supabase session persistence, pgvector long-term memory. Companion repo to the lyceumagents.com curriculum.",
3
+ "version": "0.20.0",
4
+ "description": "A multi-model, multi-agent orchestration framework — YAML-defined syndicates on the engine's own agent loop (Google ADK optional until 1.0), Supabase session persistence, pgvector long-term memory. Companion repo to the lyceumagents.com curriculum.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -12,7 +12,7 @@
12
12
  "bugs": {
13
13
  "url": "https://github.com/jhwadman/melch-ai/issues"
14
14
  },
15
- "//": "adk + genai are pinned exact in dev: newer genai minors change response part shapes and silently break the chat printer. Bump deliberately, verify a live run, then update the pin. adk itself is a PEER dependency — the consumer's app owns the ADK instance so the model registry is a singleton. The adk peer range is capped to the tested minor (~2.2.0): a new adk minor can pull a second genai copy, so the range widens only after a release is tested against it.",
15
+ "//": "adk + genai are pinned exact in dev: newer genai minors change response part shapes and silently break the chat printer. Bump deliberately, verify a live run, then update the pin. adk itself is an OPTIONAL PEER dependency since 0.20.0 (ADR 0102): the native runtime, the default, needs none of it; a consumer on MELCHIZEDEK_RUNTIME=adk installs it, and their app owns the ADK instance so the model registry is a singleton. lib/adkPeer.ts is the one module that loads it. The adk peer range is capped to the tested minor (~2.2.0): a new adk minor can pull a second genai copy, so the range widens only after a release is tested against it.",
16
16
  "engines": {
17
17
  "node": ">=22.6.0"
18
18
  },
@@ -168,6 +168,9 @@
168
168
  "@anthropic-ai/bedrock-sdk": {
169
169
  "optional": true
170
170
  },
171
+ "@google/adk": {
172
+ "optional": true
173
+ },
171
174
  "@anthropic-ai/vertex-sdk": {
172
175
  "optional": true
173
176
  },