melchizedek-agents 1.0.3 → 1.2.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 (172) hide show
  1. package/.env.example +29 -2
  2. package/AGENT_SETUP.md +3 -1
  3. package/CHANGELOG.md +524 -0
  4. package/DOCUMENTATION.md +178 -43
  5. package/ONBOARDING.md +339 -0
  6. package/QUICKSTART.md +15 -2
  7. package/README.md +6 -2
  8. package/config/agents/README.md +2 -2
  9. package/config/agents/examples/ares.yaml +6 -10
  10. package/config/agents/examples/assistant.yaml +6 -6
  11. package/config/agents/examples/augustin.yaml +9 -15
  12. package/config/agents/examples/cartographers.yaml +6 -6
  13. package/config/agents/examples/council.yaml +7 -7
  14. package/config/agents/examples/critic.yaml +24 -24
  15. package/config/agents/examples/harness.yaml +4 -4
  16. package/config/agents/examples/image_production.yaml +37 -35
  17. package/config/agents/examples/librarian.yaml +4 -4
  18. package/config/agents/examples/patient_advocate.yaml +6 -10
  19. package/config/agents/examples/pipeline.yaml +20 -19
  20. package/config/agents/examples/research.yaml +36 -43
  21. package/config/agents/examples/scribe.yaml +31 -32
  22. package/config/agents/examples/scriptorium.yaml +6 -6
  23. package/config/agents/examples/structured_critic.yaml +82 -0
  24. package/config/agents/examples/style_council.yaml +9 -11
  25. package/config/agents/examples/syndicate.yaml +3 -5
  26. package/config/agents/examples/tutor.yaml +2 -2
  27. package/config/agents/examples/weather.yaml +2 -2
  28. package/config/agents/syndicate.schema.json +859 -13
  29. package/config/agents/syndicateSchema.yaml +122 -73
  30. package/config/agents/templates/README.md +1 -1
  31. package/config/agents/templates/account_memory.yaml +4 -4
  32. package/config/agents/templates/case_desk.yaml +6 -6
  33. package/config/agents/templates/conversational.yaml +2 -2
  34. package/config/agents/templates/draft_review.yaml +6 -6
  35. package/config/agents/templates/intake_extractor.yaml +64 -62
  36. package/config/agents/templates/research_brief.yaml +6 -6
  37. package/config/agents/templates/research_desk.yaml +6 -6
  38. package/config/agents/templates/review_panel.yaml +8 -8
  39. package/config/agents/templates/support_triage.yaml +30 -29
  40. package/config/agents/templates/systems_operator.yaml +24 -5
  41. package/db/migrations/0014_durable_runs.sql +31 -0
  42. package/dist/lib/a2a/app.d.ts +27 -0
  43. package/dist/lib/a2a/app.js +64 -5
  44. package/dist/lib/a2a/executor.d.ts +7 -1
  45. package/dist/lib/a2a/executor.js +43 -5
  46. package/dist/lib/a2a/oauthSetup.d.ts +106 -0
  47. package/dist/lib/a2a/oauthSetup.js +259 -0
  48. package/dist/lib/agentDialect.d.ts +76 -0
  49. package/dist/lib/agentDialect.js +151 -0
  50. package/dist/lib/compile.d.ts +42 -6
  51. package/dist/lib/compile.js +97 -45
  52. package/dist/lib/compileNative.d.ts +11 -3
  53. package/dist/lib/compileNative.js +20 -1
  54. package/dist/lib/dispatch.d.ts +10 -5
  55. package/dist/lib/dispatch.js +10 -5
  56. package/dist/lib/doctor.d.ts +110 -1
  57. package/dist/lib/doctor.js +205 -9
  58. package/dist/lib/loadSyndicate.d.ts +54 -2
  59. package/dist/lib/loadSyndicate.js +22 -2
  60. package/dist/lib/memory/namespace.d.ts +6 -5
  61. package/dist/lib/memory/namespace.js +6 -5
  62. package/dist/lib/memory/providers.js +3 -4
  63. package/dist/lib/models/capabilities.d.ts +25 -1
  64. package/dist/lib/models/capabilities.js +79 -1
  65. package/dist/lib/models/chatCompletionsAdapter.d.ts +11 -3
  66. package/dist/lib/models/chatCompletionsAdapter.js +19 -5
  67. package/dist/lib/models/claudeAdapter.js +4 -0
  68. package/dist/lib/models/contract.d.ts +5 -2
  69. package/dist/lib/models/gateway.d.ts +2 -1
  70. package/dist/lib/models/gateway.js +4 -2
  71. package/dist/lib/models/gatewayAdapter.d.ts +7 -1
  72. package/dist/lib/models/gatewayAdapter.js +8 -0
  73. package/dist/lib/models/geminiAdapter.js +8 -2
  74. package/dist/lib/models/genaiMapping.d.ts +8 -4
  75. package/dist/lib/models/genaiMapping.js +23 -9
  76. package/dist/lib/models/gptAdapter.d.ts +4 -2
  77. package/dist/lib/models/gptAdapter.js +11 -4
  78. package/dist/lib/models/grokAdapter.d.ts +3 -1
  79. package/dist/lib/models/grokAdapter.js +11 -3
  80. package/dist/lib/models/kimiAdapter.d.ts +2 -1
  81. package/dist/lib/models/kimiAdapter.js +3 -3
  82. package/dist/lib/models/reasoning.d.ts +30 -2
  83. package/dist/lib/models/reasoning.js +56 -5
  84. package/dist/lib/observability/tracer.d.ts +8 -1
  85. package/dist/lib/observability/tracer.js +33 -5
  86. package/dist/lib/onboarding.d.ts +134 -0
  87. package/dist/lib/onboarding.js +539 -0
  88. package/dist/lib/runtime/approvals.d.ts +8 -0
  89. package/dist/lib/runtime/approvals.js +4 -3
  90. package/dist/lib/runtime/credentials.d.ts +6 -0
  91. package/dist/lib/runtime/native/agentLoop.d.ts +23 -3
  92. package/dist/lib/runtime/native/agentLoop.js +107 -11
  93. package/dist/lib/runtime/native/checkpoint.d.ts +171 -0
  94. package/dist/lib/runtime/native/checkpoint.js +359 -0
  95. package/dist/lib/runtime/native/delegate.d.ts +222 -18
  96. package/dist/lib/runtime/native/delegate.js +390 -39
  97. package/dist/lib/runtime/native/interrupts.d.ts +184 -2
  98. package/dist/lib/runtime/native/interrupts.js +391 -1
  99. package/dist/lib/runtime/native/request.d.ts +13 -3
  100. package/dist/lib/runtime/native/request.js +28 -4
  101. package/dist/lib/runtime/native/telemetry.js +10 -15
  102. package/dist/lib/runtime/questions.d.ts +7 -4
  103. package/dist/lib/runtime/sessions.d.ts +1 -1
  104. package/dist/lib/runtime/syndicateTurn.d.ts +11 -3
  105. package/dist/lib/runtime/syndicateTurn.js +353 -68
  106. package/dist/lib/runtime/turnControl.d.ts +56 -0
  107. package/dist/lib/runtime/turnControl.js +10 -0
  108. package/dist/lib/session/transcript.d.ts +9 -0
  109. package/dist/lib/session/transcript.js +20 -1
  110. package/dist/lib/skills.d.ts +18 -0
  111. package/dist/lib/skills.js +73 -1
  112. package/dist/lib/storage/postgres/index.d.ts +12 -2
  113. package/dist/lib/storage/postgres/index.js +8 -3
  114. package/dist/lib/storage/postgres/taskQueue.d.ts +20 -4
  115. package/dist/lib/storage/postgres/taskQueue.js +29 -5
  116. package/dist/lib/storage/postgres/taskStore.d.ts +26 -2
  117. package/dist/lib/storage/postgres/taskStore.js +74 -8
  118. package/dist/lib/syndicateSchema.d.ts +316 -1
  119. package/dist/lib/syndicateSchema.js +203 -29
  120. package/dist/lib/tools/auth.d.ts +20 -6
  121. package/dist/lib/tools/auth.js +21 -5
  122. package/dist/lib/tools/credentialEnv.d.ts +28 -0
  123. package/dist/lib/tools/credentialEnv.js +52 -0
  124. package/dist/lib/tools/credentialHosts.d.ts +88 -0
  125. package/dist/lib/tools/credentialHosts.js +165 -0
  126. package/dist/lib/tools/credentialStore.js +9 -2
  127. package/dist/lib/tools/credentialUses.d.ts +92 -0
  128. package/dist/lib/tools/credentialUses.js +112 -0
  129. package/dist/lib/tools/mcpToolFactory.d.ts +25 -2
  130. package/dist/lib/tools/mcpToolFactory.js +179 -21
  131. package/dist/lib/tools/oauthConsent.js +8 -1
  132. package/dist/lib/tools/oauthHosts.d.ts +73 -0
  133. package/dist/lib/tools/oauthHosts.js +154 -0
  134. package/dist/lib/tools/oauthTools.d.ts +242 -0
  135. package/dist/lib/tools/oauthTools.js +447 -0
  136. package/dist/lib/tools/openapiTools.d.ts +11 -6
  137. package/dist/lib/tools/openapiTools.js +88 -38
  138. package/dist/lib/tools/taskTools.d.ts +61 -6
  139. package/dist/lib/tools/taskTools.js +145 -21
  140. package/dist/lib/workflow/turn.d.ts +22 -3
  141. package/dist/lib/workflow/turn.js +151 -31
  142. package/dist/lib/workflow.d.ts +14 -7
  143. package/dist/lib/workflow.js +14 -7
  144. package/dist/lib/workflowConfig.d.ts +6 -0
  145. package/dist/scripts/a2a_server.d.ts +36 -0
  146. package/dist/scripts/a2a_server.js +131 -1
  147. package/dist/scripts/assistant_worker.d.ts +12 -0
  148. package/dist/scripts/assistant_worker.js +81 -22
  149. package/dist/scripts/doctor.d.ts +7 -2
  150. package/dist/scripts/doctor.js +9 -4
  151. package/dist/scripts/setup.d.ts +23 -0
  152. package/dist/scripts/setup.js +154 -0
  153. package/dist/scripts/skills_install.d.ts +3 -1
  154. package/dist/scripts/skills_install.js +17 -2
  155. package/dist/scripts/yaml_codemod.d.ts +62 -0
  156. package/dist/scripts/yaml_codemod.js +652 -0
  157. package/package.json +8 -2
  158. package/skills/README.md +11 -2
  159. package/skills/briefs/_shared.md +1 -1
  160. package/skills/briefs/melchizedek-author.md +7 -5
  161. package/skills/briefs/melchizedek-models.md +7 -4
  162. package/skills/briefs/melchizedek.md +1 -0
  163. package/skills/briefs/skills-readme.md +3 -1
  164. package/skills/melchizedek/SKILL.md +1 -0
  165. package/skills/melchizedek-author/SKILL.md +7 -4
  166. package/skills/melchizedek-author/assets/minimal.yaml +13 -13
  167. package/skills/melchizedek-models/SKILL.md +11 -4
  168. package/skills/melchizedek-onboard/SKILL.md +49 -0
  169. package/skills/melchizedek-onboard-cloud/SKILL.md +38 -0
  170. package/skills/melchizedek-onboard-keys/SKILL.md +35 -0
  171. package/skills/melchizedek-onboard-local/SKILL.md +41 -0
  172. package/skills/melchizedek-onboard-serve/SKILL.md +43 -0
@@ -13,12 +13,21 @@
13
13
  *
14
14
  * ONE CALL, AS ADK'S AgentTool RAN IT:
15
15
  * 1. The message: `{ role: 'user', parts: [{ text: args.request }] }`.
16
- * 2. The subagent's session: the one under the subagent's own name as the
17
- * app name, with the caller's user id and session id. It is created on
18
- * the first call, with the caller's state as it stands (the session's,
19
- * then this call's writes; `temp:` keys dropped), and kept: a later
20
- * call, in this turn or another, continues it. This is not a branch of
21
- * the caller's session; ADK's AgentTool runs a Runner of its own.
16
+ * 2. The subagent's session: filed under the agent path (childAppName,
17
+ * ADR 0111), with the caller's user id and session id: the caller's
18
+ * app name, the caller's name and the subagent's at the top
19
+ * (`<app>/<caller>/<subagent>`), the caller's app name and the
20
+ * subagent's below it (`<app>/<caller>/<subagent>/<inner>`). It is
21
+ * created on the first call, with the caller's state as it stands (the
22
+ * session's, then this call's writes; `temp:` keys dropped), and kept:
23
+ * a later call, in this turn or another, continues it. This is not a
24
+ * branch of the caller's session; ADK's AgentTool runs a Runner of its
25
+ * own. ADK filed it under the subagent's name alone, so two syndicates
26
+ * with a same-named subagent on one conversation shared it. A session
27
+ * stored under that old key is still read (legacyChild): when no
28
+ * session exists under the path and the caller has called the
29
+ * subagent before (or is resuming a pause found there), the old one is
30
+ * continued, so a stored conversation resumes where it stopped.
22
31
  * 3. A turn already stopped answers an empty text, before or after the
23
32
  * message is stored.
24
33
  * 4. The message is stored as a user event under a fresh invocation id
@@ -34,13 +43,34 @@
34
43
  * schema it is parsed as JSON; text that does not parse fails the call
35
44
  * with the parser's message, as on ADK.
36
45
  *
37
- * A PAUSE INSIDE A SUBAGENT (an ask_user call, an approval request) cannot
38
- * reach the caller, exactly as under ADK (ADR 0028): the child run ends
39
- * paused, its last event carries no text, the call answers '', and the gated
40
- * tool never runs. WS6-2a lifts this.
46
+ * A PAUSE INSIDE A SUBAGENT (an ask_user call, an approval request, or a
47
+ * pause inside its own delegated call) reaches the caller (ADR 0110, which
48
+ * lifts ADR 0028's refusal). The child run ends paused; the call resolves to
49
+ * a SubagentPause naming the ids it waits on, and the caller stores no
50
+ * response to it: the call stays open, and the caller's run ends paused too
51
+ * (agentLoop.ts). The turn finds the pause by walking down the open calls
52
+ * (interrupts.ts delegatedPauses) and reports it with the agent path. The
53
+ * answer comes back as the caller's next user message; the caller resumes
54
+ * the open call (resumeSubagent), which stores the same answer parts as
55
+ * the child's next message, under a fresh invocation id, and runs the
56
+ * child's loop on: it reads the answer from its own session as a top-level
57
+ * agent reads it (an approval runs or refuses its pinned call, a question's
58
+ * answer is the call's response), finishes, and its answer is the open
59
+ * call's response. A child that pauses again leaves the call open again.
60
+ * A skill script's approval is the same pause, and so is an OAuth consent
61
+ * request (ADR 0118): the child gets the caller's consent step, its flows
62
+ * bound to the caller's app (consentPinnedTo), and a grant's answer travels
63
+ * down the open call as a decision does.
41
64
  *
42
- * Calls to subagents in one step run one after another, in call order, as
43
- * ADK runs them; running them concurrently is WS6.
65
+ * CALLS TO SUBAGENTS IN ONE STEP RUN CONCURRENTLY (ADR 0116), under the
66
+ * caller's `max_concurrency` (the syndicate's root key, default
67
+ * DEFAULT_MAX_CONCURRENCY): agentLoop.ts runs each delegated call through
68
+ * the step's DelegationGate, which starts them in call order as slots free
69
+ * up, and runs two calls to the same subagent (one child session) one after
70
+ * the other. The step's responses are stored once every call has answered,
71
+ * in call order, whatever order the children finished in; a child that
72
+ * pauses leaves its call open while the others finish and are stored. ADK
73
+ * ran them one after another; `max_concurrency: 1` keeps that order.
44
74
  *
45
75
  * NESTED SYNDICATES: a `yaml_reference` subagent is the nested syndicate's
46
76
  * orchestrator, named as the entry, listing its own subagent tools. It runs
@@ -49,6 +79,13 @@
49
79
  *
50
80
  * A NESTED WORKFLOW (ADR 0098): a `yaml_reference` to a workflow syndicate
51
81
  * is a workflowSubagentTool, holding the whole graph as a WorkflowSubagent.
82
+ * A walk that ends paused (an ask_user node's question, an agent node's
83
+ * approval request) leaves the call open as a child loop's pause does
84
+ * (ADR 0111): the call resolves to a SubagentPause naming the walk's open
85
+ * interrupts, and resumeWorkflowSubagent walks the graph again on the
86
+ * answer, which the walk's own resume (lib/workflow/resume.ts) reads as it
87
+ * reads a top-level workflow's. An `ask_user` answer reaches the walk as
88
+ * ADK's explicit reply to its `adk_request_input` call.
52
89
  * A call runs as steps 1 to 6 say, with the walk in place of the loop: the
53
90
  * graph's walk (lib/workflow/turn.ts, runNativeWorkflow, which
54
91
  * lib/compileNative.ts hands over) stores the message itself and yields the
@@ -69,7 +106,9 @@ import { randomUUID } from 'node:crypto';
69
106
  import { resolveAdapter } from '../../models/registry.js';
70
107
  import { createTurnEvent } from '../events.js';
71
108
  import { TEMP_STATE_PREFIX } from '../sessions.js';
109
+ import { REQUEST_INPUT_CALL } from './history.js';
72
110
  import { SelfCorrection } from './selfCorrection.js';
111
+ import { currentTurnControl } from '../turnControl.js';
73
112
  // ── The subagent tool ────────────────────────────────────────────────────────
74
113
  /** Where a subagent tool keeps the agent it runs. A global symbol, so a second copy of this module still finds it. */
75
114
  export const SUBAGENT = Symbol.for('melchizedek.subagent');
@@ -128,13 +167,91 @@ export function subagentOf(tool) {
128
167
  return agent;
129
168
  return undefined;
130
169
  }
131
- const queues = new WeakMap();
132
- /** Runs `run` after every earlier call queued under `key`, whether it succeeded or not. */
133
- function inTurn(key, run) {
134
- const before = queues.get(key) ?? Promise.resolve();
135
- const mine = before.then(run, run);
136
- queues.set(key, mine.then(() => undefined, () => undefined));
137
- return mine;
170
+ // ── Running one call ─────────────────────────────────────────────────────────
171
+ /**
172
+ * A delegated call whose child run ended paused (ADR 0110): the ids the
173
+ * child waits on. The caller stores no response to the call and ends its
174
+ * own run paused.
175
+ */
176
+ export class SubagentPause {
177
+ pending;
178
+ constructor(pending) {
179
+ this.pending = pending;
180
+ }
181
+ }
182
+ /** Delegated calls one step runs at once when the syndicate sets no `max_concurrency` (ADR 0116). */
183
+ export const DEFAULT_MAX_CONCURRENCY = 4;
184
+ /**
185
+ * The delegated calls of one step (ADR 0116): at most `limit` run at once,
186
+ * started in the order they entered (agentLoop.ts enters them in call
187
+ * order, synchronously, before any of them runs), and calls sharing a key
188
+ * (the subagent's name, so one child session) run one after another in
189
+ * that order. A slot is held from the call's start to its release, which
190
+ * comes as soon as the child's run ends: what the step does after (self-
191
+ * correction's bookkeeping, which waits on earlier calls) holds no slot. A
192
+ * call waiting on an earlier same-key call holds its slot meanwhile: the
193
+ * earlier call already holds one, so the wait always ends.
194
+ */
195
+ export class DelegationGate {
196
+ limit;
197
+ active = 0;
198
+ waiting = [];
199
+ lanes = new Map();
200
+ constructor(limit = DEFAULT_MAX_CONCURRENCY) {
201
+ this.limit = Number.isInteger(limit) && limit > 0 ? limit : DEFAULT_MAX_CONCURRENCY;
202
+ }
203
+ /** Enters one call under `key`, behind every call entered before it. */
204
+ enter(key) {
205
+ let granted = false;
206
+ let released = false;
207
+ let freeLane;
208
+ const laneDone = new Promise((resolve) => (freeLane = resolve));
209
+ const slot = new Promise((resolve) => {
210
+ this.waiting.push(() => {
211
+ granted = true;
212
+ resolve();
213
+ });
214
+ this.pump();
215
+ });
216
+ const before = this.lanes.get(key);
217
+ this.lanes.set(key, laneDone);
218
+ const ready = Promise.all([slot, before]).then(() => undefined);
219
+ const release = () => {
220
+ if (released)
221
+ return;
222
+ released = true;
223
+ freeLane();
224
+ if (granted) {
225
+ this.active -= 1;
226
+ this.pump();
227
+ }
228
+ else {
229
+ // Never started (a step that failed before reaching the child): the slot it would have taken is not held, and it never will be.
230
+ void slot.then(() => {
231
+ this.active -= 1;
232
+ this.pump();
233
+ });
234
+ }
235
+ };
236
+ return { ready, release };
237
+ }
238
+ /** Runs `run` under a ticket for `key`: once ready, the ticket released when it settles. */
239
+ async run(key, run) {
240
+ const ticket = this.enter(key);
241
+ try {
242
+ await ticket.ready;
243
+ return await run();
244
+ }
245
+ finally {
246
+ ticket.release();
247
+ }
248
+ }
249
+ pump() {
250
+ while (this.active < this.limit && this.waiting.length > 0) {
251
+ this.active += 1;
252
+ this.waiting.shift()();
253
+ }
254
+ }
138
255
  }
139
256
  /** ADK's AgentTool answer: the last event's non-thought text, joined by a newline; parsed under an output schema. */
140
257
  function answerOf(agent, last) {
@@ -148,11 +265,107 @@ function answerOf(agent, last) {
148
265
  .join('\n');
149
266
  return agent.outputSchema ? JSON.parse(text) : text;
150
267
  }
151
- /** The child's session, as ADK's AgentTool gets or creates it: under the subagent's name, from the caller's state the first time. */
152
- async function childSession(name, context, scope) {
268
+ /**
269
+ * The app name a delegated call's child session is filed under (ADR 0111):
270
+ * the agent path. Below a top-level agent, its app name, its name and the
271
+ * subagent's (`<app>/<caller>/<subagent>`); below a delegated subagent,
272
+ * whose own session is already filed under its path, that app name and the
273
+ * subagent's. Agent names are identifiers (lib/syndicateSchema.ts), so
274
+ * never hold a `/`.
275
+ */
276
+ export function childAppName(parent, caller, name) {
277
+ return parent.delegated ? `${parent.appName}/${name}` : `${parent.appName}/${caller}/${name}`;
278
+ }
279
+ /**
280
+ * The app name a nested syndicate run as a dispatch route or a workflow
281
+ * node is filed under (ADR 0119, ADR 0120): the session that runs it, and
282
+ * the entry's name behind its kind (`<app>/route:<route>`,
283
+ * `<walk's app>/node:<node>`). Agent names are identifiers, so a `:` never
284
+ * occurs in one, and a delegated call's key (childAppName: names only)
285
+ * never equals a route's or a node's: a delegate orchestrator `X` calling
286
+ * `Y` files `<app>/X/Y`, a route `X` with a nested node `Y` files
287
+ * `<app>/route:X/node:Y`. F3 (ADR 0119) filed it under `<app>/<entry>`
288
+ * (entryAppNameF3) and ADR 0106 under the entry's name alone; entrySession
289
+ * and the pause walk still read both.
290
+ */
291
+ export function entryAppName(parentAppName, name, kind) {
292
+ return `${parentAppName}/${kind}:${name}`;
293
+ }
294
+ /** Where ADR 0119 filed a route's or a node's session before the kind segment (ADR 0120): `<app>/<entry>`. */
295
+ export function entryAppNameF3(parentAppName, name) {
296
+ return `${parentAppName}/${name}`;
297
+ }
298
+ /**
299
+ * The session a delegated call ran in before ADR 0111, filed under the
300
+ * subagent's name alone, when it is the one to continue (`continues`): the
301
+ * caller of a stored conversation called the subagent there before, or a
302
+ * pause was found waiting in it. Read only when no session exists under the
303
+ * agent path.
304
+ */
305
+ export async function legacyChild(sessions, key, name, continues) {
306
+ if (!continues)
307
+ return undefined;
308
+ return sessions.get({ appName: name, userId: key.userId, sessionId: key.sessionId });
309
+ }
310
+ /**
311
+ * The session a route's or a node's entry ran in before the kind segment
312
+ * (ADR 0120), when it is the one to continue (`continues`): the one F3
313
+ * filed under `<app>/<entry>`, else the one ADR 0106 filed under the
314
+ * entry's name alone. Read only when none exists under entryAppName.
315
+ */
316
+ export async function legacyEntry(sessions, key, name, continues) {
317
+ if (!continues)
318
+ return undefined;
319
+ return (await sessions.get({ appName: entryAppNameF3(key.appName, name), userId: key.userId, sessionId: key.sessionId })) ?? (await legacyChild(sessions, key, name, true));
320
+ }
321
+ /**
322
+ * The child session a nested syndicate run as a dispatch route or a
323
+ * workflow node runs on (ADR 0119, ADR 0120): the one filed under
324
+ * entryAppName, else, when the entry ran in `parent` before (an event it
325
+ * authored there) or a pause was found waiting below it (`resuming`), the
326
+ * one filed under an older key (legacyEntry), else a new one from `state`.
327
+ */
328
+ export async function entrySession(sessions, parent, name, kind, state, resuming = false) {
329
+ const key = { appName: entryAppName(parent.appName, name, kind), userId: parent.userId, sessionId: parent.sessionId };
330
+ const own = await sessions.get(key);
331
+ if (own)
332
+ return own;
333
+ const legacy = await legacyEntry(sessions, parent, name, resuming || parent.events.some((e) => e.author === name));
334
+ return legacy ?? (await sessions.create({ ...key, state }));
335
+ }
336
+ /** Whether `caller` called `name` in `events` in a call other than `callId`: the subagent ran for it before. */
337
+ function calledBefore(events, caller, name, callId) {
338
+ return events.some((e) => e.author === caller && (e.content?.parts ?? []).some((p) => p.functionCall?.name === name && p.functionCall.id !== callId));
339
+ }
340
+ /**
341
+ * The child's session, as ADK's AgentTool gets or creates it: filed under
342
+ * the agent path (childAppName), from the caller's state the first time. A
343
+ * session under the old key is continued when the caller called the
344
+ * subagent before, or when `resuming` a pause found there (legacyChild):
345
+ * a syndicate that never called it, another syndicate's caller included,
346
+ * starts its own.
347
+ */
348
+ async function childSession(name, context, scope, resuming) {
153
349
  const { sessions, session: parent } = scope.ctx;
154
- const key = { appName: name, userId: parent.userId, sessionId: parent.id };
155
- return (await sessions.get(key)) ?? (await sessions.create({ ...key, state: { ...scope.stateBase, ...context.stateDelta } }));
350
+ const key = { appName: childAppName({ appName: parent.appName, delegated: scope.ctx.delegated === true }, scope.caller, name), userId: parent.userId, sessionId: parent.id };
351
+ const own = await sessions.get(key);
352
+ if (own)
353
+ return own;
354
+ const legacy = await legacyChild(sessions, key, name, resuming || calledBefore(parent.events, scope.caller, name, context.functionCallId));
355
+ return legacy ?? (await sessions.create({ ...key, state: { ...scope.stateBase, ...context.stateDelta } }));
356
+ }
357
+ /**
358
+ * The consent step as a child run reaches it (ADR 0118): a flow it begins
359
+ * is bound to `appName`, the caller's app, so the callback stores the grant
360
+ * under the app the run's pinned credentials read (the root's), never under
361
+ * the child session's agent path. A grandchild pins its caller's pinned
362
+ * step again; the first pin is applied last, so the root's app wins.
363
+ */
364
+ export function consentPinnedTo(consent, appName) {
365
+ return {
366
+ has: (provider) => consent.has(provider),
367
+ begin: (binding) => consent.begin({ ...binding, appName }),
368
+ };
156
369
  }
157
370
  /** Step 5: an event's state writes, `temp:` keys aside, into the call's state delta. */
158
371
  function recordState(event, context) {
@@ -168,13 +381,22 @@ function recordState(event, context) {
168
381
  * throws, and for an output-schema answer that does not parse.
169
382
  */
170
383
  export function runSubagent(agent, args, context, scope) {
171
- return inTurn(scope.queue, () => runChild(agent, args, context, scope));
384
+ return runChild(agent, { role: 'user', parts: [{ text: args.request }] }, context, scope, false);
172
385
  }
173
- async function runChild(agent, args, context, scope) {
386
+ /**
387
+ * Resumes a delegated call the child left paused (ADR 0110): `answer` (the
388
+ * caller's user message parts that answer what waits below the call) is
389
+ * stored as the child's next message and the child's loop runs on, as
390
+ * runSubagent runs it. Resolves as runSubagent does, a SubagentPause again
391
+ * when the child pauses again.
392
+ */
393
+ export function resumeSubagent(agent, answer, context, scope) {
394
+ return runChild(agent, { role: 'user', parts: answer }, context, scope, true);
395
+ }
396
+ async function runChild(agent, content, context, scope, resuming) {
174
397
  const { ctx, signal } = scope;
175
398
  const { sessions } = ctx;
176
- const content = { role: 'user', parts: [{ text: args.request }] };
177
- const session = await childSession(agent.name, context, scope);
399
+ const session = await childSession(agent.name, context, scope, resuming);
178
400
  if (signal?.aborted)
179
401
  return '';
180
402
  const invocationId = `e-${randomUUID()}`;
@@ -187,24 +409,36 @@ async function runChild(agent, args, context, scope) {
187
409
  invocationId,
188
410
  userContent: content,
189
411
  stream: false,
412
+ // Its own delegations are filed below its path (childAppName, ADR 0111).
413
+ delegated: true,
190
414
  // ADK's AgentTool builds its sub-runner without the reflect-and-retry
191
415
  // plugins, so a subagent's own errors are not retried (ADR 0075).
192
416
  selfCorrection: new SelfCorrection({ model_errors: 0, tool_errors: 0 }),
193
417
  ...(ctx.memory ? { memory: ctx.memory } : {}),
194
- // The parent's grants, pinned to the root's app (ADR 0072). Not its consent step: a subagent's
195
- // pause would end inside the call, as ADK's AgentTool swallows it (ADR 0085).
418
+ // The parent's grants, pinned to the root's app (ADR 0072), and its consent step: a call that
419
+ // needs a grant pauses the child, and the pause reaches the turn through the open call (ADR 0118).
196
420
  ...(ctx.credentials ? { credentials: ctx.credentials } : {}),
421
+ ...(ctx.consent ? { consent: consentPinnedTo(ctx.consent, ctx.session.appName) } : {}),
197
422
  ...(ctx.signal ? { signal: ctx.signal } : {}),
198
423
  ...(ctx.adapterFor ? { adapterFor: ctx.adapterFor } : {}),
199
424
  ...(ctx.log ? { log: ctx.log } : {}),
200
425
  });
201
426
  let last;
202
- for await (const event of loop) {
427
+ for (;;) {
428
+ const next = await loop.next();
429
+ if (next.done) {
430
+ // A child that ended waiting on a person leaves the call open (ADR 0110).
431
+ if (next.value.reason === 'paused' && !signal?.aborted)
432
+ return new SubagentPause(next.value.pending ?? []);
433
+ break;
434
+ }
203
435
  // ADK's Runner stops yielding once the turn stopped: the answer is the last event it yielded.
204
- if (signal?.aborted)
436
+ if (signal?.aborted) {
437
+ await loop.return(undefined);
205
438
  break;
206
- recordState(event, context);
207
- last = event;
439
+ }
440
+ recordState(next.value, context);
441
+ last = next.value;
208
442
  }
209
443
  return answerOf(agent, last);
210
444
  }
@@ -215,11 +449,26 @@ async function runChild(agent, args, context, scope) {
215
449
  * yielded event's text, '' for none. Throws what the walk throws.
216
450
  */
217
451
  export function runWorkflowSubagent(workflow, args, context, scope) {
218
- return inTurn(scope.queue, () => walkChild(workflow, args, context, scope));
452
+ return walkChild(workflow, [{ text: args.request }], context, scope, false);
453
+ }
454
+ /** The engine's ask_user tool name (lib/runtime/questions.ts ASK_USER). */
455
+ const ASK_USER_CALL = 'ask_user';
456
+ /**
457
+ * Resumes a delegated call whose nested workflow ended paused (ADR 0111):
458
+ * the graph is walked again on `answer` (the caller's user message parts
459
+ * that answer the walk's open interrupt), which the walk's resume reads as
460
+ * it reads a top-level workflow's. An approval decision goes as it came; an
461
+ * `ask_user` answer goes as ADK's explicit reply to the `adk_request_input`
462
+ * call it answers, under that call's name. Resolves as runWorkflowSubagent
463
+ * does, a SubagentPause again when the walk pauses again.
464
+ */
465
+ export function resumeWorkflowSubagent(workflow, answer, context, scope) {
466
+ const parts = answer.map((p) => (p.functionResponse?.name === ASK_USER_CALL ? { functionResponse: { ...p.functionResponse, name: REQUEST_INPUT_CALL } } : p));
467
+ return walkChild(workflow, parts, context, scope, true);
219
468
  }
220
- async function walkChild(workflow, args, context, scope) {
469
+ async function walkChild(workflow, userParts, context, scope, resuming) {
221
470
  const { ctx, signal } = scope;
222
- const session = await childSession(workflow.name, context, scope);
471
+ const session = await childSession(workflow.name, context, scope, resuming);
223
472
  if (signal?.aborted)
224
473
  return '';
225
474
  const walk = workflow.walk({
@@ -227,7 +476,7 @@ async function walkChild(workflow, args, context, scope) {
227
476
  appName: session.appName,
228
477
  userId: session.userId,
229
478
  sessionId: session.id,
230
- userParts: [{ text: args.request }],
479
+ userParts,
231
480
  adapterFor: ctx.adapterFor ?? resolveAdapter,
232
481
  // As runChild: ADK's AgentTool runs its sub-runner without the reflect-and-retry plugins (ADR 0075).
233
482
  selfCorrection: new SelfCorrection({ model_errors: 0, tool_errors: 0 }),
@@ -237,9 +486,20 @@ async function walkChild(workflow, args, context, scope) {
237
486
  ...(ctx.credentials ? { credentials: ctx.credentials } : {}),
238
487
  });
239
488
  let last;
240
- for await (const event of walk) {
241
- if (signal?.aborted)
489
+ for (;;) {
490
+ const next = await walk.next();
491
+ if (next.done) {
492
+ // A walk that ended waiting on a person leaves the call open (ADR 0111).
493
+ const paused = next.value?.run?.interruptIds ?? [];
494
+ if (paused.length > 0 && !signal?.aborted)
495
+ return new SubagentPause(paused);
496
+ break;
497
+ }
498
+ if (signal?.aborted) {
499
+ await walk.return(undefined);
242
500
  break;
501
+ }
502
+ const event = next.value;
243
503
  if (event.partial)
244
504
  continue;
245
505
  recordState(event, context);
@@ -247,3 +507,94 @@ async function walkChild(workflow, args, context, scope) {
247
507
  }
248
508
  return answerOf({}, last);
249
509
  }
510
+ /** The engine's approval request call (lib/runtime/approvals.ts APPROVAL_REQUEST). */
511
+ const APPROVAL_REQUEST_CALL = 'adk_request_confirmation';
512
+ /**
513
+ * The message a nested dispatch syndicate's turn reads (ADR 0120): the
514
+ * parts as they came, except an answer to a question, which comes down as
515
+ * an `ask_user` reply (a delegated call's resume) or an `adk_request_input`
516
+ * reply (a workflow node's), and goes in as the plain text it was, as a
517
+ * person's answer reaches a turn at the top.
518
+ */
519
+ function dispatchParts(parts) {
520
+ return parts.map((p) => {
521
+ const r = p.functionResponse;
522
+ if (!r || (r.name !== ASK_USER_CALL && r.name !== REQUEST_INPUT_CALL))
523
+ return p;
524
+ const result = r.response?.result;
525
+ return { text: typeof result === 'string' ? result : JSON.stringify(result ?? '') };
526
+ });
527
+ }
528
+ /**
529
+ * The request call a paused nested dispatch syndicate's turn waits on, as a
530
+ * workflow node raises it again on its caller's walk: an approval request
531
+ * pinned to the gated call, or an input request carrying the question.
532
+ * Undefined for a consent request, which a walk cannot raise (ADR 0119).
533
+ */
534
+ function requestCallOf(end) {
535
+ const id = end.interruptId;
536
+ if (!id)
537
+ return undefined;
538
+ const approval = end.approval;
539
+ if (approval) {
540
+ return {
541
+ id,
542
+ name: APPROVAL_REQUEST_CALL,
543
+ args: { originalFunctionCall: { name: approval.tool ?? '', args: approval.args ?? {}, ...(approval.callId ? { id: approval.callId } : {}) }, toolConfirmation: { confirmed: false } },
544
+ };
545
+ }
546
+ const input = end.input;
547
+ if (input) {
548
+ return {
549
+ id,
550
+ name: REQUEST_INPUT_CALL,
551
+ args: {
552
+ interruptId: id,
553
+ message: input.message ?? '',
554
+ ...(input.payload !== undefined ? { payload: input.payload } : {}),
555
+ ...(input.schema !== undefined ? { response_schema: input.schema } : {}),
556
+ },
557
+ };
558
+ }
559
+ return undefined;
560
+ }
561
+ /**
562
+ * Runs a nested dispatch syndicate as a walk (ADR 0120): its own turn, on
563
+ * the conversation its caller opened (`run`), through the turn runner the
564
+ * current turn carries (TurnControl.nestedDispatch), under the turn's
565
+ * controls. Completed, it yields one event, never stored, that carries the
566
+ * route's final text and the conversation's state writes: the caller reads
567
+ * the answer and records the writes from it, as from a walk's last event.
568
+ * Paused, it ends with the request's id, and the request call itself for a
569
+ * workflow node to raise again. Failed, it throws the turn's error. A
570
+ * delegated call, a dispatch route and a workflow node all run it.
571
+ */
572
+ export async function* nestedDispatchWalk(entry, run) {
573
+ const runner = currentTurnControl()?.nestedDispatch;
574
+ if (!runner)
575
+ throw new Error(`'${entry.name}' is a nested dispatch syndicate, which runs only inside a syndicate turn (runSyndicateTurn).`);
576
+ const end = await runner({
577
+ config: entry.config,
578
+ compile: entry.compile,
579
+ name: entry.name,
580
+ sessions: run.sessions,
581
+ appName: run.appName,
582
+ userId: run.userId,
583
+ sessionId: run.sessionId,
584
+ parts: dispatchParts(run.userParts),
585
+ });
586
+ if (end.status === 'canceled' || run.signal?.aborted || currentTurnControl()?.stopReason)
587
+ return { stopped: true };
588
+ if (end.status === 'failed')
589
+ throw new Error(end.error?.message ?? `${entry.name} failed.`);
590
+ if (end.status === 'input-required' && end.interruptId) {
591
+ const request = requestCallOf(end);
592
+ return { run: { interruptIds: [end.interruptId] }, ...(request ? { requests: [request] } : {}) };
593
+ }
594
+ yield createTurnEvent({
595
+ author: entry.name,
596
+ content: { role: 'model', parts: [{ text: end.text }] },
597
+ ...(Object.keys(end.stateDelta).length ? { actions: { stateDelta: end.stateDelta } } : {}),
598
+ });
599
+ return { run: { interruptIds: [] } };
600
+ }