melchizedek-agents 1.5.0 → 1.7.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 (44) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/DOCUMENTATION.md +28 -2
  3. package/README.md +1 -1
  4. package/config/agents/syndicate.schema.json +63 -0
  5. package/config/agents/syndicateSchema.yaml +50 -0
  6. package/db/hardening.sql +5 -2
  7. package/db/migrations/0015_model_pricing.sql +107 -0
  8. package/db/telemetry.sql +79 -1
  9. package/dist/lib/a2a/executor.js +21 -0
  10. package/dist/lib/compile.d.ts +4 -3
  11. package/dist/lib/compile.js +8 -3
  12. package/dist/lib/index.d.ts +9 -1
  13. package/dist/lib/index.js +5 -0
  14. package/dist/lib/loadSyndicate.d.ts +13 -0
  15. package/dist/lib/loadSyndicate.js +11 -1
  16. package/dist/lib/observability/supabaseSpanExporter.js +2 -1
  17. package/dist/lib/observability/tracer.d.ts +11 -2
  18. package/dist/lib/observability/tracer.js +17 -4
  19. package/dist/lib/runtime/native/agentLoop.d.ts +6 -0
  20. package/dist/lib/runtime/native/checkpoint.d.ts +2 -1
  21. package/dist/lib/runtime/native/checkpoint.js +6 -2
  22. package/dist/lib/runtime/native/delegate.js +30 -1
  23. package/dist/lib/runtime/native/interrupts.js +9 -2
  24. package/dist/lib/runtime/nativeTurn.d.ts +2 -1
  25. package/dist/lib/runtime/nativeTurn.js +5 -1
  26. package/dist/lib/runtime/syndicateTurn.d.ts +25 -2
  27. package/dist/lib/runtime/syndicateTurn.js +387 -5
  28. package/dist/lib/runtime/turnControl.d.ts +27 -0
  29. package/dist/lib/sandbox/sandbox.d.ts +121 -0
  30. package/dist/lib/sandbox/sandbox.js +276 -0
  31. package/dist/lib/sandbox/worker.d.ts +29 -0
  32. package/dist/lib/sandbox/worker.js +280 -0
  33. package/dist/lib/script/config.d.ts +35 -0
  34. package/dist/lib/script/config.js +17 -0
  35. package/dist/lib/script/journal.d.ts +137 -0
  36. package/dist/lib/script/journal.js +193 -0
  37. package/dist/lib/script/run.d.ts +138 -0
  38. package/dist/lib/script/run.js +364 -0
  39. package/dist/lib/syndicateSchema.d.ts +20 -0
  40. package/dist/lib/syndicateSchema.js +98 -6
  41. package/dist/lib/toolRegistry.js +4 -0
  42. package/dist/lib/tools/runCodeTool.d.ts +50 -0
  43. package/dist/lib/tools/runCodeTool.js +59 -0
  44. package/package.json +5 -1
@@ -37,6 +37,7 @@
37
37
  import { relative } from 'node:path';
38
38
  import { isDispatchSyndicate } from './dispatch.js';
39
39
  import { isWorkflowSyndicate, nodeKind } from './workflowConfig.js';
40
+ import { isScriptSyndicate } from './script/config.js';
40
41
  import { loadSyndicate, nestedLoader } from './loadSyndicate.js';
41
42
  import { REASONING_OLDER_SPELLING } from './syndicateSchema.js';
42
43
  import { reasoningConfig } from './models/reasoning.js';
@@ -134,7 +135,7 @@ function gateTools(tools, names, agentName) {
134
135
  }
135
136
  /** True when any agent of a syndicate may pause for approval: a gated tool, or skill scripts. */
136
137
  export function declaresApprovals(config) {
137
- return agentGates(config.orchestrator) || (config.subagents ?? []).some((s) => agentGates(s));
138
+ return agentGates(config.orchestrator) || (config.subagents ?? []).some((s) => agentGates(s)) || !!config.script?.require_approval?.length;
138
139
  }
139
140
  /**
140
141
  * An agent's `examples:` as their InstructionTool (lib/tools/examples.ts):
@@ -398,8 +399,9 @@ export async function compileEntrySpec(subCfg, opts = {}, delegated = false) {
398
399
  if (isWorkflowSyndicate(nested)) {
399
400
  return { kind: 'workflow', workflow: await compileWorkflowSpec(nested, nestedOpts, subCfg.name, subCfg.description, subCfg.yaml_reference, delegated) };
400
401
  }
401
- // A nested dispatch syndicate classifies and routes as at the top (ADR 0120), wherever it runs.
402
- if (isDispatchSyndicate(nested))
402
+ // A nested dispatch syndicate classifies and routes as at the top (ADR 0120), wherever it runs; a nested script
403
+ // syndicate runs its whole script the same way, as a turn of its own (ADR 0137 item 15).
404
+ if (isDispatchSyndicate(nested) || isScriptSyndicate(nested))
403
405
  return { kind: 'dispatch', dispatch: compileDispatchEntry(subCfg, nested, nestedOpts) };
404
406
  return { kind: 'agent', spec: await compileSpec(nested, nestedOpts, subCfg.name, subCfg.description) };
405
407
  }
@@ -437,6 +439,9 @@ export async function compileSubagentSpec(subCfg, opts = {}) {
437
439
  if (isDispatchSyndicate(nested)) {
438
440
  throw new Error(`${subCfg.yaml_reference}: '${subCfg.name}' is a dispatch syndicate, which runs as its own turn (its classifier, then a route), not as one agent; compile the entry with compileEntrySpec (ADR 0120).`);
439
441
  }
442
+ if (isScriptSyndicate(nested)) {
443
+ throw new Error(`${subCfg.yaml_reference}: '${subCfg.name}' is a script syndicate, which runs as its own turn (its script), not as one agent; compile the entry with compileEntrySpec (ADR 0137).`);
444
+ }
440
445
  return compileSpec(nested, nestedOpts, subCfg.name, subCfg.description);
441
446
  }
442
447
  // YAML v2 (ADR 0115): the engine form, for a config built in code (a loaded one already is).
@@ -24,10 +24,18 @@ export type { MemoryEntry, MemoryIngestOptions, MemorySearchRequest, MemorySearc
24
24
  export type { TurnEvent } from './runtime/events.ts';
25
25
  export { approvalResponsePart, pendingApproval, APPROVAL_REQUEST } from './runtime/approvals.ts';
26
26
  export type { PendingApproval } from './runtime/approvals.ts';
27
- export type { DrainedRun, MessagePart, TurnUsage, RouteDecision, SyndicateTurnOptions, SyndicateTurnResult, TraceOptions, TurnEvents, TurnStage, } from './runtime/syndicateTurn.ts';
27
+ export type { AgentBranch, DrainedRun, MessagePart, TurnUsage, RouteDecision, SyndicateTurnOptions, SyndicateTurnResult, TraceOptions, TurnEvents, TurnStage, } from './runtime/syndicateTurn.ts';
28
28
  export type { CompileOptions } from './compile.ts';
29
29
  export { isWorkflowSyndicate, describeInput } from './workflow.ts';
30
30
  export type { WorkflowConfig, WorkflowNodeYaml, EdgeElement, PendingInput } from './workflow.ts';
31
+ export { isScriptSyndicate } from './script/config.ts';
32
+ export type { ScriptConfig } from './script/config.ts';
33
+ export { openRun, runScript, ScriptFatal, SCRIPT_CALL_LIMIT } from './script/run.ts';
34
+ export type { JournalEntry, OpenRun, ScriptCallSite, ScriptHost, ScriptPauseRecord, ScriptPinRecord, ScriptRunInput, ScriptRunOutcome } from './script/run.ts';
35
+ export { CODE_SANDBOX_ENV, CodeSandboxOffError, DEFAULT_SANDBOX_LIMITS, SandboxHalt, codeSandboxEnabled, runInSandbox } from './sandbox/sandbox.ts';
36
+ export type { SandboxLimits, SandboxResult, SandboxRunOptions } from './sandbox/sandbox.ts';
37
+ export { RUN_CODE, runCodeTool } from './tools/runCodeTool.ts';
38
+ export type { CodeRun, RunCodeOutput } from './tools/runCodeTool.ts';
31
39
  export { createA2AApp, compileAgentCard, currentRequestContext } from './a2a/app.ts';
32
40
  export type { A2AApp, A2AAppOptions, RequestIdentity } from './a2a/app.ts';
33
41
  export type { A2AContext } from './a2a/executor.ts';
package/dist/lib/index.js CHANGED
@@ -23,6 +23,11 @@ export { DEFAULT_RUNTIME, describeRuntime, RuntimeRemovedError, UnsupportedOnRun
23
23
  export { InProcessSessionService } from './runtime/sessions.js';
24
24
  export { approvalResponsePart, pendingApproval, APPROVAL_REQUEST } from './runtime/approvals.js';
25
25
  export { isWorkflowSyndicate, describeInput } from './workflow.js';
26
+ // SCRIPT orchestration and the code sandbox (ADR 0137).
27
+ export { isScriptSyndicate } from './script/config.js';
28
+ export { openRun, runScript, ScriptFatal, SCRIPT_CALL_LIMIT } from './script/run.js';
29
+ export { CODE_SANDBOX_ENV, CodeSandboxOffError, DEFAULT_SANDBOX_LIMITS, SandboxHalt, codeSandboxEnabled, runInSandbox } from './sandbox/sandbox.js';
30
+ export { RUN_CODE, runCodeTool } from './tools/runCodeTool.js';
26
31
  // ── Serve it over A2A, or call a remote A2A agent ───────────────────────────
27
32
  export { createA2AApp, compileAgentCard, currentRequestContext } from './a2a/app.js';
28
33
  export { callerTokens, firstOf, hashCallerToken, jwtIdentity, parseCallers, scopeSegment, sharedSecret, trustedHeader, } from './a2a/identity.js';
@@ -11,6 +11,8 @@ export type { SkillsConfig } from './tools/skillToolset.ts';
11
11
  import type { WorkflowConfig } from './workflowConfig.ts';
12
12
  import type { ReasoningSetting } from './models/contract.ts';
13
13
  export type { WorkflowConfig } from './workflowConfig.ts';
14
+ export type { ScriptConfig } from './script/config.ts';
15
+ import type { ScriptConfig } from './script/config.ts';
14
16
  export interface ThinkingConfig {
15
17
  /** Token budget for internal reasoning. 0 = off, -1 = dynamic. */
16
18
  thinkingBudget?: number;
@@ -175,6 +177,10 @@ export interface AgentYamlConfig {
175
177
  openapi?: OpenApiConfig[];
176
178
  /** `gemini`: the model's Python runs in Gemini's server-side sandbox (ADR 0033). Gemini models only. */
177
179
  code_execution?: 'gemini';
180
+ /** How this agent's `run_code` tool behaves (ADR 0137): `attach` puts each run's code and result on the reply. Needs run_code in `tools`. */
181
+ run_code?: {
182
+ attach?: boolean;
183
+ };
178
184
  /** Compact a long conversation into a summary past a token threshold (ADR 0033). Delegate orchestrator only. */
179
185
  context?: ContextConfig;
180
186
  /** `task`: the agent works until it calls finish_task; its arguments are the node's output. Workflow nodes only. */
@@ -239,6 +245,13 @@ export interface SyndicateYamlConfig {
239
245
  * Contract: lib/workflow.ts.
240
246
  */
241
247
  workflow?: WorkflowConfig;
248
+ /**
249
+ * Opts this syndicate into SCRIPT orchestration (ADR 0137): one JavaScript
250
+ * module, run in the engine's sandbox, calls the agents, the allowed tools
251
+ * and a person as functions. Cannot be combined with `dispatch` or
252
+ * `workflow`. Needs MELCHIZEDEK_CODE_SANDBOX=on. Contract: lib/script/.
253
+ */
254
+ script?: ScriptConfig;
242
255
  /** Optional default variable definitions for {{token}} interpolation. */
243
256
  variables?: VariableMap;
244
257
  /** Defines the persistence and semantic memory layer for the syndicate. */
@@ -87,7 +87,12 @@ function resolveAndValidate(raw, label, options) {
87
87
  ...yamlVars,
88
88
  ...bindings,
89
89
  };
90
- const interpolated = isPlainObject(raw) ? interpolateDeep(raw, merged) : raw;
90
+ // A script's source is code, not a template: `{{` is JavaScript there, and the script reads `variables` itself (ADR 0137 item 5).
91
+ const scriptSource = isPlainObject(raw) && isPlainObject(raw.script) && typeof raw.script.source === 'string' ? raw.script.source : undefined;
92
+ let interpolated = isPlainObject(raw) ? interpolateDeep(raw, merged) : raw;
93
+ if (scriptSource !== undefined && isPlainObject(interpolated) && isPlainObject(interpolated.script)) {
94
+ interpolated = { ...interpolated, script: { ...interpolated.script, source: scriptSource } };
95
+ }
91
96
  warnV1Spellings(interpolated, label, options.onWarning);
92
97
  const unresolved = findUnresolvedTokens(interpolated);
93
98
  if (unresolved.length > 0) {
@@ -96,6 +101,11 @@ function resolveAndValidate(raw, label, options) {
96
101
  const valid = validateSyndicateConfig(bundle !== undefined && isPlainObject(interpolated) ? { ...interpolated, bundled_references: bundle } : interpolated, label);
97
102
  const { variables: _stripped, ...clean } = valid;
98
103
  let config = clean;
104
+ // The script reads the bound variables (ADR 0137 item 6): the YAML's, then the caller's bindings, as {{token}} resolves them.
105
+ if (config.script) {
106
+ const yamlBound = isPlainObject(interpolated) && isPlainObject(interpolated.variables) ? interpolated.variables : {};
107
+ config = { ...config, script: { ...config.script, variables: { ...defaultBindings(), ...yamlBound, ...bindings } } };
108
+ }
99
109
  if (overrides) {
100
110
  config = applyOverrides(config, overrides);
101
111
  }
@@ -81,7 +81,8 @@ export function payloadDecision(policy, traceId, facts) {
81
81
  }
82
82
  // ── Span → row mapping (pure; tested offline) ───────────────────────────────
83
83
  const ROOT_PREFIX = 'Syndicate Execution: ';
84
- const EXPORTED_SPAN_NAMES = /^(llm\.request$|Syndicate Execution: )/;
84
+ // A script's own spans (ADR 0137 item 16) carry its run, its calls' kinds, names, sequence and status, never an argument or a result.
85
+ const EXPORTED_SPAN_NAMES = /^(llm\.request$|Syndicate Execution: |script\.(invoke|call) )/;
85
86
  function attrsOf(span) {
86
87
  return span.attributes;
87
88
  }
@@ -1,4 +1,4 @@
1
- import type { Span, SpanContext, SpanOptions } from '@opentelemetry/api';
1
+ import type { Context, Span, SpanContext, SpanOptions } from '@opentelemetry/api';
2
2
  import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
3
3
  import type { LlmResponse } from '../models/genaiMapping.ts';
4
4
  import type { TurnEvent as Event } from '../runtime/events.ts';
@@ -45,8 +45,17 @@ interface TraceStats {
45
45
  toolMs: number;
46
46
  llmCalls: number;
47
47
  models: Set<string>;
48
+ /** Each model's calls and tokens, input summed per call (what a provider bills), for `syndicate.usage` (ADR 0136). */
49
+ usage: Map<string, ModelUsage>;
48
50
  startedAt: number;
49
51
  }
52
+ /** One model's share of a turn: what the ledger prices it from (model_pricing, ADR 0136). */
53
+ export interface ModelUsage {
54
+ calls: number;
55
+ input: number;
56
+ output: number;
57
+ thinking: number;
58
+ }
50
59
  /** Read-and-forget the stats for a finished trace. */
51
60
  export declare function takeTraceStats(traceId: string): TraceStats | undefined;
52
61
  /** Name of the agent of the nearest enclosing agent span (ADK's `invoke_agent`, the loop's `agent.invoke`), or null. */
@@ -58,7 +67,7 @@ export declare function initializeTracing(): void;
58
67
  * the engine opens inside a turn goes through here or checks turnUntraced
59
68
  * itself.
60
69
  */
61
- export declare function startEngineSpan(tracerName: string, name: string, options?: SpanOptions): Span;
70
+ export declare function startEngineSpan(tracerName: string, name: string, options?: SpanOptions, parent?: Context): Span;
62
71
  export interface TraceMetadata {
63
72
  syndicateName: string;
64
73
  bindings?: Record<string, any>;
@@ -194,7 +194,7 @@ const TRACE_STATS_TTL_MS = 60 * 60 * 1000;
194
194
  function statsFor(traceId) {
195
195
  let s = traceStats.get(traceId);
196
196
  if (!s) {
197
- s = { modelMs: 0, toolMs: 0, llmCalls: 0, models: new Set(), startedAt: Date.now() };
197
+ s = { modelMs: 0, toolMs: 0, llmCalls: 0, models: new Set(), usage: new Map(), startedAt: Date.now() };
198
198
  traceStats.set(traceId, s);
199
199
  if (traceStats.size > 2_000) {
200
200
  const cutoff = Date.now() - TRACE_STATS_TTL_MS;
@@ -244,8 +244,19 @@ class SpanLineageProcessor {
244
244
  s.modelMs += spanDurationMs(span);
245
245
  s.llmCalls += 1;
246
246
  const model = span.attributes['llm.model'];
247
- if (typeof model === 'string' && model)
247
+ if (typeof model === 'string' && model) {
248
248
  s.models.add(model);
249
+ const tokens = (key) => {
250
+ const v = span.attributes[key];
251
+ return typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : 0;
252
+ };
253
+ const u = s.usage.get(model) ?? { calls: 0, input: 0, output: 0, thinking: 0 };
254
+ u.calls += 1;
255
+ u.input += tokens('llm.tokens.input');
256
+ u.output += tokens('llm.tokens.output');
257
+ u.thinking += tokens('llm.tokens.thinking');
258
+ s.usage.set(model, u);
259
+ }
249
260
  }
250
261
  else if (isToolSpanName(span.name)) {
251
262
  statsFor(traceId).toolMs += spanDurationMs(span);
@@ -334,11 +345,11 @@ function nonRecordingSpan() {
334
345
  * the engine opens inside a turn goes through here or checks turnUntraced
335
346
  * itself.
336
347
  */
337
- export function startEngineSpan(tracerName, name, options) {
348
+ export function startEngineSpan(tracerName, name, options, parent) {
338
349
  if (turnUntraced())
339
350
  return nonRecordingSpan();
340
351
  initializeTracing();
341
- return trace.getTracer(tracerName).startSpan(name, options);
352
+ return trace.getTracer(tracerName).startSpan(name, options, parent);
342
353
  }
343
354
  /** Turn identity by trace id, so llm.request children can carry it too. */
344
355
  const turnContexts = new Map();
@@ -546,6 +557,8 @@ export async function* traceAgentRun(stream, metadata) {
546
557
  span.setAttribute('syndicate.latency.tool_ms', Math.round(stats.toolMs));
547
558
  span.setAttribute('syndicate.llm_calls', stats.llmCalls);
548
559
  span.setAttribute('syndicate.models', [...stats.models].sort().join(','));
560
+ // The ledger prices the turn from this, per model (melchizedek_usage_cost, ADR 0136).
561
+ span.setAttribute('syndicate.usage', JSON.stringify(Object.fromEntries([...stats.usage].sort(([a], [b]) => a.localeCompare(b)))));
549
562
  }
550
563
  try {
551
564
  for (const [key, value] of Object.entries(metadata.onEnd?.() ?? {})) {
@@ -182,6 +182,12 @@ export interface AgentLoopContext extends Omit<ModelStepOptions, 'agent' | 'adap
182
182
  * its own delegations are filed below that path. Set by runChild only.
183
183
  */
184
184
  delegated?: boolean;
185
+ /**
186
+ * A delegated run's place in the turn: the agents from the caller at the
187
+ * top of the delegation down to this one, itself last (ADR 0135). Its own
188
+ * delegations extend it. Set by runChild only.
189
+ */
190
+ agentPath?: readonly string[];
185
191
  }
186
192
  /** How the run ended. */
187
193
  export type AgentLoopEndReason =
@@ -15,7 +15,8 @@
15
15
  * runner, unchanged, continues where the snapshot stood. runDurableTurn
16
16
  * is runSyndicateTurn with that store and a sink.
17
17
  *
18
- * A STEP BOUNDARY is an append that carries function responses after which
18
+ * A STEP BOUNDARY is an append that carries function responses, or a
19
+ * script's journal event (ADR 0137 item 8), after which
19
20
  * every function call any session of the run holds has its response (calls
20
21
  * listed in their event's longRunningToolIds, which wait on a person, aside).
21
22
  * So a parent waiting on a delegated subagent is never at a boundary, the
@@ -15,7 +15,8 @@
15
15
  * runner, unchanged, continues where the snapshot stood. runDurableTurn
16
16
  * is runSyndicateTurn with that store and a sink.
17
17
  *
18
- * A STEP BOUNDARY is an append that carries function responses after which
18
+ * A STEP BOUNDARY is an append that carries function responses, or a
19
+ * script's journal event (ADR 0137 item 8), after which
19
20
  * every function call any session of the run holds has its response (calls
20
21
  * listed in their event's longRunningToolIds, which wait on a person, aside).
21
22
  * So a parent waiting on a delegated subagent is never at a boundary, the
@@ -75,6 +76,7 @@
75
76
  */
76
77
  import { createHash, randomUUID } from 'node:crypto';
77
78
  import { getFunctionCalls, getFunctionResponses, parseTurnEvents } from '../events.js';
79
+ import { JOURNAL_EVERY_CALL, isJournalEvent } from '../../script/journal.js';
78
80
  import { InProcessSessionService } from '../sessions.js';
79
81
  import { runSyndicateTurn } from '../syndicateTurn.js';
80
82
  // ── Shapes ───────────────────────────────────────────────────────────────────
@@ -127,6 +129,8 @@ export function usableCheckpoint(cp, runId, parts) {
127
129
  /** Content compared as stored JSON. */
128
130
  const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
129
131
  class CheckpointingStore {
132
+ /** A script run stores each call's journal event as it ends, and each is a step boundary (ADR 0137 item 8). */
133
+ [JOURNAL_EVERY_CALL] = true;
130
134
  inner;
131
135
  opts;
132
136
  openingParts;
@@ -250,7 +254,7 @@ class CheckpointingStore {
250
254
  if (stored.author !== 'user' && this.firstAgentSeq === undefined)
251
255
  this.firstAgentSeq = order;
252
256
  this.track(stored);
253
- if (getFunctionResponses(stored).length > 0 && this.openCalls.size === 0)
257
+ if ((getFunctionResponses(stored).length > 0 || isJournalEvent(stored)) && this.openCalls.size === 0)
254
258
  await this.boundary();
255
259
  return stored;
256
260
  }
@@ -394,9 +394,31 @@ export function runSubagent(agent, args, context, scope) {
394
394
  export function resumeSubagent(agent, answer, context, scope) {
395
395
  return runChild(agent, { role: 'user', parts: answer }, context, scope, true);
396
396
  }
397
+ /**
398
+ * Where a delegated call's run sits (ADR 0135): the path below the caller's,
399
+ * `name` last, and the call's id. A top-level caller's path is its name.
400
+ */
401
+ function branchOf(name, context, scope) {
402
+ const path = [...(scope.ctx.agentPath ?? [scope.caller]), name];
403
+ return { agent: name, path, ...(context.functionCallId ? { callId: context.functionCallId } : {}) };
404
+ }
405
+ /** Hands one event to the turn's observer (ADR 0135). An observer that throws never breaks the run. */
406
+ function observe(sink, event, branch) {
407
+ if (!sink)
408
+ return;
409
+ try {
410
+ sink(event, branch);
411
+ }
412
+ catch {
413
+ /* the observer is the caller's; the turn goes on */
414
+ }
415
+ }
397
416
  async function runChild(agent, content, context, scope, resuming) {
398
417
  const { ctx, signal } = scope;
399
418
  const { sessions } = ctx;
419
+ // With an observer the child streams, so its thinking and text reach it as partials (ADR 0135); partials are stored nowhere.
420
+ const sink = currentTurnControl()?.agentEvents;
421
+ const branch = branchOf(agent.name, context, scope);
400
422
  const session = await childSession(agent.name, context, scope, resuming);
401
423
  if (signal?.aborted)
402
424
  return '';
@@ -409,9 +431,10 @@ async function runChild(agent, content, context, scope, resuming) {
409
431
  sessions,
410
432
  invocationId,
411
433
  userContent: content,
412
- stream: false,
434
+ stream: sink !== undefined,
413
435
  // Its own delegations are filed below its path (childAppName, ADR 0111).
414
436
  delegated: true,
437
+ agentPath: branch.path,
415
438
  // ADK's AgentTool builds its sub-runner without the reflect-and-retry
416
439
  // plugins, so a subagent's own errors are not retried (ADR 0075).
417
440
  selfCorrection: new SelfCorrection({ model_errors: 0, tool_errors: 0 }),
@@ -438,6 +461,9 @@ async function runChild(agent, content, context, scope, resuming) {
438
461
  await loop.return(undefined);
439
462
  break;
440
463
  }
464
+ observe(sink, next.value, branch);
465
+ if (next.value.partial)
466
+ continue;
441
467
  recordState(next.value, context);
442
468
  last = next.value;
443
469
  }
@@ -469,6 +495,8 @@ export function resumeWorkflowSubagent(workflow, answer, context, scope) {
469
495
  }
470
496
  async function walkChild(workflow, userParts, context, scope, resuming) {
471
497
  const { ctx, signal } = scope;
498
+ const sink = currentTurnControl()?.agentEvents;
499
+ const branch = branchOf(workflow.name, context, scope);
472
500
  const session = await childSession(workflow.name, context, scope, resuming);
473
501
  if (signal?.aborted)
474
502
  return '';
@@ -503,6 +531,7 @@ async function walkChild(workflow, userParts, context, scope, resuming) {
503
531
  break;
504
532
  }
505
533
  const event = next.value;
534
+ observe(sink, event, branch);
506
535
  if (event.partial)
507
536
  continue;
508
537
  recordState(event, context);
@@ -163,6 +163,7 @@ import { approvalRequestOf, pendingApproval } from '../approvals.js';
163
163
  import { consentFrom, pendingConsent } from '../credentials.js';
164
164
  import { getFunctionCalls, getFunctionResponses } from '../events.js';
165
165
  import { ASK_USER, pendingQuestion } from '../questions.js';
166
+ import { isJournalEvent, scriptPauseIn } from '../../script/journal.js';
166
167
  import { childAppName, entryAppName, legacyChild, legacyEntry } from './delegate.js';
167
168
  import { REQUEST_CONFIRMATION_CALL, REQUEST_CREDENTIAL_CALL, REQUEST_INPUT_CALL, isSegmentPrefix } from './history.js';
168
169
  import { isToolset } from './request.js';
@@ -516,6 +517,12 @@ async function conversationPause(key, child, name, id, depth, seen = new Set())
516
517
  ...(routed.consent ? { consent: { ...routed.consent, path } } : {}),
517
518
  };
518
519
  }
520
+ // A nested script syndicate's own pause (ADR 0137 item 15): its askUser or a gated tool() call, authored by the script.
521
+ const scripted = scriptPauseIn(child.events, id);
522
+ if (scripted) {
523
+ const path = scripted.asker === name ? [name] : down([scripted.asker]);
524
+ return { path, ...(scripted.approval ? { approval: { ...scripted.approval, path } } : {}), ...(scripted.question ? { question: { ...scripted.question, path } } : {}) };
525
+ }
519
526
  const approval = pendingApproval(child.events);
520
527
  if (approval && approval.agent && approval.agent !== 'user' && approval.agent !== name && wanted(approval.id)) {
521
528
  const path = down([approval.agent]);
@@ -543,8 +550,8 @@ async function conversationPause(key, child, name, id, depth, seen = new Set())
543
550
  ...(first.consent ? { consent: { ...first.consent, path } } : {}),
544
551
  };
545
552
  }
546
- /** A walk's session: its node events carry a dotted node path (`<workflow>.<node>`). */
547
- const isWalk = (session) => session.events.some((e) => typeof e.nodeInfo?.path === 'string' && e.nodeInfo.path.includes('.'));
553
+ /** A walk's session: its node events carry a dotted node path (`<workflow>.<node>`); a script's journal events do not count. */
554
+ const isWalk = (session) => session.events.some((e) => typeof e.nodeInfo?.path === 'string' && e.nodeInfo.path.includes('.') && !isJournalEvent(e));
548
555
  /**
549
556
  * The pause a nested workflow's walk holds in its child session (ADR 0111):
550
557
  * the workflow's own pause record, the last event, authored by the
@@ -18,7 +18,8 @@
18
18
  * feature (UnsupportedOnRuntimeError): a caller's `transformAgent` (it
19
19
  * transformed ADK agents, and ADK left in 1.0.0), and an `ask_user` tool on
20
20
  * a workflow's agent (the schema refuses it; a config that skipped
21
- * validation is refused here, ADR 0095). A workflow syndicate runs on the
21
+ * validation is refused here, ADR 0095), and a script or a `run_code`
22
+ * tool while MELCHIZEDEK_CODE_SANDBOX is off (CodeSandboxOffError, ADR 0137). A workflow syndicate runs on the
22
23
  * engine's scheduler (lib/workflow/turn.ts, ADR 0095). An answer to an
23
24
  * approval resumes the call (lib/runtime/native/interrupts.ts), an answer
24
25
  * to a question resumes the agent that asked (lib/runtime/questions.ts, ADR
@@ -18,7 +18,8 @@
18
18
  * feature (UnsupportedOnRuntimeError): a caller's `transformAgent` (it
19
19
  * transformed ADK agents, and ADK left in 1.0.0), and an `ask_user` tool on
20
20
  * a workflow's agent (the schema refuses it; a config that skipped
21
- * validation is refused here, ADR 0095). A workflow syndicate runs on the
21
+ * validation is refused here, ADR 0095), and a script or a `run_code`
22
+ * tool while MELCHIZEDEK_CODE_SANDBOX is off (CodeSandboxOffError, ADR 0137). A workflow syndicate runs on the
22
23
  * engine's scheduler (lib/workflow/turn.ts, ADR 0095). An answer to an
23
24
  * approval resumes the call (lib/runtime/native/interrupts.ts), an answer
24
25
  * to a question resumes the agent that asked (lib/runtime/questions.ts, ADR
@@ -30,6 +31,7 @@ import { createTurnEvent } from './events.js';
30
31
  import { runAgentLoop } from './native/agentLoop.js';
31
32
  import { ASK_USER } from './questions.js';
32
33
  import { unsupportedOnNative } from './runtimeFlag.js';
34
+ import { refuseWithoutSandbox } from '../sandbox/sandbox.js';
33
35
  // ── What native refuses before a turn starts ─────────────────────────────────
34
36
  /**
35
37
  * Throws UnsupportedOnRuntimeError for a syndicate or a call the runtime
@@ -39,6 +41,8 @@ export function refuseOnNative(config, call) {
39
41
  const where = config.syndicate_name || config.orchestrator?.name || 'syndicate';
40
42
  if (call.transformAgent)
41
43
  throw unsupportedOnNative('transformAgent (it transformed ADK agents; the ADK runtime was removed in 1.0.0)', where);
44
+ // A script, or an agent listing run_code, runs code: only once the operator turned the sandbox on (ADR 0137 item 4).
45
+ refuseWithoutSandbox(config);
42
46
  if (call.isWorkflow) {
43
47
  // A pause inside an agent node cannot be resumed by the native walk (lib/workflow/resume.ts).
44
48
  const asking = [config.orchestrator, ...(config.subagents ?? [])].find((agent) => (agent?.tools ?? []).includes(ASK_USER));
@@ -64,7 +64,10 @@ export { ASK_USER, pendingQuestion, questionAnswerPart } from './questions.ts';
64
64
  import type { PendingApproval } from './approvals.ts';
65
65
  export { approvalResponsePart, describeApproval, pendingApproval } from './approvals.ts';
66
66
  export type { PendingApproval } from './approvals.ts';
67
- import type { TurnStopReason } from './turnControl.ts';
67
+ import type { CodeRun } from '../tools/runCodeTool.ts';
68
+ export type { CodeRun } from '../tools/runCodeTool.ts';
69
+ import type { AgentBranch, TurnStopReason } from './turnControl.ts';
70
+ export type { AgentBranch } from './turnControl.ts';
68
71
  /** One part of the user's message. Text is the common case; the genai part
69
72
  * shapes (inlineData, fileData) pass through to the model unchanged. */
70
73
  export type MessagePart = {
@@ -86,6 +89,18 @@ export interface TurnEvents {
86
89
  onTextDelta?(delta: string): void;
87
90
  /** The text streamed so far was narration before a tool call: discard it. */
88
91
  onTextReset?(): void;
92
+ /**
93
+ * Every event of a delegated run, live, with where it runs (ADR 0135): a
94
+ * subagent's thinking and text as partial events, then each event it
95
+ * stores (its tool calls, their results, its answer), and a nested
96
+ * workflow's events; at any depth, parallel calls interleaved and told
97
+ * apart by `branch.callId` and `branch.path`. The answering agent's
98
+ * events stay on onEvent and onTextDelta, which this does not change.
99
+ * Passing it makes each delegated run stream; what the sessions store is
100
+ * the same. Fires for a syndicate with guards too: a delegated run's text
101
+ * is never the answer a guard rewrites. An observer that throws is ignored.
102
+ */
103
+ onAgentEvent?(event: TurnEvent, branch: AgentBranch): void;
89
104
  /** Diagnostic lines (the server prefixes and prints them). */
90
105
  log?(message: string): void;
91
106
  warn?(message: string): void;
@@ -209,8 +224,10 @@ export interface DrainedRun {
209
224
  code: string;
210
225
  message: string;
211
226
  }>;
227
+ /** Every run_code call that got its result, with the agent that made it (ADR 0137). */
228
+ codeRuns: CodeRun[];
212
229
  }
213
- export type TurnStage = 'classify' | 'dispatch' | 'delegate' | 'workflow';
230
+ export type TurnStage = 'classify' | 'dispatch' | 'delegate' | 'workflow' | 'script';
214
231
  export interface RouteDecision extends RouteResolution {
215
232
  /** `approval`: the turn resumed the route that asked for an approval (ADR 0028).
216
233
  * `answer`: the message answered the route's open `ask_user` question.
@@ -259,6 +276,12 @@ export interface SyndicateTurnResult {
259
276
  usage: TurnUsage;
260
277
  /** Set when a control (cancel, deadline, step limit) stopped the turn. */
261
278
  stopReason?: TurnStopReason;
279
+ /**
280
+ * The run_code runs of agents whose YAML sets `run_code: { attach: true }`
281
+ * (ADR 0137 item 3), in order: the reply's `code_run` data parts. Absent
282
+ * when there are none.
283
+ */
284
+ codeRuns?: CodeRun[];
262
285
  }
263
286
  /** A turn's spend: model calls and the tokens the providers reported. */
264
287
  export interface TurnUsage {