@tangle-network/agent-runtime 0.162.0 → 0.164.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-DxGP0h_p.js → activation-CneAm4jL.js} +3 -3
- package/dist/{activation-DxGP0h_p.js.map → activation-CneAm4jL.js.map} +1 -1
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +2 -2
- package/dist/{authoring-kO7vE_c9.js → authoring-CZM22nUG.js} +2 -2
- package/dist/{authoring-kO7vE_c9.js.map → authoring-CZM22nUG.js.map} +1 -1
- package/dist/candidate-execution/index.js +4 -4
- package/dist/{candidate-execution-izJj02pU.js → candidate-execution-DNLk6h0A.js} +4 -4
- package/dist/{candidate-execution-izJj02pU.js.map → candidate-execution-DNLk6h0A.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-Bk8rg4gs.d.ts → environment-provider-BXFFK_rZ.d.ts} +2 -2
- package/dist/environment-provider.d.ts +1 -1
- package/dist/{graph-COikUkrj.js → graph-DzE4SwfP.js} +2 -2
- package/dist/{graph-COikUkrj.js.map → graph-DzE4SwfP.js.map} +1 -1
- package/dist/graph.d.ts +230 -4
- package/dist/graph.js +873 -50
- package/dist/graph.js.map +1 -1
- package/dist/{improvement-cycle-dGtgX1vr.js → improvement-cycle-a_L8FFql.js} +4 -4
- package/dist/{improvement-cycle-dGtgX1vr.js.map → improvement-cycle-a_L8FFql.js.map} +1 -1
- package/dist/{index-8b0o4dMF.d.ts → index-BP11HXYi.d.ts} +5 -5
- package/dist/{index-4mRXbu_V.d.ts → index-C2X-OVPZ.d.ts} +4 -4
- package/dist/{index-CTQ97IMO.d.ts → index-OFOvnNc2.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 +6 -6
- package/dist/{knowledge-CZT8ahp6.js → knowledge-Ccdghy81.js} +5 -5
- package/dist/{knowledge-CZT8ahp6.js.map → knowledge-Ccdghy81.js.map} +1 -1
- package/dist/knowledge.d.ts +1 -1
- package/dist/knowledge.js +1 -1
- package/dist/{loop-runner-bin-DHU5kmJW.d.ts → loop-runner-bin-2NITRIFc.d.ts} +3 -3
- package/dist/{loop-runner-bin-CIjmKf7v.js → loop-runner-bin-DM-AnPC-.js} +3 -3
- package/dist/{loop-runner-bin-CIjmKf7v.js.map → loop-runner-bin-DM-AnPC-.js.map} +1 -1
- 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-DG779Z6S.js → openai-tools-CIo9tB75.js} +2 -2
- package/dist/{openai-tools-DG779Z6S.js.map → openai-tools-CIo9tB75.js.map} +1 -1
- package/dist/{prepare-VLGnSyav.js → prepare-D7Fb75XR.js} +6 -4
- package/dist/prepare-D7Fb75XR.js.map +1 -0
- package/dist/primeintellect/index.d.ts +1 -1
- package/dist/{protected-model-port-CcCH0Uji.js → protected-model-port-RZkxRZhp.js} +2 -2
- package/dist/{protected-model-port-CcCH0Uji.js.map → protected-model-port-RZkxRZhp.js.map} +1 -1
- package/dist/{redact-BUIB5IWj.d.ts → redact-D4vlt6p4.d.ts} +3 -3
- package/dist/{runtime-BT00zfIS.js → runtime-BDHsMgCK.js} +6 -6
- package/dist/{runtime-BT00zfIS.js.map → runtime-BDHsMgCK.js.map} +1 -1
- package/dist/{runtime-B0FIFwpH.d.ts → runtime-D44YrQ4t.d.ts} +4 -4
- package/dist/{stream-agent-turn-BEhmmYmL.d.ts → stream-agent-turn-Dx2YrEWW.d.ts} +2 -2
- package/dist/{structural-rollout-DAenyNls.js → structural-rollout-CBK2LHni.js} +2 -2
- package/dist/{structural-rollout-DAenyNls.js.map → structural-rollout-CBK2LHni.js.map} +1 -1
- package/dist/{supervise-Di6xvnOL.js → supervise-CzllE0vn.js} +2 -2
- package/dist/{supervise-Di6xvnOL.js.map → supervise-CzllE0vn.js.map} +1 -1
- package/dist/{supervisor-DmWq4vkU.js → supervisor-D5K8ihKQ.js} +106 -36
- package/dist/supervisor-D5K8ihKQ.js.map +1 -0
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +12 -12
- package/dist/{types-DXKDG6DQ.d.ts → types-C2l1jaos.d.ts} +7 -4
- package/dist/{workspace-archive-BptlGCEU.js → workspace-archive-DK0mDjSB.js} +2 -2
- package/dist/{workspace-archive-BptlGCEU.js.map → workspace-archive-DK0mDjSB.js.map} +1 -1
- package/package.json +1 -1
- package/dist/prepare-VLGnSyav.js.map +0 -1
- package/dist/supervisor-DmWq4vkU.js.map +0 -1
package/dist/graph.js
CHANGED
|
@@ -1,62 +1,217 @@
|
|
|
1
1
|
import { m as ValidationError } from "./errors-CDZ8XsVj.js";
|
|
2
|
-
import { x as contentAddress } from "./spawn-journal-oewq1g2h.js";
|
|
3
|
-
import { i as
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
2
|
+
import { i as InMemorySpawnJournal, r as InMemoryResultBlobStore, x as contentAddress } from "./spawn-journal-oewq1g2h.js";
|
|
3
|
+
import { A as createBudgetPool, B as createExecutorRegistry, i as collectDelivered, o as runFinalizer, r as bestDelivered, u as createScope } from "./supervisor-D5K8ihKQ.js";
|
|
4
|
+
import { i as workerFromBackend, s as supervisorAgent } from "./supervise-CzllE0vn.js";
|
|
5
|
+
import { t as GraphEdgeCapError } from "./graph-DzE4SwfP.js";
|
|
6
|
+
//#region src/runtime/graph/condition.ts
|
|
7
|
+
/**
|
|
8
|
+
* The ONE predicate tree: every guard in a graph — an edge `guard`, a filter projection's
|
|
9
|
+
* predicate — is this shape, evaluated by this evaluator, so `contains`, ordering and equality can
|
|
10
|
+
* never mean two different things on two surfaces (adopted from ADC's `workflow-conditions`,
|
|
11
|
+
* agent-runtime#968). A declarative tree keeps an untrusted predicate incapable of code execution;
|
|
12
|
+
* the size and depth bounds keep a predicate authored once from burning CPU on every settle.
|
|
13
|
+
*
|
|
14
|
+
* No zod: the kernel's grain is a hand-written validator that throws `ValidationError` by name.
|
|
15
|
+
*/
|
|
16
|
+
/** The leaf comparison operators; `exists`/`truthy` are unary, the rest compare against `value`. */
|
|
17
|
+
const CONDITION_OPS = [
|
|
18
|
+
"eq",
|
|
19
|
+
"neq",
|
|
20
|
+
"gt",
|
|
21
|
+
"gte",
|
|
22
|
+
"lt",
|
|
23
|
+
"lte",
|
|
24
|
+
"in",
|
|
25
|
+
"contains",
|
|
26
|
+
"exists",
|
|
27
|
+
"truthy"
|
|
28
|
+
];
|
|
29
|
+
/** Operators that compare against `value`; `exists`/`truthy` are unary and must omit it. */
|
|
30
|
+
const OPS_NEEDING_VALUE = new Set(CONDITION_OPS.filter((op) => op !== "exists" && op !== "truthy"));
|
|
31
|
+
const MAX_PATH_LENGTH = 256;
|
|
32
|
+
const MAX_PATH_SEGMENTS = 16;
|
|
33
|
+
const PATH_SEGMENT = /^[A-Za-z_$][A-Za-z0-9_$-]*$/u;
|
|
34
|
+
/** Parse `a.b[0].c` into steps; refuse anything outside the bounded grammar. */
|
|
35
|
+
function parseConditionPath(path, context) {
|
|
36
|
+
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`);
|
|
37
|
+
const steps = [];
|
|
38
|
+
for (const part of path.split(".")) {
|
|
39
|
+
const open = part.indexOf("[");
|
|
40
|
+
const head = open === -1 ? part : part.slice(0, open);
|
|
41
|
+
if (head.length > 0) {
|
|
42
|
+
if (!PATH_SEGMENT.test(head)) throw new ValidationError(`${context}: path segment ${JSON.stringify(head)} is not addressable`);
|
|
43
|
+
steps.push(head);
|
|
44
|
+
} else if (open !== 0 || steps.length === 0) throw new ValidationError(`${context}: path ${JSON.stringify(path)} has an empty segment`);
|
|
45
|
+
let rest = open === -1 ? "" : part.slice(open);
|
|
46
|
+
while (rest.length > 0) {
|
|
47
|
+
const match = /^\[(\d{1,6})\]/u.exec(rest);
|
|
48
|
+
if (!match) throw new ValidationError(`${context}: path index in ${JSON.stringify(part)} must be [N]`);
|
|
49
|
+
steps.push(Number(match[1]));
|
|
50
|
+
rest = rest.slice(match[0].length);
|
|
24
51
|
}
|
|
52
|
+
if (steps.length > MAX_PATH_SEGMENTS) throw new ValidationError(`${context}: path exceeds ${MAX_PATH_SEGMENTS} segments`);
|
|
25
53
|
}
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
if (kind.onCrash !== "restart" && kind.onCrash !== "resume") throw new ValidationError(`${who}: onCrash must be "restart" or "resume"`);
|
|
29
|
-
if (kind.budget !== "metered" && kind.budget !== "exempt") throw new ValidationError(`${who}: budget must be "metered" or "exempt"`);
|
|
30
|
-
if (typeof kind.run !== "function") throw new ValidationError(`${who}: run must be a function`);
|
|
31
|
-
return kind;
|
|
54
|
+
if (steps.length === 0) throw new ValidationError(`${context}: path resolves no segment`);
|
|
55
|
+
return steps;
|
|
32
56
|
}
|
|
33
|
-
/**
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
57
|
+
/** Walk a parsed path; any miss resolves `undefined`, never a throw — absence is an answer. */
|
|
58
|
+
function resolveConditionPath(context, steps) {
|
|
59
|
+
let current = context;
|
|
60
|
+
for (const step of steps) {
|
|
61
|
+
if (current === null || current === void 0) return void 0;
|
|
62
|
+
if (typeof step === "number") {
|
|
63
|
+
if (!Array.isArray(current)) return void 0;
|
|
64
|
+
current = current[step];
|
|
65
|
+
} else {
|
|
66
|
+
if (typeof current !== "object" || Array.isArray(current)) return void 0;
|
|
67
|
+
current = current[step];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return current;
|
|
71
|
+
}
|
|
72
|
+
function isLeaf(condition) {
|
|
73
|
+
return typeof condition.path === "string";
|
|
74
|
+
}
|
|
75
|
+
/** Validate shape, bounds, and per-leaf path/operator rules; returns the input for chaining. */
|
|
76
|
+
function validateCondition(raw, context) {
|
|
77
|
+
let nodes = 0;
|
|
78
|
+
const walk = (value, depth) => {
|
|
79
|
+
nodes += 1;
|
|
80
|
+
if (nodes > 40) throw new ValidationError(`${context}: condition exceeds 40 nodes`);
|
|
81
|
+
if (depth > 6) throw new ValidationError(`${context}: condition exceeds depth 6`);
|
|
82
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new ValidationError(`${context}: a condition must be an object`);
|
|
83
|
+
const record = value;
|
|
84
|
+
const combinators = [
|
|
85
|
+
"all",
|
|
86
|
+
"any",
|
|
87
|
+
"not"
|
|
88
|
+
].filter((key) => record[key] !== void 0);
|
|
89
|
+
if (combinators.length > 1) throw new ValidationError(`${context}: a condition carries ONE of all/any/not, got ${combinators.join("+")}`);
|
|
90
|
+
if (record.all !== void 0 || record.any !== void 0) {
|
|
91
|
+
const key = record.all !== void 0 ? "all" : "any";
|
|
92
|
+
const branch = record[key];
|
|
93
|
+
if (!Array.isArray(branch) || branch.length === 0) throw new ValidationError(`${context}: ${key} must be a non-empty array`);
|
|
94
|
+
for (const child of branch) walk(child, depth + 1);
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
if (record.not !== void 0) {
|
|
98
|
+
walk(record.not, depth + 1);
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
const op = record.op;
|
|
102
|
+
if (typeof record.path !== "string" || typeof op !== "string") throw new ValidationError(`${context}: a leaf needs { path, op }`);
|
|
103
|
+
if (!CONDITION_OPS.includes(op)) throw new ValidationError(`${context}: unknown op ${JSON.stringify(op)}; known: ${CONDITION_OPS.join(", ")}`);
|
|
104
|
+
parseConditionPath(record.path, context);
|
|
105
|
+
const needsValue = OPS_NEEDING_VALUE.has(op);
|
|
106
|
+
if (needsValue && !("value" in record)) throw new ValidationError(`${context}: op ${JSON.stringify(op)} requires a value`);
|
|
107
|
+
if (!needsValue && "value" in record) throw new ValidationError(`${context}: op ${JSON.stringify(op)} is unary — remove value`);
|
|
108
|
+
if (op === "in" && !Array.isArray(record.value)) throw new ValidationError(`${context}: op "in" takes an array value`);
|
|
109
|
+
return value;
|
|
38
110
|
};
|
|
111
|
+
return walk(raw, 1);
|
|
112
|
+
}
|
|
113
|
+
function canonicalEquals(a, b) {
|
|
114
|
+
if (Object.is(a, b)) return true;
|
|
115
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
116
|
+
try {
|
|
117
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function ordering(op, left, right) {
|
|
123
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
124
|
+
if (Number.isNaN(left) || Number.isNaN(right)) return false;
|
|
125
|
+
if (op === "gt") return left > right;
|
|
126
|
+
if (op === "gte") return left >= right;
|
|
127
|
+
if (op === "lt") return left < right;
|
|
128
|
+
return left <= right;
|
|
129
|
+
}
|
|
130
|
+
if (typeof left === "string" && typeof right === "string") {
|
|
131
|
+
if (op === "gt") return left > right;
|
|
132
|
+
if (op === "gte") return left >= right;
|
|
133
|
+
if (op === "lt") return left < right;
|
|
134
|
+
return left <= right;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
/** Walk a validated condition over a context to a boolean. Never throws on data shape. */
|
|
139
|
+
function evaluateCondition(condition, context) {
|
|
140
|
+
if (isLeaf(condition)) {
|
|
141
|
+
const resolved = resolveConditionPath(context, parseConditionPath(condition.path, "evaluate"));
|
|
142
|
+
switch (condition.op) {
|
|
143
|
+
case "exists": return resolved !== void 0 && resolved !== null;
|
|
144
|
+
case "truthy": return Boolean(resolved);
|
|
145
|
+
case "eq": return canonicalEquals(resolved, condition.value);
|
|
146
|
+
case "neq": return !canonicalEquals(resolved, condition.value);
|
|
147
|
+
case "in": return Array.isArray(condition.value) ? condition.value.some((candidate) => canonicalEquals(resolved, candidate)) : false;
|
|
148
|
+
case "contains":
|
|
149
|
+
if (Array.isArray(resolved)) return resolved.some((element) => canonicalEquals(element, condition.value));
|
|
150
|
+
return typeof resolved === "string" && typeof condition.value === "string" ? resolved.includes(condition.value) : false;
|
|
151
|
+
default: return ordering(condition.op, resolved, condition.value);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if ("all" in condition) return condition.all.every((child) => evaluateCondition(child, context));
|
|
155
|
+
if ("any" in condition) return condition.any.some((child) => evaluateCondition(child, context));
|
|
156
|
+
return !evaluateCondition(condition.not, context);
|
|
39
157
|
}
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/runtime/graph/projection.ts
|
|
40
160
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
161
|
+
* The ONE pure projection a `data` edge may carry (agent-runtime#971): the bounded,
|
|
162
|
+
* schema-preserving subset of ADC's collection helpers. Anything richer is a `script` NODE, so it
|
|
163
|
+
* is journaled (`inputRef` → `outRef`), typed, and visible. Exactly one operator per projection.
|
|
44
164
|
*/
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
165
|
+
const PROJECTION_KEYS = [
|
|
166
|
+
"path",
|
|
167
|
+
"pick",
|
|
168
|
+
"map",
|
|
169
|
+
"filter",
|
|
170
|
+
"first",
|
|
171
|
+
"last",
|
|
172
|
+
"count"
|
|
173
|
+
];
|
|
174
|
+
/** Validate a projection: exactly one known operator, its argument well-formed. */
|
|
175
|
+
function validateProjection(raw, context) {
|
|
176
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new ValidationError(`${context}: a projection must be an object`);
|
|
177
|
+
const record = raw;
|
|
178
|
+
const keys = PROJECTION_KEYS.filter((key) => record[key] !== void 0);
|
|
179
|
+
const unknown = Object.keys(record).filter((key) => !PROJECTION_KEYS.includes(key));
|
|
180
|
+
if (unknown.length > 0) throw new ValidationError(`${context}: unknown projection key(s) ${unknown.join(", ")}; known: ${PROJECTION_KEYS.join(", ")}`);
|
|
181
|
+
if (keys.length !== 1) throw new ValidationError(`${context}: a projection carries exactly ONE of ${PROJECTION_KEYS.join("/")}`);
|
|
182
|
+
const key = keys[0];
|
|
183
|
+
if (key === "path" || key === "map") parseConditionPath(record[key], context);
|
|
184
|
+
if (key === "pick") {
|
|
185
|
+
const fields = record.pick;
|
|
186
|
+
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`);
|
|
54
187
|
}
|
|
55
|
-
if (
|
|
56
|
-
|
|
188
|
+
if (key === "filter") validateCondition(record.filter, context);
|
|
189
|
+
if ((key === "first" || key === "last" || key === "count") && record[key] !== true) throw new ValidationError(`${context}: ${key} must be literally true`);
|
|
190
|
+
return raw;
|
|
57
191
|
}
|
|
58
|
-
|
|
59
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Apply a validated projection to an admitted payload. Collection operators over a non-array
|
|
194
|
+
* refuse by name — a shape the author did not expect is a graph defect, not an empty result.
|
|
195
|
+
*/
|
|
196
|
+
function applyProjection(value, projection, context) {
|
|
197
|
+
if ("path" in projection) return resolveConditionPath(value, parseConditionPath(projection.path, context));
|
|
198
|
+
if ("pick" in projection) {
|
|
199
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new ValidationError(`${context}: pick needs an object payload`);
|
|
200
|
+
const record = value;
|
|
201
|
+
const out = {};
|
|
202
|
+
for (const field of projection.pick) if (field in record) out[field] = record[field];
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
const collection = value;
|
|
206
|
+
if (!Array.isArray(collection)) throw new ValidationError(`${context}: this projection needs an array payload`);
|
|
207
|
+
if ("map" in projection) {
|
|
208
|
+
const steps = parseConditionPath(projection.map, context);
|
|
209
|
+
return collection.map((element) => resolveConditionPath(element, steps));
|
|
210
|
+
}
|
|
211
|
+
if ("filter" in projection) return collection.filter((element) => evaluateCondition(projection.filter, element));
|
|
212
|
+
if ("first" in projection) return collection[0];
|
|
213
|
+
if ("last" in projection) return collection[collection.length - 1];
|
|
214
|
+
return collection.length;
|
|
60
215
|
}
|
|
61
216
|
//#endregion
|
|
62
217
|
//#region src/runtime/graph/registry.ts
|
|
@@ -131,6 +286,273 @@ function createRegistry(label, seed = []) {
|
|
|
131
286
|
return registry;
|
|
132
287
|
}
|
|
133
288
|
//#endregion
|
|
289
|
+
//#region src/runtime/graph/definition.ts
|
|
290
|
+
/** Which gating-edge outcomes release a node (adopted from ADC, agent-runtime#968). */
|
|
291
|
+
const JOIN_RULES = [
|
|
292
|
+
"all",
|
|
293
|
+
"any",
|
|
294
|
+
"any_failed",
|
|
295
|
+
"all_done"
|
|
296
|
+
];
|
|
297
|
+
/** ADC-compatible visit backstop: nothing may be ENTERED more than this many times. */
|
|
298
|
+
const DEFAULT_MAX_NODE_VISITS = 25;
|
|
299
|
+
/** The hard ceiling an author's `maxVisits`/`maxNodeVisits` override may reach. */
|
|
300
|
+
const MAX_MAX_NODE_VISITS = 100;
|
|
301
|
+
/** Structural validation only — everything a registry is not needed for. */
|
|
302
|
+
function validateEngineGraphSpec(spec, context = "compileGraph") {
|
|
303
|
+
if (!Array.isArray(spec.nodes) || spec.nodes.length === 0) throw new ValidationError(`${context}: a graph needs at least one node`);
|
|
304
|
+
const ids = /* @__PURE__ */ new Set();
|
|
305
|
+
for (const node of spec.nodes) {
|
|
306
|
+
if (typeof node.id !== "string" || node.id.length === 0) throw new ValidationError(`${context}: every node needs a non-empty id`);
|
|
307
|
+
if (ids.has(node.id)) throw new ValidationError(`${context}: duplicate node id ${JSON.stringify(node.id)}`);
|
|
308
|
+
ids.add(node.id);
|
|
309
|
+
parseRegistryHandle(node.kind, `${context}: node ${node.id} kind`);
|
|
310
|
+
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(", ")}`);
|
|
311
|
+
if (node.maxVisits !== void 0) {
|
|
312
|
+
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]`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (!Array.isArray(spec.edges)) throw new ValidationError(`${context}: edges must be an array`);
|
|
316
|
+
for (const [index, edge] of spec.edges.entries()) {
|
|
317
|
+
const who = `${context}: edge[${index}]`;
|
|
318
|
+
if (edge.kind !== "delegates" && edge.kind !== "analyzes" && edge.kind !== "data") throw new ValidationError(`${who}: kind must be delegates | analyzes | data`);
|
|
319
|
+
for (const end of ["from", "to"]) {
|
|
320
|
+
const ref = edge[end];
|
|
321
|
+
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`);
|
|
322
|
+
}
|
|
323
|
+
if (edge.guard !== void 0) validateCondition(edge.guard, `${who} guard`);
|
|
324
|
+
if (edge.projection !== void 0) {
|
|
325
|
+
if (edge.kind !== "data") throw new ValidationError(`${who}: only a data edge carries a projection`);
|
|
326
|
+
validateProjection(edge.projection, `${who} projection`);
|
|
327
|
+
}
|
|
328
|
+
if (edge.maxTraversals !== void 0) {
|
|
329
|
+
if (!Number.isSafeInteger(edge.maxTraversals) || edge.maxTraversals < 1) throw new ValidationError(`${who}: maxTraversals must be a positive integer`);
|
|
330
|
+
}
|
|
331
|
+
if (edge.kind === "data" && edge.directive !== void 0) throw new ValidationError(`${who}: a data edge carries a port binding, never a directive`);
|
|
332
|
+
}
|
|
333
|
+
if (spec.root !== void 0 && !ids.has(spec.root)) throw new ValidationError(`${context}: root ${JSON.stringify(spec.root)} names no node`);
|
|
334
|
+
if (spec.maxNodeVisits !== void 0) {
|
|
335
|
+
if (!Number.isSafeInteger(spec.maxNodeVisits) || spec.maxNodeVisits < 1 || spec.maxNodeVisits > 100) throw new ValidationError(`${context}: maxNodeVisits must be an integer in [1, 100]`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/runtime/graph/compile.ts
|
|
340
|
+
/**
|
|
341
|
+
* Compile an authored graph against an engine's kind registry into the schedulable form. Every
|
|
342
|
+
* refusal here happens before any spend: an unknown kind, a port that does not exist, a `data`
|
|
343
|
+
* binding whose schemas cannot fit, a `delegates`/`data` edge into an oracle, a terminal with no
|
|
344
|
+
* completion check (agent-runtime#971, #973).
|
|
345
|
+
*/
|
|
346
|
+
/** A node's ports: its kind's declared outputs plus the two implicit ones every node has. */
|
|
347
|
+
const IMPLICIT_OUTPUT_PORTS = ["out", "trace"];
|
|
348
|
+
/** A node's ports: node-level declarations merged OVER its kind's (the node wins on a name). */
|
|
349
|
+
function nodePorts(kind, node) {
|
|
350
|
+
const merge = (declared, own) => {
|
|
351
|
+
if (own === void 0 || own.length === 0) return declared;
|
|
352
|
+
const names = new Set(own.map((port) => port.name));
|
|
353
|
+
return [...own, ...declared.filter((port) => !names.has(port.name))];
|
|
354
|
+
};
|
|
355
|
+
return {
|
|
356
|
+
inputs: merge(kind.inputs, node.ports?.inputs),
|
|
357
|
+
outputs: merge(kind.outputs, node.ports?.outputs)
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
function outputPort(ports, port) {
|
|
361
|
+
if (IMPLICIT_OUTPUT_PORTS.includes(port)) return {
|
|
362
|
+
name: port,
|
|
363
|
+
schema: {}
|
|
364
|
+
};
|
|
365
|
+
return ports.outputs.find((candidate) => candidate.name === port);
|
|
366
|
+
}
|
|
367
|
+
function inputPort(ports, port) {
|
|
368
|
+
return ports.inputs.find((candidate) => candidate.name === port);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Bounded structural acceptance: does a value of `source`'s shape fit `target`? Schemas with no
|
|
372
|
+
* `type` accept anything; object targets require their `required` properties to be present and
|
|
373
|
+
* accepted when the source declares properties. Depth-bounded — this is a compile-time tripwire,
|
|
374
|
+
* not a full JSON Schema validator.
|
|
375
|
+
*/
|
|
376
|
+
function schemaAccepts(source, target, depth = 0) {
|
|
377
|
+
if (depth > 6) return true;
|
|
378
|
+
const sourceType = source.type;
|
|
379
|
+
const targetType = target.type;
|
|
380
|
+
if (targetType === void 0 || sourceType === void 0) return true;
|
|
381
|
+
const targets = Array.isArray(targetType) ? targetType : [targetType];
|
|
382
|
+
const sources = Array.isArray(sourceType) ? sourceType : [sourceType];
|
|
383
|
+
if (sources.filter((candidate) => targets.includes(candidate === "integer" ? "number" : candidate) || targets.includes(candidate)).length === 0) return false;
|
|
384
|
+
if (targets.includes("object") && sources.includes("object")) {
|
|
385
|
+
const required = Array.isArray(target.required) ? target.required : [];
|
|
386
|
+
const sourceProps = source.properties;
|
|
387
|
+
const targetProps = target.properties;
|
|
388
|
+
if (sourceProps !== void 0) for (const name of required) {
|
|
389
|
+
const sourceProp = sourceProps[name];
|
|
390
|
+
if (sourceProp === void 0) return false;
|
|
391
|
+
const targetProp = targetProps?.[name];
|
|
392
|
+
if (targetProp !== void 0 && !schemaAccepts(sourceProp, targetProp, depth + 1)) return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (targets.includes("array") && sources.includes("array")) {
|
|
396
|
+
const sourceItems = source.items;
|
|
397
|
+
const targetItems = target.items;
|
|
398
|
+
if (sourceItems !== void 0 && targetItems !== void 0) return schemaAccepts(sourceItems, targetItems, depth + 1);
|
|
399
|
+
}
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Lower an authored graph against an engine's kind registry into the schedulable form, refusing
|
|
404
|
+
* every structural defect before any spend.
|
|
405
|
+
*/
|
|
406
|
+
function compileGraph(engine, spec, context = "compileGraph") {
|
|
407
|
+
validateEngineGraphSpec(spec, context);
|
|
408
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
409
|
+
const configs = /* @__PURE__ */ new Map();
|
|
410
|
+
for (const node of spec.nodes) {
|
|
411
|
+
const handle = parseRegistryHandle(node.kind, `${context}: node ${node.id}`);
|
|
412
|
+
const kind = engine.kinds.require(handle, `${context}: node ${node.id}`);
|
|
413
|
+
kinds.set(node.id, kind);
|
|
414
|
+
configs.set(node.id, kind.validateConfig(node.config ?? {}, `${context}: node ${node.id}`));
|
|
415
|
+
if (node.flags?.pure && kind.id !== "script") throw new ValidationError(`${context}: node ${node.id} sets pure, which only a script node may claim`);
|
|
416
|
+
}
|
|
417
|
+
const compiledEdges = [];
|
|
418
|
+
const inbound = /* @__PURE__ */ new Map();
|
|
419
|
+
const outbound = /* @__PURE__ */ new Map();
|
|
420
|
+
for (const [index, edge] of spec.edges.entries()) {
|
|
421
|
+
const who = `${context}: edge[${index}] ${edge.from.node}->${edge.to.node}`;
|
|
422
|
+
const fromKind = kinds.get(edge.from.node);
|
|
423
|
+
const toKind = kinds.get(edge.to.node);
|
|
424
|
+
if (!fromKind || !toKind) throw new ValidationError(`${who}: unresolved endpoint`);
|
|
425
|
+
const toNode = spec.nodes.find((node) => node.id === edge.to.node);
|
|
426
|
+
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`);
|
|
427
|
+
const fromPort = edge.from.port ?? (edge.kind === "analyzes" ? "trace" : "out");
|
|
428
|
+
if (edge.kind === "analyzes" && fromPort !== "trace") throw new ValidationError(`${who}: an analyzes edge reads the trace port, got ${fromPort}`);
|
|
429
|
+
const fromPorts = nodePorts(fromKind, spec.nodes.find((node) => node.id === edge.from.node));
|
|
430
|
+
const toPorts = nodePorts(toKind, toNode);
|
|
431
|
+
const sourcePort = outputPort(fromPorts, fromPort);
|
|
432
|
+
if (!sourcePort) {
|
|
433
|
+
const known = [...IMPLICIT_OUTPUT_PORTS, ...fromPorts.outputs.map((port) => port.name)];
|
|
434
|
+
throw new ValidationError(`${who}: ${formatRegistryHandle(fromKind)} has no output port ${JSON.stringify(fromPort)}; known: ${known.join(", ")}`);
|
|
435
|
+
}
|
|
436
|
+
let toPort = edge.to.port ?? "";
|
|
437
|
+
if (edge.kind === "data") {
|
|
438
|
+
if (toPort === "") if (toPorts.inputs.length === 1) toPort = toPorts.inputs[0]?.name ?? "";
|
|
439
|
+
else throw new ValidationError(`${who}: a data edge needs to.port; ${formatRegistryHandle(toKind)} declares ${toPorts.inputs.length} inputs`);
|
|
440
|
+
const targetPort = inputPort(toPorts, toPort);
|
|
441
|
+
if (!targetPort) throw new ValidationError(`${who}: ${formatRegistryHandle(toKind)} has no input port ${JSON.stringify(toPort)}; known: ${toPorts.inputs.map((port) => port.name).join(", ") || "(none)"}`);
|
|
442
|
+
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")})`);
|
|
443
|
+
}
|
|
444
|
+
const compiled = {
|
|
445
|
+
id: edge.id ?? `${edge.from.node}->${edge.to.node}#${index}`,
|
|
446
|
+
spec: edge,
|
|
447
|
+
fromPort,
|
|
448
|
+
toPort: toPort === "" ? "out" : toPort
|
|
449
|
+
};
|
|
450
|
+
compiledEdges.push(compiled);
|
|
451
|
+
outbound.set(edge.from.node, [...outbound.get(edge.from.node) ?? [], compiled]);
|
|
452
|
+
inbound.set(edge.to.node, [...inbound.get(edge.to.node) ?? [], compiled]);
|
|
453
|
+
}
|
|
454
|
+
const dupes = /* @__PURE__ */ new Set();
|
|
455
|
+
for (const edge of compiledEdges) {
|
|
456
|
+
if (dupes.has(edge.id)) throw new ValidationError(`${context}: duplicate edge id ${edge.id}`);
|
|
457
|
+
dupes.add(edge.id);
|
|
458
|
+
}
|
|
459
|
+
const entries = spec.nodes.filter((node) => (inbound.get(node.id) ?? []).length === 0);
|
|
460
|
+
if (entries.length === 0) throw new ValidationError(`${context}: no entry node — every node has an inbound edge`);
|
|
461
|
+
const root = spec.root ?? (entries.length === 1 ? entries[0]?.id ?? "" : (() => {
|
|
462
|
+
throw new ValidationError(`${context}: ${entries.length} entry nodes (${entries.map((node) => node.id).join(", ")}) — name spec.root`);
|
|
463
|
+
})());
|
|
464
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
465
|
+
const terminals = [];
|
|
466
|
+
for (const node of spec.nodes) {
|
|
467
|
+
const kind = kinds.get(node.id);
|
|
468
|
+
if (!kind) throw new ValidationError(`${context}: unresolved node ${node.id}`);
|
|
469
|
+
const terminal = node.terminal ?? (outbound.get(node.id) ?? []).length === 0;
|
|
470
|
+
const deliverable = node.deliverable ?? (node.id === root ? spec.deliverable : void 0);
|
|
471
|
+
if (terminal) terminals.push(node.id);
|
|
472
|
+
nodes.set(node.id, {
|
|
473
|
+
id: node.id,
|
|
474
|
+
kind,
|
|
475
|
+
config: configs.get(node.id),
|
|
476
|
+
join: node.join ?? "all",
|
|
477
|
+
maxVisits: node.maxVisits ?? spec.maxNodeVisits ?? 25,
|
|
478
|
+
oracle: node.flags?.oracle ?? false,
|
|
479
|
+
pure: node.flags?.pure ?? false,
|
|
480
|
+
terminal,
|
|
481
|
+
...deliverable === void 0 ? {} : { deliverable },
|
|
482
|
+
inbound: inbound.get(node.id) ?? [],
|
|
483
|
+
outbound: outbound.get(node.id) ?? [],
|
|
484
|
+
spec: node
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
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`);
|
|
488
|
+
return {
|
|
489
|
+
nodes,
|
|
490
|
+
edges: compiledEdges,
|
|
491
|
+
entries: entries.map((node) => node.id),
|
|
492
|
+
terminals,
|
|
493
|
+
root,
|
|
494
|
+
maxNodeVisits: spec.maxNodeVisits ?? 25
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
//#endregion
|
|
498
|
+
//#region src/runtime/graph/kind.ts
|
|
499
|
+
/** Validate a kind declaration at registration — so a malformed kind is refused by name once,
|
|
500
|
+
* not at the first node that uses it. */
|
|
501
|
+
function validateNodeKind(kind, context = "registerNodeKind") {
|
|
502
|
+
const who = `${context}: kind ${JSON.stringify(`${kind.id}/v${kind.version}`)}`;
|
|
503
|
+
if (typeof kind.id !== "string" || kind.id.length === 0) throw new ValidationError(`${context}: a kind must carry a non-empty id`);
|
|
504
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(kind.id)) throw new ValidationError(`${who}: id may contain only letters, digits, ".", "_" and "-"`);
|
|
505
|
+
if (!Number.isSafeInteger(kind.version) || kind.version < 1) throw new ValidationError(`${who}: version must be a positive integer`);
|
|
506
|
+
if (typeof kind.description !== "string" || kind.description.trim().length === 0) throw new ValidationError(`${who}: description is required`);
|
|
507
|
+
if (typeof kind.validateConfig !== "function") throw new ValidationError(`${who}: validateConfig must be a function`);
|
|
508
|
+
if (!isRecord(kind.configSchema)) throw new ValidationError(`${who}: configSchema must be a JSON Schema object`);
|
|
509
|
+
for (const [field, ports] of [["inputs", kind.inputs], ["outputs", kind.outputs]]) {
|
|
510
|
+
if (!Array.isArray(ports)) throw new ValidationError(`${who}: ${field} must be an array`);
|
|
511
|
+
const seen = /* @__PURE__ */ new Set();
|
|
512
|
+
for (const port of ports) {
|
|
513
|
+
if (!port || typeof port.name !== "string" || port.name.length === 0) throw new ValidationError(`${who}: every ${field} port needs a non-empty name`);
|
|
514
|
+
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`);
|
|
515
|
+
if (seen.has(port.name)) throw new ValidationError(`${who}: duplicate ${field} port ${JSON.stringify(port.name)}`);
|
|
516
|
+
seen.add(port.name);
|
|
517
|
+
if (!isRecord(port.schema)) throw new ValidationError(`${who}: ${field} port ${JSON.stringify(port.name)} needs a JSON Schema`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
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`);
|
|
521
|
+
if (new Set(kind.effects).size !== kind.effects.length) throw new ValidationError(`${who}: effects must not repeat a name`);
|
|
522
|
+
if (kind.onCrash !== "restart" && kind.onCrash !== "resume") throw new ValidationError(`${who}: onCrash must be "restart" or "resume"`);
|
|
523
|
+
if (kind.budget !== "metered" && kind.budget !== "exempt") throw new ValidationError(`${who}: budget must be "metered" or "exempt"`);
|
|
524
|
+
if (typeof kind.run !== "function") throw new ValidationError(`${who}: run must be a function`);
|
|
525
|
+
return kind;
|
|
526
|
+
}
|
|
527
|
+
/** The handle a graph writes to name this kind. */
|
|
528
|
+
function kindHandle(kind) {
|
|
529
|
+
return {
|
|
530
|
+
id: kind.id,
|
|
531
|
+
version: kind.version
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Narrow a host's effect table to exactly what one kind declared. Anything the kind did not
|
|
536
|
+
* declare is absent — `undefined` on read — so a kind cannot reach past its declaration, and the
|
|
537
|
+
* engine can list a graph's required effects before spending a token.
|
|
538
|
+
*/
|
|
539
|
+
function narrowEffects(declared, provided, context) {
|
|
540
|
+
const out = {};
|
|
541
|
+
const missing = [];
|
|
542
|
+
for (const name of declared) {
|
|
543
|
+
if (!(name in provided)) {
|
|
544
|
+
missing.push(name);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
out[name] = provided[name];
|
|
548
|
+
}
|
|
549
|
+
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"}`);
|
|
550
|
+
return Object.freeze(out);
|
|
551
|
+
}
|
|
552
|
+
function isRecord(value) {
|
|
553
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
554
|
+
}
|
|
555
|
+
//#endregion
|
|
134
556
|
//#region src/runtime/graph/engine.ts
|
|
135
557
|
/**
|
|
136
558
|
* `createGraphEngine` — one engine instance: its kind registry (core kinds pre-registered, host
|
|
@@ -375,6 +797,407 @@ function asRecord(value, context) {
|
|
|
375
797
|
return value;
|
|
376
798
|
}
|
|
377
799
|
//#endregion
|
|
378
|
-
|
|
800
|
+
//#region src/runtime/graph/scheduler.ts
|
|
801
|
+
/**
|
|
802
|
+
* The engine scheduler (agent-runtime#980): run a compiled graph by hosting every node instance on
|
|
803
|
+
* one kernel `Scope` — the pool, the journal, the blob store and cancellation are the kernel's,
|
|
804
|
+
* never re-implemented. The scheduler owns only what a graph adds: joins over guarded edges,
|
|
805
|
+
* traversal and visit caps, directive/payload delivery, terminals and the finalizer reduce.
|
|
806
|
+
*
|
|
807
|
+
* Edge semantics are adopted from ADC's workflow graph (agent-runtime#968): an edge settles
|
|
808
|
+
* SATISFIED / DEAD / FAILED per its source's LATEST completion; join rules `all | any | any_failed
|
|
809
|
+
* | all_done` decide release; a release CONSUMES the outcomes that produced it (settled edges
|
|
810
|
+
* re-arm; a pending edge at release is consumed-once, so an OR-diamond's second completer never
|
|
811
|
+
* double-fires). Per-edge `maxTraversals` refuses the consumption and ledgers `unpropagated`; a
|
|
812
|
+
* node entered past `maxVisits` fails the run `cycle-budget-exceeded` (#973).
|
|
813
|
+
*/
|
|
814
|
+
/** Admission for every value crossing an edge (#971): JSON round-trip, `undefined` stripped, a
|
|
815
|
+
* non-representable value becomes a RECORD of that fact — a degraded record beats a vanished
|
|
816
|
+
* edge. */
|
|
817
|
+
function admitPayload(value) {
|
|
818
|
+
if (value === void 0) return void 0;
|
|
819
|
+
try {
|
|
820
|
+
const text = JSON.stringify(value);
|
|
821
|
+
if (text === void 0) return { nonCanonical: `payload of type ${typeof value}` };
|
|
822
|
+
return JSON.parse(text);
|
|
823
|
+
} catch (error) {
|
|
824
|
+
return { nonCanonical: error instanceof Error ? error.message : String(error) };
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Run a graph: host every node instance on one kernel `Scope`, resolve joins over guarded edges,
|
|
829
|
+
* enforce the traversal and visit caps, and reduce the terminal settlements through the finalizer.
|
|
830
|
+
*/
|
|
831
|
+
async function runEngineGraph(engine, spec, task, options) {
|
|
832
|
+
const compiled = "nodes" in spec && spec.nodes instanceof Map ? spec : compileGraph(engine, spec);
|
|
833
|
+
const context = "runEngineGraph";
|
|
834
|
+
for (const node of compiled.nodes.values()) {
|
|
835
|
+
const missing = node.kind.effects.filter((name) => !(name in engine.effects));
|
|
836
|
+
if (missing.length > 0) throw new ValidationError(`${context}: node ${node.id} needs effect(s) ${missing.join(", ")} the host did not provide`);
|
|
837
|
+
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`);
|
|
838
|
+
}
|
|
839
|
+
for (const edge of compiled.edges) if (edge.spec.directive !== void 0 && options.prompts === void 0) throw new ValidationError(`${context}: edge ${edge.id} carries a directive but options.prompts is absent`);
|
|
840
|
+
const now = options.now ?? Date.now;
|
|
841
|
+
const started = now();
|
|
842
|
+
const runId = options.runId ?? `graph:${contentAddress({
|
|
843
|
+
task,
|
|
844
|
+
at: started
|
|
845
|
+
}).slice(0, 18)}`;
|
|
846
|
+
const pool = createBudgetPool(options.budget, () => now() - started);
|
|
847
|
+
const journal = options.journal ?? new InMemorySpawnJournal();
|
|
848
|
+
const blobs = options.blobs ?? new InMemoryResultBlobStore();
|
|
849
|
+
await journal.beginTree(runId, new Date(started).toISOString());
|
|
850
|
+
const abort = new AbortController();
|
|
851
|
+
const onOuterAbort = () => abort.abort(options.signal?.reason);
|
|
852
|
+
options.signal?.addEventListener("abort", onOuterAbort, { once: true });
|
|
853
|
+
if (options.signal?.aborted) abort.abort(options.signal.reason);
|
|
854
|
+
const scope = createScope({
|
|
855
|
+
parentId: runId,
|
|
856
|
+
root: runId,
|
|
857
|
+
pool,
|
|
858
|
+
journal,
|
|
859
|
+
blobs,
|
|
860
|
+
executors: createExecutorRegistry(),
|
|
861
|
+
seams: {},
|
|
862
|
+
depth: 0,
|
|
863
|
+
signal: abort.signal,
|
|
864
|
+
now
|
|
865
|
+
});
|
|
866
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
867
|
+
for (const id of compiled.nodes.keys()) nodes.set(id, {
|
|
868
|
+
visits: 0,
|
|
869
|
+
live: 0,
|
|
870
|
+
settles: [],
|
|
871
|
+
inputs: /* @__PURE__ */ new Map(),
|
|
872
|
+
directives: [],
|
|
873
|
+
traces: [],
|
|
874
|
+
blocked: false
|
|
875
|
+
});
|
|
876
|
+
const edges = /* @__PURE__ */ new Map();
|
|
877
|
+
for (const edge of compiled.edges) edges.set(edge.id, {
|
|
878
|
+
edge,
|
|
879
|
+
state: "pending",
|
|
880
|
+
consumedOnce: false,
|
|
881
|
+
traversals: 0,
|
|
882
|
+
payload: void 0
|
|
883
|
+
});
|
|
884
|
+
const ledger = [];
|
|
885
|
+
const exhaustedEdges = /* @__PURE__ */ new Set();
|
|
886
|
+
const settles = [];
|
|
887
|
+
const liveHandles = /* @__PURE__ */ new Map();
|
|
888
|
+
const waitingForBudget = [];
|
|
889
|
+
let ledgerSeq = 0;
|
|
890
|
+
let liveCount = 0;
|
|
891
|
+
let failure;
|
|
892
|
+
const record = (runtime, outcome, reason) => {
|
|
893
|
+
runtime.traversals += 1;
|
|
894
|
+
const spec = runtime.edge.spec;
|
|
895
|
+
const entry = {
|
|
896
|
+
edge: runtime.edge.id,
|
|
897
|
+
kind: spec.kind,
|
|
898
|
+
from: spec.from.node,
|
|
899
|
+
to: spec.to.node,
|
|
900
|
+
traversal: runtime.traversals,
|
|
901
|
+
outcome,
|
|
902
|
+
...spec.directive !== void 0 ? { directive: `${spec.directive.surface}/v${spec.directive.version}` } : {},
|
|
903
|
+
...spec.kind === "data" ? { port: runtime.edge.toPort } : {},
|
|
904
|
+
...reason !== void 0 ? { reason } : {}
|
|
905
|
+
};
|
|
906
|
+
ledger.push(entry);
|
|
907
|
+
journal.appendEvent(runId, {
|
|
908
|
+
kind: "edge",
|
|
909
|
+
id: `graph:${spec.to.node}`,
|
|
910
|
+
edge: {
|
|
911
|
+
kind: spec.kind,
|
|
912
|
+
from: spec.from.node,
|
|
913
|
+
to: spec.to.node,
|
|
914
|
+
...entry.directive !== void 0 ? { directive: entry.directive } : {},
|
|
915
|
+
...entry.port !== void 0 ? { port: entry.port } : {}
|
|
916
|
+
},
|
|
917
|
+
traversal: entry.traversal,
|
|
918
|
+
outcome: outcome === "delivered" ? "delivered" : outcome,
|
|
919
|
+
bytes: 0,
|
|
920
|
+
...reason !== void 0 ? { reason } : {},
|
|
921
|
+
seq: ledgerSeq++,
|
|
922
|
+
at: new Date(now()).toISOString()
|
|
923
|
+
});
|
|
924
|
+
};
|
|
925
|
+
/** Enter a node: count the visit, build its Agent from consumed inputs, spawn on the Scope. */
|
|
926
|
+
const enter = (id) => {
|
|
927
|
+
if (failure) return;
|
|
928
|
+
const node = compiled.nodes.get(id);
|
|
929
|
+
const runtime = nodes.get(id);
|
|
930
|
+
if (!node || !runtime) throw new ValidationError(`${context}: unknown node ${id}`);
|
|
931
|
+
runtime.visits += 1;
|
|
932
|
+
if (runtime.visits > node.maxVisits) {
|
|
933
|
+
failure = {
|
|
934
|
+
reason: "cycle-budget-exceeded",
|
|
935
|
+
error: {
|
|
936
|
+
name: "GraphCycleBudget",
|
|
937
|
+
message: `node ${id} entered ${runtime.visits} times; maxVisits ${node.maxVisits}`
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
abort.abort(`cycle-budget-exceeded: ${id}`);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
const inputs = {};
|
|
944
|
+
for (const [port, value] of runtime.inputs) inputs[port] = value;
|
|
945
|
+
const directives = runtime.directives.splice(0);
|
|
946
|
+
const traces = runtime.traces.splice(0);
|
|
947
|
+
const nodeTask = [
|
|
948
|
+
id === compiled.root ? task : "",
|
|
949
|
+
...directives,
|
|
950
|
+
...traces.map((trace) => `trace of ${trace.node}: ${trace.traceRef ?? "(no traceRef)"}`)
|
|
951
|
+
].filter((part) => part.length > 0).join("\n\n");
|
|
952
|
+
const effects = narrowEffects(node.kind.effects, engine.effects, `${context}: node ${id}`);
|
|
953
|
+
const agent = node.kind.run({
|
|
954
|
+
config: node.config,
|
|
955
|
+
profile: {
|
|
956
|
+
name: id,
|
|
957
|
+
...node.spec.profile ?? {}
|
|
958
|
+
},
|
|
959
|
+
inputs,
|
|
960
|
+
effects
|
|
961
|
+
});
|
|
962
|
+
const budget = node.spec.budget ?? options.perNode;
|
|
963
|
+
const spawned = scope.spawn(agent, nodeTask.length > 0 ? nodeTask : task, {
|
|
964
|
+
label: id,
|
|
965
|
+
budget
|
|
966
|
+
});
|
|
967
|
+
if (spawned.ok) {
|
|
968
|
+
liveCount += 1;
|
|
969
|
+
runtime.live += 1;
|
|
970
|
+
liveHandles.set(spawned.handle.id, id);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
if (spawned.reason === "budget-exhausted" || spawned.reason === "max-live-workers") {
|
|
974
|
+
waitingForBudget.push(id);
|
|
975
|
+
runtime.visits -= 1;
|
|
976
|
+
runtime.directives.unshift(...directives);
|
|
977
|
+
runtime.traces.unshift(...traces);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
failure = {
|
|
981
|
+
reason: "driver-failed",
|
|
982
|
+
error: {
|
|
983
|
+
name: "SpawnRefused",
|
|
984
|
+
message: `node ${id}: ${spawned.reason}`
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
abort.abort(`spawn refused: ${id}`);
|
|
988
|
+
};
|
|
989
|
+
/** Join evaluation per ADC's rules; returns whether the node released. */
|
|
990
|
+
const tryRelease = (id) => {
|
|
991
|
+
const node = compiled.nodes.get(id);
|
|
992
|
+
const runtime = nodes.get(id);
|
|
993
|
+
if (!node || !runtime || runtime.blocked || failure) return false;
|
|
994
|
+
const gating = node.inbound.map((edge) => edges.get(edge.id));
|
|
995
|
+
if (gating.length === 0) return false;
|
|
996
|
+
const settled = gating.filter((edge) => edge.state !== "pending");
|
|
997
|
+
const satisfied = gating.filter((edge) => edge.state === "satisfied");
|
|
998
|
+
const failed = gating.filter((edge) => edge.state === "failed");
|
|
999
|
+
const allSettled = settled.length === gating.length;
|
|
1000
|
+
let release = false;
|
|
1001
|
+
switch (node.join) {
|
|
1002
|
+
case "all":
|
|
1003
|
+
if (gating.some((edge) => edge.state === "dead" || edge.state === "failed")) release = false;
|
|
1004
|
+
else release = satisfied.length === gating.length;
|
|
1005
|
+
break;
|
|
1006
|
+
case "any":
|
|
1007
|
+
release = satisfied.length > 0;
|
|
1008
|
+
if (!release && allSettled) runtime.blocked = true;
|
|
1009
|
+
break;
|
|
1010
|
+
case "any_failed":
|
|
1011
|
+
release = failed.length > 0;
|
|
1012
|
+
if (!release && allSettled) runtime.blocked = true;
|
|
1013
|
+
break;
|
|
1014
|
+
case "all_done":
|
|
1015
|
+
release = allSettled;
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
if (!release) return false;
|
|
1019
|
+
const consuming = node.join === "any" ? [satisfied[0]] : node.join === "any_failed" ? [failed[0]] : settled;
|
|
1020
|
+
for (const runtimeEdge of consuming) {
|
|
1021
|
+
const cap = runtimeEdge.edge.spec.maxTraversals;
|
|
1022
|
+
if (cap !== void 0 && runtimeEdge.traversals >= cap) {
|
|
1023
|
+
record(runtimeEdge, "unpropagated", `traversal-cap-exhausted (max ${cap})`);
|
|
1024
|
+
runtimeEdge.traversals -= 1;
|
|
1025
|
+
exhaustedEdges.add(runtimeEdge.edge.id);
|
|
1026
|
+
runtime.blocked = node.join === "any" || node.join === "any_failed" ? runtime.blocked : true;
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
for (const runtimeEdge of consuming) {
|
|
1031
|
+
const spec = runtimeEdge.edge.spec;
|
|
1032
|
+
if (runtimeEdge.state === "satisfied" && spec.kind === "data") {
|
|
1033
|
+
let payload = runtimeEdge.payload;
|
|
1034
|
+
let outcome = "delivered";
|
|
1035
|
+
let reason;
|
|
1036
|
+
if (spec.projection !== void 0) try {
|
|
1037
|
+
payload = admitPayload(applyProjection(payload, spec.projection, `edge ${runtimeEdge.edge.id}`));
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
outcome = "empty";
|
|
1040
|
+
reason = error instanceof Error ? error.message : String(error);
|
|
1041
|
+
payload = void 0;
|
|
1042
|
+
}
|
|
1043
|
+
if (payload === void 0 && outcome === "delivered") outcome = "empty";
|
|
1044
|
+
record(runtimeEdge, outcome, reason);
|
|
1045
|
+
if (payload !== void 0) runtime.inputs.set(runtimeEdge.edge.toPort, payload);
|
|
1046
|
+
} else if (runtimeEdge.state === "satisfied" || node.join === "any_failed" || node.join === "all_done") {
|
|
1047
|
+
if (spec.directive !== void 0 && options.prompts !== void 0) runtime.directives.push(options.prompts.resolve(spec.directive).text);
|
|
1048
|
+
if (spec.kind === "analyzes" && runtimeEdge.state === "satisfied") {
|
|
1049
|
+
const trace = runtimeEdge.payload;
|
|
1050
|
+
runtime.traces.push({
|
|
1051
|
+
node: spec.from.node,
|
|
1052
|
+
...trace?.traceRef !== void 0 ? { traceRef: trace.traceRef } : {}
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
record(runtimeEdge, "delivered");
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
for (const runtimeEdge of gating) if (runtimeEdge.state !== "pending") {
|
|
1059
|
+
runtimeEdge.state = "pending";
|
|
1060
|
+
runtimeEdge.payload = void 0;
|
|
1061
|
+
} else if ((nodes.get(runtimeEdge.edge.spec.from.node)?.live ?? 0) > 0) runtimeEdge.consumedOnce = true;
|
|
1062
|
+
enter(id);
|
|
1063
|
+
return true;
|
|
1064
|
+
};
|
|
1065
|
+
/** Settle every outbound edge of a completed source per its LATEST completion, then try joins. */
|
|
1066
|
+
const propagate = (id, settle) => {
|
|
1067
|
+
const node = compiled.nodes.get(id);
|
|
1068
|
+
if (!node) return;
|
|
1069
|
+
const succeeded = settle.status === "done" && settle.valid !== false;
|
|
1070
|
+
for (const edge of node.outbound) {
|
|
1071
|
+
const runtime = edges.get(edge.id);
|
|
1072
|
+
if (!runtime) continue;
|
|
1073
|
+
if (runtime.consumedOnce && runtime.state === "pending") {
|
|
1074
|
+
runtime.consumedOnce = false;
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
if (!succeeded) {
|
|
1078
|
+
runtime.state = "failed";
|
|
1079
|
+
runtime.payload = void 0;
|
|
1080
|
+
} else {
|
|
1081
|
+
const guardContext = {
|
|
1082
|
+
node: id,
|
|
1083
|
+
out: settle.out,
|
|
1084
|
+
visits: nodes.get(id)?.visits ?? 0,
|
|
1085
|
+
valid: settle.valid ?? true
|
|
1086
|
+
};
|
|
1087
|
+
const pass = edge.spec.guard === void 0 || evaluateCondition(edge.spec.guard, guardContext);
|
|
1088
|
+
runtime.state = pass ? "satisfied" : "dead";
|
|
1089
|
+
runtime.payload = pass ? edge.spec.kind === "analyzes" ? { traceRef: settle.traceRef } : settle.out : void 0;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
for (const edge of node.outbound) tryRelease(edge.spec.to.node);
|
|
1093
|
+
};
|
|
1094
|
+
const allTerminalsSettled = () => compiled.terminals.every((id) => (nodes.get(id)?.settles.length ?? 0) > 0);
|
|
1095
|
+
for (const id of compiled.entries) enter(id);
|
|
1096
|
+
while (!failure && liveCount > 0) {
|
|
1097
|
+
const settled = await scope.next();
|
|
1098
|
+
if (settled === null) break;
|
|
1099
|
+
const nodeId = liveHandles.get(settled.handle.id);
|
|
1100
|
+
if (nodeId === void 0) continue;
|
|
1101
|
+
liveHandles.delete(settled.handle.id);
|
|
1102
|
+
liveCount -= 1;
|
|
1103
|
+
const node = compiled.nodes.get(nodeId);
|
|
1104
|
+
const runtime = nodes.get(nodeId);
|
|
1105
|
+
if (!node || !runtime) continue;
|
|
1106
|
+
runtime.live -= 1;
|
|
1107
|
+
let entry;
|
|
1108
|
+
if (settled.kind === "done") {
|
|
1109
|
+
const admitted = admitPayload(settled.out);
|
|
1110
|
+
let valid;
|
|
1111
|
+
if (node.deliverable !== void 0) try {
|
|
1112
|
+
valid = await node.deliverable.check(admitted);
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
valid = false;
|
|
1115
|
+
}
|
|
1116
|
+
entry = {
|
|
1117
|
+
node: nodeId,
|
|
1118
|
+
visit: runtime.visits,
|
|
1119
|
+
status: "done",
|
|
1120
|
+
...valid !== void 0 ? { valid } : {},
|
|
1121
|
+
out: admitted,
|
|
1122
|
+
outRef: settled.outRef
|
|
1123
|
+
};
|
|
1124
|
+
if (settled.trace?.status === "available") entry.traceRef = settled.trace.traceRef;
|
|
1125
|
+
} else if (settled.kind === "down") entry = {
|
|
1126
|
+
node: nodeId,
|
|
1127
|
+
visit: runtime.visits,
|
|
1128
|
+
status: "down",
|
|
1129
|
+
reason: settled.reason
|
|
1130
|
+
};
|
|
1131
|
+
else continue;
|
|
1132
|
+
runtime.settles.push(entry);
|
|
1133
|
+
settles.push(entry);
|
|
1134
|
+
if (nodeId === compiled.root && entry.status === "done" && entry.valid === true) {
|
|
1135
|
+
abort.abort("root delivered");
|
|
1136
|
+
break;
|
|
1137
|
+
}
|
|
1138
|
+
propagate(nodeId, entry);
|
|
1139
|
+
if (allTerminalsSettled()) break;
|
|
1140
|
+
for (const parked of waitingForBudget.splice(0)) enter(parked);
|
|
1141
|
+
if (liveCount === 0 && !failure) break;
|
|
1142
|
+
}
|
|
1143
|
+
options.signal?.removeEventListener("abort", onOuterAbort);
|
|
1144
|
+
abort.abort("run complete");
|
|
1145
|
+
while (await scope.next() !== null);
|
|
1146
|
+
const finalize = async () => {
|
|
1147
|
+
const rows = compiled.terminals.flatMap((id) => (nodes.get(id)?.settles ?? []).map((settle) => ({
|
|
1148
|
+
id: settle.node,
|
|
1149
|
+
status: settle.status,
|
|
1150
|
+
valid: settle.status === "done" && settle.valid !== false,
|
|
1151
|
+
...settle.outRef !== void 0 ? { outRef: settle.outRef } : {}
|
|
1152
|
+
})));
|
|
1153
|
+
return runFinalizer(options.finalizer === void 0 || options.finalizer === "bestDelivered" ? bestDelivered : options.finalizer === "collectDelivered" ? collectDelivered : options.finalizer, {
|
|
1154
|
+
settled: rows,
|
|
1155
|
+
blobs,
|
|
1156
|
+
tree: scope.view,
|
|
1157
|
+
budget: scope.budget
|
|
1158
|
+
});
|
|
1159
|
+
};
|
|
1160
|
+
const terminals = compiled.terminals.flatMap((id) => nodes.get(id)?.settles ?? []);
|
|
1161
|
+
const finish = (result) => {
|
|
1162
|
+
if (result.kind === "no-winner" && exhaustedEdges.size > 0) throw new GraphEdgeCapError(Object.freeze([...exhaustedEdges]), Object.freeze([...ledger]), result);
|
|
1163
|
+
return result;
|
|
1164
|
+
};
|
|
1165
|
+
if (failure) return finish({
|
|
1166
|
+
kind: "no-winner",
|
|
1167
|
+
reason: failure.reason,
|
|
1168
|
+
...failure.error ? { error: failure.error } : {},
|
|
1169
|
+
terminals,
|
|
1170
|
+
settles,
|
|
1171
|
+
ledger,
|
|
1172
|
+
unreachable: []
|
|
1173
|
+
});
|
|
1174
|
+
if (options.signal?.aborted) return finish({
|
|
1175
|
+
kind: "no-winner",
|
|
1176
|
+
reason: "aborted",
|
|
1177
|
+
terminals,
|
|
1178
|
+
settles,
|
|
1179
|
+
ledger,
|
|
1180
|
+
unreachable: []
|
|
1181
|
+
});
|
|
1182
|
+
const out = compiled.terminals.some((id) => (nodes.get(id)?.settles ?? []).some((settle) => settle.status === "done" && settle.valid !== false)) ? await finalize() : void 0;
|
|
1183
|
+
if (out !== void 0) return {
|
|
1184
|
+
kind: "winner",
|
|
1185
|
+
out,
|
|
1186
|
+
terminals,
|
|
1187
|
+
settles,
|
|
1188
|
+
ledger
|
|
1189
|
+
};
|
|
1190
|
+
const unreachable = [...compiled.nodes.keys()].filter((id) => (nodes.get(id)?.settles.length ?? 0) === 0);
|
|
1191
|
+
return finish({
|
|
1192
|
+
kind: "no-winner",
|
|
1193
|
+
reason: compiled.terminals.some((id) => unreachable.includes(id)) ? unreachable.length === compiled.nodes.size ? "budget-exhausted" : "unreachable-terminal" : "all-children-down",
|
|
1194
|
+
terminals,
|
|
1195
|
+
settles,
|
|
1196
|
+
ledger,
|
|
1197
|
+
unreachable
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
//#endregion
|
|
1201
|
+
export { CONDITION_OPS, DEFAULT_MAX_NODE_VISITS, JOIN_RULES, MAX_MAX_NODE_VISITS, admitPayload, agentKind, applyProjection, compileGraph, createGraphEngine, createRegistry, evaluateCondition, formatRegistryHandle, kindHandle, narrowEffects, parseRegistryHandle, runEngineGraph, schemaAccepts, scriptKind, subgraphKind, supervisorKind, validateCondition, validateNodeKind, validateProjection };
|
|
379
1202
|
|
|
380
1203
|
//# sourceMappingURL=graph.js.map
|