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
@@ -72,6 +72,17 @@
72
72
  *
73
73
  * DELEGATION: a call to a subagent tool runs the subagent as its own child
74
74
  * loop, as ADK's AgentTool runs it (lib/runtime/native/delegate.ts, ADR 0074).
75
+ * A step's delegated calls run at once through the step's DelegationGate,
76
+ * at most the agent's `maxConcurrency` (the syndicate's `max_concurrency`)
77
+ * at a time, entered in call order, their responses stored in call order
78
+ * (ADR 0116).
79
+ * A child that ends paused leaves the call open (ADR 0110): no response is
80
+ * stored for it, the step's other responses are, and the run ends paused on
81
+ * the call's id. Before each step, after the approval resume, a delegated
82
+ * call whose pause the latest user message answers is resumed with that
83
+ * answer (interrupts.ts resumedDelegations, delegate.ts resumeSubagent) and
84
+ * its response stored; while another open call still waits, the run ends
85
+ * paused again on it without a model step.
75
86
  *
76
87
  * SELF-CORRECTION (ADR 0034, ADR 0075) is ADK's reflect-and-retry plugins,
77
88
  * ported to lib/runtime/native/selfCorrection.ts: the step declares the
@@ -102,6 +113,8 @@
102
113
  * Before each step, a credential request the latest user message answers
103
114
  * runs its paused call again (interrupts.ts grantedCalls, ADK's auth
104
115
  * preprocessor), ahead of the approval resume.
116
+ * Inside a delegated subagent the same pause leaves the caller's call open,
117
+ * and the grant's answer comes down through it (ADR 0118).
105
118
  *
106
119
  * NOT HERE (later tickets): transfer_to_agent (no compiled syndicate sets
107
120
  * subAgents). The run's spans (agent.invoke, model.call,
@@ -158,10 +171,17 @@ export interface AgentLoopContext extends Omit<ModelStepOptions, 'agent' | 'adap
158
171
  /**
159
172
  * The consent step (ADR 0085): a call whose provider the user has not
160
173
  * granted asks for it (`requestCredential`, or `accessToken` by itself),
161
- * and the run pauses on ADK's `adk_request_credential` call. Not passed to
162
- * a delegated subagent's loop.
174
+ * and the run pauses on ADK's `adk_request_credential` call. A delegated
175
+ * subagent's loop gets it too, its flows bound to the caller's app
176
+ * (delegate.ts consentPinnedTo, ADR 0118): its pause leaves the call open.
163
177
  */
164
178
  consent?: Pick<OAuthConsent, 'has' | 'begin'>;
179
+ /**
180
+ * The run is a delegated subagent's, on its own child session filed under
181
+ * its agent path (lib/runtime/native/delegate.ts childAppName, ADR 0111):
182
+ * its own delegations are filed below that path. Set by runChild only.
183
+ */
184
+ delegated?: boolean;
165
185
  }
166
186
  /** How the run ended. */
167
187
  export type AgentLoopEndReason =
@@ -181,7 +201,7 @@ export interface AgentLoopEnd {
181
201
  steps: number;
182
202
  /** The last event stored. */
183
203
  lastEvent?: TurnEvent;
184
- /** For `paused`: the ids waiting on a person (the ask_user call ids, or the adk_request_confirmation call ids). */
204
+ /** For `paused`: the ids waiting on a person (the ask_user call ids, the adk_request_confirmation call ids, or the delegated calls whose child waits; ADR 0110). */
185
205
  pending?: string[];
186
206
  /** For `stopped`: the turn's code and message. */
187
207
  stop?: StepStop;
@@ -72,6 +72,17 @@
72
72
  *
73
73
  * DELEGATION: a call to a subagent tool runs the subagent as its own child
74
74
  * loop, as ADK's AgentTool runs it (lib/runtime/native/delegate.ts, ADR 0074).
75
+ * A step's delegated calls run at once through the step's DelegationGate,
76
+ * at most the agent's `maxConcurrency` (the syndicate's `max_concurrency`)
77
+ * at a time, entered in call order, their responses stored in call order
78
+ * (ADR 0116).
79
+ * A child that ends paused leaves the call open (ADR 0110): no response is
80
+ * stored for it, the step's other responses are, and the run ends paused on
81
+ * the call's id. Before each step, after the approval resume, a delegated
82
+ * call whose pause the latest user message answers is resumed with that
83
+ * answer (interrupts.ts resumedDelegations, delegate.ts resumeSubagent) and
84
+ * its response stored; while another open call still waits, the run ends
85
+ * paused again on it without a model step.
75
86
  *
76
87
  * SELF-CORRECTION (ADR 0034, ADR 0075) is ADK's reflect-and-retry plugins,
77
88
  * ported to lib/runtime/native/selfCorrection.ts: the step declares the
@@ -102,6 +113,8 @@
102
113
  * Before each step, a credential request the latest user message answers
103
114
  * runs its paused call again (interrupts.ts grantedCalls, ADK's auth
104
115
  * preprocessor), ahead of the approval resume.
116
+ * Inside a delegated subagent the same pause leaves the caller's call open,
117
+ * and the grant's answer comes down through it (ADR 0118).
105
118
  *
106
119
  * NOT HERE (later tickets): transfer_to_agent (no compiled syndicate sets
107
120
  * subAgents). The run's spans (agent.invoke, model.call,
@@ -125,8 +138,8 @@ import { MAX_VALUE_DEPTH, nestedDeeperThan } from '../valueDepth.js';
125
138
  import { currentTurnSignal } from '../turnControl.js';
126
139
  import { compactBeforeStep } from './compaction.js';
127
140
  import { ADK_CALL_ID_PREFIX } from './history.js';
128
- import { runSubagent, runWorkflowSubagent, subagentOf, workflowSubagentOf } from './delegate.js';
129
- import { approvedCalls, grantedCalls } from './interrupts.js';
141
+ import { DEFAULT_MAX_CONCURRENCY, DelegationGate, SubagentPause, resumeSubagent, resumeWorkflowSubagent, runSubagent, runWorkflowSubagent, subagentOf, workflowSubagentOf } from './delegate.js';
142
+ import { approvedCalls, grantedCalls, resumedDelegations } from './interrupts.js';
130
143
  import { SelfCorrection } from './selfCorrection.js';
131
144
  import { eitherSignal, runModelStep, stopOf } from './step.js';
132
145
  import { taskNodeRun } from './taskMode.js';
@@ -342,7 +355,7 @@ async function runOwnTool(tool, args, context) {
342
355
  throw new Error(`Error in tool '${tool.name}': ${message}`);
343
356
  }
344
357
  }
345
- async function runCall(scope, call, tools, correction, confirmation) {
358
+ async function runCall(scope, call, tools, correction, confirmation, resume, ticket) {
346
359
  const context = callContext(scope, call.id || undefined, confirmation);
347
360
  const name = call.name ?? '';
348
361
  const tool = name && tools.has(name) ? tools.get(name) : undefined;
@@ -363,17 +376,26 @@ async function runCall(scope, call, tools, correction, confirmation) {
363
376
  // Delegation (WS2-6): a subagent runs as its own child loop, a nested workflow as its own walk (ADR 0098), before the generic path (delegate.ts).
364
377
  const subagent = subagentOf(tool);
365
378
  const workflow = subagent ? undefined : workflowSubagentOf(tool);
366
- const delegation = { ctx: scope.ctx, stateBase: scope.stateBase, signal: scope.signal, runLoop: runAgentLoop, queue: scope };
379
+ const delegation = { ctx: scope.ctx, caller: scope.agent.name, stateBase: scope.stateBase, signal: scope.signal, runLoop: runAgentLoop };
367
380
  let response;
368
381
  let failure;
369
382
  try {
383
+ // ADR 0116: a delegated call waits for its turn in the step's gate, and frees it as soon as the child's run ends.
384
+ if (ticket && (subagent || workflow))
385
+ await ticket.ready;
370
386
  response = subagent
371
- ? await runSubagent(subagent, args, context, delegation)
387
+ ? resume
388
+ ? await resumeSubagent(subagent, resume, context, delegation)
389
+ : await runSubagent(subagent, args, context, delegation)
372
390
  : workflow
373
- ? await runWorkflowSubagent(workflow, args, context, delegation)
391
+ ? resume
392
+ ? await resumeWorkflowSubagent(workflow, resume, context, delegation)
393
+ : await runWorkflowSubagent(workflow, args, context, delegation)
374
394
  : await runOwnTool(tool, args, context);
395
+ ticket?.release();
375
396
  }
376
397
  catch (e) {
398
+ ticket?.release();
377
399
  failure = e instanceof Error ? e.message : e;
378
400
  // Self-correction answers a thrown Error with reflection guidance in its place; a call waiting on a grant is not a failure.
379
401
  const guided = isEmptyRecord(context.actions.requestedAuthConfigs) ? await correction?.failed(toolName, args, e) : undefined;
@@ -386,6 +408,12 @@ async function runCall(scope, call, tools, correction, confirmation) {
386
408
  const provider = String(asked[0]?.credentialKey ?? '');
387
409
  return { part: { functionResponse: { id: context.functionCallId, name: toolName, response: { result: CONSENT_TEXTS.pending(provider) } } }, actions: context.actions };
388
410
  }
411
+ // A child that ended paused (ADR 0110): the call stays open, with no response, and the run ends paused on it.
412
+ if (response instanceof SubagentPause) {
413
+ if (context.functionCallId)
414
+ (scope.paused ??= []).push(context.functionCallId);
415
+ return isDefaultActions(context.actions) ? undefined : { actions: context.actions };
416
+ }
389
417
  if (failure === undefined)
390
418
  await correction?.answered(toolName, response);
391
419
  // As ADK: a long-running call with no response answers nothing, even when it threw.
@@ -401,10 +429,33 @@ async function runCall(scope, call, tools, correction, confirmation) {
401
429
  * The calls run, in parallel, as the one response event ADK stores for them
402
430
  * (not yet stored); undefined when none answered. `confirmations` holds the
403
431
  * person's answer for a pinned call an approval resumes, by call id.
432
+ *
433
+ * Delegated calls (a subagent, a nested workflow) run through the step's
434
+ * DelegationGate (ADR 0116): at most the agent's `maxConcurrency` at once,
435
+ * entered in call order, two calls to one subagent one after the other.
436
+ * Other calls start at once, as before. The event is built once every call
437
+ * has answered, its parts in call order, and the open calls a paused child
438
+ * left (scope.paused) are listed in call order too: neither depends on
439
+ * which call finished first.
404
440
  */
405
- async function runCalls(scope, calls, tools, confirmations) {
441
+ async function runCalls(scope, calls, tools, confirmations, resumes) {
406
442
  const order = scope.selfCorrection?.forCalls(scope.ctx.invocationId, calls.length);
407
- const outcomes = (await Promise.all(calls.map((call, i) => traceToolCall(call, tools.get(call.name ?? ''), () => runCall(scope, call, tools, order?.call(i), call.id ? confirmations?.get(call.id) : undefined)).finally(() => order?.release(i))))).filter((o) => !!o);
443
+ const gate = new DelegationGate(scope.agent.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY);
444
+ const outcomes = (await Promise.all(calls.map((call, i) => {
445
+ const tool = tools.get(call.name ?? '');
446
+ // Entered here, synchronously and in call order, before any call runs (ADR 0116).
447
+ const delegated = subagentOf(tool) ?? workflowSubagentOf(tool);
448
+ const ticket = delegated ? gate.enter(delegated.name) : undefined;
449
+ return traceToolCall(call, tool, () => runCall(scope, call, tools, order?.call(i), call.id ? confirmations?.get(call.id) : undefined, call.id ? resumes?.get(call.id) : undefined, ticket)).finally(() => {
450
+ ticket?.release();
451
+ order?.release(i);
452
+ });
453
+ }))).filter((o) => !!o);
454
+ // The open calls, in call order rather than the order their children paused in.
455
+ if (scope.paused && scope.paused.length > 1) {
456
+ const at = (id) => calls.findIndex((c) => c.id === id);
457
+ scope.paused.sort((a, b) => at(a) - at(b));
458
+ }
408
459
  if (outcomes.length === 0)
409
460
  return undefined;
410
461
  const base = { invocationId: scope.ctx.invocationId, author: scope.agent.name };
@@ -503,6 +554,32 @@ async function resumeGrants(agent, ctx, stateBase, selfCorrection) {
503
554
  const auth = authEvent(scope, response);
504
555
  return { response, ...(auth ? { auth } : {}) };
505
556
  }
557
+ /**
558
+ * Delegated calls the latest user message resumes (ADR 0110), before a step
559
+ * and after the approval resume: each open call whose child waits on what
560
+ * the message answers runs the child on with that answer, and their
561
+ * response is returned to be stored, with the open calls still paused:
562
+ * those the child paused again, and those the message did not answer.
563
+ * Undefined when the message answers none; 'stopped' when the turn stopped
564
+ * while they ran (nothing is stored).
565
+ */
566
+ async function resumeDelegations(agent, ctx, stateBase, selfCorrection) {
567
+ const resumed = await resumedDelegations(agent, ctx, ctx.sessions);
568
+ if (!resumed)
569
+ return undefined;
570
+ const calls = resumed.calls.filter((c) => {
571
+ const tool = resumed.tools.get(c.name ?? '');
572
+ return subagentOf(tool) || workflowSubagentOf(tool);
573
+ });
574
+ if (calls.length === 0)
575
+ return undefined;
576
+ const signal = eitherSignal(ctx.signal, currentTurnSignal());
577
+ const scope = { agent, ctx, stateBase, selfCorrection, ...(signal ? { signal } : {}) };
578
+ const response = await runCalls(scope, calls, resumed.tools, undefined, resumed.answers);
579
+ if (signal?.aborted)
580
+ return 'stopped';
581
+ return { ...(response ? { response } : {}), paused: [...(scope.paused ?? []), ...resumed.waiting] };
582
+ }
506
583
  // ── The model step, with partials and the fallback ───────────────────────────
507
584
  /** Runs a step, yielding its partial events as they arrive; returns the step's result. */
508
585
  async function* withPartials(start) {
@@ -640,6 +717,18 @@ async function* agentLoop(agent, ctx) {
640
717
  lastEvent = await store(resumed);
641
718
  yield lastEvent;
642
719
  }
720
+ // Delegation hook (WS6-2a, ADR 0110): an answer to a pause inside a delegated call resumes that call before the step.
721
+ const redelegated = await resumeDelegations(agent, ctx, withStateOverlay(session.state, runTemp.values()), selfCorrection);
722
+ if (redelegated === 'stopped')
723
+ return { reason: 'stopped', steps, lastEvent, stop: stopOf() };
724
+ if (redelegated) {
725
+ if (redelegated.response) {
726
+ lastEvent = await store(redelegated.response);
727
+ yield lastEvent;
728
+ }
729
+ if (redelegated.paused.length > 0)
730
+ return { reason: 'paused', steps, lastEvent, pending: redelegated.paused };
731
+ }
643
732
  // Compaction hook (WS2-9, lib/runtime/native/compaction.ts): with `context:`, the summary is stored before the step reads the history,
644
733
  // before the step budget, as ADK's request processors run before its call count.
645
734
  const compacted = await compactBeforeStep(agent, {
@@ -679,6 +768,8 @@ async function* agentLoop(agent, ctx) {
679
768
  // As ADK: a turn that stopped while the calls ran (a long subagent run, say) stores no response.
680
769
  if (scope.signal?.aborted)
681
770
  return { reason: 'stopped', steps, lastEvent, stop: stopOf() };
771
+ // Delegated calls whose child ended paused (ADR 0110): they stay open, and the run ends paused on them once the step's responses are stored.
772
+ const delegated = scope.paused ?? [];
682
773
  if (response) {
683
774
  const auth = authEvent(scope, response);
684
775
  if (auth) {
@@ -690,17 +781,22 @@ async function* agentLoop(agent, ctx) {
690
781
  if (confirmation) {
691
782
  lastEvent = await store(confirmation);
692
783
  yield lastEvent;
693
- return { reason: 'paused', steps, lastEvent, pending: [...(lastEvent.longRunningToolIds ?? [])] };
784
+ return { reason: 'paused', steps, lastEvent, pending: [...(lastEvent.longRunningToolIds ?? []), ...delegated] };
694
785
  }
695
786
  lastEvent = await store(response);
696
787
  yield lastEvent;
697
788
  // As ADK: the response asking for auth is final, and the run waits on its credential request (ADR 0085).
698
789
  if (authIds.length > 0)
699
- return { reason: 'paused', steps, lastEvent, pending: authIds };
790
+ return { reason: 'paused', steps, lastEvent, pending: [...authIds, ...delegated] };
700
791
  stepEnd = lastEvent;
701
- if (task?.finished)
792
+ if (task?.finished && delegated.length === 0)
702
793
  return { reason: 'final', steps, lastEvent, output: task.output };
703
794
  }
795
+ if (delegated.length > 0) {
796
+ const answered = new Set(getFunctionResponses(stepEnd).map((r) => r.id));
797
+ const longRunning = (modelEvent.longRunningToolIds ?? []).filter((id) => !answered.has(id));
798
+ return { reason: 'paused', steps, lastEvent, pending: [...longRunning, ...delegated] };
799
+ }
704
800
  }
705
801
  // ADK's runAsyncImpl: stop on a final event, unless it is an empty metadata event after tool calls.
706
802
  const emptyMetadata = stepEnd.author === agent.name && !stepEnd.partial && !hasParts(stepEnd) && isDefaultActions(stepEnd.actions);
@@ -0,0 +1,171 @@
1
+ /**
2
+ * lib/runtime/native/checkpoint.ts — durable long runs: the run's sessions
3
+ * snapshotted at every step boundary, and a run restored from the last
4
+ * snapshot taking the next step (ADR 0113, ADR 0015).
5
+ *
6
+ * WHY this file exists:
7
+ * A background job (ADR 0015) runs as one turn on an in-process session
8
+ * store. A worker that stopped mid-job lost everything the job had done,
9
+ * and the next claim started again from the first model call. The turn
10
+ * runner's signature is pinned by every surface and the boundary suite
11
+ * (ADR 0024), so durability lives at the one seam the runner already
12
+ * takes: the SessionService. checkpointingSessions wraps the in-process
13
+ * store a run writes, hands a snapshot of every session to a sink at each
14
+ * step boundary, and restores a snapshot into a fresh store so that the
15
+ * runner, unchanged, continues where the snapshot stood. runDurableTurn
16
+ * is runSyndicateTurn with that store and a sink.
17
+ *
18
+ * A STEP BOUNDARY is an append that carries function responses after which
19
+ * every function call any session of the run holds has its response (calls
20
+ * listed in their event's longRunningToolIds, which wait on a person, aside).
21
+ * So a parent waiting on a delegated subagent is never at a boundary, the
22
+ * subagent's own steps are not either, and a final answer never is: a
23
+ * resumed run redoes the step that was in flight, and the last one.
24
+ *
25
+ * WHAT IT GUARANTEES:
26
+ * - A resumed run's stored events equal an uninterrupted run's, ids and
27
+ * times of the steps run after the resume aside: restored events keep
28
+ * their ids, times and invocation ids, and their state deltas are
29
+ * applied again in order (InProcessSessionService.append, applyEvent).
30
+ * - The opening message is stored once. A session whose first event is
31
+ * the run's own message (the run's session, a workflow route's child)
32
+ * is held back on restore: it does not exist until the runner creates
33
+ * it, so `resumedSession` reads as on a fresh run, and the runner's
34
+ * append of the message is answered with the stored events, the
35
+ * opening one returned in its place, onto the caller's own session
36
+ * object. Every other session (a delegated subagent's) is restored at
37
+ * once, before the runner starts.
38
+ * - A checkpoint is used only by the run that wrote it, for the message
39
+ * it was written for: its runId and the sha256 of the message's parts
40
+ * must match, and its events must parse (parseTurnEvents). Otherwise
41
+ * the run starts fresh.
42
+ * - The sink's save resolving false means the run no longer holds its
43
+ * claim (the job was cancelled or leased elsewhere): runDurableTurn
44
+ * aborts the run and reports lostClaim. A save that throws is reported
45
+ * to onSaveError and the run goes on; a later attempt resumes from the
46
+ * older checkpoint.
47
+ *
48
+ * LIMITS:
49
+ * - A dispatch syndicate's route classifier runs in a throwaway store, so
50
+ * its model call is not checkpointed and runs again on resume.
51
+ * - A dispatch route that is an agent answers through the transcript
52
+ * projection (ProjectedSessionService, lib/session/transcript.ts), which
53
+ * copies the session when the route opens it. The held opening session
54
+ * is empty at that open, so the copy is too; the route's append of the
55
+ * message is answered with the stored events, and the projection takes
56
+ * every event the real session gained, unprojected (the current turn),
57
+ * so the route resumes like a single agent. A workflow route writes its
58
+ * own child session and resumes. Checkpoints saved before this, which
59
+ * 1.1.0 set aside, restore the same way.
60
+ * - A task backend stores a checkpoint only up to its cap
61
+ * (TaskBackendOptions.checkpointMaxBytes, lib/tools/taskTools.ts,
62
+ * default 5 MiB of JSON); a save above it keeps the previous checkpoint
63
+ * and still answers whether the claim holds, so a huge run resumes from
64
+ * an older boundary.
65
+ * - Each resumed attempt has its own step budget (max_steps, the loop's
66
+ * call ceiling); attempts are bounded by the queue's MAX_ATTEMPTS.
67
+ * - `temp:` state lasts one invocation and is never stored, so it does not
68
+ * survive a resume.
69
+ * - A workflow walk resumes through its own rules (ADR 0094); its
70
+ * checkpoint is the sessions it walks.
71
+ * - A checkpoint holds the run's sessions, so it carries the user's text,
72
+ * model output and tool results: it lives only where the run's record
73
+ * lives, under the same owner, and is deleted with it. This module never
74
+ * clears one; the caller does when the run finishes.
75
+ */
76
+ import type { TurnEvent } from '../events.ts';
77
+ import { InProcessSessionService } from '../sessions.ts';
78
+ import type { SessionService } from '../sessions.ts';
79
+ import type { MessagePart, SyndicateTurnOptions, SyndicateTurnResult } from '../syndicateTurn.ts';
80
+ export declare const CHECKPOINT_VERSION = 1;
81
+ /** One session of a run, as it stood at a step boundary. */
82
+ export interface CheckpointSession {
83
+ appName: string;
84
+ userId: string;
85
+ id: string;
86
+ /** The state given to create(), before any event. */
87
+ createdState: Record<string, unknown>;
88
+ events: TurnEvent[];
89
+ /** The first event is the run's own opening user message: restore holds the session back until the runner stores that message. */
90
+ opening: boolean;
91
+ }
92
+ /** A run's sessions at its latest step boundary. */
93
+ export interface RunCheckpoint {
94
+ version: 1;
95
+ runId: string;
96
+ /** The run's own session id: a resumed run uses it again. */
97
+ sessionId: string;
98
+ /** sha256 hex of JSON.stringify(parts) of the run's message (strings as `{ text }`). */
99
+ openingHash: string;
100
+ /** Step boundaries recorded so far, across every attempt. */
101
+ steps: number;
102
+ /** ISO time of the save. */
103
+ savedAt: string;
104
+ sessions: CheckpointSession[];
105
+ }
106
+ /** Where a run's checkpoints go. save resolves false when the run no longer holds its claim (cancelled, or re-leased): the run must stop. */
107
+ export interface CheckpointSink {
108
+ load(): Promise<RunCheckpoint | null>;
109
+ save(checkpoint: RunCheckpoint): Promise<boolean>;
110
+ }
111
+ export interface CheckpointingOptions {
112
+ runId: string;
113
+ /** The run's own session id (the one runSyndicateTurn is given). */
114
+ sessionId: string;
115
+ /** The run's message. */
116
+ parts: MessagePart[];
117
+ /** The checkpoint to restore; ignored unless its runId and openingHash match and it parses. */
118
+ from?: RunCheckpoint | null;
119
+ save: (checkpoint: RunCheckpoint) => Promise<boolean>;
120
+ /** Called once, when a save resolves false. */
121
+ onLost?: () => void;
122
+ onSaveError?: (err: unknown) => void;
123
+ /** The store wrapped. Default a fresh InProcessSessionService. */
124
+ inner?: InProcessSessionService;
125
+ }
126
+ export type CheckpointingSessionService = SessionService & {
127
+ /** The checkpoint given was restored. */
128
+ readonly restored: boolean;
129
+ /** Step boundaries recorded, the restored checkpoint's included. */
130
+ readonly steps: number;
131
+ };
132
+ /** sha256 hex of the message's parts, as a checkpoint names its run's message. */
133
+ export declare function openingHashOf(parts: readonly MessagePart[]): string;
134
+ /**
135
+ * The checkpoint, checked, when it belongs to this run and this message;
136
+ * null otherwise. A stored checkpoint is data read back from a table or a
137
+ * file, so every field the restore reads is checked before it is trusted.
138
+ */
139
+ export declare function usableCheckpoint(cp: unknown, runId: string, parts: readonly MessagePart[]): RunCheckpoint | null;
140
+ /**
141
+ * A session store that checkpoints a run: the in-process store `inner`,
142
+ * restored from `from` when it is this run's checkpoint for this message,
143
+ * saving a snapshot of every session at each step boundary (see the header).
144
+ */
145
+ export declare function checkpointingSessions(opts: CheckpointingOptions): CheckpointingSessionService;
146
+ export type DurableTurnOptions = Omit<SyndicateTurnOptions, 'sessionService' | 'sessionId'> & {
147
+ /** Names the run: a checkpoint is restored only by the run that wrote it. */
148
+ runId: string;
149
+ checkpoints: CheckpointSink;
150
+ /** Called once when a save resolves false; the run is then aborted. */
151
+ onLost?: () => void;
152
+ onSaveError?: (err: unknown) => void;
153
+ /** The in-process store the run writes. Default a fresh one; a test passes its own to read the sessions afterwards. */
154
+ sessions?: InProcessSessionService;
155
+ };
156
+ export type DurableTurnResult = SyndicateTurnResult & {
157
+ /** The step boundaries the restored checkpoint had recorded; 0 on a fresh run. */
158
+ resumedFromStep: number;
159
+ /** A save resolved false: the run no longer held its claim and was aborted. */
160
+ lostClaim: boolean;
161
+ /** The run's own session id: the checkpoint's on a resume, a new UUID otherwise. */
162
+ sessionId: string;
163
+ };
164
+ /**
165
+ * runSyndicateTurn, durable: the run's sessions live in a fresh in-process
166
+ * store, restored from the sink's checkpoint when it is this run's for this
167
+ * message, and checkpointed at every step boundary. A save that resolves
168
+ * false aborts the run (`lostClaim`). The caller clears the checkpoint when
169
+ * the run finishes; this never does.
170
+ */
171
+ export declare function runDurableTurn(opts: DurableTurnOptions): Promise<DurableTurnResult>;