@noetaris/harness 0.3.1 → 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 +258 -125
- package/dist/index.js +479 -81
- 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 };
|
|
@@ -732,7 +1056,7 @@ function noopStop() {
|
|
|
732
1056
|
}
|
|
733
1057
|
|
|
734
1058
|
// src/agent/session-lifecycle.ts
|
|
735
|
-
function resolveSessionStore(storeEntries) {
|
|
1059
|
+
function resolveSessionStore(storeEntries, overrides) {
|
|
736
1060
|
let result = void 0;
|
|
737
1061
|
for (const entry of storeEntries) {
|
|
738
1062
|
if (entry.kind !== "store") continue;
|
|
@@ -740,7 +1064,8 @@ function resolveSessionStore(storeEntries) {
|
|
|
740
1064
|
if (value === null || typeof value !== "object") continue;
|
|
741
1065
|
const rec = value;
|
|
742
1066
|
if (!("session" in rec)) continue;
|
|
743
|
-
const
|
|
1067
|
+
const rawSession = rec["session"];
|
|
1068
|
+
const session = overrides !== void 0 && "session" in overrides ? overrides["session"] : rawSession;
|
|
744
1069
|
if (session === null || typeof session !== "object") continue;
|
|
745
1070
|
const s = session;
|
|
746
1071
|
if (typeof s["load"] === "function" && typeof s["save"] === "function") {
|
|
@@ -865,19 +1190,53 @@ var NoInterruptError = class extends Error {
|
|
|
865
1190
|
this.name = "NoInterruptError";
|
|
866
1191
|
}
|
|
867
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
|
+
}
|
|
868
1234
|
async function injectInterruptResponse(store, agentId, sessionId, interruptId, response) {
|
|
869
1235
|
const loaded = await store.load(agentId, sessionId);
|
|
870
1236
|
if (loaded === null) throw new NoInterruptError();
|
|
871
1237
|
if (loaded.phase !== "paused") throw new NoInterruptError();
|
|
872
|
-
const
|
|
873
|
-
if (
|
|
874
|
-
if (interrupt.interruptId !== interruptId) throw new NoInterruptError();
|
|
875
|
-
const existingResponses = loaded.finalState.$interruptResponses ?? {};
|
|
876
|
-
const newState = {
|
|
877
|
-
...loaded.finalState,
|
|
878
|
-
$interrupt: null,
|
|
879
|
-
$interruptResponses: { ...existingResponses, [interruptId]: response }
|
|
880
|
-
};
|
|
1238
|
+
const result = injectInterruptResponseIntoState(loaded.step, loaded.finalState, interruptId, response);
|
|
1239
|
+
if (!result.found) throw new NoInterruptError();
|
|
881
1240
|
const updated = {
|
|
882
1241
|
agentId: loaded.agentId,
|
|
883
1242
|
runId: loaded.runId,
|
|
@@ -887,8 +1246,8 @@ async function injectInterruptResponse(store, agentId, sessionId, interruptId, r
|
|
|
887
1246
|
settledAt: loaded.settledAt,
|
|
888
1247
|
phase: "paused",
|
|
889
1248
|
initialState: loaded.initialState,
|
|
890
|
-
finalState:
|
|
891
|
-
...
|
|
1249
|
+
finalState: result.state,
|
|
1250
|
+
...result.cursor !== void 0 ? { step: result.cursor } : {},
|
|
892
1251
|
...loaded.signal !== void 0 ? { signal: loaded.signal } : {}
|
|
893
1252
|
};
|
|
894
1253
|
await store.save(agentId, sessionId, updated);
|
|
@@ -1022,6 +1381,24 @@ var UnknownRunSlotError = class extends Error {
|
|
|
1022
1381
|
this.key = key;
|
|
1023
1382
|
}
|
|
1024
1383
|
};
|
|
1384
|
+
var MissingStoreSlotError = class extends Error {
|
|
1385
|
+
/** The store key (e.g. `"session"`) that was not provided. */
|
|
1386
|
+
key;
|
|
1387
|
+
constructor(key) {
|
|
1388
|
+
super(`required store slot "${key}" was not provided in createAgent options.stores`);
|
|
1389
|
+
this.name = "MissingStoreSlotError";
|
|
1390
|
+
this.key = key;
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
var UnexpectedStoreSlotError = class extends Error {
|
|
1394
|
+
/** The unexpected store key. */
|
|
1395
|
+
key;
|
|
1396
|
+
constructor(key) {
|
|
1397
|
+
super(`store slot "${key}" was not declared as required() in the harness \u2014 do not pass it in options.stores`);
|
|
1398
|
+
this.name = "UnexpectedStoreSlotError";
|
|
1399
|
+
this.key = key;
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1025
1402
|
var _agentInternals = /* @__PURE__ */ Symbol("_agentInternals");
|
|
1026
1403
|
function createAgent(id, h, slots, agentOptions) {
|
|
1027
1404
|
const internals = getInternals(h);
|
|
@@ -1070,7 +1447,27 @@ function createAgent(id, h, slots, agentOptions) {
|
|
|
1070
1447
|
}
|
|
1071
1448
|
}
|
|
1072
1449
|
}
|
|
1073
|
-
|
|
1450
|
+
let sessionIsRequired = false;
|
|
1451
|
+
for (const entry of storeEntries) {
|
|
1452
|
+
const storeValue = entry.value;
|
|
1453
|
+
if (storeValue === null || typeof storeValue !== "object") continue;
|
|
1454
|
+
const storeRec = storeValue;
|
|
1455
|
+
if ("session" in storeRec && isRequiredMarker(storeRec["session"])) {
|
|
1456
|
+
sessionIsRequired = true;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
const sessionStoreOverride = agentOptions?.stores?.["session"];
|
|
1460
|
+
const storeOverrides = {};
|
|
1461
|
+
if (sessionIsRequired) {
|
|
1462
|
+
if (sessionStoreOverride === void 0) throw new MissingStoreSlotError("session");
|
|
1463
|
+
storeOverrides["session"] = sessionStoreOverride;
|
|
1464
|
+
} else if (sessionStoreOverride !== void 0) {
|
|
1465
|
+
throw new UnexpectedStoreSlotError("session");
|
|
1466
|
+
}
|
|
1467
|
+
const capturedStore = resolveSessionStore(
|
|
1468
|
+
storeEntries,
|
|
1469
|
+
Object.keys(storeOverrides).length > 0 ? storeOverrides : void 0
|
|
1470
|
+
);
|
|
1074
1471
|
const agentInternals = {
|
|
1075
1472
|
agentId: id,
|
|
1076
1473
|
resolvedProviders,
|
|
@@ -1409,11 +1806,10 @@ function createAgent(id, h, slots, agentOptions) {
|
|
|
1409
1806
|
}
|
|
1410
1807
|
const prev = lastResult;
|
|
1411
1808
|
if (prev === null) throw new NoInterruptError();
|
|
1412
|
-
const
|
|
1413
|
-
|
|
1414
|
-
state
|
|
1415
|
-
|
|
1416
|
-
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;
|
|
1417
1813
|
const r = await runLoop(
|
|
1418
1814
|
agentInternals.loopDef,
|
|
1419
1815
|
state,
|
|
@@ -1521,6 +1917,8 @@ export {
|
|
|
1521
1917
|
createAgent,
|
|
1522
1918
|
createHarness,
|
|
1523
1919
|
field,
|
|
1920
|
+
isForkCursor,
|
|
1921
|
+
isForkDef,
|
|
1524
1922
|
isRequiredMarker,
|
|
1525
1923
|
isRuntimeMarker,
|
|
1526
1924
|
required,
|