@noetaris/harness 0.1.0 → 0.3.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
@@ -304,6 +304,12 @@ var HarnessInternalsError = class extends Error {
304
304
  this.name = "HarnessInternalsError";
305
305
  }
306
306
  };
307
+ var LoopNotDefinedError = class extends Error {
308
+ constructor() {
309
+ super("loop is not defined \u2014 call h.loop() before accessing h.definition()");
310
+ this.name = "LoopNotDefinedError";
311
+ }
312
+ };
307
313
  function createHarness() {
308
314
  return (stateSchema) => {
309
315
  const internals = {
@@ -367,14 +373,17 @@ function createBuilderInstance(internals) {
367
373
  };
368
374
  return createBuilderInstance(newInternals);
369
375
  },
376
+ definition() {
377
+ if (internals.loopDef === void 0) {
378
+ throw new LoopNotDefinedError();
379
+ }
380
+ return internals.loopDef;
381
+ },
370
382
  [_internals]: internals
371
383
  };
372
384
  return builder;
373
385
  }
374
386
 
375
- // src/agent/session-lifecycle.ts
376
- import { randomUUID } from "crypto";
377
-
378
387
  // src/agent/ctx-interrupt.ts
379
388
  var InterruptPause = class extends Error {
380
389
  interruptId;
@@ -406,9 +415,12 @@ function createInterruptFn(state, callCountRef) {
406
415
  }
407
416
 
408
417
  // src/agent/ctx-emit.ts
409
- function createEmitFn(listeners) {
418
+ function createEmitFn(listeners, observer, stepCtxRef) {
410
419
  return (name, payload) => {
411
420
  listeners[name]?.(payload);
421
+ if (observer !== void 0 && stepCtxRef !== void 0 && stepCtxRef.current !== null) {
422
+ observer.onEvent?.(stepCtxRef.current, name, payload);
423
+ }
412
424
  };
413
425
  }
414
426
  function extractRunListeners(resources) {
@@ -462,21 +474,42 @@ function applyUpdate(state, update, schema) {
462
474
  }
463
475
  async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callbacks) {
464
476
  const { onBeforeStep, onAfterStep, onError, onComplete, onInterrupt } = callbacks ?? {};
477
+ const obs = callbacks?.observer;
465
478
  const implicitNextMap = buildImplicitNextMap(graph);
466
479
  if (!("$error" in state)) state.$error = null;
467
480
  if (!("$interrupt" in state)) state.$interrupt = null;
468
481
  if (!("$interruptResponses" in state)) state.$interruptResponses = {};
482
+ const runStart = Date.now();
483
+ const runCtx = {
484
+ agentId: ctx.agentId,
485
+ sessionId: ctx.sessionId,
486
+ runId: typeof callbacks?.runId === "string" ? callbacks.runId : "",
487
+ ...typeof callbacks?.parentRunId === "string" ? { parentRunId: callbacks.parentRunId } : {},
488
+ ...typeof ctx.instanceId === "string" ? { instanceId: ctx.instanceId } : {}
489
+ };
490
+ obs?.onRunStart?.(runCtx);
469
491
  const callCountRef = { current: 0 };
492
+ const stepCtxRef = { current: null };
470
493
  ctx["interrupt"] = createInterruptFn(state, callCountRef);
471
- ctx["emit"] = createEmitFn(callbacks?.listeners ?? {});
494
+ ctx["emit"] = createEmitFn(callbacks?.listeners ?? {}, obs, stepCtxRef);
472
495
  let cursor = startCursor ?? graph.entryStep;
473
496
  while (true) {
474
497
  if (shouldStop?.()) {
498
+ obs?.onRunEnd?.(runCtx, { signal: "$stopped", durationMs: Date.now() - runStart });
475
499
  return { state, signal: null, cursor, paused: true };
476
500
  }
477
501
  onBeforeStep?.(cursor, { ...state });
478
502
  const step = graph.steps.find((s) => s.name === cursor);
479
503
  callCountRef.current = 0;
504
+ const stepCtx = { agentId: ctx.agentId, sessionId: ctx.sessionId, stepName: cursor };
505
+ stepCtxRef.current = stepCtx;
506
+ const stepStart = Date.now();
507
+ obs?.onStepStart?.(stepCtx);
508
+ if (callbacks?.setStepContextSlots !== void 0) {
509
+ for (const slot of callbacks.setStepContextSlots) {
510
+ slot.setStepContext(stepCtx);
511
+ }
512
+ }
480
513
  let runSucceeded = false;
481
514
  if (step.run !== void 0) {
482
515
  const hadResponses = Object.keys(state.$interruptResponses).length > 0;
@@ -494,15 +527,19 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
494
527
  } catch (e) {
495
528
  if (isInterruptPause(e)) {
496
529
  onInterrupt?.(e.prompt, e.interruptId);
530
+ obs?.onInterrupt?.(stepCtx, { prompt: e.prompt, interruptId: e.interruptId });
531
+ obs?.onRunEnd?.(runCtx, { signal: "$interrupt", durationMs: Date.now() - runStart });
497
532
  return { state, signal: "$interrupt", cursor, paused: true };
498
533
  }
499
534
  state.$error = e instanceof Error ? e : new Error(String(e));
500
535
  onError?.(state.$error, cursor);
536
+ obs?.onStepError?.(stepCtx, { error: state.$error, durationMs: Date.now() - stepStart });
501
537
  }
502
538
  }
503
539
  if (runSucceeded) {
504
540
  state.$error = null;
505
541
  onAfterStep?.(cursor, state);
542
+ obs?.onStepEnd?.(stepCtx, { durationMs: Date.now() - stepStart });
506
543
  }
507
544
  const callRoute = step.route !== void 0 && (runSucceeded || step.run === void 0 || step.errorAware);
508
545
  if (callRoute) {
@@ -512,6 +549,7 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
512
549
  signal = routeFn(state);
513
550
  } catch (e) {
514
551
  state.$error = e instanceof Error ? e : new Error(String(e));
552
+ obs?.onRunEnd?.(runCtx, { signal: "$error", durationMs: Date.now() - runStart });
515
553
  return { state, signal: "$error", cursor, paused: true };
516
554
  }
517
555
  const transition = step.transitions.find((t) => t.signal === signal);
@@ -520,6 +558,7 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
520
558
  }
521
559
  if (transition.target.kind === "end") {
522
560
  onComplete?.(state, signal);
561
+ obs?.onRunEnd?.(runCtx, { signal, durationMs: Date.now() - runStart });
523
562
  return { state, signal, cursor: null, paused: false };
524
563
  }
525
564
  cursor = transition.target.name;
@@ -527,6 +566,7 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
527
566
  if (graph.onError !== void 0) {
528
567
  cursor = graph.onError;
529
568
  } else {
569
+ obs?.onRunEnd?.(runCtx, { signal: "$error", durationMs: Date.now() - runStart });
530
570
  return { state, signal: "$error", cursor, paused: true };
531
571
  }
532
572
  } else {
@@ -604,6 +644,7 @@ function buildResumedState(stored, initialStateArg, argKeys, schema) {
604
644
 
605
645
  // src/agent/concurrency-errors.ts
606
646
  var SessionInFlightError = class extends Error {
647
+ /** The session that was already in-flight. */
607
648
  sessionId;
608
649
  constructor(sessionId) {
609
650
  super(`session "${sessionId}" is already in-flight`);
@@ -612,6 +653,7 @@ var SessionInFlightError = class extends Error {
612
653
  }
613
654
  };
614
655
  var SessionPendingInterruptError = class extends Error {
656
+ /** The session that is awaiting an interrupt response. */
615
657
  sessionId;
616
658
  constructor(sessionId) {
617
659
  super(`session "${sessionId}" is paused on an interrupt \u2014 use agent.resume() instead of agent.run()`);
@@ -627,6 +669,67 @@ var StoreLoadError = class extends Error {
627
669
  this.cause = cause;
628
670
  }
629
671
  };
672
+ var SessionBusyError = class extends Error {
673
+ /** The session that is currently claimed by another instance. */
674
+ sessionId;
675
+ constructor(sessionId, retryAfter) {
676
+ super(`session "${sessionId}" is currently claimed by another instance`);
677
+ this.name = "SessionBusyError";
678
+ this.sessionId = sessionId;
679
+ if (retryAfter !== void 0) this.retryAfter = retryAfter;
680
+ }
681
+ };
682
+ var LeaseExpiredError = class extends Error {
683
+ /** The session whose lease expired. */
684
+ sessionId;
685
+ constructor(sessionId) {
686
+ super(`lease for session "${sessionId}" has expired \u2014 run stopped without saving`);
687
+ this.name = "LeaseExpiredError";
688
+ this.sessionId = sessionId;
689
+ }
690
+ };
691
+
692
+ // src/agent/ctx-keep-alive.ts
693
+ function createKeepAliveFn(leaseRef, store, originalTtlMs) {
694
+ function keepAlive(options) {
695
+ const isBackground = typeof options === "object" && options !== null && "every" in options;
696
+ const extendClaim = store?.extendClaim;
697
+ if (isBackground) {
698
+ if (leaseRef.current === null || typeof extendClaim !== "function") {
699
+ return noopStop;
700
+ }
701
+ let handle = setInterval(() => {
702
+ if (leaseRef.current === null) {
703
+ return;
704
+ }
705
+ void (async () => {
706
+ try {
707
+ const renewed = await extendClaim.call(store, leaseRef.current, { ttlMs: originalTtlMs });
708
+ leaseRef.current = renewed;
709
+ } catch {
710
+ }
711
+ })();
712
+ }, options.every);
713
+ return function stop() {
714
+ if (handle !== null) {
715
+ clearInterval(handle);
716
+ handle = null;
717
+ }
718
+ };
719
+ }
720
+ if (leaseRef.current === null || typeof extendClaim !== "function") {
721
+ return Promise.resolve();
722
+ }
723
+ const snapshot = leaseRef.current;
724
+ return (async () => {
725
+ const renewed = await extendClaim.call(store, snapshot, { ttlMs: originalTtlMs });
726
+ leaseRef.current = renewed;
727
+ })();
728
+ }
729
+ return keepAlive;
730
+ }
731
+ function noopStop() {
732
+ }
630
733
 
631
734
  // src/agent/session-lifecycle.ts
632
735
  function resolveSessionStore(storeEntries) {
@@ -646,21 +749,43 @@ function resolveSessionStore(storeEntries) {
646
749
  }
647
750
  return result;
648
751
  }
649
- async function querySessionPhase(store, sessionId) {
752
+ async function querySessionPhase(store, agentId, sessionId) {
650
753
  if (store === void 0) {
651
754
  return { phase: "fresh" };
652
755
  }
653
- const loaded = await store.load(sessionId);
756
+ const loaded = await store.load(agentId, sessionId);
654
757
  return storedSessionToPhase(loaded);
655
758
  }
656
- async function runWithSession(store, sessionId, graph, initialStateArg, schema, ctx, options) {
759
+ async function runWithSession(store, agentId, sessionId, runId, graph, initialStateArg, schema, ctx, options) {
760
+ const leaseRef = options?.leaseRef ?? { current: null };
761
+ const keepAlive = createKeepAliveFn(leaseRef, store, options?.claimTtlMs ?? 3e4);
762
+ ctx["keepAlive"] = keepAlive;
763
+ let leaseExpired = false;
764
+ const composedShouldStop = () => {
765
+ if (options?.shouldStop?.() === true) return true;
766
+ if (leaseRef.current !== null && leaseRef.current.expiresAt <= Date.now()) {
767
+ leaseExpired = true;
768
+ return true;
769
+ }
770
+ return false;
771
+ };
657
772
  if (store === void 0) {
658
773
  const state2 = initializeState(null, initialStateArg, schema);
659
- return runLoop(graph, state2, ctx, schema, options?.shouldStop, void 0, options);
774
+ const result2 = await runLoop(graph, state2, ctx, schema, composedShouldStop, void 0, {
775
+ ...options,
776
+ runId,
777
+ ...options?.parentRunId !== void 0 ? { parentRunId: options.parentRunId } : {}
778
+ });
779
+ if (leaseExpired) {
780
+ const expiredError = new LeaseExpiredError(sessionId);
781
+ options?.onStoreError?.(expiredError, "claim");
782
+ throw expiredError;
783
+ }
784
+ return result2;
660
785
  }
661
786
  let loaded;
662
787
  try {
663
- loaded = await store.load(sessionId);
788
+ loaded = await store.load(agentId, sessionId);
664
789
  } catch (error) {
665
790
  const storeError = new StoreLoadError(error);
666
791
  options?.onStoreError?.(storeError, "load");
@@ -668,38 +793,62 @@ async function runWithSession(store, sessionId, graph, initialStateArg, schema,
668
793
  failState["$error"] = storeError;
669
794
  return { state: failState, signal: "$error", paused: false, cursor: null };
670
795
  }
671
- const runId = randomUUID();
796
+ if (loaded !== null && loaded.phase === "completed") {
797
+ return {
798
+ state: loaded.finalState,
799
+ signal: loaded.signal ?? null,
800
+ cursor: null,
801
+ paused: false
802
+ };
803
+ }
672
804
  const state = initializeState(loaded, initialStateArg, schema);
673
805
  const initialStateSnapshot = { ...state };
674
806
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
675
- const result = await runLoop(graph, state, ctx, schema, options?.shouldStop, loaded?.step, options);
807
+ const result = await runLoop(graph, state, ctx, schema, composedShouldStop, loaded?.step, {
808
+ ...options,
809
+ runId,
810
+ ...options?.parentRunId !== void 0 ? { parentRunId: options.parentRunId } : {}
811
+ });
812
+ if (leaseExpired) {
813
+ const expiredError = new LeaseExpiredError(sessionId);
814
+ options?.onStoreError?.(expiredError, "claim");
815
+ throw expiredError;
816
+ }
676
817
  const settledAt = (/* @__PURE__ */ new Date()).toISOString();
818
+ const metadata = options?.instanceId !== void 0 ? { instanceId: options.instanceId } : void 0;
677
819
  try {
820
+ const version = (loaded?.version ?? -1) + 1;
678
821
  if (result.paused) {
679
822
  const saved = {
823
+ agentId,
680
824
  runId,
681
825
  sessionId,
826
+ version,
682
827
  startedAt,
683
828
  settledAt,
684
829
  phase: "paused",
685
830
  initialState: initialStateSnapshot,
686
831
  finalState: result.state,
687
832
  step: result.cursor,
688
- ...result.signal !== null ? { signal: result.signal } : {}
833
+ ...result.signal !== null ? { signal: result.signal } : {},
834
+ ...metadata !== void 0 ? { metadata } : {}
689
835
  };
690
- await store.save(sessionId, saved);
836
+ await store.save(agentId, sessionId, saved);
691
837
  } else {
692
838
  const saved = {
839
+ agentId,
693
840
  runId,
694
841
  sessionId,
842
+ version,
695
843
  startedAt,
696
844
  settledAt,
697
845
  phase: "completed",
698
846
  initialState: initialStateSnapshot,
699
847
  finalState: result.state,
700
- ...result.signal !== null ? { signal: result.signal } : {}
848
+ ...result.signal !== null ? { signal: result.signal } : {},
849
+ ...metadata !== void 0 ? { metadata } : {}
701
850
  };
702
- await store.save(sessionId, saved);
851
+ await store.save(agentId, sessionId, saved);
703
852
  }
704
853
  } catch (error) {
705
854
  options?.onStoreError?.(error, "persist");
@@ -716,8 +865,8 @@ var NoInterruptError = class extends Error {
716
865
  this.name = "NoInterruptError";
717
866
  }
718
867
  };
719
- async function injectInterruptResponse(store, sessionId, interruptId, response) {
720
- const loaded = await store.load(sessionId);
868
+ async function injectInterruptResponse(store, agentId, sessionId, interruptId, response) {
869
+ const loaded = await store.load(agentId, sessionId);
721
870
  if (loaded === null) throw new NoInterruptError();
722
871
  if (loaded.phase !== "paused") throw new NoInterruptError();
723
872
  const interrupt = loaded.finalState.$interrupt;
@@ -730,8 +879,10 @@ async function injectInterruptResponse(store, sessionId, interruptId, response)
730
879
  $interruptResponses: { ...existingResponses, [interruptId]: response }
731
880
  };
732
881
  const updated = {
882
+ agentId: loaded.agentId,
733
883
  runId: loaded.runId,
734
884
  sessionId: loaded.sessionId,
885
+ version: loaded.version + 1,
735
886
  startedAt: loaded.startedAt,
736
887
  settledAt: loaded.settledAt,
737
888
  phase: "paused",
@@ -740,11 +891,11 @@ async function injectInterruptResponse(store, sessionId, interruptId, response)
740
891
  ...loaded.step !== void 0 ? { step: loaded.step } : {},
741
892
  ...loaded.signal !== void 0 ? { signal: loaded.signal } : {}
742
893
  };
743
- await store.save(sessionId, updated);
894
+ await store.save(agentId, sessionId, updated);
744
895
  }
745
896
 
746
897
  // src/agent/run-handle.ts
747
- function createRunHandle(sessionId, execution, stopFlag, stepRef, resumeFn) {
898
+ function createRunHandle(sessionId, runId, execution, stopFlag, stepRef, resumeFn) {
748
899
  let settledOutcome = null;
749
900
  const settled = execution.then(
750
901
  (outcome) => {
@@ -782,6 +933,9 @@ function createRunHandle(sessionId, execution, stopFlag, stepRef, resumeFn) {
782
933
  get sessionId() {
783
934
  return sessionId;
784
935
  },
936
+ get runId() {
937
+ return runId;
938
+ },
785
939
  get currentStep() {
786
940
  return stepRef.current;
787
941
  }
@@ -790,7 +944,7 @@ function createRunHandle(sessionId, execution, stopFlag, stepRef, resumeFn) {
790
944
  }
791
945
 
792
946
  // src/agent/create-agent.ts
793
- import { randomUUID as randomUUID2 } from "crypto";
947
+ import { randomUUID } from "crypto";
794
948
 
795
949
  // src/agent/event-callbacks.ts
796
950
  function extractRunEvents(resources) {
@@ -802,13 +956,20 @@ function extractRunEvents(resources) {
802
956
  }
803
957
 
804
958
  // src/agent/create-agent.ts
959
+ var NOOP_OBSERVER = {};
960
+ function extractRunObserver(resources) {
961
+ const raw = resources["observer"];
962
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return void 0;
963
+ return raw;
964
+ }
805
965
  var MissingLoopError = class extends Error {
806
966
  constructor() {
807
- super("harness has no loop \u2014 call h.loop() before createAgent()");
967
+ super("harness has no loop \u2014 call h.loop() before createAgent(id, h, slots)");
808
968
  this.name = "MissingLoopError";
809
969
  }
810
970
  };
811
971
  var MissingSlotError = class extends Error {
972
+ /** The missing slot key. */
812
973
  key;
813
974
  constructor(key) {
814
975
  super(`required slot "${key}" was not provided in createAgent slots`);
@@ -817,6 +978,7 @@ var MissingSlotError = class extends Error {
817
978
  }
818
979
  };
819
980
  var RuntimeSlotInAgentError = class extends Error {
981
+ /** The offending slot key. */
820
982
  key;
821
983
  constructor(key) {
822
984
  super(`slot "${key}" is a runtime() slot \u2014 provide it in agent.run(), not createAgent()`);
@@ -825,6 +987,7 @@ var RuntimeSlotInAgentError = class extends Error {
825
987
  }
826
988
  };
827
989
  var UnknownSlotError = class extends Error {
990
+ /** The unknown slot key. */
828
991
  key;
829
992
  constructor(key) {
830
993
  super(`slot "${key}" is not declared in the harness \u2014 remove it or add h.provide("${key}", required())`);
@@ -833,6 +996,7 @@ var UnknownSlotError = class extends Error {
833
996
  }
834
997
  };
835
998
  var MissingRuntimeSlotError = class extends Error {
999
+ /** The missing runtime slot key. */
836
1000
  key;
837
1001
  constructor(key) {
838
1002
  super(`runtime slot "${key}" was not provided in agent.run() resources`);
@@ -841,6 +1005,7 @@ var MissingRuntimeSlotError = class extends Error {
841
1005
  }
842
1006
  };
843
1007
  var RequiredSlotInRunError = class extends Error {
1008
+ /** The offending slot key. */
844
1009
  key;
845
1010
  constructor(key) {
846
1011
  super(`slot "${key}" is a required() slot \u2014 provide it in createAgent(), not agent.run()`);
@@ -849,6 +1014,7 @@ var RequiredSlotInRunError = class extends Error {
849
1014
  }
850
1015
  };
851
1016
  var UnknownRunSlotError = class extends Error {
1017
+ /** The unknown resource key. */
852
1018
  key;
853
1019
  constructor(key) {
854
1020
  super(`slot "${key}" is not a runtime() slot \u2014 do not pass it in agent.run() resources`);
@@ -857,7 +1023,7 @@ var UnknownRunSlotError = class extends Error {
857
1023
  }
858
1024
  };
859
1025
  var _agentInternals = /* @__PURE__ */ Symbol("_agentInternals");
860
- function createAgent(h, slots) {
1026
+ function createAgent(id, h, slots, agentOptions) {
861
1027
  const internals = getInternals(h);
862
1028
  if (internals.loopDef === void 0) {
863
1029
  throw new MissingLoopError();
@@ -906,6 +1072,7 @@ function createAgent(h, slots) {
906
1072
  }
907
1073
  const capturedStore = resolveSessionStore(storeEntries);
908
1074
  const agentInternals = {
1075
+ agentId: id,
909
1076
  resolvedProviders,
910
1077
  storeEntries,
911
1078
  loopDef: internals.loopDef,
@@ -914,38 +1081,104 @@ function createAgent(h, slots) {
914
1081
  runtimeKeys,
915
1082
  requiredKeys
916
1083
  };
917
- const reservedRunKeys = /* @__PURE__ */ new Set(["sessionId", "signal", "events", "listeners"]);
1084
+ const reservedRunKeys = /* @__PURE__ */ new Set(["sessionId", "signal", "events", "listeners", "observer", "claimOptions", "parentRunId"]);
918
1085
  const inFlightSessions = /* @__PURE__ */ new Set();
919
1086
  const interruptPendingSessions = /* @__PURE__ */ new Set();
920
- const makeAgentResumeHandle = (resp, sId, iId) => {
921
- const flag = { stopped: false };
1087
+ const makeAgentResumeHandle = (resp, sId, iId, resumeOpts, observer, setStepContextSlots) => {
1088
+ let _stopped = false;
1089
+ const abortController = new AbortController();
1090
+ const flag = {
1091
+ get stopped() {
1092
+ return _stopped;
1093
+ },
1094
+ set stopped(v) {
1095
+ _stopped = v;
1096
+ if (v) abortController.abort();
1097
+ }
1098
+ };
922
1099
  const ref = { current: null };
1100
+ const rId = randomUUID();
923
1101
  const exec = (async () => {
924
1102
  await Promise.resolve();
1103
+ const claimOptions = { ttlMs: 3e4 };
1104
+ let leaseRef = void 0;
925
1105
  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;
1106
+ if (typeof capturedStore?.claim === "function") {
1107
+ let lease;
1108
+ try {
1109
+ lease = await capturedStore.claim(id, sId, claimOptions);
1110
+ } catch (claimError) {
1111
+ const loadError = new StoreLoadError(claimError);
1112
+ resumeOpts?.onStoreError?.(loadError, "claim");
1113
+ const failState = initializeState(null, {}, agentInternals.stateSchema);
1114
+ failState["$error"] = loadError;
1115
+ return { state: failState, signal: "$error" };
1116
+ }
1117
+ if (lease === null) {
1118
+ const busyError = new SessionBusyError(sId);
1119
+ resumeOpts?.onStoreError?.(busyError, "claim");
1120
+ const failState = initializeState(null, {}, agentInternals.stateSchema);
1121
+ failState["$error"] = busyError;
1122
+ return { state: failState, signal: "$error" };
1123
+ }
1124
+ leaseRef = { current: lease };
1125
+ }
1126
+ try {
1127
+ if (capturedStore === void 0) throw new NoInterruptError();
1128
+ try {
1129
+ await injectInterruptResponse(capturedStore, id, sId, iId, resp);
1130
+ } catch (injectError) {
1131
+ if (injectError instanceof NoInterruptError) throw injectError;
1132
+ const storeError = new StoreLoadError(injectError);
1133
+ resumeOpts?.onStoreError?.(storeError, "claim");
1134
+ const failState = initializeState(null, {}, agentInternals.stateSchema);
1135
+ failState["$error"] = storeError;
1136
+ return { state: failState, signal: "$error" };
1137
+ }
1138
+ const agentCtx = {
1139
+ ...Object.fromEntries(agentInternals.resolvedProviders),
1140
+ agentId: id,
1141
+ sessionId: sId,
1142
+ runId: rId,
1143
+ signal: abortController.signal
1144
+ };
1145
+ const r = await runWithSession(
1146
+ capturedStore,
1147
+ id,
1148
+ sId,
1149
+ rId,
1150
+ agentInternals.loopDef,
1151
+ {},
1152
+ agentInternals.stateSchema,
1153
+ agentCtx,
1154
+ {
1155
+ shouldStop: () => flag.stopped,
1156
+ onBeforeStep: (n) => {
1157
+ ref.current = n;
1158
+ },
1159
+ ...resumeOpts?.onStoreError !== void 0 ? { onStoreError: resumeOpts.onStoreError } : {},
1160
+ ...leaseRef !== void 0 ? { leaseRef, claimTtlMs: claimOptions.ttlMs } : {},
1161
+ ...observer !== void 0 ? { observer } : {},
1162
+ ...setStepContextSlots !== void 0 && setStepContextSlots.length > 0 ? { setStepContextSlots } : {}
943
1163
  }
944
- // _state unused in cross-process resume — no events available
1164
+ );
1165
+ if (r.signal === "$interrupt") interruptPendingSessions.add(sId);
1166
+ return { state: r.state, signal: r.signal };
1167
+ } catch (error) {
1168
+ if (error instanceof LeaseExpiredError) {
1169
+ const failState = initializeState(null, {}, agentInternals.stateSchema);
1170
+ failState["$error"] = error;
1171
+ return { state: failState, signal: "$error" };
945
1172
  }
946
- );
947
- if (r.signal === "$interrupt") interruptPendingSessions.add(sId);
948
- return { state: r.state, signal: r.signal };
1173
+ throw error;
1174
+ } finally {
1175
+ if (leaseRef !== void 0 && typeof capturedStore?.release === "function") {
1176
+ try {
1177
+ await capturedStore.release(leaseRef.current);
1178
+ } catch {
1179
+ }
1180
+ }
1181
+ }
949
1182
  } finally {
950
1183
  inFlightSessions.delete(sId);
951
1184
  }
@@ -956,9 +1189,10 @@ function createAgent(h, slots) {
956
1189
  inFlightSessions.add(sId);
957
1190
  return makeAgentResumeHandle(r, sId, i);
958
1191
  };
959
- return createRunHandle(sId, exec, flag, ref, resumeFn);
1192
+ return createRunHandle(sId, rId, exec, flag, ref, resumeFn);
960
1193
  };
961
1194
  const agent = {
1195
+ id,
962
1196
  run: (initialState, resources) => {
963
1197
  for (const key of Object.keys(resources)) {
964
1198
  if (reservedRunKeys.has(key)) continue;
@@ -977,15 +1211,29 @@ function createAgent(h, slots) {
977
1211
  throw new MissingRuntimeSlotError(key);
978
1212
  }
979
1213
  }
980
- const sessionId = typeof resources["sessionId"] === "string" ? resources["sessionId"] : randomUUID2();
1214
+ const sessionId = typeof resources["sessionId"] === "string" ? resources["sessionId"] : randomUUID();
1215
+ const runId = randomUUID();
981
1216
  if (inFlightSessions.has(sessionId)) throw new SessionInFlightError(sessionId);
982
1217
  if (interruptPendingSessions.has(sessionId)) throw new SessionPendingInterruptError(sessionId);
983
1218
  inFlightSessions.add(sessionId);
984
- const stopFlag = { stopped: false };
1219
+ let _stopped = false;
1220
+ const abortController = new AbortController();
1221
+ const stopFlag = {
1222
+ get stopped() {
1223
+ return _stopped;
1224
+ },
1225
+ set stopped(v) {
1226
+ _stopped = v;
1227
+ if (v) abortController.abort();
1228
+ }
1229
+ };
985
1230
  const rawSignal = resources["signal"];
1231
+ let ctxSignal = abortController.signal;
986
1232
  if (rawSignal instanceof AbortSignal) {
1233
+ ctxSignal = rawSignal;
987
1234
  rawSignal.addEventListener("abort", () => {
988
1235
  stopFlag.stopped = true;
1236
+ abortController.abort();
989
1237
  }, { once: true });
990
1238
  if (rawSignal.aborted) stopFlag.stopped = true;
991
1239
  }
@@ -998,10 +1246,27 @@ function createAgent(h, slots) {
998
1246
  const ctx = {
999
1247
  ...Object.fromEntries(agentInternals.resolvedProviders),
1000
1248
  ...runtimeSlots,
1001
- sessionId
1249
+ agentId: id,
1250
+ sessionId,
1251
+ runId,
1252
+ signal: ctxSignal,
1253
+ ...agentOptions?.instanceId !== void 0 ? { instanceId: agentOptions.instanceId } : {}
1002
1254
  };
1003
1255
  const events = extractRunEvents(resources);
1004
1256
  const listeners = extractRunListeners(resources);
1257
+ const observer = extractRunObserver(resources);
1258
+ for (const slot of [...agentInternals.resolvedProviders.values(), ...Object.values(runtimeSlots)]) {
1259
+ if (slot !== null && typeof slot === "object" && typeof slot["bindObserver"] === "function") {
1260
+ slot.bindObserver(observer ?? NOOP_OBSERVER);
1261
+ }
1262
+ }
1263
+ const setStepContextSlots = [];
1264
+ for (const slot of [...agentInternals.resolvedProviders.values(), ...Object.values(runtimeSlots)]) {
1265
+ if (slot !== null && typeof slot === "object" && typeof slot["setStepContext"] === "function") {
1266
+ setStepContextSlots.push(slot);
1267
+ }
1268
+ }
1269
+ const parentRunId = typeof resources["parentRunId"] === "string" ? resources["parentRunId"] : void 0;
1005
1270
  const options = {
1006
1271
  shouldStop: () => stopFlag.stopped,
1007
1272
  onBeforeStep: (name, state) => {
@@ -1014,24 +1279,72 @@ function createAgent(h, slots) {
1014
1279
  ...events.onError !== void 0 ? { onError: events.onError } : {},
1015
1280
  ...events.onComplete !== void 0 ? { onComplete: events.onComplete } : {},
1016
1281
  ...events.onInterrupt !== void 0 ? { onInterrupt: events.onInterrupt } : {},
1017
- ...Object.keys(listeners).length > 0 ? { listeners } : {}
1282
+ ...Object.keys(listeners).length > 0 ? { listeners } : {},
1283
+ ...observer !== void 0 ? { observer } : {},
1284
+ ...agentOptions?.instanceId !== void 0 ? { instanceId: agentOptions.instanceId } : {},
1285
+ ...parentRunId !== void 0 ? { parentRunId } : {},
1286
+ ...setStepContextSlots.length > 0 ? { setStepContextSlots } : {}
1018
1287
  };
1019
1288
  let lastResult = null;
1020
1289
  const execution = (async () => {
1021
1290
  await Promise.resolve();
1291
+ const rawClaimOpts = resources["claimOptions"];
1292
+ const claimOptions = typeof rawClaimOpts === "object" && rawClaimOpts !== null && typeof rawClaimOpts["ttlMs"] === "number" ? rawClaimOpts : { ttlMs: 3e4 };
1293
+ let leaseRef = void 0;
1022
1294
  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 };
1295
+ if (typeof capturedStore?.claim === "function") {
1296
+ let lease;
1297
+ try {
1298
+ lease = await capturedStore.claim(id, sessionId, claimOptions);
1299
+ } catch (claimError) {
1300
+ const loadError = new StoreLoadError(claimError);
1301
+ events.onStoreError?.(loadError, "claim");
1302
+ const failState = initializeState(null, initialState, agentInternals.stateSchema);
1303
+ failState["$error"] = loadError;
1304
+ return { state: failState, signal: "$error" };
1305
+ }
1306
+ if (lease === null) {
1307
+ const busyError = new SessionBusyError(sessionId);
1308
+ events.onStoreError?.(busyError, "claim");
1309
+ const failState = initializeState(null, initialState, agentInternals.stateSchema);
1310
+ failState["$error"] = busyError;
1311
+ return { state: failState, signal: "$error" };
1312
+ }
1313
+ leaseRef = { current: lease };
1314
+ }
1315
+ try {
1316
+ const r = await runWithSession(
1317
+ capturedStore,
1318
+ id,
1319
+ sessionId,
1320
+ runId,
1321
+ agentInternals.loopDef,
1322
+ initialState,
1323
+ agentInternals.stateSchema,
1324
+ ctx,
1325
+ {
1326
+ ...options,
1327
+ ...leaseRef !== void 0 ? { leaseRef, claimTtlMs: claimOptions.ttlMs } : {}
1328
+ }
1329
+ );
1330
+ if (r.signal === "$interrupt") interruptPendingSessions.add(sessionId);
1331
+ lastResult = { state: r.state, cursor: r.cursor };
1332
+ return { state: r.state, signal: r.signal };
1333
+ } catch (error) {
1334
+ if (error instanceof LeaseExpiredError) {
1335
+ const failState = initializeState(null, initialState, agentInternals.stateSchema);
1336
+ failState["$error"] = error;
1337
+ return { state: failState, signal: "$error" };
1338
+ }
1339
+ throw error;
1340
+ } finally {
1341
+ if (leaseRef !== void 0 && typeof capturedStore?.release === "function") {
1342
+ try {
1343
+ await capturedStore.release(leaseRef.current);
1344
+ } catch {
1345
+ }
1346
+ }
1347
+ }
1035
1348
  } finally {
1036
1349
  inFlightSessions.delete(sessionId);
1037
1350
  }
@@ -1041,20 +1354,38 @@ function createAgent(h, slots) {
1041
1354
  if (inFlightSessions.has(sessionId)) throw new SessionInFlightError(sessionId);
1042
1355
  interruptPendingSessions.delete(sessionId);
1043
1356
  inFlightSessions.add(sessionId);
1044
- const resumeStopFlag = { stopped: false };
1357
+ let _resumeStopped = false;
1358
+ const resumeAbortController = new AbortController();
1359
+ const resumeStopFlag = {
1360
+ get stopped() {
1361
+ return _resumeStopped;
1362
+ },
1363
+ set stopped(v) {
1364
+ _resumeStopped = v;
1365
+ if (v) resumeAbortController.abort();
1366
+ }
1367
+ };
1045
1368
  const resumeStepRef = { current: null };
1369
+ const resumeRunId = randomUUID();
1046
1370
  const resumeExecution = (async () => {
1047
1371
  await Promise.resolve();
1048
1372
  try {
1373
+ const resumeCtx = {
1374
+ ...ctx,
1375
+ runId: resumeRunId,
1376
+ signal: resumeAbortController.signal
1377
+ };
1049
1378
  if (capturedStore !== void 0) {
1050
- await injectInterruptResponse(capturedStore, sessionId, interruptId, response);
1379
+ await injectInterruptResponse(capturedStore, id, sessionId, interruptId, response);
1051
1380
  const r2 = await runWithSession(
1052
1381
  capturedStore,
1382
+ id,
1053
1383
  sessionId,
1384
+ resumeRunId,
1054
1385
  agentInternals.loopDef,
1055
1386
  {},
1056
1387
  agentInternals.stateSchema,
1057
- ctx,
1388
+ resumeCtx,
1058
1389
  {
1059
1390
  shouldStop: () => resumeStopFlag.stopped,
1060
1391
  onBeforeStep: (n, s) => {
@@ -1067,7 +1398,9 @@ function createAgent(h, slots) {
1067
1398
  ...events.onError !== void 0 ? { onError: events.onError } : {},
1068
1399
  ...events.onComplete !== void 0 ? { onComplete: events.onComplete } : {},
1069
1400
  ...events.onInterrupt !== void 0 ? { onInterrupt: events.onInterrupt } : {},
1070
- ...Object.keys(listeners).length > 0 ? { listeners } : {}
1401
+ ...Object.keys(listeners).length > 0 ? { listeners } : {},
1402
+ ...observer !== void 0 ? { observer } : {},
1403
+ ...setStepContextSlots.length > 0 ? { setStepContextSlots } : {}
1071
1404
  }
1072
1405
  );
1073
1406
  if (r2.signal === "$interrupt") interruptPendingSessions.add(sessionId);
@@ -1084,11 +1417,12 @@ function createAgent(h, slots) {
1084
1417
  const r = await runLoop(
1085
1418
  agentInternals.loopDef,
1086
1419
  state,
1087
- ctx,
1420
+ resumeCtx,
1088
1421
  agentInternals.stateSchema,
1089
1422
  () => resumeStopFlag.stopped,
1090
1423
  cursor,
1091
1424
  {
1425
+ runId: resumeRunId,
1092
1426
  onBeforeStep: (n, s) => {
1093
1427
  resumeStepRef.current = n;
1094
1428
  events.onBeforeStep?.(n, s);
@@ -1097,39 +1431,91 @@ function createAgent(h, slots) {
1097
1431
  ...events.onError !== void 0 ? { onError: events.onError } : {},
1098
1432
  ...events.onComplete !== void 0 ? { onComplete: events.onComplete } : {},
1099
1433
  ...events.onInterrupt !== void 0 ? { onInterrupt: events.onInterrupt } : {},
1100
- ...Object.keys(listeners).length > 0 ? { listeners } : {}
1434
+ ...Object.keys(listeners).length > 0 ? { listeners } : {},
1435
+ ...observer !== void 0 ? { observer } : {},
1436
+ ...setStepContextSlots.length > 0 ? { setStepContextSlots } : {}
1101
1437
  }
1102
1438
  );
1103
1439
  if (r.signal === "$interrupt") interruptPendingSessions.add(sessionId);
1104
1440
  lastResult = { state: r.state, cursor: r.cursor };
1105
1441
  return { state: r.state, signal: r.signal };
1442
+ } catch (error) {
1443
+ if (error instanceof LeaseExpiredError) {
1444
+ const failState = initializeState(null, initialState, agentInternals.stateSchema);
1445
+ failState["$error"] = error;
1446
+ return { state: failState, signal: "$error" };
1447
+ }
1448
+ throw error;
1106
1449
  } finally {
1107
1450
  inFlightSessions.delete(sessionId);
1108
1451
  }
1109
1452
  })();
1110
- return createRunHandle(sessionId, resumeExecution, resumeStopFlag, resumeStepRef, buildResumeFn());
1453
+ return createRunHandle(sessionId, resumeRunId, resumeExecution, resumeStopFlag, resumeStepRef, buildResumeFn());
1111
1454
  };
1112
1455
  };
1113
- return createRunHandle(sessionId, execution, stopFlag, stepRef, buildResumeFn());
1456
+ return createRunHandle(sessionId, runId, execution, stopFlag, stepRef, buildResumeFn());
1114
1457
  },
1115
- resume: (response, sessionId, interruptId) => {
1458
+ resume: (response, sessionId, interruptId, resources) => {
1116
1459
  if (inFlightSessions.has(sessionId)) throw new SessionInFlightError(sessionId);
1117
1460
  interruptPendingSessions.delete(sessionId);
1118
1461
  inFlightSessions.add(sessionId);
1119
- return makeAgentResumeHandle(response, sessionId, interruptId);
1462
+ const events = extractRunEvents(resources ?? {});
1463
+ const resumeOpts = events.onStoreError !== void 0 ? { onStoreError: events.onStoreError } : void 0;
1464
+ const observer = extractRunObserver(resources ?? {});
1465
+ for (const slot of agentInternals.resolvedProviders.values()) {
1466
+ if (slot !== null && typeof slot === "object" && typeof slot["bindObserver"] === "function") {
1467
+ slot.bindObserver(observer ?? NOOP_OBSERVER);
1468
+ }
1469
+ }
1470
+ const setStepContextSlots = [];
1471
+ for (const slot of agentInternals.resolvedProviders.values()) {
1472
+ if (slot !== null && typeof slot === "object" && typeof slot["setStepContext"] === "function") {
1473
+ setStepContextSlots.push(slot);
1474
+ }
1475
+ }
1476
+ return makeAgentResumeHandle(response, sessionId, interruptId, resumeOpts, observer ?? void 0, setStepContextSlots.length > 0 ? setStepContextSlots : void 0);
1120
1477
  },
1121
- status: (sessionId) => querySessionPhase(capturedStore, sessionId),
1478
+ status: (sessionId) => querySessionPhase(capturedStore, id, sessionId),
1122
1479
  [_agentInternals]: agentInternals
1123
1480
  };
1124
1481
  return agent;
1125
1482
  }
1483
+
1484
+ // src/agent/observer.ts
1485
+ function composeObservers(...observers) {
1486
+ return {
1487
+ onRunStart: (ctx) => {
1488
+ for (const o of observers) o.onRunStart?.(ctx);
1489
+ },
1490
+ onRunEnd: (ctx, event) => {
1491
+ for (const o of observers) o.onRunEnd?.(ctx, event);
1492
+ },
1493
+ onStepStart: (ctx) => {
1494
+ for (const o of observers) o.onStepStart?.(ctx);
1495
+ },
1496
+ onStepEnd: (ctx, event) => {
1497
+ for (const o of observers) o.onStepEnd?.(ctx, event);
1498
+ },
1499
+ onStepError: (ctx, event) => {
1500
+ for (const o of observers) o.onStepError?.(ctx, event);
1501
+ },
1502
+ onInterrupt: (ctx, event) => {
1503
+ for (const o of observers) o.onInterrupt?.(ctx, event);
1504
+ },
1505
+ onEvent: (ctx, type, payload) => {
1506
+ for (const o of observers) o.onEvent?.(ctx, type, payload);
1507
+ }
1508
+ };
1509
+ }
1126
1510
  export {
1511
+ LoopNotDefinedError,
1127
1512
  NoInterruptError,
1128
1513
  REQUIRED_TAG,
1129
1514
  RUNTIME_TAG,
1130
1515
  SessionInFlightError,
1131
1516
  SessionPendingInterruptError,
1132
1517
  StoreLoadError,
1518
+ composeObservers,
1133
1519
  createAgent,
1134
1520
  createHarness,
1135
1521
  field,