@tangle-network/agent-runtime 0.164.0 → 0.166.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/dist/{activation-CneAm4jL.js → activation-BkO0p4I1.js} +3 -3
- package/dist/{activation-CneAm4jL.js.map → activation-BkO0p4I1.js.map} +1 -1
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +2 -2
- package/dist/{authoring-CZM22nUG.js → authoring-Bt17KOML.js} +2 -2
- package/dist/{authoring-CZM22nUG.js.map → authoring-Bt17KOML.js.map} +1 -1
- package/dist/candidate-execution/index.js +4 -4
- package/dist/{candidate-execution-DNLk6h0A.js → candidate-execution-DC0T8l2p.js} +4 -4
- package/dist/{candidate-execution-DNLk6h0A.js.map → candidate-execution-DC0T8l2p.js.map} +1 -1
- package/dist/conversation.d.ts +1 -1
- package/dist/durable.d.ts +2 -2
- package/dist/durable.js +1 -1
- package/dist/{environment-provider-BXFFK_rZ.d.ts → environment-provider-DWjbGH8W.d.ts} +2 -2
- package/dist/environment-provider.d.ts +1 -1
- package/dist/graph-E0U0c2YP.js +2668 -0
- package/dist/graph-E0U0c2YP.js.map +1 -0
- package/dist/graph.d.ts +319 -53
- package/dist/graph.js +2 -1203
- package/dist/{improvement-cycle-a_L8FFql.js → improvement-cycle-BAWOyeTA.js} +4 -4
- package/dist/{improvement-cycle-a_L8FFql.js.map → improvement-cycle-BAWOyeTA.js.map} +1 -1
- package/dist/{index-BP11HXYi.d.ts → index-BGQtpQuJ.d.ts} +5 -5
- package/dist/{index-C2X-OVPZ.d.ts → index-BgptUCKl.d.ts} +4 -4
- package/dist/{index-OFOvnNc2.d.ts → index-CFMB9ux3.d.ts} +2 -2
- package/dist/index.d.ts +7 -7
- package/dist/index.js +11 -11
- package/dist/intelligence.d.ts +4 -4
- package/dist/intelligence.js +5 -5
- package/dist/kernel.d.ts +5 -5
- package/dist/kernel.js +7 -7
- package/dist/{knowledge-Ccdghy81.js → knowledge-Bhhz6lvH.js} +5 -5
- package/dist/{knowledge-Ccdghy81.js.map → knowledge-Bhhz6lvH.js.map} +1 -1
- package/dist/knowledge.d.ts +1 -1
- package/dist/knowledge.js +1 -1
- package/dist/{loop-runner-bin-DM-AnPC-.js → loop-runner-bin-BQnpSn0h.js} +3 -3
- package/dist/{loop-runner-bin-DM-AnPC-.js.map → loop-runner-bin-BQnpSn0h.js.map} +1 -1
- package/dist/{loop-runner-bin-2NITRIFc.d.ts → loop-runner-bin-C2NXQK7J.d.ts} +3 -3
- package/dist/loop-runner-bin.d.ts +1 -1
- package/dist/loop-runner-bin.js +1 -1
- package/dist/mcp/bin.js +3 -3
- package/dist/mcp/index.d.ts +3 -3
- package/dist/mcp/index.js +4 -4
- package/dist/{openai-tools-CIo9tB75.js → openai-tools-t6spVYKL.js} +2 -2
- package/dist/{openai-tools-CIo9tB75.js.map → openai-tools-t6spVYKL.js.map} +1 -1
- package/dist/{prepare-D7Fb75XR.js → prepare-BCScSDCv.js} +2 -2
- package/dist/{prepare-D7Fb75XR.js.map → prepare-BCScSDCv.js.map} +1 -1
- package/dist/primeintellect/index.d.ts +1 -1
- package/dist/{protected-model-port-RZkxRZhp.js → protected-model-port-D-5E3HBe.js} +2 -2
- package/dist/{protected-model-port-RZkxRZhp.js.map → protected-model-port-D-5E3HBe.js.map} +1 -1
- package/dist/{redact-D4vlt6p4.d.ts → redact-DkObldQP.d.ts} +3 -3
- package/dist/{runtime-D44YrQ4t.d.ts → runtime-B0HZbuEv.d.ts} +3 -3
- package/dist/{runtime-BDHsMgCK.js → runtime-BJoNta1Y.js} +7 -7
- package/dist/{runtime-BDHsMgCK.js.map → runtime-BJoNta1Y.js.map} +1 -1
- package/dist/{spawn-journal-oewq1g2h.js → spawn-journal-Tgvp5VS5.js} +8 -2
- package/dist/{spawn-journal-oewq1g2h.js.map → spawn-journal-Tgvp5VS5.js.map} +1 -1
- package/dist/{stream-agent-turn-Dx2YrEWW.d.ts → stream-agent-turn-DxQ3GOuR.d.ts} +2 -2
- package/dist/{structural-rollout-CBK2LHni.js → structural-rollout-B5E0mWCH.js} +3 -3
- package/dist/{structural-rollout-CBK2LHni.js.map → structural-rollout-B5E0mWCH.js.map} +1 -1
- package/dist/{supervise-CzllE0vn.js → supervise-CnxqHx1P.js} +3 -3
- package/dist/{supervise-CzllE0vn.js.map → supervise-CnxqHx1P.js.map} +1 -1
- package/dist/{supervisor-D5K8ihKQ.js → supervisor-C8kgh8mf.js} +29 -6
- package/dist/{supervisor-D5K8ihKQ.js.map → supervisor-C8kgh8mf.js.map} +1 -1
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +12 -12
- package/dist/{types-C2l1jaos.d.ts → types-DjVO-p7R.d.ts} +58 -3
- package/dist/{workspace-archive-DK0mDjSB.js → workspace-archive-CHY5HvrF.js} +2 -2
- package/dist/{workspace-archive-DK0mDjSB.js.map → workspace-archive-CHY5HvrF.js.map} +1 -1
- package/package.json +1 -1
- package/dist/graph-DzE4SwfP.js +0 -532
- package/dist/graph-DzE4SwfP.js.map +0 -1
- package/dist/graph.js.map +0 -1
|
@@ -0,0 +1,2668 @@
|
|
|
1
|
+
import { m as ValidationError } from "./errors-CDZ8XsVj.js";
|
|
2
|
+
import { d as replaySpawnTree, i as InMemorySpawnJournal, l as materializeTreeView, o as closesCursorSlot, r as InMemoryResultBlobStore, u as pendingWaits, x as contentAddress } from "./spawn-journal-Tgvp5VS5.js";
|
|
3
|
+
import { t as addSpend } from "./util-D6ZEuBMi.js";
|
|
4
|
+
import { f as harnessRunsAgent } from "./model-policy-Sw4ywhtL.js";
|
|
5
|
+
import { N as createBudgetPool, U as createExecutorRegistry, a as uncertainSpawnBudgets, i as sumMeasuredSpendFromEvents, l as runFinalizer, o as bestDelivered, p as createScope, r as maxSeqOf, s as collectDelivered } from "./supervisor-C8kgh8mf.js";
|
|
6
|
+
import { t as composeRuntimeHooks } from "./runtime-hooks-tXpAarhW.js";
|
|
7
|
+
import { h as kernelPromptRegistry, i as workerFromBackend, m as formatPromptHandle, n as supervise, r as superviseWithTestBrain, s as supervisorAgent } from "./supervise-CnxqHx1P.js";
|
|
8
|
+
import { agentProfileSchema, canonicalCandidateDigest } from "@tangle-network/agent-interface";
|
|
9
|
+
//#region src/runtime/graph/condition.ts
|
|
10
|
+
/**
|
|
11
|
+
* The ONE predicate tree: every guard in a graph — an edge `guard`, a filter projection's
|
|
12
|
+
* predicate — is this shape, evaluated by this evaluator, so `contains`, ordering and equality can
|
|
13
|
+
* never mean two different things on two surfaces (adopted from ADC's `workflow-conditions`,
|
|
14
|
+
* agent-runtime#968). A declarative tree keeps an untrusted predicate incapable of code execution;
|
|
15
|
+
* the size and depth bounds keep a predicate authored once from burning CPU on every settle.
|
|
16
|
+
*
|
|
17
|
+
* No zod: the kernel's grain is a hand-written validator that throws `ValidationError` by name.
|
|
18
|
+
*/
|
|
19
|
+
/** The leaf comparison operators; `exists`/`truthy` are unary, the rest compare against `value`. */
|
|
20
|
+
const CONDITION_OPS = [
|
|
21
|
+
"eq",
|
|
22
|
+
"neq",
|
|
23
|
+
"gt",
|
|
24
|
+
"gte",
|
|
25
|
+
"lt",
|
|
26
|
+
"lte",
|
|
27
|
+
"in",
|
|
28
|
+
"contains",
|
|
29
|
+
"exists",
|
|
30
|
+
"truthy"
|
|
31
|
+
];
|
|
32
|
+
/** Operators that compare against `value`; `exists`/`truthy` are unary and must omit it. */
|
|
33
|
+
const OPS_NEEDING_VALUE = new Set(CONDITION_OPS.filter((op) => op !== "exists" && op !== "truthy"));
|
|
34
|
+
const MAX_PATH_LENGTH = 256;
|
|
35
|
+
const MAX_PATH_SEGMENTS = 16;
|
|
36
|
+
const PATH_SEGMENT = /^[A-Za-z_$][A-Za-z0-9_$-]*$/u;
|
|
37
|
+
/** Parse `a.b[0].c` into steps; refuse anything outside the bounded grammar. */
|
|
38
|
+
function parseConditionPath(path, context) {
|
|
39
|
+
if (typeof path !== "string" || path.length === 0 || path.length > MAX_PATH_LENGTH) throw new ValidationError(`${context}: path must be a non-empty string of at most ${MAX_PATH_LENGTH} chars`);
|
|
40
|
+
const steps = [];
|
|
41
|
+
for (const part of path.split(".")) {
|
|
42
|
+
const open = part.indexOf("[");
|
|
43
|
+
const head = open === -1 ? part : part.slice(0, open);
|
|
44
|
+
if (head.length > 0) {
|
|
45
|
+
if (!PATH_SEGMENT.test(head)) throw new ValidationError(`${context}: path segment ${JSON.stringify(head)} is not addressable`);
|
|
46
|
+
steps.push(head);
|
|
47
|
+
} else if (open !== 0 || steps.length === 0) throw new ValidationError(`${context}: path ${JSON.stringify(path)} has an empty segment`);
|
|
48
|
+
let rest = open === -1 ? "" : part.slice(open);
|
|
49
|
+
while (rest.length > 0) {
|
|
50
|
+
const match = /^\[(\d{1,6})\]/u.exec(rest);
|
|
51
|
+
if (!match) throw new ValidationError(`${context}: path index in ${JSON.stringify(part)} must be [N]`);
|
|
52
|
+
steps.push(Number(match[1]));
|
|
53
|
+
rest = rest.slice(match[0].length);
|
|
54
|
+
}
|
|
55
|
+
if (steps.length > MAX_PATH_SEGMENTS) throw new ValidationError(`${context}: path exceeds ${MAX_PATH_SEGMENTS} segments`);
|
|
56
|
+
}
|
|
57
|
+
if (steps.length === 0) throw new ValidationError(`${context}: path resolves no segment`);
|
|
58
|
+
return steps;
|
|
59
|
+
}
|
|
60
|
+
/** Walk a parsed path; any miss resolves `undefined`, never a throw — absence is an answer. */
|
|
61
|
+
function resolveConditionPath(context, steps) {
|
|
62
|
+
let current = context;
|
|
63
|
+
for (const step of steps) {
|
|
64
|
+
if (current === null || current === void 0) return void 0;
|
|
65
|
+
if (typeof step === "number") {
|
|
66
|
+
if (!Array.isArray(current)) return void 0;
|
|
67
|
+
current = current[step];
|
|
68
|
+
} else {
|
|
69
|
+
if (typeof current !== "object" || Array.isArray(current)) return void 0;
|
|
70
|
+
current = current[step];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return current;
|
|
74
|
+
}
|
|
75
|
+
function isLeaf(condition) {
|
|
76
|
+
return typeof condition.path === "string";
|
|
77
|
+
}
|
|
78
|
+
/** Validate shape, bounds, and per-leaf path/operator rules; returns the input for chaining. */
|
|
79
|
+
function validateCondition(raw, context) {
|
|
80
|
+
let nodes = 0;
|
|
81
|
+
const walk = (value, depth) => {
|
|
82
|
+
nodes += 1;
|
|
83
|
+
if (nodes > 40) throw new ValidationError(`${context}: condition exceeds 40 nodes`);
|
|
84
|
+
if (depth > 6) throw new ValidationError(`${context}: condition exceeds depth 6`);
|
|
85
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new ValidationError(`${context}: a condition must be an object`);
|
|
86
|
+
const record = value;
|
|
87
|
+
const combinators = [
|
|
88
|
+
"all",
|
|
89
|
+
"any",
|
|
90
|
+
"not"
|
|
91
|
+
].filter((key) => record[key] !== void 0);
|
|
92
|
+
if (combinators.length > 1) throw new ValidationError(`${context}: a condition carries ONE of all/any/not, got ${combinators.join("+")}`);
|
|
93
|
+
if (record.all !== void 0 || record.any !== void 0) {
|
|
94
|
+
const key = record.all !== void 0 ? "all" : "any";
|
|
95
|
+
const branch = record[key];
|
|
96
|
+
if (!Array.isArray(branch) || branch.length === 0) throw new ValidationError(`${context}: ${key} must be a non-empty array`);
|
|
97
|
+
for (const child of branch) walk(child, depth + 1);
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
if (record.not !== void 0) {
|
|
101
|
+
walk(record.not, depth + 1);
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
const op = record.op;
|
|
105
|
+
if (typeof record.path !== "string" || typeof op !== "string") throw new ValidationError(`${context}: a leaf needs { path, op }`);
|
|
106
|
+
if (!CONDITION_OPS.includes(op)) throw new ValidationError(`${context}: unknown op ${JSON.stringify(op)}; known: ${CONDITION_OPS.join(", ")}`);
|
|
107
|
+
parseConditionPath(record.path, context);
|
|
108
|
+
const needsValue = OPS_NEEDING_VALUE.has(op);
|
|
109
|
+
if (needsValue && !("value" in record)) throw new ValidationError(`${context}: op ${JSON.stringify(op)} requires a value`);
|
|
110
|
+
if (!needsValue && "value" in record) throw new ValidationError(`${context}: op ${JSON.stringify(op)} is unary — remove value`);
|
|
111
|
+
if (op === "in" && !Array.isArray(record.value)) throw new ValidationError(`${context}: op "in" takes an array value`);
|
|
112
|
+
return value;
|
|
113
|
+
};
|
|
114
|
+
return walk(raw, 1);
|
|
115
|
+
}
|
|
116
|
+
function canonicalEquals(a, b) {
|
|
117
|
+
if (Object.is(a, b)) return true;
|
|
118
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
119
|
+
try {
|
|
120
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function ordering(op, left, right) {
|
|
126
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
127
|
+
if (Number.isNaN(left) || Number.isNaN(right)) return false;
|
|
128
|
+
if (op === "gt") return left > right;
|
|
129
|
+
if (op === "gte") return left >= right;
|
|
130
|
+
if (op === "lt") return left < right;
|
|
131
|
+
return left <= right;
|
|
132
|
+
}
|
|
133
|
+
if (typeof left === "string" && typeof right === "string") {
|
|
134
|
+
if (op === "gt") return left > right;
|
|
135
|
+
if (op === "gte") return left >= right;
|
|
136
|
+
if (op === "lt") return left < right;
|
|
137
|
+
return left <= right;
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
/** Walk a validated condition over a context to a boolean. Never throws on data shape. */
|
|
142
|
+
function evaluateCondition(condition, context) {
|
|
143
|
+
if (isLeaf(condition)) {
|
|
144
|
+
const resolved = resolveConditionPath(context, parseConditionPath(condition.path, "evaluate"));
|
|
145
|
+
switch (condition.op) {
|
|
146
|
+
case "exists": return resolved !== void 0 && resolved !== null;
|
|
147
|
+
case "truthy": return Boolean(resolved);
|
|
148
|
+
case "eq": return canonicalEquals(resolved, condition.value);
|
|
149
|
+
case "neq": return !canonicalEquals(resolved, condition.value);
|
|
150
|
+
case "in": return Array.isArray(condition.value) ? condition.value.some((candidate) => canonicalEquals(resolved, candidate)) : false;
|
|
151
|
+
case "contains":
|
|
152
|
+
if (Array.isArray(resolved)) return resolved.some((element) => canonicalEquals(element, condition.value));
|
|
153
|
+
return typeof resolved === "string" && typeof condition.value === "string" ? resolved.includes(condition.value) : false;
|
|
154
|
+
default: return ordering(condition.op, resolved, condition.value);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if ("all" in condition) return condition.all.every((child) => evaluateCondition(child, context));
|
|
158
|
+
if ("any" in condition) return condition.any.some((child) => evaluateCondition(child, context));
|
|
159
|
+
return !evaluateCondition(condition.not, context);
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/runtime/graph/projection.ts
|
|
163
|
+
/**
|
|
164
|
+
* The ONE pure projection a `data` edge may carry (agent-runtime#971): the bounded,
|
|
165
|
+
* schema-preserving subset of ADC's collection helpers. Anything richer is a `script` NODE, so it
|
|
166
|
+
* is journaled (`inputRef` → `outRef`), typed, and visible. Exactly one operator per projection.
|
|
167
|
+
*/
|
|
168
|
+
const PROJECTION_KEYS = [
|
|
169
|
+
"path",
|
|
170
|
+
"pick",
|
|
171
|
+
"map",
|
|
172
|
+
"filter",
|
|
173
|
+
"first",
|
|
174
|
+
"last",
|
|
175
|
+
"count"
|
|
176
|
+
];
|
|
177
|
+
/** Validate a projection: exactly one known operator, its argument well-formed. */
|
|
178
|
+
function validateProjection(raw, context) {
|
|
179
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new ValidationError(`${context}: a projection must be an object`);
|
|
180
|
+
const record = raw;
|
|
181
|
+
const keys = PROJECTION_KEYS.filter((key) => record[key] !== void 0);
|
|
182
|
+
const unknown = Object.keys(record).filter((key) => !PROJECTION_KEYS.includes(key));
|
|
183
|
+
if (unknown.length > 0) throw new ValidationError(`${context}: unknown projection key(s) ${unknown.join(", ")}; known: ${PROJECTION_KEYS.join(", ")}`);
|
|
184
|
+
if (keys.length !== 1) throw new ValidationError(`${context}: a projection carries exactly ONE of ${PROJECTION_KEYS.join("/")}`);
|
|
185
|
+
const key = keys[0];
|
|
186
|
+
if (key === "path" || key === "map") parseConditionPath(record[key], context);
|
|
187
|
+
if (key === "pick") {
|
|
188
|
+
const fields = record.pick;
|
|
189
|
+
if (!Array.isArray(fields) || fields.length === 0 || fields.some((f) => typeof f !== "string")) throw new ValidationError(`${context}: pick must be a non-empty array of field names`);
|
|
190
|
+
}
|
|
191
|
+
if (key === "filter") validateCondition(record.filter, context);
|
|
192
|
+
if ((key === "first" || key === "last" || key === "count") && record[key] !== true) throw new ValidationError(`${context}: ${key} must be literally true`);
|
|
193
|
+
return raw;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Apply a validated projection to an admitted payload. Collection operators over a non-array
|
|
197
|
+
* refuse by name — a shape the author did not expect is a graph defect, not an empty result.
|
|
198
|
+
*/
|
|
199
|
+
function applyProjection(value, projection, context) {
|
|
200
|
+
if ("path" in projection) return resolveConditionPath(value, parseConditionPath(projection.path, context));
|
|
201
|
+
if ("pick" in projection) {
|
|
202
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new ValidationError(`${context}: pick needs an object payload`);
|
|
203
|
+
const record = value;
|
|
204
|
+
const out = {};
|
|
205
|
+
for (const field of projection.pick) if (field in record) out[field] = record[field];
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
const collection = value;
|
|
209
|
+
if (!Array.isArray(collection)) throw new ValidationError(`${context}: this projection needs an array payload`);
|
|
210
|
+
if ("map" in projection) {
|
|
211
|
+
const steps = parseConditionPath(projection.map, context);
|
|
212
|
+
return collection.map((element) => resolveConditionPath(element, steps));
|
|
213
|
+
}
|
|
214
|
+
if ("filter" in projection) return collection.filter((element) => evaluateCondition(projection.filter, element));
|
|
215
|
+
if ("first" in projection) return collection[0];
|
|
216
|
+
if ("last" in projection) return collection[collection.length - 1];
|
|
217
|
+
return collection.length;
|
|
218
|
+
}
|
|
219
|
+
//#endregion
|
|
220
|
+
//#region src/runtime/graph/registry.ts
|
|
221
|
+
/**
|
|
222
|
+
* `Registry<T>` — the ONE name→thing shape for the graph engine.
|
|
223
|
+
*
|
|
224
|
+
* The kernel grew fourteen of these (agent-runtime#978) that differ in three properties:
|
|
225
|
+
* whether names can be listed, what a miss does, and whether the table is global. This one
|
|
226
|
+
* fixes all three — enumerable, a miss is refused BY NAME listing what is registered, and every
|
|
227
|
+
* registry is per-instance — and it is lifted from `AgentEnvironmentProviderRegistry`, the
|
|
228
|
+
* richest and best-tested of the fourteen, not invented.
|
|
229
|
+
*
|
|
230
|
+
* Entries are addressed by a versioned handle, `<id>/v<n>`, the same way the prompt registry
|
|
231
|
+
* addresses directives. A graph names a kind by handle; a host registers exact versions; a
|
|
232
|
+
* missing version is refused, never served by a newer one.
|
|
233
|
+
*/
|
|
234
|
+
/** `<id>/v<n>` — the only spelling a handle has on the wire, in a journal, or in an error. */
|
|
235
|
+
function formatRegistryHandle(handle) {
|
|
236
|
+
return `${handle.id}/v${handle.version}`;
|
|
237
|
+
}
|
|
238
|
+
/** Parse the wire spelling back. Refuses anything that is not exactly `<id>/v<n>`. */
|
|
239
|
+
function parseRegistryHandle(text, context) {
|
|
240
|
+
const match = /^([A-Za-z0-9][A-Za-z0-9._-]*)\/v(\d+)$/u.exec(text);
|
|
241
|
+
if (!match) throw new ValidationError(`${context}: ${JSON.stringify(text)} is not a registry handle; expected "<id>/v<n>"`);
|
|
242
|
+
const version = Number(match[2]);
|
|
243
|
+
if (!Number.isSafeInteger(version) || version < 1) throw new ValidationError(`${context}: handle version must be a positive integer`);
|
|
244
|
+
return {
|
|
245
|
+
id: match[1],
|
|
246
|
+
version
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Create a registry. Per-instance by construction: two engines in one process may hold
|
|
251
|
+
* different kind sets, a test is hermetic, and a run can print its own table. There is
|
|
252
|
+
* deliberately no module-level singleton — `builtinShapes` was the one mutable global in the
|
|
253
|
+
* kernel and it had zero tests.
|
|
254
|
+
*/
|
|
255
|
+
function createRegistry(label, seed = []) {
|
|
256
|
+
const table = /* @__PURE__ */ new Map();
|
|
257
|
+
const registry = {
|
|
258
|
+
register(entry, options = {}) {
|
|
259
|
+
if (typeof entry.id !== "string" || entry.id.length === 0) throw new ValidationError(`${label}: an entry must carry a non-empty id`);
|
|
260
|
+
if (!Number.isSafeInteger(entry.version) || entry.version < 1) throw new ValidationError(`${label}: ${JSON.stringify(entry.id)} must carry a positive integer version`);
|
|
261
|
+
const key = formatRegistryHandle(entry);
|
|
262
|
+
if (!options.replace && table.has(key)) throw new ValidationError(`${label}: ${JSON.stringify(key)} is already registered`);
|
|
263
|
+
table.set(key, entry);
|
|
264
|
+
},
|
|
265
|
+
has(handle) {
|
|
266
|
+
return table.has(formatRegistryHandle(handle));
|
|
267
|
+
},
|
|
268
|
+
get(handle) {
|
|
269
|
+
return table.get(formatRegistryHandle(handle));
|
|
270
|
+
},
|
|
271
|
+
require(handle, context = label) {
|
|
272
|
+
const key = formatRegistryHandle(handle);
|
|
273
|
+
const entry = table.get(key);
|
|
274
|
+
if (entry === void 0) {
|
|
275
|
+
const known = registry.names();
|
|
276
|
+
const suffix = known.length > 0 ? `; registered: ${known.join(", ")}` : "; nothing is registered";
|
|
277
|
+
throw new ValidationError(`${context}: ${JSON.stringify(key)} is not registered${suffix}`);
|
|
278
|
+
}
|
|
279
|
+
return entry;
|
|
280
|
+
},
|
|
281
|
+
names() {
|
|
282
|
+
return Array.from(table.keys()).sort();
|
|
283
|
+
},
|
|
284
|
+
entries() {
|
|
285
|
+
return registry.names().map((key) => table.get(key));
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
for (const entry of seed) registry.register(entry);
|
|
289
|
+
return registry;
|
|
290
|
+
}
|
|
291
|
+
//#endregion
|
|
292
|
+
//#region src/runtime/graph/definition.ts
|
|
293
|
+
/** Which gating-edge outcomes release a node (adopted from ADC, agent-runtime#968). */
|
|
294
|
+
const JOIN_RULES = [
|
|
295
|
+
"all",
|
|
296
|
+
"any",
|
|
297
|
+
"any_failed",
|
|
298
|
+
"all_done"
|
|
299
|
+
];
|
|
300
|
+
/** ADC-compatible visit backstop: nothing may be ENTERED more than this many times. */
|
|
301
|
+
const DEFAULT_MAX_NODE_VISITS = 25;
|
|
302
|
+
/** The hard ceiling an author's `maxVisits`/`maxNodeVisits` override may reach. */
|
|
303
|
+
const MAX_MAX_NODE_VISITS = 100;
|
|
304
|
+
/** Structural validation only — everything a registry is not needed for. */
|
|
305
|
+
function validateEngineGraphSpec(spec, context = "compileGraph") {
|
|
306
|
+
if (!Array.isArray(spec.nodes) || spec.nodes.length === 0) throw new ValidationError(`${context}: a graph needs at least one node`);
|
|
307
|
+
const ids = /* @__PURE__ */ new Set();
|
|
308
|
+
for (const node of spec.nodes) {
|
|
309
|
+
if (typeof node.id !== "string" || node.id.length === 0) throw new ValidationError(`${context}: every node needs a non-empty id`);
|
|
310
|
+
if (ids.has(node.id)) throw new ValidationError(`${context}: duplicate node id ${JSON.stringify(node.id)}`);
|
|
311
|
+
ids.add(node.id);
|
|
312
|
+
parseRegistryHandle(node.kind, `${context}: node ${node.id} kind`);
|
|
313
|
+
if (node.join !== void 0 && !JOIN_RULES.includes(node.join)) throw new ValidationError(`${context}: node ${node.id} join ${JSON.stringify(node.join)}; known: ${JOIN_RULES.join(", ")}`);
|
|
314
|
+
if (node.maxVisits !== void 0) {
|
|
315
|
+
if (!Number.isSafeInteger(node.maxVisits) || node.maxVisits < 1 || node.maxVisits > 100) throw new ValidationError(`${context}: node ${node.id} maxVisits must be an integer in [1, 100]`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (!Array.isArray(spec.edges)) throw new ValidationError(`${context}: edges must be an array`);
|
|
319
|
+
for (const [index, edge] of spec.edges.entries()) {
|
|
320
|
+
const who = `${context}: edge[${index}]`;
|
|
321
|
+
if (edge.kind !== "delegates" && edge.kind !== "analyzes" && edge.kind !== "data") throw new ValidationError(`${who}: kind must be delegates | analyzes | data`);
|
|
322
|
+
for (const end of ["from", "to"]) {
|
|
323
|
+
const ref = edge[end];
|
|
324
|
+
if (typeof ref !== "object" || ref === null || typeof ref.node !== "string" || !ids.has(ref.node)) throw new ValidationError(`${who}: ${end}.node must name a node in this graph`);
|
|
325
|
+
}
|
|
326
|
+
if (edge.guard !== void 0) validateCondition(edge.guard, `${who} guard`);
|
|
327
|
+
if (edge.projection !== void 0) {
|
|
328
|
+
if (edge.kind !== "data") throw new ValidationError(`${who}: only a data edge carries a projection`);
|
|
329
|
+
validateProjection(edge.projection, `${who} projection`);
|
|
330
|
+
}
|
|
331
|
+
if (edge.maxTraversals !== void 0) {
|
|
332
|
+
if (!Number.isSafeInteger(edge.maxTraversals) || edge.maxTraversals < 0) throw new ValidationError(`${who}: maxTraversals must be a non-negative integer`);
|
|
333
|
+
}
|
|
334
|
+
if (edge.kind === "data" && edge.directive !== void 0) throw new ValidationError(`${who}: a data edge carries a port binding, never a directive`);
|
|
335
|
+
}
|
|
336
|
+
if (spec.root !== void 0 && !ids.has(spec.root)) throw new ValidationError(`${context}: root ${JSON.stringify(spec.root)} names no node`);
|
|
337
|
+
if (spec.maxNodeVisits !== void 0) {
|
|
338
|
+
if (!Number.isSafeInteger(spec.maxNodeVisits) || spec.maxNodeVisits < 1 || spec.maxNodeVisits > 100) throw new ValidationError(`${context}: maxNodeVisits must be an integer in [1, 100]`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
//#endregion
|
|
342
|
+
//#region src/runtime/graph/compile.ts
|
|
343
|
+
/**
|
|
344
|
+
* Compile an authored graph against an engine's kind registry into the schedulable form. Every
|
|
345
|
+
* refusal here happens before any spend: an unknown kind, a port that does not exist, a `data`
|
|
346
|
+
* binding whose schemas cannot fit, a `delegates`/`data` edge into an oracle, a terminal with no
|
|
347
|
+
* completion check (agent-runtime#971, #973).
|
|
348
|
+
*/
|
|
349
|
+
/** A node's ports: its kind's declared outputs plus the two implicit ones every node has. */
|
|
350
|
+
const IMPLICIT_OUTPUT_PORTS = ["out", "trace"];
|
|
351
|
+
/** `delegates` is the one MODEL-fired edge kind (agent-runtime#971): its payload is a directive and
|
|
352
|
+
* its target is spawned by the source supervisor through the coordination protocol, not released
|
|
353
|
+
* by the scheduler. `data` and `analyzes` are engine-fired. */
|
|
354
|
+
function isEngineFired(edge) {
|
|
355
|
+
return edge.spec.kind !== "delegates";
|
|
356
|
+
}
|
|
357
|
+
/** A node's ports: node-level declarations merged OVER its kind's (the node wins on a name). */
|
|
358
|
+
function nodePorts(kind, node) {
|
|
359
|
+
const merge = (declared, own) => {
|
|
360
|
+
if (own === void 0 || own.length === 0) return declared;
|
|
361
|
+
const names = new Set(own.map((port) => port.name));
|
|
362
|
+
return [...own, ...declared.filter((port) => !names.has(port.name))];
|
|
363
|
+
};
|
|
364
|
+
return {
|
|
365
|
+
inputs: merge(kind.inputs, node.ports?.inputs),
|
|
366
|
+
outputs: merge(kind.outputs, node.ports?.outputs)
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function outputPort(ports, port) {
|
|
370
|
+
if (IMPLICIT_OUTPUT_PORTS.includes(port)) return {
|
|
371
|
+
name: port,
|
|
372
|
+
schema: {}
|
|
373
|
+
};
|
|
374
|
+
return ports.outputs.find((candidate) => candidate.name === port);
|
|
375
|
+
}
|
|
376
|
+
function inputPort(ports, port) {
|
|
377
|
+
return ports.inputs.find((candidate) => candidate.name === port);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Bounded structural acceptance: does a value of `source`'s shape fit `target`? Schemas with no
|
|
381
|
+
* `type` accept anything; object targets require their `required` properties to be present and
|
|
382
|
+
* accepted when the source declares properties. Depth-bounded — this is a compile-time tripwire,
|
|
383
|
+
* not a full JSON Schema validator.
|
|
384
|
+
*/
|
|
385
|
+
function schemaAccepts(source, target, depth = 0) {
|
|
386
|
+
if (depth > 6) return true;
|
|
387
|
+
const sourceType = source.type;
|
|
388
|
+
const targetType = target.type;
|
|
389
|
+
if (targetType === void 0 || sourceType === void 0) return true;
|
|
390
|
+
const targets = Array.isArray(targetType) ? targetType : [targetType];
|
|
391
|
+
const sources = Array.isArray(sourceType) ? sourceType : [sourceType];
|
|
392
|
+
if (sources.filter((candidate) => targets.includes(candidate === "integer" ? "number" : candidate) || targets.includes(candidate)).length === 0) return false;
|
|
393
|
+
if (targets.includes("object") && sources.includes("object")) {
|
|
394
|
+
const required = Array.isArray(target.required) ? target.required : [];
|
|
395
|
+
const sourceProps = source.properties;
|
|
396
|
+
const targetProps = target.properties;
|
|
397
|
+
if (sourceProps !== void 0) for (const name of required) {
|
|
398
|
+
const sourceProp = sourceProps[name];
|
|
399
|
+
if (sourceProp === void 0) return false;
|
|
400
|
+
const targetProp = targetProps?.[name];
|
|
401
|
+
if (targetProp !== void 0 && !schemaAccepts(sourceProp, targetProp, depth + 1)) return false;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (targets.includes("array") && sources.includes("array")) {
|
|
405
|
+
const sourceItems = source.items;
|
|
406
|
+
const targetItems = target.items;
|
|
407
|
+
if (sourceItems !== void 0 && targetItems !== void 0) return schemaAccepts(sourceItems, targetItems, depth + 1);
|
|
408
|
+
}
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Lower an authored graph against an engine's kind registry into the schedulable form, refusing
|
|
413
|
+
* every structural defect before any spend.
|
|
414
|
+
*/
|
|
415
|
+
function compileGraph(engine, spec, context = "compileGraph") {
|
|
416
|
+
validateEngineGraphSpec(spec, context);
|
|
417
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
418
|
+
const configs = /* @__PURE__ */ new Map();
|
|
419
|
+
for (const node of spec.nodes) {
|
|
420
|
+
const handle = parseRegistryHandle(node.kind, `${context}: node ${node.id}`);
|
|
421
|
+
const kind = engine.kinds.require(handle, `${context}: node ${node.id}`);
|
|
422
|
+
kinds.set(node.id, kind);
|
|
423
|
+
configs.set(node.id, kind.validateConfig(node.config ?? {}, `${context}: node ${node.id}`));
|
|
424
|
+
if (node.flags?.pure && kind.id !== "script") throw new ValidationError(`${context}: node ${node.id} sets pure, which only a script node may claim`);
|
|
425
|
+
}
|
|
426
|
+
const compiledEdges = [];
|
|
427
|
+
const inbound = /* @__PURE__ */ new Map();
|
|
428
|
+
const outbound = /* @__PURE__ */ new Map();
|
|
429
|
+
for (const [index, edge] of spec.edges.entries()) {
|
|
430
|
+
const who = `${context}: edge[${index}] ${edge.from.node}->${edge.to.node}`;
|
|
431
|
+
const fromKind = kinds.get(edge.from.node);
|
|
432
|
+
const toKind = kinds.get(edge.to.node);
|
|
433
|
+
if (!fromKind || !toKind) throw new ValidationError(`${who}: unresolved endpoint`);
|
|
434
|
+
const toNode = spec.nodes.find((node) => node.id === edge.to.node);
|
|
435
|
+
if (edge.kind !== "analyzes" && toNode?.flags?.oracle) throw new ValidationError(`${who}: a ${edge.kind} edge may not target oracle node ${edge.to.node}; use analyzes`);
|
|
436
|
+
const fromPort = edge.from.port ?? (edge.kind === "analyzes" ? "trace" : "out");
|
|
437
|
+
if (edge.kind === "analyzes" && fromPort !== "trace") throw new ValidationError(`${who}: an analyzes edge reads the trace port, got ${fromPort}`);
|
|
438
|
+
const fromPorts = nodePorts(fromKind, spec.nodes.find((node) => node.id === edge.from.node));
|
|
439
|
+
const toPorts = nodePorts(toKind, toNode);
|
|
440
|
+
const sourcePort = outputPort(fromPorts, fromPort);
|
|
441
|
+
if (!sourcePort) {
|
|
442
|
+
const known = [...IMPLICIT_OUTPUT_PORTS, ...fromPorts.outputs.map((port) => port.name)];
|
|
443
|
+
throw new ValidationError(`${who}: ${formatRegistryHandle(fromKind)} has no output port ${JSON.stringify(fromPort)}; known: ${known.join(", ")}`);
|
|
444
|
+
}
|
|
445
|
+
let toPort = edge.to.port ?? "";
|
|
446
|
+
if (edge.kind === "data") {
|
|
447
|
+
if (toPort === "") if (toPorts.inputs.length === 1) toPort = toPorts.inputs[0]?.name ?? "";
|
|
448
|
+
else throw new ValidationError(`${who}: a data edge needs to.port; ${formatRegistryHandle(toKind)} declares ${toPorts.inputs.length} inputs`);
|
|
449
|
+
const targetPort = inputPort(toPorts, toPort);
|
|
450
|
+
if (!targetPort) throw new ValidationError(`${who}: ${formatRegistryHandle(toKind)} has no input port ${JSON.stringify(toPort)}; known: ${toPorts.inputs.map((port) => port.name).join(", ") || "(none)"}`);
|
|
451
|
+
if (edge.projection === void 0 && !schemaAccepts(sourcePort.schema, targetPort.schema)) throw new ValidationError(`${who}: output ${fromPort} (${JSON.stringify(sourcePort.schema.type ?? "any")}) cannot fit input ${toPort} (${JSON.stringify(targetPort.schema.type ?? "any")})`);
|
|
452
|
+
}
|
|
453
|
+
const compiled = {
|
|
454
|
+
id: edge.id ?? `${edge.from.node}->${edge.to.node}#${index}`,
|
|
455
|
+
spec: edge,
|
|
456
|
+
fromPort,
|
|
457
|
+
toPort: toPort === "" ? "out" : toPort
|
|
458
|
+
};
|
|
459
|
+
compiledEdges.push(compiled);
|
|
460
|
+
outbound.set(edge.from.node, [...outbound.get(edge.from.node) ?? [], compiled]);
|
|
461
|
+
inbound.set(edge.to.node, [...inbound.get(edge.to.node) ?? [], compiled]);
|
|
462
|
+
}
|
|
463
|
+
const dupes = /* @__PURE__ */ new Set();
|
|
464
|
+
for (const edge of compiledEdges) {
|
|
465
|
+
if (dupes.has(edge.id)) throw new ValidationError(`${context}: duplicate edge id ${edge.id}`);
|
|
466
|
+
dupes.add(edge.id);
|
|
467
|
+
}
|
|
468
|
+
const engineInbound = (id) => (inbound.get(id) ?? []).filter(isEngineFired);
|
|
469
|
+
const engineOutbound = (id) => (outbound.get(id) ?? []).filter(isEngineFired);
|
|
470
|
+
const unfed = spec.nodes.filter((node) => (inbound.get(node.id) ?? []).length === 0);
|
|
471
|
+
if (unfed.length === 0 && spec.root === void 0) throw new ValidationError(`${context}: no entry node — every node has an inbound edge`);
|
|
472
|
+
const root = spec.root ?? (unfed.length === 1 ? unfed[0]?.id ?? "" : (() => {
|
|
473
|
+
throw new ValidationError(`${context}: ${unfed.length} entry nodes (${unfed.map((node) => node.id).join(", ")}) — name spec.root`);
|
|
474
|
+
})());
|
|
475
|
+
const declaredOut = new Set(spec.nodes.filter((node) => node.entry === false).map((node) => node.id));
|
|
476
|
+
const entries = [root, ...unfed.map((node) => node.id).filter((id) => id !== root)].filter((id) => !declaredOut.has(id));
|
|
477
|
+
if (entries.length === 0) throw new ValidationError(`${context}: every entry node declares entry: false — nothing starts`);
|
|
478
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
479
|
+
const terminals = [];
|
|
480
|
+
for (const node of spec.nodes) {
|
|
481
|
+
const kind = kinds.get(node.id);
|
|
482
|
+
if (!kind) throw new ValidationError(`${context}: unresolved node ${node.id}`);
|
|
483
|
+
const modelFired = (inbound.get(node.id) ?? []).some((edge) => !isEngineFired(edge));
|
|
484
|
+
const terminal = node.terminal ?? (!modelFired && engineOutbound(node.id).length === 0);
|
|
485
|
+
const deliverable = node.deliverable ?? (node.id === root ? spec.deliverable : void 0);
|
|
486
|
+
if (terminal) terminals.push(node.id);
|
|
487
|
+
nodes.set(node.id, {
|
|
488
|
+
id: node.id,
|
|
489
|
+
kind,
|
|
490
|
+
config: configs.get(node.id),
|
|
491
|
+
join: node.join ?? "all",
|
|
492
|
+
maxVisits: node.maxVisits ?? spec.maxNodeVisits ?? 25,
|
|
493
|
+
oracle: node.flags?.oracle ?? false,
|
|
494
|
+
pure: node.flags?.pure ?? false,
|
|
495
|
+
terminal,
|
|
496
|
+
...deliverable === void 0 ? {} : { deliverable },
|
|
497
|
+
inbound: engineInbound(node.id),
|
|
498
|
+
outbound: outbound.get(node.id) ?? [],
|
|
499
|
+
modelFired,
|
|
500
|
+
spec: node
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
if (terminals.filter((id) => nodes.get(id)?.deliverable === void 0).length === terminals.length) throw new ValidationError(`${context}: no terminal declares a completion check (terminals: ${terminals.join(", ")}); give a node a deliverable or set spec.deliverable`);
|
|
504
|
+
return {
|
|
505
|
+
nodes,
|
|
506
|
+
edges: compiledEdges,
|
|
507
|
+
entries,
|
|
508
|
+
terminals,
|
|
509
|
+
root,
|
|
510
|
+
maxNodeVisits: spec.maxNodeVisits ?? 25
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
//#endregion
|
|
514
|
+
//#region src/runtime/graph/kind.ts
|
|
515
|
+
/** Validate a kind declaration at registration — so a malformed kind is refused by name once,
|
|
516
|
+
* not at the first node that uses it. */
|
|
517
|
+
function validateNodeKind(kind, context = "registerNodeKind") {
|
|
518
|
+
const who = `${context}: kind ${JSON.stringify(`${kind.id}/v${kind.version}`)}`;
|
|
519
|
+
if (typeof kind.id !== "string" || kind.id.length === 0) throw new ValidationError(`${context}: a kind must carry a non-empty id`);
|
|
520
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(kind.id)) throw new ValidationError(`${who}: id may contain only letters, digits, ".", "_" and "-"`);
|
|
521
|
+
if (!Number.isSafeInteger(kind.version) || kind.version < 1) throw new ValidationError(`${who}: version must be a positive integer`);
|
|
522
|
+
if (typeof kind.description !== "string" || kind.description.trim().length === 0) throw new ValidationError(`${who}: description is required`);
|
|
523
|
+
if (typeof kind.validateConfig !== "function") throw new ValidationError(`${who}: validateConfig must be a function`);
|
|
524
|
+
if (!isRecord(kind.configSchema)) throw new ValidationError(`${who}: configSchema must be a JSON Schema object`);
|
|
525
|
+
for (const [field, ports] of [["inputs", kind.inputs], ["outputs", kind.outputs]]) {
|
|
526
|
+
if (!Array.isArray(ports)) throw new ValidationError(`${who}: ${field} must be an array`);
|
|
527
|
+
const seen = /* @__PURE__ */ new Set();
|
|
528
|
+
for (const port of ports) {
|
|
529
|
+
if (!port || typeof port.name !== "string" || port.name.length === 0) throw new ValidationError(`${who}: every ${field} port needs a non-empty name`);
|
|
530
|
+
if (field === "outputs" && (port.name === "out" || port.name === "trace")) throw new ValidationError(`${who}: output port ${JSON.stringify(port.name)} is implicit on every node and cannot be declared`);
|
|
531
|
+
if (seen.has(port.name)) throw new ValidationError(`${who}: duplicate ${field} port ${JSON.stringify(port.name)}`);
|
|
532
|
+
seen.add(port.name);
|
|
533
|
+
if (!isRecord(port.schema)) throw new ValidationError(`${who}: ${field} port ${JSON.stringify(port.name)} needs a JSON Schema`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (!Array.isArray(kind.effects) || kind.effects.some((e) => typeof e !== "string" || e.length === 0)) throw new ValidationError(`${who}: effects must be an array of non-empty names`);
|
|
537
|
+
if (new Set(kind.effects).size !== kind.effects.length) throw new ValidationError(`${who}: effects must not repeat a name`);
|
|
538
|
+
if (kind.onCrash !== "restart" && kind.onCrash !== "resume") throw new ValidationError(`${who}: onCrash must be "restart" or "resume"`);
|
|
539
|
+
if (kind.budget !== "metered" && kind.budget !== "exempt") throw new ValidationError(`${who}: budget must be "metered" or "exempt"`);
|
|
540
|
+
if (typeof kind.run !== "function") throw new ValidationError(`${who}: run must be a function`);
|
|
541
|
+
return kind;
|
|
542
|
+
}
|
|
543
|
+
/** The handle a graph writes to name this kind. */
|
|
544
|
+
function kindHandle(kind) {
|
|
545
|
+
return {
|
|
546
|
+
id: kind.id,
|
|
547
|
+
version: kind.version
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Narrow a host's effect table to exactly what one kind declared. Anything the kind did not
|
|
552
|
+
* declare is absent — `undefined` on read — so a kind cannot reach past its declaration, and the
|
|
553
|
+
* engine can list a graph's required effects before spending a token.
|
|
554
|
+
*/
|
|
555
|
+
function narrowEffects(declared, provided, context) {
|
|
556
|
+
const out = {};
|
|
557
|
+
const missing = [];
|
|
558
|
+
for (const name of declared) {
|
|
559
|
+
if (!(name in provided)) {
|
|
560
|
+
missing.push(name);
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
out[name] = provided[name];
|
|
564
|
+
}
|
|
565
|
+
if (missing.length > 0) throw new ValidationError(`${context}: host provides no effect for ${missing.map((m) => JSON.stringify(m)).join(", ")}; provided: ${Object.keys(provided).sort().join(", ") || "none"}`);
|
|
566
|
+
return Object.freeze(out);
|
|
567
|
+
}
|
|
568
|
+
function isRecord(value) {
|
|
569
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
570
|
+
}
|
|
571
|
+
//#endregion
|
|
572
|
+
//#region src/runtime/graph/engine.ts
|
|
573
|
+
/**
|
|
574
|
+
* `createGraphEngine` — one engine instance: its kind registry (core kinds pre-registered, host
|
|
575
|
+
* kinds added by the caller), its effect table, and nothing global.
|
|
576
|
+
*
|
|
577
|
+
* The scheduler (#980), journal fold (#981) and the `runGraph` preset (#982) attach here; this
|
|
578
|
+
* file is the part that must exist first so a host can register kinds and a compiler can ask
|
|
579
|
+
* "which effects does this graph need" before a token is spent.
|
|
580
|
+
*/
|
|
581
|
+
/**
|
|
582
|
+
* Build one engine: a kind registry seeded with the core kinds plus the host's, and the host's
|
|
583
|
+
* effect values. Every kind is validated by name at construction, so a malformed host kind fails
|
|
584
|
+
* here, never at its first node.
|
|
585
|
+
*/
|
|
586
|
+
function createGraphEngine(options) {
|
|
587
|
+
const kinds = createRegistry("graph kinds");
|
|
588
|
+
for (const kind of options.coreKinds) kinds.register(validateNodeKind(kind, "createGraphEngine"));
|
|
589
|
+
for (const kind of options.kinds ?? []) kinds.register(validateNodeKind(kind, "createGraphEngine"));
|
|
590
|
+
const effects = Object.freeze({ ...options.effects ?? {} });
|
|
591
|
+
if (Object.keys(effects).some((name) => name.length === 0)) throw new ValidationError("createGraphEngine: an effect name must be non-empty");
|
|
592
|
+
const engine = {
|
|
593
|
+
kinds,
|
|
594
|
+
effects,
|
|
595
|
+
requiredEffects() {
|
|
596
|
+
const names = /* @__PURE__ */ new Set();
|
|
597
|
+
for (const kind of kinds.entries()) for (const name of kind.effects) names.add(name);
|
|
598
|
+
return Array.from(names).sort();
|
|
599
|
+
},
|
|
600
|
+
missingEffects() {
|
|
601
|
+
return engine.requiredEffects().filter((name) => !(name in effects));
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
return engine;
|
|
605
|
+
}
|
|
606
|
+
//#endregion
|
|
607
|
+
//#region src/runtime/graph/kinds.ts
|
|
608
|
+
/**
|
|
609
|
+
* One profile, one run: the kernel's leaf, exactly as `supervise()` derives it from `backend`.
|
|
610
|
+
* The model cannot decide this — it is what gets run.
|
|
611
|
+
*/
|
|
612
|
+
function agentKind(defaults) {
|
|
613
|
+
return {
|
|
614
|
+
id: "agent",
|
|
615
|
+
version: 1,
|
|
616
|
+
description: "One AgentProfile run as a leaf on a backend; the kernel derives the executor.",
|
|
617
|
+
validateConfig: (raw, context) => {
|
|
618
|
+
const config = asRecord(raw, `${context}: agent config`);
|
|
619
|
+
return {
|
|
620
|
+
...config.backend !== void 0 ? { backend: config.backend } : {},
|
|
621
|
+
...config.deliverable !== void 0 ? { deliverable: config.deliverable } : {}
|
|
622
|
+
};
|
|
623
|
+
},
|
|
624
|
+
configSchema: {
|
|
625
|
+
type: "object",
|
|
626
|
+
properties: {
|
|
627
|
+
backend: { type: "object" },
|
|
628
|
+
deliverable: { type: "object" }
|
|
629
|
+
},
|
|
630
|
+
additionalProperties: false
|
|
631
|
+
},
|
|
632
|
+
inputs: [],
|
|
633
|
+
outputs: [],
|
|
634
|
+
effects: [],
|
|
635
|
+
onCrash: "restart",
|
|
636
|
+
budget: "metered",
|
|
637
|
+
run: ({ config, profile, spawn }) => {
|
|
638
|
+
const backend = config.backend ?? defaults.backend;
|
|
639
|
+
if (!backend) throw new ValidationError(`agent kind: node ${JSON.stringify(profile.name)} has no backend — set config.backend or the engine default`);
|
|
640
|
+
return workerFromBackend(backend, config.deliverable ?? defaults.deliverable)(profile, spawn);
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* The thing that DECIDES: a nested `supervisorAgent` with the coordination verbs. Its children
|
|
646
|
+
* are its own tree — the graph sees one node in, one `Settled` out. A `subgraph` constrains what
|
|
647
|
+
* it may spawn; without one it is free-form under `profileSecurity` and `allowedModels`.
|
|
648
|
+
*/
|
|
649
|
+
function supervisorKind(deps) {
|
|
650
|
+
return {
|
|
651
|
+
id: "supervisor",
|
|
652
|
+
version: 1,
|
|
653
|
+
description: "A nested supervisor: spawns, observes, steers and awaits its own children with the coordination verbs.",
|
|
654
|
+
validateConfig: (raw, context) => {
|
|
655
|
+
const config = asRecord(raw, `${context}: supervisor config`);
|
|
656
|
+
return {
|
|
657
|
+
perWorker: asRecord(config.perWorker, `${context}: supervisor config.perWorker`),
|
|
658
|
+
...config.maxLiveWorkers !== void 0 ? { maxLiveWorkers: Number(config.maxLiveWorkers) } : {}
|
|
659
|
+
};
|
|
660
|
+
},
|
|
661
|
+
configSchema: {
|
|
662
|
+
type: "object",
|
|
663
|
+
properties: {
|
|
664
|
+
perWorker: { type: "object" },
|
|
665
|
+
maxLiveWorkers: { type: "integer" }
|
|
666
|
+
},
|
|
667
|
+
required: ["perWorker"],
|
|
668
|
+
additionalProperties: false
|
|
669
|
+
},
|
|
670
|
+
inputs: [],
|
|
671
|
+
outputs: [],
|
|
672
|
+
effects: [],
|
|
673
|
+
onCrash: "restart",
|
|
674
|
+
budget: "metered",
|
|
675
|
+
run: ({ config, profile }) => supervisorAgent(profile, {
|
|
676
|
+
blobs: deps.blobs,
|
|
677
|
+
makeWorkerAgent: deps.makeWorkerAgent,
|
|
678
|
+
perWorker: config.perWorker,
|
|
679
|
+
...config.maxLiveWorkers !== void 0 ? { maxLiveWorkers: config.maxLiveWorkers } : {},
|
|
680
|
+
...deps.router ? { router: deps.router } : {},
|
|
681
|
+
...deps.driveHarness ? { driveHarness: deps.driveHarness } : {}
|
|
682
|
+
})
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
/** The script kind's handle; it also names the kind in every script node's identity. */
|
|
686
|
+
const SCRIPT = {
|
|
687
|
+
id: "script",
|
|
688
|
+
version: 1
|
|
689
|
+
};
|
|
690
|
+
/**
|
|
691
|
+
* Caller code as a node. The one kind with no kernel primitive behind it: the kernel has no
|
|
692
|
+
* "data→data with no execution" concept (agent-runtime#970 fact-finding), so this is new. It is
|
|
693
|
+
* still a leaf `Agent` carrying an `Executor`, so the journal, the gate and the pool treat it like
|
|
694
|
+
* any other node.
|
|
695
|
+
*/
|
|
696
|
+
function scriptKind() {
|
|
697
|
+
return {
|
|
698
|
+
...SCRIPT,
|
|
699
|
+
description: "Run caller code over the resolved inputs; pure scripts are exempt and restorable.",
|
|
700
|
+
validateConfig: (raw, context) => {
|
|
701
|
+
const config = asRecord(raw, `${context}: script config`);
|
|
702
|
+
if (typeof config.body !== "function") throw new ValidationError(`${context}: script config.body must be a function`);
|
|
703
|
+
if (config.pure !== void 0 && typeof config.pure !== "boolean") throw new ValidationError(`${context}: script config.pure must be a boolean`);
|
|
704
|
+
if (config.pure === true && config.spent !== void 0) throw new ValidationError(`${context}: a pure script is budget-exempt and cannot report spent`);
|
|
705
|
+
return {
|
|
706
|
+
body: config.body,
|
|
707
|
+
...config.pure !== void 0 ? { pure: config.pure } : {},
|
|
708
|
+
...config.spent !== void 0 ? { spent: config.spent } : {}
|
|
709
|
+
};
|
|
710
|
+
},
|
|
711
|
+
configSchema: {
|
|
712
|
+
type: "object",
|
|
713
|
+
properties: {
|
|
714
|
+
pure: { type: "boolean" },
|
|
715
|
+
spent: { type: "object" }
|
|
716
|
+
},
|
|
717
|
+
additionalProperties: true
|
|
718
|
+
},
|
|
719
|
+
inputs: [],
|
|
720
|
+
outputs: [],
|
|
721
|
+
effects: [],
|
|
722
|
+
onCrash: "restart",
|
|
723
|
+
budget: "metered",
|
|
724
|
+
run: ({ config, profile, inputs }) => scriptAgent(profile, config, inputs, SCRIPT)
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function scriptAgent(profile, config, inputs, kind) {
|
|
728
|
+
let artifact;
|
|
729
|
+
const spec = {
|
|
730
|
+
profile,
|
|
731
|
+
harness: null,
|
|
732
|
+
executor: {
|
|
733
|
+
runtime: "inline",
|
|
734
|
+
...config.pure ? { budgetExempt: true } : {},
|
|
735
|
+
async execute(_task, signal) {
|
|
736
|
+
const startedAt = Date.now();
|
|
737
|
+
const out = await config.body(inputs, signal);
|
|
738
|
+
const ms = Date.now() - startedAt;
|
|
739
|
+
const spent = config.pure ? {
|
|
740
|
+
iterations: 0,
|
|
741
|
+
tokens: {
|
|
742
|
+
input: 0,
|
|
743
|
+
output: 0
|
|
744
|
+
},
|
|
745
|
+
usd: 0,
|
|
746
|
+
ms
|
|
747
|
+
} : config.spent ?? {
|
|
748
|
+
iterations: 1,
|
|
749
|
+
tokens: {
|
|
750
|
+
input: 0,
|
|
751
|
+
output: 0,
|
|
752
|
+
tokensKnown: false
|
|
753
|
+
},
|
|
754
|
+
tokensKnown: false,
|
|
755
|
+
usd: 0,
|
|
756
|
+
usdKnown: false,
|
|
757
|
+
ms
|
|
758
|
+
};
|
|
759
|
+
artifact = {
|
|
760
|
+
outRef: contentAddress(out),
|
|
761
|
+
out,
|
|
762
|
+
spent
|
|
763
|
+
};
|
|
764
|
+
return artifact;
|
|
765
|
+
},
|
|
766
|
+
teardown: () => Promise.resolve({ destroyed: true }),
|
|
767
|
+
resultArtifact() {
|
|
768
|
+
if (!artifact) throw new ValidationError("script: resultArtifact() read before execute()");
|
|
769
|
+
return artifact;
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
execution: { correlation: { nodeKind: formatRegistryHandle(kind) } }
|
|
773
|
+
};
|
|
774
|
+
return {
|
|
775
|
+
name: profile.name ?? "script",
|
|
776
|
+
act: () => Promise.reject(new ValidationError("script: act() is not the execution path")),
|
|
777
|
+
executorSpec: spec
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* A node carrying its own graph: the constraint on what a supervisor may spawn at depth>1. Its
|
|
782
|
+
* executor is a nested engine run; that needs the scheduler (#980), so until then this kind is
|
|
783
|
+
* registered and REFUSES at run time by name rather than being absent — a graph that names it
|
|
784
|
+
* compiles, and the refusal says exactly what is missing.
|
|
785
|
+
*/
|
|
786
|
+
function subgraphKind() {
|
|
787
|
+
return {
|
|
788
|
+
id: "subgraph",
|
|
789
|
+
version: 1,
|
|
790
|
+
description: "A node that runs its own graph; constrains a supervisor at depth>1.",
|
|
791
|
+
validateConfig: (raw, context) => {
|
|
792
|
+
const config = asRecord(raw, `${context}: subgraph config`);
|
|
793
|
+
if (config.graph === void 0) throw new ValidationError(`${context}: subgraph config.graph is required`);
|
|
794
|
+
return { graph: config.graph };
|
|
795
|
+
},
|
|
796
|
+
configSchema: {
|
|
797
|
+
type: "object",
|
|
798
|
+
properties: { graph: { type: "object" } },
|
|
799
|
+
required: ["graph"]
|
|
800
|
+
},
|
|
801
|
+
inputs: [],
|
|
802
|
+
outputs: [],
|
|
803
|
+
effects: [],
|
|
804
|
+
onCrash: "restart",
|
|
805
|
+
budget: "metered",
|
|
806
|
+
run: ({ profile }) => {
|
|
807
|
+
throw new ValidationError(`subgraph kind: node ${JSON.stringify(profile.name)} cannot run yet — the scheduler that executes a nested graph lands in agent-runtime#980`);
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
function asRecord(value, context) {
|
|
812
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new ValidationError(`${context} must be an object`);
|
|
813
|
+
return value;
|
|
814
|
+
}
|
|
815
|
+
//#endregion
|
|
816
|
+
//#region src/runtime/graph/admit.ts
|
|
817
|
+
/**
|
|
818
|
+
* Edge payload admission (agent-runtime#971): every value crossing an edge is JSON round-tripped,
|
|
819
|
+
* `undefined` stripped to absence, and a non-representable value (a cycle, a BigInt, a function)
|
|
820
|
+
* becomes a RECORD of that fact. This is the kernel's existing findings-guard rule, and it is what
|
|
821
|
+
* makes `inputRef` stable and `onCrash: 'restart'` well defined — a degraded record beats a
|
|
822
|
+
* vanished edge.
|
|
823
|
+
*/
|
|
824
|
+
function admitPayload(value) {
|
|
825
|
+
if (value === void 0) return void 0;
|
|
826
|
+
try {
|
|
827
|
+
const text = JSON.stringify(value);
|
|
828
|
+
if (text === void 0) return { nonCanonical: `payload of type ${typeof value}` };
|
|
829
|
+
return JSON.parse(text);
|
|
830
|
+
} catch (error) {
|
|
831
|
+
return { nonCanonical: error instanceof Error ? error.message : String(error) };
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
//#endregion
|
|
835
|
+
//#region src/runtime/graph/fold.ts
|
|
836
|
+
/**
|
|
837
|
+
* The fold (agent-runtime#974): scheduler state is a pure function of the journal, reconstructed
|
|
838
|
+
* by replaying events through the SAME reducer the live scheduler feeds — fold, never checkpoint.
|
|
839
|
+
* Kernel events (`spawned`, `settled`, `waiting`, `woken`) carry what the kernel owns; the engine's
|
|
840
|
+
* events (`node-inputs-resolved`, `edge-verdict`, `join-state`) carry what the scheduler decided,
|
|
841
|
+
* each journaled BEFORE its effect was visible. `edge` stays observability and is skipped here
|
|
842
|
+
* exactly as the kernel's own replay skips it.
|
|
843
|
+
*
|
|
844
|
+
* Events are consumed in APPEND order (the journal file's line order), not by `seq`: kernel and
|
|
845
|
+
* engine events carry independent ordinal counters, and append order is the one total order both
|
|
846
|
+
* share.
|
|
847
|
+
*/
|
|
848
|
+
/** The reducer's zero: every node unvisited, every edge pending, nothing suspended. */
|
|
849
|
+
function emptyFoldState(compiled) {
|
|
850
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
851
|
+
for (const id of compiled.nodes.keys()) nodes.set(id, {
|
|
852
|
+
visits: 0,
|
|
853
|
+
blocked: false,
|
|
854
|
+
settles: []
|
|
855
|
+
});
|
|
856
|
+
const edges = /* @__PURE__ */ new Map();
|
|
857
|
+
for (const edge of compiled.edges) edges.set(edge.id, {
|
|
858
|
+
state: "pending",
|
|
859
|
+
consumedOnce: false,
|
|
860
|
+
traversals: 0,
|
|
861
|
+
judgedSourceSettles: 0,
|
|
862
|
+
capped: false
|
|
863
|
+
});
|
|
864
|
+
return {
|
|
865
|
+
nodes,
|
|
866
|
+
edges,
|
|
867
|
+
instances: /* @__PURE__ */ new Map(),
|
|
868
|
+
spawnedIds: /* @__PURE__ */ new Map(),
|
|
869
|
+
suspensions: /* @__PURE__ */ new Map(),
|
|
870
|
+
exhaustedEdges: /* @__PURE__ */ new Set()
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
function instanceOf(state, label) {
|
|
874
|
+
return state.instances.get(label);
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Apply ONE journal event. The live scheduler calls this right after each append; the restart path
|
|
878
|
+
* calls it over the loaded journal. Unknown kernel events are ignored — the engine folds only what
|
|
879
|
+
* it understands, exactly as the kernel's replay skips the engine's events.
|
|
880
|
+
*/
|
|
881
|
+
function applyGraphFoldEvent(state, ev, compiled) {
|
|
882
|
+
switch (ev.kind) {
|
|
883
|
+
case "node-inputs-resolved": {
|
|
884
|
+
const visit = Number(ev.instance.split("#").at(-1));
|
|
885
|
+
if (!Number.isSafeInteger(visit)) throw new ValidationError(`graph fold: instance ${ev.instance} carries no visit ordinal`);
|
|
886
|
+
state.instances.set(ev.instance, {
|
|
887
|
+
node: ev.node,
|
|
888
|
+
instance: ev.instance,
|
|
889
|
+
visit,
|
|
890
|
+
inputRef: ev.inputRef,
|
|
891
|
+
status: "released"
|
|
892
|
+
});
|
|
893
|
+
const node = state.nodes.get(ev.node);
|
|
894
|
+
if (node) node.visits = Math.max(node.visits, visit);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
case "spawned": {
|
|
898
|
+
const instance = instanceOf(state, ev.label);
|
|
899
|
+
if (instance) {
|
|
900
|
+
instance.status = "live";
|
|
901
|
+
state.spawnedIds.set(ev.id, ev.label);
|
|
902
|
+
}
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
case "settled": {
|
|
906
|
+
const label = state.spawnedIds.get(ev.id);
|
|
907
|
+
const instance = label === void 0 ? void 0 : instanceOf(state, label);
|
|
908
|
+
if (!instance) return;
|
|
909
|
+
instance.status = ev.status;
|
|
910
|
+
const settle = {
|
|
911
|
+
node: instance.node,
|
|
912
|
+
visit: instance.visit,
|
|
913
|
+
status: ev.status,
|
|
914
|
+
...ev.outRef !== void 0 ? { outRef: ev.outRef } : {},
|
|
915
|
+
...ev.reason !== void 0 ? { reason: ev.reason } : {}
|
|
916
|
+
};
|
|
917
|
+
if (ev.trace?.status === "available") settle.traceRef = ev.trace.traceRef;
|
|
918
|
+
instance.settle = settle;
|
|
919
|
+
state.nodes.get(instance.node)?.settles.push(settle);
|
|
920
|
+
const node = compiled.nodes.get(instance.node);
|
|
921
|
+
for (const edge of node?.outbound ?? []) {
|
|
922
|
+
const folded = state.edges.get(edge.id);
|
|
923
|
+
if (folded?.consumedOnce && folded.state === "pending") {
|
|
924
|
+
folded.consumedOnce = false;
|
|
925
|
+
folded.judgedSourceSettles += 1;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
case "edge-verdict": {
|
|
931
|
+
const folded = state.edges.get(ev.edge);
|
|
932
|
+
if (!folded) return;
|
|
933
|
+
if (ev.capped) {
|
|
934
|
+
folded.capped = true;
|
|
935
|
+
state.exhaustedEdges.add(ev.edge);
|
|
936
|
+
const target = compiled.edges.find((edge) => edge.id === ev.edge)?.spec.to.node;
|
|
937
|
+
const compiledTarget = target === void 0 ? void 0 : compiled.nodes.get(target);
|
|
938
|
+
if (compiledTarget && (compiledTarget.join === "all" || compiledTarget.join === "all_done")) {
|
|
939
|
+
const node = state.nodes.get(target);
|
|
940
|
+
if (node) node.blocked = true;
|
|
941
|
+
}
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
folded.judgedSourceSettles += 1;
|
|
945
|
+
if (!ev.fired) {
|
|
946
|
+
folded.state = ev.sourceStatus === "done" ? "dead" : "failed";
|
|
947
|
+
folded.payloadRef = void 0;
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
folded.state = "satisfied";
|
|
951
|
+
folded.payloadRef = ev.inputRef;
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
case "join-state": {
|
|
955
|
+
for (const edgeId of ev.satisfiedBy) {
|
|
956
|
+
const folded = state.edges.get(edgeId);
|
|
957
|
+
if (!folded) continue;
|
|
958
|
+
folded.traversals += 1;
|
|
959
|
+
folded.state = "pending";
|
|
960
|
+
folded.payloadRef = void 0;
|
|
961
|
+
}
|
|
962
|
+
const target = compiled.nodes.get(ev.node);
|
|
963
|
+
for (const edge of target?.inbound ?? []) {
|
|
964
|
+
const folded = state.edges.get(edge.id);
|
|
965
|
+
if (!folded) continue;
|
|
966
|
+
if (ev.consumedPending.includes(edge.id)) folded.consumedOnce = true;
|
|
967
|
+
else if (folded.state !== "pending" && !ev.satisfiedBy.includes(edge.id)) {
|
|
968
|
+
folded.state = "pending";
|
|
969
|
+
folded.payloadRef = void 0;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
case "waiting": {
|
|
975
|
+
if (ev.spec.kind !== "token") return;
|
|
976
|
+
const instance = instanceOf(state, ev.label);
|
|
977
|
+
if (instance) {
|
|
978
|
+
instance.status = "suspended";
|
|
979
|
+
if (instance.settle !== void 0) {
|
|
980
|
+
const settles = state.nodes.get(instance.node)?.settles;
|
|
981
|
+
if (settles && settles.at(-1) === instance.settle) settles.pop();
|
|
982
|
+
instance.settle = void 0;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
state.suspensions.set(ev.spec.token, {
|
|
986
|
+
token: ev.spec.token,
|
|
987
|
+
node: instance?.node ?? ev.label.split("#")[0] ?? ev.label,
|
|
988
|
+
instance: ev.label,
|
|
989
|
+
onExpire: ev.spec.onExpire,
|
|
990
|
+
...ev.spec.expiresAtMs !== void 0 ? { expiresAtMs: ev.spec.expiresAtMs } : {},
|
|
991
|
+
...ev.spec.defaultRef !== void 0 ? { defaultRef: ev.spec.defaultRef } : {},
|
|
992
|
+
status: "pending"
|
|
993
|
+
});
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
case "woken": {
|
|
997
|
+
const token = ev.id.startsWith("graphwait:") ? ev.id.slice(10) : void 0;
|
|
998
|
+
const suspension = token === void 0 ? void 0 : state.suspensions.get(token);
|
|
999
|
+
if (!suspension) return;
|
|
1000
|
+
const instance = instanceOf(state, suspension.instance);
|
|
1001
|
+
if (ev.by === "expired") {
|
|
1002
|
+
suspension.status = "expired";
|
|
1003
|
+
if (instance) {
|
|
1004
|
+
instance.status = "down";
|
|
1005
|
+
const settle = {
|
|
1006
|
+
node: suspension.node,
|
|
1007
|
+
visit: instance.visit,
|
|
1008
|
+
status: "down",
|
|
1009
|
+
reason: "suspension expired"
|
|
1010
|
+
};
|
|
1011
|
+
instance.settle = settle;
|
|
1012
|
+
state.nodes.get(suspension.node)?.settles.push(settle);
|
|
1013
|
+
}
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
suspension.status = "woken";
|
|
1017
|
+
if (instance) {
|
|
1018
|
+
instance.status = "done";
|
|
1019
|
+
const settle = {
|
|
1020
|
+
node: suspension.node,
|
|
1021
|
+
visit: instance.visit,
|
|
1022
|
+
status: "done",
|
|
1023
|
+
...ev.outRef !== void 0 ? { outRef: ev.outRef } : {}
|
|
1024
|
+
};
|
|
1025
|
+
instance.settle = settle;
|
|
1026
|
+
state.nodes.get(suspension.node)?.settles.push(settle);
|
|
1027
|
+
}
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
default: return;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
/** Fold a loaded journal (append order) into scheduler state. */
|
|
1034
|
+
function foldGraphJournal(events, compiled) {
|
|
1035
|
+
const state = emptyFoldState(compiled);
|
|
1036
|
+
for (const ev of events) applyGraphFoldEvent(state, ev, compiled);
|
|
1037
|
+
return state;
|
|
1038
|
+
}
|
|
1039
|
+
//#endregion
|
|
1040
|
+
//#region src/runtime/graph/join.ts
|
|
1041
|
+
const NOTHING = {
|
|
1042
|
+
release: false,
|
|
1043
|
+
consuming: [],
|
|
1044
|
+
blocked: false
|
|
1045
|
+
};
|
|
1046
|
+
/** Decide whether a node's gating edges release it, and which of them the release consumes. */
|
|
1047
|
+
function decideJoin(rule, gating) {
|
|
1048
|
+
if (gating.length === 0) return NOTHING;
|
|
1049
|
+
const settled = gating.filter((entry) => entry.folded && entry.folded.state !== "pending");
|
|
1050
|
+
const satisfied = gating.filter((entry) => entry.folded?.state === "satisfied");
|
|
1051
|
+
const failed = gating.filter((entry) => entry.folded?.state === "failed");
|
|
1052
|
+
const allSettled = settled.length === gating.length;
|
|
1053
|
+
switch (rule) {
|
|
1054
|
+
case "all": {
|
|
1055
|
+
const release = !gating.some((entry) => entry.folded?.state === "dead" || entry.folded?.state === "failed") && satisfied.length === gating.length;
|
|
1056
|
+
return {
|
|
1057
|
+
release,
|
|
1058
|
+
consuming: release ? settled : [],
|
|
1059
|
+
blocked: false
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
case "any": {
|
|
1063
|
+
const first = satisfied[0];
|
|
1064
|
+
if (first === void 0) return {
|
|
1065
|
+
release: false,
|
|
1066
|
+
consuming: [],
|
|
1067
|
+
blocked: allSettled
|
|
1068
|
+
};
|
|
1069
|
+
return {
|
|
1070
|
+
release: true,
|
|
1071
|
+
consuming: [first],
|
|
1072
|
+
blocked: false
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
case "any_failed": {
|
|
1076
|
+
const first = failed[0];
|
|
1077
|
+
if (first === void 0) return {
|
|
1078
|
+
release: false,
|
|
1079
|
+
consuming: [],
|
|
1080
|
+
blocked: allSettled
|
|
1081
|
+
};
|
|
1082
|
+
return {
|
|
1083
|
+
release: true,
|
|
1084
|
+
consuming: [first],
|
|
1085
|
+
blocked: false
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
case "all_done": return {
|
|
1089
|
+
release: allSettled,
|
|
1090
|
+
consuming: allSettled ? settled : [],
|
|
1091
|
+
blocked: false
|
|
1092
|
+
};
|
|
1093
|
+
default: return NOTHING;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
//#endregion
|
|
1097
|
+
//#region src/runtime/graph/ledger.ts
|
|
1098
|
+
/** Open a ledger for one run; its ordinals continue past whatever a prior process recorded. */
|
|
1099
|
+
function createEdgeLedger(args) {
|
|
1100
|
+
const entries = [];
|
|
1101
|
+
let seq = args.startSeq;
|
|
1102
|
+
return {
|
|
1103
|
+
entries,
|
|
1104
|
+
async record(edge, traversal, outcome, reason) {
|
|
1105
|
+
const spec = edge.spec;
|
|
1106
|
+
const entry = {
|
|
1107
|
+
edge: edge.id,
|
|
1108
|
+
kind: spec.kind,
|
|
1109
|
+
from: spec.from.node,
|
|
1110
|
+
to: spec.to.node,
|
|
1111
|
+
traversal,
|
|
1112
|
+
outcome,
|
|
1113
|
+
...spec.directive !== void 0 ? { directive: `${spec.directive.surface}/v${spec.directive.version}` } : {},
|
|
1114
|
+
...spec.kind === "data" ? { port: edge.toPort } : {},
|
|
1115
|
+
...reason !== void 0 ? { reason } : {}
|
|
1116
|
+
};
|
|
1117
|
+
entries.push(entry);
|
|
1118
|
+
await args.journal.appendEvent(args.runId, {
|
|
1119
|
+
kind: "edge",
|
|
1120
|
+
id: `graph:${spec.to.node}`,
|
|
1121
|
+
edge: {
|
|
1122
|
+
kind: spec.kind,
|
|
1123
|
+
from: spec.from.node,
|
|
1124
|
+
to: spec.to.node,
|
|
1125
|
+
...entry.directive !== void 0 ? { directive: entry.directive } : {},
|
|
1126
|
+
...entry.port !== void 0 ? { port: entry.port } : {}
|
|
1127
|
+
},
|
|
1128
|
+
traversal,
|
|
1129
|
+
outcome,
|
|
1130
|
+
bytes: 0,
|
|
1131
|
+
...reason !== void 0 ? { reason } : {},
|
|
1132
|
+
seq: seq++,
|
|
1133
|
+
at: new Date(args.now()).toISOString()
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
//#endregion
|
|
1139
|
+
//#region src/runtime/graph/result.ts
|
|
1140
|
+
/**
|
|
1141
|
+
* Turning a finished run into its result: rehydrate each settle's output, reduce the terminals
|
|
1142
|
+
* through the kernel's finalizer seam, and name the honest cause when nothing delivered
|
|
1143
|
+
* (agent-runtime#973). Separate from the loop so "what happened" is readable without reading
|
|
1144
|
+
* "what ran".
|
|
1145
|
+
*/
|
|
1146
|
+
function resolveFinalizer(choice) {
|
|
1147
|
+
if (choice === void 0 || choice === "bestDelivered") return bestDelivered;
|
|
1148
|
+
if (choice === "collectDelivered") return collectDelivered;
|
|
1149
|
+
return choice;
|
|
1150
|
+
}
|
|
1151
|
+
/** Every node settlement with its output rehydrated and its completion check applied. */
|
|
1152
|
+
async function materializeSettles(compiled, state, blobs, outCache) {
|
|
1153
|
+
const rehydrate = async (settle) => {
|
|
1154
|
+
if (settle.out !== void 0 || settle.outRef === void 0) return settle;
|
|
1155
|
+
const out = outCache.get(settle.outRef) ?? admitPayload(await blobs.get(settle.outRef));
|
|
1156
|
+
const node = compiled.nodes.get(settle.node);
|
|
1157
|
+
let valid = settle.valid;
|
|
1158
|
+
if (valid === void 0 && node?.deliverable !== void 0 && settle.status === "done") try {
|
|
1159
|
+
valid = await node.deliverable.check(out);
|
|
1160
|
+
} catch {
|
|
1161
|
+
valid = false;
|
|
1162
|
+
}
|
|
1163
|
+
return {
|
|
1164
|
+
...settle,
|
|
1165
|
+
out,
|
|
1166
|
+
...valid !== void 0 ? { valid } : {}
|
|
1167
|
+
};
|
|
1168
|
+
};
|
|
1169
|
+
return Promise.all([...compiled.nodes.keys()].flatMap((id) => state.nodes.get(id)?.settles ?? []).map(rehydrate));
|
|
1170
|
+
}
|
|
1171
|
+
/** Turn a finished run into its result: rehydrate, reduce the terminals, classify a no-winner. */
|
|
1172
|
+
async function assembleGraphResult(args) {
|
|
1173
|
+
const settles = await materializeSettles(args.compiled, args.state, args.blobs, args.outCache);
|
|
1174
|
+
const terminals = settles.filter((settle) => args.compiled.nodes.get(settle.node)?.terminal);
|
|
1175
|
+
const ledger = args.ledger;
|
|
1176
|
+
const finish = (result) => {
|
|
1177
|
+
if (result.kind === "no-winner" && args.state.exhaustedEdges.size > 0) throw new GraphEdgeCapError(Object.freeze([...args.state.exhaustedEdges]), Object.freeze([...ledger]), result);
|
|
1178
|
+
return result;
|
|
1179
|
+
};
|
|
1180
|
+
if (args.failure) return finish({
|
|
1181
|
+
kind: "no-winner",
|
|
1182
|
+
reason: args.failure.reason,
|
|
1183
|
+
...args.failure.error ? { error: args.failure.error } : {},
|
|
1184
|
+
terminals,
|
|
1185
|
+
settles,
|
|
1186
|
+
ledger,
|
|
1187
|
+
unreachable: []
|
|
1188
|
+
});
|
|
1189
|
+
const allTerminalsSettled = args.compiled.terminals.every((id) => (args.state.nodes.get(id)?.settles.length ?? 0) > 0);
|
|
1190
|
+
const pendingTokens = [...args.state.suspensions.values()].filter((suspension) => suspension.status === "pending").map((suspension) => suspension.token);
|
|
1191
|
+
if (pendingTokens.length > 0 && !allTerminalsSettled) return {
|
|
1192
|
+
kind: "suspended",
|
|
1193
|
+
tokens: pendingTokens,
|
|
1194
|
+
terminals,
|
|
1195
|
+
settles,
|
|
1196
|
+
ledger
|
|
1197
|
+
};
|
|
1198
|
+
if (args.aborted) return finish({
|
|
1199
|
+
kind: "no-winner",
|
|
1200
|
+
reason: "aborted",
|
|
1201
|
+
terminals,
|
|
1202
|
+
settles,
|
|
1203
|
+
ledger,
|
|
1204
|
+
unreachable: []
|
|
1205
|
+
});
|
|
1206
|
+
const out = terminals.filter((settle) => settle.status === "done" && settle.valid !== false).length === 0 ? void 0 : await runFinalizer(resolveFinalizer(args.finalizer), {
|
|
1207
|
+
settled: terminals.map((settle) => ({
|
|
1208
|
+
id: settle.node,
|
|
1209
|
+
status: settle.status,
|
|
1210
|
+
valid: settle.status === "done" && settle.valid !== false,
|
|
1211
|
+
...settle.outRef !== void 0 ? { outRef: settle.outRef } : {}
|
|
1212
|
+
})),
|
|
1213
|
+
blobs: args.blobs,
|
|
1214
|
+
tree: args.scope.view,
|
|
1215
|
+
budget: args.scope.budget
|
|
1216
|
+
});
|
|
1217
|
+
if (out !== void 0) return {
|
|
1218
|
+
kind: "winner",
|
|
1219
|
+
out,
|
|
1220
|
+
terminals,
|
|
1221
|
+
settles,
|
|
1222
|
+
ledger
|
|
1223
|
+
};
|
|
1224
|
+
const unreachable = [...args.compiled.nodes.keys()].filter((id) => (args.state.nodes.get(id)?.settles.length ?? 0) === 0);
|
|
1225
|
+
return finish({
|
|
1226
|
+
kind: "no-winner",
|
|
1227
|
+
reason: args.compiled.terminals.some((id) => unreachable.includes(id)) ? unreachable.length === args.compiled.nodes.size ? "budget-exhausted" : "unreachable-terminal" : "all-children-down",
|
|
1228
|
+
terminals,
|
|
1229
|
+
settles,
|
|
1230
|
+
ledger,
|
|
1231
|
+
unreachable
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
//#endregion
|
|
1235
|
+
//#region src/runtime/graph/run-context.ts
|
|
1236
|
+
/**
|
|
1237
|
+
* Opening a graph run: begin or resume the journaled tree, rebuild the pool and the kernel `Scope`
|
|
1238
|
+
* on the SAME recipe the kernel supervisor uses for its own resume, and fold the prior journal
|
|
1239
|
+
* into scheduler state. Extracted so the restart contract is one readable unit rather than a
|
|
1240
|
+
* preamble inside the loop.
|
|
1241
|
+
*/
|
|
1242
|
+
/** Engine-appended `woken` ordinals start here — far above any kernel cursor counter, so the two
|
|
1243
|
+
* counters can advance independently without ever colliding. */
|
|
1244
|
+
const ENGINE_WOKEN_SEQ_BASE = 1e7;
|
|
1245
|
+
/** Begin or resume a run's journaled tree, pool, scope and folded state. */
|
|
1246
|
+
async function openGraphRun(args) {
|
|
1247
|
+
const journal = args.journal ?? new InMemorySpawnJournal();
|
|
1248
|
+
const blobs = args.blobs ?? new InMemoryResultBlobStore();
|
|
1249
|
+
const loaded = await journal.loadTree(args.runId);
|
|
1250
|
+
if (loaded !== void 0 && args.resume !== true) throw new ValidationError(`runEngineGraph: runId '${args.runId}' already exists; pass resume: true to continue it or use a new runId`);
|
|
1251
|
+
const prior = loaded ?? [];
|
|
1252
|
+
const resuming = args.resume === true && prior.length > 0;
|
|
1253
|
+
let runEpochMs = args.now();
|
|
1254
|
+
let rootBudget = args.budget;
|
|
1255
|
+
if (loaded === void 0) await journal.beginTree(args.runId, new Date(runEpochMs).toISOString());
|
|
1256
|
+
if (resuming) {
|
|
1257
|
+
const root = prior.find((ev) => ev.kind === "spawned" && ev.id === args.runId);
|
|
1258
|
+
if (root?.kind === "spawned") {
|
|
1259
|
+
runEpochMs = Date.parse(root.at);
|
|
1260
|
+
rootBudget = root.budget;
|
|
1261
|
+
}
|
|
1262
|
+
} else await journal.appendEvent(args.runId, {
|
|
1263
|
+
kind: "spawned",
|
|
1264
|
+
id: args.runId,
|
|
1265
|
+
label: "graph-root",
|
|
1266
|
+
budget: args.budget,
|
|
1267
|
+
runtime: "inline",
|
|
1268
|
+
seq: 0,
|
|
1269
|
+
at: new Date(runEpochMs).toISOString()
|
|
1270
|
+
});
|
|
1271
|
+
const elapsed = () => args.now() - runEpochMs;
|
|
1272
|
+
const measured = resuming ? sumMeasuredSpendFromEvents([...prior]) : void 0;
|
|
1273
|
+
const pool = createBudgetPool(rootBudget, elapsed, measured === void 0 ? void 0 : {
|
|
1274
|
+
committed: addSpend(measured.childWork, measured.driverInference),
|
|
1275
|
+
uncertainReservations: uncertainSpawnBudgets([...prior]),
|
|
1276
|
+
...rootBudget.deadlineMs !== void 0 ? { absoluteDeadlineMs: runEpochMs + rootBudget.deadlineMs } : {}
|
|
1277
|
+
});
|
|
1278
|
+
const abort = new AbortController();
|
|
1279
|
+
if (args.signal !== void 0) {
|
|
1280
|
+
const forward = () => abort.abort(args.signal?.reason);
|
|
1281
|
+
args.signal.addEventListener("abort", forward, { once: true });
|
|
1282
|
+
args.onAbort(() => args.signal?.removeEventListener("abort", forward));
|
|
1283
|
+
if (args.signal.aborted) abort.abort(args.signal.reason);
|
|
1284
|
+
}
|
|
1285
|
+
const scope = createScope({
|
|
1286
|
+
parentId: args.runId,
|
|
1287
|
+
root: args.runId,
|
|
1288
|
+
pool,
|
|
1289
|
+
journal,
|
|
1290
|
+
blobs,
|
|
1291
|
+
executors: createExecutorRegistry(),
|
|
1292
|
+
seams: {},
|
|
1293
|
+
depth: 0,
|
|
1294
|
+
signal: abort.signal,
|
|
1295
|
+
now: args.now,
|
|
1296
|
+
...resuming ? { resumeFrom: {
|
|
1297
|
+
settled: await replaySpawnTree(journal, blobs, args.runId),
|
|
1298
|
+
view: materializeTreeView([...prior]),
|
|
1299
|
+
maxSpawnOrdinal: maxSeqOf([...prior], (ev) => ev.kind === "spawned"),
|
|
1300
|
+
maxCursorSeq: maxSeqOf([...prior], closesCursorSlot),
|
|
1301
|
+
maxWaitOrdinal: maxSeqOf([...prior], (ev) => ev.kind === "waiting"),
|
|
1302
|
+
waits: pendingWaits([...prior]),
|
|
1303
|
+
keys: /* @__PURE__ */ new Map(),
|
|
1304
|
+
priorSpend: sumMeasuredSpendFromEvents([...prior])
|
|
1305
|
+
} } : {}
|
|
1306
|
+
});
|
|
1307
|
+
const state = emptyFoldState(args.compiled);
|
|
1308
|
+
if (resuming) for (const ev of prior) applyGraphFoldEvent(state, ev, args.compiled);
|
|
1309
|
+
return {
|
|
1310
|
+
runId: args.runId,
|
|
1311
|
+
journal,
|
|
1312
|
+
blobs,
|
|
1313
|
+
scope,
|
|
1314
|
+
abort,
|
|
1315
|
+
state,
|
|
1316
|
+
resuming,
|
|
1317
|
+
prior,
|
|
1318
|
+
engineSeq: resuming ? maxEngineSeq(prior) + 1 : 0,
|
|
1319
|
+
engineWokenSeq: ENGINE_WOKEN_SEQ_BASE + (resuming ? prior.filter((ev) => ev.kind === "woken" && ev.seq >= 1e7).length : 0),
|
|
1320
|
+
ledgerSeq: prior.filter((ev) => ev.kind === "edge").length
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
function maxEngineSeq(events) {
|
|
1324
|
+
let max = -1;
|
|
1325
|
+
for (const ev of events) if ((ev.kind === "node-inputs-resolved" || ev.kind === "edge-verdict" || ev.kind === "join-state" || ev.kind === "waiting" && ev.spec.kind === "token") && ev.seq > max) max = ev.seq;
|
|
1326
|
+
return max;
|
|
1327
|
+
}
|
|
1328
|
+
//#endregion
|
|
1329
|
+
//#region src/runtime/graph/suspension.ts
|
|
1330
|
+
/**
|
|
1331
|
+
* Suspensions (agent-runtime#976): a node parks on a host wake as the kernel's `waiting`/`woken`
|
|
1332
|
+
* pair. The engine owns the transition table; a host owns only how the wake arrives.
|
|
1333
|
+
*/
|
|
1334
|
+
const SUSPEND_MARK = "__graphSuspension";
|
|
1335
|
+
/** Build a suspension request. `wait` never expires; `fail` settles the node down at its deadline;
|
|
1336
|
+
* `default` resolves with the given payload. */
|
|
1337
|
+
function suspended(options = {}) {
|
|
1338
|
+
const onExpire = options.onExpire ?? "wait";
|
|
1339
|
+
if (onExpire !== "wait" && options.expiresInMs === void 0) throw new ValidationError(`suspended: onExpire '${onExpire}' requires expiresInMs`);
|
|
1340
|
+
if (onExpire === "wait" && options.expiresInMs !== void 0) throw new ValidationError("suspended: onExpire 'wait' never expires — remove expiresInMs");
|
|
1341
|
+
if (onExpire === "default" && options.default === void 0) throw new ValidationError("suspended: onExpire 'default' requires a default payload");
|
|
1342
|
+
return {
|
|
1343
|
+
[SUSPEND_MARK]: true,
|
|
1344
|
+
onExpire,
|
|
1345
|
+
...options.expiresInMs !== void 0 ? { expiresInMs: options.expiresInMs } : {},
|
|
1346
|
+
...options.default !== void 0 ? { default: options.default } : {}
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
/** Whether a node's output is a park request rather than its result. */
|
|
1350
|
+
function isSuspensionRequest(value) {
|
|
1351
|
+
return typeof value === "object" && value !== null && value[SUSPEND_MARK] === true;
|
|
1352
|
+
}
|
|
1353
|
+
/** Content-addressed over the run identity, so a restart recomputes it and needs no token table. */
|
|
1354
|
+
function mintSuspensionToken(runId, instance) {
|
|
1355
|
+
return contentAddress({
|
|
1356
|
+
runId,
|
|
1357
|
+
instance,
|
|
1358
|
+
kind: "graph-suspension"
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
/** The journal id one suspension's `waiting`/`woken` pair shares. */
|
|
1362
|
+
function suspensionNodeId(token) {
|
|
1363
|
+
return `graphwait:${token}`;
|
|
1364
|
+
}
|
|
1365
|
+
/** The token inside a suspension node id, or `undefined` for any other id. */
|
|
1366
|
+
function tokenFromSuspensionNodeId(id) {
|
|
1367
|
+
return id.startsWith("graphwait:") ? id.slice(10) : void 0;
|
|
1368
|
+
}
|
|
1369
|
+
//#endregion
|
|
1370
|
+
//#region src/runtime/graph/scheduler.ts
|
|
1371
|
+
/**
|
|
1372
|
+
* The engine scheduler (agent-runtime#980, durable per #974/#976): run a compiled graph by hosting
|
|
1373
|
+
* every node instance on one kernel `Scope`. The pool, the journal, the blob store and
|
|
1374
|
+
* cancellation are the kernel's; the scheduler owns only what a graph adds — releasing nodes over
|
|
1375
|
+
* guarded edges, delivering payloads and directives, the two cycle caps, and suspensions.
|
|
1376
|
+
*
|
|
1377
|
+
* DURABILITY — fold, never checkpoint. Every decision is journaled BEFORE its effect is visible
|
|
1378
|
+
* (blob-then-journal where a ref is minted), then applied to live state through the SAME reducer
|
|
1379
|
+
* (`applyGraphFoldEvent`) a restart replays the journal through. Kill the process at any journal
|
|
1380
|
+
* boundary and a restart re-enters the exact state: settled nodes restore from `outRef` and never
|
|
1381
|
+
* re-execute; a released-but-unspawned instance re-enters from its pinned `inputRef`; an in-flight
|
|
1382
|
+
* instance is in-doubt and re-enters per its kind's `onCrash`.
|
|
1383
|
+
*
|
|
1384
|
+
* The parts this file does NOT own, so it stays a loop and not a god object: payload admission
|
|
1385
|
+
* (`admit.ts`), the edge ledger (`ledger.ts`), the join rule (`join.ts`), suspension vocabulary
|
|
1386
|
+
* (`suspension.ts`), the journal/pool/scope bootstrap (`run-context.ts`), and result assembly
|
|
1387
|
+
* (`result.ts`).
|
|
1388
|
+
*/
|
|
1389
|
+
/** Run a graph to its result: `createGraphRun` awaited — the one-call form for a run that needs no
|
|
1390
|
+
* live host wakes. */
|
|
1391
|
+
async function runEngineGraph(engine, spec, task, options) {
|
|
1392
|
+
return createGraphRun(engine, spec, task, options).done;
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Start (or resume) a graph run and return its handle: await `done` for the result; deliver host
|
|
1396
|
+
* wakes through `resume`/`expire` while it runs (#976).
|
|
1397
|
+
*/
|
|
1398
|
+
function createGraphRun(engine, spec, task, options) {
|
|
1399
|
+
const compiled = asCompiled(engine, spec);
|
|
1400
|
+
const wakes = [];
|
|
1401
|
+
let signalWake = () => {};
|
|
1402
|
+
let finished = false;
|
|
1403
|
+
const done = runGraphLoop(engine, compiled, task, options, wakes, (fn) => {
|
|
1404
|
+
signalWake = fn;
|
|
1405
|
+
}).finally(() => {
|
|
1406
|
+
finished = true;
|
|
1407
|
+
});
|
|
1408
|
+
const queue = (token, payload, expire) => {
|
|
1409
|
+
if (finished) return Promise.reject(new ValidationError(`graph resume: run completed; start a new run over the same journal to wake '${token}'`));
|
|
1410
|
+
return new Promise((settle, fail) => {
|
|
1411
|
+
wakes.push({
|
|
1412
|
+
token,
|
|
1413
|
+
payload,
|
|
1414
|
+
expire,
|
|
1415
|
+
settle,
|
|
1416
|
+
fail
|
|
1417
|
+
});
|
|
1418
|
+
signalWake();
|
|
1419
|
+
});
|
|
1420
|
+
};
|
|
1421
|
+
return {
|
|
1422
|
+
done,
|
|
1423
|
+
resume: (token, payload) => queue(token, payload, false),
|
|
1424
|
+
expire: (token) => queue(token, void 0, true)
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
function asCompiled(engine, spec) {
|
|
1428
|
+
return "nodes" in spec && spec.nodes instanceof Map ? spec : compileGraph(engine, spec);
|
|
1429
|
+
}
|
|
1430
|
+
/** Everything a run must be able to satisfy before it spends anything. */
|
|
1431
|
+
function assertRunnable(engine, compiled, options) {
|
|
1432
|
+
const context = "runEngineGraph";
|
|
1433
|
+
for (const node of compiled.nodes.values()) {
|
|
1434
|
+
const missing = node.kind.effects.filter((name) => !(name in engine.effects));
|
|
1435
|
+
if (missing.length > 0) throw new ValidationError(`${context}: node ${node.id} needs effect(s) ${missing.join(", ")} the host did not provide`);
|
|
1436
|
+
if (node.spec.budget === void 0 && options.perNode === void 0) throw new ValidationError(`${context}: node ${node.id} declares no budget and options.perNode is absent — the engine invents no split`);
|
|
1437
|
+
}
|
|
1438
|
+
for (const edge of compiled.edges) if (isEngineFired(edge) && edge.spec.directive !== void 0 && options.prompts === void 0) throw new ValidationError(`${context}: edge ${edge.id} carries a directive but options.prompts is absent`);
|
|
1439
|
+
}
|
|
1440
|
+
async function runGraphLoop(engine, compiled, task, options, wakes, onWakeSignal) {
|
|
1441
|
+
assertRunnable(engine, compiled, options);
|
|
1442
|
+
const now = options.now ?? Date.now;
|
|
1443
|
+
const runId = options.runId ?? `graph:${contentAddress({ task }).slice(0, 18)}`;
|
|
1444
|
+
let detachOuterAbort = () => {};
|
|
1445
|
+
const context = await openGraphRun({
|
|
1446
|
+
compiled,
|
|
1447
|
+
runId,
|
|
1448
|
+
budget: options.budget,
|
|
1449
|
+
...options.journal !== void 0 ? { journal: options.journal } : {},
|
|
1450
|
+
...options.blobs !== void 0 ? { blobs: options.blobs } : {},
|
|
1451
|
+
now,
|
|
1452
|
+
...options.resume !== void 0 ? { resume: options.resume } : {},
|
|
1453
|
+
...options.signal !== void 0 ? { signal: options.signal } : {},
|
|
1454
|
+
onAbort: (detach) => {
|
|
1455
|
+
detachOuterAbort = detach;
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
const { abort, blobs, journal, scope, state } = context;
|
|
1459
|
+
const ledger = createEdgeLedger({
|
|
1460
|
+
journal,
|
|
1461
|
+
runId,
|
|
1462
|
+
now,
|
|
1463
|
+
startSeq: context.ledgerSeq
|
|
1464
|
+
});
|
|
1465
|
+
let engineSeq = context.engineSeq;
|
|
1466
|
+
let wokenSeq = context.engineWokenSeq;
|
|
1467
|
+
const liveHandles = /* @__PURE__ */ new Map();
|
|
1468
|
+
const waitingForBudget = [];
|
|
1469
|
+
const outCache = /* @__PURE__ */ new Map();
|
|
1470
|
+
let liveCount = 0;
|
|
1471
|
+
let failure;
|
|
1472
|
+
/** Journal one engine event, then apply it through the reducer a restart will replay. */
|
|
1473
|
+
const emit = async (ev) => {
|
|
1474
|
+
await journal.appendEvent(runId, ev);
|
|
1475
|
+
applyGraphFoldEvent(state, ev, compiled);
|
|
1476
|
+
};
|
|
1477
|
+
const stamp = () => new Date(now()).toISOString();
|
|
1478
|
+
const fail = (reason, name, message) => {
|
|
1479
|
+
failure = {
|
|
1480
|
+
reason,
|
|
1481
|
+
error: {
|
|
1482
|
+
name,
|
|
1483
|
+
message
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
abort.abort(`${reason}: ${message}`);
|
|
1487
|
+
};
|
|
1488
|
+
/** The ONE entry path: spawn a released instance from its journaled envelope. Used by a fresh
|
|
1489
|
+
* release, a budget-parked retry, and a restart re-entry alike. */
|
|
1490
|
+
const spawnInstance = async (label) => {
|
|
1491
|
+
if (failure) return;
|
|
1492
|
+
const instance = state.instances.get(label);
|
|
1493
|
+
const node = instance === void 0 ? void 0 : compiled.nodes.get(instance.node);
|
|
1494
|
+
if (!instance || !node || instance.inputRef === void 0) throw new ValidationError(`runEngineGraph: instance ${label} has no journaled envelope`);
|
|
1495
|
+
const envelope = await blobs.get(instance.inputRef);
|
|
1496
|
+
if (envelope === void 0) throw new ValidationError(`runEngineGraph: envelope ${instance.inputRef} is not in the blob store`);
|
|
1497
|
+
const agent = node.kind.run({
|
|
1498
|
+
config: node.config,
|
|
1499
|
+
profile: {
|
|
1500
|
+
name: node.id,
|
|
1501
|
+
...node.spec.profile ?? {}
|
|
1502
|
+
},
|
|
1503
|
+
inputs: envelope.inputs,
|
|
1504
|
+
effects: narrowEffects(node.kind.effects, engine.effects, `runEngineGraph: node ${node.id}`)
|
|
1505
|
+
});
|
|
1506
|
+
const budget = node.spec.budget ?? options.perNode;
|
|
1507
|
+
const spawned = scope.spawn(agent, envelope.task, {
|
|
1508
|
+
label,
|
|
1509
|
+
budget
|
|
1510
|
+
});
|
|
1511
|
+
if (spawned.ok) {
|
|
1512
|
+
liveCount += 1;
|
|
1513
|
+
liveHandles.set(spawned.handle.id, label);
|
|
1514
|
+
applyGraphFoldEvent(state, {
|
|
1515
|
+
kind: "spawned",
|
|
1516
|
+
id: spawned.handle.id,
|
|
1517
|
+
label,
|
|
1518
|
+
budget,
|
|
1519
|
+
runtime: "inline",
|
|
1520
|
+
seq: 0,
|
|
1521
|
+
at: ""
|
|
1522
|
+
}, compiled);
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
if (spawned.reason === "budget-exhausted" || spawned.reason === "max-live-workers") {
|
|
1526
|
+
waitingForBudget.push(label);
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
fail("driver-failed", "SpawnRefused", `node ${node.id}: ${spawned.reason}`);
|
|
1530
|
+
};
|
|
1531
|
+
/** Pin an instance's envelope (inputs + task) by content address; answers the instance label. */
|
|
1532
|
+
const openInstance = async (nodeId, visit, envelope) => {
|
|
1533
|
+
const label = `${nodeId}#${visit}`;
|
|
1534
|
+
const inputRef = contentAddress(envelope);
|
|
1535
|
+
await blobs.put(inputRef, envelope);
|
|
1536
|
+
await emit({
|
|
1537
|
+
kind: "node-inputs-resolved",
|
|
1538
|
+
id: label,
|
|
1539
|
+
node: nodeId,
|
|
1540
|
+
instance: label,
|
|
1541
|
+
inputRef,
|
|
1542
|
+
seq: engineSeq++,
|
|
1543
|
+
at: stamp()
|
|
1544
|
+
});
|
|
1545
|
+
return label;
|
|
1546
|
+
};
|
|
1547
|
+
const enterEntryNode = async (nodeId) => {
|
|
1548
|
+
const node = compiled.nodes.get(nodeId);
|
|
1549
|
+
const folded = state.nodes.get(nodeId);
|
|
1550
|
+
if (!node || !folded) return;
|
|
1551
|
+
const visit = folded.visits + 1;
|
|
1552
|
+
if (visit > node.maxVisits) return;
|
|
1553
|
+
await spawnInstance(await openInstance(nodeId, visit, {
|
|
1554
|
+
task,
|
|
1555
|
+
inputs: {}
|
|
1556
|
+
}));
|
|
1557
|
+
};
|
|
1558
|
+
/** Build the released instance's envelope from its wave: data payloads projected and admitted,
|
|
1559
|
+
* directives resolved, trace refs lined up — every consumption ledgered as it is taken. */
|
|
1560
|
+
const consumeWave = async (nodeId, consuming) => {
|
|
1561
|
+
const inputs = {};
|
|
1562
|
+
const directives = [];
|
|
1563
|
+
const traces = [];
|
|
1564
|
+
for (const { edge, folded } of consuming) {
|
|
1565
|
+
const spec = edge.spec;
|
|
1566
|
+
const traversal = (folded?.traversals ?? 0) + 1;
|
|
1567
|
+
if (spec.kind === "data" && folded?.state === "satisfied") {
|
|
1568
|
+
let payload = folded.payloadRef === void 0 ? void 0 : outCache.get(folded.payloadRef) ?? admitPayload(await blobs.get(folded.payloadRef));
|
|
1569
|
+
let outcome = "delivered";
|
|
1570
|
+
let reason;
|
|
1571
|
+
if (spec.projection !== void 0) try {
|
|
1572
|
+
payload = admitPayload(applyProjection(payload, spec.projection, `edge ${edge.id}`));
|
|
1573
|
+
} catch (error) {
|
|
1574
|
+
outcome = "empty";
|
|
1575
|
+
reason = error instanceof Error ? error.message : String(error);
|
|
1576
|
+
payload = void 0;
|
|
1577
|
+
}
|
|
1578
|
+
if (payload === void 0) outcome = "empty";
|
|
1579
|
+
await ledger.record(edge, traversal, outcome, reason);
|
|
1580
|
+
if (payload !== void 0) inputs[edge.toPort] = payload;
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
if (spec.directive !== void 0 && options.prompts !== void 0) directives.push(options.prompts.resolve(spec.directive).text);
|
|
1584
|
+
if (spec.kind === "analyzes" && folded?.state === "satisfied") traces.push(`trace of ${spec.from.node}: ${folded.payloadRef ?? "(no traceRef)"}`);
|
|
1585
|
+
await ledger.record(edge, traversal, "delivered");
|
|
1586
|
+
}
|
|
1587
|
+
const composed = [
|
|
1588
|
+
nodeId === compiled.root ? task : "",
|
|
1589
|
+
...directives,
|
|
1590
|
+
...traces
|
|
1591
|
+
].filter((part) => part.length > 0).join("\n\n");
|
|
1592
|
+
return {
|
|
1593
|
+
task: composed.length > 0 ? composed : task,
|
|
1594
|
+
inputs
|
|
1595
|
+
};
|
|
1596
|
+
};
|
|
1597
|
+
const release = async (nodeId, consuming, consumedPending) => {
|
|
1598
|
+
const node = compiled.nodes.get(nodeId);
|
|
1599
|
+
const folded = state.nodes.get(nodeId);
|
|
1600
|
+
if (!node || !folded) return;
|
|
1601
|
+
const visit = folded.visits + 1;
|
|
1602
|
+
if (visit > node.maxVisits) {
|
|
1603
|
+
fail("cycle-budget-exceeded", "GraphCycleBudget", `node ${nodeId} entered ${visit} times; maxVisits ${node.maxVisits}`);
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
const envelope = await consumeWave(nodeId, consuming);
|
|
1607
|
+
const label = await openInstance(nodeId, visit, envelope);
|
|
1608
|
+
await emit({
|
|
1609
|
+
kind: "join-state",
|
|
1610
|
+
id: label,
|
|
1611
|
+
node: nodeId,
|
|
1612
|
+
rule: node.join,
|
|
1613
|
+
satisfiedBy: consuming.map(({ edge }) => edge.id),
|
|
1614
|
+
consumedPending: [...consumedPending],
|
|
1615
|
+
instance: label,
|
|
1616
|
+
seq: engineSeq++,
|
|
1617
|
+
at: stamp()
|
|
1618
|
+
});
|
|
1619
|
+
await spawnInstance(label);
|
|
1620
|
+
};
|
|
1621
|
+
const tryRelease = async (nodeId) => {
|
|
1622
|
+
const node = compiled.nodes.get(nodeId);
|
|
1623
|
+
const folded = state.nodes.get(nodeId);
|
|
1624
|
+
if (!node || !folded || folded.blocked || failure) return;
|
|
1625
|
+
const gating = node.inbound.map((edge) => ({
|
|
1626
|
+
edge,
|
|
1627
|
+
folded: state.edges.get(edge.id)
|
|
1628
|
+
}));
|
|
1629
|
+
const decision = decideJoin(node.join, gating);
|
|
1630
|
+
if (decision.blocked) folded.blocked = true;
|
|
1631
|
+
if (!decision.release) return;
|
|
1632
|
+
for (const entry of decision.consuming) {
|
|
1633
|
+
const cap = entry.edge.spec.maxTraversals;
|
|
1634
|
+
if (cap !== void 0 && (entry.folded?.traversals ?? 0) >= cap) {
|
|
1635
|
+
await ledger.record(entry.edge, (entry.folded?.traversals ?? 0) + 1, "unpropagated", `traversal-cap-exhausted (max ${cap})`);
|
|
1636
|
+
await emit({
|
|
1637
|
+
kind: "edge-verdict",
|
|
1638
|
+
id: `graph:${nodeId}`,
|
|
1639
|
+
edge: entry.edge.id,
|
|
1640
|
+
fired: false,
|
|
1641
|
+
sourceStatus: "done",
|
|
1642
|
+
capped: true,
|
|
1643
|
+
seq: engineSeq++,
|
|
1644
|
+
at: stamp()
|
|
1645
|
+
});
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
const consumedPending = gating.filter((entry) => entry.folded?.state === "pending" && hasLiveInstance(entry.edge.spec.from.node)).map((entry) => entry.edge.id);
|
|
1650
|
+
await release(nodeId, decision.consuming, consumedPending);
|
|
1651
|
+
};
|
|
1652
|
+
const hasLiveInstance = (nodeId) => {
|
|
1653
|
+
for (const label of liveHandles.values()) if (state.instances.get(label)?.node === nodeId) return true;
|
|
1654
|
+
return false;
|
|
1655
|
+
};
|
|
1656
|
+
/** Journal a verdict for every unaccounted outbound edge of a settle, then try the joins. */
|
|
1657
|
+
const propagate = async (label, settle) => {
|
|
1658
|
+
const node = compiled.nodes.get(settle.node);
|
|
1659
|
+
if (!node) return;
|
|
1660
|
+
const succeeded = settle.status === "done" && settle.valid !== false;
|
|
1661
|
+
const sourceSettles = state.nodes.get(settle.node)?.settles.length ?? 0;
|
|
1662
|
+
for (const edge of node.outbound) {
|
|
1663
|
+
if (!isEngineFired(edge)) continue;
|
|
1664
|
+
const folded = state.edges.get(edge.id);
|
|
1665
|
+
if (!folded || folded.judgedSourceSettles >= sourceSettles) continue;
|
|
1666
|
+
let fired = false;
|
|
1667
|
+
let inputRef;
|
|
1668
|
+
if (succeeded) {
|
|
1669
|
+
fired = edge.spec.guard === void 0 || evaluateCondition(edge.spec.guard, {
|
|
1670
|
+
node: settle.node,
|
|
1671
|
+
out: settle.out,
|
|
1672
|
+
visits: state.nodes.get(settle.node)?.visits ?? 0,
|
|
1673
|
+
valid: settle.valid ?? true
|
|
1674
|
+
});
|
|
1675
|
+
if (fired) inputRef = edge.spec.kind === "analyzes" ? settle.traceRef : settle.outRef;
|
|
1676
|
+
}
|
|
1677
|
+
await emit({
|
|
1678
|
+
kind: "edge-verdict",
|
|
1679
|
+
id: label,
|
|
1680
|
+
edge: edge.id,
|
|
1681
|
+
fired,
|
|
1682
|
+
sourceStatus: settle.status === "down" ? "down" : succeeded ? "done" : "invalid",
|
|
1683
|
+
...inputRef !== void 0 ? { inputRef } : {},
|
|
1684
|
+
seq: engineSeq++,
|
|
1685
|
+
at: stamp()
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
for (const edge of node.outbound) if (isEngineFired(edge)) await tryRelease(edge.spec.to.node);
|
|
1689
|
+
};
|
|
1690
|
+
const park = async (label, request) => {
|
|
1691
|
+
const token = mintSuspensionToken(runId, label);
|
|
1692
|
+
let defaultRef;
|
|
1693
|
+
if (request.default !== void 0) {
|
|
1694
|
+
const admitted = admitPayload(request.default);
|
|
1695
|
+
defaultRef = contentAddress(admitted);
|
|
1696
|
+
await blobs.put(defaultRef, admitted);
|
|
1697
|
+
}
|
|
1698
|
+
await emit({
|
|
1699
|
+
kind: "waiting",
|
|
1700
|
+
id: suspensionNodeId(token),
|
|
1701
|
+
label,
|
|
1702
|
+
spec: {
|
|
1703
|
+
kind: "token",
|
|
1704
|
+
token,
|
|
1705
|
+
onExpire: request.onExpire,
|
|
1706
|
+
...request.expiresInMs !== void 0 ? { expiresAtMs: now() + request.expiresInMs } : {},
|
|
1707
|
+
...defaultRef !== void 0 ? { defaultRef } : {}
|
|
1708
|
+
},
|
|
1709
|
+
armedAt: now(),
|
|
1710
|
+
seq: engineSeq++,
|
|
1711
|
+
at: stamp()
|
|
1712
|
+
});
|
|
1713
|
+
};
|
|
1714
|
+
const wake = async (suspension, by, outRef) => {
|
|
1715
|
+
await emit({
|
|
1716
|
+
kind: "woken",
|
|
1717
|
+
id: suspensionNodeId(suspension.token),
|
|
1718
|
+
by,
|
|
1719
|
+
...outRef !== void 0 ? { outRef } : {},
|
|
1720
|
+
seq: wokenSeq++,
|
|
1721
|
+
at: stamp()
|
|
1722
|
+
});
|
|
1723
|
+
const settle = state.instances.get(suspension.instance)?.settle;
|
|
1724
|
+
if (settle === void 0) return;
|
|
1725
|
+
const out = settle.outRef === void 0 ? void 0 : admitPayload(await blobs.get(settle.outRef));
|
|
1726
|
+
if (settle.outRef !== void 0 && out !== void 0) outCache.set(settle.outRef, out);
|
|
1727
|
+
await propagate(suspension.instance, out === void 0 ? settle : {
|
|
1728
|
+
...settle,
|
|
1729
|
+
out
|
|
1730
|
+
});
|
|
1731
|
+
};
|
|
1732
|
+
/** Expiries are engine-clocked, so an offline run transitions them without a host sweep (#976). */
|
|
1733
|
+
const expireDue = async () => {
|
|
1734
|
+
let transitioned = false;
|
|
1735
|
+
for (const suspension of [...state.suspensions.values()]) {
|
|
1736
|
+
if (suspension.status !== "pending") continue;
|
|
1737
|
+
if (suspension.expiresAtMs === void 0 || now() < suspension.expiresAtMs) continue;
|
|
1738
|
+
if (suspension.onExpire === "fail") await wake(suspension, "expired");
|
|
1739
|
+
else if (suspension.onExpire === "default") await wake(suspension, "fired", suspension.defaultRef);
|
|
1740
|
+
else continue;
|
|
1741
|
+
transitioned = true;
|
|
1742
|
+
}
|
|
1743
|
+
return transitioned;
|
|
1744
|
+
};
|
|
1745
|
+
const drainWakes = async () => {
|
|
1746
|
+
while (wakes.length > 0) {
|
|
1747
|
+
const request = wakes.shift();
|
|
1748
|
+
if (!request) return;
|
|
1749
|
+
try {
|
|
1750
|
+
const suspension = state.suspensions.get(request.token);
|
|
1751
|
+
if (!suspension) throw new ValidationError(`graph resume: unknown token '${request.token}'`);
|
|
1752
|
+
if (suspension.status !== "pending") throw new ValidationError(`graph resume: token '${request.token}' already woken`);
|
|
1753
|
+
if (request.expire) await wake(suspension, suspension.onExpire === "default" ? "fired" : "expired", suspension.onExpire === "default" ? suspension.defaultRef : void 0);
|
|
1754
|
+
else {
|
|
1755
|
+
const admitted = admitPayload(request.payload);
|
|
1756
|
+
const outRef = contentAddress(admitted);
|
|
1757
|
+
await blobs.put(outRef, admitted);
|
|
1758
|
+
outCache.set(outRef, admitted);
|
|
1759
|
+
await wake(suspension, "fired", outRef);
|
|
1760
|
+
}
|
|
1761
|
+
request.settle();
|
|
1762
|
+
} catch (error) {
|
|
1763
|
+
request.fail(error);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
};
|
|
1767
|
+
const handleSettle = async (settled) => {
|
|
1768
|
+
const label = liveHandles.get(settled.handle.id);
|
|
1769
|
+
if (label === void 0) return;
|
|
1770
|
+
liveHandles.delete(settled.handle.id);
|
|
1771
|
+
liveCount -= 1;
|
|
1772
|
+
const instance = state.instances.get(label);
|
|
1773
|
+
const node = instance === void 0 ? void 0 : compiled.nodes.get(instance.node);
|
|
1774
|
+
if (!instance || !node) return;
|
|
1775
|
+
if (settled.kind === "done" && isSuspensionRequest(settled.out)) {
|
|
1776
|
+
await park(label, settled.out);
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
applyGraphFoldEvent(state, {
|
|
1780
|
+
kind: "settled",
|
|
1781
|
+
id: settled.handle.id,
|
|
1782
|
+
status: settled.kind === "done" ? "done" : "down",
|
|
1783
|
+
...settled.kind === "done" ? { outRef: settled.outRef } : {},
|
|
1784
|
+
...settled.kind === "down" ? { reason: settled.reason } : {},
|
|
1785
|
+
...settled.kind === "done" && settled.trace?.status === "available" ? { trace: settled.trace } : {},
|
|
1786
|
+
spent: {
|
|
1787
|
+
iterations: 0,
|
|
1788
|
+
tokens: {
|
|
1789
|
+
input: 0,
|
|
1790
|
+
output: 0
|
|
1791
|
+
},
|
|
1792
|
+
usd: 0,
|
|
1793
|
+
ms: 0
|
|
1794
|
+
},
|
|
1795
|
+
seq: 0,
|
|
1796
|
+
at: ""
|
|
1797
|
+
}, compiled);
|
|
1798
|
+
const folded = state.instances.get(label)?.settle;
|
|
1799
|
+
if (!folded) return;
|
|
1800
|
+
let enriched = folded;
|
|
1801
|
+
if (settled.kind === "done") {
|
|
1802
|
+
const admitted = admitPayload(settled.out);
|
|
1803
|
+
if (settled.outRef !== void 0) outCache.set(settled.outRef, admitted);
|
|
1804
|
+
enriched = {
|
|
1805
|
+
...folded,
|
|
1806
|
+
out: admitted,
|
|
1807
|
+
...await checkDeliverable(node, admitted)
|
|
1808
|
+
};
|
|
1809
|
+
const settles = state.nodes.get(instance.node)?.settles;
|
|
1810
|
+
if (settles) settles[settles.length - 1] = enriched;
|
|
1811
|
+
state.instances.get(label).settle = enriched;
|
|
1812
|
+
}
|
|
1813
|
+
if (instance.node === compiled.root && enriched.status === "done" && enriched.valid === true) {
|
|
1814
|
+
abort.abort("root delivered");
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
await propagate(label, enriched);
|
|
1818
|
+
};
|
|
1819
|
+
const checkDeliverable = async (node, out) => {
|
|
1820
|
+
if (node.deliverable === void 0) return {};
|
|
1821
|
+
try {
|
|
1822
|
+
return { valid: await node.deliverable.check(out) };
|
|
1823
|
+
} catch {
|
|
1824
|
+
return { valid: false };
|
|
1825
|
+
}
|
|
1826
|
+
};
|
|
1827
|
+
if (!context.resuming) for (const id of compiled.entries) await enterEntryNode(id);
|
|
1828
|
+
else await reenterAfterCrash();
|
|
1829
|
+
/** Kill-anywhere re-entry: finish every half-done transition the crashed process left, from the
|
|
1830
|
+
* pinned envelopes and journaled settles only — never by re-deriving anything (#974). */
|
|
1831
|
+
async function reenterAfterCrash() {
|
|
1832
|
+
for (const instance of [...state.instances.values()]) {
|
|
1833
|
+
if (instance.status === "released") {
|
|
1834
|
+
await spawnInstance(instance.instance);
|
|
1835
|
+
continue;
|
|
1836
|
+
}
|
|
1837
|
+
if (instance.status !== "live") continue;
|
|
1838
|
+
const node = compiled.nodes.get(instance.node);
|
|
1839
|
+
const folded = state.nodes.get(instance.node);
|
|
1840
|
+
if (!node || !folded) continue;
|
|
1841
|
+
const visit = folded.visits + 1;
|
|
1842
|
+
if (visit > node.maxVisits) {
|
|
1843
|
+
fail("cycle-budget-exceeded", "GraphCycleBudget", `node ${instance.node} entered ${visit} times; maxVisits ${node.maxVisits}`);
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
instance.status = "down";
|
|
1847
|
+
const label = `${instance.node}#${visit}`;
|
|
1848
|
+
await emit({
|
|
1849
|
+
kind: "node-inputs-resolved",
|
|
1850
|
+
id: label,
|
|
1851
|
+
node: instance.node,
|
|
1852
|
+
instance: label,
|
|
1853
|
+
inputRef: instance.inputRef,
|
|
1854
|
+
seq: engineSeq++,
|
|
1855
|
+
at: stamp()
|
|
1856
|
+
});
|
|
1857
|
+
await spawnInstance(label);
|
|
1858
|
+
}
|
|
1859
|
+
for (const [nodeId, folded] of state.nodes) {
|
|
1860
|
+
const latest = folded.settles.at(-1);
|
|
1861
|
+
if (latest === void 0) continue;
|
|
1862
|
+
const label = `${nodeId}#${latest.visit}`;
|
|
1863
|
+
if (latest.status === "done" && latest.outRef !== void 0) {
|
|
1864
|
+
const out = admitPayload(await blobs.get(latest.outRef));
|
|
1865
|
+
if (isSuspensionRequest(out)) {
|
|
1866
|
+
folded.settles.pop();
|
|
1867
|
+
const instance = state.instances.get(label);
|
|
1868
|
+
if (instance) instance.settle = void 0;
|
|
1869
|
+
await park(label, out);
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
outCache.set(latest.outRef, out);
|
|
1873
|
+
const node = compiled.nodes.get(nodeId);
|
|
1874
|
+
await propagate(label, {
|
|
1875
|
+
...latest,
|
|
1876
|
+
out,
|
|
1877
|
+
...node === void 0 ? {} : await checkDeliverable(node, out)
|
|
1878
|
+
});
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
await propagate(label, latest);
|
|
1882
|
+
}
|
|
1883
|
+
for (const id of compiled.entries) if ((state.nodes.get(id)?.visits ?? 0) === 0) await enterEntryNode(id);
|
|
1884
|
+
for (const id of compiled.nodes.keys()) await tryRelease(id);
|
|
1885
|
+
}
|
|
1886
|
+
const allTerminalsSettled = () => compiled.terminals.every((id) => (state.nodes.get(id)?.settles.length ?? 0) > 0);
|
|
1887
|
+
let wakeSignal = Promise.resolve();
|
|
1888
|
+
const rearmWakeSignal = () => {
|
|
1889
|
+
wakeSignal = new Promise((fire) => {
|
|
1890
|
+
onWakeSignal(fire);
|
|
1891
|
+
});
|
|
1892
|
+
};
|
|
1893
|
+
rearmWakeSignal();
|
|
1894
|
+
let pendingNext;
|
|
1895
|
+
while (!failure) {
|
|
1896
|
+
if (await expireDue()) continue;
|
|
1897
|
+
if (wakes.length > 0) {
|
|
1898
|
+
await drainWakes();
|
|
1899
|
+
continue;
|
|
1900
|
+
}
|
|
1901
|
+
if (allTerminalsSettled()) break;
|
|
1902
|
+
if (liveCount === 0) {
|
|
1903
|
+
const parked = [...state.suspensions.values()].filter((suspension) => suspension.status === "pending");
|
|
1904
|
+
if (parked.length === 0) break;
|
|
1905
|
+
if (options.waitForWakes) {
|
|
1906
|
+
rearmWakeSignal();
|
|
1907
|
+
await wakeSignal;
|
|
1908
|
+
continue;
|
|
1909
|
+
}
|
|
1910
|
+
const defaulting = parked.filter((suspension) => suspension.onExpire === "default");
|
|
1911
|
+
if (defaulting.length === 0) break;
|
|
1912
|
+
for (const suspension of defaulting) await wake(suspension, "fired", suspension.defaultRef);
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
pendingNext ??= scope.next();
|
|
1916
|
+
const raced = await Promise.race([pendingNext.then((settle) => ({ settle })), wakeSignal.then(() => "wake")]);
|
|
1917
|
+
if (raced === "wake") {
|
|
1918
|
+
rearmWakeSignal();
|
|
1919
|
+
continue;
|
|
1920
|
+
}
|
|
1921
|
+
pendingNext = void 0;
|
|
1922
|
+
if (raced.settle === null) break;
|
|
1923
|
+
await handleSettle(raced.settle);
|
|
1924
|
+
for (const label of waitingForBudget.splice(0)) await spawnInstance(label);
|
|
1925
|
+
}
|
|
1926
|
+
detachOuterAbort();
|
|
1927
|
+
abort.abort("graph loop complete");
|
|
1928
|
+
while (await (pendingNext ?? scope.next()) !== null) pendingNext = void 0;
|
|
1929
|
+
return assembleGraphResult({
|
|
1930
|
+
compiled,
|
|
1931
|
+
state,
|
|
1932
|
+
blobs,
|
|
1933
|
+
scope,
|
|
1934
|
+
outCache,
|
|
1935
|
+
ledger: ledger.entries,
|
|
1936
|
+
...options.finalizer !== void 0 ? { finalizer: options.finalizer } : {},
|
|
1937
|
+
...failure !== void 0 ? { failure } : {},
|
|
1938
|
+
aborted: options.signal?.aborted ?? false
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
//#endregion
|
|
1942
|
+
//#region src/runtime/graph/preset-run-graph.ts
|
|
1943
|
+
/**
|
|
1944
|
+
* The `runGraph` preset (agent-runtime#982, #975): today's `AgentGraph` compiled into an engine
|
|
1945
|
+
* graph and run by the engine. `runGraph` keeps its signature and becomes this call, so the six
|
|
1946
|
+
* consumers measured in #967 change nothing.
|
|
1947
|
+
*
|
|
1948
|
+
* WHAT THE ENGINE GRAPH LOOKS LIKE. One `supervisor` root carrying the whole graph, one pinned
|
|
1949
|
+
* `agent` node per worker, and the `delegates`/`analyzes` edges as authored. A `delegates` edge is
|
|
1950
|
+
* MODEL-fired (#971): its target is spawned by the supervisor through the coordination protocol,
|
|
1951
|
+
* with the node pin and the directive applied inside the kernel's authorized path. So the engine
|
|
1952
|
+
* schedules exactly one node — the supervisor — and the worker nodes are the declarative record of
|
|
1953
|
+
* what it may spawn. That record is what makes a `runGraph` graph a first-class engine graph:
|
|
1954
|
+
* compiled, type-checked, inspectable, and composable with `data` edges and `script` nodes.
|
|
1955
|
+
*
|
|
1956
|
+
* WHY THE ROOT NODE RUNS `superviseAgentGraph`. The engine's `supervisor` kind IS `supervisorAgent`
|
|
1957
|
+
* (#970), and the graph authority — pinning, the edge ledger, per-edge caps, continuity — already
|
|
1958
|
+
* lives in that one function. Executing it as the root node's body preserves every one of #967's
|
|
1959
|
+
* load-bearing properties by construction rather than by re-implementation.
|
|
1960
|
+
*/
|
|
1961
|
+
/** The preset's root kind: one node whose body is the graph's supervise run. */
|
|
1962
|
+
const RUN_GRAPH_KIND = "run-graph.supervisor";
|
|
1963
|
+
function spendOf(out) {
|
|
1964
|
+
return (out.ok ? out.result.result.spentTotal : void 0) ?? {
|
|
1965
|
+
iterations: 1,
|
|
1966
|
+
tokens: {
|
|
1967
|
+
input: 0,
|
|
1968
|
+
output: 0
|
|
1969
|
+
},
|
|
1970
|
+
usd: 0,
|
|
1971
|
+
ms: 0
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* The node kind behind the preset. Its executor runs the graph and reports the run's own measured
|
|
1976
|
+
* spend, so the engine pool debits what the graph actually used, never a second copy of it.
|
|
1977
|
+
*/
|
|
1978
|
+
function runGraphKind() {
|
|
1979
|
+
return {
|
|
1980
|
+
id: RUN_GRAPH_KIND,
|
|
1981
|
+
version: 1,
|
|
1982
|
+
description: "One AgentGraph supervise run as an engine node (the runGraph preset).",
|
|
1983
|
+
validateConfig: (raw, context) => {
|
|
1984
|
+
if (typeof raw !== "object" || raw === null) throw new ValidationError(`${context}: config must carry { graph, options }`);
|
|
1985
|
+
const config = raw;
|
|
1986
|
+
if (config.graph === void 0 || config.options === void 0 || config.run === void 0 || config.capture === void 0) throw new ValidationError(`${context}: config must carry { graph, options, run, capture }`);
|
|
1987
|
+
return config;
|
|
1988
|
+
},
|
|
1989
|
+
configSchema: {
|
|
1990
|
+
type: "object",
|
|
1991
|
+
properties: { graph: { type: "object" } },
|
|
1992
|
+
required: ["graph"]
|
|
1993
|
+
},
|
|
1994
|
+
inputs: [],
|
|
1995
|
+
outputs: [],
|
|
1996
|
+
effects: [],
|
|
1997
|
+
onCrash: "restart",
|
|
1998
|
+
budget: "metered",
|
|
1999
|
+
run: ({ config, profile }) => {
|
|
2000
|
+
let artifact;
|
|
2001
|
+
return {
|
|
2002
|
+
name: profile.name ?? "graph",
|
|
2003
|
+
act: () => Promise.reject(new ValidationError(`${RUN_GRAPH_KIND}: act() is not the execution path`)),
|
|
2004
|
+
executorSpec: {
|
|
2005
|
+
profile,
|
|
2006
|
+
harness: null,
|
|
2007
|
+
executor: {
|
|
2008
|
+
runtime: "inline",
|
|
2009
|
+
async execute() {
|
|
2010
|
+
const out = await config.run(config.graph, config.options, config.brain).then((result) => ({
|
|
2011
|
+
ok: true,
|
|
2012
|
+
result
|
|
2013
|
+
}), (error) => {
|
|
2014
|
+
config.capture.error = error;
|
|
2015
|
+
return {
|
|
2016
|
+
ok: false,
|
|
2017
|
+
error: String(error)
|
|
2018
|
+
};
|
|
2019
|
+
});
|
|
2020
|
+
artifact = {
|
|
2021
|
+
outRef: contentAddress({ runGraph: out.ok }),
|
|
2022
|
+
out,
|
|
2023
|
+
spent: spendOf(out)
|
|
2024
|
+
};
|
|
2025
|
+
return artifact;
|
|
2026
|
+
},
|
|
2027
|
+
teardown: () => Promise.resolve({ destroyed: true }),
|
|
2028
|
+
resultArtifact: () => {
|
|
2029
|
+
if (!artifact) throw new ValidationError(`${RUN_GRAPH_KIND}: resultArtifact() read before execute()`);
|
|
2030
|
+
return artifact;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
/** An engine with the preset kind registered beside the core `agent` kind the workers use. */
|
|
2039
|
+
function runGraphEngine() {
|
|
2040
|
+
return createGraphEngine({ coreKinds: [agentKind({}), runGraphKind()] });
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* Compile an `AgentGraph` into the engine graph that represents it: the supervisor root, one
|
|
2044
|
+
* pinned `agent` node per worker, and every authored edge. Pure — nothing runs, so a caller can
|
|
2045
|
+
* inspect, diff, or extend the result before handing it to the engine.
|
|
2046
|
+
*/
|
|
2047
|
+
function graphFromRunGraph(graph, options, run, capture = {}, brain) {
|
|
2048
|
+
const root = graph.nodes[0];
|
|
2049
|
+
if (root === void 0) throw new ValidationError("graphFromRunGraph: a graph needs a root node");
|
|
2050
|
+
return {
|
|
2051
|
+
nodes: graph.nodes.map((node, index) => index === 0 ? {
|
|
2052
|
+
id: node.id,
|
|
2053
|
+
kind: `${RUN_GRAPH_KIND}/v1`,
|
|
2054
|
+
config: {
|
|
2055
|
+
graph,
|
|
2056
|
+
options,
|
|
2057
|
+
run,
|
|
2058
|
+
capture,
|
|
2059
|
+
...brain !== void 0 ? { brain } : {}
|
|
2060
|
+
},
|
|
2061
|
+
profile: node.profile,
|
|
2062
|
+
budget: graph.budget,
|
|
2063
|
+
terminal: true,
|
|
2064
|
+
deliverable: { check: (out) => out?.ok === true }
|
|
2065
|
+
} : {
|
|
2066
|
+
id: node.id,
|
|
2067
|
+
kind: "agent/v1",
|
|
2068
|
+
profile: node.profile,
|
|
2069
|
+
entry: false,
|
|
2070
|
+
terminal: false,
|
|
2071
|
+
budget: options.perWorker ?? graph.budget
|
|
2072
|
+
}),
|
|
2073
|
+
edges: graph.edges.flatMap((edge) => edge.kind === "delegates" ? [{
|
|
2074
|
+
kind: "delegates",
|
|
2075
|
+
from: { node: edge.from },
|
|
2076
|
+
to: { node: edge.to },
|
|
2077
|
+
directive: edge.directive,
|
|
2078
|
+
...edge.maxTraversals !== void 0 ? { maxTraversals: edge.maxTraversals } : {}
|
|
2079
|
+
}] : edge.over.map((source) => ({
|
|
2080
|
+
kind: "analyzes",
|
|
2081
|
+
from: {
|
|
2082
|
+
node: source,
|
|
2083
|
+
port: "trace"
|
|
2084
|
+
},
|
|
2085
|
+
to: { node: edge.to },
|
|
2086
|
+
directive: edge.directive,
|
|
2087
|
+
...edge.maxTraversals !== void 0 ? { maxTraversals: edge.maxTraversals } : {}
|
|
2088
|
+
}))),
|
|
2089
|
+
root: root.id,
|
|
2090
|
+
deliverable: graph.deliverable
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Run an `AgentGraph` through the engine: compile it with {@link graphFromRunGraph}, schedule it,
|
|
2095
|
+
* and answer the caller in `runGraph`'s own vocabulary. A typed failure the graph threw — an
|
|
2096
|
+
* exhausted delegates cap — is rethrown as the exact error object it was, never flattened into a
|
|
2097
|
+
* settle reason.
|
|
2098
|
+
*/
|
|
2099
|
+
function runGraphThroughEngine(graph, options, run, brain) {
|
|
2100
|
+
const engine = runGraphEngine();
|
|
2101
|
+
const capture = {};
|
|
2102
|
+
const spec = graphFromRunGraph(graph, options, run, capture, brain);
|
|
2103
|
+
const root = spec.root;
|
|
2104
|
+
const engineRunId = `${options.runId ?? contentAddress({ graph: root }).slice(0, 18)}:engine`;
|
|
2105
|
+
return runEngineGraph(engine, compileGraph(engine, spec, "runGraph"), graphRootTask(graph), {
|
|
2106
|
+
budget: graph.budget,
|
|
2107
|
+
perNode: graph.budget,
|
|
2108
|
+
...options.journal !== void 0 ? { journal: options.journal } : {},
|
|
2109
|
+
...options.blobs !== void 0 ? { blobs: options.blobs } : {},
|
|
2110
|
+
prompts: options.registry ?? kernelPromptRegistry(),
|
|
2111
|
+
runId: engineRunId
|
|
2112
|
+
}).then((finished) => mapEngineResult(finished, root, capture));
|
|
2113
|
+
}
|
|
2114
|
+
function mapEngineResult(run, root, capture) {
|
|
2115
|
+
if (capture.error !== void 0) throw capture.error;
|
|
2116
|
+
const out = run.settles.find((entry) => entry.node === root)?.out;
|
|
2117
|
+
if (out === void 0) throw new ValidationError(`runGraph: the engine returned no settlement for root node '${root}' (${run.kind}${run.kind === "no-winner" ? `/${run.reason}` : ""})`);
|
|
2118
|
+
if (!out.ok) throw new ValidationError(`runGraph: ${out.error}`);
|
|
2119
|
+
return out.result;
|
|
2120
|
+
}
|
|
2121
|
+
/** The root's task line, as `runGraph` composes it today. */
|
|
2122
|
+
function graphRootTask(graph) {
|
|
2123
|
+
const root = graph.nodes[0];
|
|
2124
|
+
return root === void 0 ? "run the graph" : `run graph from '${root.id}'`;
|
|
2125
|
+
}
|
|
2126
|
+
//#endregion
|
|
2127
|
+
//#region src/runtime/supervise/graph.ts
|
|
2128
|
+
/**
|
|
2129
|
+
*
|
|
2130
|
+
* `runGraph` — agent graphs: profiles as nodes, registry-backed prompt directives as edges.
|
|
2131
|
+
*
|
|
2132
|
+
* A topology is PLAIN DATA an agent can author in a few lines: nodes are canonical
|
|
2133
|
+
* `AgentProfile`s (the ONLY way a node is described — no role-builder functions), edges are typed
|
|
2134
|
+
* values carrying versioned {@link PromptHandle} directives, `deliverable` (termination) and
|
|
2135
|
+
* `budget` (one conserved pool) are mandatory. Driver↔worker is the two-node cyclic instance;
|
|
2136
|
+
* "agent 3 analyzes 1 and 2 and reports to 1" is ONE edge, not a framework.
|
|
2137
|
+
*
|
|
2138
|
+
* NOT A SECOND SCHEDULER. `runGraph` is an interpretation layer over what already runs:
|
|
2139
|
+
* `supervise()` is the execution core — the same `supervisorAgent`/`driverAgent` machinery,
|
|
2140
|
+
* backend-derived worker path (authorized, classified, recursive), conserved-pool budget, and
|
|
2141
|
+
* deliverable-gated settlement every
|
|
2142
|
+
* supervised run uses. (`runAgentRounds` is deliberately NOT the substrate here.) What the graph
|
|
2143
|
+
* layer ADDS is exactly what a bespoke driver loop never
|
|
2144
|
+
* had:
|
|
2145
|
+
*
|
|
2146
|
+
* 1. **Node pinning** — a spawn names a node (`profile.name` = node id) and the node's canonical
|
|
2147
|
+
* profile is what runs; a driver cannot smuggle capabilities into a worker it did not define.
|
|
2148
|
+
* 2. **Observable edges** — every delegates/analyzes traversal lands in an EDGE LEDGER
|
|
2149
|
+
* (`delivered | stripped | empty | unpropagated`, with byte counts), in memory on
|
|
2150
|
+
* the result AND as `edge` events in the run journal. The motivating incident: a filter
|
|
2151
|
+
* silently replaced 1,700-char steering with 241 chars of boilerplate for three rounds and
|
|
2152
|
+
* NO artifact said so — an unobservable edge cannot be trusted and its directive cannot be
|
|
2153
|
+
* optimized.
|
|
2154
|
+
* 3. **Directives as data** — edge text lives in the prompt registry (`<surface>/v<n>`), so every
|
|
2155
|
+
* edge is a versioned optimization target, never prose hardcoded in a builder function.
|
|
2156
|
+
* 4. **Per-edge traversal caps** — the cyclic-graph backstop. A delegates edge whose cap is
|
|
2157
|
+
* exhausted REFUSES further traversals (fail loud), so a cycle cannot spin the pool dry.
|
|
2158
|
+
* 5. **Continuity as data** — a delegates edge may declare `continuity: 'resume'`, so each spawn
|
|
2159
|
+
* after the node's first re-attaches to its latest SETTLED session (the spawn context hands
|
|
2160
|
+
* the executor seam `resume: { ofWorker, sequence }`; the kernel keeps identity, ordering,
|
|
2161
|
+
* ledger truth, and the one conserved pool). Every ledger row states how its hop continued:
|
|
2162
|
+
* `'fresh' | 'resume'` for spawns, `'steer'` for mid-run deliveries — fresh respawns, session
|
|
2163
|
+
* resumes, and live steers are all plain data, each a ledgered fact.
|
|
2164
|
+
*
|
|
2165
|
+
* ORACLES ARE ENVIRONMENT, NEVER WORKERS. Graders/verifiers must not be spawnable in the graph —
|
|
2166
|
+
* a delegates edge to them leaks the rubric. An `analyzes` edge names its analyst in one of two
|
|
2167
|
+
* forms: a LENS id from the environment's registry (a pure function over trace evidence), or the
|
|
2168
|
+
* id of a graph NODE — a tool-equipped analyst AGENT spawned on each matching settle with the
|
|
2169
|
+
* node's pinned profile, whose settle output IS the findings. Either way the oracle doctrine
|
|
2170
|
+
* holds: an analyst node can never be a delegates target (refused loudly), so no driver can hand
|
|
2171
|
+
* it work, and an id living in both the registry and the nodes is refused as ambiguous.
|
|
2172
|
+
*
|
|
2173
|
+
* @experimental
|
|
2174
|
+
*/
|
|
2175
|
+
/** Default per-edge traversal cap — the cyclic-graph backstop when an edge names none. */
|
|
2176
|
+
const defaultEdgeTraversalCap = 32;
|
|
2177
|
+
/** A delegates edge exhausted its traversal cap and the run produced no winner: the cap, not the
|
|
2178
|
+
* task, ended it. Carries the full evidence so failing loud loses nothing. */
|
|
2179
|
+
var GraphEdgeCapError = class extends Error {
|
|
2180
|
+
exhaustedEdges;
|
|
2181
|
+
ledger;
|
|
2182
|
+
result;
|
|
2183
|
+
constructor(exhaustedEdges, ledger, result) {
|
|
2184
|
+
super(`runGraph: edge traversal cap exhausted on ${exhaustedEdges.join(", ")} and the run delivered no winner — the cap (the cyclic-graph backstop), not the task, ended this run. Raise maxTraversals on the edge or fix the cycle; the full edge ledger and the supervised result ride on this error.`);
|
|
2185
|
+
this.name = "GraphEdgeCapError";
|
|
2186
|
+
this.exhaustedEdges = exhaustedEdges;
|
|
2187
|
+
this.ledger = ledger;
|
|
2188
|
+
this.result = result;
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2191
|
+
/**
|
|
2192
|
+
* Forwarded to the root `supervise()` VERBATIM. Everything not owned, transformed, or refused
|
|
2193
|
+
* above belongs here — the default is "a graph honors it", not "someone remembered to add it".
|
|
2194
|
+
*/
|
|
2195
|
+
const GRAPH_FORWARDED_SUPERVISE_OPTIONS = [
|
|
2196
|
+
"backend",
|
|
2197
|
+
"rootHandle",
|
|
2198
|
+
"signal",
|
|
2199
|
+
"execution",
|
|
2200
|
+
"resolveDeliverable",
|
|
2201
|
+
"coordination",
|
|
2202
|
+
"peerMail",
|
|
2203
|
+
"driverBackend",
|
|
2204
|
+
"profileSecurity",
|
|
2205
|
+
"authorizeSpawn",
|
|
2206
|
+
"isDriverProfile",
|
|
2207
|
+
"router",
|
|
2208
|
+
"driveHarness",
|
|
2209
|
+
"driverRetry",
|
|
2210
|
+
"onDriverAttempt",
|
|
2211
|
+
"childSettleGraceMs",
|
|
2212
|
+
"resolveDriveHarness",
|
|
2213
|
+
"driveHarnessMaterialization",
|
|
2214
|
+
"resolveSupervisorTools",
|
|
2215
|
+
"extraTools",
|
|
2216
|
+
"executeExtraTool",
|
|
2217
|
+
"perWorker",
|
|
2218
|
+
"maxLiveWorkers",
|
|
2219
|
+
"watchWorkers",
|
|
2220
|
+
"stallAfterMs",
|
|
2221
|
+
"runDir",
|
|
2222
|
+
"probes",
|
|
2223
|
+
"stopRule",
|
|
2224
|
+
"onProgressStop",
|
|
2225
|
+
"maxDepth",
|
|
2226
|
+
"maxTurns",
|
|
2227
|
+
"compaction",
|
|
2228
|
+
"now",
|
|
2229
|
+
"allowedModels",
|
|
2230
|
+
"finalizer",
|
|
2231
|
+
"otel"
|
|
2232
|
+
];
|
|
2233
|
+
/** Copy every forwarded option the caller actually set. Absent stays absent: `supervise()` and the
|
|
2234
|
+
* graph must not disagree about what "unset" means. */
|
|
2235
|
+
function forwardedSuperviseOptions(opts) {
|
|
2236
|
+
const forwarded = {};
|
|
2237
|
+
for (const key of GRAPH_FORWARDED_SUPERVISE_OPTIONS) {
|
|
2238
|
+
const value = opts[key];
|
|
2239
|
+
if (value !== void 0) forwarded[key] = value;
|
|
2240
|
+
}
|
|
2241
|
+
return forwarded;
|
|
2242
|
+
}
|
|
2243
|
+
function edgeId(edge) {
|
|
2244
|
+
return edge.kind === "delegates" ? `delegates:${edge.from}->${edge.to}` : `analyzes:${edge.analyst}:${edge.over.join("+")}->${edge.to}`;
|
|
2245
|
+
}
|
|
2246
|
+
/** Validate the graph and resolve every directive BEFORE any compute is spent — an invalid
|
|
2247
|
+
* topology or an unknown directive is a configuration fault, never a mid-run surprise. */
|
|
2248
|
+
function validateGraph(graph, registry, analysts) {
|
|
2249
|
+
if (!Array.isArray(graph.nodes) || graph.nodes.length === 0) throw new ValidationError("runGraph: graph.nodes must be a non-empty array");
|
|
2250
|
+
if (!Array.isArray(graph.edges) || graph.edges.length === 0) throw new ValidationError("runGraph: graph.edges must be a non-empty array");
|
|
2251
|
+
if (typeof graph.deliverable?.check !== "function") throw new ValidationError("runGraph: graph.deliverable is mandatory (termination oracle)");
|
|
2252
|
+
if (typeof graph.budget !== "object" || graph.budget === null) throw new ValidationError("runGraph: graph.budget is mandatory (the conserved pool)");
|
|
2253
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2254
|
+
for (const node of graph.nodes) {
|
|
2255
|
+
if (typeof node.id !== "string" || node.id.length === 0) throw new ValidationError("runGraph: every node needs a non-empty string id");
|
|
2256
|
+
if (byId.has(node.id)) throw new ValidationError(`runGraph: duplicate node id '${node.id}'`);
|
|
2257
|
+
const parsed = agentProfileSchema.safeParse(node.profile);
|
|
2258
|
+
if (!parsed.success) throw new ValidationError(`runGraph: node '${node.id}' has an invalid AgentProfile: ${parsed.error.message}`);
|
|
2259
|
+
if (node.profile.name !== node.id) throw new ValidationError(`runGraph: node '${node.id}' has profile.name ${JSON.stringify(node.profile.name)} — profile.name IS the node identity (node pinning and analyst routing match on it) and must equal the node id`);
|
|
2260
|
+
byId.set(node.id, node);
|
|
2261
|
+
}
|
|
2262
|
+
const requireNode = (id, where) => {
|
|
2263
|
+
const node = byId.get(id);
|
|
2264
|
+
if (!node) throw new ValidationError(`runGraph: ${where} references unknown node '${id}'`);
|
|
2265
|
+
return node;
|
|
2266
|
+
};
|
|
2267
|
+
const delegates = graph.edges.filter((edge) => edge.kind === "delegates");
|
|
2268
|
+
const analyzes = graph.edges.filter((edge) => edge.kind === "analyzes");
|
|
2269
|
+
if (delegates.length === 0) throw new ValidationError("runGraph: at least one delegates edge is required (who spawns whom)");
|
|
2270
|
+
for (const edge of graph.edges) registry.resolve(edge.directive);
|
|
2271
|
+
for (const edge of delegates) {
|
|
2272
|
+
requireNode(edge.from, edgeId(edge));
|
|
2273
|
+
requireNode(edge.to, edgeId(edge));
|
|
2274
|
+
if (edge.from === edge.to) throw new ValidationError(`runGraph: ${edgeId(edge)} delegates to itself — the driver↔worker cycle is the settle-return loop, not a self-edge`);
|
|
2275
|
+
if (edge.continuity !== void 0 && edge.continuity !== "fresh" && edge.continuity !== "resume") throw new ValidationError(`runGraph: ${edgeId(edge)} has invalid continuity ${JSON.stringify(edge.continuity)} — a delegates edge's continuity is 'fresh' or 'resume'`);
|
|
2276
|
+
}
|
|
2277
|
+
const delegatedTo = new Set(delegates.map((edge) => edge.to));
|
|
2278
|
+
const roots = [...new Set(delegates.map((edge) => edge.from))].filter((id) => !delegatedTo.has(id));
|
|
2279
|
+
if (roots.length !== 1) throw new ValidationError(`runGraph: expected exactly ONE root (a node that delegates and is never delegated to), found ${roots.length === 0 ? "none — delegates edges form a cycle with no entry" : roots.join(", ")}. P0 executes driver↔worker(s); nested driver graphs are P3.`);
|
|
2280
|
+
const root = requireNode(roots[0], "root resolution");
|
|
2281
|
+
for (const edge of delegates) if (edge.from !== root.id) throw new ValidationError(`runGraph: ${edgeId(edge)} delegates from a non-root node — P0 executes one driver over its workers (the 2-node cyclic case, star-generalized); deeper delegation is P3`);
|
|
2282
|
+
const analystIds = /* @__PURE__ */ new Set();
|
|
2283
|
+
const analystNodes = /* @__PURE__ */ new Map();
|
|
2284
|
+
for (const edge of analyzes) {
|
|
2285
|
+
if (edge.continuity !== void 0) throw new ValidationError(`runGraph: ${edgeId(edge)} carries continuity — analysts are spawned by the analyst machinery (every analyst run is a fresh session over settled evidence), so continuity is a delegates-edge axis only`);
|
|
2286
|
+
if (analystIds.has(edge.analyst)) throw new ValidationError(`runGraph: two analyzes edges share analyst '${edge.analyst}' — one analyzes edge per analyst lens (traversals are ledgered by analyst id; a second edge would silently absorb the first's). Register the lens under a second id for a second edge.`);
|
|
2287
|
+
analystIds.add(edge.analyst);
|
|
2288
|
+
const analystNode = byId.get(edge.analyst);
|
|
2289
|
+
const inRegistry = analysts?.kinds.some((kind) => kind.id === edge.analyst) === true;
|
|
2290
|
+
if (analystNode !== void 0 && inRegistry) throw new ValidationError(`runGraph: ${edgeId(edge)} analyst '${edge.analyst}' is BOTH a graph node and a lens in the analysts registry — the id alone distinguishes the two analyst forms, so this is ambiguous; rename the node or register the lens under another id`);
|
|
2291
|
+
if (analystNode !== void 0) {
|
|
2292
|
+
if (analystNode.id === root.id) throw new ValidationError(`runGraph: ${edgeId(edge)} names the ROOT as its analyst — the root is the driver; give the analyst its own node with no delegates edge pointing at it`);
|
|
2293
|
+
if (delegatedTo.has(analystNode.id)) throw new ValidationError(`runGraph: ${edgeId(edge)} names node '${edge.analyst}' as its analyst, but that node is a delegates target — oracle doctrine: an analyst is never delegated to. An analyst NODE is legal only with NO delegates edge pointing at it; give the analyst its own delegates-free node or pass a lens id from RunGraphOptions.analysts.`);
|
|
2294
|
+
analystNodes.set(analystNode.id, analystNode);
|
|
2295
|
+
} else if (!analysts) throw new ValidationError(`runGraph: ${edgeId(edge)} analyst '${edge.analyst}' is not a graph node, and no RunGraphOptions.analysts registry was provided to resolve it as a lens`);
|
|
2296
|
+
else if (!inRegistry) throw new ValidationError(`runGraph: ${edgeId(edge)} analyst '${edge.analyst}' is neither a graph node nor in the analysts registry (known lenses: ${analysts.kinds.map((kind) => kind.id).join(", ") || "none"})`);
|
|
2297
|
+
if (edge.over.length === 0) throw new ValidationError(`runGraph: ${edgeId(edge)} must analyze at least one node`);
|
|
2298
|
+
for (const over of edge.over) {
|
|
2299
|
+
requireNode(over, edgeId(edge));
|
|
2300
|
+
if (over === root.id) throw new ValidationError(`runGraph: ${edgeId(edge)} analyzes the ROOT — analysts observe settled workers, and the root never settles as one, so this edge would silently never fire; list delegates-target nodes only`);
|
|
2301
|
+
}
|
|
2302
|
+
requireNode(edge.to, edgeId(edge));
|
|
2303
|
+
}
|
|
2304
|
+
for (const edge of analyzes) for (const over of edge.over) if (analystNodes.has(over)) throw new ValidationError(`runGraph: ${edgeId(edge)} analyzes '${over}', which is an analyst node — an analyst run settles as a finding, never as a worker, so this edge would silently never fire; analyst nodes are not analyzable`);
|
|
2305
|
+
const workers = /* @__PURE__ */ new Map();
|
|
2306
|
+
const delegatesByWorker = /* @__PURE__ */ new Map();
|
|
2307
|
+
for (const edge of delegates) {
|
|
2308
|
+
if (delegatesByWorker.has(edge.to)) throw new ValidationError(`runGraph: node '${edge.to}' is the target of two delegates edges — one delegation directive per worker node (version the directive instead of forking the edge)`);
|
|
2309
|
+
delegatesByWorker.set(edge.to, edge);
|
|
2310
|
+
workers.set(edge.to, requireNode(edge.to, edgeId(edge)));
|
|
2311
|
+
}
|
|
2312
|
+
for (const node of graph.nodes) if (node.id !== root.id && !workers.has(node.id) && !analystNodes.has(node.id)) throw new ValidationError(`runGraph: node '${node.id}' has no delegates edge to it — an unreachable node never runs`);
|
|
2313
|
+
return {
|
|
2314
|
+
root,
|
|
2315
|
+
workers,
|
|
2316
|
+
delegatesByWorker,
|
|
2317
|
+
analyzes,
|
|
2318
|
+
analystNodes
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
const byteLength = (text) => Buffer.byteLength(text, "utf8");
|
|
2322
|
+
function stringifyPayload(payload) {
|
|
2323
|
+
if (typeof payload === "string") return payload;
|
|
2324
|
+
try {
|
|
2325
|
+
return JSON.stringify(payload) ?? String(payload);
|
|
2326
|
+
} catch {
|
|
2327
|
+
return String(payload);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
/**
|
|
2331
|
+
* Execute an {@link AgentGraph}. The root node becomes the supervisor (`supervise()` — the
|
|
2332
|
+
* execution core), each worker node is spawnable BY NODE ID (`spawn_agent` with
|
|
2333
|
+
* `profile: { name: '<node id>' }`; the node's canonical profile is pinned by the graph), each
|
|
2334
|
+
* delegates directive is appended to the worker profile's `prompt.instructions` per traversal,
|
|
2335
|
+
* and each analyzes edge becomes an analyst-on-settle route with a real DESTINATION. Every
|
|
2336
|
+
* traversal is ledgered and journaled.
|
|
2337
|
+
*/
|
|
2338
|
+
function runGraph(graph, opts) {
|
|
2339
|
+
const { brain, ...runtimeOptions } = opts;
|
|
2340
|
+
assertRunGraphAuthoring(graph, runtimeOptions, brain);
|
|
2341
|
+
return runGraphThroughEngine(graph, runtimeOptions, superviseAgentGraph, brain);
|
|
2342
|
+
}
|
|
2343
|
+
/** Alias for graph tests written before `RunGraphOptions.brain` was production. The production
|
|
2344
|
+
* entry accepts the same shape; this wrapper only keeps the `/testing` import path working. */
|
|
2345
|
+
function runGraphWithTestBrain(graph, opts) {
|
|
2346
|
+
const { brain, ...runtimeOptions } = opts;
|
|
2347
|
+
assertRunGraphAuthoring(graph, runtimeOptions, brain);
|
|
2348
|
+
return runGraphThroughEngine(graph, runtimeOptions, superviseAgentGraph, brain);
|
|
2349
|
+
}
|
|
2350
|
+
/**
|
|
2351
|
+
* The graph supervise run, as its own entry: the engine's `run-graph` preset node executes exactly
|
|
2352
|
+
* this, so the preset and `runGraph` are the same code path by construction (agent-runtime#982).
|
|
2353
|
+
*/
|
|
2354
|
+
/**
|
|
2355
|
+
* Every refusal a graph earns before any compute, in one place: the authoring contract. `runGraph`
|
|
2356
|
+
* calls it FIRST so a malformed graph throws synchronously, exactly as it did before the engine
|
|
2357
|
+
* preset (agent-runtime#982) moved execution behind a promise.
|
|
2358
|
+
*/
|
|
2359
|
+
function assertRunGraphAuthoring(graph, opts, brain) {
|
|
2360
|
+
const validated = validateGraph(graph, opts.registry ?? kernelPromptRegistry(), opts.analysts);
|
|
2361
|
+
const { root } = validated;
|
|
2362
|
+
if (brain && opts.driverBackend) throw new ValidationError("runGraph: brain and driverBackend are mutually exclusive — a caller brain makes the root model calls, a driverBackend places a harness that makes its own");
|
|
2363
|
+
if (brain && harnessRunsAgent(root.profile.harness)) throw new ValidationError(`runGraph: root node '${root.id}' declares harness '${root.profile.harness}', so the harness drives it — a caller brain applies only to a router-brained root (profile.harness omitted or 'cli-base')`);
|
|
2364
|
+
if (!opts.backend && !opts.makeLeafAgent) throw new ValidationError("runGraph: provide opts.backend (where nodes run) or opts.makeLeafAgent");
|
|
2365
|
+
return validated;
|
|
2366
|
+
}
|
|
2367
|
+
function superviseAgentGraph(graph, opts, brain) {
|
|
2368
|
+
const registry = opts.registry ?? kernelPromptRegistry();
|
|
2369
|
+
const { root, workers, delegatesByWorker, analyzes, analystNodes } = assertRunGraphAuthoring(graph, opts, brain);
|
|
2370
|
+
const journal = opts.journal ?? new InMemorySpawnJournal();
|
|
2371
|
+
const blobs = opts.blobs ?? new InMemoryResultBlobStore();
|
|
2372
|
+
const runId = opts.runId ?? `graph-${canonicalCandidateDigest(graph.nodes.map((n) => n.id)).slice(7, 19)}`;
|
|
2373
|
+
const now = opts.now ?? Date.now;
|
|
2374
|
+
const ledger = [];
|
|
2375
|
+
const journaled = /* @__PURE__ */ new Set();
|
|
2376
|
+
const traversalCounts = /* @__PURE__ */ new Map();
|
|
2377
|
+
const exhausted = /* @__PURE__ */ new Set();
|
|
2378
|
+
const exhaustedDelegates = /* @__PURE__ */ new Set();
|
|
2379
|
+
const journalWrites = [];
|
|
2380
|
+
let ledgerSeq = 0;
|
|
2381
|
+
const appendJournal = (entry, nodeIdForEvent) => {
|
|
2382
|
+
if (journaled.has(entry)) return Promise.resolve();
|
|
2383
|
+
journaled.add(entry);
|
|
2384
|
+
const write = journal.appendEvent(runId, {
|
|
2385
|
+
kind: "edge",
|
|
2386
|
+
id: nodeIdForEvent,
|
|
2387
|
+
edge: {
|
|
2388
|
+
kind: entry.kind,
|
|
2389
|
+
from: entry.from,
|
|
2390
|
+
to: entry.to,
|
|
2391
|
+
directive: entry.directive
|
|
2392
|
+
},
|
|
2393
|
+
traversal: entry.traversal,
|
|
2394
|
+
outcome: entry.outcome,
|
|
2395
|
+
continuity: entry.continuity,
|
|
2396
|
+
bytes: entry.bytes,
|
|
2397
|
+
...entry.reason !== void 0 ? { reason: entry.reason } : {},
|
|
2398
|
+
seq: ledgerSeq++,
|
|
2399
|
+
at: new Date(now()).toISOString()
|
|
2400
|
+
});
|
|
2401
|
+
journalWrites.push(write);
|
|
2402
|
+
return write;
|
|
2403
|
+
};
|
|
2404
|
+
const record = (entry, journalNow) => {
|
|
2405
|
+
const count = (traversalCounts.get(entry.edge) ?? 0) + 1;
|
|
2406
|
+
traversalCounts.set(entry.edge, count);
|
|
2407
|
+
const row = {
|
|
2408
|
+
...entry,
|
|
2409
|
+
traversal: count
|
|
2410
|
+
};
|
|
2411
|
+
ledger.push(row);
|
|
2412
|
+
if (journalNow) appendJournal(row, row.workerId ?? `graph:${row.to}`);
|
|
2413
|
+
return row;
|
|
2414
|
+
};
|
|
2415
|
+
const nodeByWorkerId = /* @__PURE__ */ new Map();
|
|
2416
|
+
const pendingByAssignment = /* @__PURE__ */ new Map();
|
|
2417
|
+
const pinNode = (input) => {
|
|
2418
|
+
const requested = typeof input.profile?.name === "string" ? input.profile.name : void 0;
|
|
2419
|
+
if (input.analyst !== void 0) {
|
|
2420
|
+
const analystNode = analystNodes.get(input.analyst);
|
|
2421
|
+
if (!analystNode || requested !== analystNode.id) throw new ValidationError(`runGraph: analyst run for ${JSON.stringify(input.analyst)} does not name an analyst node of this graph (analyst nodes: ${[...analystNodes.keys()].join(", ") || "none"})`);
|
|
2422
|
+
return analystNode.profile;
|
|
2423
|
+
}
|
|
2424
|
+
const node = requested !== void 0 ? workers.get(requested) : void 0;
|
|
2425
|
+
if (!node) throw new ValidationError(`runGraph: spawn_agent named profile ${JSON.stringify(requested)} which is not a worker node of this graph (nodes: ${[...workers.keys()].join(", ")}). Spawn by node id: profile.name selects the node; the node profile itself is pinned by the graph.`);
|
|
2426
|
+
const edge = delegatesByWorker.get(node.id);
|
|
2427
|
+
const id = edgeId(edge);
|
|
2428
|
+
const cap = edge.maxTraversals ?? 32;
|
|
2429
|
+
const used = traversalCounts.get(id) ?? 0;
|
|
2430
|
+
const spawnContinuity = input.continuity ?? "fresh";
|
|
2431
|
+
if (used >= cap) {
|
|
2432
|
+
exhausted.add(id);
|
|
2433
|
+
exhaustedDelegates.add(id);
|
|
2434
|
+
record({
|
|
2435
|
+
edge: id,
|
|
2436
|
+
kind: "delegates",
|
|
2437
|
+
from: edge.from,
|
|
2438
|
+
to: edge.to,
|
|
2439
|
+
directive: formatPromptHandle(edge.directive),
|
|
2440
|
+
outcome: "unpropagated",
|
|
2441
|
+
continuity: spawnContinuity,
|
|
2442
|
+
bytes: 0,
|
|
2443
|
+
reason: `traversal-cap-exhausted (max ${cap})`
|
|
2444
|
+
}, true);
|
|
2445
|
+
throw new ValidationError(`runGraph: delegates edge ${id} exhausted its traversal cap (${cap}) — the cyclic-graph backstop refused this spawn`);
|
|
2446
|
+
}
|
|
2447
|
+
const directiveText = registry.resolve(edge.directive).text;
|
|
2448
|
+
const taskText = stringifyPayload(input.task);
|
|
2449
|
+
const bytes = byteLength(directiveText) + byteLength(taskText);
|
|
2450
|
+
const row = record({
|
|
2451
|
+
edge: id,
|
|
2452
|
+
kind: "delegates",
|
|
2453
|
+
from: edge.from,
|
|
2454
|
+
to: edge.to,
|
|
2455
|
+
directive: formatPromptHandle(edge.directive),
|
|
2456
|
+
outcome: bytes === 0 ? "empty" : "delivered",
|
|
2457
|
+
continuity: spawnContinuity,
|
|
2458
|
+
bytes,
|
|
2459
|
+
...bytes === 0 ? { reason: "no directive text and no task payload" } : {}
|
|
2460
|
+
}, false);
|
|
2461
|
+
pendingByAssignment.set(input.assignmentId, row);
|
|
2462
|
+
return directiveText.length === 0 ? node.profile : {
|
|
2463
|
+
...node.profile,
|
|
2464
|
+
prompt: {
|
|
2465
|
+
...node.profile.prompt ?? {},
|
|
2466
|
+
instructions: [...node.profile.prompt?.instructions ?? [], directiveText]
|
|
2467
|
+
}
|
|
2468
|
+
};
|
|
2469
|
+
};
|
|
2470
|
+
const resolveSpawnProfile = (authored) => {
|
|
2471
|
+
const requested = typeof authored.name === "string" ? authored.name : void 0;
|
|
2472
|
+
return ((requested !== void 0 ? workers.get(requested) : void 0) ?? (requested !== void 0 ? analystNodes.get(requested) : void 0))?.profile ?? authored;
|
|
2473
|
+
};
|
|
2474
|
+
const graphAuthorizeSpawn = (input) => {
|
|
2475
|
+
const pinned = pinNode(input);
|
|
2476
|
+
if (!opts.authorizeSpawn) return { profile: pinned };
|
|
2477
|
+
return opts.authorizeSpawn({
|
|
2478
|
+
...input,
|
|
2479
|
+
profile: pinned
|
|
2480
|
+
});
|
|
2481
|
+
};
|
|
2482
|
+
const routes = analyzes.map((edge) => {
|
|
2483
|
+
const analystNode = analystNodes.get(edge.analyst);
|
|
2484
|
+
if (analystNode) return {
|
|
2485
|
+
kind: edge.analyst,
|
|
2486
|
+
over: edge.over,
|
|
2487
|
+
agent: analystNode.profile,
|
|
2488
|
+
directive: registry.resolve(edge.directive).text,
|
|
2489
|
+
...edge.to === root.id ? {} : { to: edge.to }
|
|
2490
|
+
};
|
|
2491
|
+
return edge.to === root.id ? {
|
|
2492
|
+
kind: edge.analyst,
|
|
2493
|
+
over: edge.over
|
|
2494
|
+
} : {
|
|
2495
|
+
kind: edge.analyst,
|
|
2496
|
+
over: edge.over,
|
|
2497
|
+
to: edge.to,
|
|
2498
|
+
directive: registry.resolve(edge.directive).text
|
|
2499
|
+
};
|
|
2500
|
+
});
|
|
2501
|
+
const driverAnalyzesBriefs = analyzes.filter((edge) => edge.to === root.id).map((edge) => analystNodes.has(edge.analyst) ? `Findings from analyst '${edge.analyst}' (a tool-equipped analyst agent node, over: ${edge.over.join(", ")}) will arrive as finding events.` : `Findings from analyst '${edge.analyst}' (over: ${edge.over.join(", ")}) will arrive as finding events.\n${registry.resolve(edge.directive).text}`);
|
|
2502
|
+
const continuityByProfile = {};
|
|
2503
|
+
for (const [nodeId, edge] of delegatesByWorker) if (edge.continuity !== void 0) continuityByProfile[nodeId] = edge.continuity;
|
|
2504
|
+
const graphBrief = [
|
|
2505
|
+
"AGENT GRAPH: you are the driver node of a fixed topology. You may spawn ONLY these worker",
|
|
2506
|
+
"nodes, by EXACT name (spawn_agent with profile: { name: '<node id>' }; the node's full",
|
|
2507
|
+
"profile is pinned by the graph — any other profile fields you author are ignored):",
|
|
2508
|
+
...[...workers.values()].map((node) => {
|
|
2509
|
+
const edge = delegatesByWorker.get(node.id);
|
|
2510
|
+
const cap = edge.maxTraversals ?? 32;
|
|
2511
|
+
const description = typeof node.profile.description === "string" && node.profile.description.length > 0 ? ` — ${node.profile.description}` : "";
|
|
2512
|
+
const continuityNote = edge.continuity === "resume" ? "; continuity: resume — each spawn after the first re-attaches to this node's latest settled session (spawn again to continue it; steer while it is live)" : "";
|
|
2513
|
+
return `- '${node.id}'${description} (delegation cap: ${cap} traversals${continuityNote})`;
|
|
2514
|
+
}),
|
|
2515
|
+
...driverAnalyzesBriefs.length > 0 ? ["", ...driverAnalyzesBriefs] : []
|
|
2516
|
+
].join("\n");
|
|
2517
|
+
const rootProfile = {
|
|
2518
|
+
...root.profile,
|
|
2519
|
+
prompt: {
|
|
2520
|
+
...root.profile.prompt ?? {},
|
|
2521
|
+
instructions: [...root.profile.prompt?.instructions ?? [], graphBrief]
|
|
2522
|
+
}
|
|
2523
|
+
};
|
|
2524
|
+
const strippedByDigest = /* @__PURE__ */ new Map();
|
|
2525
|
+
const authorizeMessage = (input) => {
|
|
2526
|
+
if (!opts.authorizeMessage) return { instruction: input.instruction };
|
|
2527
|
+
const decision = opts.authorizeMessage(input);
|
|
2528
|
+
if (decision.instruction !== input.instruction) strippedByDigest.set(canonicalCandidateDigest(decision.instruction), { composedBytes: byteLength(input.instruction) });
|
|
2529
|
+
return decision;
|
|
2530
|
+
};
|
|
2531
|
+
const routedAnalyzesByAnalyst = /* @__PURE__ */ new Map();
|
|
2532
|
+
const driverAnalyzesByAnalyst = /* @__PURE__ */ new Map();
|
|
2533
|
+
for (const edge of analyzes) (edge.to === root.id ? driverAnalyzesByAnalyst : routedAnalyzesByAnalyst).set(edge.analyst, edge);
|
|
2534
|
+
const analyzesCapReached = (edge) => {
|
|
2535
|
+
const cap = edge.maxTraversals ?? 32;
|
|
2536
|
+
if ((traversalCounts.get(edgeId(edge)) ?? 0) < cap) return false;
|
|
2537
|
+
exhausted.add(edgeId(edge));
|
|
2538
|
+
return true;
|
|
2539
|
+
};
|
|
2540
|
+
const ledgerAnalyzes = (edge, outcome, bytes, reason, workerId) => {
|
|
2541
|
+
const capped = analyzesCapReached(edge);
|
|
2542
|
+
record({
|
|
2543
|
+
edge: edgeId(edge),
|
|
2544
|
+
kind: "analyzes",
|
|
2545
|
+
from: edge.over.join("+"),
|
|
2546
|
+
to: edge.to,
|
|
2547
|
+
directive: formatPromptHandle(edge.directive),
|
|
2548
|
+
outcome: capped ? "unpropagated" : outcome,
|
|
2549
|
+
continuity: "steer",
|
|
2550
|
+
bytes,
|
|
2551
|
+
...capped ? { reason: `traversal-cap-exhausted (max ${edge.maxTraversals ?? 32})` } : reason !== void 0 ? { reason } : {},
|
|
2552
|
+
...workerId !== void 0 ? { workerId } : {}
|
|
2553
|
+
}, true);
|
|
2554
|
+
};
|
|
2555
|
+
const onCoordinationEvent = async (_context, _eventId, recordEnvelope) => {
|
|
2556
|
+
const event = recordEnvelope.event;
|
|
2557
|
+
if (event.type === "finding") {
|
|
2558
|
+
const edge = driverAnalyzesByAnalyst.get(event.finding.analyst);
|
|
2559
|
+
if (!edge) return;
|
|
2560
|
+
const sourceNode = nodeByWorkerId.get(event.finding.fromWorker);
|
|
2561
|
+
if (sourceNode === void 0 || !edge.over.includes(sourceNode)) return;
|
|
2562
|
+
const findingsText = event.finding.findings === void 0 ? "" : stringifyPayload(event.finding.findings);
|
|
2563
|
+
const directiveBytes = byteLength(registry.resolve(edge.directive).text);
|
|
2564
|
+
const empty = findingsText.length === 0;
|
|
2565
|
+
ledgerAnalyzes(edge, empty ? "empty" : "delivered", directiveBytes + byteLength(findingsText), empty ? "analyst returned no findings" : void 0, event.finding.fromWorker);
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
if (event.type === "steer") {
|
|
2569
|
+
const down = event.down;
|
|
2570
|
+
if (event.analyst !== void 0) {
|
|
2571
|
+
const edge = routedAnalyzesByAnalyst.get(event.analyst);
|
|
2572
|
+
if (!edge) return;
|
|
2573
|
+
ledgerAnalyzes(edge, down.delivered ? "delivered" : "unpropagated", byteLength(down.instruction), down.delivered ? void 0 : down.outcome, down.toWorker);
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
const nodeId = nodeByWorkerId.get(down.toWorker);
|
|
2577
|
+
if (nodeId === void 0) return;
|
|
2578
|
+
const edge = delegatesByWorker.get(nodeId);
|
|
2579
|
+
if (!edge) return;
|
|
2580
|
+
const stripped = strippedByDigest.get(down.instructionDigest);
|
|
2581
|
+
record({
|
|
2582
|
+
edge: edgeId(edge),
|
|
2583
|
+
kind: "delegates",
|
|
2584
|
+
from: edge.from,
|
|
2585
|
+
to: edge.to,
|
|
2586
|
+
directive: formatPromptHandle(edge.directive),
|
|
2587
|
+
outcome: !down.delivered ? "unpropagated" : stripped ? "stripped" : "delivered",
|
|
2588
|
+
continuity: "steer",
|
|
2589
|
+
bytes: byteLength(down.instruction),
|
|
2590
|
+
...!down.delivered ? { reason: down.outcome } : stripped ? { reason: `authorization narrowed ${stripped.composedBytes} composed bytes` } : {},
|
|
2591
|
+
workerId: down.toWorker
|
|
2592
|
+
}, true);
|
|
2593
|
+
}
|
|
2594
|
+
};
|
|
2595
|
+
const hooks = composeRuntimeHooks({ onEvent: (event) => {
|
|
2596
|
+
if (event.target !== "agent.spawn" || event.phase !== "after") return;
|
|
2597
|
+
const payload = event.payload;
|
|
2598
|
+
if (typeof payload?.childId !== "string" || typeof payload.assignmentId !== "string") return;
|
|
2599
|
+
const pending = pendingByAssignment.get(payload.assignmentId);
|
|
2600
|
+
if (!pending) return;
|
|
2601
|
+
pendingByAssignment.delete(payload.assignmentId);
|
|
2602
|
+
const bound = {
|
|
2603
|
+
...pending,
|
|
2604
|
+
workerId: payload.childId
|
|
2605
|
+
};
|
|
2606
|
+
ledger[ledger.indexOf(pending)] = bound;
|
|
2607
|
+
nodeByWorkerId.set(payload.childId, bound.to);
|
|
2608
|
+
return appendJournal(bound, payload.childId);
|
|
2609
|
+
} }, opts.hooks);
|
|
2610
|
+
const start = async () => {
|
|
2611
|
+
const superviseOptions = {
|
|
2612
|
+
...forwardedSuperviseOptions(opts),
|
|
2613
|
+
rootDriverFromBackend: false,
|
|
2614
|
+
budget: graph.budget,
|
|
2615
|
+
deliverable: graph.deliverable,
|
|
2616
|
+
authorizeSpawn: graphAuthorizeSpawn,
|
|
2617
|
+
resolveSpawnProfile,
|
|
2618
|
+
...opts.makeLeafAgent ? { makeLeafAgent: opts.makeLeafAgent } : {},
|
|
2619
|
+
journal,
|
|
2620
|
+
blobs,
|
|
2621
|
+
runId,
|
|
2622
|
+
hooks,
|
|
2623
|
+
onCoordinationEvent,
|
|
2624
|
+
...routes.length > 0 ? {
|
|
2625
|
+
analyzeOnSettle: routes,
|
|
2626
|
+
...opts.analysts ? { analysts: opts.analysts } : {}
|
|
2627
|
+
} : {},
|
|
2628
|
+
...Object.keys(continuityByProfile).length > 0 ? { continuityByProfile } : {},
|
|
2629
|
+
authorizeMessage
|
|
2630
|
+
};
|
|
2631
|
+
const result = brain === void 0 ? await supervise(rootProfile, graphTask(graph, root), superviseOptions) : await superviseWithTestBrain(rootProfile, graphTask(graph, root), {
|
|
2632
|
+
...superviseOptions,
|
|
2633
|
+
brain
|
|
2634
|
+
});
|
|
2635
|
+
for (const pending of pendingByAssignment.values()) {
|
|
2636
|
+
const refused = {
|
|
2637
|
+
...pending,
|
|
2638
|
+
outcome: "unpropagated",
|
|
2639
|
+
bytes: 0,
|
|
2640
|
+
reason: `no-live-worker-bound (spawn refused after the factory, or a keyed re-spawn deduplicated to a completed result; ${pending.bytes} composed bytes never crossed)`
|
|
2641
|
+
};
|
|
2642
|
+
ledger[ledger.indexOf(pending)] = refused;
|
|
2643
|
+
await appendJournal(refused, `graph:${refused.to}`);
|
|
2644
|
+
}
|
|
2645
|
+
pendingByAssignment.clear();
|
|
2646
|
+
await Promise.all(journalWrites);
|
|
2647
|
+
const exhaustedEdges = Object.freeze([...exhausted]);
|
|
2648
|
+
const frozenLedger = Object.freeze(ledger.map((row) => Object.freeze({ ...row })));
|
|
2649
|
+
const lifecycleEnded = result.kind === "no-winner" && (result.reason === "aborted" || result.reason === "budget-exhausted");
|
|
2650
|
+
if (result.kind !== "winner" && !lifecycleEnded && exhaustedDelegates.size > 0) throw new GraphEdgeCapError(Object.freeze([...exhaustedDelegates]), frozenLedger, result);
|
|
2651
|
+
return {
|
|
2652
|
+
result,
|
|
2653
|
+
ledger: frozenLedger,
|
|
2654
|
+
exhaustedEdges,
|
|
2655
|
+
runId
|
|
2656
|
+
};
|
|
2657
|
+
};
|
|
2658
|
+
return start();
|
|
2659
|
+
}
|
|
2660
|
+
/** The root task: the graph's own framing. The deliverable (mandatory) is the termination; the
|
|
2661
|
+
* task names what the topology exists to produce. */
|
|
2662
|
+
function graphTask(graph, root) {
|
|
2663
|
+
return graph.deliverable.describe ?? `Deliver the graph's deliverable by driving your worker nodes (root: '${root.id}').`;
|
|
2664
|
+
}
|
|
2665
|
+
//#endregion
|
|
2666
|
+
export { supervisorKind as A, MAX_MAX_NODE_VISITS as B, applyGraphFoldEvent as C, agentKind as D, admitPayload as E, compileGraph as F, validateProjection as G, formatRegistryHandle as H, isEngineFired as I, validateCondition as J, CONDITION_OPS as K, schemaAccepts as L, kindHandle as M, narrowEffects as N, scriptKind as O, validateNodeKind as P, DEFAULT_MAX_NODE_VISITS as R, decideJoin as S, foldGraphJournal as T, parseRegistryHandle as U, createRegistry as V, applyProjection as W, ENGINE_WOKEN_SEQ_BASE as _, RUN_GRAPH_KIND as a, materializeSettles as b, runGraphKind as c, runEngineGraph as d, isSuspensionRequest as f, tokenFromSuspensionNodeId as g, suspensionNodeId as h, runGraphWithTestBrain as i, createGraphEngine as j, subgraphKind as k, runGraphThroughEngine as l, suspended as m, defaultEdgeTraversalCap as n, graphFromRunGraph as o, mintSuspensionToken as p, evaluateCondition as q, runGraph as r, runGraphEngine as s, GraphEdgeCapError as t, createGraphRun as u, openGraphRun as v, emptyFoldState as w, createEdgeLedger as x, assembleGraphResult as y, JOIN_RULES as z };
|
|
2667
|
+
|
|
2668
|
+
//# sourceMappingURL=graph-E0U0c2YP.js.map
|