melchizedek-agents 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/.env.example +19 -2
  2. package/CHANGELOG.md +333 -0
  3. package/DOCUMENTATION.md +99 -39
  4. package/README.md +1 -0
  5. package/config/agents/README.md +2 -2
  6. package/config/agents/examples/ares.yaml +6 -10
  7. package/config/agents/examples/assistant.yaml +6 -6
  8. package/config/agents/examples/augustin.yaml +9 -15
  9. package/config/agents/examples/cartographers.yaml +6 -6
  10. package/config/agents/examples/council.yaml +6 -6
  11. package/config/agents/examples/critic.yaml +16 -16
  12. package/config/agents/examples/harness.yaml +4 -4
  13. package/config/agents/examples/image_production.yaml +33 -32
  14. package/config/agents/examples/librarian.yaml +4 -4
  15. package/config/agents/examples/patient_advocate.yaml +6 -10
  16. package/config/agents/examples/pipeline.yaml +20 -19
  17. package/config/agents/examples/research.yaml +36 -43
  18. package/config/agents/examples/scribe.yaml +31 -32
  19. package/config/agents/examples/scriptorium.yaml +6 -6
  20. package/config/agents/examples/structured_critic.yaml +82 -0
  21. package/config/agents/examples/style_council.yaml +9 -11
  22. package/config/agents/examples/syndicate.yaml +3 -5
  23. package/config/agents/examples/tutor.yaml +2 -2
  24. package/config/agents/examples/weather.yaml +2 -2
  25. package/config/agents/syndicate.schema.json +1979 -0
  26. package/config/agents/syndicateSchema.yaml +109 -72
  27. package/config/agents/templates/README.md +1 -1
  28. package/config/agents/templates/account_memory.yaml +4 -4
  29. package/config/agents/templates/case_desk.yaml +6 -6
  30. package/config/agents/templates/conversational.yaml +2 -2
  31. package/config/agents/templates/draft_review.yaml +6 -6
  32. package/config/agents/templates/intake_extractor.yaml +61 -60
  33. package/config/agents/templates/research_brief.yaml +6 -6
  34. package/config/agents/templates/research_desk.yaml +6 -6
  35. package/config/agents/templates/review_panel.yaml +8 -8
  36. package/config/agents/templates/support_triage.yaml +30 -29
  37. package/config/agents/templates/systems_operator.yaml +21 -5
  38. package/db/migrations/0014_durable_runs.sql +31 -0
  39. package/dist/lib/a2a/app.d.ts +15 -0
  40. package/dist/lib/a2a/app.js +53 -5
  41. package/dist/lib/a2a/executor.d.ts +7 -1
  42. package/dist/lib/a2a/executor.js +34 -4
  43. package/dist/lib/a2a/oauthSetup.d.ts +90 -0
  44. package/dist/lib/a2a/oauthSetup.js +205 -0
  45. package/dist/lib/agentDialect.d.ts +66 -0
  46. package/dist/lib/agentDialect.js +134 -0
  47. package/dist/lib/compile.d.ts +10 -5
  48. package/dist/lib/compile.js +69 -37
  49. package/dist/lib/compileNative.js +2 -0
  50. package/dist/lib/dispatch.d.ts +10 -5
  51. package/dist/lib/dispatch.js +10 -5
  52. package/dist/lib/doctor.d.ts +34 -1
  53. package/dist/lib/doctor.js +47 -3
  54. package/dist/lib/loadSyndicate.d.ts +45 -2
  55. package/dist/lib/loadSyndicate.js +22 -2
  56. package/dist/lib/memory/namespace.d.ts +6 -5
  57. package/dist/lib/memory/namespace.js +6 -5
  58. package/dist/lib/memory/providers.js +3 -4
  59. package/dist/lib/models/capabilities.d.ts +16 -1
  60. package/dist/lib/models/capabilities.js +55 -1
  61. package/dist/lib/observability/tracer.d.ts +8 -1
  62. package/dist/lib/observability/tracer.js +33 -5
  63. package/dist/lib/runtime/approvals.d.ts +6 -0
  64. package/dist/lib/runtime/native/agentLoop.d.ts +18 -1
  65. package/dist/lib/runtime/native/agentLoop.js +105 -11
  66. package/dist/lib/runtime/native/checkpoint.d.ts +164 -0
  67. package/dist/lib/runtime/native/checkpoint.js +365 -0
  68. package/dist/lib/runtime/native/delegate.d.ts +141 -18
  69. package/dist/lib/runtime/native/delegate.js +235 -39
  70. package/dist/lib/runtime/native/interrupts.d.ts +101 -2
  71. package/dist/lib/runtime/native/interrupts.js +201 -1
  72. package/dist/lib/runtime/native/request.d.ts +13 -3
  73. package/dist/lib/runtime/native/request.js +28 -4
  74. package/dist/lib/runtime/native/telemetry.js +10 -15
  75. package/dist/lib/runtime/questions.d.ts +7 -4
  76. package/dist/lib/runtime/sessions.d.ts +1 -1
  77. package/dist/lib/runtime/syndicateTurn.d.ts +7 -1
  78. package/dist/lib/runtime/syndicateTurn.js +74 -22
  79. package/dist/lib/runtime/turnControl.d.ts +15 -0
  80. package/dist/lib/runtime/turnControl.js +10 -0
  81. package/dist/lib/storage/postgres/index.d.ts +6 -1
  82. package/dist/lib/storage/postgres/index.js +7 -2
  83. package/dist/lib/storage/postgres/taskQueue.d.ts +14 -2
  84. package/dist/lib/storage/postgres/taskQueue.js +20 -3
  85. package/dist/lib/storage/postgres/taskStore.d.ts +26 -2
  86. package/dist/lib/storage/postgres/taskStore.js +74 -8
  87. package/dist/lib/syndicateSchema.d.ts +278 -0
  88. package/dist/lib/syndicateSchema.js +182 -15
  89. package/dist/lib/tools/auth.d.ts +7 -5
  90. package/dist/lib/tools/auth.js +12 -4
  91. package/dist/lib/tools/credentialEnv.d.ts +28 -0
  92. package/dist/lib/tools/credentialEnv.js +52 -0
  93. package/dist/lib/tools/credentialStore.js +9 -2
  94. package/dist/lib/tools/mcpToolFactory.d.ts +25 -2
  95. package/dist/lib/tools/mcpToolFactory.js +179 -21
  96. package/dist/lib/tools/oauthHosts.d.ts +71 -0
  97. package/dist/lib/tools/oauthHosts.js +154 -0
  98. package/dist/lib/tools/oauthTools.d.ts +233 -0
  99. package/dist/lib/tools/oauthTools.js +421 -0
  100. package/dist/lib/tools/openapiTools.d.ts +4 -6
  101. package/dist/lib/tools/openapiTools.js +40 -36
  102. package/dist/lib/tools/taskTools.d.ts +30 -5
  103. package/dist/lib/tools/taskTools.js +74 -10
  104. package/dist/lib/workflow.d.ts +8 -4
  105. package/dist/lib/workflow.js +8 -4
  106. package/dist/lib/workflowConfig.d.ts +6 -0
  107. package/dist/scripts/a2a_server.d.ts +36 -0
  108. package/dist/scripts/a2a_server.js +122 -1
  109. package/dist/scripts/assistant_worker.d.ts +10 -0
  110. package/dist/scripts/assistant_worker.js +69 -19
  111. package/dist/scripts/doctor.d.ts +4 -1
  112. package/dist/scripts/doctor.js +5 -2
  113. package/dist/scripts/yaml_codemod.d.ts +52 -0
  114. package/dist/scripts/yaml_codemod.js +570 -0
  115. package/package.json +11 -7
  116. package/skills/briefs/melchizedek-author.md +7 -5
  117. package/skills/briefs/melchizedek-models.md +4 -3
  118. package/skills/briefs/melchizedek.md +1 -0
  119. package/skills/melchizedek/SKILL.md +1 -0
  120. package/skills/melchizedek-author/SKILL.md +7 -4
  121. package/skills/melchizedek-author/assets/minimal.yaml +13 -13
  122. package/skills/melchizedek-models/SKILL.md +6 -3
@@ -1,9 +1,9 @@
1
- import { trace, context, defaultTextMapGetter, ROOT_CONTEXT } from '@opentelemetry/api';
1
+ import { trace, context, defaultTextMapGetter, ROOT_CONTEXT, INVALID_SPAN_CONTEXT } from '@opentelemetry/api';
2
2
  import { createRequire } from 'node:module';
3
3
  import { ADK_SPAN_SCOPE, RUNTIME_SPAN_SCOPE, TELEMETRY_SCHEMA_VERSION, agentOfSpanName, engineVersion, isModelCallSpan, isToolSpanName } from './lineage.js';
4
4
  import { FilteringSpanExporter, otlpContentMode } from './otlpFilter.js';
5
5
  import { telemetryRedactor } from './redact.js';
6
- import { chargeLlmCall, chargeTokens } from '../runtime/turnControl.js';
6
+ import { chargeLlmCall, chargeTokens, turnUntraced } from '../runtime/turnControl.js';
7
7
  import sdkNode from '@opentelemetry/sdk-trace-node';
8
8
  const { NodeTracerProvider } = sdkNode;
9
9
  import sdkBase from '@opentelemetry/sdk-trace-base';
@@ -275,6 +275,26 @@ export function initializeTracing() {
275
275
  }
276
276
  }
277
277
  const tracer = trace.getTracer('melchizedek-tracer');
278
+ /**
279
+ * A span that records nothing, for code running in a turn that opted out of
280
+ * tracing (`runSyndicateTurn({ trace: false })`, turnUntraced): its
281
+ * attributes, events and end go nowhere, and no tracer is started for it.
282
+ */
283
+ function nonRecordingSpan() {
284
+ return trace.wrapSpanContext(INVALID_SPAN_CONTEXT);
285
+ }
286
+ /**
287
+ * Starts a span on the named tracer, starting the tracer first; in a turn
288
+ * that opted out of tracing, a non-recording span and no tracer. Every span
289
+ * the engine opens inside a turn goes through here or checks turnUntraced
290
+ * itself.
291
+ */
292
+ export function startEngineSpan(tracerName, name, options) {
293
+ if (turnUntraced())
294
+ return nonRecordingSpan();
295
+ initializeTracing();
296
+ return trace.getTracer(tracerName).startSpan(name, options);
297
+ }
278
298
  /** Turn identity by trace id, so llm.request children can carry it too. */
279
299
  const turnContexts = new Map();
280
300
  /**
@@ -328,6 +348,11 @@ export function callerSpanContext(traceparent) {
328
348
  return trace.getSpanContext(ctx);
329
349
  }
330
350
  export async function* traceAgentRun(stream, metadata) {
351
+ // A turn that opted out of tracing gets its events untouched and no span.
352
+ if (turnUntraced()) {
353
+ yield* stream;
354
+ return;
355
+ }
331
356
  initializeTracing();
332
357
  const caller = callerSpanContext(metadata.traceparent);
333
358
  const span = tracer.startSpan(`Syndicate Execution: ${metadata.syndicateName}`, caller ? { links: [{ context: caller }], attributes: { 'caller.trace_id': caller.traceId } } : undefined);
@@ -551,7 +576,10 @@ function refineErrorCode(code, message) {
551
576
  * (e.g. llm.web_search.native) via setLlmSpanAttribute below.
552
577
  */
553
578
  export async function* traceLlmGeneration(meta, inner) {
554
- initializeTracing();
579
+ // An untraced turn still charges the call and its tokens; it opens no span.
580
+ const untraced = turnUntraced();
581
+ if (!untraced)
582
+ initializeTracing();
555
583
  // Every model call on every provider passes here, so this is where the
556
584
  // turn's step budget and cancellation are enforced (lib/runtime/
557
585
  // turnControl.ts). A refused call never reaches the provider.
@@ -561,7 +589,7 @@ export async function* traceLlmGeneration(meta, inner) {
561
589
  yield { errorCode: charge.code, errorMessage: charge.message };
562
590
  return;
563
591
  }
564
- const span = tracer.startSpan('llm.request');
592
+ const span = untraced ? nonRecordingSpan() : tracer.startSpan('llm.request');
565
593
  span.setAttribute('llm.provider', meta.provider);
566
594
  span.setAttribute('llm.model', meta.model);
567
595
  // OpenTelemetry GenAI semantic conventions alongside the llm.* names, so
@@ -633,7 +661,7 @@ export async function* traceLlmGeneration(meta, inner) {
633
661
  if (thinkingPreview) {
634
662
  span.addEvent('llm.thinking', { 'thinking.preview': thinkingPreview });
635
663
  }
636
- if (errorCode) {
664
+ if (errorCode && span.isRecording()) {
637
665
  span.setAttribute('llm.error_code', errorCode);
638
666
  span.setAttribute('llm.error_message', errorMessage.slice(0, ERROR_MESSAGE_MAX_CHARS));
639
667
  // Make this span a payload candidate (see supabaseSpanExporter's
@@ -24,6 +24,12 @@ export interface PendingApproval {
24
24
  args: Record<string, unknown>;
25
25
  /** The original call's id. */
26
26
  callId?: string;
27
+ /**
28
+ * For a call gated inside a delegated subagent (ADR 0110): the agents from
29
+ * the turn's own agent down to the one that asked, `agent` last. Absent
30
+ * when the turn's own agent asked.
31
+ */
32
+ path?: string[];
27
33
  }
28
34
  /**
29
35
  * The approval request still waiting for an answer, or undefined. A request
@@ -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
@@ -162,6 +173,12 @@ export interface AgentLoopContext extends Omit<ModelStepOptions, 'agent' | 'adap
162
173
  * a delegated subagent's loop.
163
174
  */
164
175
  consent?: Pick<OAuthConsent, 'has' | 'begin'>;
176
+ /**
177
+ * The run is a delegated subagent's, on its own child session filed under
178
+ * its agent path (lib/runtime/native/delegate.ts childAppName, ADR 0111):
179
+ * its own delegations are filed below that path. Set by runChild only.
180
+ */
181
+ delegated?: boolean;
165
182
  }
166
183
  /** How the run ended. */
167
184
  export type AgentLoopEndReason =
@@ -181,7 +198,7 @@ export interface AgentLoopEnd {
181
198
  steps: number;
182
199
  /** The last event stored. */
183
200
  lastEvent?: TurnEvent;
184
- /** For `paused`: the ids waiting on a person (the ask_user call ids, or the adk_request_confirmation call ids). */
201
+ /** 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
202
  pending?: string[];
186
203
  /** For `stopped`: the turn's code and message. */
187
204
  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
@@ -125,8 +136,8 @@ import { MAX_VALUE_DEPTH, nestedDeeperThan } from '../valueDepth.js';
125
136
  import { currentTurnSignal } from '../turnControl.js';
126
137
  import { compactBeforeStep } from './compaction.js';
127
138
  import { ADK_CALL_ID_PREFIX } from './history.js';
128
- import { runSubagent, runWorkflowSubagent, subagentOf, workflowSubagentOf } from './delegate.js';
129
- import { approvedCalls, grantedCalls } from './interrupts.js';
139
+ import { DEFAULT_MAX_CONCURRENCY, DelegationGate, SubagentPause, resumeSubagent, resumeWorkflowSubagent, runSubagent, runWorkflowSubagent, subagentOf, workflowSubagentOf } from './delegate.js';
140
+ import { approvedCalls, grantedCalls, resumedDelegations } from './interrupts.js';
130
141
  import { SelfCorrection } from './selfCorrection.js';
131
142
  import { eitherSignal, runModelStep, stopOf } from './step.js';
132
143
  import { taskNodeRun } from './taskMode.js';
@@ -342,7 +353,7 @@ async function runOwnTool(tool, args, context) {
342
353
  throw new Error(`Error in tool '${tool.name}': ${message}`);
343
354
  }
344
355
  }
345
- async function runCall(scope, call, tools, correction, confirmation) {
356
+ async function runCall(scope, call, tools, correction, confirmation, resume, ticket) {
346
357
  const context = callContext(scope, call.id || undefined, confirmation);
347
358
  const name = call.name ?? '';
348
359
  const tool = name && tools.has(name) ? tools.get(name) : undefined;
@@ -363,17 +374,26 @@ async function runCall(scope, call, tools, correction, confirmation) {
363
374
  // 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
375
  const subagent = subagentOf(tool);
365
376
  const workflow = subagent ? undefined : workflowSubagentOf(tool);
366
- const delegation = { ctx: scope.ctx, stateBase: scope.stateBase, signal: scope.signal, runLoop: runAgentLoop, queue: scope };
377
+ const delegation = { ctx: scope.ctx, caller: scope.agent.name, stateBase: scope.stateBase, signal: scope.signal, runLoop: runAgentLoop };
367
378
  let response;
368
379
  let failure;
369
380
  try {
381
+ // 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.
382
+ if (ticket && (subagent || workflow))
383
+ await ticket.ready;
370
384
  response = subagent
371
- ? await runSubagent(subagent, args, context, delegation)
385
+ ? resume
386
+ ? await resumeSubagent(subagent, resume, context, delegation)
387
+ : await runSubagent(subagent, args, context, delegation)
372
388
  : workflow
373
- ? await runWorkflowSubagent(workflow, args, context, delegation)
389
+ ? resume
390
+ ? await resumeWorkflowSubagent(workflow, resume, context, delegation)
391
+ : await runWorkflowSubagent(workflow, args, context, delegation)
374
392
  : await runOwnTool(tool, args, context);
393
+ ticket?.release();
375
394
  }
376
395
  catch (e) {
396
+ ticket?.release();
377
397
  failure = e instanceof Error ? e.message : e;
378
398
  // Self-correction answers a thrown Error with reflection guidance in its place; a call waiting on a grant is not a failure.
379
399
  const guided = isEmptyRecord(context.actions.requestedAuthConfigs) ? await correction?.failed(toolName, args, e) : undefined;
@@ -386,6 +406,12 @@ async function runCall(scope, call, tools, correction, confirmation) {
386
406
  const provider = String(asked[0]?.credentialKey ?? '');
387
407
  return { part: { functionResponse: { id: context.functionCallId, name: toolName, response: { result: CONSENT_TEXTS.pending(provider) } } }, actions: context.actions };
388
408
  }
409
+ // A child that ended paused (ADR 0110): the call stays open, with no response, and the run ends paused on it.
410
+ if (response instanceof SubagentPause) {
411
+ if (context.functionCallId)
412
+ (scope.paused ??= []).push(context.functionCallId);
413
+ return isDefaultActions(context.actions) ? undefined : { actions: context.actions };
414
+ }
389
415
  if (failure === undefined)
390
416
  await correction?.answered(toolName, response);
391
417
  // As ADK: a long-running call with no response answers nothing, even when it threw.
@@ -401,10 +427,33 @@ async function runCall(scope, call, tools, correction, confirmation) {
401
427
  * The calls run, in parallel, as the one response event ADK stores for them
402
428
  * (not yet stored); undefined when none answered. `confirmations` holds the
403
429
  * person's answer for a pinned call an approval resumes, by call id.
430
+ *
431
+ * Delegated calls (a subagent, a nested workflow) run through the step's
432
+ * DelegationGate (ADR 0116): at most the agent's `maxConcurrency` at once,
433
+ * entered in call order, two calls to one subagent one after the other.
434
+ * Other calls start at once, as before. The event is built once every call
435
+ * has answered, its parts in call order, and the open calls a paused child
436
+ * left (scope.paused) are listed in call order too: neither depends on
437
+ * which call finished first.
404
438
  */
405
- async function runCalls(scope, calls, tools, confirmations) {
439
+ async function runCalls(scope, calls, tools, confirmations, resumes) {
406
440
  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);
441
+ const gate = new DelegationGate(scope.agent.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY);
442
+ const outcomes = (await Promise.all(calls.map((call, i) => {
443
+ const tool = tools.get(call.name ?? '');
444
+ // Entered here, synchronously and in call order, before any call runs (ADR 0116).
445
+ const delegated = subagentOf(tool) ?? workflowSubagentOf(tool);
446
+ const ticket = delegated ? gate.enter(delegated.name) : undefined;
447
+ 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(() => {
448
+ ticket?.release();
449
+ order?.release(i);
450
+ });
451
+ }))).filter((o) => !!o);
452
+ // The open calls, in call order rather than the order their children paused in.
453
+ if (scope.paused && scope.paused.length > 1) {
454
+ const at = (id) => calls.findIndex((c) => c.id === id);
455
+ scope.paused.sort((a, b) => at(a) - at(b));
456
+ }
408
457
  if (outcomes.length === 0)
409
458
  return undefined;
410
459
  const base = { invocationId: scope.ctx.invocationId, author: scope.agent.name };
@@ -503,6 +552,32 @@ async function resumeGrants(agent, ctx, stateBase, selfCorrection) {
503
552
  const auth = authEvent(scope, response);
504
553
  return { response, ...(auth ? { auth } : {}) };
505
554
  }
555
+ /**
556
+ * Delegated calls the latest user message resumes (ADR 0110), before a step
557
+ * and after the approval resume: each open call whose child waits on what
558
+ * the message answers runs the child on with that answer, and their
559
+ * response is returned to be stored, with the open calls still paused:
560
+ * those the child paused again, and those the message did not answer.
561
+ * Undefined when the message answers none; 'stopped' when the turn stopped
562
+ * while they ran (nothing is stored).
563
+ */
564
+ async function resumeDelegations(agent, ctx, stateBase, selfCorrection) {
565
+ const resumed = await resumedDelegations(agent, ctx, ctx.sessions);
566
+ if (!resumed)
567
+ return undefined;
568
+ const calls = resumed.calls.filter((c) => {
569
+ const tool = resumed.tools.get(c.name ?? '');
570
+ return subagentOf(tool) || workflowSubagentOf(tool);
571
+ });
572
+ if (calls.length === 0)
573
+ return undefined;
574
+ const signal = eitherSignal(ctx.signal, currentTurnSignal());
575
+ const scope = { agent, ctx, stateBase, selfCorrection, ...(signal ? { signal } : {}) };
576
+ const response = await runCalls(scope, calls, resumed.tools, undefined, resumed.answers);
577
+ if (signal?.aborted)
578
+ return 'stopped';
579
+ return { ...(response ? { response } : {}), paused: [...(scope.paused ?? []), ...resumed.waiting] };
580
+ }
506
581
  // ── The model step, with partials and the fallback ───────────────────────────
507
582
  /** Runs a step, yielding its partial events as they arrive; returns the step's result. */
508
583
  async function* withPartials(start) {
@@ -640,6 +715,18 @@ async function* agentLoop(agent, ctx) {
640
715
  lastEvent = await store(resumed);
641
716
  yield lastEvent;
642
717
  }
718
+ // Delegation hook (WS6-2a, ADR 0110): an answer to a pause inside a delegated call resumes that call before the step.
719
+ const redelegated = await resumeDelegations(agent, ctx, withStateOverlay(session.state, runTemp.values()), selfCorrection);
720
+ if (redelegated === 'stopped')
721
+ return { reason: 'stopped', steps, lastEvent, stop: stopOf() };
722
+ if (redelegated) {
723
+ if (redelegated.response) {
724
+ lastEvent = await store(redelegated.response);
725
+ yield lastEvent;
726
+ }
727
+ if (redelegated.paused.length > 0)
728
+ return { reason: 'paused', steps, lastEvent, pending: redelegated.paused };
729
+ }
643
730
  // Compaction hook (WS2-9, lib/runtime/native/compaction.ts): with `context:`, the summary is stored before the step reads the history,
644
731
  // before the step budget, as ADK's request processors run before its call count.
645
732
  const compacted = await compactBeforeStep(agent, {
@@ -679,6 +766,8 @@ async function* agentLoop(agent, ctx) {
679
766
  // As ADK: a turn that stopped while the calls ran (a long subagent run, say) stores no response.
680
767
  if (scope.signal?.aborted)
681
768
  return { reason: 'stopped', steps, lastEvent, stop: stopOf() };
769
+ // 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.
770
+ const delegated = scope.paused ?? [];
682
771
  if (response) {
683
772
  const auth = authEvent(scope, response);
684
773
  if (auth) {
@@ -690,17 +779,22 @@ async function* agentLoop(agent, ctx) {
690
779
  if (confirmation) {
691
780
  lastEvent = await store(confirmation);
692
781
  yield lastEvent;
693
- return { reason: 'paused', steps, lastEvent, pending: [...(lastEvent.longRunningToolIds ?? [])] };
782
+ return { reason: 'paused', steps, lastEvent, pending: [...(lastEvent.longRunningToolIds ?? []), ...delegated] };
694
783
  }
695
784
  lastEvent = await store(response);
696
785
  yield lastEvent;
697
786
  // As ADK: the response asking for auth is final, and the run waits on its credential request (ADR 0085).
698
787
  if (authIds.length > 0)
699
- return { reason: 'paused', steps, lastEvent, pending: authIds };
788
+ return { reason: 'paused', steps, lastEvent, pending: [...authIds, ...delegated] };
700
789
  stepEnd = lastEvent;
701
- if (task?.finished)
790
+ if (task?.finished && delegated.length === 0)
702
791
  return { reason: 'final', steps, lastEvent, output: task.output };
703
792
  }
793
+ if (delegated.length > 0) {
794
+ const answered = new Set(getFunctionResponses(stepEnd).map((r) => r.id));
795
+ const longRunning = (modelEvent.longRunningToolIds ?? []).filter((id) => !answered.has(id));
796
+ return { reason: 'paused', steps, lastEvent, pending: [...longRunning, ...delegated] };
797
+ }
704
798
  }
705
799
  // ADK's runAsyncImpl: stop on a final event, unless it is an empty metadata event after tool calls.
706
800
  const emptyMetadata = stepEnd.author === agent.name && !stepEnd.partial && !hasParts(stepEnd) && isDefaultActions(stepEnd.actions);
@@ -0,0 +1,164 @@
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 and applies only the
54
+ * route's own appends to that copy. Restored events cannot reach that
55
+ * copy through the store, so a checkpoint taken inside such a route is
56
+ * not resumed: runDurableTurn starts that run fresh. A workflow route
57
+ * writes its own child session and resumes.
58
+ * - Each resumed attempt has its own step budget (max_steps, the loop's
59
+ * call ceiling); attempts are bounded by the queue's MAX_ATTEMPTS.
60
+ * - `temp:` state lasts one invocation and is never stored, so it does not
61
+ * survive a resume.
62
+ * - A workflow walk resumes through its own rules (ADR 0094); its
63
+ * checkpoint is the sessions it walks.
64
+ * - A checkpoint holds the run's sessions, so it carries the user's text,
65
+ * model output and tool results: it lives only where the run's record
66
+ * lives, under the same owner, and is deleted with it. This module never
67
+ * clears one; the caller does when the run finishes.
68
+ */
69
+ import type { TurnEvent } from '../events.ts';
70
+ import { InProcessSessionService } from '../sessions.ts';
71
+ import type { SessionService } from '../sessions.ts';
72
+ import type { MessagePart, SyndicateTurnOptions, SyndicateTurnResult } from '../syndicateTurn.ts';
73
+ export declare const CHECKPOINT_VERSION = 1;
74
+ /** One session of a run, as it stood at a step boundary. */
75
+ export interface CheckpointSession {
76
+ appName: string;
77
+ userId: string;
78
+ id: string;
79
+ /** The state given to create(), before any event. */
80
+ createdState: Record<string, unknown>;
81
+ events: TurnEvent[];
82
+ /** The first event is the run's own opening user message: restore holds the session back until the runner stores that message. */
83
+ opening: boolean;
84
+ }
85
+ /** A run's sessions at its latest step boundary. */
86
+ export interface RunCheckpoint {
87
+ version: 1;
88
+ runId: string;
89
+ /** The run's own session id: a resumed run uses it again. */
90
+ sessionId: string;
91
+ /** sha256 hex of JSON.stringify(parts) of the run's message (strings as `{ text }`). */
92
+ openingHash: string;
93
+ /** Step boundaries recorded so far, across every attempt. */
94
+ steps: number;
95
+ /** ISO time of the save. */
96
+ savedAt: string;
97
+ sessions: CheckpointSession[];
98
+ }
99
+ /** 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. */
100
+ export interface CheckpointSink {
101
+ load(): Promise<RunCheckpoint | null>;
102
+ save(checkpoint: RunCheckpoint): Promise<boolean>;
103
+ }
104
+ export interface CheckpointingOptions {
105
+ runId: string;
106
+ /** The run's own session id (the one runSyndicateTurn is given). */
107
+ sessionId: string;
108
+ /** The run's message. */
109
+ parts: MessagePart[];
110
+ /** The checkpoint to restore; ignored unless its runId and openingHash match and it parses. */
111
+ from?: RunCheckpoint | null;
112
+ save: (checkpoint: RunCheckpoint) => Promise<boolean>;
113
+ /** Called once, when a save resolves false. */
114
+ onLost?: () => void;
115
+ onSaveError?: (err: unknown) => void;
116
+ /** The store wrapped. Default a fresh InProcessSessionService. */
117
+ inner?: InProcessSessionService;
118
+ }
119
+ export type CheckpointingSessionService = SessionService & {
120
+ /** The checkpoint given was restored. */
121
+ readonly restored: boolean;
122
+ /** Step boundaries recorded, the restored checkpoint's included. */
123
+ readonly steps: number;
124
+ };
125
+ /** sha256 hex of the message's parts, as a checkpoint names its run's message. */
126
+ export declare function openingHashOf(parts: readonly MessagePart[]): string;
127
+ /**
128
+ * The checkpoint, checked, when it belongs to this run and this message;
129
+ * null otherwise. A stored checkpoint is data read back from a table or a
130
+ * file, so every field the restore reads is checked before it is trusted.
131
+ */
132
+ export declare function usableCheckpoint(cp: unknown, runId: string, parts: readonly MessagePart[]): RunCheckpoint | null;
133
+ /**
134
+ * A session store that checkpoints a run: the in-process store `inner`,
135
+ * restored from `from` when it is this run's checkpoint for this message,
136
+ * saving a snapshot of every session at each step boundary (see the header).
137
+ */
138
+ export declare function checkpointingSessions(opts: CheckpointingOptions): CheckpointingSessionService;
139
+ export type DurableTurnOptions = Omit<SyndicateTurnOptions, 'sessionService' | 'sessionId'> & {
140
+ /** Names the run: a checkpoint is restored only by the run that wrote it. */
141
+ runId: string;
142
+ checkpoints: CheckpointSink;
143
+ /** Called once when a save resolves false; the run is then aborted. */
144
+ onLost?: () => void;
145
+ onSaveError?: (err: unknown) => void;
146
+ /** The in-process store the run writes. Default a fresh one; a test passes its own to read the sessions afterwards. */
147
+ sessions?: InProcessSessionService;
148
+ };
149
+ export type DurableTurnResult = SyndicateTurnResult & {
150
+ /** The step boundaries the restored checkpoint had recorded; 0 on a fresh run. */
151
+ resumedFromStep: number;
152
+ /** A save resolved false: the run no longer held its claim and was aborted. */
153
+ lostClaim: boolean;
154
+ /** The run's own session id: the checkpoint's on a resume, a new UUID otherwise. */
155
+ sessionId: string;
156
+ };
157
+ /**
158
+ * runSyndicateTurn, durable: the run's sessions live in a fresh in-process
159
+ * store, restored from the sink's checkpoint when it is this run's for this
160
+ * message, and checkpointed at every step boundary. A save that resolves
161
+ * false aborts the run (`lostClaim`). The caller clears the checkpoint when
162
+ * the run finishes; this never does.
163
+ */
164
+ export declare function runDurableTurn(opts: DurableTurnOptions): Promise<DurableTurnResult>;