@noetaris/harness 0.1.0 → 0.2.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
@@ -372,9 +372,6 @@ function createBuilderInstance(internals) {
372
372
  return builder;
373
373
  }
374
374
 
375
- // src/agent/session-lifecycle.ts
376
- import { randomUUID } from "crypto";
377
-
378
375
  // src/agent/ctx-interrupt.ts
379
376
  var InterruptPause = class extends Error {
380
377
  interruptId;
@@ -406,9 +403,12 @@ function createInterruptFn(state, callCountRef) {
406
403
  }
407
404
 
408
405
  // src/agent/ctx-emit.ts
409
- function createEmitFn(listeners) {
406
+ function createEmitFn(listeners, observer, stepCtxRef) {
410
407
  return (name, payload) => {
411
408
  listeners[name]?.(payload);
409
+ if (observer !== void 0 && stepCtxRef !== void 0 && stepCtxRef.current !== null) {
410
+ observer.onEvent?.(stepCtxRef.current, name, payload);
411
+ }
412
412
  };
413
413
  }
414
414
  function extractRunListeners(resources) {
@@ -462,21 +462,37 @@ function applyUpdate(state, update, schema) {
462
462
  }
463
463
  async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callbacks) {
464
464
  const { onBeforeStep, onAfterStep, onError, onComplete, onInterrupt } = callbacks ?? {};
465
+ const obs = callbacks?.observer;
465
466
  const implicitNextMap = buildImplicitNextMap(graph);
466
467
  if (!("$error" in state)) state.$error = null;
467
468
  if (!("$interrupt" in state)) state.$interrupt = null;
468
469
  if (!("$interruptResponses" in state)) state.$interruptResponses = {};
470
+ const runStart = Date.now();
471
+ const runCtx = {
472
+ agentId: ctx.agentId,
473
+ sessionId: ctx.sessionId,
474
+ runId: typeof callbacks?.runId === "string" ? callbacks.runId : "",
475
+ ...typeof callbacks?.parentRunId === "string" ? { parentRunId: callbacks.parentRunId } : {},
476
+ ...typeof ctx.instanceId === "string" ? { instanceId: ctx.instanceId } : {}
477
+ };
478
+ obs?.onRunStart?.(runCtx);
469
479
  const callCountRef = { current: 0 };
480
+ const stepCtxRef = { current: null };
470
481
  ctx["interrupt"] = createInterruptFn(state, callCountRef);
471
- ctx["emit"] = createEmitFn(callbacks?.listeners ?? {});
482
+ ctx["emit"] = createEmitFn(callbacks?.listeners ?? {}, obs, stepCtxRef);
472
483
  let cursor = startCursor ?? graph.entryStep;
473
484
  while (true) {
474
485
  if (shouldStop?.()) {
486
+ obs?.onRunEnd?.(runCtx, { signal: "$stopped", durationMs: Date.now() - runStart });
475
487
  return { state, signal: null, cursor, paused: true };
476
488
  }
477
489
  onBeforeStep?.(cursor, { ...state });
478
490
  const step = graph.steps.find((s) => s.name === cursor);
479
491
  callCountRef.current = 0;
492
+ const stepCtx = { agentId: ctx.agentId, sessionId: ctx.sessionId, stepName: cursor };
493
+ stepCtxRef.current = stepCtx;
494
+ const stepStart = Date.now();
495
+ obs?.onStepStart?.(stepCtx);
480
496
  let runSucceeded = false;
481
497
  if (step.run !== void 0) {
482
498
  const hadResponses = Object.keys(state.$interruptResponses).length > 0;
@@ -494,15 +510,19 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
494
510
  } catch (e) {
495
511
  if (isInterruptPause(e)) {
496
512
  onInterrupt?.(e.prompt, e.interruptId);
513
+ obs?.onInterrupt?.(stepCtx, { prompt: e.prompt, interruptId: e.interruptId });
514
+ obs?.onRunEnd?.(runCtx, { signal: "$interrupt", durationMs: Date.now() - runStart });
497
515
  return { state, signal: "$interrupt", cursor, paused: true };
498
516
  }
499
517
  state.$error = e instanceof Error ? e : new Error(String(e));
500
518
  onError?.(state.$error, cursor);
519
+ obs?.onStepError?.(stepCtx, { error: state.$error, durationMs: Date.now() - stepStart });
501
520
  }
502
521
  }
503
522
  if (runSucceeded) {
504
523
  state.$error = null;
505
524
  onAfterStep?.(cursor, state);
525
+ obs?.onStepEnd?.(stepCtx, { durationMs: Date.now() - stepStart });
506
526
  }
507
527
  const callRoute = step.route !== void 0 && (runSucceeded || step.run === void 0 || step.errorAware);
508
528
  if (callRoute) {
@@ -512,6 +532,7 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
512
532
  signal = routeFn(state);
513
533
  } catch (e) {
514
534
  state.$error = e instanceof Error ? e : new Error(String(e));
535
+ obs?.onRunEnd?.(runCtx, { signal: "$error", durationMs: Date.now() - runStart });
515
536
  return { state, signal: "$error", cursor, paused: true };
516
537
  }
517
538
  const transition = step.transitions.find((t) => t.signal === signal);
@@ -520,6 +541,7 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
520
541
  }
521
542
  if (transition.target.kind === "end") {
522
543
  onComplete?.(state, signal);
544
+ obs?.onRunEnd?.(runCtx, { signal, durationMs: Date.now() - runStart });
523
545
  return { state, signal, cursor: null, paused: false };
524
546
  }
525
547
  cursor = transition.target.name;
@@ -527,6 +549,7 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
527
549
  if (graph.onError !== void 0) {
528
550
  cursor = graph.onError;
529
551
  } else {
552
+ obs?.onRunEnd?.(runCtx, { signal: "$error", durationMs: Date.now() - runStart });
530
553
  return { state, signal: "$error", cursor, paused: true };
531
554
  }
532
555
  } else {
@@ -604,6 +627,7 @@ function buildResumedState(stored, initialStateArg, argKeys, schema) {
604
627
 
605
628
  // src/agent/concurrency-errors.ts
606
629
  var SessionInFlightError = class extends Error {
630
+ /** The session that was already in-flight. */
607
631
  sessionId;
608
632
  constructor(sessionId) {
609
633
  super(`session "${sessionId}" is already in-flight`);
@@ -612,6 +636,7 @@ var SessionInFlightError = class extends Error {
612
636
  }
613
637
  };
614
638
  var SessionPendingInterruptError = class extends Error {
639
+ /** The session that is awaiting an interrupt response. */
615
640
  sessionId;
616
641
  constructor(sessionId) {
617
642
  super(`session "${sessionId}" is paused on an interrupt \u2014 use agent.resume() instead of agent.run()`);
@@ -627,6 +652,67 @@ var StoreLoadError = class extends Error {
627
652
  this.cause = cause;
628
653
  }
629
654
  };
655
+ var SessionBusyError = class extends Error {
656
+ /** The session that is currently claimed by another instance. */
657
+ sessionId;
658
+ constructor(sessionId, retryAfter) {
659
+ super(`session "${sessionId}" is currently claimed by another instance`);
660
+ this.name = "SessionBusyError";
661
+ this.sessionId = sessionId;
662
+ if (retryAfter !== void 0) this.retryAfter = retryAfter;
663
+ }
664
+ };
665
+ var LeaseExpiredError = class extends Error {
666
+ /** The session whose lease expired. */
667
+ sessionId;
668
+ constructor(sessionId) {
669
+ super(`lease for session "${sessionId}" has expired \u2014 run stopped without saving`);
670
+ this.name = "LeaseExpiredError";
671
+ this.sessionId = sessionId;
672
+ }
673
+ };
674
+
675
+ // src/agent/ctx-keep-alive.ts
676
+ function createKeepAliveFn(leaseRef, store, originalTtlMs) {
677
+ function keepAlive(options) {
678
+ const isBackground = typeof options === "object" && options !== null && "every" in options;
679
+ const extendClaim = store?.extendClaim;
680
+ if (isBackground) {
681
+ if (leaseRef.current === null || typeof extendClaim !== "function") {
682
+ return noopStop;
683
+ }
684
+ let handle = setInterval(() => {
685
+ if (leaseRef.current === null) {
686
+ return;
687
+ }
688
+ void (async () => {
689
+ try {
690
+ const renewed = await extendClaim.call(store, leaseRef.current, { ttlMs: originalTtlMs });
691
+ leaseRef.current = renewed;
692
+ } catch {
693
+ }
694
+ })();
695
+ }, options.every);
696
+ return function stop() {
697
+ if (handle !== null) {
698
+ clearInterval(handle);
699
+ handle = null;
700
+ }
701
+ };
702
+ }
703
+ if (leaseRef.current === null || typeof extendClaim !== "function") {
704
+ return Promise.resolve();
705
+ }
706
+ const snapshot = leaseRef.current;
707
+ return (async () => {
708
+ const renewed = await extendClaim.call(store, snapshot, { ttlMs: originalTtlMs });
709
+ leaseRef.current = renewed;
710
+ })();
711
+ }
712
+ return keepAlive;
713
+ }
714
+ function noopStop() {
715
+ }
630
716
 
631
717
  // src/agent/session-lifecycle.ts
632
718
  function resolveSessionStore(storeEntries) {
@@ -646,21 +732,43 @@ function resolveSessionStore(storeEntries) {
646
732
  }
647
733
  return result;
648
734
  }
649
- async function querySessionPhase(store, sessionId) {
735
+ async function querySessionPhase(store, agentId, sessionId) {
650
736
  if (store === void 0) {
651
737
  return { phase: "fresh" };
652
738
  }
653
- const loaded = await store.load(sessionId);
739
+ const loaded = await store.load(agentId, sessionId);
654
740
  return storedSessionToPhase(loaded);
655
741
  }
656
- async function runWithSession(store, sessionId, graph, initialStateArg, schema, ctx, options) {
742
+ async function runWithSession(store, agentId, sessionId, runId, graph, initialStateArg, schema, ctx, options) {
743
+ const leaseRef = options?.leaseRef ?? { current: null };
744
+ const keepAlive = createKeepAliveFn(leaseRef, store, options?.claimTtlMs ?? 3e4);
745
+ ctx["keepAlive"] = keepAlive;
746
+ let leaseExpired = false;
747
+ const composedShouldStop = () => {
748
+ if (options?.shouldStop?.() === true) return true;
749
+ if (leaseRef.current !== null && leaseRef.current.expiresAt <= Date.now()) {
750
+ leaseExpired = true;
751
+ return true;
752
+ }
753
+ return false;
754
+ };
657
755
  if (store === void 0) {
658
756
  const state2 = initializeState(null, initialStateArg, schema);
659
- return runLoop(graph, state2, ctx, schema, options?.shouldStop, void 0, options);
757
+ const result2 = await runLoop(graph, state2, ctx, schema, composedShouldStop, void 0, {
758
+ ...options,
759
+ runId,
760
+ ...options?.parentRunId !== void 0 ? { parentRunId: options.parentRunId } : {}
761
+ });
762
+ if (leaseExpired) {
763
+ const expiredError = new LeaseExpiredError(sessionId);
764
+ options?.onStoreError?.(expiredError, "claim");
765
+ throw expiredError;
766
+ }
767
+ return result2;
660
768
  }
661
769
  let loaded;
662
770
  try {
663
- loaded = await store.load(sessionId);
771
+ loaded = await store.load(agentId, sessionId);
664
772
  } catch (error) {
665
773
  const storeError = new StoreLoadError(error);
666
774
  options?.onStoreError?.(storeError, "load");
@@ -668,38 +776,62 @@ async function runWithSession(store, sessionId, graph, initialStateArg, schema,
668
776
  failState["$error"] = storeError;
669
777
  return { state: failState, signal: "$error", paused: false, cursor: null };
670
778
  }
671
- const runId = randomUUID();
779
+ if (loaded !== null && loaded.phase === "completed") {
780
+ return {
781
+ state: loaded.finalState,
782
+ signal: loaded.signal ?? null,
783
+ cursor: null,
784
+ paused: false
785
+ };
786
+ }
672
787
  const state = initializeState(loaded, initialStateArg, schema);
673
788
  const initialStateSnapshot = { ...state };
674
789
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
675
- const result = await runLoop(graph, state, ctx, schema, options?.shouldStop, loaded?.step, options);
790
+ const result = await runLoop(graph, state, ctx, schema, composedShouldStop, loaded?.step, {
791
+ ...options,
792
+ runId,
793
+ ...options?.parentRunId !== void 0 ? { parentRunId: options.parentRunId } : {}
794
+ });
795
+ if (leaseExpired) {
796
+ const expiredError = new LeaseExpiredError(sessionId);
797
+ options?.onStoreError?.(expiredError, "claim");
798
+ throw expiredError;
799
+ }
676
800
  const settledAt = (/* @__PURE__ */ new Date()).toISOString();
801
+ const metadata = options?.instanceId !== void 0 ? { instanceId: options.instanceId } : void 0;
677
802
  try {
803
+ const version = (loaded?.version ?? -1) + 1;
678
804
  if (result.paused) {
679
805
  const saved = {
806
+ agentId,
680
807
  runId,
681
808
  sessionId,
809
+ version,
682
810
  startedAt,
683
811
  settledAt,
684
812
  phase: "paused",
685
813
  initialState: initialStateSnapshot,
686
814
  finalState: result.state,
687
815
  step: result.cursor,
688
- ...result.signal !== null ? { signal: result.signal } : {}
816
+ ...result.signal !== null ? { signal: result.signal } : {},
817
+ ...metadata !== void 0 ? { metadata } : {}
689
818
  };
690
- await store.save(sessionId, saved);
819
+ await store.save(agentId, sessionId, saved);
691
820
  } else {
692
821
  const saved = {
822
+ agentId,
693
823
  runId,
694
824
  sessionId,
825
+ version,
695
826
  startedAt,
696
827
  settledAt,
697
828
  phase: "completed",
698
829
  initialState: initialStateSnapshot,
699
830
  finalState: result.state,
700
- ...result.signal !== null ? { signal: result.signal } : {}
831
+ ...result.signal !== null ? { signal: result.signal } : {},
832
+ ...metadata !== void 0 ? { metadata } : {}
701
833
  };
702
- await store.save(sessionId, saved);
834
+ await store.save(agentId, sessionId, saved);
703
835
  }
704
836
  } catch (error) {
705
837
  options?.onStoreError?.(error, "persist");
@@ -716,8 +848,8 @@ var NoInterruptError = class extends Error {
716
848
  this.name = "NoInterruptError";
717
849
  }
718
850
  };
719
- async function injectInterruptResponse(store, sessionId, interruptId, response) {
720
- const loaded = await store.load(sessionId);
851
+ async function injectInterruptResponse(store, agentId, sessionId, interruptId, response) {
852
+ const loaded = await store.load(agentId, sessionId);
721
853
  if (loaded === null) throw new NoInterruptError();
722
854
  if (loaded.phase !== "paused") throw new NoInterruptError();
723
855
  const interrupt = loaded.finalState.$interrupt;
@@ -730,8 +862,10 @@ async function injectInterruptResponse(store, sessionId, interruptId, response)
730
862
  $interruptResponses: { ...existingResponses, [interruptId]: response }
731
863
  };
732
864
  const updated = {
865
+ agentId: loaded.agentId,
733
866
  runId: loaded.runId,
734
867
  sessionId: loaded.sessionId,
868
+ version: loaded.version + 1,
735
869
  startedAt: loaded.startedAt,
736
870
  settledAt: loaded.settledAt,
737
871
  phase: "paused",
@@ -740,11 +874,11 @@ async function injectInterruptResponse(store, sessionId, interruptId, response)
740
874
  ...loaded.step !== void 0 ? { step: loaded.step } : {},
741
875
  ...loaded.signal !== void 0 ? { signal: loaded.signal } : {}
742
876
  };
743
- await store.save(sessionId, updated);
877
+ await store.save(agentId, sessionId, updated);
744
878
  }
745
879
 
746
880
  // src/agent/run-handle.ts
747
- function createRunHandle(sessionId, execution, stopFlag, stepRef, resumeFn) {
881
+ function createRunHandle(sessionId, runId, execution, stopFlag, stepRef, resumeFn) {
748
882
  let settledOutcome = null;
749
883
  const settled = execution.then(
750
884
  (outcome) => {
@@ -782,6 +916,9 @@ function createRunHandle(sessionId, execution, stopFlag, stepRef, resumeFn) {
782
916
  get sessionId() {
783
917
  return sessionId;
784
918
  },
919
+ get runId() {
920
+ return runId;
921
+ },
785
922
  get currentStep() {
786
923
  return stepRef.current;
787
924
  }
@@ -790,7 +927,7 @@ function createRunHandle(sessionId, execution, stopFlag, stepRef, resumeFn) {
790
927
  }
791
928
 
792
929
  // src/agent/create-agent.ts
793
- import { randomUUID as randomUUID2 } from "crypto";
930
+ import { randomUUID } from "crypto";
794
931
 
795
932
  // src/agent/event-callbacks.ts
796
933
  function extractRunEvents(resources) {
@@ -802,13 +939,20 @@ function extractRunEvents(resources) {
802
939
  }
803
940
 
804
941
  // src/agent/create-agent.ts
942
+ var NOOP_OBSERVER = {};
943
+ function extractRunObserver(resources) {
944
+ const raw = resources["observer"];
945
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return void 0;
946
+ return raw;
947
+ }
805
948
  var MissingLoopError = class extends Error {
806
949
  constructor() {
807
- super("harness has no loop \u2014 call h.loop() before createAgent()");
950
+ super("harness has no loop \u2014 call h.loop() before createAgent(id, h, slots)");
808
951
  this.name = "MissingLoopError";
809
952
  }
810
953
  };
811
954
  var MissingSlotError = class extends Error {
955
+ /** The missing slot key. */
812
956
  key;
813
957
  constructor(key) {
814
958
  super(`required slot "${key}" was not provided in createAgent slots`);
@@ -817,6 +961,7 @@ var MissingSlotError = class extends Error {
817
961
  }
818
962
  };
819
963
  var RuntimeSlotInAgentError = class extends Error {
964
+ /** The offending slot key. */
820
965
  key;
821
966
  constructor(key) {
822
967
  super(`slot "${key}" is a runtime() slot \u2014 provide it in agent.run(), not createAgent()`);
@@ -825,6 +970,7 @@ var RuntimeSlotInAgentError = class extends Error {
825
970
  }
826
971
  };
827
972
  var UnknownSlotError = class extends Error {
973
+ /** The unknown slot key. */
828
974
  key;
829
975
  constructor(key) {
830
976
  super(`slot "${key}" is not declared in the harness \u2014 remove it or add h.provide("${key}", required())`);
@@ -833,6 +979,7 @@ var UnknownSlotError = class extends Error {
833
979
  }
834
980
  };
835
981
  var MissingRuntimeSlotError = class extends Error {
982
+ /** The missing runtime slot key. */
836
983
  key;
837
984
  constructor(key) {
838
985
  super(`runtime slot "${key}" was not provided in agent.run() resources`);
@@ -841,6 +988,7 @@ var MissingRuntimeSlotError = class extends Error {
841
988
  }
842
989
  };
843
990
  var RequiredSlotInRunError = class extends Error {
991
+ /** The offending slot key. */
844
992
  key;
845
993
  constructor(key) {
846
994
  super(`slot "${key}" is a required() slot \u2014 provide it in createAgent(), not agent.run()`);
@@ -849,6 +997,7 @@ var RequiredSlotInRunError = class extends Error {
849
997
  }
850
998
  };
851
999
  var UnknownRunSlotError = class extends Error {
1000
+ /** The unknown resource key. */
852
1001
  key;
853
1002
  constructor(key) {
854
1003
  super(`slot "${key}" is not a runtime() slot \u2014 do not pass it in agent.run() resources`);
@@ -857,7 +1006,7 @@ var UnknownRunSlotError = class extends Error {
857
1006
  }
858
1007
  };
859
1008
  var _agentInternals = /* @__PURE__ */ Symbol("_agentInternals");
860
- function createAgent(h, slots) {
1009
+ function createAgent(id, h, slots, agentOptions) {
861
1010
  const internals = getInternals(h);
862
1011
  if (internals.loopDef === void 0) {
863
1012
  throw new MissingLoopError();
@@ -906,6 +1055,7 @@ function createAgent(h, slots) {
906
1055
  }
907
1056
  const capturedStore = resolveSessionStore(storeEntries);
908
1057
  const agentInternals = {
1058
+ agentId: id,
909
1059
  resolvedProviders,
910
1060
  storeEntries,
911
1061
  loopDef: internals.loopDef,
@@ -914,38 +1064,93 @@ function createAgent(h, slots) {
914
1064
  runtimeKeys,
915
1065
  requiredKeys
916
1066
  };
917
- const reservedRunKeys = /* @__PURE__ */ new Set(["sessionId", "signal", "events", "listeners"]);
1067
+ const reservedRunKeys = /* @__PURE__ */ new Set(["sessionId", "signal", "events", "listeners", "observer", "claimOptions", "parentRunId"]);
918
1068
  const inFlightSessions = /* @__PURE__ */ new Set();
919
1069
  const interruptPendingSessions = /* @__PURE__ */ new Set();
920
- const makeAgentResumeHandle = (resp, sId, iId) => {
921
- const flag = { stopped: false };
1070
+ const makeAgentResumeHandle = (resp, sId, iId, resumeOpts) => {
1071
+ let _stopped = false;
1072
+ const abortController = new AbortController();
1073
+ const flag = {
1074
+ get stopped() {
1075
+ return _stopped;
1076
+ },
1077
+ set stopped(v) {
1078
+ _stopped = v;
1079
+ if (v) abortController.abort();
1080
+ }
1081
+ };
922
1082
  const ref = { current: null };
1083
+ const rId = randomUUID();
923
1084
  const exec = (async () => {
924
1085
  await Promise.resolve();
1086
+ const claimOptions = { ttlMs: 3e4 };
1087
+ let leaseRef = void 0;
925
1088
  try {
926
- if (capturedStore === void 0) throw new NoInterruptError();
927
- await injectInterruptResponse(capturedStore, sId, iId, resp);
928
- const agentCtx = {
929
- ...Object.fromEntries(agentInternals.resolvedProviders),
930
- sessionId: sId
931
- };
932
- const r = await runWithSession(
933
- capturedStore,
934
- sId,
935
- agentInternals.loopDef,
936
- {},
937
- agentInternals.stateSchema,
938
- agentCtx,
939
- {
940
- shouldStop: () => flag.stopped,
941
- onBeforeStep: (n) => {
942
- ref.current = n;
1089
+ if (typeof capturedStore?.claim === "function") {
1090
+ let lease;
1091
+ try {
1092
+ lease = await capturedStore.claim(id, sId, claimOptions);
1093
+ } catch (claimError) {
1094
+ const loadError = new StoreLoadError(claimError);
1095
+ resumeOpts?.onStoreError?.(loadError, "claim");
1096
+ const failState = initializeState(null, {}, agentInternals.stateSchema);
1097
+ failState["$error"] = loadError;
1098
+ return { state: failState, signal: "$error" };
1099
+ }
1100
+ if (lease === null) {
1101
+ const busyError = new SessionBusyError(sId);
1102
+ resumeOpts?.onStoreError?.(busyError, "claim");
1103
+ const failState = initializeState(null, {}, agentInternals.stateSchema);
1104
+ failState["$error"] = busyError;
1105
+ return { state: failState, signal: "$error" };
1106
+ }
1107
+ leaseRef = { current: lease };
1108
+ }
1109
+ try {
1110
+ if (capturedStore === void 0) throw new NoInterruptError();
1111
+ await injectInterruptResponse(capturedStore, id, sId, iId, resp);
1112
+ const agentCtx = {
1113
+ ...Object.fromEntries(agentInternals.resolvedProviders),
1114
+ agentId: id,
1115
+ sessionId: sId,
1116
+ runId: rId,
1117
+ signal: abortController.signal
1118
+ };
1119
+ const r = await runWithSession(
1120
+ capturedStore,
1121
+ id,
1122
+ sId,
1123
+ rId,
1124
+ agentInternals.loopDef,
1125
+ {},
1126
+ agentInternals.stateSchema,
1127
+ agentCtx,
1128
+ {
1129
+ shouldStop: () => flag.stopped,
1130
+ onBeforeStep: (n) => {
1131
+ ref.current = n;
1132
+ },
1133
+ ...resumeOpts?.onStoreError !== void 0 ? { onStoreError: resumeOpts.onStoreError } : {},
1134
+ ...leaseRef !== void 0 ? { leaseRef, claimTtlMs: claimOptions.ttlMs } : {}
943
1135
  }
944
- // _state unused in cross-process resume — no events available
1136
+ );
1137
+ if (r.signal === "$interrupt") interruptPendingSessions.add(sId);
1138
+ return { state: r.state, signal: r.signal };
1139
+ } catch (error) {
1140
+ if (error instanceof LeaseExpiredError) {
1141
+ const failState = initializeState(null, {}, agentInternals.stateSchema);
1142
+ failState["$error"] = error;
1143
+ return { state: failState, signal: "$error" };
945
1144
  }
946
- );
947
- if (r.signal === "$interrupt") interruptPendingSessions.add(sId);
948
- return { state: r.state, signal: r.signal };
1145
+ throw error;
1146
+ } finally {
1147
+ if (leaseRef !== void 0 && typeof capturedStore?.release === "function") {
1148
+ try {
1149
+ await capturedStore.release(leaseRef.current);
1150
+ } catch {
1151
+ }
1152
+ }
1153
+ }
949
1154
  } finally {
950
1155
  inFlightSessions.delete(sId);
951
1156
  }
@@ -956,9 +1161,10 @@ function createAgent(h, slots) {
956
1161
  inFlightSessions.add(sId);
957
1162
  return makeAgentResumeHandle(r, sId, i);
958
1163
  };
959
- return createRunHandle(sId, exec, flag, ref, resumeFn);
1164
+ return createRunHandle(sId, rId, exec, flag, ref, resumeFn);
960
1165
  };
961
1166
  const agent = {
1167
+ id,
962
1168
  run: (initialState, resources) => {
963
1169
  for (const key of Object.keys(resources)) {
964
1170
  if (reservedRunKeys.has(key)) continue;
@@ -977,15 +1183,29 @@ function createAgent(h, slots) {
977
1183
  throw new MissingRuntimeSlotError(key);
978
1184
  }
979
1185
  }
980
- const sessionId = typeof resources["sessionId"] === "string" ? resources["sessionId"] : randomUUID2();
1186
+ const sessionId = typeof resources["sessionId"] === "string" ? resources["sessionId"] : randomUUID();
1187
+ const runId = randomUUID();
981
1188
  if (inFlightSessions.has(sessionId)) throw new SessionInFlightError(sessionId);
982
1189
  if (interruptPendingSessions.has(sessionId)) throw new SessionPendingInterruptError(sessionId);
983
1190
  inFlightSessions.add(sessionId);
984
- const stopFlag = { stopped: false };
1191
+ let _stopped = false;
1192
+ const abortController = new AbortController();
1193
+ const stopFlag = {
1194
+ get stopped() {
1195
+ return _stopped;
1196
+ },
1197
+ set stopped(v) {
1198
+ _stopped = v;
1199
+ if (v) abortController.abort();
1200
+ }
1201
+ };
985
1202
  const rawSignal = resources["signal"];
1203
+ let ctxSignal = abortController.signal;
986
1204
  if (rawSignal instanceof AbortSignal) {
1205
+ ctxSignal = rawSignal;
987
1206
  rawSignal.addEventListener("abort", () => {
988
1207
  stopFlag.stopped = true;
1208
+ abortController.abort();
989
1209
  }, { once: true });
990
1210
  if (rawSignal.aborted) stopFlag.stopped = true;
991
1211
  }
@@ -998,10 +1218,21 @@ function createAgent(h, slots) {
998
1218
  const ctx = {
999
1219
  ...Object.fromEntries(agentInternals.resolvedProviders),
1000
1220
  ...runtimeSlots,
1001
- sessionId
1221
+ agentId: id,
1222
+ sessionId,
1223
+ runId,
1224
+ signal: ctxSignal,
1225
+ ...agentOptions?.instanceId !== void 0 ? { instanceId: agentOptions.instanceId } : {}
1002
1226
  };
1003
1227
  const events = extractRunEvents(resources);
1004
1228
  const listeners = extractRunListeners(resources);
1229
+ const observer = extractRunObserver(resources);
1230
+ for (const slot of [...agentInternals.resolvedProviders.values(), ...Object.values(runtimeSlots)]) {
1231
+ if (slot !== null && typeof slot === "object" && typeof slot["bindObserver"] === "function") {
1232
+ slot.bindObserver(observer ?? NOOP_OBSERVER);
1233
+ }
1234
+ }
1235
+ const parentRunId = typeof resources["parentRunId"] === "string" ? resources["parentRunId"] : void 0;
1005
1236
  const options = {
1006
1237
  shouldStop: () => stopFlag.stopped,
1007
1238
  onBeforeStep: (name, state) => {
@@ -1014,24 +1245,71 @@ function createAgent(h, slots) {
1014
1245
  ...events.onError !== void 0 ? { onError: events.onError } : {},
1015
1246
  ...events.onComplete !== void 0 ? { onComplete: events.onComplete } : {},
1016
1247
  ...events.onInterrupt !== void 0 ? { onInterrupt: events.onInterrupt } : {},
1017
- ...Object.keys(listeners).length > 0 ? { listeners } : {}
1248
+ ...Object.keys(listeners).length > 0 ? { listeners } : {},
1249
+ ...observer !== void 0 ? { observer } : {},
1250
+ ...agentOptions?.instanceId !== void 0 ? { instanceId: agentOptions.instanceId } : {},
1251
+ ...parentRunId !== void 0 ? { parentRunId } : {}
1018
1252
  };
1019
1253
  let lastResult = null;
1020
1254
  const execution = (async () => {
1021
1255
  await Promise.resolve();
1256
+ const rawClaimOpts = resources["claimOptions"];
1257
+ const claimOptions = typeof rawClaimOpts === "object" && rawClaimOpts !== null && typeof rawClaimOpts["ttlMs"] === "number" ? rawClaimOpts : { ttlMs: 3e4 };
1258
+ let leaseRef = void 0;
1022
1259
  try {
1023
- const r = await runWithSession(
1024
- capturedStore,
1025
- sessionId,
1026
- agentInternals.loopDef,
1027
- initialState,
1028
- agentInternals.stateSchema,
1029
- ctx,
1030
- options
1031
- );
1032
- if (r.signal === "$interrupt") interruptPendingSessions.add(sessionId);
1033
- lastResult = { state: r.state, cursor: r.cursor };
1034
- return { state: r.state, signal: r.signal };
1260
+ if (typeof capturedStore?.claim === "function") {
1261
+ let lease;
1262
+ try {
1263
+ lease = await capturedStore.claim(id, sessionId, claimOptions);
1264
+ } catch (claimError) {
1265
+ const loadError = new StoreLoadError(claimError);
1266
+ events.onStoreError?.(loadError, "claim");
1267
+ const failState = initializeState(null, initialState, agentInternals.stateSchema);
1268
+ failState["$error"] = loadError;
1269
+ return { state: failState, signal: "$error" };
1270
+ }
1271
+ if (lease === null) {
1272
+ const busyError = new SessionBusyError(sessionId);
1273
+ events.onStoreError?.(busyError, "claim");
1274
+ const failState = initializeState(null, initialState, agentInternals.stateSchema);
1275
+ failState["$error"] = busyError;
1276
+ return { state: failState, signal: "$error" };
1277
+ }
1278
+ leaseRef = { current: lease };
1279
+ }
1280
+ try {
1281
+ const r = await runWithSession(
1282
+ capturedStore,
1283
+ id,
1284
+ sessionId,
1285
+ runId,
1286
+ agentInternals.loopDef,
1287
+ initialState,
1288
+ agentInternals.stateSchema,
1289
+ ctx,
1290
+ {
1291
+ ...options,
1292
+ ...leaseRef !== void 0 ? { leaseRef, claimTtlMs: claimOptions.ttlMs } : {}
1293
+ }
1294
+ );
1295
+ if (r.signal === "$interrupt") interruptPendingSessions.add(sessionId);
1296
+ lastResult = { state: r.state, cursor: r.cursor };
1297
+ return { state: r.state, signal: r.signal };
1298
+ } catch (error) {
1299
+ if (error instanceof LeaseExpiredError) {
1300
+ const failState = initializeState(null, initialState, agentInternals.stateSchema);
1301
+ failState["$error"] = error;
1302
+ return { state: failState, signal: "$error" };
1303
+ }
1304
+ throw error;
1305
+ } finally {
1306
+ if (leaseRef !== void 0 && typeof capturedStore?.release === "function") {
1307
+ try {
1308
+ await capturedStore.release(leaseRef.current);
1309
+ } catch {
1310
+ }
1311
+ }
1312
+ }
1035
1313
  } finally {
1036
1314
  inFlightSessions.delete(sessionId);
1037
1315
  }
@@ -1041,20 +1319,38 @@ function createAgent(h, slots) {
1041
1319
  if (inFlightSessions.has(sessionId)) throw new SessionInFlightError(sessionId);
1042
1320
  interruptPendingSessions.delete(sessionId);
1043
1321
  inFlightSessions.add(sessionId);
1044
- const resumeStopFlag = { stopped: false };
1322
+ let _resumeStopped = false;
1323
+ const resumeAbortController = new AbortController();
1324
+ const resumeStopFlag = {
1325
+ get stopped() {
1326
+ return _resumeStopped;
1327
+ },
1328
+ set stopped(v) {
1329
+ _resumeStopped = v;
1330
+ if (v) resumeAbortController.abort();
1331
+ }
1332
+ };
1045
1333
  const resumeStepRef = { current: null };
1334
+ const resumeRunId = randomUUID();
1046
1335
  const resumeExecution = (async () => {
1047
1336
  await Promise.resolve();
1048
1337
  try {
1338
+ const resumeCtx = {
1339
+ ...ctx,
1340
+ runId: resumeRunId,
1341
+ signal: resumeAbortController.signal
1342
+ };
1049
1343
  if (capturedStore !== void 0) {
1050
- await injectInterruptResponse(capturedStore, sessionId, interruptId, response);
1344
+ await injectInterruptResponse(capturedStore, id, sessionId, interruptId, response);
1051
1345
  const r2 = await runWithSession(
1052
1346
  capturedStore,
1347
+ id,
1053
1348
  sessionId,
1349
+ resumeRunId,
1054
1350
  agentInternals.loopDef,
1055
1351
  {},
1056
1352
  agentInternals.stateSchema,
1057
- ctx,
1353
+ resumeCtx,
1058
1354
  {
1059
1355
  shouldStop: () => resumeStopFlag.stopped,
1060
1356
  onBeforeStep: (n, s) => {
@@ -1084,7 +1380,7 @@ function createAgent(h, slots) {
1084
1380
  const r = await runLoop(
1085
1381
  agentInternals.loopDef,
1086
1382
  state,
1087
- ctx,
1383
+ resumeCtx,
1088
1384
  agentInternals.stateSchema,
1089
1385
  () => resumeStopFlag.stopped,
1090
1386
  cursor,
@@ -1103,26 +1399,61 @@ function createAgent(h, slots) {
1103
1399
  if (r.signal === "$interrupt") interruptPendingSessions.add(sessionId);
1104
1400
  lastResult = { state: r.state, cursor: r.cursor };
1105
1401
  return { state: r.state, signal: r.signal };
1402
+ } catch (error) {
1403
+ if (error instanceof LeaseExpiredError) {
1404
+ const failState = initializeState(null, initialState, agentInternals.stateSchema);
1405
+ failState["$error"] = error;
1406
+ return { state: failState, signal: "$error" };
1407
+ }
1408
+ throw error;
1106
1409
  } finally {
1107
1410
  inFlightSessions.delete(sessionId);
1108
1411
  }
1109
1412
  })();
1110
- return createRunHandle(sessionId, resumeExecution, resumeStopFlag, resumeStepRef, buildResumeFn());
1413
+ return createRunHandle(sessionId, resumeRunId, resumeExecution, resumeStopFlag, resumeStepRef, buildResumeFn());
1111
1414
  };
1112
1415
  };
1113
- return createRunHandle(sessionId, execution, stopFlag, stepRef, buildResumeFn());
1416
+ return createRunHandle(sessionId, runId, execution, stopFlag, stepRef, buildResumeFn());
1114
1417
  },
1115
- resume: (response, sessionId, interruptId) => {
1418
+ resume: (response, sessionId, interruptId, resumeOptions) => {
1116
1419
  if (inFlightSessions.has(sessionId)) throw new SessionInFlightError(sessionId);
1117
1420
  interruptPendingSessions.delete(sessionId);
1118
1421
  inFlightSessions.add(sessionId);
1119
- return makeAgentResumeHandle(response, sessionId, interruptId);
1422
+ const resumeOpts = resumeOptions?.events?.onStoreError !== void 0 ? { onStoreError: resumeOptions.events.onStoreError } : void 0;
1423
+ return makeAgentResumeHandle(response, sessionId, interruptId, resumeOpts);
1120
1424
  },
1121
- status: (sessionId) => querySessionPhase(capturedStore, sessionId),
1425
+ status: (sessionId) => querySessionPhase(capturedStore, id, sessionId),
1122
1426
  [_agentInternals]: agentInternals
1123
1427
  };
1124
1428
  return agent;
1125
1429
  }
1430
+
1431
+ // src/agent/observer.ts
1432
+ function composeObservers(...observers) {
1433
+ return {
1434
+ onRunStart: (ctx) => {
1435
+ for (const o of observers) o.onRunStart?.(ctx);
1436
+ },
1437
+ onRunEnd: (ctx, event) => {
1438
+ for (const o of observers) o.onRunEnd?.(ctx, event);
1439
+ },
1440
+ onStepStart: (ctx) => {
1441
+ for (const o of observers) o.onStepStart?.(ctx);
1442
+ },
1443
+ onStepEnd: (ctx, event) => {
1444
+ for (const o of observers) o.onStepEnd?.(ctx, event);
1445
+ },
1446
+ onStepError: (ctx, event) => {
1447
+ for (const o of observers) o.onStepError?.(ctx, event);
1448
+ },
1449
+ onInterrupt: (ctx, event) => {
1450
+ for (const o of observers) o.onInterrupt?.(ctx, event);
1451
+ },
1452
+ onEvent: (ctx, type, payload) => {
1453
+ for (const o of observers) o.onEvent?.(ctx, type, payload);
1454
+ }
1455
+ };
1456
+ }
1126
1457
  export {
1127
1458
  NoInterruptError,
1128
1459
  REQUIRED_TAG,
@@ -1130,6 +1461,7 @@ export {
1130
1461
  SessionInFlightError,
1131
1462
  SessionPendingInterruptError,
1132
1463
  StoreLoadError,
1464
+ composeObservers,
1133
1465
  createAgent,
1134
1466
  createHarness,
1135
1467
  field,