@noetaris/harness 0.10.0 → 0.11.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
@@ -140,6 +140,53 @@ h.store({
140
140
 
141
141
  The framework injects `ctx.sessionId` automatically on every run — no declaration in `Ctx` needed.
142
142
 
143
+ ### Turns and `requestId`
144
+
145
+ With a session store, a session is a conversation thread. Each `agent.run()` on it is one
146
+ **turn** (a request). A turn may span several runs — `run()`, then a `resume()` for each
147
+ interrupt — and every run of a turn is saved with the turn's `requestId`.
148
+
149
+ ```ts
150
+ // one turn per user message, on one thread
151
+ await agent.run({ messages: [msg1] }, { sessionId: 'chat-1', requestId: 'msg-1' })
152
+ await agent.run({ messages: [msg2] }, { sessionId: 'chat-1', requestId: 'msg-2' })
153
+ ```
154
+
155
+ A new turn continues from the state the last turn left: each input field is merged with its
156
+ `reduce` (or replaced when it has none), and the run starts again from the entry step.
157
+ `$error`, `$interrupt` and `$interruptResponses` start fresh for each new turn.
158
+
159
+ `requestId` is optional. It is the turn's idempotency key: a call with a `requestId` the
160
+ session already ran does **not** run it again. What `agent.run(input, { sessionId, requestId })`
161
+ does depends on the session's latest run:
162
+
163
+ | Latest run | No `requestId`, or a new one | The latest run's `requestId` (a retry) |
164
+ |---|---|---|
165
+ | none | runs from the entry step | – |
166
+ | completed | a new turn | returns its outcome, runs nothing |
167
+ | paused on an interrupt | resolves `$error` with `SessionPendingInterruptError` | returns its outcome (`signal: '$interrupt'`), runs nothing |
168
+ | paused by a stop or an unhandled error | merges the input and continues from where it paused | continues from where it paused, **without** merging the input again |
169
+
170
+ A retry of an earlier request (not the latest) is found with the store's optional
171
+ `findRequest` and its outcome is returned; it never runs again, since later turns have
172
+ moved on from it. A store without `findRequest` only recognises the latest run's
173
+ `requestId`, so a late retry of an earlier request runs as a new turn.
174
+
175
+ Notes:
176
+ - A retry returns the request's **current** outcome, not its first response: if the turn
177
+ paused and was then resumed to completion, the retry returns the completed outcome. A
178
+ retry that returns `signal: '$interrupt'` can be resumed from its handle.
179
+ - A returned (not re-run) outcome fires no observer events and saves nothing; its
180
+ `handle.runId` has no stored run and no trace.
181
+ - Without a `requestId`, every call is a new turn. For a session that serves one request,
182
+ pass `requestId: sessionId` to get "run once, then return the result".
183
+ - Duplicate calls at the same time are safe only with a store that supports claims: the
184
+ second call resolves `$error` with `SessionBusyError` and its retry then returns the
185
+ outcome. Without claims, both calls may run.
186
+ - Without a session store, `requestId` is ignored.
187
+ - `sessionId` and `requestId` must be non-empty strings when given; otherwise `agent.run()`
188
+ throws `InvalidRunResourceError`.
189
+
143
190
  ### Interrupts
144
191
 
145
192
  A run can be stopped or resumed:
@@ -331,9 +378,13 @@ Some runs continue an earlier one. Their `RunContext` says which, so an observer
331
378
  |---|---|
332
379
  | 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 |
333
380
  | the first run of a session created by `SessionStore.branch()` | `branchedFrom: { sessionId, runId, carrier? }` — the run it was branched from, in the source session |
381
+ | a new turn on a session whose last run completed (see [Turns and `requestId`](#turns-and-requestid)) | `continuedFrom: { runId, carrier? }` — the previous turn's last run |
382
+
383
+ A run has at most one of the three, only on its top-level context (never on a fork branch's). A
384
+ call that returns a request's outcome without running has none: it has no run.
334
385
 
335
- A run has at most one of the two, only on its top-level context (never on a fork branch's), and a
336
- session that already completed runs nothing, so it has neither.
386
+ Every context of a turn's runs (fork branches included) also carries `requestId` when the turn
387
+ has one.
337
388
 
338
389
  `carrier` is what the earlier run's observer returned from `carry(ctx)`: core calls `carry` once,
339
390
  right after `onRunStart`, keeps its string values, and saves them as `StoredRun.carrier` (or on the
@@ -416,9 +467,9 @@ The harness never trusts a wrapper. `fn` always runs exactly once, and the calle
416
467
  | `required()` | Marks a provider slot as required at `createAgent()`. |
417
468
  | `runtime()` | Marks a provider slot as required at `agent.run()`. |
418
469
  | `composeObservers([a, b], onObserverError?)` | Merges multiple `Observer` instances into one fan-out observer; a throwing observer is isolated and reported to `onObserverError`. |
419
- | `SessionStore` | Interface for session persistence backends. |
420
- | `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). |
421
- | `RunRef` | `{ runId, carrier? }` — points at an earlier run; used by `RunContext.resumedFrom` and `branchedFrom` (which adds `sessionId`). |
470
+ | `SessionStore` | Interface for session persistence backends. Optional extensions: `loadHistory`, `branch`, `claim` / `release` / `extendClaim`, and `findRequest` (the newest run of a request, for retries of earlier requests). |
471
+ | `StoredRun` | Type for a persisted run snapshot. Includes `agentId`, `runId`, `sessionId`, `phase`, state, and optionally `requestId` (the run's turn), `carrier` (the run's `carry` result) and `branchedFrom` (on a branch seed). |
472
+ | `RunRef` | `{ runId, carrier? }` — points at an earlier run; used by `RunContext.resumedFrom`, `continuedFrom` and `branchedFrom` (which adds `sessionId`). |
422
473
  | `RunCarrier` | `Readonly<Record<string, string>>` — the opaque map an observer's `carry` returns. |
423
474
  | `Observer` | Interface for telemetry hooks on run and step lifecycle events (see [Observers](#observers)). |
424
475
  | `StepSettledEvent` | Payload of `Observer.onStepSettled` — outcome, duration, update, signal, next, error, interrupt, fork. |
@@ -426,11 +477,12 @@ The harness never trusts a wrapper. `fn` always runs exactly once, and the calle
426
477
  | `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`. |
427
478
  | `TelemetryContext` | What `withTelemetry` receives: `observer` (the run's observer, fault-isolated), `stepContext`, and `within(type, payload, fn)` bound to that step. |
428
479
  | `WrapScope` | What `Observer.wrap` is asked to enclose: `{ kind: 'step' }` or `{ kind: 'event', type, payload }`. |
429
- | `RunContext` | Context passed to run-level observer hooks — `agentId`, `sessionId`, `runId`, `parentRunId?`, `instanceId?`, `branchPath?`, `resumedFrom?`, `branchedFrom?` (see [Linking runs](#linking-runs)). |
480
+ | `RunContext` | Context passed to run-level observer hooks — `agentId`, `sessionId`, `runId`, `parentRunId?`, `instanceId?`, `branchPath?`, `resumedFrom?`, `branchedFrom?`, `continuedFrom?`, `requestId?` (see [Linking runs](#linking-runs)). |
430
481
  | `StepContext` | Context passed to step-level observer hooks — `agentId`, `sessionId`, `runId`, `stepName`, `branchPath?`. |
482
+ | `InvalidRunResourceError` | Thrown by `agent.run()` when `sessionId` or `requestId` is given but is not a non-empty string; `key` and `value` name it. |
431
483
  | `NoInterruptError` | Thrown when `resume()` is called but the session is not paused on a matching interrupt. |
432
484
  | `SessionInFlightError` | Thrown when a session is already running. |
433
- | `SessionPendingInterruptError` | Thrown when a session is paused on a pending interrupt — use `agent.resume()` instead of `agent.run()`. |
485
+ | `SessionPendingInterruptError` | `agent.run()` on a session paused on a pending interrupt — use `agent.resume()` instead. With a session store, the run resolves with `signal: '$error'` and this error in `state.$error`; without one, `agent.run()` throws it. |
434
486
  | `StoreLoadError` | Thrown when the session store fails to load state. |
435
487
 
436
488
  ## Design Principles
package/dist/index.d.ts CHANGED
@@ -133,7 +133,8 @@ interface RunContext {
133
133
  /**
134
134
  * The run this one continues: the run that paused the session (on an interrupt, a stop or
135
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`.
136
+ * top-level run only — never on a fork branch's context, never together with `branchedFrom` or
137
+ * `continuedFrom`.
137
138
  */
138
139
  readonly resumedFrom?: RunRef;
139
140
  /**
@@ -143,6 +144,18 @@ interface RunContext {
143
144
  readonly branchedFrom?: RunRef & {
144
145
  readonly sessionId: string;
145
146
  };
147
+ /**
148
+ * The previous turn's run, when this run starts a new turn on a session whose last run
149
+ * completed. Present on the top-level run only — never together with `resumedFrom` or
150
+ * `branchedFrom`.
151
+ */
152
+ readonly continuedFrom?: RunRef;
153
+ /**
154
+ * The request (turn) this run belongs to — the `requestId` passed to `agent.run()`, kept by
155
+ * every resume of that turn. Present on every context of the run, fork branches included.
156
+ * Absent for keyless turns and when the agent has no session store.
157
+ */
158
+ readonly requestId?: string;
146
159
  }
147
160
  /**
148
161
  * Identifies the agent, session, and current step for step-level observer callbacks.
@@ -321,7 +334,7 @@ interface Observer {
321
334
  * Called once per top-level run, right after `onRunStart` (never for a fork branch). Return
322
335
  * an opaque string map that lets a later run refer back to this one — e.g. the run's trace
323
336
  * 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
337
+ * (or `branchedFrom.carrier`, `continuedFrom.carrier`) to the run that continues or branches from it. Return
325
338
  * `undefined` for nothing. Only string values are kept; a throw is reported via
326
339
  * `onObserverError` (hook name `'carry'`) and counts as `undefined`.
327
340
  */
@@ -537,7 +550,7 @@ interface SessionStore {
537
550
  * no `step`, both states equal to the source run's `finalState`, and `branchedFrom` set to the
538
551
  * source session, `runId` and the source run's `carrier` (when it has one). When the source is
539
552
  * 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.
553
+ * own `runId` never ran. The seed has no `carrier` and no `requestId` of its own.
541
554
  *
542
555
  * @throws {@link BranchNotFoundError} when `runId` is not found in history.
543
556
  */
@@ -574,6 +587,16 @@ interface SessionStore {
574
587
  * Optional — omit if your store does not support claim/release.
575
588
  */
576
589
  extendClaim?(lease: Lease, options: ClaimOptions): Promise<Lease>;
590
+ /**
591
+ * The newest run of the session whose `requestId` equals `requestId`, or `null` when there is none.
592
+ *
593
+ * Core calls it only from `agent.run()` with a `requestId` that the latest run does not carry,
594
+ * so a late retry of an earlier request is replayed instead of run again. Without it, core
595
+ * dedupes against the latest run only.
596
+ *
597
+ * Optional — omit if your store keeps no history.
598
+ */
599
+ findRequest?(agentId: string, sessionId: string, requestId: string): Promise<StoredRun | null>;
577
600
  }
578
601
  /**
579
602
  * Serializable snapshot of a single agent run, written to the store after
@@ -620,6 +643,12 @@ interface StoredRun {
620
643
  readonly branchedFrom?: RunRef & {
621
644
  readonly sessionId: string;
622
645
  };
646
+ /**
647
+ * The request (turn) this run belongs to: the `requestId` passed to `agent.run()`, inherited by
648
+ * every resume of that turn. Absent for keyless turns, branch seeds and runs saved before this
649
+ * field was added.
650
+ */
651
+ readonly requestId?: string;
623
652
  }
624
653
  /**
625
654
  * Discriminated union returned by {@link Agent.status} describing the lifecycle
@@ -1056,7 +1085,15 @@ interface Agent {
1056
1085
  /** The agent's unique identifier, as provided to createAgent(). */
1057
1086
  readonly id: string;
1058
1087
  /**
1059
- * Start a new run. Returns a RunHandle synchronously before execution begins.
1088
+ * Start a run. Returns a RunHandle synchronously before execution begins.
1089
+ *
1090
+ * Reserved `resources` keys that shape the session:
1091
+ * - `sessionId?: string` — the conversation thread; a random UUID when absent
1092
+ * - `requestId?: string` — this turn and its idempotency key. With a session store, a call whose
1093
+ * `requestId` the session already ran returns that request's current outcome without running
1094
+ * it again; without one, every call is a new turn. Ignored when the agent has no session store
1095
+ *
1096
+ * Both must be non-empty strings when given; otherwise `run` throws `InvalidRunResourceError`.
1060
1097
  */
1061
1098
  run(initialState: Record<string, unknown>, resources: Record<string, unknown>): RunHandle;
1062
1099
  /**
@@ -1078,6 +1115,14 @@ interface Agent {
1078
1115
  */
1079
1116
  status(sessionId: string): Promise<SessionPhase>;
1080
1117
  }
1118
+ /** Thrown by `agent.run()` when `sessionId` or `requestId` is given but is not a non-empty string. */
1119
+ declare class InvalidRunResourceError extends Error {
1120
+ /** The resource key that was rejected. */
1121
+ readonly key: 'sessionId' | 'requestId';
1122
+ /** The rejected value, as given. */
1123
+ readonly value: unknown;
1124
+ constructor(key: 'sessionId' | 'requestId', value: unknown);
1125
+ }
1081
1126
  /**
1082
1127
  * Instantiate an agent from a fully-configured {@link Harness}.
1083
1128
  *
@@ -1104,17 +1149,6 @@ interface Agent {
1104
1149
  */
1105
1150
  declare function createAgent<Ctx, State, Req extends keyof Ctx, Run extends keyof Ctx>(id: string, h: Harness<Ctx, State, Req, Run>, slots: Pick<Ctx, Req>, agentOptions?: AgentOptions): Agent;
1106
1151
 
1107
- /**
1108
- * Thrown by {@link RunHandle.resume} or {@link Agent.resume} when the session
1109
- * is not currently paused on a matching interrupt.
1110
- *
1111
- * Common causes: the run has not yet settled, the session does not exist,
1112
- * or the supplied `interruptId` does not match the pending interrupt.
1113
- */
1114
- declare class NoInterruptError extends Error {
1115
- constructor();
1116
- }
1117
-
1118
1152
  /**
1119
1153
  * Thrown by {@link Agent.run} or {@link Agent.resume} when the given session
1120
1154
  * is already executing a run in the same process.
@@ -1174,4 +1208,15 @@ declare class LeaseExpiredError extends Error {
1174
1208
  constructor(sessionId: string);
1175
1209
  }
1176
1210
 
1177
- export { type Agent, type AgentOptions, 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 };
1211
+ /**
1212
+ * Thrown by {@link RunHandle.resume} or {@link Agent.resume} when the session
1213
+ * is not currently paused on a matching interrupt.
1214
+ *
1215
+ * Common causes: the run has not yet settled, the session does not exist,
1216
+ * or the supplied `interruptId` does not match the pending interrupt.
1217
+ */
1218
+ declare class NoInterruptError extends Error {
1219
+ constructor();
1220
+ }
1221
+
1222
+ export { type Agent, type AgentOptions, type BranchCursor, type BranchDef, type ClaimOptions, type Cursor, type DeepWithMarkers, type FieldDefinition, type ForkCursor, type ForkDef, type FrameworkState, type Harness, InvalidRunResourceError, 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
@@ -745,11 +745,13 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
745
745
  sessionId: ctx.sessionId,
746
746
  runId: typeof callbacks?.runId === "string" ? callbacks.runId : "",
747
747
  ...typeof callbacks?.parentRunId === "string" ? { parentRunId: callbacks.parentRunId } : {},
748
+ ...typeof callbacks?.requestId === "string" ? { requestId: callbacks.requestId } : {},
748
749
  ...typeof ctx.instanceId === "string" ? { instanceId: ctx.instanceId } : {},
749
750
  ...isBranch ? { branchPath } : {},
750
751
  // Branch runLoops receive the outer callbacks by spread, so the refs are dropped here.
751
752
  ...!isBranch && callbacks?.resumedFrom !== void 0 ? { resumedFrom: callbacks.resumedFrom } : {},
752
- ...!isBranch && callbacks?.branchedFrom !== void 0 ? { branchedFrom: callbacks.branchedFrom } : {}
753
+ ...!isBranch && callbacks?.branchedFrom !== void 0 ? { branchedFrom: callbacks.branchedFrom } : {},
754
+ ...!isBranch && callbacks?.continuedFrom !== void 0 ? { continuedFrom: callbacks.continuedFrom } : {}
753
755
  };
754
756
  safeInvoke("onRunStart", () => obs?.onRunStart?.(runCtx), onObserverError);
755
757
  const carrier = isBranch ? void 0 : normalizeCarrier(safeCall("carry", () => obs?.carry?.(runCtx), onObserverError));
@@ -1287,12 +1289,32 @@ async function querySessionPhase(store, agentId, sessionId) {
1287
1289
  return storedSessionToPhase(loaded);
1288
1290
  }
1289
1291
  function runRefsOf(loaded) {
1290
- if (loaded === null || loaded.phase !== "paused") return {};
1291
- if (loaded.step !== void 0) {
1292
- return { resumedFrom: { runId: loaded.runId, ...loaded.carrier !== void 0 ? { carrier: loaded.carrier } : {} } };
1293
- }
1292
+ if (loaded === null) return {};
1293
+ const ref = { runId: loaded.runId, ...loaded.carrier !== void 0 ? { carrier: loaded.carrier } : {} };
1294
+ if (loaded.phase === "completed") return { continuedFrom: ref };
1295
+ if (loaded.step !== void 0) return { resumedFrom: ref };
1294
1296
  return loaded.branchedFrom !== void 0 ? { branchedFrom: loaded.branchedFrom } : {};
1295
1297
  }
1298
+ function decideRun(args) {
1299
+ const { loaded, older, mode, requestId, sessionId } = args;
1300
+ if (loaded === null) return { action: "start" };
1301
+ const pending = loaded.phase === "paused" && collectPendingInterrupts(loaded.step, loaded.finalState).length > 0;
1302
+ if (mode === "resume") {
1303
+ return loaded.phase === "completed" ? { action: "replay", row: loaded } : { action: "continue", mergeInput: false };
1304
+ }
1305
+ if (requestId !== void 0 && loaded.requestId === requestId) {
1306
+ if (loaded.phase === "completed" || pending) return { action: "replay", row: loaded };
1307
+ return { action: "continue", mergeInput: false };
1308
+ }
1309
+ if (older !== null) return { action: "replay", row: older };
1310
+ if (loaded.phase === "completed") return { action: "new-turn" };
1311
+ if (pending) return { action: "reject", error: new SessionPendingInterruptError(sessionId) };
1312
+ return { action: "continue", mergeInput: true };
1313
+ }
1314
+ function withoutSystemFields(loaded) {
1315
+ const { $error: _error, $interrupt: _interrupt, $interruptResponses: _responses, ...finalState } = loaded.finalState;
1316
+ return { ...loaded, finalState };
1317
+ }
1296
1318
  async function runWithSession(store, agentId, sessionId, runId, graph, initialStateArg, schema, ctx, options) {
1297
1319
  const leaseRef = options?.leaseRef ?? { current: null };
1298
1320
  const keepAlive = createKeepAliveFn(leaseRef, store, options?.claimTtlMs ?? defaultClaimTtlMs());
@@ -1306,10 +1328,11 @@ async function runWithSession(store, agentId, sessionId, runId, graph, initialSt
1306
1328
  }
1307
1329
  return false;
1308
1330
  };
1331
+ const { mode = "run", requestId: callerRequestId, ...loopOptions } = options ?? {};
1309
1332
  if (store === void 0) {
1310
1333
  const state2 = initializeState(null, initialStateArg, schema);
1311
1334
  const result2 = await runLoop(graph, state2, ctx, schema, composedShouldStop, void 0, {
1312
- ...options,
1335
+ ...loopOptions,
1313
1336
  runId,
1314
1337
  ...options?.parentRunId !== void 0 ? { parentRunId: options.parentRunId } : {}
1315
1338
  });
@@ -1320,31 +1343,46 @@ async function runWithSession(store, agentId, sessionId, runId, graph, initialSt
1320
1343
  }
1321
1344
  return result2;
1322
1345
  }
1346
+ const requestId = mode === "run" ? callerRequestId : void 0;
1347
+ const fail = (error) => {
1348
+ const failState = initializeState(null, initialStateArg, schema);
1349
+ failState["$error"] = error;
1350
+ return { state: failState, signal: "$error", paused: false, cursor: null };
1351
+ };
1323
1352
  let loaded;
1353
+ let older = null;
1324
1354
  try {
1325
1355
  loaded = await store.load(agentId, sessionId);
1356
+ if (requestId !== void 0 && loaded !== null && loaded.requestId !== requestId && typeof store.findRequest === "function") {
1357
+ older = await store.findRequest(agentId, sessionId, requestId);
1358
+ }
1326
1359
  } catch (error) {
1327
1360
  const storeError = new StoreLoadError(error);
1328
1361
  options?.onStoreError?.(storeError, "load");
1329
- const failState = initializeState(null, initialStateArg, schema);
1330
- failState["$error"] = storeError;
1331
- return { state: failState, signal: "$error", paused: false, cursor: null };
1362
+ return fail(storeError);
1332
1363
  }
1333
- if (loaded !== null && loaded.phase === "completed") {
1364
+ const decision = decideRun({ loaded, older, mode, requestId, sessionId });
1365
+ if (decision.action === "reject") return fail(decision.error);
1366
+ if (decision.action === "replay") {
1367
+ const { row } = decision;
1334
1368
  return {
1335
- state: loaded.finalState,
1336
- signal: loaded.signal ?? null,
1337
- cursor: null,
1338
- paused: false
1369
+ state: row.finalState,
1370
+ signal: row.signal ?? null,
1371
+ cursor: row.step ?? null,
1372
+ paused: row.phase === "paused"
1339
1373
  };
1340
1374
  }
1341
- const state = initializeState(loaded, initialStateArg, schema);
1375
+ const segmentRequestId = mode === "run" ? requestId : loaded?.requestId;
1376
+ const base = decision.action === "new-turn" && loaded !== null ? withoutSystemFields(loaded) : loaded;
1377
+ const input = decision.action === "continue" && !decision.mergeInput ? {} : initialStateArg;
1378
+ const state = initializeState(base, input, schema);
1342
1379
  const initialStateSnapshot = { ...state };
1343
1380
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1344
1381
  const result = await runLoop(graph, state, ctx, schema, composedShouldStop, loaded?.step, {
1345
- ...options,
1382
+ ...loopOptions,
1346
1383
  runId,
1347
1384
  ...options?.parentRunId !== void 0 ? { parentRunId: options.parentRunId } : {},
1385
+ ...segmentRequestId !== void 0 ? { requestId: segmentRequestId } : {},
1348
1386
  ...runRefsOf(loaded)
1349
1387
  });
1350
1388
  if (leaseExpired) {
@@ -1370,7 +1408,8 @@ async function runWithSession(store, agentId, sessionId, runId, graph, initialSt
1370
1408
  step: result.cursor,
1371
1409
  ...result.signal !== null ? { signal: result.signal } : {},
1372
1410
  ...metadata !== void 0 ? { metadata } : {},
1373
- ...result.carrier !== void 0 ? { carrier: result.carrier } : {}
1411
+ ...result.carrier !== void 0 ? { carrier: result.carrier } : {},
1412
+ ...segmentRequestId !== void 0 ? { requestId: segmentRequestId } : {}
1374
1413
  };
1375
1414
  await store.save(agentId, sessionId, saved);
1376
1415
  } else {
@@ -1386,7 +1425,8 @@ async function runWithSession(store, agentId, sessionId, runId, graph, initialSt
1386
1425
  finalState: result.state,
1387
1426
  ...result.signal !== null ? { signal: result.signal } : {},
1388
1427
  ...metadata !== void 0 ? { metadata } : {},
1389
- ...result.carrier !== void 0 ? { carrier: result.carrier } : {}
1428
+ ...result.carrier !== void 0 ? { carrier: result.carrier } : {},
1429
+ ...segmentRequestId !== void 0 ? { requestId: segmentRequestId } : {}
1390
1430
  };
1391
1431
  await store.save(agentId, sessionId, saved);
1392
1432
  }
@@ -1510,7 +1550,8 @@ async function injectInterruptResponse(store, agentId, sessionId, interruptId, r
1510
1550
  ...loaded.signal !== void 0 ? { signal: loaded.signal } : {},
1511
1551
  ...loaded.metadata !== void 0 ? { metadata: loaded.metadata } : {},
1512
1552
  ...loaded.carrier !== void 0 ? { carrier: loaded.carrier } : {},
1513
- ...loaded.branchedFrom !== void 0 ? { branchedFrom: loaded.branchedFrom } : {}
1553
+ ...loaded.branchedFrom !== void 0 ? { branchedFrom: loaded.branchedFrom } : {},
1554
+ ...loaded.requestId !== void 0 ? { requestId: loaded.requestId } : {}
1514
1555
  };
1515
1556
  await store.save(agentId, sessionId, updated);
1516
1557
  }
@@ -1697,6 +1738,24 @@ var InvalidClaimOptionsError = class extends Error {
1697
1738
  this.ttlMs = ttlMs;
1698
1739
  }
1699
1740
  };
1741
+ var InvalidRunResourceError = class extends Error {
1742
+ /** The resource key that was rejected. */
1743
+ key;
1744
+ /** The rejected value, as given. */
1745
+ value;
1746
+ constructor(key, value) {
1747
+ super(`${key} must be a non-empty string \u2014 got ${describeValue(value)} in agent.run() resources`);
1748
+ this.name = "InvalidRunResourceError";
1749
+ this.key = key;
1750
+ this.value = value;
1751
+ }
1752
+ };
1753
+ function readIdResource(resources, key) {
1754
+ const value = resources[key];
1755
+ if (value === void 0) return void 0;
1756
+ if (typeof value !== "string" || value.length === 0) throw new InvalidRunResourceError(key, value);
1757
+ return value;
1758
+ }
1700
1759
  var _agentInternals = /* @__PURE__ */ Symbol("_agentInternals");
1701
1760
  function createAgent(id, h, slots, agentOptions) {
1702
1761
  const internals = getInternals(h);
@@ -1783,7 +1842,7 @@ function createAgent(id, h, slots, agentOptions) {
1783
1842
  runtimeKeys,
1784
1843
  requiredKeys
1785
1844
  };
1786
- const reservedRunKeys = /* @__PURE__ */ new Set(["sessionId", "signal", "events", "listeners", "observer", "onObserverError", "claimOptions", "parentRunId"]);
1845
+ const reservedRunKeys = /* @__PURE__ */ new Set(["sessionId", "requestId", "signal", "events", "listeners", "observer", "onObserverError", "claimOptions", "parentRunId"]);
1787
1846
  const inFlightSessions = /* @__PURE__ */ new Set();
1788
1847
  const interruptPendingSessions = /* @__PURE__ */ new Set();
1789
1848
  const resumeFailState = () => initializeState(null, {}, agentInternals.stateSchema);
@@ -1842,6 +1901,7 @@ function createAgent(id, h, slots, agentOptions) {
1842
1901
  agentInternals.stateSchema,
1843
1902
  agentCtx,
1844
1903
  {
1904
+ mode: "resume",
1845
1905
  shouldStop: () => flag.stopped,
1846
1906
  onBeforeStep: (n) => {
1847
1907
  ref.current = n;
@@ -1855,7 +1915,6 @@ function createAgent(id, h, slots, agentOptions) {
1855
1915
  ...parentRunId !== void 0 ? { parentRunId } : {}
1856
1916
  }
1857
1917
  );
1858
- if (r.signal === "$interrupt") interruptPendingSessions.add(sId);
1859
1918
  return { state: r.state, signal: r.signal };
1860
1919
  }
1861
1920
  });
@@ -1894,10 +1953,11 @@ function createAgent(id, h, slots, agentOptions) {
1894
1953
  throw new MissingRuntimeSlotError(key);
1895
1954
  }
1896
1955
  }
1897
- const sessionId = typeof resources["sessionId"] === "string" ? resources["sessionId"] : randomUUID();
1956
+ const sessionId = readIdResource(resources, "sessionId") ?? randomUUID();
1957
+ const requestId = readIdResource(resources, "requestId");
1898
1958
  const runId = randomUUID();
1899
1959
  if (inFlightSessions.has(sessionId)) throw new SessionInFlightError(sessionId);
1900
- if (interruptPendingSessions.has(sessionId)) throw new SessionPendingInterruptError(sessionId);
1960
+ if (capturedStore === void 0 && interruptPendingSessions.has(sessionId)) throw new SessionPendingInterruptError(sessionId);
1901
1961
  inFlightSessions.add(sessionId);
1902
1962
  let _stopped = false;
1903
1963
  const abortController = new AbortController();
@@ -1946,6 +2006,8 @@ function createAgent(id, h, slots, agentOptions) {
1946
2006
  const parentRunId = typeof resources["parentRunId"] === "string" ? resources["parentRunId"] : void 0;
1947
2007
  const claimOptions = parseClaimOptions(resources["claimOptions"], fallbackClaimOptions());
1948
2008
  const options = {
2009
+ mode: "run",
2010
+ ...requestId !== void 0 ? { requestId } : {},
1949
2011
  shouldStop: () => stopFlag.stopped,
1950
2012
  onBeforeStep: (name, state) => {
1951
2013
  stepRef.current = name;
@@ -1987,7 +2049,7 @@ function createAgent(id, h, slots, agentOptions) {
1987
2049
  ctx,
1988
2050
  { ...options, ...lease }
1989
2051
  );
1990
- if (r.signal === "$interrupt") interruptPendingSessions.add(sessionId);
2052
+ if (capturedStore === void 0 && r.signal === "$interrupt") interruptPendingSessions.add(sessionId);
1991
2053
  lastResult = { state: r.state, cursor: r.cursor, runId, ...r.carrier !== void 0 ? { carrier: r.carrier } : {} };
1992
2054
  return { state: r.state, signal: r.signal };
1993
2055
  }
@@ -2051,6 +2113,7 @@ function createAgent(id, h, slots, agentOptions) {
2051
2113
  agentInternals.stateSchema,
2052
2114
  resumeCtx,
2053
2115
  {
2116
+ mode: "resume",
2054
2117
  shouldStop: () => resumeStopFlag.stopped,
2055
2118
  onBeforeStep: (n, s) => {
2056
2119
  resumeStepRef.current = n;
@@ -2070,7 +2133,6 @@ function createAgent(id, h, slots, agentOptions) {
2070
2133
  ...lease
2071
2134
  }
2072
2135
  );
2073
- if (r2.signal === "$interrupt") interruptPendingSessions.add(sessionId);
2074
2136
  lastResult = { state: r2.state, cursor: r2.cursor, runId: resumeRunId, ...r2.carrier !== void 0 ? { carrier: r2.carrier } : {} };
2075
2137
  return { state: r2.state, signal: r2.signal };
2076
2138
  }
@@ -2144,6 +2206,7 @@ function createAgent(id, h, slots, agentOptions) {
2144
2206
  return agent;
2145
2207
  }
2146
2208
  export {
2209
+ InvalidRunResourceError,
2147
2210
  LeaseExpiredError,
2148
2211
  LoopNotDefinedError,
2149
2212
  NoInterruptError,