@noetaris/harness 0.11.0 → 0.13.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 +215 -10
- package/dist/index.d.ts +303 -11
- package/dist/index.js +1035 -300
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,13 +29,15 @@ interface Ctx {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// 2. Create the harness — fixes Ctx and infers State from the field schema
|
|
32
|
-
const
|
|
32
|
+
const base = createHarness<Ctx>()({
|
|
33
33
|
messages: field<string[]>({ default: () => [], reduce: (a, b) => [...a, ...b] }),
|
|
34
34
|
toolCalls: field<any[]> ({ default: () => [] }),
|
|
35
|
+
toolResults: field<any[]>({ default: () => [] }),
|
|
35
36
|
})
|
|
36
37
|
|
|
37
|
-
// 3. Define the loop — validated immediately at call time
|
|
38
|
-
|
|
38
|
+
// 3. Define the loop — validated immediately at call time.
|
|
39
|
+
// Each builder call returns a new harness: keep the result.
|
|
40
|
+
const withLoop = base.loop(l =>
|
|
39
41
|
l.start()
|
|
40
42
|
.step('think', {
|
|
41
43
|
run: async (state, ctx) => {
|
|
@@ -57,9 +59,10 @@ h.loop(l =>
|
|
|
57
59
|
)
|
|
58
60
|
|
|
59
61
|
// 4. Declare providers
|
|
60
|
-
h
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
const h = withLoop
|
|
63
|
+
.provide('tools', { search: new MySearchTool() }) // hard-coded
|
|
64
|
+
.provide('prompts', required()) // must be supplied at createAgent()
|
|
65
|
+
.provide('model', runtime()) // must be supplied at agent.run()
|
|
63
66
|
|
|
64
67
|
// 5. Create an agent — assigns an ID and fills required() slots
|
|
65
68
|
const agent = createAgent('my-agent', h, {
|
|
@@ -166,6 +169,7 @@ does depends on the session's latest run:
|
|
|
166
169
|
| completed | a new turn | returns its outcome, runs nothing |
|
|
167
170
|
| paused on an interrupt | resolves `$error` with `SessionPendingInterruptError` | returns its outcome (`signal: '$interrupt'`), runs nothing |
|
|
168
171
|
| 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 |
|
|
172
|
+
| a checkpoint (`'running'`: its instance died mid-run, see [Checkpoints](#checkpoints)) | merges the input and continues from the last finished step | continues after the last finished step, **without** merging the input again |
|
|
169
173
|
|
|
170
174
|
A retry of an earlier request (not the latest) is found with the store's optional
|
|
171
175
|
`findRequest` and its outcome is returned; it never runs again, since later turns have
|
|
@@ -187,6 +191,106 @@ Notes:
|
|
|
187
191
|
- `sessionId` and `requestId` must be non-empty strings when given; otherwise `agent.run()`
|
|
188
192
|
throws `InvalidRunResourceError`.
|
|
189
193
|
|
|
194
|
+
### Checkpoints
|
|
195
|
+
|
|
196
|
+
By default a session is saved only when a run settles. If the instance dies in the middle of a
|
|
197
|
+
long turn, the next run starts the whole turn again, and the side effects of the steps that
|
|
198
|
+
already finished happen again. With `checkpoint: 'step'`, the session is also saved after every
|
|
199
|
+
finished top-level step:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
const agent = createAgent('agent', h, {}, { checkpoint: 'step' }) // every run of this agent
|
|
203
|
+
agent.run(input, { sessionId, requestId, checkpoint: 'off' }) // or per call: 'off' | 'step'
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
turn ── a ── save ── b ── save ── c ──✕ crash
|
|
208
|
+
↑
|
|
209
|
+
next run ─────────┘ continues from c; a and b do not run again
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
- Each checkpoint is a new stored run with `phase: 'running'`, `step` = the next step to run,
|
|
213
|
+
and the state after the finished step. `agent.status()` reports it as
|
|
214
|
+
`{ phase: 'running', step, instanceId?, checkpointedAt }`.
|
|
215
|
+
- Running the session again continues from that step, like a stopped run (see the table above):
|
|
216
|
+
a retry with the same `requestId` does not merge its input again. With a store that supports
|
|
217
|
+
claims, another instance gets `SessionBusyError` while the owner's claim is alive, and can take
|
|
218
|
+
over once it expires.
|
|
219
|
+
- Steps run **at least once**: only the step that was running when the instance died runs
|
|
220
|
+
again, so only that step needs to be idempotent.
|
|
221
|
+
- A fork is saved as a whole: there is a checkpoint before it and after it, none inside. A crash
|
|
222
|
+
inside a fork runs the whole fork again.
|
|
223
|
+
- After each checkpoint the claim is extended. A single step that runs longer than the claim TTL
|
|
224
|
+
still needs `ctx.keepAlive()`.
|
|
225
|
+
- `onCheckpoint(ctx, { step, version, durationMs })` on an [observer](#observers) reports each
|
|
226
|
+
saved checkpoint. A checkpoint that is skipped (its state can't be copied with
|
|
227
|
+
`structuredClone`, or the store failed) is reported to `events.onStoreError` with phase
|
|
228
|
+
`'checkpoint'`, and the run goes on.
|
|
229
|
+
- When the store reports that another writer saved the session first, or that this instance's
|
|
230
|
+
claim was taken over, the run stops at once with `LeaseExpiredError` (reported with phase
|
|
231
|
+
`'claim'`) and saves nothing more. Detecting a taken-over claim needs a store that checks
|
|
232
|
+
`SaveOptions.lease` on `save()` (see [Fencing](#fencing)).
|
|
233
|
+
- Without a session store the option does nothing. Each checkpoint is one store write per step.
|
|
234
|
+
|
|
235
|
+
Limits:
|
|
236
|
+
- Nothing in core looks for runs whose instance died. The session continues only when something
|
|
237
|
+
runs it again: a retry with the same `requestId`, a queue that redelivers the request, or your
|
|
238
|
+
own sweep over sessions whose `agent.status()` is `'running'`.
|
|
239
|
+
- Every checkpoint is a stored run, and none are removed: `loadHistory()` grows by one run per
|
|
240
|
+
finished step. Filter by `phase` when you read it.
|
|
241
|
+
- `branch()` starts from the last stored run of a `runId`. It can't start from an earlier
|
|
242
|
+
checkpoint of that run.
|
|
243
|
+
- It is all top-level steps or none; you can't pick which steps save.
|
|
244
|
+
- A step has no fencing token to pass to other systems. A stale instance is stopped at its next
|
|
245
|
+
save to the session store, but what its step already wrote elsewhere stays written.
|
|
246
|
+
- If a save reached the store but the call still failed (a timeout, a dropped connection), the
|
|
247
|
+
next checkpoint conflicts with it and the run stops with `LeaseExpiredError`. The next run
|
|
248
|
+
continues from that save; steps still run at least once.
|
|
249
|
+
|
|
250
|
+
> **Upgrading.**
|
|
251
|
+
> - `StoredRun.phase` and `SessionPhase` have a new value, `'running'`. A `switch` that handles
|
|
252
|
+
> every phase needs a case for it.
|
|
253
|
+
> - `checkpoint` is now a reserved run resource. A `runtime()` slot named `checkpoint` no longer
|
|
254
|
+
> reaches `ctx`, and the value passed under that key must be `'off'` or `'step'`. Rename the slot.
|
|
255
|
+
> - `SessionStore.save` takes an optional fourth argument, `SaveOptions`. Existing stores keep
|
|
256
|
+
> working, without [fencing](#fencing).
|
|
257
|
+
> - With a store that fences, a run whose final save is rejected because it lost its claim now
|
|
258
|
+
> settles `$error` with `LeaseExpiredError`. Before, it returned its result although the store
|
|
259
|
+
> did not keep it.
|
|
260
|
+
|
|
261
|
+
### Fencing
|
|
262
|
+
|
|
263
|
+
With a store that supports claims, a run holds a lease on its session. A lease can be lost while
|
|
264
|
+
the run still works: the claim expires (a step ran longer than the claim TTL without
|
|
265
|
+
`ctx.keepAlive()`), and another instance claims the session and runs it. The first instance is
|
|
266
|
+
now stale. Without fencing, its next save would overwrite the new owner's progress.
|
|
267
|
+
|
|
268
|
+
Core already stops a run at the next step boundary once its lease has expired by the local clock.
|
|
269
|
+
Fencing covers what that check can't see: a save made before that boundary, and clocks that differ
|
|
270
|
+
between instances.
|
|
271
|
+
|
|
272
|
+
So every save a run makes while it holds a claim (each checkpoint, the final save, and writing an
|
|
273
|
+
interrupt answer on `resume()`) passes the lease as `SaveOptions.lease`. A store that fences
|
|
274
|
+
checks, in the same atomic step as the version check, that the session's claim is still that
|
|
275
|
+
lease. If not, it writes nothing and throws an error named `LeaseLostError`.
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
A (stale): save {lease A} ──► store: claim is now B's → LeaseLostError → A stops, $error
|
|
279
|
+
B (owner): save {lease B} ──► store: claim ok, version ok → saved
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
What the caller of the stale run sees:
|
|
283
|
+
- The run settles `signal: '$error'` with `LeaseExpiredError` in `state.$error`. Its `cause` is the
|
|
284
|
+
store's `LeaseLostError`. `events.onStoreError` gets the same `LeaseExpiredError` with phase
|
|
285
|
+
`'claim'`.
|
|
286
|
+
- Nothing more is saved for that run.
|
|
287
|
+
- Any other error from the final save is still only reported (phase `'persist'`), and the run
|
|
288
|
+
keeps its signal.
|
|
289
|
+
|
|
290
|
+
`PostgresSessionStore` and `RedisSessionStore` fence. `InMemorySessionStore` and
|
|
291
|
+
`LocalFileSessionStore` have no claims, so there is nothing to fence. A custom store that ignores
|
|
292
|
+
`SaveOptions` keeps only the version check.
|
|
293
|
+
|
|
190
294
|
### Interrupts
|
|
191
295
|
|
|
192
296
|
A run can be stopped or resumed:
|
|
@@ -299,14 +403,99 @@ h.loop(l =>
|
|
|
299
403
|
If a fork fails fast while a sub agent is paused, that sub agent's own session stays
|
|
300
404
|
paused in its store — clean it up or reuse it the next time the step runs.
|
|
301
405
|
|
|
406
|
+
### Debugging a run
|
|
407
|
+
|
|
408
|
+
`agent.debug(initialState, resources)` starts a run held before its first step and returns a
|
|
409
|
+
`DebugSession`. Each command runs part of the run and resolves with the next **stop**: where the
|
|
410
|
+
run is held, the step that just settled, and a copy of the state. It takes the same arguments as
|
|
411
|
+
`agent.run()` and drives the whole run chain — an interrupt is answered inside the session.
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
const dbg = agent.debug({}, { sessionId: 's1' })
|
|
415
|
+
|
|
416
|
+
let stop = await dbg.current // held before the first step: kind 'step', reason 'start'
|
|
417
|
+
stop = await dbg.next() // run one step; stop.last is the step that settled
|
|
418
|
+
if (stop.kind === 'step') console.log(stop.last?.name, stop.last?.update?.values)
|
|
419
|
+
|
|
420
|
+
dbg.setBreakpoint('review', (state) => (state.score as number) < 0.5)
|
|
421
|
+
stop = await dbg.continue() // runs until a breakpoint, an interrupt or the end
|
|
422
|
+
|
|
423
|
+
if (stop.kind === 'step') dbg.setState({ score: 0.9 }) // edit state before 'review' runs
|
|
424
|
+
stop = await dbg.continue()
|
|
425
|
+
if (stop.kind === 'interrupt') stop = await dbg.answer(true) // resume; held before the re-run
|
|
426
|
+
|
|
427
|
+
stop = await dbg.continue() // { kind: 'end', outcome: { state, signal } }
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
The session is local to this process. While it lasts, `run()`, `resume()` and `debug()` on its
|
|
431
|
+
session throw `SessionInDebugError` here; see [LIMITATIONS.md](./LIMITATIONS.md) for other processes.
|
|
432
|
+
|
|
433
|
+
**Stops** (`DebugStop`):
|
|
434
|
+
|
|
435
|
+
| `kind` | Fields | When |
|
|
436
|
+
|---|---|---|
|
|
437
|
+
| `step` | `reason`, `next` (the step about to run), `last` (the step that just settled: `name`, `outcome`, `update`, `signal`, `next`, `error`, `before`), `state`, `frame` | held at a step boundary. `reason` is `start`, `step`, `breakpoint`, `resume` (after `answer`), `fork-entry` or `branch-end` |
|
|
438
|
+
| `interrupt` | `interruptId`, `prompt`, `pending` (every interrupt the run waits on), `last`, `state`, `frame` | a step called `ctx.interrupt()` |
|
|
439
|
+
| `end` | `outcome` (`{ state, signal }`), `last` | the run completed, paused on an error, or was stopped |
|
|
440
|
+
|
|
441
|
+
`state`, `last.before` and `last.update` are `Snapshot`s: `{ values, shared }`. Each top-level key
|
|
442
|
+
is copied with `structuredClone`; a key that can't be cloned (a function, say) keeps its reference
|
|
443
|
+
and is listed in `shared`.
|
|
444
|
+
|
|
445
|
+
**Commands** (`DebugSession`):
|
|
446
|
+
|
|
447
|
+
| Command | Does |
|
|
448
|
+
|---|---|
|
|
449
|
+
| `current` | promise of the latest stop (the start stop at first) |
|
|
450
|
+
| `next()` | run exactly one step (a fork counts as one step) |
|
|
451
|
+
| `continue()` | run until a breakpoint hits, an interrupt, or the end |
|
|
452
|
+
| `setBreakpoint(step, when?)`, `clearBreakpoint(step)`, `breakpoints` | stop before `step` when `when(state)` is truthy (always without `when`); one breakpoint per step |
|
|
453
|
+
| `setState(patch)` | while held at a `step` stop: overwrite keys before the next step (no reducers; values are copied; `$` keys are refused). Observers get event `debug.setState` with `{ patch }` |
|
|
454
|
+
| `answer(response, interruptId?)` | at an `interrupt` stop: resume through the normal resume path and hold before the resumed step |
|
|
455
|
+
| `stop()` | end the session at any time; resolves with the `end` stop. At an interrupt stop the interrupt stays pending as saved |
|
|
456
|
+
| `done` | promise of the last run's outcome, as `await handle` gives it |
|
|
457
|
+
|
|
458
|
+
One command at a time: a second `next()`, `continue()` or `answer()` while one runs rejects with
|
|
459
|
+
`DebugCommandError` (`reason: 'busy'`). Other reasons: `not-held`, `not-a-fork`, `reserved-key`,
|
|
460
|
+
`ended`.
|
|
461
|
+
|
|
462
|
+
While the run is held, the session renews its claim on the session store (when the store supports
|
|
463
|
+
claims), so the lease doesn't expire during a pause. Aborting the `signal` resource ends the
|
|
464
|
+
session in any state; so does `stop()`. Step durations leave out the time held.
|
|
465
|
+
|
|
466
|
+
#### Fork branches
|
|
467
|
+
|
|
468
|
+
`next()` steps over a fork. To step inside it, call `stepInto()` when the next step is a fork:
|
|
469
|
+
every branch starts and is held at its first step, and the first branch is focused. Branches are
|
|
470
|
+
**frames**, named by their branch path (`[]` is the top level).
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
stop = await dbg.stepInto() // { frame: ['x'], reason: 'fork-entry', next: 'sx' }
|
|
474
|
+
dbg.frames // [{ frame: [], status: 'in-fork' }, { frame: ['x'], status: 'held' }, …]
|
|
475
|
+
stop = dbg.focus(['y']) // switch to another held branch
|
|
476
|
+
dbg.setState({ note: 'edited in y' }) // counts as written by branch y, so the join keeps it
|
|
477
|
+
stop = await dbg.continueAll() // release every branch; resolves with the first stop any frame makes
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
`next()` and `continue()` drive only the focused frame; other branches wait. When the driven frame
|
|
481
|
+
ends, focus moves to the next held frame (`reason: 'branch-end'`, with `ended`). Breakpoints hit in
|
|
482
|
+
any frame. If a branch fails and the fork fails fast, held siblings are released and end.
|
|
483
|
+
|
|
302
484
|
### Observers
|
|
303
485
|
|
|
304
486
|
An `Observer` receives telemetry hooks for a run. Pass it as `observer` in the resources
|
|
305
|
-
of `agent.run()` or `agent.resume()
|
|
487
|
+
of `agent.run()` or `agent.resume()`, or once for every run of an agent in
|
|
488
|
+
`createAgent(id, h, slots, { observer })`. Combine several with `composeObservers([a, b])`;
|
|
306
489
|
a hook that throws is reported to `onObserverError` (or `console.error`) and never stops
|
|
307
490
|
the run. The observer that LLM adapters call is isolated the same way, so an observer that
|
|
308
491
|
throws on an adapter event never fails a step. Every hook is optional.
|
|
309
492
|
|
|
493
|
+
An agent's observer serves `agent.run()`, `handle.resume()` and `agent.resume()`. A per-call
|
|
494
|
+
`observer` is composed on top of it, never replacing it: the agent's hooks fire first, its `wrap` is
|
|
495
|
+
outermost, and on a shared `carry` key the per-call value wins. Passing the agent's own observer per
|
|
496
|
+
call uses it once. Hook errors go to the per-call `onObserverError`, else the agent's
|
|
497
|
+
`onObserverError` option, else `console.error`.
|
|
498
|
+
|
|
310
499
|
| Hook | Fires |
|
|
311
500
|
|---|---|
|
|
312
501
|
| `onRunStart(ctx)` | once when a run starts |
|
|
@@ -319,6 +508,7 @@ throws on an adapter event never fails a step. Every hook is optional.
|
|
|
319
508
|
| `onStepSettled(ctx, event)` | once per step, when its outcome and destination are final |
|
|
320
509
|
| `wrap(ctx, scope, fn)` | around a step's `run`, and around calls made with `ctx.within` |
|
|
321
510
|
| `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)) |
|
|
511
|
+
| `onCheckpoint(ctx, { step, version, durationMs })` | after a checkpoint was saved, before `step` starts; top-level runs with `checkpoint: 'step'` only (see [Checkpoints](#checkpoints)) |
|
|
322
512
|
|
|
323
513
|
Step hooks receive a `StepContext`: `agentId`, `sessionId`, `runId`, `stepName` and, inside a
|
|
324
514
|
fork branch, `branchPath`. `runId` is the same value as `RunContext.runId`. Steps inside a branch
|
|
@@ -462,28 +652,41 @@ The harness never trusts a wrapper. `fn` always runs exactly once, and the calle
|
|
|
462
652
|
| Export | Description |
|
|
463
653
|
|---|---|
|
|
464
654
|
| `createHarness<Ctx>()(schema)` | Creates a harness. Fixes `Ctx`, infers `State` from schema. |
|
|
465
|
-
| `createAgent(id, h, slots, options?)` | Assigns the agent an ID and fills `required()` slots. `options` (`AgentOptions`) takes `instanceId`, `stores` (for `required()` store slots)
|
|
655
|
+
| `createAgent(id, h, slots, options?)` | Assigns the agent an ID and fills `required()` slots. `options` (`AgentOptions`) takes `instanceId`, `stores` (for `required()` store slots), `claimOptions` (this agent's default claim TTL; per-call `claimOptions` wins, then `HARNESS_CLAIM_TTL_MS`, then 30 000 ms), `observer` (for every run of the agent; see [Observers](#observers)), `onObserverError` (the default sink for hook errors) and `checkpoint` (`'off'` or `'step'`; see [Checkpoints](#checkpoints)). Returns an `Agent`. |
|
|
466
656
|
| `field<T>(opts)` | Declares a state field with a default and optional reduce function. |
|
|
467
657
|
| `required()` | Marks a provider slot as required at `createAgent()`. |
|
|
468
658
|
| `runtime()` | Marks a provider slot as required at `agent.run()`. |
|
|
469
659
|
| `composeObservers([a, b], onObserverError?)` | Merges multiple `Observer` instances into one fan-out observer; a throwing observer is isolated and reported to `onObserverError`. |
|
|
470
660
|
| `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
|
-
| `
|
|
661
|
+
| `SaveOptions` | Last argument of `SessionStore.save`: `{ lease? }`, the lease the caller holds. A store that supports claims rejects a save whose lease is no longer the session's claim with an error named `LeaseLostError`. |
|
|
662
|
+
| `CheckpointMode` | `'off' \| 'step'` — the `checkpoint` option and resource. |
|
|
663
|
+
| `StoredRun` | Type for a persisted run snapshot. Includes `agentId`, `runId`, `sessionId`, `phase` (`'paused'`, `'completed'` or `'running'` for a checkpoint), state, and optionally `requestId` (the run's turn), `carrier` (the run's `carry` result) and `branchedFrom` (on a branch seed). |
|
|
472
664
|
| `RunRef` | `{ runId, carrier? }` — points at an earlier run; used by `RunContext.resumedFrom`, `continuedFrom` and `branchedFrom` (which adds `sessionId`). |
|
|
473
665
|
| `RunCarrier` | `Readonly<Record<string, string>>` — the opaque map an observer's `carry` returns. |
|
|
474
666
|
| `Observer` | Interface for telemetry hooks on run and step lifecycle events (see [Observers](#observers)). |
|
|
475
667
|
| `StepSettledEvent` | Payload of `Observer.onStepSettled` — outcome, duration, update, signal, next, error, interrupt, fork. |
|
|
668
|
+
| `CheckpointEvent` | Payload of `Observer.onCheckpoint` — `step`, `version`, `durationMs`. |
|
|
476
669
|
| `StepSettledNext` | Where the run goes after a step settles: `step`, `end`, `pause`, or `throw`. |
|
|
477
670
|
| `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`. |
|
|
478
671
|
| `TelemetryContext` | What `withTelemetry` receives: `observer` (the run's observer, fault-isolated), `stepContext`, and `within(type, payload, fn)` bound to that step. |
|
|
479
672
|
| `WrapScope` | What `Observer.wrap` is asked to enclose: `{ kind: 'step' }` or `{ kind: 'event', type, payload }`. |
|
|
480
673
|
| `RunContext` | Context passed to run-level observer hooks — `agentId`, `sessionId`, `runId`, `parentRunId?`, `instanceId?`, `branchPath?`, `resumedFrom?`, `branchedFrom?`, `continuedFrom?`, `requestId?` (see [Linking runs](#linking-runs)). |
|
|
481
674
|
| `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
|
|
675
|
+
| `InvalidRunResourceError` | Thrown by `agent.run()` when `sessionId` or `requestId` is given but is not a non-empty string, and by `run()`, `debug()` and `resume()` when `checkpoint` is not `'off'` or `'step'`; `key` and `value` name it. |
|
|
483
676
|
| `NoInterruptError` | Thrown when `resume()` is called but the session is not paused on a matching interrupt. |
|
|
484
677
|
| `SessionInFlightError` | Thrown when a session is already running. |
|
|
485
678
|
| `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. |
|
|
486
679
|
| `StoreLoadError` | Thrown when the session store fails to load state. |
|
|
680
|
+
| `LeaseExpiredError` | In `state.$error` when a run lost its claim: the lease expired, or the store rejected a save because the session's claim is no longer this run's lease (see [Fencing](#fencing)). `sessionId`; `cause` holds the store's error when there is one. |
|
|
681
|
+
| `InvalidAgentOptionError` | Thrown by `createAgent()` when `observer` is not an object, `onObserverError` is not a function, or `checkpoint` is not `'off'` or `'step'`; `key` and `value` name it. |
|
|
682
|
+
| `InvalidClaimOptionsError` | Thrown by `createAgent()` when `claimOptions.ttlMs` is not a finite number greater than 0. |
|
|
683
|
+
| `DebugSession` | Returned by `agent.debug()`: a run driven step by step (see [Debugging a run](#debugging-a-run)). |
|
|
684
|
+
| `DebugStop` | Where a debug session stopped: `step`, `interrupt` or `end`. |
|
|
685
|
+
| `SettledStep`, `Snapshot` | The step that just settled at a stop, and a per-key copy of a state or update. |
|
|
686
|
+
| `DebugFrame`, `EndedFrame`, `PendingInterrupt` | A fork branch held by a debug session, a frame that ended, an interrupt the run waits on. |
|
|
687
|
+
| `DebugBreakpoint` | `{ step, when? }`. |
|
|
688
|
+
| `DebugCommandError` | A debug command that can't run now; `reason` is `busy`, `not-held`, `not-a-fork`, `reserved-key` or `ended`. |
|
|
689
|
+
| `SessionInDebugError` | `run()`, `resume()` or `debug()` on a session that is being debugged in this process. |
|
|
487
690
|
|
|
488
691
|
## Design Principles
|
|
489
692
|
|
|
@@ -508,6 +711,8 @@ See [LIMITATIONS.md](LIMITATIONS.md) for known limitations.
|
|
|
508
711
|
- [`@noetaris/harness-openai`](https://github.com/noetaris-lab/harness-openai) — OpenAI adapter
|
|
509
712
|
- [`@noetaris/harness-google`](https://github.com/noetaris-lab/harness-google) — Google Gemini adapter
|
|
510
713
|
- [`@noetaris/harness-otel`](https://github.com/noetaris-lab/harness-otel) — OpenTelemetry observer bridge
|
|
714
|
+
- [`@noetaris/harness-recorder`](https://github.com/noetaris-lab/harness-recorder) — records runs step by step and plays a finished session back
|
|
715
|
+
- [`@noetaris/harness-testing`](https://github.com/noetaris-lab/harness-testing) — `runStep`, `runRoute`, `MockObserver` for tests
|
|
511
716
|
- [`@noetaris/harness-otel-genai`](https://github.com/noetaris-lab/harness-otel-genai) — GenAI semantic-conventions extension for the bridge
|
|
512
717
|
|
|
513
718
|
## License
|