@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/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 currentStep = state.steps.at(-1);
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?.transitions.push({
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?.transitions.push({
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 currentStep = state.steps.at(-1);
96
- if (currentStep !== void 0) {
97
- currentStep.next = name;
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((step) => {
116
- const transitions = Object.freeze([...step.transitions]);
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: step.name,
123
- run: step.run,
124
- route: step.route,
171
+ name: node.name,
172
+ run: node.run,
173
+ route: node.route,
125
174
  transitions,
126
- next: step.next,
127
- errorAware: step.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
- for (const step of def.steps) {
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 def.steps) {
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, step] of def.steps.entries()) {
164
- if (step === void 0) continue;
165
- if (step.route === void 0) {
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 && step.next === void 0) {
271
+ if (isLast && node.next === void 0) {
168
272
  violations.push(
169
- `step "${step.name}" has no route, no explicit .next(), and no following step \u2014 no exit path`
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 def.steps) {
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 def.steps) {
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
- if (step.next !== void 0) {
197
- if (!names.has(step.next)) {
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 "${step.name}" has .next("${step.next}") but step "${step.next}" is not declared`
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 def.steps) {
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 currentStep = def.steps.find((s) => s.name === currentName);
240
- if (!currentStep) continue;
241
- for (const transition of currentStep.transitions) {
242
- if (transition.target.kind === "step") {
243
- if (!reachable.has(transition.target.name)) {
244
- reachable.add(transition.target.name);
245
- queue.push(transition.target.name);
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
- if (currentStep.route !== void 0) {
250
- if (currentStep.next !== void 0 && !reachable.has(currentStep.next)) {
251
- reachable.add(currentStep.next);
252
- queue.push(currentStep.next);
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 (currentStep.next !== void 0) {
256
- if (!reachable.has(currentStep.next)) {
257
- reachable.add(currentStep.next);
258
- queue.push(currentStep.next);
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 nextStep = def.steps[currentIndex + 1];
264
- if (nextStep !== void 0) {
265
- const nextStepName = nextStep.name;
266
- if (!reachable.has(nextStepName)) {
267
- reachable.add(nextStepName);
268
- queue.push(nextStepName);
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 def.steps) {
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
- if (violations.length > 0) {
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 step = graph.steps.find((s) => s.name === cursor);
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 = { agentId: ctx.agentId, sessionId: ctx.sessionId, stepName: cursor };
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 interrupt = loaded.finalState.$interrupt;
874
- if (interrupt === null) throw new NoInterruptError();
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: newState,
892
- ...loaded.step !== void 0 ? { step: loaded.step } : {},
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 state = prev.state;
1452
- const existing = state.$interruptResponses ?? {};
1453
- state.$interruptResponses = { ...existing, [interruptId]: response };
1454
- state.$interrupt = null;
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,