@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/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 };
@@ -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 session = rec["session"];
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 interrupt = loaded.finalState.$interrupt;
873
- if (interrupt === null) throw new NoInterruptError();
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: newState,
891
- ...loaded.step !== void 0 ? { step: loaded.step } : {},
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
- const capturedStore = resolveSessionStore(storeEntries);
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 state = prev.state;
1413
- const existing = state.$interruptResponses ?? {};
1414
- state.$interruptResponses = { ...existing, [interruptId]: response };
1415
- state.$interrupt = null;
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,