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.
- package/CHANGELOG.md +115 -0
- package/DOCUMENTATION.md +28 -2
- package/README.md +1 -1
- package/config/agents/syndicate.schema.json +63 -0
- package/config/agents/syndicateSchema.yaml +50 -0
- package/db/hardening.sql +5 -2
- package/db/migrations/0015_model_pricing.sql +107 -0
- package/db/telemetry.sql +79 -1
- package/dist/lib/a2a/executor.js +21 -0
- package/dist/lib/compile.d.ts +4 -3
- package/dist/lib/compile.js +8 -3
- package/dist/lib/index.d.ts +9 -1
- package/dist/lib/index.js +5 -0
- package/dist/lib/loadSyndicate.d.ts +13 -0
- package/dist/lib/loadSyndicate.js +11 -1
- package/dist/lib/observability/supabaseSpanExporter.js +2 -1
- package/dist/lib/observability/tracer.d.ts +11 -2
- package/dist/lib/observability/tracer.js +17 -4
- package/dist/lib/runtime/native/agentLoop.d.ts +6 -0
- package/dist/lib/runtime/native/checkpoint.d.ts +2 -1
- package/dist/lib/runtime/native/checkpoint.js +6 -2
- package/dist/lib/runtime/native/delegate.js +30 -1
- package/dist/lib/runtime/native/interrupts.js +9 -2
- package/dist/lib/runtime/nativeTurn.d.ts +2 -1
- package/dist/lib/runtime/nativeTurn.js +5 -1
- package/dist/lib/runtime/syndicateTurn.d.ts +25 -2
- package/dist/lib/runtime/syndicateTurn.js +387 -5
- package/dist/lib/runtime/turnControl.d.ts +27 -0
- package/dist/lib/sandbox/sandbox.d.ts +121 -0
- package/dist/lib/sandbox/sandbox.js +276 -0
- package/dist/lib/sandbox/worker.d.ts +29 -0
- package/dist/lib/sandbox/worker.js +280 -0
- package/dist/lib/script/config.d.ts +35 -0
- package/dist/lib/script/config.js +17 -0
- package/dist/lib/script/journal.d.ts +137 -0
- package/dist/lib/script/journal.js +193 -0
- package/dist/lib/script/run.d.ts +138 -0
- package/dist/lib/script/run.js +364 -0
- package/dist/lib/syndicateSchema.d.ts +20 -0
- package/dist/lib/syndicateSchema.js +98 -6
- package/dist/lib/toolRegistry.js +4 -0
- package/dist/lib/tools/runCodeTool.d.ts +50 -0
- package/dist/lib/tools/runCodeTool.js +59 -0
- package/package.json +5 -1
package/dist/lib/compile.js
CHANGED
|
@@ -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
|
-
|
|
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).
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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)
|
|
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)
|
|
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 {
|
|
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 {
|