@noetaris/harness 0.7.0 → 0.9.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
@@ -156,6 +156,13 @@ const resumed = run.resume(response, interruptId)
156
156
  const resumed = agent.resume(response, sessionId, interruptId)
157
157
  ```
158
158
 
159
+ `agent.resume()` takes an optional fourth argument with these reserved resources (a subset of
160
+ `run()`'s): `observer`, `onObserverError`, `events: { onStoreError }`, and `parentRunId` (the caller's run id,
161
+ for a sub agent resumed from a step). `parentRunId` applies to that resumed run only: calling
162
+ `resume()` again on the handle it returns keeps the other resources but not `parentRunId`, which
163
+ is stale by then. With a store, both `agent.resume()` and `run.resume()` record the agent's
164
+ `instanceId` in the resumed run's `metadata`.
165
+
159
166
  **Resume replays the interrupted step from the top.** On resume, the step that called
160
167
  `ctx.interrupt()` runs again from its first line; each `ctx.interrupt()` call it reaches
161
168
  returns the stored response instead of pausing. State updates from the paused attempt are
@@ -255,7 +262,7 @@ throws on an adapter event never fails a step. Every hook is optional.
255
262
  | Hook | Fires |
256
263
  |---|---|
257
264
  | `onRunStart(ctx)` | once when a run starts |
258
- | `onRunEnd(ctx, { signal, durationMs })` | once when a run settles — completed, stopped (`$stopped`), paused, or rejected by a thrown error (`$error`) |
265
+ | `onRunEnd(ctx, { signal, durationMs, error? })` | once when a run settles — completed, stopped (`$stopped`), paused, or rejected by a thrown error (`$error`). `error` is set when the run ends with `$error` because of an error no `onError` step handled (paused) or a rejection; a route that returns `'$error'` itself gives no `error`, so test for `error`, not the signal |
259
266
  | `onStepStart(ctx)` | before each step |
260
267
  | `onStepEnd(ctx, { durationMs })` | after a step's `run` succeeds, before its `route` |
261
268
  | `onStepError(ctx, { error, durationMs })` | when a step's `run` throws |
@@ -263,6 +270,7 @@ throws on an adapter event never fails a step. Every hook is optional.
263
270
  | `onEvent(ctx, type, payload)` | on `ctx.emit()` and adapter events such as `llm.response` |
264
271
  | `onStepSettled(ctx, event)` | once per step, when its outcome and destination are final |
265
272
  | `wrap(ctx, scope, fn)` | around a step's `run`, and around calls made with `ctx.within` |
273
+ | `carry(ctx)` | once per top-level run, right after `onRunStart`; returns a string map to keep on the run's record (see [Linking runs](#linking-runs)) |
266
274
 
267
275
  Step hooks receive a `StepContext`: `agentId`, `sessionId`, `runId`, `stepName` and, inside a
268
276
  fork branch, `branchPath`. `runId` is the same value as `RunContext.runId`. Steps inside a branch
@@ -314,6 +322,27 @@ await createAgent('demo', h, {}).run({}, { observer: logger })
314
322
  // decide: ok end (signal done)
315
323
  ```
316
324
 
325
+ ### Linking runs
326
+
327
+ Some runs continue an earlier one. Their `RunContext` says which, so an observer can link them:
328
+
329
+ | The run is | `RunContext` field |
330
+ |---|---|
331
+ | the next run on a session that paused — on an interrupt, a stop, or an unhandled error — whether started by `agent.resume()`, `run.resume()` (with or without a store), or `agent.run()` on that session | `resumedFrom: { runId, carrier? }` — the run that paused |
332
+ | the first run of a session created by `SessionStore.branch()` | `branchedFrom: { sessionId, runId, carrier? }` — the run it was branched from, in the source session |
333
+
334
+ A run has at most one of the two, only on its top-level context (never on a fork branch's), and a
335
+ session that already completed runs nothing, so it has neither.
336
+
337
+ `carrier` is what the earlier run's observer returned from `carry(ctx)`: core calls `carry` once,
338
+ right after `onRunStart`, keeps its string values, and saves them as `StoredRun.carrier` (or on the
339
+ handle, for `run.resume()` without a store). It describes that run only — when the earlier run had
340
+ no observer with `carry`, `carrier` is absent rather than taken from an older run.
341
+ `@noetaris/harness-otel` uses this to link each run's root span to the one before it.
342
+ `composeObservers` merges the maps its observers return (on a shared key the later observer wins);
343
+ a `carry` that throws is reported to `onObserverError` with hook name `'carry'` and counts as
344
+ returning nothing.
345
+
317
346
  ### Running code inside a span
318
347
 
319
348
  Hooks are notifications: they return before the step runs, so they cannot make a span the
@@ -387,14 +416,16 @@ The harness never trusts a wrapper. `fn` always runs exactly once, and the calle
387
416
  | `runtime()` | Marks a provider slot as required at `agent.run()`. |
388
417
  | `composeObservers([a, b], onObserverError?)` | Merges multiple `Observer` instances into one fan-out observer; a throwing observer is isolated and reported to `onObserverError`. |
389
418
  | `SessionStore` | Interface for session persistence backends. |
390
- | `StoredRun` | Type for a persisted run snapshot. Includes `agentId`, `runId`, `sessionId`, `phase`, and state. |
419
+ | `StoredRun` | Type for a persisted run snapshot. Includes `agentId`, `runId`, `sessionId`, `phase`, state, and optionally `carrier` (the run's `carry` result) and `branchedFrom` (on a branch seed). |
420
+ | `RunRef` | `{ runId, carrier? }` — points at an earlier run; used by `RunContext.resumedFrom` and `branchedFrom` (which adds `sessionId`). |
421
+ | `RunCarrier` | `Readonly<Record<string, string>>` — the opaque map an observer's `carry` returns. |
391
422
  | `Observer` | Interface for telemetry hooks on run and step lifecycle events (see [Observers](#observers)). |
392
423
  | `StepSettledEvent` | Payload of `Observer.onStepSettled` — outcome, duration, update, signal, next, error, interrupt, fork. |
393
424
  | `StepSettledNext` | Where the run goes after a step settles: `step`, `end`, `pause`, or `throw`. |
394
425
  | `ObserverAware` | Interface for provider objects that want per-step telemetry: the harness calls `withTelemetry(telemetry)` once per step and installs the returned view on `ctx`. |
395
426
  | `TelemetryContext` | What `withTelemetry` receives: `observer` (the run's observer, fault-isolated), `stepContext`, and `within(type, payload, fn)` bound to that step. |
396
427
  | `WrapScope` | What `Observer.wrap` is asked to enclose: `{ kind: 'step' }` or `{ kind: 'event', type, payload }`. |
397
- | `RunContext` | Context passed to run-level observer hooks — `agentId`, `sessionId`, `runId`, `parentRunId?`, `instanceId?`, `branchPath?`. |
428
+ | `RunContext` | Context passed to run-level observer hooks — `agentId`, `sessionId`, `runId`, `parentRunId?`, `instanceId?`, `branchPath?`, `resumedFrom?`, `branchedFrom?` (see [Linking runs](#linking-runs)). |
398
429
  | `StepContext` | Context passed to step-level observer hooks — `agentId`, `sessionId`, `runId`, `stepName`, `branchPath?`. |
399
430
  | `NoInterruptError` | Thrown when `resume()` is called but the session is not paused on a matching interrupt. |
400
431
  | `SessionInFlightError` | Thrown when a session is already running. |
@@ -424,6 +455,7 @@ See [LIMITATIONS.md](LIMITATIONS.md) for known limitations.
424
455
  - [`@noetaris/harness-openai`](https://github.com/noetaris-lab/harness-openai) — OpenAI adapter
425
456
  - [`@noetaris/harness-google`](https://github.com/noetaris-lab/harness-google) — Google Gemini adapter
426
457
  - [`@noetaris/harness-otel`](https://github.com/noetaris-lab/harness-otel) — OpenTelemetry observer bridge
458
+ - [`@noetaris/harness-otel-genai`](https://github.com/noetaris-lab/harness-otel-genai) — GenAI semantic-conventions extension for the bridge
427
459
 
428
460
  ## License
429
461
 
package/dist/index.d.ts CHANGED
@@ -130,6 +130,19 @@ interface RunContext {
130
130
  * from the real top-level pair. Absent for the top-level run.
131
131
  */
132
132
  readonly branchPath?: readonly string[];
133
+ /**
134
+ * The run this one continues: the run that paused the session (on an interrupt, a stop or
135
+ * an unhandled error), with the carrier its observer produced. Present on a resumed
136
+ * top-level run only — never on a fork branch's context, never together with `branchedFrom`.
137
+ */
138
+ readonly resumedFrom?: RunRef;
139
+ /**
140
+ * The run this session was branched from (`SessionStore.branch`), in the source session.
141
+ * Present on the branched session's first top-level run only.
142
+ */
143
+ readonly branchedFrom?: RunRef & {
144
+ readonly sessionId: string;
145
+ };
133
146
  }
134
147
  /**
135
148
  * Identifies the agent, session, and current step for step-level observer callbacks.
@@ -247,12 +260,19 @@ interface Observer {
247
260
  onRunStart?: (ctx: RunContext) => void;
248
261
  /**
249
262
  * Called once when a run settles — completed, stopped, paused, or rejected by a thrown error
250
- * (a graph-definition error or a throwing callback). On a rejection `signal` is `'$error'` and
251
- * the run rejects right after, with the original error.
263
+ * (a graph-definition error or a throwing callback).
264
+ *
265
+ * When the run ends because of an error, `signal` is `'$error'` and `error` holds it: the error
266
+ * a step's `run` or `route` threw, or a failed fork's branch error, when no `onError` step
267
+ * handled it (the run resolves paused); or the original thrown value when the run rejects (it
268
+ * rejects right after). `error` is absent on every other ending. A route may also return
269
+ * `'$error'` as an ordinary signal; that ending has no `error` key, so test for `error`, not
270
+ * for the signal.
252
271
  */
253
272
  onRunEnd?: (ctx: RunContext, event: {
254
273
  signal: string;
255
274
  durationMs: number;
275
+ error?: unknown;
256
276
  }) => void;
257
277
  /** Called immediately before each step's `run` function is invoked. */
258
278
  onStepStart?: (ctx: StepContext) => void;
@@ -297,6 +317,15 @@ interface Observer {
297
317
  * Not guarded: a wrapper that never settles hangs the step it encloses.
298
318
  */
299
319
  wrap?: <T>(ctx: StepContext, scope: WrapScope, fn: () => Promise<T>) => Promise<T>;
320
+ /**
321
+ * Called once per top-level run, right after `onRunStart` (never for a fork branch). Return
322
+ * an opaque string map that lets a later run refer back to this one — e.g. the run's trace
323
+ * context. Core saves it on the run's record and hands it back as `RunContext.resumedFrom.carrier`
324
+ * (or `branchedFrom.carrier`) to the run that continues or branches from it. Return
325
+ * `undefined` for nothing. Only string values are kept; a throw is reported via
326
+ * `onObserverError` (hook name `'carry'`) and counts as `undefined`.
327
+ */
328
+ carry?: (ctx: RunContext) => Record<string, string> | undefined;
300
329
  }
301
330
  /**
302
331
  * The telemetry a resource needs to attribute an `invoke()` call to the correct
@@ -455,6 +484,18 @@ interface StoredRunMetadata {
455
484
  /** Open extension point — domain-specific fields. */
456
485
  [key: string]: unknown;
457
486
  }
487
+ /**
488
+ * Opaque string map an observer's `carry` hook returns for a run (e.g. a trace
489
+ * context). Core stores it on the run's record and hands it back to later runs; it never
490
+ * reads it.
491
+ */
492
+ type RunCarrier = Readonly<Record<string, string>>;
493
+ /** Points at one earlier run, with the carrier that run's observer produced (if any). */
494
+ interface RunRef {
495
+ readonly runId: string;
496
+ /** Absent when that run had no observer that carries, or its `carry` returned nothing. */
497
+ readonly carrier?: RunCarrier;
498
+ }
458
499
  /**
459
500
  * Persistence contract for agent sessions.
460
501
  *
@@ -492,6 +533,12 @@ interface SessionStore {
492
533
  * whose initial state equals the forked run's `finalState`.
493
534
  * Optional — omit if your store does not support branching.
494
535
  *
536
+ * Save the new session's version-0 record as a seed the next run continues: `phase: 'paused'`,
537
+ * no `step`, both states equal to the source run's `finalState`, and `branchedFrom` set to the
538
+ * source session, `runId` and the source run's `carrier` (when it has one). When the source is
539
+ * itself such a seed, copy its `branchedFrom` instead (or leave it out if it has none): a seed's
540
+ * own `runId` never ran. The seed has no `carrier` of its own.
541
+ *
495
542
  * @throws {@link BranchNotFoundError} when `runId` is not found in history.
496
543
  */
497
544
  branch?(agentId: string, sessionId: string, runId: string): Promise<string>;
@@ -560,6 +607,19 @@ interface StoredRun {
560
607
  * domain-defined. Absent for runs produced before this field was added.
561
608
  */
562
609
  readonly metadata?: StoredRunMetadata;
610
+ /**
611
+ * What the run's observer returned from `carry` (e.g. its trace context). Describes this
612
+ * run only. Absent when no observer carried anything.
613
+ */
614
+ readonly carrier?: RunCarrier;
615
+ /**
616
+ * Set by `SessionStore.branch` on the seed record of a branched session: the run it was
617
+ * branched from, in the source session. The branched session's first run reports it as
618
+ * `RunContext.branchedFrom`.
619
+ */
620
+ readonly branchedFrom?: RunRef & {
621
+ readonly sessionId: string;
622
+ };
563
623
  }
564
624
  /**
565
625
  * Discriminated union returned by {@link Agent.status} describing the lifecycle
@@ -999,6 +1059,8 @@ interface Agent {
999
1059
  *
1000
1060
  * The optional fourth argument `resources` accepts:
1001
1061
  * - `observer?: Observer` — structured telemetry for the resumed run (see Observability)
1062
+ * - `parentRunId?: string` — the caller's run id, as for `run()`; set on this resumed run's
1063
+ * `RunContext` only (a later `handle.resume()` on the returned handle does not inherit it)
1002
1064
  * - `events?.onStoreError?` — raw store-error callback (unchanged from prior shape)
1003
1065
  */
1004
1066
  resume(response: unknown, sessionId: string, interruptId: string, resources?: Record<string, unknown>): RunHandle;
@@ -1103,4 +1165,4 @@ declare class LeaseExpiredError extends Error {
1103
1165
  constructor(sessionId: string);
1104
1166
  }
1105
1167
 
1106
- 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 StepSettledEvent, type StepSettledNext, type StepState, StoreLoadError, type StoredRun, type StoredRunMetadata, type TelemetryContext, type TransitionTarget, type WrapScope, composeObservers, createAgent, createHarness, field, isForkCursor, isForkDef, isRequiredMarker, isRuntimeMarker, required, runtime };
1168
+ 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 RunCarrier, type RunContext, type RunFn, type RunRef, type RuntimeMarker, SessionBusyError, SessionInFlightError, SessionPendingInterruptError, type SessionStore, type SignalTransition, type StateFromSchema, type StepContext, type StepDef, type StepSettledEvent, type StepSettledNext, type StepState, StoreLoadError, type StoredRun, type StoredRunMetadata, type TelemetryContext, type TransitionTarget, type WrapScope, composeObservers, createAgent, createHarness, field, isForkCursor, isForkDef, isRequiredMarker, isRuntimeMarker, required, runtime };
package/dist/index.js CHANGED
@@ -510,6 +510,14 @@ function safeInvoke(hookName, call, sink) {
510
510
  reportObserverError(error, { hookName }, sink);
511
511
  }
512
512
  }
513
+ function safeCall(hookName, call, sink) {
514
+ try {
515
+ return call();
516
+ } catch (error) {
517
+ reportObserverError(error, { hookName }, sink);
518
+ return void 0;
519
+ }
520
+ }
513
521
  async function runWrapped(wrap, fn, onObserverError) {
514
522
  let p;
515
523
  const inner = () => {
@@ -556,6 +564,16 @@ function composeObservers(observers, onObserverError) {
556
564
  onStepSettled: (ctx, event) => {
557
565
  for (const o of observers) safeInvoke("onStepSettled", () => o.onStepSettled?.(ctx, event), onObserverError);
558
566
  },
567
+ // Each observer isolated on its own; later keys win.
568
+ carry: (ctx) => {
569
+ let merged;
570
+ for (const o of observers) {
571
+ const carried = safeCall("carry", () => o.carry?.(ctx), onObserverError);
572
+ if (typeof carried !== "object" || carried === null || Array.isArray(carried)) continue;
573
+ merged = Object.assign(merged ?? {}, carried);
574
+ }
575
+ return merged;
576
+ },
559
577
  // First observer outermost; each layer isolated on its own by runWrapped.
560
578
  wrap: (ctx, scope, fn) => {
561
579
  const layer = (i) => i === wrappers.length ? fn() : runWrapped((inner) => wrappers[i].wrap(ctx, scope, inner), () => layer(i + 1), onObserverError);
@@ -706,6 +724,11 @@ function stepContextOf(runCtx, stepName) {
706
724
  function asError(e) {
707
725
  return e instanceof Error ? e : new Error(String(e));
708
726
  }
727
+ function normalizeCarrier(carried) {
728
+ if (typeof carried !== "object" || carried === null || Array.isArray(carried)) return void 0;
729
+ const entries = Object.entries(carried).filter(([, v]) => typeof v === "string");
730
+ return entries.length > 0 ? Object.freeze(Object.fromEntries(entries)) : void 0;
731
+ }
709
732
  async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callbacks) {
710
733
  const { onBeforeStep, onAfterStep, onError, onComplete, onInterrupt } = callbacks ?? {};
711
734
  const obs = callbacks?.observer;
@@ -715,19 +738,27 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
715
738
  if (!("$interrupt" in state)) state.$interrupt = null;
716
739
  if (!("$interruptResponses" in state)) state.$interruptResponses = {};
717
740
  const runStart = Date.now();
741
+ const branchPath = callbacks?.branchPath;
742
+ const isBranch = branchPath !== void 0 && branchPath.length > 0;
718
743
  const runCtx = {
719
744
  agentId: ctx.agentId,
720
745
  sessionId: ctx.sessionId,
721
746
  runId: typeof callbacks?.runId === "string" ? callbacks.runId : "",
722
747
  ...typeof callbacks?.parentRunId === "string" ? { parentRunId: callbacks.parentRunId } : {},
723
748
  ...typeof ctx.instanceId === "string" ? { instanceId: ctx.instanceId } : {},
724
- ...callbacks?.branchPath !== void 0 && callbacks.branchPath.length > 0 ? { branchPath: callbacks.branchPath } : {}
749
+ ...isBranch ? { branchPath } : {},
750
+ // Branch runLoops receive the outer callbacks by spread, so the refs are dropped here.
751
+ ...!isBranch && callbacks?.resumedFrom !== void 0 ? { resumedFrom: callbacks.resumedFrom } : {},
752
+ ...!isBranch && callbacks?.branchedFrom !== void 0 ? { branchedFrom: callbacks.branchedFrom } : {}
725
753
  };
726
754
  safeInvoke("onRunStart", () => obs?.onRunStart?.(runCtx), onObserverError);
755
+ const carrier = isBranch ? void 0 : normalizeCarrier(safeCall("carry", () => obs?.carry?.(runCtx), onObserverError));
756
+ const withCarrier = (result) => carrier !== void 0 ? { ...result, carrier } : result;
727
757
  let runEnded = false;
728
- const endRun = (signal) => {
758
+ const endRun = (signal, failure) => {
729
759
  runEnded = true;
730
- safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal, durationMs: Date.now() - runStart }), onObserverError);
760
+ const event = { signal, durationMs: Date.now() - runStart, ...failure !== void 0 ? { error: failure.error } : {} };
761
+ safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, event), onObserverError);
731
762
  };
732
763
  const callCountRef = { current: 0 };
733
764
  const stepCtxRef = { current: null };
@@ -740,7 +771,7 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
740
771
  while (true) {
741
772
  if (shouldStop?.()) {
742
773
  endRun("$stopped");
743
- return { state, signal: null, cursor, paused: true };
774
+ return withCarrier({ state, signal: null, cursor, paused: true });
744
775
  }
745
776
  if (isForkCursor(cursor)) {
746
777
  const forkCursor = cursor;
@@ -764,7 +795,7 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
764
795
  graph
765
796
  );
766
797
  if (outcome.done) {
767
- return outcome.result;
798
+ return withCarrier(outcome.result);
768
799
  }
769
800
  cursor = outcome.nextCursor;
770
801
  continue;
@@ -787,7 +818,7 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
787
818
  graph
788
819
  );
789
820
  if (outcome.done) {
790
- return outcome.result;
821
+ return withCarrier(outcome.result);
791
822
  }
792
823
  cursor = outcome.nextCursor;
793
824
  continue;
@@ -833,7 +864,7 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
833
864
  safeInvoke("onInterrupt", () => obs?.onInterrupt?.(stepCtx, { prompt: e.prompt, interruptId: e.interruptId }), onObserverError);
834
865
  settle({ outcome: "interrupt", interrupt: { interruptId: e.interruptId, prompt: e.prompt }, next: { kind: "pause" } });
835
866
  endRun("$interrupt");
836
- return { state, signal: "$interrupt", cursor, paused: true };
867
+ return withCarrier({ state, signal: "$interrupt", cursor, paused: true });
837
868
  }
838
869
  state.$error = e instanceof Error ? e : new Error(String(e));
839
870
  onError?.(state.$error, cursor);
@@ -854,8 +885,8 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
854
885
  } catch (e) {
855
886
  state.$error = e instanceof Error ? e : new Error(String(e));
856
887
  settle({ outcome: "error", error: state.$error, next: { kind: "pause" } });
857
- endRun("$error");
858
- return { state, signal: "$error", cursor, paused: true };
888
+ endRun("$error", { error: state.$error });
889
+ return withCarrier({ state, signal: "$error", cursor, paused: true });
859
890
  }
860
891
  const routed = runSucceeded || step.run === void 0 ? { outcome: "ok", signal } : { outcome: "error", signal, error: state.$error };
861
892
  const transition = step.transitions.find((t) => t.signal === signal);
@@ -868,7 +899,7 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
868
899
  settle({ ...routed, next: { kind: "end" } });
869
900
  onComplete?.(state, signal);
870
901
  endRun(signal);
871
- return { state, signal, cursor: null, paused: false };
902
+ return withCarrier({ state, signal, cursor: null, paused: false });
872
903
  }
873
904
  settle({ ...routed, next: { kind: "step", name: transition.target.name } });
874
905
  cursor = transition.target.name;
@@ -878,8 +909,8 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
878
909
  cursor = graph.onError;
879
910
  } else {
880
911
  settle({ outcome: "error", error: state.$error, next: { kind: "pause" } });
881
- endRun("$error");
882
- return { state, signal: "$error", cursor, paused: true };
912
+ endRun("$error", { error: state.$error });
913
+ return withCarrier({ state, signal: "$error", cursor, paused: true });
883
914
  }
884
915
  } else {
885
916
  const next = step.next ?? implicitNextMap.get(cursor) ?? null;
@@ -893,7 +924,7 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
893
924
  }
894
925
  }
895
926
  } catch (e) {
896
- if (!runEnded) endRun("$error");
927
+ if (!runEnded) endRun("$error", { error: e });
897
928
  throw e;
898
929
  }
899
930
  }
@@ -1036,7 +1067,7 @@ async function handleFork(forkNode, priorBranches, state, ctx, schema, shouldSto
1036
1067
  return { done: false, nextCursor: graph.onError };
1037
1068
  }
1038
1069
  fireSettled(obs, stepCtx, stepStart, { outcome: "error", error: state.$error, next: { kind: "pause" } }, onObserverError);
1039
- endRun("$error");
1070
+ endRun("$error", { error: state.$error });
1040
1071
  return { done: true, result: { state, signal: "$error", cursor: forkNode.name, paused: true } };
1041
1072
  }
1042
1073
  state.$error = null;
@@ -1248,6 +1279,13 @@ async function querySessionPhase(store, agentId, sessionId) {
1248
1279
  const loaded = await store.load(agentId, sessionId);
1249
1280
  return storedSessionToPhase(loaded);
1250
1281
  }
1282
+ function runRefsOf(loaded) {
1283
+ if (loaded === null || loaded.phase !== "paused") return {};
1284
+ if (loaded.step !== void 0) {
1285
+ return { resumedFrom: { runId: loaded.runId, ...loaded.carrier !== void 0 ? { carrier: loaded.carrier } : {} } };
1286
+ }
1287
+ return loaded.branchedFrom !== void 0 ? { branchedFrom: loaded.branchedFrom } : {};
1288
+ }
1251
1289
  async function runWithSession(store, agentId, sessionId, runId, graph, initialStateArg, schema, ctx, options) {
1252
1290
  const leaseRef = options?.leaseRef ?? { current: null };
1253
1291
  const keepAlive = createKeepAliveFn(leaseRef, store, options?.claimTtlMs ?? 3e4);
@@ -1299,7 +1337,8 @@ async function runWithSession(store, agentId, sessionId, runId, graph, initialSt
1299
1337
  const result = await runLoop(graph, state, ctx, schema, composedShouldStop, loaded?.step, {
1300
1338
  ...options,
1301
1339
  runId,
1302
- ...options?.parentRunId !== void 0 ? { parentRunId: options.parentRunId } : {}
1340
+ ...options?.parentRunId !== void 0 ? { parentRunId: options.parentRunId } : {},
1341
+ ...runRefsOf(loaded)
1303
1342
  });
1304
1343
  if (leaseExpired) {
1305
1344
  const expiredError = new LeaseExpiredError(sessionId);
@@ -1323,7 +1362,8 @@ async function runWithSession(store, agentId, sessionId, runId, graph, initialSt
1323
1362
  finalState: result.state,
1324
1363
  step: result.cursor,
1325
1364
  ...result.signal !== null ? { signal: result.signal } : {},
1326
- ...metadata !== void 0 ? { metadata } : {}
1365
+ ...metadata !== void 0 ? { metadata } : {},
1366
+ ...result.carrier !== void 0 ? { carrier: result.carrier } : {}
1327
1367
  };
1328
1368
  await store.save(agentId, sessionId, saved);
1329
1369
  } else {
@@ -1338,7 +1378,8 @@ async function runWithSession(store, agentId, sessionId, runId, graph, initialSt
1338
1378
  initialState: initialStateSnapshot,
1339
1379
  finalState: result.state,
1340
1380
  ...result.signal !== null ? { signal: result.signal } : {},
1341
- ...metadata !== void 0 ? { metadata } : {}
1381
+ ...metadata !== void 0 ? { metadata } : {},
1382
+ ...result.carrier !== void 0 ? { carrier: result.carrier } : {}
1342
1383
  };
1343
1384
  await store.save(agentId, sessionId, saved);
1344
1385
  }
@@ -1415,7 +1456,10 @@ async function injectInterruptResponse(store, agentId, sessionId, interruptId, r
1415
1456
  initialState: loaded.initialState,
1416
1457
  finalState: result.state,
1417
1458
  ...result.cursor !== void 0 ? { step: result.cursor } : {},
1418
- ...loaded.signal !== void 0 ? { signal: loaded.signal } : {}
1459
+ ...loaded.signal !== void 0 ? { signal: loaded.signal } : {},
1460
+ ...loaded.metadata !== void 0 ? { metadata: loaded.metadata } : {},
1461
+ ...loaded.carrier !== void 0 ? { carrier: loaded.carrier } : {},
1462
+ ...loaded.branchedFrom !== void 0 ? { branchedFrom: loaded.branchedFrom } : {}
1419
1463
  };
1420
1464
  await store.save(agentId, sessionId, updated);
1421
1465
  }
@@ -1660,7 +1704,8 @@ function createAgent(id, h, slots, agentOptions) {
1660
1704
  const reservedRunKeys = /* @__PURE__ */ new Set(["sessionId", "signal", "events", "listeners", "observer", "onObserverError", "claimOptions", "parentRunId"]);
1661
1705
  const inFlightSessions = /* @__PURE__ */ new Set();
1662
1706
  const interruptPendingSessions = /* @__PURE__ */ new Set();
1663
- const makeAgentResumeHandle = (resp, sId, iId, resumeOpts, observer, onObserverError, telemetrySlots) => {
1707
+ const makeAgentResumeHandle = (resp, sId, iId, resumeResources = {}) => {
1708
+ const { onStoreError, observer, onObserverError, telemetrySlots, parentRunId } = resumeResources;
1664
1709
  let _stopped = false;
1665
1710
  const abortController = new AbortController();
1666
1711
  const flag = {
@@ -1685,14 +1730,14 @@ function createAgent(id, h, slots, agentOptions) {
1685
1730
  lease = await capturedStore.claim(id, sId, claimOptions);
1686
1731
  } catch (claimError) {
1687
1732
  const loadError = new StoreLoadError(claimError);
1688
- resumeOpts?.onStoreError?.(loadError, "claim");
1733
+ onStoreError?.(loadError, "claim");
1689
1734
  const failState = initializeState(null, {}, agentInternals.stateSchema);
1690
1735
  failState["$error"] = loadError;
1691
1736
  return { state: failState, signal: "$error" };
1692
1737
  }
1693
1738
  if (lease === null) {
1694
1739
  const busyError = new SessionBusyError(sId);
1695
- resumeOpts?.onStoreError?.(busyError, "claim");
1740
+ onStoreError?.(busyError, "claim");
1696
1741
  const failState = initializeState(null, {}, agentInternals.stateSchema);
1697
1742
  failState["$error"] = busyError;
1698
1743
  return { state: failState, signal: "$error" };
@@ -1706,7 +1751,7 @@ function createAgent(id, h, slots, agentOptions) {
1706
1751
  } catch (injectError) {
1707
1752
  if (injectError instanceof NoInterruptError) throw injectError;
1708
1753
  const storeError = new StoreLoadError(injectError);
1709
- resumeOpts?.onStoreError?.(storeError, "claim");
1754
+ onStoreError?.(storeError, "claim");
1710
1755
  const failState = initializeState(null, {}, agentInternals.stateSchema);
1711
1756
  failState["$error"] = storeError;
1712
1757
  return { state: failState, signal: "$error" };
@@ -1716,7 +1761,8 @@ function createAgent(id, h, slots, agentOptions) {
1716
1761
  agentId: id,
1717
1762
  sessionId: sId,
1718
1763
  runId: rId,
1719
- signal: abortController.signal
1764
+ signal: abortController.signal,
1765
+ ...agentOptions?.instanceId !== void 0 ? { instanceId: agentOptions.instanceId } : {}
1720
1766
  };
1721
1767
  const r = await runWithSession(
1722
1768
  capturedStore,
@@ -1732,11 +1778,13 @@ function createAgent(id, h, slots, agentOptions) {
1732
1778
  onBeforeStep: (n) => {
1733
1779
  ref.current = n;
1734
1780
  },
1735
- ...resumeOpts?.onStoreError !== void 0 ? { onStoreError: resumeOpts.onStoreError } : {},
1781
+ ...onStoreError !== void 0 ? { onStoreError } : {},
1736
1782
  ...leaseRef !== void 0 ? { leaseRef, claimTtlMs: claimOptions.ttlMs } : {},
1737
1783
  ...observer !== void 0 ? { observer } : {},
1738
1784
  ...onObserverError !== void 0 ? { onObserverError } : {},
1739
- ...telemetrySlots !== void 0 && telemetrySlots.length > 0 ? { telemetrySlots } : {}
1785
+ ...telemetrySlots !== void 0 && telemetrySlots.length > 0 ? { telemetrySlots } : {},
1786
+ ...agentOptions?.instanceId !== void 0 ? { instanceId: agentOptions.instanceId } : {},
1787
+ ...parentRunId !== void 0 ? { parentRunId } : {}
1740
1788
  }
1741
1789
  );
1742
1790
  if (r.signal === "$interrupt") interruptPendingSessions.add(sId);
@@ -1764,7 +1812,8 @@ function createAgent(id, h, slots, agentOptions) {
1764
1812
  if (inFlightSessions.has(sId)) throw new SessionInFlightError(sId);
1765
1813
  interruptPendingSessions.delete(sId);
1766
1814
  inFlightSessions.add(sId);
1767
- return makeAgentResumeHandle(r, sId, i);
1815
+ const { parentRunId: _staleParentRunId, ...inherited } = resumeResources;
1816
+ return makeAgentResumeHandle(r, sId, i, inherited);
1768
1817
  };
1769
1818
  return createRunHandle(sId, rId, exec, flag, ref, resumeFn);
1770
1819
  };
@@ -1900,7 +1949,7 @@ function createAgent(id, h, slots, agentOptions) {
1900
1949
  }
1901
1950
  );
1902
1951
  if (r.signal === "$interrupt") interruptPendingSessions.add(sessionId);
1903
- lastResult = { state: r.state, cursor: r.cursor };
1952
+ lastResult = { state: r.state, cursor: r.cursor, runId, ...r.carrier !== void 0 ? { carrier: r.carrier } : {} };
1904
1953
  return { state: r.state, signal: r.signal };
1905
1954
  } catch (error) {
1906
1955
  if (error instanceof LeaseExpiredError) {
@@ -1973,11 +2022,12 @@ function createAgent(id, h, slots, agentOptions) {
1973
2022
  ...Object.keys(listeners).length > 0 ? { listeners } : {},
1974
2023
  ...observer !== void 0 ? { observer } : {},
1975
2024
  ...onObserverError !== void 0 ? { onObserverError } : {},
1976
- ...telemetrySlots.length > 0 ? { telemetrySlots } : {}
2025
+ ...telemetrySlots.length > 0 ? { telemetrySlots } : {},
2026
+ ...agentOptions?.instanceId !== void 0 ? { instanceId: agentOptions.instanceId } : {}
1977
2027
  }
1978
2028
  );
1979
2029
  if (r2.signal === "$interrupt") interruptPendingSessions.add(sessionId);
1980
- lastResult = { state: r2.state, cursor: r2.cursor };
2030
+ lastResult = { state: r2.state, cursor: r2.cursor, runId: resumeRunId, ...r2.carrier !== void 0 ? { carrier: r2.carrier } : {} };
1981
2031
  return { state: r2.state, signal: r2.signal };
1982
2032
  }
1983
2033
  const prev = lastResult;
@@ -1995,6 +2045,7 @@ function createAgent(id, h, slots, agentOptions) {
1995
2045
  cursor,
1996
2046
  {
1997
2047
  runId: resumeRunId,
2048
+ resumedFrom: { runId: prev.runId, ...prev.carrier !== void 0 ? { carrier: prev.carrier } : {} },
1998
2049
  onBeforeStep: (n, s) => {
1999
2050
  resumeStepRef.current = n;
2000
2051
  events.onBeforeStep?.(n, s);
@@ -2010,7 +2061,7 @@ function createAgent(id, h, slots, agentOptions) {
2010
2061
  }
2011
2062
  );
2012
2063
  if (r.signal === "$interrupt") interruptPendingSessions.add(sessionId);
2013
- lastResult = { state: r.state, cursor: r.cursor };
2064
+ lastResult = { state: r.state, cursor: r.cursor, runId: resumeRunId, ...r.carrier !== void 0 ? { carrier: r.carrier } : {} };
2014
2065
  return { state: r.state, signal: r.signal };
2015
2066
  } catch (error) {
2016
2067
  if (error instanceof LeaseExpiredError) {
@@ -2033,11 +2084,17 @@ function createAgent(id, h, slots, agentOptions) {
2033
2084
  interruptPendingSessions.delete(sessionId);
2034
2085
  inFlightSessions.add(sessionId);
2035
2086
  const events = extractRunEvents(resources ?? {});
2036
- const resumeOpts = events.onStoreError !== void 0 ? { onStoreError: events.onStoreError } : void 0;
2037
2087
  const observer = extractRunObserver(resources ?? {});
2038
2088
  const onObserverError = extractRunErrorSink(resources ?? {});
2039
2089
  const telemetrySlots = buildTelemetrySlots(agentInternals.resolvedProviders.entries());
2040
- return makeAgentResumeHandle(response, sessionId, interruptId, resumeOpts, observer ?? void 0, onObserverError ?? void 0, telemetrySlots.length > 0 ? telemetrySlots : void 0);
2090
+ const parentRunId = resources?.["parentRunId"];
2091
+ return makeAgentResumeHandle(response, sessionId, interruptId, {
2092
+ ...events.onStoreError !== void 0 ? { onStoreError: events.onStoreError } : {},
2093
+ ...observer !== void 0 ? { observer } : {},
2094
+ ...onObserverError !== void 0 ? { onObserverError } : {},
2095
+ ...telemetrySlots.length > 0 ? { telemetrySlots } : {},
2096
+ ...typeof parentRunId === "string" ? { parentRunId } : {}
2097
+ });
2041
2098
  },
2042
2099
  status: (sessionId) => querySessionPhase(capturedStore, id, sessionId),
2043
2100
  [_agentInternals]: agentInternals