@noetaris/harness 0.4.0 → 0.5.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/README.md CHANGED
@@ -190,6 +190,8 @@ const resumed = agent.resume(response, sessionId, interruptId)
190
190
  - ESM only (`"type": "module"`)
191
191
  - Zero runtime dependencies
192
192
 
193
+ See [LIMITATIONS.md](LIMITATIONS.md) for known limitations.
194
+
193
195
  ## Related Packages
194
196
 
195
197
  - [`@noetaris/harness-store`](https://github.com/noetaris-lab/harness-store) — session store implementations (`InMemorySessionStore`, `LocalFileSessionStore`, etc.)
package/dist/index.d.ts CHANGED
@@ -195,38 +195,38 @@ interface Observer {
195
195
  onEvent?: (ctx: StepContext, type: string, payload: unknown) => void;
196
196
  }
197
197
  /**
198
- * Implemented by resources (e.g. LLM adapters) that accept an {@link Observer}
199
- * at run time. The harness calls `bindObserver` on every slot value that
200
- * implements this interface before the first step runs.
198
+ * The telemetry a resource needs to attribute an `invoke()` call to the correct
199
+ * run and step: the run's {@link Observer} and the current {@link StepContext}.
201
200
  *
202
- * Optionally, the harness calls `setStepContext` at the start of each step for
203
- * every slot that exposes it. Adapters use this to attribute per-step telemetry
204
- * (e.g. `observer.onEvent`) to the correct step without manual calls from step code.
201
+ * The harness passes this per call rather than storing it on the resource instance,
202
+ * so concurrently-running fork branches that share one instance never race on it —
203
+ * each call carries its own context by value.
204
+ */
205
+ interface TelemetryContext {
206
+ readonly observer: Observer;
207
+ readonly stepContext: StepContext;
208
+ }
209
+ /**
210
+ * Implemented by resources (e.g. LLM adapters) that emit per-step telemetry.
211
+ *
212
+ * The harness calls {@link ObserverAware.withTelemetry} at the start of each step for every
213
+ * slot that exposes it, and installs the returned value as that slot on the step's `ctx`.
214
+ * Step code then calls the returned view (e.g. `ctx.llm.invoke(...)`) with no telemetry
215
+ * arguments, and the view forwards the {@link TelemetryContext} into the call it wraps — so
216
+ * telemetry travels *with the call* rather than living in mutable instance state.
217
+ *
218
+ * The return is typed `unknown` because core exports no LLM-domain type; a concrete adapter
219
+ * narrows it (e.g. to its `LLM` view). The harness treats the returned value as an opaque
220
+ * slot, exactly as it treats every provided slot.
205
221
  */
206
222
  interface ObserverAware {
207
223
  /**
208
- * Receive the run's observer. The harness calls this once per `agent.run()`
209
- * invocation before execution begins.
224
+ * Return a view of this resource scoped to the given telemetry. The harness installs
225
+ * the returned value as the resource's slot on the step's `ctx` before calling `step.run`.
226
+ * Called once per step (and once per step per branch), so each concurrent branch gets its
227
+ * own scoped view and there is no shared field to race on.
210
228
  */
211
- bindObserver(observer: Observer): void;
212
- /**
213
- * Receive the current step context. The harness calls this at the start of
214
- * each step for every slot that exposes this method, before calling `step.run`.
215
- *
216
- * Adapters that emit `observer.onEvent` in their `invoke()` method store the
217
- * provided `StepContext` and use it as the first argument to `onEvent`, so
218
- * events are attributed to the correct step automatically.
219
- *
220
- * **Known limitation (fork/join branches):** the store-in-a-field pattern above is safe only
221
- * because exactly one step runs at a time in a non-forked run. When a fork's branches run
222
- * concurrently, they typically share one `ObserverAware` slot instance (`h.provide('llm', ...)`
223
- * provides it once), so concurrent branches race on that stored field and `onEvent` calls may
224
- * be attributed to the wrong branch/step. This is a known v1 limitation of the built-in LLM
225
- * adapters (`@noetaris/harness-anthropic`/`-openai`/`-google`/`-ollama`), not fixed for this
226
- * release — a correct fix means passing `StepContext` through the call instead of storing it.
227
- * Treat per-step LLM telemetry as unreliable for concurrently-running branches until then.
228
- */
229
- setStepContext?(ctx: StepContext): void;
229
+ withTelemetry(ctx: TelemetryContext): unknown;
230
230
  }
231
231
  /**
232
232
  * Combine multiple {@link Observer} instances into one. Each hook on the
@@ -234,10 +234,27 @@ interface ObserverAware {
234
234
  *
235
235
  * @example
236
236
  * ```ts
237
- * const observer = composeObservers(otelObserver, metricsObserver)
237
+ * const observer = composeObservers([otelObserver, metricsObserver])
238
238
  * ```
239
239
  */
240
- declare function composeObservers(...observers: Observer[]): Observer;
240
+ /**
241
+ * Identifies which observer hook threw. Passed to an {@link ObserverErrorSink}.
242
+ */
243
+ interface ObserverErrorContext {
244
+ /** The observer hook that threw, e.g. `'onStepStart'`, `'onRunEnd'`, `'onEvent'`. */
245
+ readonly hookName: string;
246
+ }
247
+ /**
248
+ * Opt-in sink for errors thrown by observer callbacks. Supplied under the
249
+ * `'onObserverError'` key in `agent.run()` / `agent.resume()` resources.
250
+ *
251
+ * Telemetry is a side-channel: an observer that throws must never crash a run.
252
+ * The harness routes every such throw here and continues. The sink is called at
253
+ * most once per failed hook invocation and MUST NOT rethrow into the run — if it
254
+ * does, the harness contains that too (see {@link safeInvoke}).
255
+ */
256
+ type ObserverErrorSink = (error: unknown, ctx: ObserverErrorContext) => void;
257
+ declare function composeObservers(observers: Observer[], onObserverError?: ObserverErrorSink): Observer;
241
258
 
242
259
  type Cursor = string | ForkCursor;
243
260
  /**
@@ -664,11 +681,10 @@ interface LoopBuilder<S, Ctx> {
664
681
  * with .branch() immediately after. The step immediately following the fork automatically
665
682
  * waits for every branch to settle (the "join") — no separate join primitive exists.
666
683
  *
667
- * Known limitation: the built-in LLM adapters (`@noetaris/harness-anthropic`/`-openai`/
668
- * `-google`/`-ollama`) attribute per-step telemetry via a stored-field pattern that is not
669
- * safe when one adapter instance is shared across concurrently-running branches (the normal
670
- * setup). See `ObserverAware.setStepContext`'s doc comment (`agent/observer.ts`) for details
671
- * — not fixed for this release.
684
+ * Per-step telemetry is attributed correctly across concurrent branches: the harness scopes
685
+ * each branch's slots per step via {@link ObserverAware.withTelemetry}, so telemetry travels
686
+ * with the call rather than through shared instance state. See `ObserverAware`'s doc comment
687
+ * (`agent/observer.ts`).
672
688
  */
673
689
  fork(name: string): LoopBuilder<S, Ctx>;
674
690
  /**
@@ -967,4 +983,4 @@ declare class LeaseExpiredError extends Error {
967
983
  constructor(sessionId: string);
968
984
  }
969
985
 
970
- export { type Agent, type BranchCursor, type BranchDef, type ClaimOptions, type Cursor, type DeepWithMarkers, type FieldDefinition, type ForkCursor, type ForkDef, type FrameworkState, type Harness, type Lease, LeaseExpiredError, type LoopDefinition, type LoopNode, LoopNotDefinedError, NoInterruptError, type Observer, type ObserverAware, REQUIRED_TAG, RUNTIME_TAG, type RequiredMarker, type RouteFn, type RunContext, type RunFn, type RuntimeMarker, SessionBusyError, SessionInFlightError, SessionPendingInterruptError, type SessionStore, type SignalTransition, type StateFromSchema, type StepContext, type StepDef, type StepState, StoreLoadError, type StoredRun, type StoredRunMetadata, type TransitionTarget, composeObservers, createAgent, createHarness, field, isForkCursor, isForkDef, isRequiredMarker, isRuntimeMarker, required, runtime };
986
+ export { type Agent, type BranchCursor, type BranchDef, type ClaimOptions, type Cursor, type DeepWithMarkers, type FieldDefinition, type ForkCursor, type ForkDef, type FrameworkState, type Harness, type Lease, LeaseExpiredError, type LoopDefinition, type LoopNode, LoopNotDefinedError, NoInterruptError, type Observer, type ObserverAware, type ObserverErrorContext, type ObserverErrorSink, REQUIRED_TAG, RUNTIME_TAG, type RequiredMarker, type RouteFn, type RunContext, type RunFn, type RuntimeMarker, SessionBusyError, SessionInFlightError, SessionPendingInterruptError, type SessionStore, type SignalTransition, type StateFromSchema, type StepContext, type StepDef, type StepState, StoreLoadError, type StoredRun, type StoredRunMetadata, type TelemetryContext, type TransitionTarget, composeObservers, createAgent, createHarness, field, isForkCursor, isForkDef, isRequiredMarker, isRuntimeMarker, required, runtime };
package/dist/index.js CHANGED
@@ -491,6 +491,51 @@ function createBuilderInstance(internals) {
491
491
  return builder;
492
492
  }
493
493
 
494
+ // src/agent/observer.ts
495
+ function reportObserverError(error, ctx, sink) {
496
+ if (sink !== void 0) {
497
+ try {
498
+ sink(error, ctx);
499
+ } catch (sinkError) {
500
+ console.error(`[harness] onObserverError sink threw while handling '${ctx.hookName}':`, sinkError);
501
+ }
502
+ return;
503
+ }
504
+ console.error(`[harness] observer hook '${ctx.hookName}' threw:`, error);
505
+ }
506
+ function safeInvoke(hookName, call, sink) {
507
+ try {
508
+ call();
509
+ } catch (error) {
510
+ reportObserverError(error, { hookName }, sink);
511
+ }
512
+ }
513
+ function composeObservers(observers, onObserverError) {
514
+ return {
515
+ onRunStart: (ctx) => {
516
+ for (const o of observers) safeInvoke("onRunStart", () => o.onRunStart?.(ctx), onObserverError);
517
+ },
518
+ onRunEnd: (ctx, event) => {
519
+ for (const o of observers) safeInvoke("onRunEnd", () => o.onRunEnd?.(ctx, event), onObserverError);
520
+ },
521
+ onStepStart: (ctx) => {
522
+ for (const o of observers) safeInvoke("onStepStart", () => o.onStepStart?.(ctx), onObserverError);
523
+ },
524
+ onStepEnd: (ctx, event) => {
525
+ for (const o of observers) safeInvoke("onStepEnd", () => o.onStepEnd?.(ctx, event), onObserverError);
526
+ },
527
+ onStepError: (ctx, event) => {
528
+ for (const o of observers) safeInvoke("onStepError", () => o.onStepError?.(ctx, event), onObserverError);
529
+ },
530
+ onInterrupt: (ctx, event) => {
531
+ for (const o of observers) safeInvoke("onInterrupt", () => o.onInterrupt?.(ctx, event), onObserverError);
532
+ },
533
+ onEvent: (ctx, type, payload) => {
534
+ for (const o of observers) safeInvoke("onEvent", () => o.onEvent?.(ctx, type, payload), onObserverError);
535
+ }
536
+ };
537
+ }
538
+
494
539
  // src/agent/ctx-interrupt.ts
495
540
  var InterruptPause = class extends Error {
496
541
  interruptId;
@@ -523,11 +568,12 @@ function createInterruptFn(state, callCountRef, branchPath) {
523
568
  }
524
569
 
525
570
  // src/agent/ctx-emit.ts
526
- function createEmitFn(listeners, observer, stepCtxRef) {
571
+ function createEmitFn(listeners, observer, stepCtxRef, onObserverError) {
527
572
  return (name, payload) => {
528
573
  listeners[name]?.(payload);
529
574
  if (observer !== void 0 && stepCtxRef !== void 0 && stepCtxRef.current !== null) {
530
- observer.onEvent?.(stepCtxRef.current, name, payload);
575
+ const ctx = stepCtxRef.current;
576
+ safeInvoke("onEvent", () => observer.onEvent?.(ctx, name, payload), onObserverError);
531
577
  }
532
578
  };
533
579
  }
@@ -540,6 +586,7 @@ function extractRunListeners(resources) {
540
586
  }
541
587
 
542
588
  // src/loop/loop-executor.ts
589
+ var NOOP_OBSERVER = {};
543
590
  function isForkCursor(cursor) {
544
591
  return typeof cursor !== "string";
545
592
  }
@@ -601,6 +648,7 @@ function applyUpdate(state, update, schema, touchedKeys) {
601
648
  async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callbacks) {
602
649
  const { onBeforeStep, onAfterStep, onError, onComplete, onInterrupt } = callbacks ?? {};
603
650
  const obs = callbacks?.observer;
651
+ const onObserverError = callbacks?.onObserverError;
604
652
  const implicitNextMap = buildImplicitNextMap(graph);
605
653
  if (!("$error" in state)) state.$error = null;
606
654
  if (!("$interrupt" in state)) state.$interrupt = null;
@@ -614,15 +662,15 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
614
662
  ...typeof ctx.instanceId === "string" ? { instanceId: ctx.instanceId } : {},
615
663
  ...callbacks?.branchPath !== void 0 && callbacks.branchPath.length > 0 ? { branchPath: callbacks.branchPath } : {}
616
664
  };
617
- obs?.onRunStart?.(runCtx);
665
+ safeInvoke("onRunStart", () => obs?.onRunStart?.(runCtx), onObserverError);
618
666
  const callCountRef = { current: 0 };
619
667
  const stepCtxRef = { current: null };
620
668
  ctx["interrupt"] = createInterruptFn(state, callCountRef, callbacks?.branchPath);
621
- ctx["emit"] = createEmitFn(callbacks?.listeners ?? {}, obs, stepCtxRef);
669
+ ctx["emit"] = createEmitFn(callbacks?.listeners ?? {}, obs, stepCtxRef, onObserverError);
622
670
  let cursor = startCursor ?? graph.entryStep;
623
671
  while (true) {
624
672
  if (shouldStop?.()) {
625
- obs?.onRunEnd?.(runCtx, { signal: "$stopped", durationMs: Date.now() - runStart });
673
+ safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal: "$stopped", durationMs: Date.now() - runStart }), onObserverError);
626
674
  return { state, signal: null, cursor, paused: true };
627
675
  }
628
676
  if (isForkCursor(cursor)) {
@@ -685,10 +733,12 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
685
733
  };
686
734
  stepCtxRef.current = stepCtx;
687
735
  const stepStart = Date.now();
688
- obs?.onStepStart?.(stepCtx);
689
- if (callbacks?.setStepContextSlots !== void 0) {
690
- for (const slot of callbacks.setStepContextSlots) {
691
- slot.setStepContext(stepCtx);
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);
692
742
  }
693
743
  }
694
744
  let runSucceeded = false;
@@ -708,19 +758,19 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
708
758
  } catch (e) {
709
759
  if (isInterruptPause(e)) {
710
760
  onInterrupt?.(e.prompt, e.interruptId);
711
- obs?.onInterrupt?.(stepCtx, { prompt: e.prompt, interruptId: e.interruptId });
712
- obs?.onRunEnd?.(runCtx, { signal: "$interrupt", durationMs: Date.now() - runStart });
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);
713
763
  return { state, signal: "$interrupt", cursor, paused: true };
714
764
  }
715
765
  state.$error = e instanceof Error ? e : new Error(String(e));
716
766
  onError?.(state.$error, cursor);
717
- obs?.onStepError?.(stepCtx, { error: state.$error, durationMs: Date.now() - stepStart });
767
+ safeInvoke("onStepError", () => obs?.onStepError?.(stepCtx, { error: state.$error, durationMs: Date.now() - stepStart }), onObserverError);
718
768
  }
719
769
  }
720
770
  if (runSucceeded) {
721
771
  state.$error = null;
722
772
  onAfterStep?.(cursor, state);
723
- obs?.onStepEnd?.(stepCtx, { durationMs: Date.now() - stepStart });
773
+ safeInvoke("onStepEnd", () => obs?.onStepEnd?.(stepCtx, { durationMs: Date.now() - stepStart }), onObserverError);
724
774
  }
725
775
  const callRoute = step.route !== void 0 && (runSucceeded || step.run === void 0 || step.errorAware);
726
776
  if (callRoute) {
@@ -730,7 +780,7 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
730
780
  signal = routeFn(state);
731
781
  } catch (e) {
732
782
  state.$error = e instanceof Error ? e : new Error(String(e));
733
- obs?.onRunEnd?.(runCtx, { signal: "$error", durationMs: Date.now() - runStart });
783
+ safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal: "$error", durationMs: Date.now() - runStart }), onObserverError);
734
784
  return { state, signal: "$error", cursor, paused: true };
735
785
  }
736
786
  const transition = step.transitions.find((t) => t.signal === signal);
@@ -739,7 +789,7 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
739
789
  }
740
790
  if (transition.target.kind === "end") {
741
791
  onComplete?.(state, signal);
742
- obs?.onRunEnd?.(runCtx, { signal, durationMs: Date.now() - runStart });
792
+ safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal, durationMs: Date.now() - runStart }), onObserverError);
743
793
  return { state, signal, cursor: null, paused: false };
744
794
  }
745
795
  cursor = transition.target.name;
@@ -747,7 +797,7 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
747
797
  if (graph.onError !== void 0) {
748
798
  cursor = graph.onError;
749
799
  } else {
750
- obs?.onRunEnd?.(runCtx, { signal: "$error", durationMs: Date.now() - runStart });
800
+ safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal: "$error", durationMs: Date.now() - runStart }), onObserverError);
751
801
  return { state, signal: "$error", cursor, paused: true };
752
802
  }
753
803
  } else {
@@ -859,24 +909,25 @@ async function handleFork(forkNode, priorBranches, state, ctx, schema, shouldSto
859
909
  ...callbacks?.branchPath !== void 0 && callbacks.branchPath.length > 0 ? { branchPath: callbacks.branchPath } : {}
860
910
  };
861
911
  const stepStart = Date.now();
862
- obs?.onStepStart?.(stepCtx);
912
+ const onObserverError = callbacks?.onObserverError;
913
+ safeInvoke("onStepStart", () => obs?.onStepStart?.(stepCtx), onObserverError);
863
914
  const dispatch = await dispatchFork(forkNode, priorBranches, state, ctx, schema, shouldStop, callbacks);
864
915
  if (dispatch.kind === "paused") {
865
- obs?.onRunEnd?.(runCtx, { signal: "$interrupt", durationMs: Date.now() - runStart });
916
+ safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal: "$interrupt", durationMs: Date.now() - runStart }), onObserverError);
866
917
  return { done: true, result: { state, signal: "$interrupt", cursor: dispatch.forkCursor, paused: true } };
867
918
  }
868
919
  if (dispatch.kind === "failed") {
869
920
  state.$error = dispatch.error;
870
921
  callbacks?.onError?.(state.$error, forkNode.name);
871
- obs?.onStepError?.(stepCtx, { error: state.$error, durationMs: Date.now() - stepStart });
922
+ safeInvoke("onStepError", () => obs?.onStepError?.(stepCtx, { error: state.$error, durationMs: Date.now() - stepStart }), onObserverError);
872
923
  if (graph.onError !== void 0) {
873
924
  return { done: false, nextCursor: graph.onError };
874
925
  }
875
- obs?.onRunEnd?.(runCtx, { signal: "$error", durationMs: Date.now() - runStart });
926
+ safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal: "$error", durationMs: Date.now() - runStart }), onObserverError);
876
927
  return { done: true, result: { state, signal: "$error", cursor: forkNode.name, paused: true } };
877
928
  }
878
929
  state.$error = null;
879
- obs?.onStepEnd?.(stepCtx, { durationMs: Date.now() - stepStart });
930
+ safeInvoke("onStepEnd", () => obs?.onStepEnd?.(stepCtx, { durationMs: Date.now() - stepStart }), onObserverError);
880
931
  callbacks?.onAfterStep?.(forkNode.name, state);
881
932
  const next = forkNode.next ?? implicitNextMap.get(forkNode.name) ?? null;
882
933
  if (next === null) {
@@ -1315,12 +1366,24 @@ function extractRunEvents(resources) {
1315
1366
  }
1316
1367
 
1317
1368
  // src/agent/create-agent.ts
1318
- var NOOP_OBSERVER = {};
1369
+ function buildTelemetrySlots(entries) {
1370
+ const out = [];
1371
+ for (const [key, slot] of entries) {
1372
+ if (slot !== null && typeof slot === "object" && typeof slot["withTelemetry"] === "function") {
1373
+ out.push({ key, slot });
1374
+ }
1375
+ }
1376
+ return out;
1377
+ }
1319
1378
  function extractRunObserver(resources) {
1320
1379
  const raw = resources["observer"];
1321
1380
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return void 0;
1322
1381
  return raw;
1323
1382
  }
1383
+ function extractRunErrorSink(resources) {
1384
+ const raw = resources["onObserverError"];
1385
+ return typeof raw === "function" ? raw : void 0;
1386
+ }
1324
1387
  var MissingLoopError = class extends Error {
1325
1388
  constructor() {
1326
1389
  super("harness has no loop \u2014 call h.loop() before createAgent(id, h, slots)");
@@ -1478,10 +1541,10 @@ function createAgent(id, h, slots, agentOptions) {
1478
1541
  runtimeKeys,
1479
1542
  requiredKeys
1480
1543
  };
1481
- const reservedRunKeys = /* @__PURE__ */ new Set(["sessionId", "signal", "events", "listeners", "observer", "claimOptions", "parentRunId"]);
1544
+ const reservedRunKeys = /* @__PURE__ */ new Set(["sessionId", "signal", "events", "listeners", "observer", "onObserverError", "claimOptions", "parentRunId"]);
1482
1545
  const inFlightSessions = /* @__PURE__ */ new Set();
1483
1546
  const interruptPendingSessions = /* @__PURE__ */ new Set();
1484
- const makeAgentResumeHandle = (resp, sId, iId, resumeOpts, observer, setStepContextSlots) => {
1547
+ const makeAgentResumeHandle = (resp, sId, iId, resumeOpts, observer, onObserverError, telemetrySlots) => {
1485
1548
  let _stopped = false;
1486
1549
  const abortController = new AbortController();
1487
1550
  const flag = {
@@ -1556,7 +1619,8 @@ function createAgent(id, h, slots, agentOptions) {
1556
1619
  ...resumeOpts?.onStoreError !== void 0 ? { onStoreError: resumeOpts.onStoreError } : {},
1557
1620
  ...leaseRef !== void 0 ? { leaseRef, claimTtlMs: claimOptions.ttlMs } : {},
1558
1621
  ...observer !== void 0 ? { observer } : {},
1559
- ...setStepContextSlots !== void 0 && setStepContextSlots.length > 0 ? { setStepContextSlots } : {}
1622
+ ...onObserverError !== void 0 ? { onObserverError } : {},
1623
+ ...telemetrySlots !== void 0 && telemetrySlots.length > 0 ? { telemetrySlots } : {}
1560
1624
  }
1561
1625
  );
1562
1626
  if (r.signal === "$interrupt") interruptPendingSessions.add(sId);
@@ -1652,17 +1716,11 @@ function createAgent(id, h, slots, agentOptions) {
1652
1716
  const events = extractRunEvents(resources);
1653
1717
  const listeners = extractRunListeners(resources);
1654
1718
  const observer = extractRunObserver(resources);
1655
- for (const slot of [...agentInternals.resolvedProviders.values(), ...Object.values(runtimeSlots)]) {
1656
- if (slot !== null && typeof slot === "object" && typeof slot["bindObserver"] === "function") {
1657
- slot.bindObserver(observer ?? NOOP_OBSERVER);
1658
- }
1659
- }
1660
- const setStepContextSlots = [];
1661
- for (const slot of [...agentInternals.resolvedProviders.values(), ...Object.values(runtimeSlots)]) {
1662
- if (slot !== null && typeof slot === "object" && typeof slot["setStepContext"] === "function") {
1663
- setStepContextSlots.push(slot);
1664
- }
1665
- }
1719
+ const onObserverError = extractRunErrorSink(resources);
1720
+ const telemetrySlots = buildTelemetrySlots([
1721
+ ...agentInternals.resolvedProviders.entries(),
1722
+ ...Object.entries(runtimeSlots)
1723
+ ]);
1666
1724
  const parentRunId = typeof resources["parentRunId"] === "string" ? resources["parentRunId"] : void 0;
1667
1725
  const options = {
1668
1726
  shouldStop: () => stopFlag.stopped,
@@ -1678,9 +1736,10 @@ function createAgent(id, h, slots, agentOptions) {
1678
1736
  ...events.onInterrupt !== void 0 ? { onInterrupt: events.onInterrupt } : {},
1679
1737
  ...Object.keys(listeners).length > 0 ? { listeners } : {},
1680
1738
  ...observer !== void 0 ? { observer } : {},
1739
+ ...onObserverError !== void 0 ? { onObserverError } : {},
1681
1740
  ...agentOptions?.instanceId !== void 0 ? { instanceId: agentOptions.instanceId } : {},
1682
1741
  ...parentRunId !== void 0 ? { parentRunId } : {},
1683
- ...setStepContextSlots.length > 0 ? { setStepContextSlots } : {}
1742
+ ...telemetrySlots.length > 0 ? { telemetrySlots } : {}
1684
1743
  };
1685
1744
  let lastResult = null;
1686
1745
  const execution = (async () => {
@@ -1797,7 +1856,8 @@ function createAgent(id, h, slots, agentOptions) {
1797
1856
  ...events.onInterrupt !== void 0 ? { onInterrupt: events.onInterrupt } : {},
1798
1857
  ...Object.keys(listeners).length > 0 ? { listeners } : {},
1799
1858
  ...observer !== void 0 ? { observer } : {},
1800
- ...setStepContextSlots.length > 0 ? { setStepContextSlots } : {}
1859
+ ...onObserverError !== void 0 ? { onObserverError } : {},
1860
+ ...telemetrySlots.length > 0 ? { telemetrySlots } : {}
1801
1861
  }
1802
1862
  );
1803
1863
  if (r2.signal === "$interrupt") interruptPendingSessions.add(sessionId);
@@ -1829,7 +1889,8 @@ function createAgent(id, h, slots, agentOptions) {
1829
1889
  ...events.onInterrupt !== void 0 ? { onInterrupt: events.onInterrupt } : {},
1830
1890
  ...Object.keys(listeners).length > 0 ? { listeners } : {},
1831
1891
  ...observer !== void 0 ? { observer } : {},
1832
- ...setStepContextSlots.length > 0 ? { setStepContextSlots } : {}
1892
+ ...onObserverError !== void 0 ? { onObserverError } : {},
1893
+ ...telemetrySlots.length > 0 ? { telemetrySlots } : {}
1833
1894
  }
1834
1895
  );
1835
1896
  if (r.signal === "$interrupt") interruptPendingSessions.add(sessionId);
@@ -1858,51 +1919,15 @@ function createAgent(id, h, slots, agentOptions) {
1858
1919
  const events = extractRunEvents(resources ?? {});
1859
1920
  const resumeOpts = events.onStoreError !== void 0 ? { onStoreError: events.onStoreError } : void 0;
1860
1921
  const observer = extractRunObserver(resources ?? {});
1861
- for (const slot of agentInternals.resolvedProviders.values()) {
1862
- if (slot !== null && typeof slot === "object" && typeof slot["bindObserver"] === "function") {
1863
- slot.bindObserver(observer ?? NOOP_OBSERVER);
1864
- }
1865
- }
1866
- const setStepContextSlots = [];
1867
- for (const slot of agentInternals.resolvedProviders.values()) {
1868
- if (slot !== null && typeof slot === "object" && typeof slot["setStepContext"] === "function") {
1869
- setStepContextSlots.push(slot);
1870
- }
1871
- }
1872
- return makeAgentResumeHandle(response, sessionId, interruptId, resumeOpts, observer ?? void 0, setStepContextSlots.length > 0 ? setStepContextSlots : void 0);
1922
+ const onObserverError = extractRunErrorSink(resources ?? {});
1923
+ const telemetrySlots = buildTelemetrySlots(agentInternals.resolvedProviders.entries());
1924
+ return makeAgentResumeHandle(response, sessionId, interruptId, resumeOpts, observer ?? void 0, onObserverError ?? void 0, telemetrySlots.length > 0 ? telemetrySlots : void 0);
1873
1925
  },
1874
1926
  status: (sessionId) => querySessionPhase(capturedStore, id, sessionId),
1875
1927
  [_agentInternals]: agentInternals
1876
1928
  };
1877
1929
  return agent;
1878
1930
  }
1879
-
1880
- // src/agent/observer.ts
1881
- function composeObservers(...observers) {
1882
- return {
1883
- onRunStart: (ctx) => {
1884
- for (const o of observers) o.onRunStart?.(ctx);
1885
- },
1886
- onRunEnd: (ctx, event) => {
1887
- for (const o of observers) o.onRunEnd?.(ctx, event);
1888
- },
1889
- onStepStart: (ctx) => {
1890
- for (const o of observers) o.onStepStart?.(ctx);
1891
- },
1892
- onStepEnd: (ctx, event) => {
1893
- for (const o of observers) o.onStepEnd?.(ctx, event);
1894
- },
1895
- onStepError: (ctx, event) => {
1896
- for (const o of observers) o.onStepError?.(ctx, event);
1897
- },
1898
- onInterrupt: (ctx, event) => {
1899
- for (const o of observers) o.onInterrupt?.(ctx, event);
1900
- },
1901
- onEvent: (ctx, type, payload) => {
1902
- for (const o of observers) o.onEvent?.(ctx, type, payload);
1903
- }
1904
- };
1905
- }
1906
1931
  export {
1907
1932
  LeaseExpiredError,
1908
1933
  LoopNotDefinedError,