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
@@ -80,14 +80,42 @@
80
80
  * builds its request. A later step finds the agent's own events last,
81
81
  * so the call runs once.
82
82
  *
83
+ * PAUSES INSIDE DELEGATED SUBAGENTS (WS6-2a, ADR 0110). A subagent runs in
84
+ * its own child session (delegate.ts); an approval request or question it
85
+ * leaves open ends its run paused, and its caller leaves the delegated
86
+ * call open (no response stored) and ends paused too, up to the turn.
87
+ * delegatedPauses finds such a pause from any session by walking down:
88
+ * each call its agent left open (openCalls) leads to the child session
89
+ * under the call's name, whose own open request or question is the pause,
90
+ * or whose own open call leads one level further. The answer travels back
91
+ * down the same way: before each step, the loop resumes the open call
92
+ * whose pause the latest user message answers (agentLoop.ts), and the
93
+ * child reads the same answer from its own session, as it would at the top.
94
+ *
95
+ * NESTED SYNDICATES (WS6-2b, ADR 0111). The child session of a call is the
96
+ * one runSubagent files it under: the agent path (delegate.ts childAppName),
97
+ * else, for a conversation stored before ADR 0111, the subagent's name
98
+ * alone (legacyChild). A nested delegate syndicate is its orchestrator
99
+ * there, so the walk goes on through its open calls. A nested workflow
100
+ * syndicate holds a paused walk there instead: its own pause record (the
101
+ * workflow's event naming every interrupt it left open, authored by the
102
+ * workflow under the call's name) makes an approval request an agent node
103
+ * raised, or an ask_user node's question, the pause, with the node that
104
+ * asked at the end of the path. The walk stops there: a pause inside a
105
+ * workflow node's own delegation does not run (lib/workflow/agentNode.ts).
106
+ *
83
107
  * ADK stays out of this file: an ADK tool an agent still lists is asked
84
108
  * whether it gates through its own checkRequireConfirmation, by shape.
85
109
  */
86
110
  import { isDeepStrictEqual } from 'node:util';
87
111
  import { nativeToolOf } from '../../models/schemaNormalize.js';
88
112
  import { instructionToolOf, isTool, toolOf } from '../../tools/tool.js';
113
+ import { inputRequestFrom } from '../../workflowConfig.js';
114
+ import { pendingApproval } from '../approvals.js';
89
115
  import { getFunctionCalls, getFunctionResponses } from '../events.js';
90
- import { REQUEST_CONFIRMATION_CALL, REQUEST_CREDENTIAL_CALL, isSegmentPrefix } from './history.js';
116
+ import { ASK_USER, pendingQuestion } from '../questions.js';
117
+ import { childAppName, legacyChild } from './delegate.js';
118
+ import { REQUEST_CONFIRMATION_CALL, REQUEST_CREDENTIAL_CALL, REQUEST_INPUT_CALL, isSegmentPrefix } from './history.js';
91
119
  import { isToolset } from './request.js';
92
120
  /** A confirmation that does not bind to its pinned call: ADK's IntentMismatchError, by name and text. */
93
121
  export class IntentMismatchError extends Error {
@@ -308,3 +336,175 @@ export async function grantedCalls(agent, scope) {
308
336
  }
309
337
  return undefined;
310
338
  }
339
+ /** The deepest a chain of delegated pauses is followed: nested syndicates stop at 16 levels (lib/compile.ts). */
340
+ const MAX_DELEGATION_DEPTH = 16;
341
+ /** The engine's own long-running calls: never a delegation. */
342
+ const FRAMEWORK_CALLS = new Set([REQUEST_CONFIRMATION_CALL, REQUEST_CREDENTIAL_CALL, ASK_USER]);
343
+ const hasUserText = (e) => e.author === 'user' && (e.content?.parts ?? []).some((p) => typeof p.text === 'string' && p.text.trim() !== '' && !p.thought);
344
+ /**
345
+ * The calls an agent made that are still open: no later event answers it,
346
+ * and the person has not written since. Only an agent's calls count (a
347
+ * call in a user-authored event is never one), and the engine's own
348
+ * framework calls are left out. `author`, when given, keeps one agent's
349
+ * calls. Latest event first, calls in call order.
350
+ */
351
+ export function openCalls(events, author) {
352
+ const answered = new Set();
353
+ const open = [];
354
+ for (let i = events.length - 1; i >= 0; i--) {
355
+ const event = events[i];
356
+ if (hasUserText(event))
357
+ break;
358
+ for (const r of getFunctionResponses(event))
359
+ if (r.id)
360
+ answered.add(r.id);
361
+ if (!event.author || event.author === 'user' || (author !== undefined && event.author !== author))
362
+ continue;
363
+ for (const call of getFunctionCalls(event)) {
364
+ if (!call.id || !call.name || FRAMEWORK_CALLS.has(call.name) || answered.has(call.id))
365
+ continue;
366
+ open.push({ author: event.author, call });
367
+ }
368
+ }
369
+ return open;
370
+ }
371
+ /**
372
+ * What waits inside the delegated call `call` that `caller` left open: the
373
+ * child session under the call's name (where runSubagent runs it), and its
374
+ * own open approval request or question, else a pause inside a call it left
375
+ * open, one level down. Only the subagent's own requests count: a request
376
+ * or question authored by anyone else is none.
377
+ */
378
+ async function pauseBelow(key, caller, call, depth, seen) {
379
+ const name = call.name;
380
+ const id = call.id;
381
+ if (depth >= MAX_DELEGATION_DEPTH || seen.has(name))
382
+ return undefined;
383
+ // Where runSubagent filed the call (ADR 0111), else where it filed it before.
384
+ const own = key.appName === undefined ? undefined : await key.sessions.get({ appName: childAppName({ appName: key.appName, delegated: key.delegated === true }, caller, name), userId: key.userId, sessionId: key.sessionId });
385
+ const child = own ?? (await legacyChild(key.sessions, key, name, true));
386
+ if (!child)
387
+ return undefined;
388
+ const here = [caller, name];
389
+ const walked = workflowPause(child.events, name);
390
+ if (walked) {
391
+ const path = [...here, walked.asker];
392
+ return {
393
+ path,
394
+ callIds: [id],
395
+ ...(walked.approval ? { approval: { ...walked.approval, path } } : {}),
396
+ ...(walked.question ? { question: { ...walked.question, path } } : {}),
397
+ };
398
+ }
399
+ const approval = pendingApproval(child.events);
400
+ if (approval && approval.agent === name)
401
+ return { path: here, callIds: [id], approval: { ...approval, path: here } };
402
+ const question = pendingQuestion(child.events);
403
+ if (question && question.node === name)
404
+ return { path: here, callIds: [id], question: { ...question, path: here } };
405
+ const [first] = await delegatedPauses({ ...key, appName: child.appName, delegated: true }, child.events, name, depth + 1, new Set([...seen, name]));
406
+ if (!first)
407
+ return undefined;
408
+ const path = [caller, ...first.path];
409
+ return {
410
+ path,
411
+ callIds: [id, ...first.callIds],
412
+ ...(first.approval ? { approval: { ...first.approval, path } } : {}),
413
+ ...(first.question ? { question: { ...first.question, path } } : {}),
414
+ };
415
+ }
416
+ /**
417
+ * The pause a nested workflow's walk holds in its child session (ADR 0111):
418
+ * the workflow's own pause record, the last event, authored by the
419
+ * workflow (`name`) at its own path with no content, names the open
420
+ * interrupts; the approval request or ask_user question among them is the
421
+ * pause, and the node that raised it the asker. Undefined when the session
422
+ * holds no paused walk of `name`'s.
423
+ */
424
+ function workflowPause(events, name) {
425
+ const record = events[events.length - 1];
426
+ if (!record || record.author !== name || (record.content?.parts ?? []).length > 0 || record.nodeInfo?.path !== name)
427
+ return undefined;
428
+ const open = new Set(record.longRunningToolIds ?? []);
429
+ if (open.size === 0)
430
+ return undefined;
431
+ const approval = pendingApproval(events);
432
+ if (approval && open.has(approval.id) && approval.agent && approval.agent !== 'user')
433
+ return { asker: approval.agent, approval };
434
+ // The ask_user node's request the record names: the walk stores node inputs as user turns, so a later one may follow it.
435
+ for (let i = events.length - 2; i >= 0; i--) {
436
+ const event = events[i];
437
+ if (!event.author || event.author === 'user')
438
+ continue;
439
+ for (const call of getFunctionCalls(event)) {
440
+ if (call.name !== REQUEST_INPUT_CALL || !call.id || !open.has(call.id))
441
+ continue;
442
+ const question = inputRequestFrom(event.author, call);
443
+ if (question && question.id === call.id)
444
+ return { asker: event.author, question };
445
+ }
446
+ }
447
+ return undefined;
448
+ }
449
+ /**
450
+ * Every pause waiting inside a delegated call still open in `events` (one
451
+ * session's events), latest call first: each open call whose child session
452
+ * holds an open approval request or question, at any depth. `author`, when
453
+ * given, keeps one agent's calls. A call the person has written past is no
454
+ * longer open, and neither is anything waiting below it.
455
+ */
456
+ export async function delegatedPauses(key, events, author, depth = 0, seen = new Set()) {
457
+ const out = [];
458
+ for (const { author: caller, call } of openCalls(events, author)) {
459
+ const pause = await pauseBelow(key, caller, call, depth, seen);
460
+ if (pause)
461
+ out.push(pause);
462
+ }
463
+ return out;
464
+ }
465
+ /** The interrupt a delegated pause waits on: the approval request's id, or the question's. */
466
+ export function interruptIdOf(pause) {
467
+ return pause.approval?.id ?? pause.question?.id;
468
+ }
469
+ /**
470
+ * The delegated calls `agent` left open that the latest user message
471
+ * answers (ADR 0110): the message's approval decisions and question
472
+ * answers (function responses to `adk_request_confirmation` or `ask_user`)
473
+ * matched, by interrupt id, to the pause below each open call. Undefined
474
+ * when the message answers none. The child binds the answer itself: an
475
+ * approval is checked against its pinned call there (approvedCalls), as at
476
+ * the top.
477
+ */
478
+ export async function resumedDelegations(agent, scope, sessions) {
479
+ const events = scope.session.events;
480
+ let latest;
481
+ for (let i = events.length - 1; i >= 0 && !latest; i--)
482
+ if (events[i]?.author === 'user')
483
+ latest = events[i];
484
+ if (!latest)
485
+ return undefined;
486
+ const answerParts = (latest.content?.parts ?? []).filter((p) => {
487
+ const r = p.functionResponse;
488
+ return !!r?.id && (r.name === REQUEST_CONFIRMATION_CALL || r.name === ASK_USER);
489
+ });
490
+ if (answerParts.length === 0)
491
+ return undefined;
492
+ const pauses = await delegatedPauses({ sessions, userId: scope.session.userId, sessionId: scope.session.id, appName: scope.session.appName, delegated: scope.delegated === true }, events, agent.name);
493
+ if (pauses.length === 0)
494
+ return undefined;
495
+ const open = new Map(openCalls(events, agent.name).map(({ call }) => [call.id, call]));
496
+ const answers = new Map();
497
+ const waiting = [];
498
+ for (const pause of pauses) {
499
+ const callId = pause.callIds[0];
500
+ const parts = answerParts.filter((p) => p.functionResponse?.id === interruptIdOf(pause));
501
+ if (parts.length)
502
+ answers.set(callId, [...(answers.get(callId) ?? []), ...parts]);
503
+ else
504
+ waiting.push(callId);
505
+ }
506
+ if (answers.size === 0)
507
+ return undefined;
508
+ const calls = [...answers.keys()].map((id) => open.get(id)).filter((c) => !!c);
509
+ return { calls, answers, waiting: waiting.filter((id) => !answers.has(id)), tools: await toolsOf(agent, scope) };
510
+ }
@@ -13,8 +13,12 @@
13
13
  *
14
14
  * THE REQUEST, in the order ADK built it:
15
15
  * 1. The agent's generateContentConfig, and its output schema when it has
16
- * no tools (or the model takes a schema beside tools: Gemini 2 and
17
- * later on Vertex AI).
16
+ * no tools, or when every model the step may call (its own and its
17
+ * fallback_model) takes a schema beside tools in one request: Claude
18
+ * from Opus 4.8, Sonnet 5 and Haiku 5.5 on, OpenAI, Gemini 2 and later
19
+ * on Vertex AI (outputSchemaBesideTools, lib/models/capabilities.ts,
20
+ * ADR 0109). That is how an orchestrator that delegates answers in a
21
+ * schema without a set_model_response call.
18
22
  * 2. The system prompt, each piece joined to the last by a blank line:
19
23
  * - the identity lines ("You are an agent. Your internal name is …"),
20
24
  * unless the agent may transfer to no one (an output schema rules
@@ -112,6 +116,8 @@ export interface NativeAgent {
112
116
  fallbackModel?: string;
113
117
  /** `context:` (ADR 0033): compact the history into a summary past a token threshold. The loop runs it before each step (compaction.ts). */
114
118
  context?: ContextConfig;
119
+ /** The syndicate's `max_concurrency` (ADR 0116): delegated calls one step runs at once. Default DEFAULT_MAX_CONCURRENCY (delegate.ts). */
120
+ maxConcurrency?: number;
115
121
  }
116
122
  /** What an instruction function reads. */
117
123
  export interface InstructionContext {
@@ -166,7 +172,11 @@ export interface BuiltRequest {
166
172
  export declare const SET_MODEL_RESPONSE = "set_model_response";
167
173
  /** set_model_response as an own Tool: its answer is the arguments, as JSON text, and it ends the step. */
168
174
  export declare function setModelResponseTool(outputSchema: Record<string, unknown>): Tool;
169
- /** Whether the model takes an output schema beside tools in one request (ADK: Vertex AI, Gemini 2 and later). */
175
+ /**
176
+ * ADK's rule for whether the model takes an output schema beside tools in one
177
+ * request (Vertex AI, Gemini 2 and later). `mode: task` keeps it as it is,
178
+ * for the instruction line ADK's processor writes there.
179
+ */
170
180
  export declare function outputSchemaWithTools(model: string): boolean;
171
181
  /**
172
182
  * What ADK's runLlmAgentAsNode puts in a node agent's invocation context
@@ -13,8 +13,12 @@
13
13
  *
14
14
  * THE REQUEST, in the order ADK built it:
15
15
  * 1. The agent's generateContentConfig, and its output schema when it has
16
- * no tools (or the model takes a schema beside tools: Gemini 2 and
17
- * later on Vertex AI).
16
+ * no tools, or when every model the step may call (its own and its
17
+ * fallback_model) takes a schema beside tools in one request: Claude
18
+ * from Opus 4.8, Sonnet 5 and Haiku 5.5 on, OpenAI, Gemini 2 and later
19
+ * on Vertex AI (outputSchemaBesideTools, lib/models/capabilities.ts,
20
+ * ADR 0109). That is how an orchestrator that delegates answers in a
21
+ * schema without a set_model_response call.
18
22
  * 2. The system prompt, each piece joined to the last by a blank line:
19
23
  * - the identity lines ("You are an agent. Your internal name is …"),
20
24
  * unless the agent may transfer to no one (an output schema rules
@@ -66,6 +70,7 @@
66
70
  * and defineTool, as registerTool refuses one (ADR 0107).
67
71
  */
68
72
  import { contentsToMessages, reasoningOf, samplingOf, systemText, toolChoiceOf } from '../../models/genaiMapping.js';
73
+ import { outputSchemaBesideTools } from '../../models/capabilities.js';
69
74
  import { providerForModel } from '../../models/providerMap.js';
70
75
  import { contractToolDeclaration, nativeToolOf, toContractJsonSchema } from '../../models/schemaNormalize.js';
71
76
  import { createToolContext, instructionToolOf, isTool, toolOf } from '../../tools/tool.js';
@@ -131,10 +136,26 @@ function vertexAi() {
131
136
  return envFlag('GOOGLE_GENAI_USE_ENTERPRISE');
132
137
  return envFlag('GOOGLE_GENAI_USE_VERTEXAI');
133
138
  }
134
- /** Whether the model takes an output schema beside tools in one request (ADK: Vertex AI, Gemini 2 and later). */
139
+ /**
140
+ * ADK's rule for whether the model takes an output schema beside tools in one
141
+ * request (Vertex AI, Gemini 2 and later). `mode: task` keeps it as it is,
142
+ * for the instruction line ADK's processor writes there.
143
+ */
135
144
  export function outputSchemaWithTools(model) {
136
145
  return vertexAi() && isGemini2OrAbove(model);
137
146
  }
147
+ /**
148
+ * Whether the agent's output schema travels beside its tools as the
149
+ * provider's own structured-output field (ADR 0109): on every model the step
150
+ * may call, its own and its fallback_model, since a fallback answers the same
151
+ * request. ADK's Gemini rule still holds; the capability matrix's
152
+ * `structured_output_with_tools` adds Claude's current generations and
153
+ * OpenAI. Anywhere else the schema is the set_model_response tool.
154
+ */
155
+ function schemaBesideTools(agent) {
156
+ const models = agent.fallbackModel ? [agent.model, agent.fallbackModel] : [agent.model];
157
+ return models.every((m) => outputSchemaWithTools(m) || outputSchemaBesideTools(m));
158
+ }
138
159
  // ── Session state in an instruction (ADK's injectSessionState) ───────────────
139
160
  const STATE_PREFIXES = ['app:', 'user:', 'temp:'];
140
161
  const ARTIFACT_PREFIX = 'artifact.';
@@ -437,7 +458,10 @@ export async function buildModelRequest(agent, ctx) {
437
458
  throw new Error('Response schema must be set via LlmAgent.output_schema.');
438
459
  const listed = agent.tools ?? [];
439
460
  const taskMode = agent.mode === 'task';
440
- const schemaWithTools = !!agent.outputSchema && listed.length > 0 && !outputSchemaWithTools(model);
461
+ // An output schema beside tools: set_model_response unless the step's models take both in one request
462
+ // (ADR 0109). Task mode keeps ADK's rule: finish_task carries the schema there.
463
+ const besideTools = !!agent.outputSchema && listed.length > 0;
464
+ const schemaWithTools = besideTools && !(taskMode ? outputSchemaWithTools(model) : schemaBesideTools(agent));
441
465
  // 1. Basic: the config, and the output schema where the model takes it (never in task mode: finish_task carries it).
442
466
  if (agent.outputSchema && !schemaWithTools && !taskMode) {
443
467
  cfg.responseSchema = agent.outputSchema;
@@ -40,10 +40,11 @@ import { SpanStatusCode, context, trace } from '@opentelemetry/api';
40
40
  import { resolveAdapter } from '../../models/registry.js';
41
41
  import { RUNTIME_SPAN_SCOPE } from '../../observability/lineage.js';
42
42
  import { payloadPolicyFromEnv } from '../../observability/supabaseSpanExporter.js';
43
- import { initializeTracing } from '../../observability/tracer.js';
43
+ import { startEngineSpan } from '../../observability/tracer.js';
44
44
  /** The most a payload attribute holds, as for a failed call's (lib/observability/tracer.ts). */
45
45
  const PAYLOAD_MAX_CHARS = 200_000;
46
- const runtimeTracer = () => trace.getTracer(RUNTIME_SPAN_SCOPE);
46
+ /** A loop span in scope melchizedek.runtime; a non-recording one in a turn that opted out of tracing. */
47
+ const startRuntimeSpan = (name, options) => startEngineSpan(RUNTIME_SPAN_SCOPE, name, options);
47
48
  /** JSON that never throws and never exceeds the cap. */
48
49
  function payloadJson(value) {
49
50
  let json;
@@ -85,8 +86,7 @@ function failed(span, error) {
85
86
  }
86
87
  /** The agent's run as an `agent.invoke <name>` span, opened when the run starts. */
87
88
  export async function* traceAgentInvocation(agent, ctx, run) {
88
- initializeTracing();
89
- const span = runtimeTracer().startSpan(`agent.invoke ${agent.name}`, {
89
+ const span = startRuntimeSpan(`agent.invoke ${agent.name}`, {
90
90
  attributes: {
91
91
  'gen_ai.operation.name': 'invoke_agent',
92
92
  'gen_ai.agent.name': agent.name,
@@ -116,8 +116,7 @@ export async function* traceAgentInvocation(agent, ctx, run) {
116
116
  * provider that answered (the fallback's, when a fallback answered).
117
117
  */
118
118
  export async function* traceModelCall(agent, ctx, step) {
119
- initializeTracing();
120
- const span = runtimeTracer().startSpan('model.call', {
119
+ const span = startRuntimeSpan('model.call', {
121
120
  attributes: {
122
121
  'gen_ai.operation.name': 'chat',
123
122
  'gen_ai.agent.name': agent.name,
@@ -173,7 +172,7 @@ function recordModelCall(span, result, provider) {
173
172
  span.setAttribute('llm.error_code', String(response.error.code));
174
173
  return;
175
174
  }
176
- if (payloadPolicyFromEnv().mode === 'off')
175
+ if (!span.isRecording() || payloadPolicyFromEnv().mode === 'off')
177
176
  return;
178
177
  const { signal: _signal, ...request } = result.request;
179
178
  span.setAttribute('llm.payload.request', payloadJson(request));
@@ -181,10 +180,9 @@ function recordModelCall(span, result, provider) {
181
180
  }
182
181
  /** One tool call as a `tool.execute <name>` span, under the agent's. */
183
182
  export async function traceToolCall(call, tool, run) {
184
- initializeTracing();
185
183
  const name = call.name || '<unnamed>';
186
184
  const description = tool && typeof tool === 'object' ? tool.description : undefined;
187
- const span = runtimeTracer().startSpan(`tool.execute ${name}`, {
185
+ const span = startRuntimeSpan(`tool.execute ${name}`, {
188
186
  attributes: {
189
187
  'gen_ai.operation.name': 'execute_tool',
190
188
  'gen_ai.tool.name': name,
@@ -221,8 +219,7 @@ export async function traceToolCall(call, tool, run) {
221
219
  // attributes each model call to the node's agent, as on ADK.
222
220
  /** The walk as a `workflow.invoke <name>` span; every node span opens under it. */
223
221
  export async function traceWorkflowInvocation(workflow, ctx, run) {
224
- initializeTracing();
225
- const span = runtimeTracer().startSpan(`workflow.invoke ${workflow.name}`, {
222
+ const span = startRuntimeSpan(`workflow.invoke ${workflow.name}`, {
226
223
  attributes: {
227
224
  'gen_ai.operation.name': 'invoke_workflow',
228
225
  'gen_ai.conversation.id': ctx.sessionId,
@@ -248,8 +245,7 @@ export async function traceWorkflowInvocation(workflow, ctx, run) {
248
245
  * attempts the run made.
249
246
  */
250
247
  export async function traceNodeExecution(node, run) {
251
- initializeTracing();
252
- const span = runtimeTracer().startSpan(`node.execute ${node.name}`, {
248
+ const span = startRuntimeSpan(`node.execute ${node.name}`, {
253
249
  attributes: {
254
250
  'gen_ai.operation.name': 'execute_node',
255
251
  'adk.node.path': node.path,
@@ -274,9 +270,8 @@ export async function traceNodeExecution(node, run) {
274
270
  }
275
271
  /** A tool node's call as a `tool.execute <name>` span, under its node's (ToolNodeContext.traceCall). */
276
272
  export async function traceToolNodeCall(call, tool, run) {
277
- initializeTracing();
278
273
  const description = tool && typeof tool === 'object' ? tool.description : undefined;
279
- const span = runtimeTracer().startSpan(`tool.execute ${call.name}`, {
274
+ const span = startRuntimeSpan(`tool.execute ${call.name}`, {
280
275
  attributes: {
281
276
  'gen_ai.operation.name': 'execute_tool',
282
277
  'gen_ai.tool.name': call.name,
@@ -17,10 +17,13 @@
17
17
  * (lib/workflow.ts) and an approval (ADR 0028); a client handles all three
18
18
  * by showing `result.input` (or the approval) and sending the next message.
19
19
  *
20
- * WHERE: only on an agent the turn runs directly (the orchestrator, or a
21
- * plan-dispatch route), as with approval gates: inside a delegated
22
- * subagent the pause cannot reach the person. Not inside a workflow node
23
- * yet. The schema refuses both.
20
+ * WHERE: on any agent of a delegate or dispatch syndicate, as with approval
21
+ * gates. Inside a delegated subagent the question waits in the child's
22
+ * session, below the call its caller left open, and reaches the person
23
+ * with the agent path (`result.input.path`, ADR 0110); the next message is
24
+ * stored as the answer and carried down to the child
25
+ * (lib/runtime/native/interrupts.ts). Not inside a workflow node yet; the
26
+ * schema refuses it there.
24
27
  *
25
28
  * RESUME: the answer is an ordinary function response, so the loop resumes
26
29
  * a question ADK opened before 1.0.0 as one it opened itself (ADR 0079); no
@@ -37,7 +37,7 @@
37
37
  import type { TurnEvent } from './events.ts';
38
38
  /** A conversation's address. Stores key it as `<appName>:<userId>:<sessionId>`. */
39
39
  export interface SessionKey {
40
- /** The syndicate (or, for a DELEGATE subagent's own row, the subagent's name). */
40
+ /** The syndicate (or, for a DELEGATE subagent's own row, its agent path: lib/runtime/native/delegate.ts childAppName). */
41
41
  appName: string;
42
42
  userId: string;
43
43
  sessionId: string;
@@ -144,7 +144,13 @@ export interface SyndicateTurnOptions {
144
144
  maxLlmCalls?: number;
145
145
  /** Ask adapters for token-by-token partial events (the REPL's live view). */
146
146
  streaming?: boolean;
147
- /** Root-span metadata; `false` disables tracing for the turn. */
147
+ /**
148
+ * Root-span metadata. `false` disables tracing for the turn: it records no
149
+ * span, so no row reaches the ledger (adk_telemetry, adk_turns,
150
+ * adk_payloads), the console exporter, the in-process listeners or an OTLP
151
+ * endpoint, and the turn does not start the tracer. The step budget and
152
+ * the usage in the result are unaffected. Default: traced.
153
+ */
148
154
  trace?: TraceOptions | false;
149
155
  events?: TurnEvents;
150
156
  /**
@@ -63,10 +63,14 @@ import { isDispatchSyndicate, matchRouteOverride, resolveRoute } from '../dispat
63
63
  import { collectGrounding, describeGrounding, newGroundingState, webSourcesLine } from '../grounding.js';
64
64
  import { resolveGuards } from '../guards/index.js';
65
65
  import { collectGuards, nestedLoader } from '../loadSyndicate.js';
66
+ import { context } from '@opentelemetry/api';
67
+ import core from '@opentelemetry/core';
68
+ const { suppressTracing } = core;
66
69
  import { traceAgentRun } from '../observability/tracer.js';
67
70
  import { ProjectedSessionService, renderTranscriptDigest } from '../session/transcript.js';
68
71
  import { approvalDecisionIn, describeApproval, interruptedTurnStart, pendingApproval } from './approvals.js';
69
72
  import { pendingQuestion, questionAnswerPart, questionFrom, turnStartOfCall } from './questions.js';
73
+ import { delegatedPauses } from './native/interrupts.js';
70
74
  export { ASK_USER, pendingQuestion, questionAnswerPart } from './questions.js';
71
75
  export { approvalResponsePart, describeApproval, pendingApproval } from './approvals.js';
72
76
  import { RemoteA2AAgent, remoteContextId, remoteToolOutput } from '../a2a/remoteAgent.js';
@@ -274,9 +278,15 @@ export async function runSyndicateTurn(opts) {
274
278
  maxLlmCalls: opts.maxLlmCalls ?? config.max_steps ?? DEFAULT_MAX_STEPS,
275
279
  deadlineMs: opts.deadlineMs,
276
280
  signal: opts.signal,
281
+ // `trace: false`: no span from anything the turn runs, to any sink.
282
+ untraced: opts.trace === false,
277
283
  });
278
284
  try {
279
- return await runWithTurnControl(control, () => runTurnInner(opts, control, runtime));
285
+ const run = () => runWithTurnControl(control, () => runTurnInner(opts, control, runtime));
286
+ // A span a library opens through OpenTelemetry's own API (not the
287
+ // engine's, which read control.untraced) is suppressed too, while a
288
+ // registered tracer is there to read the suppression.
289
+ return await (control.untraced ? context.with(suppressTracing(context.active()), run) : run());
280
290
  }
281
291
  finally {
282
292
  control.dispose();
@@ -360,16 +370,25 @@ async function runTurnInner(opts, control, _runtime) {
360
370
  // A message answering an approval resumes the agent that asked; the answer
361
371
  // must name the request still open in this conversation.
362
372
  const decision = approvalDecisionIn(parts);
373
+ // Pauses inside delegated subagents (ADR 0110): what waits below a call an agent of this conversation left open.
374
+ const delegationKey = { sessions: sessionService, userId, sessionId, appName };
375
+ /** The first pause waiting below an open delegated call in this conversation, `author`'s calls only when given. */
376
+ const pauseBelow = async (events, author) => isWorkflowSyndicate(config) ? undefined : (await delegatedPauses(delegationKey, events, author))[0];
377
+ let resumingBelow;
363
378
  let resuming;
364
379
  if (decision) {
365
380
  resuming = pendingApproval(existing?.events ?? []);
381
+ if (!resuming) {
382
+ resumingBelow = await pauseBelow(existing?.events ?? []);
383
+ resuming = resumingBelow?.approval;
384
+ }
366
385
  if (!resuming || resuming.id !== decision.id) {
367
386
  result.status = 'failed';
368
387
  result.error = { code: 'NO_PENDING_APPROVAL', message: `No approval ${decision.id} is waiting in this conversation.` };
369
388
  return finish();
370
389
  }
371
390
  // The log names the call, not its arguments: they are user content.
372
- ev.log?.(`✓ Approval ${decision.approved ? 'granted' : 'refused'}: ${resuming.agent} → ${resuming.tool}`);
391
+ ev.log?.(`✓ Approval ${decision.approved ? 'granted' : 'refused'}: ${(resuming.path ?? [resuming.agent]).join(' → ')} → ${resuming.tool}`);
373
392
  }
374
393
  // ── Consent (lib/runtime/credentials.ts, ADR 0085) ────────────────────────
375
394
  // While a call waits for an OAuth grant, the next message resumes it once
@@ -411,24 +430,30 @@ async function runTurnInner(opts, control, _runtime) {
411
430
  // answer: it becomes that call's response, and the agent that asked
412
431
  // resumes its own tool loop (the loop reads the response from its history;
413
432
  // ADR 0079). A workflow's pauses are its walk's own business.
433
+ // A question asked inside a delegated subagent (ADR 0110) is answered the same way: the answer is stored here, and the
434
+ // open call that leads to the asker carries it down; `answering` names that call and the agent that made it.
414
435
  let answering;
415
436
  if (!decision && !granting && !isWorkflowSyndicate(config)) {
416
- const question = pendingQuestion(existing?.events ?? []);
417
437
  const plainText = parts.length > 0 && parts.every((p) => typeof p.text === 'string');
418
- if (question && plainText) {
419
- answering = { agent: question.node, id: question.id };
438
+ const own = plainText ? pendingQuestion(existing?.events ?? []) : undefined;
439
+ const below = plainText && !own ? await pauseBelow(existing?.events ?? []) : undefined;
440
+ const question = own ?? below?.question;
441
+ if (question) {
442
+ answering = below ? { agent: below.path[0], id: below.callIds[0] } : { agent: question.node, id: question.id };
420
443
  parts = [questionAnswerPart(question.id, messageText)];
421
- ev.log?.(`✓ Answer to ${question.node}'s question`);
444
+ ev.log?.(`✓ Answer to ${(question.path ?? [question.node]).join(' → ')}'s question`);
422
445
  }
423
446
  }
424
447
  /** The agent a dispatch turn must resume, and the call its interrupted turn holds. */
425
- const resumeTarget = resuming
426
- ? { agent: resuming.agent, id: resuming.id, why: 'resuming an approval' }
427
- : granting
428
- ? { agent: granting.agent, id: granting.id, why: 'resuming after an authorization' }
429
- : answering
430
- ? { ...answering, why: 'answering its question' }
431
- : undefined;
448
+ const resumeTarget = resumingBelow
449
+ ? { agent: resumingBelow.path[0], id: resumingBelow.callIds[0], why: 'resuming an approval' }
450
+ : resuming
451
+ ? { agent: resuming.agent, id: resuming.id, why: 'resuming an approval' }
452
+ : granting
453
+ ? { agent: granting.agent, id: granting.id, why: 'resuming after an authorization' }
454
+ : answering
455
+ ? { ...answering, why: 'answering its question' }
456
+ : undefined;
432
457
  /** After the answering run: is a call now waiting for a grant? */
433
458
  const awaitingConsent = async (agentName) => {
434
459
  if (!opts.toolCredentials?.consent)
@@ -454,11 +479,26 @@ async function runTurnInner(opts, control, _runtime) {
454
479
  ev.log?.(`⏸ Input needed: ${describeInput(input)}`);
455
480
  return finish();
456
481
  };
482
+ /** After the answering run: does a pause wait inside a delegated call `agentName` left open (ADR 0110)? */
483
+ const pausedBelow = async (agentName) => {
484
+ const after = await sessionService.get({ appName, userId, sessionId });
485
+ const below = await pauseBelow(after?.events ?? [], agentName);
486
+ if (below?.approval)
487
+ return pause(below.approval);
488
+ if (below?.question) {
489
+ result.status = 'input-required';
490
+ result.input = below.question;
491
+ result.text = below.question.message;
492
+ ev.log?.(`⏸ Input needed: ${describeInput(below.question)} (${below.path.join(' → ')})`);
493
+ return finish();
494
+ }
495
+ return undefined;
496
+ };
457
497
  const pause = (pending) => {
458
498
  result.status = 'input-required';
459
499
  result.approval = pending;
460
500
  result.text = `Approval needed: ${describeApproval(pending)}.`;
461
- ev.log?.(`⏸ Approval needed: ${pending.agent} → ${pending.tool}`);
501
+ ev.log?.(`⏸ Approval needed: ${(pending.path ?? [pending.agent]).join(' → ')} → ${pending.tool}`);
462
502
  return finish();
463
503
  };
464
504
  /** Run ONE agent against ONE session, under the turn's controls. */
@@ -690,13 +730,15 @@ async function runTurnInner(opts, control, _runtime) {
690
730
  userParts: parts,
691
731
  // Resuming an approval or answering a question: the interrupted turn
692
732
  // is replayed raw, so the loop finds the call the message answers.
693
- sessions: new ProjectedSessionService(sessionService, routeCfg.name, resuming
694
- ? { rawFrom: (events) => interruptedTurnStart(events, resuming.id) }
695
- : answering
696
- ? { rawFrom: (events) => turnStartOfCall(events, answering.id) }
697
- : granting
698
- ? { rawFrom: (events) => turnStartOfCall(events, granting.id) }
699
- : {}),
733
+ sessions: new ProjectedSessionService(sessionService, routeCfg.name, resumingBelow
734
+ ? { rawFrom: (events) => turnStartOfCall(events, resumingBelow.callIds[0]) }
735
+ : resuming
736
+ ? { rawFrom: (events) => interruptedTurnStart(events, resuming.id) }
737
+ : answering
738
+ ? { rawFrom: (events) => turnStartOfCall(events, answering.id) }
739
+ : granting
740
+ ? { rawFrom: (events) => turnStartOfCall(events, granting.id) }
741
+ : {}),
700
742
  stage: 'dispatch',
701
743
  route: resolution,
702
744
  publishToolStatus: true,
@@ -709,11 +751,16 @@ async function runTurnInner(opts, control, _runtime) {
709
751
  result.error = answer.error;
710
752
  return finish();
711
753
  }
712
- if (!routeCfg.a2a_agent_url && agentGates(routeCfg)) {
754
+ if (!routeCfg.a2a_agent_url && (agentGates(routeCfg) || routeCfg.yaml_reference)) {
713
755
  const pending = await awaitingApproval(routeCfg.name);
714
756
  if (pending)
715
757
  return pause(pending);
716
758
  }
759
+ if (!routeCfg.a2a_agent_url) {
760
+ const below = await pausedBelow(routeCfg.name);
761
+ if (below)
762
+ return below;
763
+ }
717
764
  if (!routeCfg.a2a_agent_url) {
718
765
  const consent = await awaitingConsent(routeCfg.name);
719
766
  if (consent)
@@ -835,6 +882,11 @@ async function runTurnInner(opts, control, _runtime) {
835
882
  const consent = await awaitingConsent(config.orchestrator.name);
836
883
  if (consent)
837
884
  return consentPause(consent);
885
+ if (config.subagents?.length) {
886
+ const below = await pausedBelow(config.orchestrator.name);
887
+ if (below)
888
+ return below;
889
+ }
838
890
  const askedOrchestrator = asked(answer);
839
891
  if (askedOrchestrator)
840
892
  return askedOrchestrator;