pi-loop-graph-sdk 0.2.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/LICENSE +21 -0
- package/README-zh.md +416 -0
- package/README.md +414 -0
- package/ROADMAP.md +60 -0
- package/dist/adapter/complete-tool.d.ts +3 -0
- package/dist/adapter/complete-tool.js +53 -0
- package/dist/adapter/debug-log.d.ts +38 -0
- package/dist/adapter/debug-log.js +151 -0
- package/dist/adapter/extension.d.ts +2 -0
- package/dist/adapter/extension.js +11 -0
- package/dist/adapter/graph-execution-host.d.ts +73 -0
- package/dist/adapter/graph-execution-host.js +181 -0
- package/dist/adapter/isolated-graph-session.d.ts +75 -0
- package/dist/adapter/isolated-graph-session.js +313 -0
- package/dist/adapter/loop-graph-extension.d.ts +96 -0
- package/dist/adapter/loop-graph-extension.js +487 -0
- package/dist/adapter/mechanism-runtime.d.ts +97 -0
- package/dist/adapter/mechanism-runtime.js +670 -0
- package/dist/adapter/model-messages.d.ts +17 -0
- package/dist/adapter/model-messages.js +11 -0
- package/dist/adapter/observability.d.ts +88 -0
- package/dist/adapter/observability.js +31 -0
- package/dist/adapter/output-contract.d.ts +12 -0
- package/dist/adapter/output-contract.js +87 -0
- package/dist/adapter/pi-node-context.d.ts +132 -0
- package/dist/adapter/pi-node-context.js +619 -0
- package/dist/adapter/projection.d.ts +121 -0
- package/dist/adapter/projection.js +169 -0
- package/dist/adapter/skill-content.d.ts +16 -0
- package/dist/adapter/skill-content.js +16 -0
- package/dist/advanced.d.ts +32 -0
- package/dist/advanced.js +17 -0
- package/dist/builders/graph.d.ts +27 -0
- package/dist/builders/graph.js +39 -0
- package/dist/builders/node.d.ts +8 -0
- package/dist/builders/node.js +9 -0
- package/dist/builders/refs.d.ts +5 -0
- package/dist/builders/refs.js +10 -0
- package/dist/builders/route.d.ts +11 -0
- package/dist/builders/route.js +18 -0
- package/dist/core/context.d.ts +73 -0
- package/dist/core/context.js +229 -0
- package/dist/core/graph.d.ts +172 -0
- package/dist/core/graph.js +57 -0
- package/dist/core/json.d.ts +8 -0
- package/dist/core/json.js +46 -0
- package/dist/core/limits.d.ts +7 -0
- package/dist/core/limits.js +14 -0
- package/dist/core/mechanism.d.ts +88 -0
- package/dist/core/mechanism.js +5 -0
- package/dist/core/result.d.ts +41 -0
- package/dist/core/result.js +1 -0
- package/dist/core/schema.d.ts +8 -0
- package/dist/core/schema.js +23 -0
- package/dist/core/skill.d.ts +12 -0
- package/dist/core/skill.js +1 -0
- package/dist/host/baseline.d.ts +11 -0
- package/dist/host/baseline.js +4 -0
- package/dist/host/graph-catalog.d.ts +8 -0
- package/dist/host/graph-catalog.js +24 -0
- package/dist/host/graph-host.d.ts +53 -0
- package/dist/host/graph-host.js +181 -0
- package/dist/host/preflight.d.ts +17 -0
- package/dist/host/preflight.js +81 -0
- package/dist/host/skill-catalog.d.ts +24 -0
- package/dist/host/skill-catalog.js +92 -0
- package/dist/host/tool-catalog.d.ts +27 -0
- package/dist/host/tool-catalog.js +33 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +10 -0
- package/dist/replay/checkpoint.d.ts +40 -0
- package/dist/replay/checkpoint.js +57 -0
- package/dist/replay/events.d.ts +40 -0
- package/dist/replay/events.js +1 -0
- package/dist/replay/finalizer.d.ts +26 -0
- package/dist/replay/finalizer.js +117 -0
- package/dist/replay/html.d.ts +3 -0
- package/dist/replay/html.js +270 -0
- package/dist/replay/index.d.ts +13 -0
- package/dist/replay/index.js +7 -0
- package/dist/replay/model.d.ts +81 -0
- package/dist/replay/model.js +1 -0
- package/dist/replay/parser.d.ts +3 -0
- package/dist/replay/parser.js +332 -0
- package/dist/replay/recorder.d.ts +30 -0
- package/dist/replay/recorder.js +195 -0
- package/dist/replay/store.d.ts +41 -0
- package/dist/replay/store.js +94 -0
- package/dist/router.d.ts +4 -0
- package/dist/router.js +61 -0
- package/dist/runtime/event-bus.d.ts +101 -0
- package/dist/runtime/event-bus.js +18 -0
- package/dist/runtime/graph-runtime.d.ts +173 -0
- package/dist/runtime/graph-runtime.js +1293 -0
- package/dist/runtime/invocation-budget.d.ts +22 -0
- package/dist/runtime/invocation-budget.js +52 -0
- package/dist/runtime/mechanism-runtime.d.ts +92 -0
- package/dist/runtime/mechanism-runtime.js +387 -0
- package/dist/runtime.d.ts +91 -0
- package/dist/runtime.js +258 -0
- package/dist/tools-resolve.d.ts +20 -0
- package/dist/tools-resolve.js +52 -0
- package/dist/type.d.ts +593 -0
- package/dist/type.js +30 -0
- package/dist/validate.d.ts +25 -0
- package/dist/validate.js +203 -0
- package/package.json +69 -0
|
@@ -0,0 +1,1293 @@
|
|
|
1
|
+
import { DEFAULT_INVOCATION_LIMITS, resolveInvocationLimits } from "../core/limits.js";
|
|
2
|
+
import { ContextState } from "../core/context.js";
|
|
3
|
+
import { checkJsonSchemaValue } from "../core/schema.js";
|
|
4
|
+
import { resolveHostBaseline } from "../host/baseline.js";
|
|
5
|
+
import { CapabilityPreflightError, preflightGraphCapabilities, resolveNodeToolNames, } from "../host/preflight.js";
|
|
6
|
+
import { RUNTIME_PROTOCOL_TOOL_NAME, } from "../host/tool-catalog.js";
|
|
7
|
+
import { RuntimeEventBus } from "./event-bus.js";
|
|
8
|
+
import { InvocationBudget, InvocationBudgetExceededError, } from "./invocation-budget.js";
|
|
9
|
+
import { MechanismRuntime, MechanismRuntimeError } from "./mechanism-runtime.js";
|
|
10
|
+
import { decodeCheckpoint, encodeCheckpoint } from "../replay/checkpoint.js";
|
|
11
|
+
/** Lets an Agent host terminate execution with a stable Runtime failure. */
|
|
12
|
+
export class AgentExecutionFailure extends Error {
|
|
13
|
+
failure;
|
|
14
|
+
constructor(failure) {
|
|
15
|
+
super(failure.message);
|
|
16
|
+
this.failure = failure;
|
|
17
|
+
this.name = "AgentExecutionFailure";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
class RuntimeFailure extends Error {
|
|
21
|
+
failure;
|
|
22
|
+
constructor(failure) {
|
|
23
|
+
super(failure.message);
|
|
24
|
+
this.failure = failure;
|
|
25
|
+
this.name = "RuntimeFailure";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class GraphRuntime {
|
|
29
|
+
host;
|
|
30
|
+
eventBus;
|
|
31
|
+
mechanismRuns = new WeakMap();
|
|
32
|
+
invocationAgentHosts = new Map();
|
|
33
|
+
activeInvocations = new Map();
|
|
34
|
+
activeGraphMechanisms = new Map();
|
|
35
|
+
constructor(host = {}) {
|
|
36
|
+
this.host = host;
|
|
37
|
+
this.eventBus = host.eventBus ?? new RuntimeEventBus();
|
|
38
|
+
}
|
|
39
|
+
async execute(graph, input, options = {}) {
|
|
40
|
+
const resolvedOptions = typeof options === "number" ? { maxSteps: options } : options;
|
|
41
|
+
const root = Object.freeze({
|
|
42
|
+
rootRunId: crypto.randomUUID(),
|
|
43
|
+
startedAt: Date.now(),
|
|
44
|
+
budget: new InvocationBudget(tightenLimits(this.host.limits ?? DEFAULT_INVOCATION_LIMITS, resolvedOptions.limits)),
|
|
45
|
+
signal: resolvedOptions.signal,
|
|
46
|
+
baseline: resolveHostBaseline(this.host.baseline),
|
|
47
|
+
});
|
|
48
|
+
const maxSteps = resolvedOptions.maxSteps ?? Number.POSITIVE_INFINITY;
|
|
49
|
+
const mechanismRuntime = new MechanismRuntime(this.host.mechanismRuntime, (message) => this.eventBus.emit({
|
|
50
|
+
type: "runtime_warning",
|
|
51
|
+
rootRunId: root.rootRunId,
|
|
52
|
+
code: "unmanaged-mechanism-access",
|
|
53
|
+
message,
|
|
54
|
+
}));
|
|
55
|
+
this.eventBus.emit({
|
|
56
|
+
type: "root_started",
|
|
57
|
+
rootRunId: root.rootRunId,
|
|
58
|
+
graphId: graph.id,
|
|
59
|
+
graphVersion: graph.version,
|
|
60
|
+
});
|
|
61
|
+
this.eventBus.emit({
|
|
62
|
+
type: "host_baseline_selected",
|
|
63
|
+
rootRunId: root.rootRunId,
|
|
64
|
+
baseline: root.baseline.kind,
|
|
65
|
+
id: root.baseline.kind === "custom" ? root.baseline.id : undefined,
|
|
66
|
+
fingerprint: root.baseline.kind === "inherit" ? root.baseline.fingerprint : undefined,
|
|
67
|
+
});
|
|
68
|
+
if (root.baseline.kind !== "isolated") {
|
|
69
|
+
this.eventBus.emit({
|
|
70
|
+
type: "runtime_warning",
|
|
71
|
+
rootRunId: root.rootRunId,
|
|
72
|
+
code: "unsafe-host-baseline",
|
|
73
|
+
message: `Host baseline "${root.baseline.kind}" is not the isolated default`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
let outcome;
|
|
77
|
+
let hostMechanisms;
|
|
78
|
+
try {
|
|
79
|
+
hostMechanisms = await mechanismRuntime.open("host", root.rootRunId, this.host.mechanisms ?? [], {
|
|
80
|
+
rootRunId: root.rootRunId,
|
|
81
|
+
});
|
|
82
|
+
this.eventBus.emit({ type: "mechanism_scope_opened", rootRunId: root.rootRunId, installation: "host", count: hostMechanisms.invocations.length });
|
|
83
|
+
await mechanismRuntime.enter([hostMechanisms], "onRootEnter");
|
|
84
|
+
this.mechanismRuns.set(root, { runtime: mechanismRuntime, host: hostMechanisms });
|
|
85
|
+
const invocation = await this.runInvocation({
|
|
86
|
+
graph,
|
|
87
|
+
input: input,
|
|
88
|
+
boundary: "root",
|
|
89
|
+
root,
|
|
90
|
+
maxSteps,
|
|
91
|
+
mechanismRuntime,
|
|
92
|
+
hostMechanisms,
|
|
93
|
+
});
|
|
94
|
+
outcome = invocation.outcome;
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
outcome = failureOutcome(mapUnexpectedFailure(error, root.signal));
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
this.mechanismRuns.delete(root);
|
|
101
|
+
if (hostMechanisms) {
|
|
102
|
+
await mechanismRuntime.rootExit(hostMechanisms);
|
|
103
|
+
await mechanismRuntime.close(hostMechanisms);
|
|
104
|
+
this.eventBus.emit({ type: "mechanism_scope_closed", rootRunId: root.rootRunId, installation: "host", count: hostMechanisms.invocations.length });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const usage = root.budget.usage;
|
|
108
|
+
const steps = usage.nodeVisits;
|
|
109
|
+
const durationMs = Math.max(0, Date.now() - root.startedAt);
|
|
110
|
+
this.eventBus.emit({
|
|
111
|
+
type: "root_finished",
|
|
112
|
+
rootRunId: root.rootRunId,
|
|
113
|
+
status: outcome.status,
|
|
114
|
+
usage,
|
|
115
|
+
});
|
|
116
|
+
const common = {
|
|
117
|
+
rootRunId: root.rootRunId,
|
|
118
|
+
graphId: graph.id,
|
|
119
|
+
graphVersion: graph.version,
|
|
120
|
+
steps,
|
|
121
|
+
durationMs,
|
|
122
|
+
replay: Object.freeze({ mode: "off", status: "off" }),
|
|
123
|
+
};
|
|
124
|
+
if (outcome.status === "completed") {
|
|
125
|
+
return { ...common, status: "completed", output: outcome.output };
|
|
126
|
+
}
|
|
127
|
+
const failure = outcome.failure ?? runtimeFailure("runtime-error", "root", "Graph failed without a failure object");
|
|
128
|
+
return outcome.status === "cancelled"
|
|
129
|
+
? { ...common, status: "cancelled", failure: failure }
|
|
130
|
+
: { ...common, status: "failed", failure };
|
|
131
|
+
}
|
|
132
|
+
async runInvocation(request) {
|
|
133
|
+
const { graph, root, parent, boundary } = request;
|
|
134
|
+
const depth = (parent?.depth ?? 0) + 1;
|
|
135
|
+
const frames = request.sharedFrames ?? [];
|
|
136
|
+
const frameRevision = request.sharedFrameRevision ?? { value: 0 };
|
|
137
|
+
let state;
|
|
138
|
+
let graphMechanisms;
|
|
139
|
+
let graphError;
|
|
140
|
+
try {
|
|
141
|
+
this.assertNotCancelled(root);
|
|
142
|
+
const graphInput = this.validateSchemaBoundary(graph.input, request.input, "invalid-input", "graph", `Graph input is invalid for ${graph.id}@${graph.version}`);
|
|
143
|
+
root.budget.enterGraph(depth);
|
|
144
|
+
state = Object.freeze({
|
|
145
|
+
graphInvocationId: crypto.randomUUID(),
|
|
146
|
+
rootRunId: root.rootRunId,
|
|
147
|
+
parentGraphInvocationId: parent?.graphInvocationId,
|
|
148
|
+
graph: Object.freeze({ id: graph.id, version: graph.version }),
|
|
149
|
+
boundary,
|
|
150
|
+
depth,
|
|
151
|
+
frames,
|
|
152
|
+
frameRevision,
|
|
153
|
+
});
|
|
154
|
+
this.eventBus.emit({
|
|
155
|
+
type: "graph_entered",
|
|
156
|
+
rootRunId: root.rootRunId,
|
|
157
|
+
graphInvocationId: state.graphInvocationId,
|
|
158
|
+
parentGraphInvocationId: state.parentGraphInvocationId,
|
|
159
|
+
graphId: graph.id,
|
|
160
|
+
graphVersion: graph.version,
|
|
161
|
+
boundary,
|
|
162
|
+
depth,
|
|
163
|
+
});
|
|
164
|
+
this.activeInvocations.set(state.graphInvocationId, { state, parentId: state.parentGraphInvocationId });
|
|
165
|
+
if (boundary !== "root" && this.host.createInvocationAgentHost) {
|
|
166
|
+
const agentHost = await this.host.createInvocationAgentHost({ root, invocation: state });
|
|
167
|
+
this.invocationAgentHosts.set(state.graphInvocationId, agentHost);
|
|
168
|
+
}
|
|
169
|
+
let graphSkills;
|
|
170
|
+
try {
|
|
171
|
+
this.validateGraphTools(graph);
|
|
172
|
+
graphSkills = this.resolveSkills(graph.skills);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
const failure = error instanceof RuntimeFailure
|
|
176
|
+
? error.failure
|
|
177
|
+
: runtimeFailure("invalid-graph", "graph", errorMessage(error), false, undefined, error);
|
|
178
|
+
return this.exitInvocation(state, frames, failureOutcome(failure));
|
|
179
|
+
}
|
|
180
|
+
const contextState = new ContextState({
|
|
181
|
+
rootRunId: root.rootRunId,
|
|
182
|
+
graphInvocationId: state.graphInvocationId,
|
|
183
|
+
graph,
|
|
184
|
+
graphInput,
|
|
185
|
+
graphSkills,
|
|
186
|
+
frames,
|
|
187
|
+
frameRevision,
|
|
188
|
+
externalContributions: (nodeVisitId) => request.mechanismRuntime.contextContributions.filter((item) => item.lifetime === "root-run"
|
|
189
|
+
|| item.lifetime === "graph-invocation" && item.scopeId === state.graphInvocationId
|
|
190
|
+
|| item.lifetime === "node-visit" && item.scopeId === nodeVisitId
|
|
191
|
+
|| item.lifetime === "agent-run"),
|
|
192
|
+
});
|
|
193
|
+
try {
|
|
194
|
+
await contextState.initialize();
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
return this.exitInvocation(state, frames, failureOutcome(runtimeFailure("runtime-error", "graph", `Graph context materialization failed for ${graph.id}@${graph.version}`, false, undefined, error)));
|
|
198
|
+
}
|
|
199
|
+
graphMechanisms = await request.mechanismRuntime.open("graph", state.graphInvocationId, graph.mechanisms ?? [], {
|
|
200
|
+
rootRunId: root.rootRunId,
|
|
201
|
+
graphInvocationId: state.graphInvocationId,
|
|
202
|
+
}, contextState);
|
|
203
|
+
this.activeGraphMechanisms.set(state.graphInvocationId, graphMechanisms);
|
|
204
|
+
this.eventBus.emit({ type: "mechanism_scope_opened", rootRunId: root.rootRunId, graphInvocationId: state.graphInvocationId, installation: "graph", count: graphMechanisms.invocations.length });
|
|
205
|
+
await request.mechanismRuntime.enter([request.hostMechanisms, graphMechanisms], "onGraphEnter");
|
|
206
|
+
const entry = await firstMatchingEntry(graph, graphInput);
|
|
207
|
+
this.assertNotCancelled(root);
|
|
208
|
+
if (!entry) {
|
|
209
|
+
return this.exitInvocation(state, frames, failureOutcome(runtimeFailure("entry-not-found", "entry", `No Entry matched Graph input for ${graph.id}@${graph.version}`, false)));
|
|
210
|
+
}
|
|
211
|
+
let stageId = entry.to;
|
|
212
|
+
let nodeInput;
|
|
213
|
+
try {
|
|
214
|
+
nodeInput = entry.mapInput ? entry.mapInput(graphInput) : graphInput;
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
return this.exitInvocation(state, frames, failureOutcome(runtimeFailure("invalid-input", "entry", `Entry "${entry.id}" failed to map Graph input`, false, undefined, error)));
|
|
218
|
+
}
|
|
219
|
+
const visits = new Map();
|
|
220
|
+
for (let localStep = 0; localStep < request.maxSteps; localStep += 1) {
|
|
221
|
+
this.assertNotCancelled(root);
|
|
222
|
+
root.budget.enterNode();
|
|
223
|
+
const stage = graph.stages[stageId];
|
|
224
|
+
if (!stage) {
|
|
225
|
+
return this.exitInvocation(state, frames, failureOutcome(runtimeFailure("invalid-graph", "graph", `Stage not found: ${stageId}`, false, stageId)));
|
|
226
|
+
}
|
|
227
|
+
const visit = (visits.get(stageId) ?? 0) + 1;
|
|
228
|
+
visits.set(stageId, visit);
|
|
229
|
+
const nodeVisit = Object.freeze({
|
|
230
|
+
nodeVisitId: crypto.randomUUID(),
|
|
231
|
+
rootRunId: root.rootRunId,
|
|
232
|
+
graphInvocationId: state.graphInvocationId,
|
|
233
|
+
stageId,
|
|
234
|
+
visit,
|
|
235
|
+
});
|
|
236
|
+
this.eventBus.emit({
|
|
237
|
+
type: "node_entered",
|
|
238
|
+
rootRunId: root.rootRunId,
|
|
239
|
+
graphInvocationId: nodeVisit.graphInvocationId,
|
|
240
|
+
nodeVisitId: nodeVisit.nodeVisitId,
|
|
241
|
+
stageId,
|
|
242
|
+
visit,
|
|
243
|
+
});
|
|
244
|
+
const exitNode = () => this.eventBus.emit({
|
|
245
|
+
type: "node_exited",
|
|
246
|
+
rootRunId: root.rootRunId,
|
|
247
|
+
graphInvocationId: nodeVisit.graphInvocationId,
|
|
248
|
+
nodeVisitId: nodeVisit.nodeVisitId,
|
|
249
|
+
stageId: nodeVisit.stageId,
|
|
250
|
+
});
|
|
251
|
+
try {
|
|
252
|
+
nodeInput = this.validateSchemaBoundary(stage.node.input, nodeInput, "invalid-input", "node", `Node input is invalid at Stage "${stageId}"`, stageId);
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
const failure = error instanceof RuntimeFailure || error instanceof AgentExecutionFailure
|
|
256
|
+
? error.failure
|
|
257
|
+
: runtimeFailure("invalid-input", "node", errorMessage(error), false, stageId, error);
|
|
258
|
+
exitNode();
|
|
259
|
+
return this.exitInvocation(state, frames, failureOutcome(failure));
|
|
260
|
+
}
|
|
261
|
+
let completion;
|
|
262
|
+
try {
|
|
263
|
+
completion = await this.executeNode(graph, stage.node, nodeInput, root, state, nodeVisit, request.maxSteps, graphSkills, contextState, request.mechanismRuntime, request.hostMechanisms, graphMechanisms);
|
|
264
|
+
this.assertNotCancelled(root);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
const failure = error instanceof RuntimeFailure
|
|
268
|
+
? error.failure
|
|
269
|
+
: error instanceof MechanismRuntimeError
|
|
270
|
+
? runtimeFailure("mechanism-failed", error.failure.installation === "node" ? "node" : "graph", error.message, false, stageId, error)
|
|
271
|
+
: isCancellationError(error, root.signal)
|
|
272
|
+
? cancelledFailure(error)
|
|
273
|
+
: runtimeFailure("runtime-error", stage.node.kind === "agent" ? "agent" : "node", errorMessage(error), true, stageId, error);
|
|
274
|
+
exitNode();
|
|
275
|
+
return this.exitInvocation(state, frames, failureOutcome(failure));
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const result = this.validateSchemaBoundary(stage.node.output, completion.result, "validation-exhausted", stage.node.kind === "agent" ? "agent" : "node", `Node output is invalid at Stage "${stageId}"`, stageId);
|
|
279
|
+
completion = Object.freeze({ result });
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
const failure = error instanceof RuntimeFailure
|
|
283
|
+
? error.failure
|
|
284
|
+
: runtimeFailure("validation-exhausted", "node", errorMessage(error), false, stageId, error);
|
|
285
|
+
exitNode();
|
|
286
|
+
return this.exitInvocation(state, frames, failureOutcome(failure));
|
|
287
|
+
}
|
|
288
|
+
let connection;
|
|
289
|
+
try {
|
|
290
|
+
connection = await selectConnection(stage.route, completion);
|
|
291
|
+
this.assertNotCancelled(root);
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
if (error instanceof RuntimeFailure) {
|
|
295
|
+
exitNode();
|
|
296
|
+
return this.exitInvocation(state, frames, failureOutcome(error.failure));
|
|
297
|
+
}
|
|
298
|
+
if (isCancellationError(error, root.signal)) {
|
|
299
|
+
exitNode();
|
|
300
|
+
return this.exitInvocation(state, frames, failureOutcome(cancelledFailure(error)));
|
|
301
|
+
}
|
|
302
|
+
exitNode();
|
|
303
|
+
return this.exitInvocation(state, frames, failureOutcome(runtimeFailure("transition-failed", "route", `Route evaluation failed at Stage "${stageId}"`, true, stageId, error)));
|
|
304
|
+
}
|
|
305
|
+
if (!connection) {
|
|
306
|
+
exitNode();
|
|
307
|
+
return this.exitInvocation(state, frames, failureOutcome(runtimeFailure("no-route", "route", `No Connection matched at Stage "${stageId}"`, false, stageId)));
|
|
308
|
+
}
|
|
309
|
+
this.eventBus.emit({
|
|
310
|
+
type: "transition_selected",
|
|
311
|
+
rootRunId: root.rootRunId,
|
|
312
|
+
graphInvocationId: state.graphInvocationId,
|
|
313
|
+
nodeVisitId: nodeVisit.nodeVisitId,
|
|
314
|
+
stageId,
|
|
315
|
+
connectionId: connection.id,
|
|
316
|
+
target: connection.to,
|
|
317
|
+
});
|
|
318
|
+
const transitionInput = { completion };
|
|
319
|
+
try {
|
|
320
|
+
if (connection.transition.frame) {
|
|
321
|
+
frames.push(connection.transition.frame(transitionInput));
|
|
322
|
+
contextState.bumpMemoryRevision();
|
|
323
|
+
}
|
|
324
|
+
this.assertNotCancelled(root);
|
|
325
|
+
if (connection.to === "__graph_finish__") {
|
|
326
|
+
const output = connection.transition.output?.(transitionInput);
|
|
327
|
+
if (output === undefined) {
|
|
328
|
+
throw new Error(`Finish Connection "${connection.id}" did not produce output`);
|
|
329
|
+
}
|
|
330
|
+
this.assertNotCancelled(root);
|
|
331
|
+
const validatedOutput = this.validateSchemaBoundary(graph.output, output, "validation-exhausted", "graph", `Graph output is invalid for ${graph.id}@${graph.version}`, stageId);
|
|
332
|
+
exitNode();
|
|
333
|
+
return this.exitInvocation(state, frames, completedOutcome(validatedOutput));
|
|
334
|
+
}
|
|
335
|
+
nodeInput = connection.transition.map?.(transitionInput) ?? completion.result;
|
|
336
|
+
this.assertNotCancelled(root);
|
|
337
|
+
stageId = connection.to;
|
|
338
|
+
exitNode();
|
|
339
|
+
// Node-boundary checkpoint: transition done, next Node not yet started.
|
|
340
|
+
await this.writeNodeCheckpoint(root, state, nodeVisit, stageId, nodeInput, frames, request.mechanismRuntime, request.hostMechanisms);
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
if (error instanceof RuntimeFailure) {
|
|
344
|
+
exitNode();
|
|
345
|
+
return this.exitInvocation(state, frames, failureOutcome(error.failure));
|
|
346
|
+
}
|
|
347
|
+
if (isCancellationError(error, root.signal)) {
|
|
348
|
+
exitNode();
|
|
349
|
+
return this.exitInvocation(state, frames, failureOutcome(cancelledFailure(error)));
|
|
350
|
+
}
|
|
351
|
+
exitNode();
|
|
352
|
+
return this.exitInvocation(state, frames, failureOutcome(runtimeFailure("transition-failed", "transition", `Transition "${connection.id}" failed`, true, stageId, error)));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return this.exitInvocation(state, frames, failureOutcome(runtimeFailure("max-steps-exceeded", "graph", `Graph Invocation exceeded maxSteps ${request.maxSteps}`, false, stageId)));
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
graphError = error;
|
|
359
|
+
const failure = mapUnexpectedFailure(error, root.signal);
|
|
360
|
+
if (!state)
|
|
361
|
+
return { outcome: failureOutcome(failure), frames: Object.freeze([...frames]) };
|
|
362
|
+
return this.exitInvocation(state, frames, failureOutcome(failure));
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
if (state) {
|
|
366
|
+
const agentHost = this.invocationAgentHosts.get(state.graphInvocationId);
|
|
367
|
+
this.invocationAgentHosts.delete(state.graphInvocationId);
|
|
368
|
+
this.activeInvocations.delete(state.graphInvocationId);
|
|
369
|
+
await agentHost?.dispose();
|
|
370
|
+
}
|
|
371
|
+
if (graphMechanisms) {
|
|
372
|
+
this.activeGraphMechanisms.delete(state.graphInvocationId);
|
|
373
|
+
await request.mechanismRuntime.graphExit([request.hostMechanisms, graphMechanisms], graphError);
|
|
374
|
+
await request.mechanismRuntime.close(graphMechanisms);
|
|
375
|
+
this.eventBus.emit({ type: "mechanism_scope_closed", rootRunId: root.rootRunId, graphInvocationId: state?.graphInvocationId, installation: "graph", count: graphMechanisms.invocations.length });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
exitInvocation(state, frames, outcome) {
|
|
380
|
+
this.eventBus.emit({
|
|
381
|
+
type: "graph_exited",
|
|
382
|
+
rootRunId: state.rootRunId,
|
|
383
|
+
graphInvocationId: state.graphInvocationId,
|
|
384
|
+
status: outcome.status,
|
|
385
|
+
failure: outcome.failure,
|
|
386
|
+
});
|
|
387
|
+
return { outcome, frames: Object.freeze([...frames]) };
|
|
388
|
+
}
|
|
389
|
+
async executeNode(graph, node, input, root, invocation, nodeVisit, maxSteps, graphSkills, contextState, mechanismRuntime, hostMechanisms, graphMechanisms) {
|
|
390
|
+
const capabilities = this.resolveNodeCapabilities(graph, nodeVisit.stageId, node, graphSkills, root, invocation);
|
|
391
|
+
const nodeMechanisms = await mechanismRuntime.open("node", nodeVisit.nodeVisitId, node.mechanisms ?? [], {
|
|
392
|
+
rootRunId: root.rootRunId,
|
|
393
|
+
graphInvocationId: invocation.graphInvocationId,
|
|
394
|
+
nodeVisitId: nodeVisit.nodeVisitId,
|
|
395
|
+
stageId: nodeVisit.stageId,
|
|
396
|
+
}, contextState);
|
|
397
|
+
this.eventBus.emit({ type: "mechanism_scope_opened", rootRunId: root.rootRunId, graphInvocationId: invocation.graphInvocationId, nodeVisitId: nodeVisit.nodeVisitId, installation: "node", count: nodeMechanisms.invocations.length });
|
|
398
|
+
const chains = [hostMechanisms, graphMechanisms, nodeMechanisms];
|
|
399
|
+
try {
|
|
400
|
+
await mechanismRuntime.enter(chains, "onNodeEnter");
|
|
401
|
+
const { snapshot } = await contextState.materializeNode(nodeVisit.nodeVisitId, nodeVisit.stageId, graph.stages[nodeVisit.stageId], input, capabilities.skills);
|
|
402
|
+
this.eventBus.emit({
|
|
403
|
+
type: "context_snapshot_materialized",
|
|
404
|
+
rootRunId: root.rootRunId,
|
|
405
|
+
graphInvocationId: invocation.graphInvocationId,
|
|
406
|
+
nodeVisitId: nodeVisit.nodeVisitId,
|
|
407
|
+
memoryRevision: snapshot.memoryRevision,
|
|
408
|
+
layerCount: snapshot.layers.length,
|
|
409
|
+
});
|
|
410
|
+
let result;
|
|
411
|
+
if (node.kind === "agent") {
|
|
412
|
+
result = await this.runAgent(graph, node, input, root, invocation, nodeVisit, 1, maxSteps, capabilities.tools, capabilities.skills, snapshot, contextState, mechanismRuntime, chains);
|
|
413
|
+
}
|
|
414
|
+
else if (node.kind === "graph") {
|
|
415
|
+
result = await this.executeGraphNode(node, input, root, invocation, maxSteps);
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
let agentIndex = 0;
|
|
419
|
+
result = await node.execute({
|
|
420
|
+
input: input,
|
|
421
|
+
complete: (value) => value,
|
|
422
|
+
rootRunId: root.rootRunId,
|
|
423
|
+
nodeVisitId: nodeVisit.nodeVisitId,
|
|
424
|
+
runAgent: async (agentRequest) => ({
|
|
425
|
+
result: await this.runCodeAgent(agentRequest, node, root, invocation, nodeVisit, ++agentIndex, maxSteps, capabilities.tools, capabilities.skills, snapshot, contextState, mechanismRuntime, chains),
|
|
426
|
+
}),
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
await mechanismRuntime.nodeExit(chains, result);
|
|
430
|
+
return { result };
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
await mechanismRuntime.nodeError(chains, error);
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
finally {
|
|
437
|
+
await mechanismRuntime.close(nodeMechanisms);
|
|
438
|
+
this.eventBus.emit({ type: "mechanism_scope_closed", rootRunId: root.rootRunId, graphInvocationId: invocation.graphInvocationId, nodeVisitId: nodeVisit.nodeVisitId, installation: "node", count: nodeMechanisms.invocations.length });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async runAgent(graph, node, input, root, invocation, nodeVisit, index, maxSteps, tools, skills, snapshot, contextState, mechanismRuntime, mechanismChains) {
|
|
442
|
+
const runAgent = this.invocationAgentHosts.get(invocation.graphInvocationId)?.runAgent ?? this.host.runAgent;
|
|
443
|
+
if (!runAgent)
|
|
444
|
+
throw new RuntimeFailure(runtimeFailure("host-unavailable", "host", "Agent Node requires host.runAgent", false, nodeVisit.stageId));
|
|
445
|
+
const state = this.beginAgent(root, invocation, nodeVisit, index);
|
|
446
|
+
try {
|
|
447
|
+
await mechanismRuntime.beforeAgentRun(mechanismChains, state.agentRunId, node.prompt ?? node.subGoal);
|
|
448
|
+
return await runAgent(node, input, this.createAgentContext(root, invocation, nodeVisit, state, maxSteps, tools, skills, contextState.refreshSnapshot(snapshot, state.agentRunId), node.output, graph.stages[nodeVisit.stageId]?.route, mechanismRuntime, mechanismChains, mechanismChains.at(-1)));
|
|
449
|
+
}
|
|
450
|
+
finally {
|
|
451
|
+
await mechanismRuntime.afterAgentRun(mechanismChains, state.agentRunId);
|
|
452
|
+
this.finishAgent(state);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async runCodeAgent(request, node, root, invocation, nodeVisit, index, maxSteps, tools, skills, snapshot, contextState, mechanismRuntime, mechanismChains) {
|
|
456
|
+
const runAgentFromCode = this.invocationAgentHosts.get(invocation.graphInvocationId)?.runAgentFromCode ?? this.host.runAgentFromCode;
|
|
457
|
+
if (!runAgentFromCode)
|
|
458
|
+
throw new RuntimeFailure(runtimeFailure("host-unavailable", "host", "Code Node runAgent requires host.runAgentFromCode", false, nodeVisit.stageId));
|
|
459
|
+
const state = this.beginAgent(root, invocation, nodeVisit, index);
|
|
460
|
+
try {
|
|
461
|
+
await mechanismRuntime.beforeAgentRun(mechanismChains, state.agentRunId, request.prompt);
|
|
462
|
+
return await runAgentFromCode(request, node, this.createAgentContext(root, invocation, nodeVisit, state, maxSteps, tools, skills, contextState.refreshSnapshot(snapshot, state.agentRunId), request.output, undefined, mechanismRuntime, mechanismChains, mechanismChains.at(-1)));
|
|
463
|
+
}
|
|
464
|
+
finally {
|
|
465
|
+
await mechanismRuntime.afterAgentRun(mechanismChains, state.agentRunId);
|
|
466
|
+
this.finishAgent(state);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
beginAgent(root, invocation, nodeVisit, index) {
|
|
470
|
+
const state = Object.freeze({
|
|
471
|
+
agentRunId: crypto.randomUUID(),
|
|
472
|
+
rootRunId: root.rootRunId,
|
|
473
|
+
graphInvocationId: invocation.graphInvocationId,
|
|
474
|
+
nodeVisitId: nodeVisit.nodeVisitId,
|
|
475
|
+
index,
|
|
476
|
+
});
|
|
477
|
+
this.eventBus.emit({ type: "agent_started", ...state });
|
|
478
|
+
return state;
|
|
479
|
+
}
|
|
480
|
+
finishAgent(state) {
|
|
481
|
+
this.eventBus.emit({ type: "agent_finished", ...state });
|
|
482
|
+
}
|
|
483
|
+
createAgentContext(root, invocation, nodeVisit, agentRun, maxSteps, tools, skills, snapshot, outputSchema, route, mechanismRuntime, mechanismChains, mechanisms) {
|
|
484
|
+
const agentSnapshot = Object.freeze({ ...snapshot, agentRunId: agentRun.agentRunId });
|
|
485
|
+
this.assertStickyContextBudget(agentSnapshot, nodeVisit.stageId);
|
|
486
|
+
return Object.freeze({
|
|
487
|
+
root,
|
|
488
|
+
invocation,
|
|
489
|
+
nodeVisit,
|
|
490
|
+
agentRun,
|
|
491
|
+
tools,
|
|
492
|
+
skills,
|
|
493
|
+
baseline: root.baseline,
|
|
494
|
+
snapshot: agentSnapshot,
|
|
495
|
+
mechanisms,
|
|
496
|
+
validateNodeCompletion: async (result) => {
|
|
497
|
+
try {
|
|
498
|
+
const checked = checkJsonSchemaValue(outputSchema, result);
|
|
499
|
+
return checked.valid
|
|
500
|
+
? { valid: true }
|
|
501
|
+
: { valid: false, reason: `Node output is invalid at Stage "${nodeVisit.stageId}": ${checked.message ?? "schema validation failed"}` };
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
return { valid: false, reason: `Invalid Node output schema: ${errorMessage(error)}` };
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
validateRouteStructure: async (result) => validateRouteStructure(route, result),
|
|
508
|
+
validateMechanismCompletion: (result) => mechanismRuntime.validateCompletion(mechanismChains, agentRun.agentRunId, result),
|
|
509
|
+
validateAgentChoice: async (result) => validateAgentChoice(route, result),
|
|
510
|
+
invokeGraph: (ref, input, boundary = "call") => this.invokeGraph(ref, input, boundary, root, invocation, maxSteps),
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
assertStickyContextBudget(snapshot, stageId) {
|
|
514
|
+
const limit = this.host.maxStickyContextBytes ?? 256 * 1024;
|
|
515
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
516
|
+
throw new RuntimeFailure(runtimeFailure("runtime-error", "host", "maxStickyContextBytes must be a positive integer", false, stageId));
|
|
517
|
+
}
|
|
518
|
+
const bytes = Buffer.byteLength(JSON.stringify(snapshot.layers
|
|
519
|
+
.filter((layer) => layer.retention === "sticky")
|
|
520
|
+
.map((layer) => layer.content)), "utf8");
|
|
521
|
+
if (bytes > limit) {
|
|
522
|
+
throw new RuntimeFailure(runtimeFailure("runtime-error", "agent", `Sticky context budget exceeded: ${bytes} bytes > ${limit} bytes`, false, stageId));
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async executeGraphNode(node, input, root, parent, maxSteps) {
|
|
526
|
+
const result = await this.invokeGraph(node.graph, input, node.boundary, root, parent, maxSteps);
|
|
527
|
+
if (result.status !== "completed" || result.output === undefined) {
|
|
528
|
+
throw new RuntimeFailure(result.failure ?? (result.status === "cancelled"
|
|
529
|
+
? cancelledFailure()
|
|
530
|
+
: runtimeFailure("runtime-error", "graph", `Child Graph failed: ${node.graph.id}`, true)));
|
|
531
|
+
}
|
|
532
|
+
return result.output;
|
|
533
|
+
}
|
|
534
|
+
async invokeGraph(ref, input, boundary, root, parent, maxSteps) {
|
|
535
|
+
const graph = this.resolveGraph(ref);
|
|
536
|
+
if (!graph) {
|
|
537
|
+
return failureOutcome(runtimeFailure("invalid-graph", "graph", `GraphRef not found: ${ref.id}@${ref.version}`, false));
|
|
538
|
+
}
|
|
539
|
+
const execute = async () => (await this.runInvocation({
|
|
540
|
+
graph,
|
|
541
|
+
input,
|
|
542
|
+
boundary,
|
|
543
|
+
root,
|
|
544
|
+
parent,
|
|
545
|
+
sharedFrames: boundary === "compose" ? parent.frames : undefined,
|
|
546
|
+
sharedFrameRevision: boundary === "compose" ? parent.frameRevision : undefined,
|
|
547
|
+
maxSteps,
|
|
548
|
+
mechanismRuntime: this.mechanismRuns.get(root)?.runtime ?? new MechanismRuntime(this.host.mechanismRuntime),
|
|
549
|
+
hostMechanisms: this.mechanismRuns.get(root)?.host ?? { invocations: [] },
|
|
550
|
+
})).outcome;
|
|
551
|
+
if (boundary !== "delegate")
|
|
552
|
+
return await execute();
|
|
553
|
+
if (!this.host.delegateGraph) {
|
|
554
|
+
return failureOutcome(runtimeFailure("host-unavailable", "host", `Delegate Host unavailable for GraphRef: ${ref.id}@${ref.version}`, false));
|
|
555
|
+
}
|
|
556
|
+
return await this.host.delegateGraph({ graph, input, root, parentInvocation: parent, execute });
|
|
557
|
+
}
|
|
558
|
+
validateGraphTools(graph) {
|
|
559
|
+
try {
|
|
560
|
+
preflightGraphCapabilities(graph, this.host);
|
|
561
|
+
}
|
|
562
|
+
catch (error) {
|
|
563
|
+
if (error instanceof CapabilityPreflightError) {
|
|
564
|
+
throw new RuntimeFailure(runtimeFailure(error.code, error.phase, error.message, false, error.stageId, error));
|
|
565
|
+
}
|
|
566
|
+
throw error;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
resolveNodeCapabilities(graph, stageId, node, graphSkills, root, invocation) {
|
|
570
|
+
const policy = graph.tools ?? [];
|
|
571
|
+
let names;
|
|
572
|
+
try {
|
|
573
|
+
names = resolveNodeToolNames(graph, stageId, node, this.host);
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
if (error instanceof CapabilityPreflightError) {
|
|
577
|
+
throw new RuntimeFailure(runtimeFailure(error.code, error.phase, error.message, false, error.stageId, error));
|
|
578
|
+
}
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
581
|
+
if (this.host.unsafeToolResolver) {
|
|
582
|
+
const outsidePolicy = [...new Set(names)].filter((name) => name !== RUNTIME_PROTOCOL_TOOL_NAME && !policy.includes(name));
|
|
583
|
+
if (outsidePolicy.length > 0) {
|
|
584
|
+
this.eventBus.emit({
|
|
585
|
+
type: "runtime_warning",
|
|
586
|
+
rootRunId: root.rootRunId,
|
|
587
|
+
graphInvocationId: invocation.graphInvocationId,
|
|
588
|
+
stageId,
|
|
589
|
+
code: "unsafe-tool-policy-bypass",
|
|
590
|
+
message: `Unsafe resolver enabled tools outside Graph policy: ${outsidePolicy.join(", ")}`,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
const tools = [];
|
|
595
|
+
for (const name of [...new Set(names)]) {
|
|
596
|
+
if (name === RUNTIME_PROTOCOL_TOOL_NAME)
|
|
597
|
+
continue;
|
|
598
|
+
const tool = this.host.toolCatalog?.resolve(name);
|
|
599
|
+
if (!tool) {
|
|
600
|
+
throw new RuntimeFailure(runtimeFailure("tool-unavailable", "host", `Host tool unavailable: ${name}`, false, stageId));
|
|
601
|
+
}
|
|
602
|
+
tools.push(tool);
|
|
603
|
+
}
|
|
604
|
+
const protocolTools = this.host.protocolTools ?? [Object.freeze({
|
|
605
|
+
name: RUNTIME_PROTOCOL_TOOL_NAME,
|
|
606
|
+
protocol: true,
|
|
607
|
+
})];
|
|
608
|
+
const completionTool = protocolTools.find((tool) => tool.name === RUNTIME_PROTOCOL_TOOL_NAME);
|
|
609
|
+
if (!completionTool) {
|
|
610
|
+
throw new RuntimeFailure(runtimeFailure("host-unavailable", "host", `Runtime protocol tool unavailable: ${RUNTIME_PROTOCOL_TOOL_NAME}`, false, stageId));
|
|
611
|
+
}
|
|
612
|
+
tools.push(completionTool);
|
|
613
|
+
return Object.freeze({
|
|
614
|
+
tools: Object.freeze(tools),
|
|
615
|
+
skills: Object.freeze([
|
|
616
|
+
...graphSkills,
|
|
617
|
+
...this.resolveSkills(node.skills),
|
|
618
|
+
]),
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
resolveSkills(refs) {
|
|
622
|
+
if (!refs?.length)
|
|
623
|
+
return Object.freeze([]);
|
|
624
|
+
const resolved = [];
|
|
625
|
+
for (const ref of refs) {
|
|
626
|
+
let skill;
|
|
627
|
+
try {
|
|
628
|
+
skill = this.host.skillCatalog?.resolve(ref);
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
if (!ref.required)
|
|
632
|
+
continue;
|
|
633
|
+
throw new RuntimeFailure(runtimeFailure("host-unavailable", "host", `Required Skill resolution failed: ${ref.name}${ref.version ? `@${ref.version}` : ""}: ${errorMessage(error)}`, false, undefined, error));
|
|
634
|
+
}
|
|
635
|
+
if (skill) {
|
|
636
|
+
resolved.push(skill);
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (ref.required) {
|
|
640
|
+
throw new RuntimeFailure(runtimeFailure("host-unavailable", "host", `Required Skill unavailable: ${ref.name}${ref.version ? `@${ref.version}` : ""}`, false));
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return Object.freeze(resolved);
|
|
644
|
+
}
|
|
645
|
+
resolveGraph(ref) {
|
|
646
|
+
return this.host.catalog?.resolve(ref) ?? this.host.resolveGraph?.(ref);
|
|
647
|
+
}
|
|
648
|
+
buildInvocationStack(rootRunId) {
|
|
649
|
+
// Walk all active invocations from root to deepest, ordered by depth.
|
|
650
|
+
const byParent = new Map();
|
|
651
|
+
for (const [id, entry] of this.activeInvocations) {
|
|
652
|
+
if (entry.state.rootRunId !== rootRunId)
|
|
653
|
+
continue;
|
|
654
|
+
const key = entry.parentId ?? undefined;
|
|
655
|
+
if (!byParent.has(key))
|
|
656
|
+
byParent.set(key, []);
|
|
657
|
+
byParent.get(key).push(id);
|
|
658
|
+
}
|
|
659
|
+
const stack = [];
|
|
660
|
+
let current = byParent.get(undefined)?.[0];
|
|
661
|
+
while (current) {
|
|
662
|
+
const entry = this.activeInvocations.get(current);
|
|
663
|
+
if (!entry)
|
|
664
|
+
break;
|
|
665
|
+
stack.push({
|
|
666
|
+
graphInvocationId: entry.state.graphInvocationId,
|
|
667
|
+
parentGraphInvocationId: entry.state.parentGraphInvocationId,
|
|
668
|
+
boundary: entry.state.boundary,
|
|
669
|
+
depth: entry.state.depth,
|
|
670
|
+
graph: entry.state.graph,
|
|
671
|
+
});
|
|
672
|
+
current = byParent.get(current)?.[0];
|
|
673
|
+
}
|
|
674
|
+
return Object.freeze(stack);
|
|
675
|
+
}
|
|
676
|
+
async writeNodeCheckpoint(root, invocation, _nodeVisit, nextStageId, nextNodeInput, frames, mechanismRuntime, hostMechanisms) {
|
|
677
|
+
const store = this.host.checkpointStore;
|
|
678
|
+
if (!store)
|
|
679
|
+
return;
|
|
680
|
+
try {
|
|
681
|
+
const invocationStack = this.buildInvocationStack(root.rootRunId);
|
|
682
|
+
// A nested continuation needs parent return/transition state in addition to
|
|
683
|
+
// the active stack. Until that is encoded, never persist a misleading point.
|
|
684
|
+
if (invocationStack.length !== 1 || invocation.boundary !== "root")
|
|
685
|
+
return;
|
|
686
|
+
const graphChains = [...this.activeGraphMechanisms.values()];
|
|
687
|
+
const mechanisms = mechanismRuntime.snapshotAll([hostMechanisms, ...graphChains]);
|
|
688
|
+
const checkpoint = Object.freeze({
|
|
689
|
+
kind: "node-boundary",
|
|
690
|
+
schemaVersion: 1,
|
|
691
|
+
checkpointId: crypto.randomUUID(),
|
|
692
|
+
createdAt: new Date().toISOString(),
|
|
693
|
+
rootRunId: root.rootRunId,
|
|
694
|
+
graph: invocation.graph,
|
|
695
|
+
invocationStack,
|
|
696
|
+
next: Object.freeze({ stageId: nextStageId, nodeInput: nextNodeInput, nodeVisitId: crypto.randomUUID() }),
|
|
697
|
+
frames: Object.freeze([...frames]),
|
|
698
|
+
budget: { ...root.budget.usage },
|
|
699
|
+
resumeAttempt: 0,
|
|
700
|
+
mechanisms,
|
|
701
|
+
});
|
|
702
|
+
await store.writeCheckpoint(root.rootRunId, checkpoint.checkpointId, encodeCheckpoint(checkpoint));
|
|
703
|
+
this.eventBus.emit({
|
|
704
|
+
type: "checkpoint_saved",
|
|
705
|
+
rootRunId: root.rootRunId,
|
|
706
|
+
graphInvocationId: invocation.graphInvocationId,
|
|
707
|
+
checkpointId: checkpoint.checkpointId,
|
|
708
|
+
nextStageId,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
// Checkpoint write failure must not alter business control flow.
|
|
713
|
+
this.eventBus.emit({
|
|
714
|
+
type: "runtime_warning",
|
|
715
|
+
rootRunId: root.rootRunId,
|
|
716
|
+
graphInvocationId: invocation.graphInvocationId,
|
|
717
|
+
code: "unmanaged-mechanism-access",
|
|
718
|
+
message: `Checkpoint write failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/** Resume a root graph run from the latest checkpoint. */
|
|
723
|
+
async resume(graph, options) {
|
|
724
|
+
const store = this.host.checkpointStore;
|
|
725
|
+
if (!store) {
|
|
726
|
+
return {
|
|
727
|
+
rootRunId: options.runId,
|
|
728
|
+
graphId: graph.id,
|
|
729
|
+
graphVersion: graph.version,
|
|
730
|
+
steps: 0,
|
|
731
|
+
durationMs: 0,
|
|
732
|
+
replay: Object.freeze({ mode: "off", status: "off" }),
|
|
733
|
+
status: "failed",
|
|
734
|
+
failure: runtimeFailure("resume-incompatible", "host", "No checkpoint store configured; cannot resume", false),
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
let checkpointIds;
|
|
738
|
+
try {
|
|
739
|
+
checkpointIds = await (store.listCheckpoints?.(options.runId) ?? []);
|
|
740
|
+
}
|
|
741
|
+
catch {
|
|
742
|
+
return {
|
|
743
|
+
rootRunId: options.runId,
|
|
744
|
+
graphId: graph.id,
|
|
745
|
+
graphVersion: graph.version,
|
|
746
|
+
steps: 0,
|
|
747
|
+
durationMs: 0,
|
|
748
|
+
replay: Object.freeze({ mode: "off", status: "off" }),
|
|
749
|
+
status: "failed",
|
|
750
|
+
failure: runtimeFailure("resume-incompatible", "host", "Failed to list checkpoints", false),
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
if (checkpointIds.length === 0) {
|
|
754
|
+
return {
|
|
755
|
+
rootRunId: options.runId,
|
|
756
|
+
graphId: graph.id,
|
|
757
|
+
graphVersion: graph.version,
|
|
758
|
+
steps: 0,
|
|
759
|
+
durationMs: 0,
|
|
760
|
+
replay: Object.freeze({ mode: "off", status: "off" }),
|
|
761
|
+
status: "failed",
|
|
762
|
+
failure: runtimeFailure("resume-incompatible", "host", `No checkpoints found for runId: ${options.runId}`, false),
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
// Checkpoint ids are opaque UUIDs. Inspect ordering metadata instead of
|
|
766
|
+
// assuming lexicographic filename order represents write order.
|
|
767
|
+
let latestId = "";
|
|
768
|
+
let checkpoint;
|
|
769
|
+
try {
|
|
770
|
+
for (const id of checkpointIds) {
|
|
771
|
+
const candidate = decodeCheckpoint(await store.readCheckpoint(options.runId, id));
|
|
772
|
+
if (!checkpoint || checkpointOrder(candidate) > checkpointOrder(checkpoint)) {
|
|
773
|
+
checkpoint = candidate;
|
|
774
|
+
latestId = id;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
return {
|
|
780
|
+
rootRunId: options.runId,
|
|
781
|
+
graphId: graph.id,
|
|
782
|
+
graphVersion: graph.version,
|
|
783
|
+
steps: 0,
|
|
784
|
+
durationMs: 0,
|
|
785
|
+
replay: Object.freeze({ mode: "off", status: "off" }),
|
|
786
|
+
status: "failed",
|
|
787
|
+
failure: runtimeFailure("resume-incompatible", "host", `Failed to read or decode checkpoint ${latestId || "unknown"}`, false, undefined, error),
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
if (!checkpoint)
|
|
791
|
+
throw new Error("Checkpoint selection failed");
|
|
792
|
+
// Version check
|
|
793
|
+
let resolvedGraphId = graph.id;
|
|
794
|
+
let resolvedGraphVersion = graph.version;
|
|
795
|
+
if (checkpoint.graph.id !== graph.id || checkpoint.graph.version !== graph.version) {
|
|
796
|
+
if (!options.checkpointMigrator) {
|
|
797
|
+
return {
|
|
798
|
+
rootRunId: options.runId,
|
|
799
|
+
graphId: graph.id,
|
|
800
|
+
graphVersion: graph.version,
|
|
801
|
+
steps: 0,
|
|
802
|
+
durationMs: 0,
|
|
803
|
+
replay: Object.freeze({ mode: "off", status: "off" }),
|
|
804
|
+
status: "failed",
|
|
805
|
+
failure: runtimeFailure("resume-incompatible", "host", `Checkpoint graph ${checkpoint.graph.id}@${checkpoint.graph.version} does not match ${graph.id}@${graph.version} and no migrator provided`, false),
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
const migrated = options.checkpointMigrator(checkpoint.graph);
|
|
809
|
+
if (migrated.id !== graph.id || migrated.version !== graph.version) {
|
|
810
|
+
return {
|
|
811
|
+
rootRunId: options.runId, graphId: graph.id, graphVersion: graph.version, steps: 0, durationMs: 0,
|
|
812
|
+
replay: Object.freeze({ mode: "off", status: "off" }), status: "failed",
|
|
813
|
+
failure: runtimeFailure("resume-incompatible", "host", `Checkpoint migrator resolved ${migrated.id}@${migrated.version}, expected ${graph.id}@${graph.version}`, false),
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
resolvedGraphId = migrated.id;
|
|
817
|
+
resolvedGraphVersion = migrated.version;
|
|
818
|
+
}
|
|
819
|
+
if (checkpoint.rootRunId !== options.runId) {
|
|
820
|
+
return {
|
|
821
|
+
rootRunId: options.runId,
|
|
822
|
+
graphId: graph.id,
|
|
823
|
+
graphVersion: graph.version,
|
|
824
|
+
steps: 0,
|
|
825
|
+
durationMs: 0,
|
|
826
|
+
replay: Object.freeze({ mode: "off", status: "off" }),
|
|
827
|
+
status: "failed",
|
|
828
|
+
failure: runtimeFailure("resume-incompatible", "host", `Checkpoint runId mismatch: ${checkpoint.rootRunId} vs ${options.runId}`, false),
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
const resumeAttempt = checkpoint.resumeAttempt + 1;
|
|
832
|
+
const root = Object.freeze({
|
|
833
|
+
rootRunId: options.runId,
|
|
834
|
+
startedAt: Date.now(),
|
|
835
|
+
budget: new InvocationBudget(resolveInvocationLimits()),
|
|
836
|
+
signal: options.signal,
|
|
837
|
+
baseline: resolveHostBaseline(this.host.baseline),
|
|
838
|
+
});
|
|
839
|
+
root.budget.restore(checkpoint.budget);
|
|
840
|
+
const maxSteps = options.maxSteps ?? Number.POSITIVE_INFINITY;
|
|
841
|
+
const mechanismRuntime = new MechanismRuntime(this.host.mechanismRuntime, (message) => this.eventBus.emit({
|
|
842
|
+
type: "runtime_warning",
|
|
843
|
+
rootRunId: root.rootRunId,
|
|
844
|
+
code: "unmanaged-mechanism-access",
|
|
845
|
+
message,
|
|
846
|
+
}));
|
|
847
|
+
this.eventBus.emit({
|
|
848
|
+
type: "root_started",
|
|
849
|
+
rootRunId: root.rootRunId,
|
|
850
|
+
graphId: resolvedGraphId,
|
|
851
|
+
graphVersion: resolvedGraphVersion,
|
|
852
|
+
});
|
|
853
|
+
let outcome;
|
|
854
|
+
let hostMechanisms;
|
|
855
|
+
const openGraphChains = [];
|
|
856
|
+
try {
|
|
857
|
+
hostMechanisms = await mechanismRuntime.open("host", root.rootRunId, this.host.mechanisms ?? [], {
|
|
858
|
+
rootRunId: root.rootRunId,
|
|
859
|
+
});
|
|
860
|
+
await mechanismRuntime.enter([hostMechanisms], "onRootEnter");
|
|
861
|
+
this.mechanismRuns.set(root, { runtime: mechanismRuntime, host: hostMechanisms });
|
|
862
|
+
if (checkpoint.invocationStack.length !== 1 || checkpoint.invocationStack[0]?.boundary !== "root") {
|
|
863
|
+
outcome = failureOutcome(runtimeFailure("resume-incompatible", "host", "Nested invocation checkpoints are not supported by this checkpoint schema", false));
|
|
864
|
+
}
|
|
865
|
+
const refGraph = graph;
|
|
866
|
+
// Restore mechanism state from checkpoint: open graph mechanisms per invocation
|
|
867
|
+
for (const entry of outcome ? [] : checkpoint.invocationStack) {
|
|
868
|
+
if (!refGraph) {
|
|
869
|
+
outcome = failureOutcome(runtimeFailure("invalid-graph", "graph", `Graph not found: ${resolvedGraphId}@${resolvedGraphVersion}`, false));
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
const gm = await mechanismRuntime.open("graph", entry.graphInvocationId, refGraph.mechanisms ?? [], {
|
|
873
|
+
rootRunId: root.rootRunId,
|
|
874
|
+
graphInvocationId: entry.graphInvocationId,
|
|
875
|
+
});
|
|
876
|
+
openGraphChains.push(gm);
|
|
877
|
+
}
|
|
878
|
+
if (!outcome) {
|
|
879
|
+
try {
|
|
880
|
+
mechanismRuntime.restoreState([hostMechanisms, ...openGraphChains], checkpoint.mechanisms);
|
|
881
|
+
}
|
|
882
|
+
catch (error) {
|
|
883
|
+
outcome = failureOutcome(runtimeFailure("resume-incompatible", "host", `Mechanism checkpoint restore failed: ${errorMessage(error)}`, false, undefined, error));
|
|
884
|
+
}
|
|
885
|
+
const deepest = outcome ? undefined : checkpoint.invocationStack[checkpoint.invocationStack.length - 1];
|
|
886
|
+
if (!deepest) {
|
|
887
|
+
outcome = failureOutcome(runtimeFailure("resume-incompatible", "host", "Checkpoint has empty invocation stack", false));
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
const graphMechanisms = openGraphChains[openGraphChains.length - 1];
|
|
891
|
+
outcome = await this.resumeFromCheckpoint(graph, checkpoint, root, maxSteps, mechanismRuntime, hostMechanisms, graphMechanisms ?? { invocations: [] }, resumeAttempt);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
catch (error) {
|
|
896
|
+
outcome = failureOutcome(mapUnexpectedFailure(error, root.signal));
|
|
897
|
+
}
|
|
898
|
+
finally {
|
|
899
|
+
this.mechanismRuns.delete(root);
|
|
900
|
+
// Close graph mechanisms opened during resume
|
|
901
|
+
for (const chain of [...openGraphChains].reverse()) {
|
|
902
|
+
try {
|
|
903
|
+
await mechanismRuntime.close(chain);
|
|
904
|
+
}
|
|
905
|
+
catch { /* cleanup is best-effort */ }
|
|
906
|
+
}
|
|
907
|
+
if (hostMechanisms) {
|
|
908
|
+
await mechanismRuntime.rootExit(hostMechanisms);
|
|
909
|
+
await mechanismRuntime.close(hostMechanisms);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const usage = root.budget.usage;
|
|
913
|
+
const steps = usage.nodeVisits;
|
|
914
|
+
const durationMs = Math.max(0, Date.now() - root.startedAt);
|
|
915
|
+
this.eventBus.emit({
|
|
916
|
+
type: "root_finished",
|
|
917
|
+
rootRunId: root.rootRunId,
|
|
918
|
+
status: outcome.status,
|
|
919
|
+
usage,
|
|
920
|
+
});
|
|
921
|
+
const common = {
|
|
922
|
+
rootRunId: root.rootRunId,
|
|
923
|
+
graphId: graph.id,
|
|
924
|
+
graphVersion: graph.version,
|
|
925
|
+
steps,
|
|
926
|
+
durationMs,
|
|
927
|
+
replay: Object.freeze({ mode: "off", status: "off" }),
|
|
928
|
+
};
|
|
929
|
+
if (outcome.status === "completed") {
|
|
930
|
+
return { ...common, status: "completed", output: outcome.output };
|
|
931
|
+
}
|
|
932
|
+
const failure = outcome.failure ?? runtimeFailure("runtime-error", "root", "Graph failed without a failure object");
|
|
933
|
+
return outcome.status === "cancelled"
|
|
934
|
+
? { ...common, status: "cancelled", failure: failure }
|
|
935
|
+
: { ...common, status: "failed", failure };
|
|
936
|
+
}
|
|
937
|
+
async resumeFromCheckpoint(graph, checkpoint, root, maxSteps, mechanismRuntime, hostMechanisms, graphMechanisms, resumeAttempt) {
|
|
938
|
+
const deepest = checkpoint.invocationStack[checkpoint.invocationStack.length - 1];
|
|
939
|
+
if (!deepest) {
|
|
940
|
+
return failureOutcome(runtimeFailure("resume-incompatible", "host", "Checkpoint has empty invocation stack", false));
|
|
941
|
+
}
|
|
942
|
+
const boundary = deepest.boundary;
|
|
943
|
+
const frames = [...checkpoint.frames];
|
|
944
|
+
const invocation = {
|
|
945
|
+
graphInvocationId: deepest.graphInvocationId,
|
|
946
|
+
rootRunId: root.rootRunId,
|
|
947
|
+
parentGraphInvocationId: deepest.parentGraphInvocationId,
|
|
948
|
+
graph: { id: checkpoint.graph.id, version: checkpoint.graph.version },
|
|
949
|
+
boundary,
|
|
950
|
+
depth: deepest.depth,
|
|
951
|
+
frames,
|
|
952
|
+
frameRevision: { value: 0 },
|
|
953
|
+
};
|
|
954
|
+
this.activeInvocations.set(invocation.graphInvocationId, { state: invocation, parentId: invocation.parentGraphInvocationId });
|
|
955
|
+
try {
|
|
956
|
+
this.eventBus.emit({
|
|
957
|
+
type: "graph_entered",
|
|
958
|
+
rootRunId: root.rootRunId,
|
|
959
|
+
graphInvocationId: invocation.graphInvocationId,
|
|
960
|
+
parentGraphInvocationId: invocation.parentGraphInvocationId,
|
|
961
|
+
graphId: checkpoint.graph.id,
|
|
962
|
+
graphVersion: checkpoint.graph.version,
|
|
963
|
+
boundary,
|
|
964
|
+
depth: deepest.depth,
|
|
965
|
+
});
|
|
966
|
+
const graphSkills = this.resolveSkills(graph.skills);
|
|
967
|
+
const graphInput = checkpoint.next.nodeInput;
|
|
968
|
+
const contextState = new ContextState({
|
|
969
|
+
rootRunId: root.rootRunId,
|
|
970
|
+
graphInvocationId: invocation.graphInvocationId,
|
|
971
|
+
graph,
|
|
972
|
+
graphInput,
|
|
973
|
+
graphSkills,
|
|
974
|
+
frames: [...checkpoint.frames],
|
|
975
|
+
frameRevision: { value: 0 },
|
|
976
|
+
externalContributions: (nodeVisitId) => mechanismRuntime.contextContributions.filter((item) => item.lifetime === "root-run"
|
|
977
|
+
|| item.lifetime === "graph-invocation" && item.scopeId === invocation.graphInvocationId
|
|
978
|
+
|| item.lifetime === "node-visit" && item.scopeId === nodeVisitId
|
|
979
|
+
|| item.lifetime === "agent-run"),
|
|
980
|
+
});
|
|
981
|
+
await contextState.initialize();
|
|
982
|
+
try {
|
|
983
|
+
this.activeGraphMechanisms.set(invocation.graphInvocationId, graphMechanisms);
|
|
984
|
+
await mechanismRuntime.enter([hostMechanisms, graphMechanisms], "onGraphEnter");
|
|
985
|
+
let stageId = checkpoint.next.stageId;
|
|
986
|
+
let nodeInput = checkpoint.next.nodeInput;
|
|
987
|
+
const visits = new Map();
|
|
988
|
+
for (let localStep = 0; localStep < maxSteps; localStep += 1) {
|
|
989
|
+
this.assertNotCancelled(root);
|
|
990
|
+
root.budget.enterNode();
|
|
991
|
+
const stage = graph.stages[stageId];
|
|
992
|
+
if (!stage) {
|
|
993
|
+
return failureOutcome(runtimeFailure("invalid-graph", "graph", `Stage not found: ${stageId}`, false, stageId));
|
|
994
|
+
}
|
|
995
|
+
const visit = (visits.get(stageId) ?? 0) + 1;
|
|
996
|
+
visits.set(stageId, visit);
|
|
997
|
+
// Inject resume identity for Code Node idempotency
|
|
998
|
+
const nodeVisit = Object.freeze({
|
|
999
|
+
nodeVisitId: checkpoint.next.nodeVisitId ?? stableResumeNodeVisitId(checkpoint),
|
|
1000
|
+
rootRunId: root.rootRunId,
|
|
1001
|
+
graphInvocationId: invocation.graphInvocationId,
|
|
1002
|
+
stageId,
|
|
1003
|
+
visit,
|
|
1004
|
+
});
|
|
1005
|
+
this.eventBus.emit({
|
|
1006
|
+
type: "node_entered",
|
|
1007
|
+
rootRunId: root.rootRunId,
|
|
1008
|
+
graphInvocationId: nodeVisit.graphInvocationId,
|
|
1009
|
+
nodeVisitId: nodeVisit.nodeVisitId,
|
|
1010
|
+
stageId,
|
|
1011
|
+
visit,
|
|
1012
|
+
});
|
|
1013
|
+
const exitNode = () => this.eventBus.emit({
|
|
1014
|
+
type: "node_exited",
|
|
1015
|
+
rootRunId: root.rootRunId,
|
|
1016
|
+
graphInvocationId: nodeVisit.graphInvocationId,
|
|
1017
|
+
nodeVisitId: nodeVisit.nodeVisitId,
|
|
1018
|
+
stageId: nodeVisit.stageId,
|
|
1019
|
+
});
|
|
1020
|
+
try {
|
|
1021
|
+
nodeInput = this.validateSchemaBoundary(stage.node.input, nodeInput, "invalid-input", "node", `Node input is invalid at Stage "${stageId}"`, stageId);
|
|
1022
|
+
}
|
|
1023
|
+
catch (error) {
|
|
1024
|
+
const failure = error instanceof RuntimeFailure || error instanceof AgentExecutionFailure
|
|
1025
|
+
? error.failure
|
|
1026
|
+
: runtimeFailure("invalid-input", "node", errorMessage(error), false, stageId, error);
|
|
1027
|
+
exitNode();
|
|
1028
|
+
return failureOutcome(failure);
|
|
1029
|
+
}
|
|
1030
|
+
let completion;
|
|
1031
|
+
try {
|
|
1032
|
+
completion = await this.executeNodeWithResume(graph, stage.node, nodeInput, root, invocation, nodeVisit, maxSteps, graphSkills, contextState, mechanismRuntime, hostMechanisms, graphMechanisms, resumeAttempt);
|
|
1033
|
+
this.assertNotCancelled(root);
|
|
1034
|
+
}
|
|
1035
|
+
catch (error) {
|
|
1036
|
+
const failure = error instanceof RuntimeFailure
|
|
1037
|
+
? error.failure
|
|
1038
|
+
: error instanceof MechanismRuntimeError
|
|
1039
|
+
? runtimeFailure("mechanism-failed", error.failure.installation === "node" ? "node" : "graph", error.message, false, stageId, error)
|
|
1040
|
+
: isCancellationError(error, root.signal)
|
|
1041
|
+
? cancelledFailure(error)
|
|
1042
|
+
: runtimeFailure("runtime-error", stage.node.kind === "agent" ? "agent" : "node", errorMessage(error), true, stageId, error);
|
|
1043
|
+
exitNode();
|
|
1044
|
+
return failureOutcome(failure);
|
|
1045
|
+
}
|
|
1046
|
+
try {
|
|
1047
|
+
const result = this.validateSchemaBoundary(stage.node.output, completion.result, "validation-exhausted", stage.node.kind === "agent" ? "agent" : "node", `Node output is invalid at Stage "${stageId}"`, stageId);
|
|
1048
|
+
completion = Object.freeze({ result });
|
|
1049
|
+
}
|
|
1050
|
+
catch (error) {
|
|
1051
|
+
const failure = error instanceof RuntimeFailure
|
|
1052
|
+
? error.failure
|
|
1053
|
+
: runtimeFailure("validation-exhausted", "node", errorMessage(error), false, stageId, error);
|
|
1054
|
+
exitNode();
|
|
1055
|
+
return failureOutcome(failure);
|
|
1056
|
+
}
|
|
1057
|
+
let connection;
|
|
1058
|
+
try {
|
|
1059
|
+
connection = await selectConnection(stage.route, completion);
|
|
1060
|
+
this.assertNotCancelled(root);
|
|
1061
|
+
}
|
|
1062
|
+
catch (error) {
|
|
1063
|
+
if (error instanceof RuntimeFailure) {
|
|
1064
|
+
exitNode();
|
|
1065
|
+
return failureOutcome(error.failure);
|
|
1066
|
+
}
|
|
1067
|
+
if (isCancellationError(error, root.signal)) {
|
|
1068
|
+
exitNode();
|
|
1069
|
+
return failureOutcome(cancelledFailure(error));
|
|
1070
|
+
}
|
|
1071
|
+
exitNode();
|
|
1072
|
+
return failureOutcome(runtimeFailure("transition-failed", "route", `Route evaluation failed at Stage "${stageId}"`, true, stageId, error));
|
|
1073
|
+
}
|
|
1074
|
+
if (!connection) {
|
|
1075
|
+
exitNode();
|
|
1076
|
+
return failureOutcome(runtimeFailure("no-route", "route", `No Connection matched at Stage "${stageId}"`, false, stageId));
|
|
1077
|
+
}
|
|
1078
|
+
this.eventBus.emit({
|
|
1079
|
+
type: "transition_selected",
|
|
1080
|
+
rootRunId: root.rootRunId,
|
|
1081
|
+
graphInvocationId: invocation.graphInvocationId,
|
|
1082
|
+
nodeVisitId: nodeVisit.nodeVisitId,
|
|
1083
|
+
stageId,
|
|
1084
|
+
connectionId: connection.id,
|
|
1085
|
+
target: connection.to,
|
|
1086
|
+
});
|
|
1087
|
+
const transitionInput = { completion };
|
|
1088
|
+
try {
|
|
1089
|
+
if (connection.transition.frame) {
|
|
1090
|
+
frames.push(connection.transition.frame(transitionInput));
|
|
1091
|
+
contextState.bumpMemoryRevision();
|
|
1092
|
+
}
|
|
1093
|
+
this.assertNotCancelled(root);
|
|
1094
|
+
if (connection.to === "__graph_finish__") {
|
|
1095
|
+
const output = connection.transition.output?.(transitionInput);
|
|
1096
|
+
if (output === undefined) {
|
|
1097
|
+
throw new Error(`Finish Connection "${connection.id}" did not produce output`);
|
|
1098
|
+
}
|
|
1099
|
+
this.assertNotCancelled(root);
|
|
1100
|
+
const validatedOutput = this.validateSchemaBoundary(graph.output, output, "validation-exhausted", "graph", `Graph output is invalid for ${graph.id}@${graph.version}`, stageId);
|
|
1101
|
+
exitNode();
|
|
1102
|
+
return completedOutcome(validatedOutput);
|
|
1103
|
+
}
|
|
1104
|
+
nodeInput = connection.transition.map?.(transitionInput) ?? completion.result;
|
|
1105
|
+
this.assertNotCancelled(root);
|
|
1106
|
+
stageId = connection.to;
|
|
1107
|
+
exitNode();
|
|
1108
|
+
// Write checkpoint at resumed node boundary
|
|
1109
|
+
await this.writeNodeCheckpoint(root, invocation, nodeVisit, stageId, nodeInput, frames, mechanismRuntime, hostMechanisms);
|
|
1110
|
+
}
|
|
1111
|
+
catch (error) {
|
|
1112
|
+
if (error instanceof RuntimeFailure) {
|
|
1113
|
+
exitNode();
|
|
1114
|
+
return failureOutcome(error.failure);
|
|
1115
|
+
}
|
|
1116
|
+
if (isCancellationError(error, root.signal)) {
|
|
1117
|
+
exitNode();
|
|
1118
|
+
return failureOutcome(cancelledFailure(error));
|
|
1119
|
+
}
|
|
1120
|
+
exitNode();
|
|
1121
|
+
return failureOutcome(runtimeFailure("transition-failed", "transition", `Transition "${connection.id}" failed`, true, stageId, error));
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return failureOutcome(runtimeFailure("max-steps-exceeded", "graph", `Graph Invocation exceeded maxSteps ${maxSteps}`, false, stageId));
|
|
1125
|
+
}
|
|
1126
|
+
finally {
|
|
1127
|
+
this.activeGraphMechanisms.delete(invocation.graphInvocationId);
|
|
1128
|
+
// graphMechanisms are owned by the resume caller, not closed here.
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
finally {
|
|
1132
|
+
this.activeInvocations.delete(invocation.graphInvocationId);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
async executeNodeWithResume(graph, node, input, root, invocation, nodeVisit, maxSteps, graphSkills, contextState, mechanismRuntime, hostMechanisms, graphMechanisms, resumeAttempt) {
|
|
1136
|
+
const capabilities = this.resolveNodeCapabilities(graph, nodeVisit.stageId, node, graphSkills, root, invocation);
|
|
1137
|
+
const nodeMechanisms = await mechanismRuntime.open("node", nodeVisit.nodeVisitId, node.mechanisms ?? [], {
|
|
1138
|
+
rootRunId: root.rootRunId,
|
|
1139
|
+
graphInvocationId: invocation.graphInvocationId,
|
|
1140
|
+
nodeVisitId: nodeVisit.nodeVisitId,
|
|
1141
|
+
stageId: nodeVisit.stageId,
|
|
1142
|
+
}, contextState);
|
|
1143
|
+
const chains = [hostMechanisms, graphMechanisms, nodeMechanisms];
|
|
1144
|
+
try {
|
|
1145
|
+
await mechanismRuntime.enter(chains, "onNodeEnter");
|
|
1146
|
+
const { snapshot } = await contextState.materializeNode(nodeVisit.nodeVisitId, nodeVisit.stageId, graph.stages[nodeVisit.stageId], input, capabilities.skills);
|
|
1147
|
+
let result;
|
|
1148
|
+
if (node.kind === "agent") {
|
|
1149
|
+
result = await this.runAgent(graph, node, input, root, invocation, nodeVisit, 1, maxSteps, capabilities.tools, capabilities.skills, snapshot, contextState, mechanismRuntime, chains);
|
|
1150
|
+
}
|
|
1151
|
+
else if (node.kind === "graph") {
|
|
1152
|
+
result = await this.executeGraphNode(node, input, root, invocation, maxSteps);
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
let agentIndex = 0;
|
|
1156
|
+
result = await node.execute({
|
|
1157
|
+
input: input,
|
|
1158
|
+
complete: (value) => value,
|
|
1159
|
+
resumeAttempt,
|
|
1160
|
+
rootRunId: root.rootRunId,
|
|
1161
|
+
nodeVisitId: nodeVisit.nodeVisitId,
|
|
1162
|
+
runAgent: async (agentRequest) => ({
|
|
1163
|
+
result: await this.runCodeAgent(agentRequest, node, root, invocation, nodeVisit, ++agentIndex, maxSteps, capabilities.tools, capabilities.skills, snapshot, contextState, mechanismRuntime, chains),
|
|
1164
|
+
}),
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
await mechanismRuntime.nodeExit(chains, result);
|
|
1168
|
+
return { result };
|
|
1169
|
+
}
|
|
1170
|
+
catch (error) {
|
|
1171
|
+
await mechanismRuntime.nodeError(chains, error);
|
|
1172
|
+
throw error;
|
|
1173
|
+
}
|
|
1174
|
+
finally {
|
|
1175
|
+
await mechanismRuntime.close(nodeMechanisms);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
assertNotCancelled(root) {
|
|
1179
|
+
if (root.signal?.aborted) {
|
|
1180
|
+
throw new RuntimeFailure(runtimeFailure("cancelled", "root", "Graph execution cancelled", false));
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
validateSchemaBoundary(schema, value, code, phase, message, stageId) {
|
|
1184
|
+
let checked;
|
|
1185
|
+
try {
|
|
1186
|
+
checked = checkJsonSchemaValue(schema, value);
|
|
1187
|
+
}
|
|
1188
|
+
catch (error) {
|
|
1189
|
+
throw new RuntimeFailure(runtimeFailure("invalid-graph", "graph", `Invalid JSON Schema: ${errorMessage(error)}`, false, stageId, error));
|
|
1190
|
+
}
|
|
1191
|
+
if (!checked.valid || checked.value === undefined) {
|
|
1192
|
+
throw new RuntimeFailure(runtimeFailure(code, phase, `${message}${checked.message ? `: ${checked.message}` : ""}`, false, stageId));
|
|
1193
|
+
}
|
|
1194
|
+
return checked.value;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
async function firstMatchingEntry(graph, input) {
|
|
1198
|
+
for (const entry of graph.entries) {
|
|
1199
|
+
if (!entry.guard || await entry.guard(input))
|
|
1200
|
+
return entry;
|
|
1201
|
+
}
|
|
1202
|
+
return undefined;
|
|
1203
|
+
}
|
|
1204
|
+
async function selectConnection(route, completion) {
|
|
1205
|
+
if (route.kind === "agent-choice" && isJsonObject(completion.result)) {
|
|
1206
|
+
const choice = completion.result.chosen_edge_id;
|
|
1207
|
+
if (typeof choice === "string")
|
|
1208
|
+
return route.connections.find((connection) => connection.id === choice);
|
|
1209
|
+
}
|
|
1210
|
+
for (const connection of route.connections) {
|
|
1211
|
+
if (!connection.transition.guard || await connection.transition.guard(completion.result))
|
|
1212
|
+
return connection;
|
|
1213
|
+
}
|
|
1214
|
+
return undefined;
|
|
1215
|
+
}
|
|
1216
|
+
function validateRouteStructure(route, result) {
|
|
1217
|
+
if (!route)
|
|
1218
|
+
return { valid: true };
|
|
1219
|
+
if (route.connections.length === 0)
|
|
1220
|
+
return { valid: false, reason: "Route has no connections" };
|
|
1221
|
+
if (route.kind !== "agent-choice")
|
|
1222
|
+
return { valid: true };
|
|
1223
|
+
return isJsonObject(result)
|
|
1224
|
+
? { valid: true }
|
|
1225
|
+
: { valid: false, reason: "agent-choice result must be an object" };
|
|
1226
|
+
}
|
|
1227
|
+
function validateAgentChoice(route, result) {
|
|
1228
|
+
if (!route || route.kind !== "agent-choice")
|
|
1229
|
+
return { valid: true };
|
|
1230
|
+
if (!isJsonObject(result))
|
|
1231
|
+
return { valid: false, reason: "agent-choice result must be an object" };
|
|
1232
|
+
const choice = result.chosen_edge_id;
|
|
1233
|
+
if (typeof choice !== "string" || !choice)
|
|
1234
|
+
return { valid: false, reason: "agent-choice requires result.chosen_edge_id" };
|
|
1235
|
+
return route.connections.some((connection) => connection.id === choice)
|
|
1236
|
+
? { valid: true }
|
|
1237
|
+
: { valid: false, reason: `Unknown agent-choice connection: ${choice}` };
|
|
1238
|
+
}
|
|
1239
|
+
function isJsonObject(value) {
|
|
1240
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1241
|
+
}
|
|
1242
|
+
function completedOutcome(output) {
|
|
1243
|
+
return Object.freeze({ status: "completed", output });
|
|
1244
|
+
}
|
|
1245
|
+
function failureOutcome(failure) {
|
|
1246
|
+
return Object.freeze({
|
|
1247
|
+
status: failure.code === "cancelled" ? "cancelled" : "failed",
|
|
1248
|
+
failure,
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
function runtimeFailure(code, phase, message, retryable = false, stageId, cause) {
|
|
1252
|
+
return Object.freeze({ code, phase, message, retryable, stageId, cause });
|
|
1253
|
+
}
|
|
1254
|
+
function mapUnexpectedFailure(error, signal) {
|
|
1255
|
+
if (error instanceof MechanismRuntimeError) {
|
|
1256
|
+
return runtimeFailure("mechanism-failed", error.failure.installation === "node" ? "node" : "graph", error.message, false, undefined, error);
|
|
1257
|
+
}
|
|
1258
|
+
if (error instanceof RuntimeFailure)
|
|
1259
|
+
return error.failure;
|
|
1260
|
+
if (isCancellationError(error, signal))
|
|
1261
|
+
return cancelledFailure(error);
|
|
1262
|
+
if (error instanceof InvocationBudgetExceededError) {
|
|
1263
|
+
return runtimeFailure("max-steps-exceeded", "root", error.message, false, undefined, error);
|
|
1264
|
+
}
|
|
1265
|
+
return runtimeFailure("runtime-error", "root", errorMessage(error), true, undefined, error);
|
|
1266
|
+
}
|
|
1267
|
+
function isCancellationError(error, signal) {
|
|
1268
|
+
return signal?.aborted === true || (error instanceof Error && error.name === "AbortError");
|
|
1269
|
+
}
|
|
1270
|
+
function cancelledFailure(cause) {
|
|
1271
|
+
return runtimeFailure("cancelled", "root", "Graph execution cancelled", false, undefined, cause);
|
|
1272
|
+
}
|
|
1273
|
+
function errorMessage(error) {
|
|
1274
|
+
return error instanceof Error ? error.message : String(error);
|
|
1275
|
+
}
|
|
1276
|
+
function tightenLimits(hard, requested) {
|
|
1277
|
+
const candidate = resolveInvocationLimits(requested ?? {});
|
|
1278
|
+
return Object.freeze({
|
|
1279
|
+
maxGraphDepth: Math.min(hard.maxGraphDepth, candidate.maxGraphDepth),
|
|
1280
|
+
maxGraphInvocations: Math.min(hard.maxGraphInvocations, candidate.maxGraphInvocations),
|
|
1281
|
+
maxTotalNodeVisits: Math.min(hard.maxTotalNodeVisits, candidate.maxTotalNodeVisits),
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
function checkpointOrder(checkpoint) {
|
|
1285
|
+
if (checkpoint.createdAt)
|
|
1286
|
+
return Date.parse(checkpoint.createdAt);
|
|
1287
|
+
// Legacy v1 checkpoints did not carry ordering metadata. They remain readable,
|
|
1288
|
+
// but tie deterministically without pretending their UUID encodes chronology.
|
|
1289
|
+
return 0;
|
|
1290
|
+
}
|
|
1291
|
+
function stableResumeNodeVisitId(checkpoint) {
|
|
1292
|
+
return `resume-${checkpoint.checkpointId}-${checkpoint.next.stageId}`;
|
|
1293
|
+
}
|