@noetaris/harness 0.5.0 → 0.7.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
@@ -510,7 +510,27 @@ function safeInvoke(hookName, call, sink) {
510
510
  reportObserverError(error, { hookName }, sink);
511
511
  }
512
512
  }
513
+ async function runWrapped(wrap, fn, onObserverError) {
514
+ let p;
515
+ const inner = () => {
516
+ if (p === void 0) {
517
+ p = new Promise((resolve) => resolve(fn()));
518
+ p.catch(() => {
519
+ });
520
+ }
521
+ return p;
522
+ };
523
+ try {
524
+ await wrap(inner);
525
+ } catch (e) {
526
+ const started = p;
527
+ const passedThrough = started !== void 0 && await started.then(() => false, (err) => err === e);
528
+ if (!passedThrough) reportObserverError(e, { hookName: "wrap" }, onObserverError);
529
+ }
530
+ return inner();
531
+ }
513
532
  function composeObservers(observers, onObserverError) {
533
+ const wrappers = observers.filter((o) => o.wrap !== void 0);
514
534
  return {
515
535
  onRunStart: (ctx) => {
516
536
  for (const o of observers) safeInvoke("onRunStart", () => o.onRunStart?.(ctx), onObserverError);
@@ -532,6 +552,14 @@ function composeObservers(observers, onObserverError) {
532
552
  },
533
553
  onEvent: (ctx, type, payload) => {
534
554
  for (const o of observers) safeInvoke("onEvent", () => o.onEvent?.(ctx, type, payload), onObserverError);
555
+ },
556
+ onStepSettled: (ctx, event) => {
557
+ for (const o of observers) safeInvoke("onStepSettled", () => o.onStepSettled?.(ctx, event), onObserverError);
558
+ },
559
+ // First observer outermost; each layer isolated on its own by runWrapped.
560
+ wrap: (ctx, scope, fn) => {
561
+ const layer = (i) => i === wrappers.length ? fn() : runWrapped((inner) => wrappers[i].wrap(ctx, scope, inner), () => layer(i + 1), onObserverError);
562
+ return layer(0);
535
563
  }
536
564
  };
537
565
  }
@@ -585,6 +613,21 @@ function extractRunListeners(resources) {
585
613
  return raw;
586
614
  }
587
615
 
616
+ // src/agent/ctx-within.ts
617
+ function createWithinFn(observer, currentStep, onObserverError) {
618
+ return (type, payload, fn) => {
619
+ const wrap = observer?.wrap;
620
+ const stepCtx = currentStep();
621
+ if (wrap === void 0 || stepCtx === null) return fn();
622
+ return runWrapped((inner) => wrap(stepCtx, { kind: "event", type, payload }, inner), fn, onObserverError);
623
+ };
624
+ }
625
+ function runStepScope(observer, stepCtx, fn, onObserverError) {
626
+ const wrap = observer?.wrap;
627
+ if (wrap === void 0) return fn();
628
+ return runWrapped((inner) => wrap(stepCtx, { kind: "step" }, inner), fn, onObserverError);
629
+ }
630
+
588
631
  // src/loop/loop-executor.ts
589
632
  var NOOP_OBSERVER = {};
590
633
  function isForkCursor(cursor) {
@@ -645,6 +688,24 @@ function applyUpdate(state, update, schema, touchedKeys) {
645
688
  touchedKeys?.add(key);
646
689
  }
647
690
  }
691
+ function fireSettled(obs, stepCtx, stepStart, fields, onObserverError) {
692
+ const hook = obs?.onStepSettled;
693
+ if (hook === void 0) return;
694
+ const event = { ...fields, durationMs: Date.now() - stepStart };
695
+ safeInvoke("onStepSettled", () => hook(stepCtx, event), onObserverError);
696
+ }
697
+ function stepContextOf(runCtx, stepName) {
698
+ return {
699
+ agentId: runCtx.agentId,
700
+ sessionId: runCtx.sessionId,
701
+ runId: runCtx.runId,
702
+ stepName,
703
+ ...runCtx.branchPath !== void 0 ? { branchPath: runCtx.branchPath } : {}
704
+ };
705
+ }
706
+ function asError(e) {
707
+ return e instanceof Error ? e : new Error(String(e));
708
+ }
648
709
  async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callbacks) {
649
710
  const { onBeforeStep, onAfterStep, onError, onComplete, onInterrupt } = callbacks ?? {};
650
711
  const obs = callbacks?.observer;
@@ -663,150 +724,177 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
663
724
  ...callbacks?.branchPath !== void 0 && callbacks.branchPath.length > 0 ? { branchPath: callbacks.branchPath } : {}
664
725
  };
665
726
  safeInvoke("onRunStart", () => obs?.onRunStart?.(runCtx), onObserverError);
727
+ let runEnded = false;
728
+ const endRun = (signal) => {
729
+ runEnded = true;
730
+ safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal, durationMs: Date.now() - runStart }), onObserverError);
731
+ };
666
732
  const callCountRef = { current: 0 };
667
733
  const stepCtxRef = { current: null };
668
734
  ctx["interrupt"] = createInterruptFn(state, callCountRef, callbacks?.branchPath);
669
735
  ctx["emit"] = createEmitFn(callbacks?.listeners ?? {}, obs, stepCtxRef, onObserverError);
736
+ ctx["within"] = createWithinFn(obs, () => stepCtxRef.current, onObserverError);
737
+ const telemetryObserver = obs !== void 0 ? composeObservers([obs], onObserverError) : NOOP_OBSERVER;
670
738
  let cursor = startCursor ?? graph.entryStep;
671
- while (true) {
672
- if (shouldStop?.()) {
673
- safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal: "$stopped", durationMs: Date.now() - runStart }), onObserverError);
674
- return { state, signal: null, cursor, paused: true };
675
- }
676
- if (isForkCursor(cursor)) {
677
- const forkCursor = cursor;
678
- const forkNode = graph.steps.find((s) => s.name === forkCursor.fork);
679
- if (!isForkDef(forkNode)) {
680
- throw new NoNextStepError(forkCursor.fork);
681
- }
682
- onBeforeStep?.(forkNode.name, { ...state });
683
- const outcome = await handleFork(
684
- forkNode,
685
- forkCursor.branches,
686
- state,
687
- ctx,
688
- schema,
689
- shouldStop,
690
- callbacks,
691
- obs,
692
- runCtx,
693
- runStart,
694
- implicitNextMap,
695
- graph
696
- );
697
- if (outcome.done) {
698
- return outcome.result;
699
- }
700
- cursor = outcome.nextCursor;
701
- continue;
702
- }
703
- onBeforeStep?.(cursor, { ...state });
704
- const node = graph.steps.find((s) => s.name === cursor);
705
- if (isForkDef(node)) {
706
- const outcome = await handleFork(
707
- node,
708
- void 0,
709
- state,
710
- ctx,
711
- schema,
712
- shouldStop,
713
- callbacks,
714
- obs,
715
- runCtx,
716
- runStart,
717
- implicitNextMap,
718
- graph
719
- );
720
- if (outcome.done) {
721
- return outcome.result;
722
- }
723
- cursor = outcome.nextCursor;
724
- continue;
725
- }
726
- const step = node;
727
- callCountRef.current = 0;
728
- const stepCtx = {
729
- agentId: ctx.agentId,
730
- sessionId: ctx.sessionId,
731
- stepName: cursor,
732
- ...callbacks?.branchPath !== void 0 && callbacks.branchPath.length > 0 ? { branchPath: callbacks.branchPath } : {}
733
- };
734
- stepCtxRef.current = stepCtx;
735
- const stepStart = Date.now();
736
- safeInvoke("onStepStart", () => obs?.onStepStart?.(stepCtx), onObserverError);
737
- if (callbacks?.telemetrySlots !== void 0) {
738
- const telemetry = { observer: obs ?? NOOP_OBSERVER, stepContext: stepCtx };
739
- for (const { key, slot } of callbacks.telemetrySlots) {
740
- ;
741
- ctx[key] = slot.withTelemetry(telemetry);
739
+ try {
740
+ while (true) {
741
+ if (shouldStop?.()) {
742
+ endRun("$stopped");
743
+ return { state, signal: null, cursor, paused: true };
742
744
  }
743
- }
744
- let runSucceeded = false;
745
- if (step.run !== void 0) {
746
- const hadResponses = Object.keys(state.$interruptResponses).length > 0;
747
- try {
748
- const update = await step.run(
745
+ if (isForkCursor(cursor)) {
746
+ const forkCursor = cursor;
747
+ const forkNode = graph.steps.find((s) => s.name === forkCursor.fork);
748
+ if (!isForkDef(forkNode)) {
749
+ throw new NoNextStepError(forkCursor.fork);
750
+ }
751
+ onBeforeStep?.(forkNode.name, { ...state });
752
+ const outcome = await handleFork(
753
+ forkNode,
754
+ forkCursor.branches,
749
755
  state,
750
- ctx
756
+ ctx,
757
+ schema,
758
+ shouldStop,
759
+ callbacks,
760
+ obs,
761
+ runCtx,
762
+ endRun,
763
+ implicitNextMap,
764
+ graph
751
765
  );
752
- applyUpdate(state, update, schema, callbacks?.touchedKeys);
753
- if (hadResponses) {
754
- state.$interrupt = null;
755
- state.$interruptResponses = {};
766
+ if (outcome.done) {
767
+ return outcome.result;
756
768
  }
757
- runSucceeded = true;
758
- } catch (e) {
759
- if (isInterruptPause(e)) {
760
- onInterrupt?.(e.prompt, e.interruptId);
761
- safeInvoke("onInterrupt", () => obs?.onInterrupt?.(stepCtx, { prompt: e.prompt, interruptId: e.interruptId }), onObserverError);
762
- safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal: "$interrupt", durationMs: Date.now() - runStart }), onObserverError);
763
- return { state, signal: "$interrupt", cursor, paused: true };
769
+ cursor = outcome.nextCursor;
770
+ continue;
771
+ }
772
+ onBeforeStep?.(cursor, { ...state });
773
+ const node = graph.steps.find((s) => s.name === cursor);
774
+ if (isForkDef(node)) {
775
+ const outcome = await handleFork(
776
+ node,
777
+ void 0,
778
+ state,
779
+ ctx,
780
+ schema,
781
+ shouldStop,
782
+ callbacks,
783
+ obs,
784
+ runCtx,
785
+ endRun,
786
+ implicitNextMap,
787
+ graph
788
+ );
789
+ if (outcome.done) {
790
+ return outcome.result;
764
791
  }
765
- state.$error = e instanceof Error ? e : new Error(String(e));
766
- onError?.(state.$error, cursor);
767
- safeInvoke("onStepError", () => obs?.onStepError?.(stepCtx, { error: state.$error, durationMs: Date.now() - stepStart }), onObserverError);
792
+ cursor = outcome.nextCursor;
793
+ continue;
768
794
  }
769
- }
770
- if (runSucceeded) {
771
- state.$error = null;
772
- onAfterStep?.(cursor, state);
773
- safeInvoke("onStepEnd", () => obs?.onStepEnd?.(stepCtx, { durationMs: Date.now() - stepStart }), onObserverError);
774
- }
775
- const callRoute = step.route !== void 0 && (runSucceeded || step.run === void 0 || step.errorAware);
776
- if (callRoute) {
777
- const routeFn = step.route;
778
- let signal;
779
- try {
780
- signal = routeFn(state);
781
- } catch (e) {
782
- state.$error = e instanceof Error ? e : new Error(String(e));
783
- safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal: "$error", durationMs: Date.now() - runStart }), onObserverError);
784
- return { state, signal: "$error", cursor, paused: true };
795
+ const step = node;
796
+ callCountRef.current = 0;
797
+ const stepCtx = stepContextOf(runCtx, cursor);
798
+ stepCtxRef.current = stepCtx;
799
+ const stepStart = Date.now();
800
+ safeInvoke("onStepStart", () => obs?.onStepStart?.(stepCtx), onObserverError);
801
+ if (callbacks?.telemetrySlots !== void 0) {
802
+ const telemetry = {
803
+ observer: telemetryObserver,
804
+ stepContext: stepCtx,
805
+ within: createWithinFn(obs, () => stepCtx, onObserverError)
806
+ };
807
+ for (const { key, slot } of callbacks.telemetrySlots) {
808
+ ;
809
+ ctx[key] = slot.withTelemetry(telemetry);
810
+ }
785
811
  }
786
- const transition = step.transitions.find((t) => t.signal === signal);
787
- if (transition === void 0) {
788
- throw new UnknownSignalError(cursor, signal);
812
+ let runSucceeded = false;
813
+ let applied;
814
+ const settle = (fields) => fireSettled(obs, stepCtx, stepStart, { ...fields, ...applied !== void 0 ? { update: applied } : {} }, onObserverError);
815
+ if (step.run !== void 0) {
816
+ const hadResponses = Object.keys(state.$interruptResponses).length > 0;
817
+ try {
818
+ const run = step.run;
819
+ const update = await runStepScope(obs, stepCtx, () => run(
820
+ state,
821
+ ctx
822
+ ), onObserverError);
823
+ applyUpdate(state, update, schema, callbacks?.touchedKeys);
824
+ applied = update;
825
+ if (hadResponses) {
826
+ state.$interrupt = null;
827
+ state.$interruptResponses = {};
828
+ }
829
+ runSucceeded = true;
830
+ } catch (e) {
831
+ if (isInterruptPause(e)) {
832
+ onInterrupt?.(e.prompt, e.interruptId);
833
+ safeInvoke("onInterrupt", () => obs?.onInterrupt?.(stepCtx, { prompt: e.prompt, interruptId: e.interruptId }), onObserverError);
834
+ settle({ outcome: "interrupt", interrupt: { interruptId: e.interruptId, prompt: e.prompt }, next: { kind: "pause" } });
835
+ endRun("$interrupt");
836
+ return { state, signal: "$interrupt", cursor, paused: true };
837
+ }
838
+ state.$error = e instanceof Error ? e : new Error(String(e));
839
+ onError?.(state.$error, cursor);
840
+ safeInvoke("onStepError", () => obs?.onStepError?.(stepCtx, { error: state.$error, durationMs: Date.now() - stepStart }), onObserverError);
841
+ }
789
842
  }
790
- if (transition.target.kind === "end") {
791
- onComplete?.(state, signal);
792
- safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal, durationMs: Date.now() - runStart }), onObserverError);
793
- return { state, signal, cursor: null, paused: false };
843
+ if (runSucceeded) {
844
+ state.$error = null;
845
+ onAfterStep?.(cursor, state);
846
+ safeInvoke("onStepEnd", () => obs?.onStepEnd?.(stepCtx, { durationMs: Date.now() - stepStart }), onObserverError);
794
847
  }
795
- cursor = transition.target.name;
796
- } else if (step.run !== void 0 && !runSucceeded) {
797
- if (graph.onError !== void 0) {
798
- cursor = graph.onError;
848
+ const callRoute = step.route !== void 0 && (runSucceeded || step.run === void 0 || step.errorAware);
849
+ if (callRoute) {
850
+ const routeFn = step.route;
851
+ let signal;
852
+ try {
853
+ signal = routeFn(state);
854
+ } catch (e) {
855
+ state.$error = e instanceof Error ? e : new Error(String(e));
856
+ settle({ outcome: "error", error: state.$error, next: { kind: "pause" } });
857
+ endRun("$error");
858
+ return { state, signal: "$error", cursor, paused: true };
859
+ }
860
+ const routed = runSucceeded || step.run === void 0 ? { outcome: "ok", signal } : { outcome: "error", signal, error: state.$error };
861
+ const transition = step.transitions.find((t) => t.signal === signal);
862
+ if (transition === void 0) {
863
+ const unknownSignal = new UnknownSignalError(cursor, signal);
864
+ settle({ outcome: "error", signal, error: unknownSignal, next: { kind: "throw" } });
865
+ throw unknownSignal;
866
+ }
867
+ if (transition.target.kind === "end") {
868
+ settle({ ...routed, next: { kind: "end" } });
869
+ onComplete?.(state, signal);
870
+ endRun(signal);
871
+ return { state, signal, cursor: null, paused: false };
872
+ }
873
+ settle({ ...routed, next: { kind: "step", name: transition.target.name } });
874
+ cursor = transition.target.name;
875
+ } else if (step.run !== void 0 && !runSucceeded) {
876
+ if (graph.onError !== void 0) {
877
+ settle({ outcome: "error", error: state.$error, next: { kind: "step", name: graph.onError } });
878
+ cursor = graph.onError;
879
+ } else {
880
+ settle({ outcome: "error", error: state.$error, next: { kind: "pause" } });
881
+ endRun("$error");
882
+ return { state, signal: "$error", cursor, paused: true };
883
+ }
799
884
  } else {
800
- safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal: "$error", durationMs: Date.now() - runStart }), onObserverError);
801
- return { state, signal: "$error", cursor, paused: true };
802
- }
803
- } else {
804
- const next = step.next ?? implicitNextMap.get(cursor) ?? null;
805
- if (next === null) {
806
- throw new NoNextStepError(cursor);
885
+ const next = step.next ?? implicitNextMap.get(cursor) ?? null;
886
+ if (next === null) {
887
+ const noNext = new NoNextStepError(cursor);
888
+ settle({ outcome: "error", error: noNext, next: { kind: "throw" } });
889
+ throw noNext;
890
+ }
891
+ settle({ outcome: "ok", next: { kind: "step", name: next } });
892
+ cursor = next;
807
893
  }
808
- cursor = next;
809
894
  }
895
+ } catch (e) {
896
+ if (!runEnded) endRun("$error");
897
+ throw e;
810
898
  }
811
899
  }
812
900
  async function runBranch(branch, prior, outerState, outerCtx, schema, shouldStop, branchSignal, branchCallbacks) {
@@ -842,16 +930,25 @@ async function dispatchFork(forkNode, priorBranches, outerState, outerCtx, schem
842
930
  );
843
931
  const { onBeforeStep: _outerOnBeforeStep, onAfterStep: _outerOnAfterStep, onComplete: _outerOnComplete, ...branchCallbacksRest } = callbacks ?? {};
844
932
  const branchCallbacksBase = callbacks !== void 0 ? branchCallbacksRest : void 0;
845
- const outcomes = await Promise.all(
933
+ const settled = await Promise.allSettled(
846
934
  forkNode.branches.map(async (branch, i) => {
847
935
  const prior = priorByName.get(branch.name);
848
- const outcome = await runBranch(branch, prior, outerState, outerCtx, schema, shouldStop, branchSignals[i], branchCallbacksBase);
936
+ let outcome;
937
+ try {
938
+ outcome = await runBranch(branch, prior, outerState, outerCtx, schema, shouldStop, branchSignals[i], branchCallbacksBase);
939
+ } catch (e) {
940
+ for (const controller of localAborts) controller.abort();
941
+ throw e;
942
+ }
849
943
  if (outcome.result.paused && outcome.result.signal === "$error") {
850
944
  for (const controller of localAborts) controller.abort();
851
945
  }
852
946
  return outcome;
853
947
  })
854
948
  );
949
+ const rejected = settled.find((r) => r.status === "rejected");
950
+ if (rejected !== void 0) throw rejected.reason;
951
+ const outcomes = settled.map((r) => r.value);
855
952
  const failed = outcomes.find((o) => o.result.paused && o.result.signal === "$error");
856
953
  if (failed !== void 0) {
857
954
  return { kind: "failed", error: failed.result.state.$error };
@@ -875,7 +972,8 @@ async function dispatchFork(forkNode, priorBranches, outerState, outerCtx, schem
875
972
  touchedKeys: [...o.touchedKeys]
876
973
  };
877
974
  });
878
- return { kind: "paused", forkCursor: { kind: "fork", fork: forkNode.name, branches } };
975
+ const interrupted = outcomes.some((o) => o.result.paused && o.result.signal === "$interrupt");
976
+ return { kind: "paused", forkCursor: { kind: "fork", fork: forkNode.name, branches }, interrupted };
879
977
  }
880
978
  const keyOwners = /* @__PURE__ */ new Map();
881
979
  for (const outcome of outcomes) {
@@ -899,31 +997,46 @@ async function dispatchFork(forkNode, priorBranches, outerState, outerCtx, schem
899
997
  }
900
998
  }
901
999
  }
902
- return { kind: "success" };
903
- }
904
- async function handleFork(forkNode, priorBranches, state, ctx, schema, shouldStop, callbacks, obs, runCtx, runStart, implicitNextMap, graph) {
905
- const stepCtx = {
906
- agentId: ctx.agentId,
907
- sessionId: ctx.sessionId,
908
- stepName: forkNode.name,
909
- ...callbacks?.branchPath !== void 0 && callbacks.branchPath.length > 0 ? { branchPath: callbacks.branchPath } : {}
1000
+ return {
1001
+ kind: "success",
1002
+ branches: outcomes.map((o) => ({ name: o.name, status: "done", touchedKeys: [...o.touchedKeys] }))
910
1003
  };
1004
+ }
1005
+ async function handleFork(forkNode, priorBranches, state, ctx, schema, shouldStop, callbacks, obs, runCtx, endRun, implicitNextMap, graph) {
1006
+ const stepCtx = stepContextOf(runCtx, forkNode.name);
911
1007
  const stepStart = Date.now();
912
1008
  const onObserverError = callbacks?.onObserverError;
913
1009
  safeInvoke("onStepStart", () => obs?.onStepStart?.(stepCtx), onObserverError);
914
- const dispatch = await dispatchFork(forkNode, priorBranches, state, ctx, schema, shouldStop, callbacks);
1010
+ let dispatch;
1011
+ try {
1012
+ dispatch = await dispatchFork(forkNode, priorBranches, state, ctx, schema, shouldStop, callbacks);
1013
+ } catch (e) {
1014
+ fireSettled(obs, stepCtx, stepStart, { outcome: "error", error: asError(e), next: { kind: "throw" } }, onObserverError);
1015
+ throw e;
1016
+ }
915
1017
  if (dispatch.kind === "paused") {
916
- safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal: "$interrupt", durationMs: Date.now() - runStart }), onObserverError);
917
- return { done: true, result: { state, signal: "$interrupt", cursor: dispatch.forkCursor, paused: true } };
1018
+ const fork = {
1019
+ branches: dispatch.forkCursor.branches.map((b) => ({ name: b.name, status: b.status, touchedKeys: b.touchedKeys }))
1020
+ };
1021
+ if (dispatch.interrupted === true) {
1022
+ fireSettled(obs, stepCtx, stepStart, { outcome: "interrupt", next: { kind: "pause" }, fork }, onObserverError);
1023
+ endRun("$interrupt");
1024
+ return { done: true, result: { state, signal: "$interrupt", cursor: dispatch.forkCursor, paused: true } };
1025
+ }
1026
+ fireSettled(obs, stepCtx, stepStart, { outcome: "stopped", next: { kind: "pause" }, fork }, onObserverError);
1027
+ endRun("$stopped");
1028
+ return { done: true, result: { state, signal: null, cursor: dispatch.forkCursor, paused: true } };
918
1029
  }
919
1030
  if (dispatch.kind === "failed") {
920
1031
  state.$error = dispatch.error;
921
1032
  callbacks?.onError?.(state.$error, forkNode.name);
922
1033
  safeInvoke("onStepError", () => obs?.onStepError?.(stepCtx, { error: state.$error, durationMs: Date.now() - stepStart }), onObserverError);
923
1034
  if (graph.onError !== void 0) {
1035
+ fireSettled(obs, stepCtx, stepStart, { outcome: "error", error: state.$error, next: { kind: "step", name: graph.onError } }, onObserverError);
924
1036
  return { done: false, nextCursor: graph.onError };
925
1037
  }
926
- safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal: "$error", durationMs: Date.now() - runStart }), onObserverError);
1038
+ fireSettled(obs, stepCtx, stepStart, { outcome: "error", error: state.$error, next: { kind: "pause" } }, onObserverError);
1039
+ endRun("$error");
927
1040
  return { done: true, result: { state, signal: "$error", cursor: forkNode.name, paused: true } };
928
1041
  }
929
1042
  state.$error = null;
@@ -931,8 +1044,11 @@ async function handleFork(forkNode, priorBranches, state, ctx, schema, shouldSto
931
1044
  callbacks?.onAfterStep?.(forkNode.name, state);
932
1045
  const next = forkNode.next ?? implicitNextMap.get(forkNode.name) ?? null;
933
1046
  if (next === null) {
934
- throw new NoNextStepError(forkNode.name);
1047
+ const noNext = new NoNextStepError(forkNode.name);
1048
+ fireSettled(obs, stepCtx, stepStart, { outcome: "error", error: noNext, next: { kind: "throw" } }, onObserverError);
1049
+ throw noNext;
935
1050
  }
1051
+ fireSettled(obs, stepCtx, stepStart, { outcome: "ok", next: { kind: "step", name: next }, fork: { branches: dispatch.branches } }, onObserverError);
936
1052
  return { done: false, nextCursor: next };
937
1053
  }
938
1054