@noetaris/harness 0.3.2 → 0.4.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/README.md +6 -3
- package/dist/index.d.ts +253 -125
- package/dist/index.js +437 -78
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -20,6 +20,9 @@ function isRuntimeMarker(value) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
// src/loop/loop-dsl.ts
|
|
23
|
+
function isForkDef(node) {
|
|
24
|
+
return "branches" in node;
|
|
25
|
+
}
|
|
23
26
|
var InvalidLoopBuilderError = class extends Error {
|
|
24
27
|
constructor() {
|
|
25
28
|
super("argument is not a LoopBuilder instance \u2014 was it created by createLoopBuilder?");
|
|
@@ -27,6 +30,9 @@ var InvalidLoopBuilderError = class extends Error {
|
|
|
27
30
|
}
|
|
28
31
|
};
|
|
29
32
|
var _builderState = /* @__PURE__ */ Symbol("builderState");
|
|
33
|
+
function isForkNodeMutable(node) {
|
|
34
|
+
return "branches" in node;
|
|
35
|
+
}
|
|
30
36
|
function deepFreeze(obj) {
|
|
31
37
|
Object.freeze(obj);
|
|
32
38
|
for (const value of Object.values(obj)) {
|
|
@@ -65,6 +71,32 @@ function createLoopBuilder() {
|
|
|
65
71
|
}
|
|
66
72
|
return this;
|
|
67
73
|
},
|
|
74
|
+
fork(name) {
|
|
75
|
+
const forkNode = { name, branches: [], next: void 0 };
|
|
76
|
+
state.steps.push(forkNode);
|
|
77
|
+
if (state.startCalled && state.entryStep === void 0) {
|
|
78
|
+
state.entryStep = name;
|
|
79
|
+
}
|
|
80
|
+
return this;
|
|
81
|
+
},
|
|
82
|
+
branch(name, builderFn) {
|
|
83
|
+
const currentNode = state.steps.at(-1);
|
|
84
|
+
if (currentNode === void 0 || !isForkNodeMutable(currentNode)) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
".branch() must be called immediately after .fork() or another .branch() on the same fork"
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const branchBuilder = createLoopBuilder();
|
|
90
|
+
builderFn(branchBuilder);
|
|
91
|
+
const branchDefinition = extractLoopDefinition(
|
|
92
|
+
branchBuilder
|
|
93
|
+
);
|
|
94
|
+
currentNode.branches.push({ name, definition: branchDefinition });
|
|
95
|
+
return this;
|
|
96
|
+
},
|
|
97
|
+
join(name, options) {
|
|
98
|
+
return this.step(name, options);
|
|
99
|
+
},
|
|
68
100
|
on(signal) {
|
|
69
101
|
if (state.steps.length === 0) {
|
|
70
102
|
return {
|
|
@@ -72,17 +104,23 @@ function createLoopBuilder() {
|
|
|
72
104
|
end: () => this
|
|
73
105
|
};
|
|
74
106
|
}
|
|
75
|
-
const
|
|
107
|
+
const currentNode = state.steps.at(-1);
|
|
108
|
+
if (isForkNodeMutable(currentNode)) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
".on() cannot be called on a fork \u2014 a fork has no run/route to emit a signal from"
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const currentStep = currentNode;
|
|
76
114
|
return {
|
|
77
115
|
to: (step) => {
|
|
78
|
-
currentStep
|
|
116
|
+
currentStep.transitions.push({
|
|
79
117
|
signal,
|
|
80
118
|
target: { kind: "step", name: step }
|
|
81
119
|
});
|
|
82
120
|
return this;
|
|
83
121
|
},
|
|
84
122
|
end: () => {
|
|
85
|
-
currentStep
|
|
123
|
+
currentStep.transitions.push({
|
|
86
124
|
signal,
|
|
87
125
|
target: { kind: "end" }
|
|
88
126
|
});
|
|
@@ -92,9 +130,9 @@ function createLoopBuilder() {
|
|
|
92
130
|
},
|
|
93
131
|
next(name) {
|
|
94
132
|
if (state.steps.length > 0) {
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
|
|
133
|
+
const currentNode = state.steps.at(-1);
|
|
134
|
+
if (currentNode !== void 0) {
|
|
135
|
+
currentNode.next = name;
|
|
98
136
|
}
|
|
99
137
|
}
|
|
100
138
|
return this;
|
|
@@ -112,19 +150,30 @@ function extractLoopDefinition(builder) {
|
|
|
112
150
|
throw new InvalidLoopBuilderError();
|
|
113
151
|
}
|
|
114
152
|
const state = builder[_builderState];
|
|
115
|
-
const immutableSteps = state.steps.map((
|
|
116
|
-
|
|
153
|
+
const immutableSteps = state.steps.map((node) => {
|
|
154
|
+
if (isForkNodeMutable(node)) {
|
|
155
|
+
const branches = Object.freeze(
|
|
156
|
+
node.branches.map((b) => Object.freeze({ name: b.name, definition: b.definition }))
|
|
157
|
+
);
|
|
158
|
+
const forkDef = {
|
|
159
|
+
name: node.name,
|
|
160
|
+
branches,
|
|
161
|
+
next: node.next
|
|
162
|
+
};
|
|
163
|
+
return Object.freeze(forkDef);
|
|
164
|
+
}
|
|
165
|
+
const transitions = Object.freeze([...node.transitions]);
|
|
117
166
|
for (const transition of transitions) {
|
|
118
167
|
Object.freeze(transition);
|
|
119
168
|
Object.freeze(transition.target);
|
|
120
169
|
}
|
|
121
170
|
const stepDef = {
|
|
122
|
-
name:
|
|
123
|
-
run:
|
|
124
|
-
route:
|
|
171
|
+
name: node.name,
|
|
172
|
+
run: node.run,
|
|
173
|
+
route: node.route,
|
|
125
174
|
transitions,
|
|
126
|
-
next:
|
|
127
|
-
errorAware:
|
|
175
|
+
next: node.next,
|
|
176
|
+
errorAware: node.errorAware
|
|
128
177
|
};
|
|
129
178
|
return Object.freeze(stepDef);
|
|
130
179
|
});
|
|
@@ -148,30 +197,85 @@ ${violations.map((v) => ` - ${v}`).join("\n")}`);
|
|
|
148
197
|
}
|
|
149
198
|
};
|
|
150
199
|
function validateLoop(def) {
|
|
200
|
+
const violations = collectViolations(def, void 0);
|
|
201
|
+
const nameLocations = /* @__PURE__ */ new Map();
|
|
202
|
+
collectNameLocations(def, void 0, nameLocations);
|
|
203
|
+
for (const [name, locations] of nameLocations) {
|
|
204
|
+
if (locations.length > 1) {
|
|
205
|
+
violations.push(
|
|
206
|
+
`name "${name}" is declared more than once across the nested structure: ${locations.join(", ")}`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (violations.length > 0) {
|
|
211
|
+
throw new LoopValidationError(violations);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function collectViolations(def, pathLabel) {
|
|
215
|
+
const prefix = pathLabel !== void 0 ? `in branch "${pathLabel}": ` : "";
|
|
216
|
+
const violations = collectStructuralViolations(def).map((v) => prefix + v);
|
|
217
|
+
for (const node of def.steps) {
|
|
218
|
+
if (!isForkDef(node)) continue;
|
|
219
|
+
if (node.branches.length < 2) {
|
|
220
|
+
violations.push(
|
|
221
|
+
`${prefix}fork "${node.name}" must have at least 2 branches (found ${node.branches.length})`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
const seenBranchNames = /* @__PURE__ */ new Set();
|
|
225
|
+
for (const branch of node.branches) {
|
|
226
|
+
if (seenBranchNames.has(branch.name)) {
|
|
227
|
+
violations.push(`${prefix}fork "${node.name}" has duplicate branch name "${branch.name}"`);
|
|
228
|
+
}
|
|
229
|
+
seenBranchNames.add(branch.name);
|
|
230
|
+
}
|
|
231
|
+
for (const branch of node.branches) {
|
|
232
|
+
const branchPath = pathLabel !== void 0 ? `${pathLabel}/${node.name}/${branch.name}` : `${node.name}/${branch.name}`;
|
|
233
|
+
violations.push(...collectViolations(branch.definition, branchPath));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return violations;
|
|
237
|
+
}
|
|
238
|
+
function collectNameLocations(def, path, acc) {
|
|
239
|
+
for (const node of def.steps) {
|
|
240
|
+
const location = path !== void 0 ? `${path}/${node.name}` : node.name;
|
|
241
|
+
const locations = acc.get(node.name) ?? [];
|
|
242
|
+
locations.push(location);
|
|
243
|
+
acc.set(node.name, locations);
|
|
244
|
+
if (isForkDef(node)) {
|
|
245
|
+
for (const branch of node.branches) {
|
|
246
|
+
const branchPath = path !== void 0 ? `${path}/${node.name}/${branch.name}` : `${node.name}/${branch.name}`;
|
|
247
|
+
collectNameLocations(branch.definition, branchPath, acc);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function collectStructuralViolations(def) {
|
|
151
253
|
const violations = [];
|
|
152
254
|
const names = new Set(def.steps.map((s) => s.name));
|
|
153
|
-
|
|
255
|
+
const stepNodes = def.steps.filter((s) => !isForkDef(s));
|
|
256
|
+
for (const step of stepNodes) {
|
|
154
257
|
if (step.run === void 0 && step.route === void 0) {
|
|
155
258
|
violations.push(`step "${step.name}" must have at least one of run or route`);
|
|
156
259
|
}
|
|
157
260
|
}
|
|
158
|
-
for (const step of
|
|
261
|
+
for (const step of stepNodes) {
|
|
159
262
|
if (step.route !== void 0 && step.transitions.length === 0) {
|
|
160
263
|
violations.push(`step "${step.name}" has route but no .on() transitions \u2014 every signal must be handled`);
|
|
161
264
|
}
|
|
162
265
|
}
|
|
163
|
-
for (const [i,
|
|
164
|
-
if (
|
|
165
|
-
|
|
266
|
+
for (const [i, node] of def.steps.entries()) {
|
|
267
|
+
if (node === void 0) continue;
|
|
268
|
+
const hasRoute = !isForkDef(node) && node.route !== void 0;
|
|
269
|
+
if (!hasRoute) {
|
|
166
270
|
const isLast = i === def.steps.length - 1;
|
|
167
|
-
if (isLast &&
|
|
271
|
+
if (isLast && node.next === void 0) {
|
|
168
272
|
violations.push(
|
|
169
|
-
`step "${
|
|
273
|
+
`step "${node.name}" has no route, no explicit .next(), and no following step \u2014 no exit path`
|
|
170
274
|
);
|
|
171
275
|
}
|
|
172
276
|
}
|
|
173
277
|
}
|
|
174
|
-
for (const step of
|
|
278
|
+
for (const step of stepNodes) {
|
|
175
279
|
if (step.route !== void 0 && step.next !== void 0) {
|
|
176
280
|
violations.push(`step "${step.name}" has both route and .next() \u2014 these are mutually exclusive`);
|
|
177
281
|
}
|
|
@@ -183,7 +287,7 @@ function validateLoop(def) {
|
|
|
183
287
|
}
|
|
184
288
|
seen.add(step.name);
|
|
185
289
|
}
|
|
186
|
-
for (const step of
|
|
290
|
+
for (const step of stepNodes) {
|
|
187
291
|
for (const transition of step.transitions) {
|
|
188
292
|
if (transition.target.kind === "step") {
|
|
189
293
|
if (!names.has(transition.target.name)) {
|
|
@@ -193,10 +297,12 @@ function validateLoop(def) {
|
|
|
193
297
|
}
|
|
194
298
|
}
|
|
195
299
|
}
|
|
196
|
-
|
|
197
|
-
|
|
300
|
+
}
|
|
301
|
+
for (const node of def.steps) {
|
|
302
|
+
if (node.next !== void 0) {
|
|
303
|
+
if (!names.has(node.next)) {
|
|
198
304
|
violations.push(
|
|
199
|
-
`step "${
|
|
305
|
+
`step "${node.name}" has .next("${node.next}") but step "${node.next}" is not declared`
|
|
200
306
|
);
|
|
201
307
|
}
|
|
202
308
|
}
|
|
@@ -213,7 +319,7 @@ function validateLoop(def) {
|
|
|
213
319
|
violations.push(`l.start() was called but no step was declared after it \u2014 declare at least one step`);
|
|
214
320
|
}
|
|
215
321
|
let hasExit = false;
|
|
216
|
-
for (const step of
|
|
322
|
+
for (const step of stepNodes) {
|
|
217
323
|
for (const transition of step.transitions) {
|
|
218
324
|
if (transition.target.kind === "end") {
|
|
219
325
|
hasExit = true;
|
|
@@ -236,36 +342,39 @@ function validateLoop(def) {
|
|
|
236
342
|
}
|
|
237
343
|
while (queue.length > 0) {
|
|
238
344
|
const currentName = queue.shift();
|
|
239
|
-
const
|
|
240
|
-
if (!
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (
|
|
244
|
-
reachable.
|
|
245
|
-
|
|
345
|
+
const currentNode = def.steps.find((s) => s.name === currentName);
|
|
346
|
+
if (!currentNode) continue;
|
|
347
|
+
if (!isForkDef(currentNode)) {
|
|
348
|
+
for (const transition of currentNode.transitions) {
|
|
349
|
+
if (transition.target.kind === "step") {
|
|
350
|
+
if (!reachable.has(transition.target.name)) {
|
|
351
|
+
reachable.add(transition.target.name);
|
|
352
|
+
queue.push(transition.target.name);
|
|
353
|
+
}
|
|
246
354
|
}
|
|
247
355
|
}
|
|
248
356
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
357
|
+
const hasRoute = !isForkDef(currentNode) && currentNode.route !== void 0;
|
|
358
|
+
if (hasRoute) {
|
|
359
|
+
if (currentNode.next !== void 0 && !reachable.has(currentNode.next)) {
|
|
360
|
+
reachable.add(currentNode.next);
|
|
361
|
+
queue.push(currentNode.next);
|
|
253
362
|
}
|
|
254
363
|
} else {
|
|
255
|
-
if (
|
|
256
|
-
if (!reachable.has(
|
|
257
|
-
reachable.add(
|
|
258
|
-
queue.push(
|
|
364
|
+
if (currentNode.next !== void 0) {
|
|
365
|
+
if (!reachable.has(currentNode.next)) {
|
|
366
|
+
reachable.add(currentNode.next);
|
|
367
|
+
queue.push(currentNode.next);
|
|
259
368
|
}
|
|
260
369
|
} else {
|
|
261
370
|
const currentIndex = def.steps.findIndex((s) => s.name === currentName);
|
|
262
371
|
if (currentIndex !== -1 && currentIndex < def.steps.length - 1) {
|
|
263
|
-
const
|
|
264
|
-
if (
|
|
265
|
-
const
|
|
266
|
-
if (!reachable.has(
|
|
267
|
-
reachable.add(
|
|
268
|
-
queue.push(
|
|
372
|
+
const nextNode = def.steps[currentIndex + 1];
|
|
373
|
+
if (nextNode !== void 0) {
|
|
374
|
+
const nextNodeName = nextNode.name;
|
|
375
|
+
if (!reachable.has(nextNodeName)) {
|
|
376
|
+
reachable.add(nextNodeName);
|
|
377
|
+
queue.push(nextNodeName);
|
|
269
378
|
}
|
|
270
379
|
}
|
|
271
380
|
}
|
|
@@ -278,16 +387,14 @@ function validateLoop(def) {
|
|
|
278
387
|
}
|
|
279
388
|
}
|
|
280
389
|
}
|
|
281
|
-
for (const step of
|
|
390
|
+
for (const step of stepNodes) {
|
|
282
391
|
if (step.route === void 0 && step.transitions.length > 0) {
|
|
283
392
|
violations.push(
|
|
284
393
|
`step "${step.name}" has .on() transitions but no route \u2014 .on() is only valid when route is set`
|
|
285
394
|
);
|
|
286
395
|
}
|
|
287
396
|
}
|
|
288
|
-
|
|
289
|
-
throw new LoopValidationError(violations);
|
|
290
|
-
}
|
|
397
|
+
return violations;
|
|
291
398
|
}
|
|
292
399
|
|
|
293
400
|
// src/harness/harness-builder.ts
|
|
@@ -398,10 +505,11 @@ var InterruptPause = class extends Error {
|
|
|
398
505
|
function isInterruptPause(error) {
|
|
399
506
|
return error instanceof InterruptPause;
|
|
400
507
|
}
|
|
401
|
-
function createInterruptFn(state, callCountRef) {
|
|
508
|
+
function createInterruptFn(state, callCountRef, branchPath) {
|
|
509
|
+
const prefix = branchPath !== void 0 && branchPath.length > 0 ? `${branchPath.join(":")}:` : "";
|
|
402
510
|
return async (prompt, id) => {
|
|
403
511
|
const isAutoId = id === void 0;
|
|
404
|
-
const effectiveId = isAutoId ? `$auto:${callCountRef.current}` : id;
|
|
512
|
+
const effectiveId = isAutoId ? `$auto:${prefix}${callCountRef.current}` : id;
|
|
405
513
|
if (isAutoId) {
|
|
406
514
|
callCountRef.current++;
|
|
407
515
|
}
|
|
@@ -432,6 +540,9 @@ function extractRunListeners(resources) {
|
|
|
432
540
|
}
|
|
433
541
|
|
|
434
542
|
// src/loop/loop-executor.ts
|
|
543
|
+
function isForkCursor(cursor) {
|
|
544
|
+
return typeof cursor !== "string";
|
|
545
|
+
}
|
|
435
546
|
var UnknownSignalError = class extends Error {
|
|
436
547
|
step;
|
|
437
548
|
signal;
|
|
@@ -452,6 +563,20 @@ var NoNextStepError = class extends Error {
|
|
|
452
563
|
this.step = step;
|
|
453
564
|
}
|
|
454
565
|
};
|
|
566
|
+
var MissingReducerError = class extends Error {
|
|
567
|
+
fork;
|
|
568
|
+
key;
|
|
569
|
+
branches;
|
|
570
|
+
constructor(fork, key, branches) {
|
|
571
|
+
super(
|
|
572
|
+
`fork "${fork}": state key "${key}" was touched by multiple branches (${branches.join(", ")}) but has no reducer declared \u2014 add field({ reduce }) for "${key}"`
|
|
573
|
+
);
|
|
574
|
+
this.name = "MissingReducerError";
|
|
575
|
+
this.fork = fork;
|
|
576
|
+
this.key = key;
|
|
577
|
+
this.branches = branches;
|
|
578
|
+
}
|
|
579
|
+
};
|
|
455
580
|
function buildImplicitNextMap(graph) {
|
|
456
581
|
const map = /* @__PURE__ */ new Map();
|
|
457
582
|
for (let i = 0; i < graph.steps.length; i++) {
|
|
@@ -461,7 +586,7 @@ function buildImplicitNextMap(graph) {
|
|
|
461
586
|
}
|
|
462
587
|
return map;
|
|
463
588
|
}
|
|
464
|
-
function applyUpdate(state, update, schema) {
|
|
589
|
+
function applyUpdate(state, update, schema, touchedKeys) {
|
|
465
590
|
for (const [key, value] of Object.entries(update)) {
|
|
466
591
|
if (key === "$error" || key === "$interrupt" || key === "$interruptResponses") continue;
|
|
467
592
|
const reducer = schema?.[key]?.reduce;
|
|
@@ -470,6 +595,7 @@ function applyUpdate(state, update, schema) {
|
|
|
470
595
|
} else {
|
|
471
596
|
state[key] = value;
|
|
472
597
|
}
|
|
598
|
+
touchedKeys?.add(key);
|
|
473
599
|
}
|
|
474
600
|
}
|
|
475
601
|
async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callbacks) {
|
|
@@ -485,12 +611,13 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
|
|
|
485
611
|
sessionId: ctx.sessionId,
|
|
486
612
|
runId: typeof callbacks?.runId === "string" ? callbacks.runId : "",
|
|
487
613
|
...typeof callbacks?.parentRunId === "string" ? { parentRunId: callbacks.parentRunId } : {},
|
|
488
|
-
...typeof ctx.instanceId === "string" ? { instanceId: ctx.instanceId } : {}
|
|
614
|
+
...typeof ctx.instanceId === "string" ? { instanceId: ctx.instanceId } : {},
|
|
615
|
+
...callbacks?.branchPath !== void 0 && callbacks.branchPath.length > 0 ? { branchPath: callbacks.branchPath } : {}
|
|
489
616
|
};
|
|
490
617
|
obs?.onRunStart?.(runCtx);
|
|
491
618
|
const callCountRef = { current: 0 };
|
|
492
619
|
const stepCtxRef = { current: null };
|
|
493
|
-
ctx["interrupt"] = createInterruptFn(state, callCountRef);
|
|
620
|
+
ctx["interrupt"] = createInterruptFn(state, callCountRef, callbacks?.branchPath);
|
|
494
621
|
ctx["emit"] = createEmitFn(callbacks?.listeners ?? {}, obs, stepCtxRef);
|
|
495
622
|
let cursor = startCursor ?? graph.entryStep;
|
|
496
623
|
while (true) {
|
|
@@ -498,10 +625,64 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
|
|
|
498
625
|
obs?.onRunEnd?.(runCtx, { signal: "$stopped", durationMs: Date.now() - runStart });
|
|
499
626
|
return { state, signal: null, cursor, paused: true };
|
|
500
627
|
}
|
|
628
|
+
if (isForkCursor(cursor)) {
|
|
629
|
+
const forkCursor = cursor;
|
|
630
|
+
const forkNode = graph.steps.find((s) => s.name === forkCursor.fork);
|
|
631
|
+
if (!isForkDef(forkNode)) {
|
|
632
|
+
throw new NoNextStepError(forkCursor.fork);
|
|
633
|
+
}
|
|
634
|
+
onBeforeStep?.(forkNode.name, { ...state });
|
|
635
|
+
const outcome = await handleFork(
|
|
636
|
+
forkNode,
|
|
637
|
+
forkCursor.branches,
|
|
638
|
+
state,
|
|
639
|
+
ctx,
|
|
640
|
+
schema,
|
|
641
|
+
shouldStop,
|
|
642
|
+
callbacks,
|
|
643
|
+
obs,
|
|
644
|
+
runCtx,
|
|
645
|
+
runStart,
|
|
646
|
+
implicitNextMap,
|
|
647
|
+
graph
|
|
648
|
+
);
|
|
649
|
+
if (outcome.done) {
|
|
650
|
+
return outcome.result;
|
|
651
|
+
}
|
|
652
|
+
cursor = outcome.nextCursor;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
501
655
|
onBeforeStep?.(cursor, { ...state });
|
|
502
|
-
const
|
|
656
|
+
const node = graph.steps.find((s) => s.name === cursor);
|
|
657
|
+
if (isForkDef(node)) {
|
|
658
|
+
const outcome = await handleFork(
|
|
659
|
+
node,
|
|
660
|
+
void 0,
|
|
661
|
+
state,
|
|
662
|
+
ctx,
|
|
663
|
+
schema,
|
|
664
|
+
shouldStop,
|
|
665
|
+
callbacks,
|
|
666
|
+
obs,
|
|
667
|
+
runCtx,
|
|
668
|
+
runStart,
|
|
669
|
+
implicitNextMap,
|
|
670
|
+
graph
|
|
671
|
+
);
|
|
672
|
+
if (outcome.done) {
|
|
673
|
+
return outcome.result;
|
|
674
|
+
}
|
|
675
|
+
cursor = outcome.nextCursor;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
const step = node;
|
|
503
679
|
callCountRef.current = 0;
|
|
504
|
-
const stepCtx = {
|
|
680
|
+
const stepCtx = {
|
|
681
|
+
agentId: ctx.agentId,
|
|
682
|
+
sessionId: ctx.sessionId,
|
|
683
|
+
stepName: cursor,
|
|
684
|
+
...callbacks?.branchPath !== void 0 && callbacks.branchPath.length > 0 ? { branchPath: callbacks.branchPath } : {}
|
|
685
|
+
};
|
|
505
686
|
stepCtxRef.current = stepCtx;
|
|
506
687
|
const stepStart = Date.now();
|
|
507
688
|
obs?.onStepStart?.(stepCtx);
|
|
@@ -518,7 +699,7 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
|
|
|
518
699
|
state,
|
|
519
700
|
ctx
|
|
520
701
|
);
|
|
521
|
-
applyUpdate(state, update, schema);
|
|
702
|
+
applyUpdate(state, update, schema, callbacks?.touchedKeys);
|
|
522
703
|
if (hadResponses) {
|
|
523
704
|
state.$interrupt = null;
|
|
524
705
|
state.$interruptResponses = {};
|
|
@@ -578,18 +759,161 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
|
|
|
578
759
|
}
|
|
579
760
|
}
|
|
580
761
|
}
|
|
762
|
+
async function runBranch(branch, prior, outerState, outerCtx, schema, shouldStop, branchSignal, branchCallbacks) {
|
|
763
|
+
if (prior !== void 0 && prior.status === "done") {
|
|
764
|
+
return {
|
|
765
|
+
name: branch.name,
|
|
766
|
+
result: { state: prior.state, signal: null, cursor: null, paused: false },
|
|
767
|
+
touchedKeys: new Set(prior.touchedKeys)
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
const touchedKeys = new Set(prior?.status === "paused" ? prior.touchedKeys : []);
|
|
771
|
+
const privateState = prior !== void 0 ? { ...prior.state } : { ...outerState };
|
|
772
|
+
const privateCtx = { ...outerCtx, signal: branchSignal };
|
|
773
|
+
const branchStartCursor = prior?.status === "paused" ? prior.cursor : void 0;
|
|
774
|
+
const branchPath = [...branchCallbacks?.branchPath ?? [], branch.name];
|
|
775
|
+
const result = await runLoop(
|
|
776
|
+
branch.definition,
|
|
777
|
+
privateState,
|
|
778
|
+
privateCtx,
|
|
779
|
+
schema,
|
|
780
|
+
shouldStop,
|
|
781
|
+
branchStartCursor,
|
|
782
|
+
{ ...branchCallbacks, touchedKeys, branchPath }
|
|
783
|
+
);
|
|
784
|
+
return { name: branch.name, result, touchedKeys };
|
|
785
|
+
}
|
|
786
|
+
async function dispatchFork(forkNode, priorBranches, outerState, outerCtx, schema, shouldStop, callbacks) {
|
|
787
|
+
const priorByName = new Map((priorBranches ?? []).map((b) => [b.name, b]));
|
|
788
|
+
const localAborts = forkNode.branches.map(() => new AbortController());
|
|
789
|
+
const outerSignal = outerCtx.signal;
|
|
790
|
+
const branchSignals = forkNode.branches.map(
|
|
791
|
+
(_, i) => outerSignal !== void 0 ? AbortSignal.any([outerSignal, localAborts[i].signal]) : localAborts[i].signal
|
|
792
|
+
);
|
|
793
|
+
const { onBeforeStep: _outerOnBeforeStep, onAfterStep: _outerOnAfterStep, onComplete: _outerOnComplete, ...branchCallbacksRest } = callbacks ?? {};
|
|
794
|
+
const branchCallbacksBase = callbacks !== void 0 ? branchCallbacksRest : void 0;
|
|
795
|
+
const outcomes = await Promise.all(
|
|
796
|
+
forkNode.branches.map(async (branch, i) => {
|
|
797
|
+
const prior = priorByName.get(branch.name);
|
|
798
|
+
const outcome = await runBranch(branch, prior, outerState, outerCtx, schema, shouldStop, branchSignals[i], branchCallbacksBase);
|
|
799
|
+
if (outcome.result.paused && outcome.result.signal === "$error") {
|
|
800
|
+
for (const controller of localAborts) controller.abort();
|
|
801
|
+
}
|
|
802
|
+
return outcome;
|
|
803
|
+
})
|
|
804
|
+
);
|
|
805
|
+
const failed = outcomes.find((o) => o.result.paused && o.result.signal === "$error");
|
|
806
|
+
if (failed !== void 0) {
|
|
807
|
+
return { kind: "failed", error: failed.result.state.$error };
|
|
808
|
+
}
|
|
809
|
+
const anyPaused = outcomes.some((o) => o.result.paused);
|
|
810
|
+
if (anyPaused) {
|
|
811
|
+
const branches = outcomes.map((o) => {
|
|
812
|
+
if (o.result.paused) {
|
|
813
|
+
return {
|
|
814
|
+
name: o.name,
|
|
815
|
+
status: "paused",
|
|
816
|
+
state: o.result.state,
|
|
817
|
+
cursor: o.result.cursor,
|
|
818
|
+
touchedKeys: [...o.touchedKeys]
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
name: o.name,
|
|
823
|
+
status: "done",
|
|
824
|
+
state: o.result.state,
|
|
825
|
+
touchedKeys: [...o.touchedKeys]
|
|
826
|
+
};
|
|
827
|
+
});
|
|
828
|
+
return { kind: "paused", forkCursor: { kind: "fork", fork: forkNode.name, branches } };
|
|
829
|
+
}
|
|
830
|
+
const keyOwners = /* @__PURE__ */ new Map();
|
|
831
|
+
for (const outcome of outcomes) {
|
|
832
|
+
for (const key of outcome.touchedKeys) {
|
|
833
|
+
const owners = keyOwners.get(key) ?? [];
|
|
834
|
+
owners.push(outcome.name);
|
|
835
|
+
keyOwners.set(key, owners);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
for (const outcome of outcomes) {
|
|
839
|
+
for (const key of outcome.touchedKeys) {
|
|
840
|
+
const owners = keyOwners.get(key);
|
|
841
|
+
if (owners.length > 1) {
|
|
842
|
+
const reducer = schema?.[key]?.reduce;
|
|
843
|
+
if (reducer === void 0) {
|
|
844
|
+
throw new MissingReducerError(forkNode.name, key, owners);
|
|
845
|
+
}
|
|
846
|
+
outerState[key] = reducer(outerState[key], outcome.result.state[key]);
|
|
847
|
+
} else {
|
|
848
|
+
outerState[key] = outcome.result.state[key];
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return { kind: "success" };
|
|
853
|
+
}
|
|
854
|
+
async function handleFork(forkNode, priorBranches, state, ctx, schema, shouldStop, callbacks, obs, runCtx, runStart, implicitNextMap, graph) {
|
|
855
|
+
const stepCtx = {
|
|
856
|
+
agentId: ctx.agentId,
|
|
857
|
+
sessionId: ctx.sessionId,
|
|
858
|
+
stepName: forkNode.name,
|
|
859
|
+
...callbacks?.branchPath !== void 0 && callbacks.branchPath.length > 0 ? { branchPath: callbacks.branchPath } : {}
|
|
860
|
+
};
|
|
861
|
+
const stepStart = Date.now();
|
|
862
|
+
obs?.onStepStart?.(stepCtx);
|
|
863
|
+
const dispatch = await dispatchFork(forkNode, priorBranches, state, ctx, schema, shouldStop, callbacks);
|
|
864
|
+
if (dispatch.kind === "paused") {
|
|
865
|
+
obs?.onRunEnd?.(runCtx, { signal: "$interrupt", durationMs: Date.now() - runStart });
|
|
866
|
+
return { done: true, result: { state, signal: "$interrupt", cursor: dispatch.forkCursor, paused: true } };
|
|
867
|
+
}
|
|
868
|
+
if (dispatch.kind === "failed") {
|
|
869
|
+
state.$error = dispatch.error;
|
|
870
|
+
callbacks?.onError?.(state.$error, forkNode.name);
|
|
871
|
+
obs?.onStepError?.(stepCtx, { error: state.$error, durationMs: Date.now() - stepStart });
|
|
872
|
+
if (graph.onError !== void 0) {
|
|
873
|
+
return { done: false, nextCursor: graph.onError };
|
|
874
|
+
}
|
|
875
|
+
obs?.onRunEnd?.(runCtx, { signal: "$error", durationMs: Date.now() - runStart });
|
|
876
|
+
return { done: true, result: { state, signal: "$error", cursor: forkNode.name, paused: true } };
|
|
877
|
+
}
|
|
878
|
+
state.$error = null;
|
|
879
|
+
obs?.onStepEnd?.(stepCtx, { durationMs: Date.now() - stepStart });
|
|
880
|
+
callbacks?.onAfterStep?.(forkNode.name, state);
|
|
881
|
+
const next = forkNode.next ?? implicitNextMap.get(forkNode.name) ?? null;
|
|
882
|
+
if (next === null) {
|
|
883
|
+
throw new NoNextStepError(forkNode.name);
|
|
884
|
+
}
|
|
885
|
+
return { done: false, nextCursor: next };
|
|
886
|
+
}
|
|
581
887
|
|
|
582
888
|
// src/agent/session-store.ts
|
|
889
|
+
function collectPendingInterrupts(cursor, state) {
|
|
890
|
+
if (cursor !== void 0 && isForkCursor(cursor)) {
|
|
891
|
+
const result = [];
|
|
892
|
+
for (const branch of cursor.branches) {
|
|
893
|
+
if (branch.status !== "paused") continue;
|
|
894
|
+
const paused = branch;
|
|
895
|
+
for (const pi of collectPendingInterrupts(paused.cursor, paused.state)) {
|
|
896
|
+
result.push({ ...pi, step: `${cursor.fork}/${paused.name}/${pi.step}` });
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return result;
|
|
900
|
+
}
|
|
901
|
+
const interrupt = state["$interrupt"];
|
|
902
|
+
if (interrupt === null || interrupt === void 0) return [];
|
|
903
|
+
const step = typeof cursor === "string" ? cursor : "";
|
|
904
|
+
return [{ interruptId: interrupt.interruptId, prompt: interrupt.prompt, step }];
|
|
905
|
+
}
|
|
583
906
|
function storedSessionToPhase(loaded) {
|
|
584
907
|
if (loaded === null) {
|
|
585
908
|
return { phase: "fresh" };
|
|
586
909
|
}
|
|
587
910
|
if (loaded.phase === "paused") {
|
|
588
|
-
const step = loaded.step
|
|
911
|
+
const step = loaded.step === void 0 ? "" : isForkCursor(loaded.step) ? loaded.step.fork : loaded.step;
|
|
912
|
+
const pendingInterrupts = collectPendingInterrupts(loaded.step, loaded.finalState);
|
|
589
913
|
if (loaded.signal !== void 0) {
|
|
590
|
-
return { phase: "paused", step, signal: loaded.signal };
|
|
914
|
+
return { phase: "paused", step, signal: loaded.signal, pendingInterrupts };
|
|
591
915
|
}
|
|
592
|
-
return { phase: "paused", step };
|
|
916
|
+
return { phase: "paused", step, pendingInterrupts };
|
|
593
917
|
}
|
|
594
918
|
if (loaded.signal !== void 0) {
|
|
595
919
|
return { phase: "completed", signal: loaded.signal };
|
|
@@ -866,19 +1190,53 @@ var NoInterruptError = class extends Error {
|
|
|
866
1190
|
this.name = "NoInterruptError";
|
|
867
1191
|
}
|
|
868
1192
|
};
|
|
1193
|
+
function injectIntoForkCursor(cursor, interruptId, response) {
|
|
1194
|
+
let matched = false;
|
|
1195
|
+
const branches = cursor.branches.map((branch) => {
|
|
1196
|
+
if (matched || branch.status !== "paused") return branch;
|
|
1197
|
+
const paused = branch;
|
|
1198
|
+
const branchInterrupt = paused.state["$interrupt"];
|
|
1199
|
+
if (branchInterrupt !== null && branchInterrupt !== void 0 && branchInterrupt.interruptId === interruptId) {
|
|
1200
|
+
matched = true;
|
|
1201
|
+
const existing = paused.state["$interruptResponses"] ?? {};
|
|
1202
|
+
return {
|
|
1203
|
+
...paused,
|
|
1204
|
+
state: { ...paused.state, $interrupt: null, $interruptResponses: { ...existing, [interruptId]: response } }
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
if (isForkCursor(paused.cursor)) {
|
|
1208
|
+
const nested = injectIntoForkCursor(paused.cursor, interruptId, response);
|
|
1209
|
+
if (nested !== null) {
|
|
1210
|
+
matched = true;
|
|
1211
|
+
return { ...paused, cursor: nested };
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return branch;
|
|
1215
|
+
});
|
|
1216
|
+
return matched ? { ...cursor, branches } : null;
|
|
1217
|
+
}
|
|
1218
|
+
function injectInterruptResponseIntoState(cursor, state, interruptId, response) {
|
|
1219
|
+
if (cursor !== void 0 && isForkCursor(cursor)) {
|
|
1220
|
+
const updated = injectIntoForkCursor(cursor, interruptId, response);
|
|
1221
|
+
return updated === null ? { cursor, state, found: false } : { cursor: updated, state, found: true };
|
|
1222
|
+
}
|
|
1223
|
+
const interrupt = state["$interrupt"];
|
|
1224
|
+
if (interrupt === null || interrupt === void 0 || interrupt.interruptId !== interruptId) {
|
|
1225
|
+
return { cursor, state, found: false };
|
|
1226
|
+
}
|
|
1227
|
+
const existing = state["$interruptResponses"] ?? {};
|
|
1228
|
+
return {
|
|
1229
|
+
cursor,
|
|
1230
|
+
state: { ...state, $interrupt: null, $interruptResponses: { ...existing, [interruptId]: response } },
|
|
1231
|
+
found: true
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
869
1234
|
async function injectInterruptResponse(store, agentId, sessionId, interruptId, response) {
|
|
870
1235
|
const loaded = await store.load(agentId, sessionId);
|
|
871
1236
|
if (loaded === null) throw new NoInterruptError();
|
|
872
1237
|
if (loaded.phase !== "paused") throw new NoInterruptError();
|
|
873
|
-
const
|
|
874
|
-
if (
|
|
875
|
-
if (interrupt.interruptId !== interruptId) throw new NoInterruptError();
|
|
876
|
-
const existingResponses = loaded.finalState.$interruptResponses ?? {};
|
|
877
|
-
const newState = {
|
|
878
|
-
...loaded.finalState,
|
|
879
|
-
$interrupt: null,
|
|
880
|
-
$interruptResponses: { ...existingResponses, [interruptId]: response }
|
|
881
|
-
};
|
|
1238
|
+
const result = injectInterruptResponseIntoState(loaded.step, loaded.finalState, interruptId, response);
|
|
1239
|
+
if (!result.found) throw new NoInterruptError();
|
|
882
1240
|
const updated = {
|
|
883
1241
|
agentId: loaded.agentId,
|
|
884
1242
|
runId: loaded.runId,
|
|
@@ -888,8 +1246,8 @@ async function injectInterruptResponse(store, agentId, sessionId, interruptId, r
|
|
|
888
1246
|
settledAt: loaded.settledAt,
|
|
889
1247
|
phase: "paused",
|
|
890
1248
|
initialState: loaded.initialState,
|
|
891
|
-
finalState:
|
|
892
|
-
...
|
|
1249
|
+
finalState: result.state,
|
|
1250
|
+
...result.cursor !== void 0 ? { step: result.cursor } : {},
|
|
893
1251
|
...loaded.signal !== void 0 ? { signal: loaded.signal } : {}
|
|
894
1252
|
};
|
|
895
1253
|
await store.save(agentId, sessionId, updated);
|
|
@@ -1448,11 +1806,10 @@ function createAgent(id, h, slots, agentOptions) {
|
|
|
1448
1806
|
}
|
|
1449
1807
|
const prev = lastResult;
|
|
1450
1808
|
if (prev === null) throw new NoInterruptError();
|
|
1451
|
-
const
|
|
1452
|
-
|
|
1453
|
-
state
|
|
1454
|
-
|
|
1455
|
-
const cursor = prev.cursor ?? void 0;
|
|
1809
|
+
const injected = injectInterruptResponseIntoState(prev.cursor ?? void 0, prev.state, interruptId, response);
|
|
1810
|
+
if (!injected.found) throw new NoInterruptError();
|
|
1811
|
+
const state = injected.state;
|
|
1812
|
+
const cursor = injected.cursor;
|
|
1456
1813
|
const r = await runLoop(
|
|
1457
1814
|
agentInternals.loopDef,
|
|
1458
1815
|
state,
|
|
@@ -1560,6 +1917,8 @@ export {
|
|
|
1560
1917
|
createAgent,
|
|
1561
1918
|
createHarness,
|
|
1562
1919
|
field,
|
|
1920
|
+
isForkCursor,
|
|
1921
|
+
isForkDef,
|
|
1563
1922
|
isRequiredMarker,
|
|
1564
1923
|
isRuntimeMarker,
|
|
1565
1924
|
required,
|