@noetaris/harness 0.9.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 +64 -11
- package/dist/index.d.ts +70 -16
- package/dist/index.js +303 -198
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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:
|
|
@@ -157,11 +204,12 @@ const resumed = agent.resume(response, sessionId, interruptId)
|
|
|
157
204
|
```
|
|
158
205
|
|
|
159
206
|
`agent.resume()` takes an optional fourth argument with these reserved resources (a subset of
|
|
160
|
-
`run()`'s): `observer`, `onObserverError`, `events: { onStoreError }`,
|
|
161
|
-
for a sub agent resumed from a step)
|
|
207
|
+
`run()`'s): `observer`, `onObserverError`, `events: { onStoreError }`, `parentRunId` (the caller's run id,
|
|
208
|
+
for a sub agent resumed from a step), and `claimOptions`. `parentRunId` applies to that resumed run only: calling
|
|
162
209
|
`resume()` again on the handle it returns keeps the other resources but not `parentRunId`, which
|
|
163
210
|
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
|
|
211
|
+
`instanceId` in the resumed run's `metadata` and, when the store supports claims, claim the
|
|
212
|
+
session for the resumed run; a `resume()` on the returned handle reuses the same claim TTL.
|
|
165
213
|
|
|
166
214
|
**Resume replays the interrupted step from the top.** On resume, the step that called
|
|
167
215
|
`ctx.interrupt()` runs again from its first line; each `ctx.interrupt()` call it reaches
|
|
@@ -330,9 +378,13 @@ Some runs continue an earlier one. Their `RunContext` says which, so an observer
|
|
|
330
378
|
|---|---|
|
|
331
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 |
|
|
332
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.
|
|
333
385
|
|
|
334
|
-
|
|
335
|
-
|
|
386
|
+
Every context of a turn's runs (fork branches included) also carries `requestId` when the turn
|
|
387
|
+
has one.
|
|
336
388
|
|
|
337
389
|
`carrier` is what the earlier run's observer returned from `carry(ctx)`: core calls `carry` once,
|
|
338
390
|
right after `onRunStart`, keeps its string values, and saves them as `StoredRun.carrier` (or on the
|
|
@@ -410,14 +462,14 @@ The harness never trusts a wrapper. `fn` always runs exactly once, and the calle
|
|
|
410
462
|
| Export | Description |
|
|
411
463
|
|---|---|
|
|
412
464
|
| `createHarness<Ctx>()(schema)` | Creates a harness. Fixes `Ctx`, infers `State` from schema. |
|
|
413
|
-
| `createAgent(id, h, slots)` | Assigns the agent an ID and fills `required()` slots. Returns an `Agent`. |
|
|
465
|
+
| `createAgent(id, h, slots, options?)` | Assigns the agent an ID and fills `required()` slots. `options` (`AgentOptions`) takes `instanceId`, `stores` (for `required()` store slots) and `claimOptions` (this agent's default claim TTL; per-call `claimOptions` wins, then `HARNESS_CLAIM_TTL_MS`, then 30 000 ms). Returns an `Agent`. |
|
|
414
466
|
| `field<T>(opts)` | Declares a state field with a default and optional reduce function. |
|
|
415
467
|
| `required()` | Marks a provider slot as required at `createAgent()`. |
|
|
416
468
|
| `runtime()` | Marks a provider slot as required at `agent.run()`. |
|
|
417
469
|
| `composeObservers([a, b], onObserverError?)` | Merges multiple `Observer` instances into one fan-out observer; a throwing observer is isolated and reported to `onObserverError`. |
|
|
418
|
-
| `SessionStore` | Interface for session persistence backends. |
|
|
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`). |
|
|
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`). |
|
|
421
473
|
| `RunCarrier` | `Readonly<Record<string, string>>` — the opaque map an observer's `carry` returns. |
|
|
422
474
|
| `Observer` | Interface for telemetry hooks on run and step lifecycle events (see [Observers](#observers)). |
|
|
423
475
|
| `StepSettledEvent` | Payload of `Observer.onStepSettled` — outcome, duration, update, signal, next, error, interrupt, fork. |
|
|
@@ -425,11 +477,12 @@ The harness never trusts a wrapper. `fn` always runs exactly once, and the calle
|
|
|
425
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`. |
|
|
426
478
|
| `TelemetryContext` | What `withTelemetry` receives: `observer` (the run's observer, fault-isolated), `stepContext`, and `within(type, payload, fn)` bound to that step. |
|
|
427
479
|
| `WrapScope` | What `Observer.wrap` is asked to enclose: `{ kind: 'step' }` or `{ kind: 'event', type, payload }`. |
|
|
428
|
-
| `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)). |
|
|
429
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. |
|
|
430
483
|
| `NoInterruptError` | Thrown when `resume()` is called but the session is not paused on a matching interrupt. |
|
|
431
484
|
| `SessionInFlightError` | Thrown when a session is already running. |
|
|
432
|
-
| `SessionPendingInterruptError` |
|
|
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. |
|
|
433
486
|
| `StoreLoadError` | Thrown when the session store fails to load state. |
|
|
434
487
|
|
|
435
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
|
|
@@ -1045,12 +1074,26 @@ interface AgentOptions {
|
|
|
1045
1074
|
* Keyed by the store key used in the harness (e.g. `{ session: myStore }`).
|
|
1046
1075
|
*/
|
|
1047
1076
|
readonly stores?: Record<string, unknown>;
|
|
1077
|
+
/**
|
|
1078
|
+
* Default claim options for every run and resume of this agent, used when the call passes none.
|
|
1079
|
+
* Resolution: per-call `claimOptions` → this → `HARNESS_CLAIM_TTL_MS` → 30 000 ms.
|
|
1080
|
+
* `ttlMs` must be a finite number greater than 0; otherwise `createAgent` throws `InvalidClaimOptionsError`.
|
|
1081
|
+
*/
|
|
1082
|
+
readonly claimOptions?: ClaimOptions;
|
|
1048
1083
|
}
|
|
1049
1084
|
interface Agent {
|
|
1050
1085
|
/** The agent's unique identifier, as provided to createAgent(). */
|
|
1051
1086
|
readonly id: string;
|
|
1052
1087
|
/**
|
|
1053
|
-
* Start a
|
|
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`.
|
|
1054
1097
|
*/
|
|
1055
1098
|
run(initialState: Record<string, unknown>, resources: Record<string, unknown>): RunHandle;
|
|
1056
1099
|
/**
|
|
@@ -1062,6 +1105,9 @@ interface Agent {
|
|
|
1062
1105
|
* - `parentRunId?: string` — the caller's run id, as for `run()`; set on this resumed run's
|
|
1063
1106
|
* `RunContext` only (a later `handle.resume()` on the returned handle does not inherit it)
|
|
1064
1107
|
* - `events?.onStoreError?` — raw store-error callback (unchanged from prior shape)
|
|
1108
|
+
* - `claimOptions?: ClaimOptions` — claim TTL for this resume, parsed like `agent.run()`'s; falls back
|
|
1109
|
+
* to `AgentOptions.claimOptions`, then `HARNESS_CLAIM_TTL_MS`, then 30 000 ms. A later `handle.resume()`
|
|
1110
|
+
* on the returned handle reuses it
|
|
1065
1111
|
*/
|
|
1066
1112
|
resume(response: unknown, sessionId: string, interruptId: string, resources?: Record<string, unknown>): RunHandle;
|
|
1067
1113
|
/**
|
|
@@ -1069,6 +1115,14 @@ interface Agent {
|
|
|
1069
1115
|
*/
|
|
1070
1116
|
status(sessionId: string): Promise<SessionPhase>;
|
|
1071
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
|
+
}
|
|
1072
1126
|
/**
|
|
1073
1127
|
* Instantiate an agent from a fully-configured {@link Harness}.
|
|
1074
1128
|
*
|
|
@@ -1095,17 +1149,6 @@ interface Agent {
|
|
|
1095
1149
|
*/
|
|
1096
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;
|
|
1097
1151
|
|
|
1098
|
-
/**
|
|
1099
|
-
* Thrown by {@link RunHandle.resume} or {@link Agent.resume} when the session
|
|
1100
|
-
* is not currently paused on a matching interrupt.
|
|
1101
|
-
*
|
|
1102
|
-
* Common causes: the run has not yet settled, the session does not exist,
|
|
1103
|
-
* or the supplied `interruptId` does not match the pending interrupt.
|
|
1104
|
-
*/
|
|
1105
|
-
declare class NoInterruptError extends Error {
|
|
1106
|
-
constructor();
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
1152
|
/**
|
|
1110
1153
|
* Thrown by {@link Agent.run} or {@link Agent.resume} when the given session
|
|
1111
1154
|
* is already executing a run in the same process.
|
|
@@ -1165,4 +1208,15 @@ declare class LeaseExpiredError extends Error {
|
|
|
1165
1208
|
constructor(sessionId: string);
|
|
1166
1209
|
}
|
|
1167
1210
|
|
|
1168
|
-
|
|
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 };
|