@noetaris/harness 0.1.0 → 0.3.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/dist/index.d.ts CHANGED
@@ -1,9 +1,27 @@
1
1
  declare const _fieldType: unique symbol;
2
+ /**
3
+ * Descriptor for a single state field, produced by {@link field}.
4
+ *
5
+ * - `default` — factory called once on session start to populate the field when no
6
+ * initial value is supplied.
7
+ * - `reduce` — merge function invoked on resume: `reduce(storedValue, incomingValue)`.
8
+ * When absent, incoming values overwrite stored ones on resume.
9
+ */
2
10
  type FieldDefinition<T> = {
3
11
  readonly [_fieldType]: T;
4
12
  readonly default?: () => T;
5
13
  readonly reduce?: (current: T, update: T) => T;
6
14
  };
15
+ /**
16
+ * Derives the concrete state record type from a schema object whose values are
17
+ * {@link FieldDefinition} descriptors.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const schema = { count: field<number>({ default: () => 0 }) }
22
+ * type State = StateFromSchema<typeof schema> // { count: number }
23
+ * ```
24
+ */
7
25
  type StateFromSchema<S> = {
8
26
  [K in keyof S]: S[K] extends FieldDefinition<infer T> ? T : never;
9
27
  };
@@ -11,25 +29,296 @@ type FieldOptions<T> = {
11
29
  default?: () => T;
12
30
  reduce?: (current: T, update: T) => T;
13
31
  };
32
+ /**
33
+ * Declare a typed state field with an optional default factory and optional
34
+ * reduce function.
35
+ *
36
+ * @param options.default - Factory called once at session start.
37
+ * @param options.reduce - Merge function for resume: `(stored, incoming) => merged`.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * const schema = {
42
+ * messages: field<string[]>({ default: () => [], reduce: (a, b) => [...a, ...b] }),
43
+ * }
44
+ * ```
45
+ */
14
46
  declare function field<T>(options?: FieldOptions<T>): FieldDefinition<T>;
15
47
 
48
+ /** @internal Discriminant tag for {@link RequiredMarker}. */
16
49
  declare const REQUIRED_TAG: "__noetaris_required__";
50
+ /** @internal Discriminant tag for {@link RuntimeMarker}. */
17
51
  declare const RUNTIME_TAG: "__noetaris_runtime__";
52
+ /**
53
+ * Sentinel returned by {@link required} to mark a context slot that **must** be
54
+ * supplied at `createAgent()` time (not at `agent.run()` time).
55
+ */
18
56
  type RequiredMarker = {
19
57
  readonly _tag: typeof REQUIRED_TAG;
20
58
  };
59
+ /**
60
+ * Sentinel returned by {@link runtime} to mark a context slot that **must** be
61
+ * supplied at `agent.run()` time (not at `createAgent()` time).
62
+ */
21
63
  type RuntimeMarker = {
22
64
  readonly _tag: typeof RUNTIME_TAG;
23
65
  };
66
+ /**
67
+ * Recursive variant of `T` that allows any nested position to also be a
68
+ * {@link RequiredMarker} or {@link RuntimeMarker}. Used as the `value`
69
+ * parameter type on {@link Harness.provide}.
70
+ */
24
71
  type DeepWithMarkers<T> = T extends (...args: any[]) => any ? T | RequiredMarker | RuntimeMarker : T extends object ? {
25
72
  [K in keyof T]: DeepWithMarkers<T[K]> | RequiredMarker | RuntimeMarker;
26
73
  } | RequiredMarker | RuntimeMarker : T | RequiredMarker | RuntimeMarker;
74
+ /**
75
+ * Return a {@link RequiredMarker} sentinel that instructs the harness to
76
+ * require this slot at `createAgent()` time.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * h.provide('llm', required())
81
+ * ```
82
+ */
27
83
  declare function required(): RequiredMarker;
84
+ /**
85
+ * Return a {@link RuntimeMarker} sentinel that instructs the harness to
86
+ * require this slot at `agent.run()` time instead of `createAgent()` time.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * h.provide('signal', runtime())
91
+ * ```
92
+ */
28
93
  declare function runtime(): RuntimeMarker;
94
+ /**
95
+ * Type guard — returns `true` when `value` is a {@link RequiredMarker}.
96
+ */
29
97
  declare function isRequiredMarker(value: unknown): value is RequiredMarker;
98
+ /**
99
+ * Type guard — returns `true` when `value` is a {@link RuntimeMarker}.
100
+ */
30
101
  declare function isRuntimeMarker(value: unknown): value is RuntimeMarker;
31
102
 
32
- /** Framework fields injected into every step function's state argument. */
103
+ /**
104
+ * Options passed to {@link SessionStore.claim}.
105
+ *
106
+ * `ttlMs` is the initial lease duration in milliseconds. The claim expires
107
+ * if not renewed within this window — enabling crash recovery by other instances.
108
+ */
109
+ interface ClaimOptions {
110
+ /** Lease duration in milliseconds. Must be a positive integer. */
111
+ readonly ttlMs: number;
112
+ }
113
+ /**
114
+ * A successfully acquired distributed lease on a session.
115
+ *
116
+ * Returned by {@link SessionStore.claim} when no other instance holds the
117
+ * session. The framework holds this object for the duration of the run and
118
+ * passes it to {@link SessionStore.release} and {@link SessionStore.extendClaim}.
119
+ */
120
+ interface Lease {
121
+ /**
122
+ * Wall-clock expiry timestamp (milliseconds since epoch).
123
+ * The framework checks `Date.now() >= lease.expiresAt` at each step boundary
124
+ * to detect TTL expiry without contacting the store.
125
+ */
126
+ readonly expiresAt: number;
127
+ /**
128
+ * The `agentId` this lease was issued for.
129
+ * Stored in the claim record for stuck-lease diagnosis.
130
+ */
131
+ readonly agentId: string;
132
+ /**
133
+ * The `sessionId` this lease covers.
134
+ */
135
+ readonly sessionId: string;
136
+ /**
137
+ * The `instanceId` of the holder, if provided to `createAgent()`.
138
+ * Written into the store's claim record for operational diagnostics.
139
+ * Absent when `instanceId` was not configured.
140
+ */
141
+ readonly instanceId?: string;
142
+ /**
143
+ * Store-implementation-specific token — e.g. a Redis key name, a lock token,
144
+ * or a database row ID. Opaque to the framework; passed back unmodified on
145
+ * `release` and `extendClaim` calls so the store can locate the record.
146
+ */
147
+ readonly token: unknown;
148
+ }
149
+ /**
150
+ * Operational metadata written by the framework into every {@link StoredRun}.
151
+ *
152
+ * The framework writes only `instanceId`. All other fields are reserved for
153
+ * domain-specific operational annotations (tenantId, region, requestId, etc.).
154
+ */
155
+ interface StoredRunMetadata {
156
+ /** The `instanceId` of the instance that produced this run. Absent when not configured. */
157
+ instanceId?: string;
158
+ /** Open extension point — domain-specific fields. */
159
+ [key: string]: unknown;
160
+ }
161
+ /**
162
+ * Persistence contract for agent sessions.
163
+ *
164
+ * Implementations must be safe to call concurrently for **different** sessions;
165
+ * concurrent calls for the **same** session are prevented by the harness concurrency guards.
166
+ *
167
+ * `loadHistory` and `branch` are optional extensions:
168
+ * - `loadHistory` — full run history for a session (used by branching and debugging tools).
169
+ * - `branch` — fork a session at a specific past run, returning a new session ID.
170
+ */
171
+ interface SessionStore {
172
+ /**
173
+ * Load the most recent {@link StoredRun} for the given session, or `null` if
174
+ * the session has never been saved.
175
+ */
176
+ load(agentId: string, sessionId: string): Promise<StoredRun | null>;
177
+ /**
178
+ * Persist a run record as a conditional write.
179
+ *
180
+ * Implementations must compare `run.version` against the version of the
181
+ * currently stored record before writing:
182
+ * - If the stored version equals `run.version - 1` (or there is no stored
183
+ * record and `run.version === 0`), the write succeeds.
184
+ * - Otherwise, another writer committed a newer version concurrently — the
185
+ * implementation must throw `ConcurrentModificationError`.
186
+ */
187
+ save(agentId: string, sessionId: string, run: StoredRun): Promise<void>;
188
+ /**
189
+ * Return the full ordered run history for a session, oldest first.
190
+ * Optional — omit if your store does not support history.
191
+ */
192
+ loadHistory?(agentId: string, sessionId: string): Promise<StoredRun[]>;
193
+ /**
194
+ * Fork the session at the state captured by `runId`, returning a new session ID
195
+ * whose initial state equals the forked run's `finalState`.
196
+ * Optional — omit if your store does not support branching.
197
+ *
198
+ * @throws {@link BranchNotFoundError} when `runId` is not found in history.
199
+ */
200
+ branch?(agentId: string, sessionId: string, runId: string): Promise<string>;
201
+ /**
202
+ * Attempt to acquire a distributed claim on the session before starting a run.
203
+ *
204
+ * - Returns a {@link Lease} when the claim is acquired.
205
+ * - Returns `null` when another instance already holds the claim — the framework
206
+ * throws {@link SessionBusyError} immediately without starting any LLM work.
207
+ *
208
+ * Optional — stores that do not support distributed locking omit this method.
209
+ * The framework checks for its presence before calling it.
210
+ */
211
+ claim?(agentId: string, sessionId: string, options: ClaimOptions): Promise<Lease | null>;
212
+ /**
213
+ * Release a held claim after the run settles.
214
+ *
215
+ * Called by the framework after `save()` completes (or after an error that
216
+ * prevents saving). Stores may use this to remove the lock record immediately
217
+ * rather than waiting for TTL expiry.
218
+ *
219
+ * Optional — omit if your store does not support claim/release.
220
+ */
221
+ release?(lease: Lease): Promise<void>;
222
+ /**
223
+ * Extend the TTL of an active claim.
224
+ *
225
+ * Called by `ctx.keepAlive()` (both one-shot and background interval modes).
226
+ * The store must update `expiresAt` on the claim record; the new expiry is
227
+ * `Date.now() + options.ttlMs`. The returned `Lease` object carries the updated
228
+ * `expiresAt`; the framework replaces its held reference with the returned value.
229
+ *
230
+ * Optional — omit if your store does not support claim/release.
231
+ */
232
+ extendClaim?(lease: Lease, options: ClaimOptions): Promise<Lease>;
233
+ }
234
+ /**
235
+ * Serializable snapshot of a single agent run, written to the store after
236
+ * every run settles (either `'paused'` on an interrupt or `'completed'`).
237
+ *
238
+ * - `phase: 'paused'` — the run paused on an interrupt; `step` is the step that
239
+ * issued the interrupt and `signal` is `'$interrupt'`.
240
+ * - `phase: 'completed'` — the loop exited normally; `signal` holds the exit
241
+ * signal (or is absent when the loop ended without emitting a signal).
242
+ */
243
+ interface StoredRun {
244
+ readonly agentId: string;
245
+ readonly runId: string;
246
+ readonly sessionId: string;
247
+ readonly version: number;
248
+ readonly startedAt: string;
249
+ readonly settledAt: string;
250
+ readonly phase: 'paused' | 'completed';
251
+ readonly initialState: Record<string, unknown>;
252
+ readonly finalState: Record<string, unknown>;
253
+ readonly signal?: string;
254
+ readonly step?: string;
255
+ /**
256
+ * Operational metadata written by the framework.
257
+ *
258
+ * The framework writes `instanceId` when configured; all other fields are
259
+ * domain-defined. Absent for runs produced before this field was added.
260
+ */
261
+ readonly metadata?: StoredRunMetadata;
262
+ }
263
+ /**
264
+ * Discriminated union returned by {@link Agent.status} describing the lifecycle
265
+ * phase of a session.
266
+ *
267
+ * - `'fresh'` — no run has been stored yet.
268
+ * - `'in-flight'` — a run is currently executing (in-process guard only; not
269
+ * detectable cross-process from the store alone).
270
+ * - `'paused'` — the last run settled on an interrupt; `step` identifies the
271
+ * step that issued the interrupt.
272
+ * - `'completed'` — the last run exited the loop; `signal` is the exit signal.
273
+ */
274
+ type SessionPhase = {
275
+ readonly phase: 'fresh';
276
+ } | {
277
+ readonly phase: 'in-flight';
278
+ readonly step: null;
279
+ } | {
280
+ readonly phase: 'paused';
281
+ readonly signal?: string;
282
+ readonly step: string;
283
+ } | {
284
+ readonly phase: 'completed';
285
+ readonly signal?: string;
286
+ };
287
+
288
+ /**
289
+ * Options for background renewal mode.
290
+ */
291
+ interface KeepAliveOptions {
292
+ /**
293
+ * Renewal interval in milliseconds. A background timer calls
294
+ * `store.extendClaim()` at this cadence.
295
+ * Must be less than the original claim `ttlMs` to ensure renewal
296
+ * reaches the store before expiry.
297
+ */
298
+ readonly every: number;
299
+ }
300
+ /**
301
+ * The stop function returned by `ctx.keepAlive({ every })`.
302
+ * Always call this in a `finally` block to cancel the background timer.
303
+ */
304
+ type StopFn = () => void;
305
+ /**
306
+ * The `ctx.keepAlive` function type injected into the step ctx.
307
+ *
308
+ * - Called with no arguments: one-shot TTL extension. Returns a Promise.
309
+ * - Called with `{ every: ms }`: starts a background interval renewal timer.
310
+ * Returns a `StopFn` synchronously.
311
+ */
312
+ type KeepAliveFn = ((() => Promise<void>) & ((options: KeepAliveOptions) => StopFn));
313
+
314
+ /**
315
+ * Framework-managed fields injected into every step function's `state` argument.
316
+ *
317
+ * - `$error` — the `Error` thrown by the previous step, or `null` if no error occurred.
318
+ * Only populated on the error path; non-error-aware steps never see a non-null value here.
319
+ * - `$interrupt` — set while a step is awaiting a resume response. The `response` field
320
+ * is populated after {@link Agent.resume} is called.
321
+ */
33
322
  interface FrameworkState {
34
323
  readonly $error: Error | null;
35
324
  readonly $interrupt: {
@@ -38,7 +327,10 @@ interface FrameworkState {
38
327
  readonly response?: unknown;
39
328
  } | null;
40
329
  }
41
- /** The full state type visible inside step functions: user state + framework fields. */
330
+ /**
331
+ * The complete state type visible inside {@link RunFn} step functions:
332
+ * the user-defined state `S` merged with {@link FrameworkState}.
333
+ */
42
334
  type StepState<S> = S & FrameworkState;
43
335
  /**
44
336
  * State transformer. Receives the full step state and ctx; returns a partial update
@@ -46,9 +338,13 @@ type StepState<S> = S & FrameworkState;
46
338
  * May be async.
47
339
  */
48
340
  type RunFn<S, Ctx> = (state: StepState<S>, ctx: Ctx & {
341
+ readonly agentId: string;
49
342
  readonly sessionId: string;
343
+ readonly runId: string;
344
+ readonly signal: AbortSignal;
50
345
  readonly interrupt: (prompt: unknown, id?: string) => Promise<unknown>;
51
346
  readonly emit: (name: string, payload?: unknown) => void;
347
+ readonly keepAlive: KeepAliveFn;
52
348
  }) => Promise<Partial<Omit<S, '$error' | '$interrupt'>>> | Partial<Omit<S, '$error' | '$interrupt'>>;
53
349
  /**
54
350
  * Pure signal emitter. Receives step state without $error — the route is not called on the error
@@ -80,6 +376,54 @@ type StepOptions<S, Ctx> = {
80
376
  run?: RunFn<S, Ctx>;
81
377
  route?: RouteFn<S>;
82
378
  };
379
+ /**
380
+ * Where a signal transition routes to.
381
+ * 'step' — routes to the named step.
382
+ * 'end' — exits the loop; run resolves with the emitted signal.
383
+ */
384
+ type TransitionTarget = {
385
+ readonly kind: 'step';
386
+ readonly name: string;
387
+ } | {
388
+ readonly kind: 'end';
389
+ };
390
+ /** A single `.on(signal).to(step)` or `.on(signal).end()` declaration. */
391
+ interface SignalTransition {
392
+ readonly signal: string;
393
+ readonly target: TransitionTarget;
394
+ }
395
+ /**
396
+ * Compiled step definition stored in LoopDefinition.
397
+ * run and route are stored as-is (user functions — validated structurally, not semantically).
398
+ * transitions: all .on() declarations attached to this step.
399
+ * next: explicit .next(name) target, or undefined if none declared.
400
+ * errorAware: true when the step was declared with optin: '$error' — the executor calls route
401
+ * on the error path when this is true.
402
+ */
403
+ interface StepDef {
404
+ readonly name: string;
405
+ readonly run: RunFn<unknown, unknown> | undefined;
406
+ readonly route: RouteFn<unknown> | undefined;
407
+ readonly transitions: readonly SignalTransition[];
408
+ readonly next: string | undefined;
409
+ readonly errorAware: boolean;
410
+ }
411
+ /**
412
+ * Complete loop topology captured from the builder lambda.
413
+ * Immutable snapshot — produced once, consumed by LoopValidator and (later) F6.
414
+ *
415
+ * startCalled: true if l.start() was invoked at least once.
416
+ * entryStep: name of the first .step() declared after .start() was called; undefined if .start()
417
+ * was never called or no step was declared after it.
418
+ * steps: all declared steps in declaration order.
419
+ * onError: the fallback step name from l.onError(), or undefined if not set.
420
+ */
421
+ interface LoopDefinition {
422
+ readonly startCalled: boolean;
423
+ readonly entryStep: string | undefined;
424
+ readonly steps: readonly StepDef[];
425
+ readonly onError: string | undefined;
426
+ }
83
427
  /**
84
428
  * Fluent chain returned by .on(signal). Caller must call either .to(step) or .end()
85
429
  * to complete the transition declaration.
@@ -117,72 +461,149 @@ interface LoopBuilder<S, Ctx> {
117
461
  onError(step: string): LoopBuilder<S, Ctx>;
118
462
  }
119
463
 
464
+ /**
465
+ * Immutable builder that accumulates context-slot declarations, store bindings,
466
+ * and the loop definition for an agent. Produced by {@link createHarness} and
467
+ * consumed by {@link createAgent}.
468
+ *
469
+ * The four type parameters track what has been declared so far:
470
+ * - `Ctx` — the full context shape expected by step functions.
471
+ * - `State` — the state record shape derived from the schema passed to `createHarness()`.
472
+ * - `Req` — keys declared as {@link required} (must be supplied to `createAgent`).
473
+ * - `Run` — keys declared as {@link runtime} (must be supplied to `agent.run()`).
474
+ *
475
+ * Each call returns a **new** `Harness` instance; the original is unchanged.
476
+ *
477
+ * @example
478
+ * ```ts
479
+ * const h = createHarness<Ctx>()(schema)
480
+ * .provide('llm', required())
481
+ * .provide('model', 'claude-3-5-haiku-20241022')
482
+ * .store({ session: new InMemorySessionStore() })
483
+ * .loop(l => {
484
+ * l.start().step('run', { run, route }).on('done').end()
485
+ * })
486
+ * ```
487
+ */
120
488
  type Harness<Ctx, State, Req extends keyof Ctx = never, Run extends keyof Ctx = never> = {
489
+ /**
490
+ * Declare a context slot.
491
+ *
492
+ * - Pass {@link required} to require the value at `createAgent()` time.
493
+ * - Pass {@link runtime} to require the value at `agent.run()` time.
494
+ * - Pass any concrete value (or a deep object mixing in markers) to bind it now.
495
+ */
121
496
  provide<K extends keyof Ctx>(key: K, value: RequiredMarker): Harness<Ctx, State, Req | K, Run>;
122
497
  provide<K extends keyof Ctx>(key: K, value: RuntimeMarker): Harness<Ctx, State, Req, Run | K>;
123
498
  provide<K extends keyof Ctx>(key: K, value: DeepWithMarkers<Ctx[K]>): Harness<Ctx, State, Req, Run>;
499
+ /**
500
+ * Bind session store(s). Pass `{ session: myStore }` to enable persistence and
501
+ * interrupt/resume across processes.
502
+ */
124
503
  store(stores: DeepWithMarkers<{
125
504
  session?: unknown;
126
505
  } & Record<string, unknown>>): Harness<Ctx, State, Req, Run>;
506
+ /**
507
+ * Define the execution loop using the {@link LoopBuilder} DSL.
508
+ *
509
+ * @param builder - Callback that receives a `LoopBuilder` and uses it to
510
+ * declare steps, transitions, and the entry point.
511
+ */
127
512
  loop(builder: (l: LoopBuilder<State, Ctx>) => void): Harness<Ctx, State, Req, Run>;
513
+ /**
514
+ * Return the frozen {@link LoopDefinition} captured when {@link loop} was called.
515
+ *
516
+ * @throws {LoopNotDefinedError} if `h.loop()` has not been called on this harness instance.
517
+ */
518
+ definition(): LoopDefinition;
128
519
  };
520
+ /**
521
+ * Thrown by {@link Harness.definition} when `h.loop()` has not been called yet.
522
+ */
523
+ declare class LoopNotDefinedError extends Error {
524
+ constructor();
525
+ }
526
+ /**
527
+ * Create a harness builder for the given context type.
528
+ *
529
+ * Call the returned function (optionally with a state schema) to get the
530
+ * {@link Harness} builder on which you chain `.provide()`, `.store()`, and `.loop()`.
531
+ *
532
+ * @example
533
+ * ```ts
534
+ * interface Ctx { llm: LLM; model: string }
535
+ * const schema = { messages: field<string[]>({ default: () => [] }) }
536
+ *
537
+ * const h = createHarness<Ctx>()(schema)
538
+ * ```
539
+ */
129
540
  declare function createHarness<Ctx = any>(): <S extends object = {}>(// any: allows createHarness() without an explicit Ctx type parameter
130
541
  stateSchema?: S) => Harness<Ctx, StateFromSchema<S>>;
131
542
 
132
- interface SessionStore {
133
- load(sessionId: string): Promise<StoredRun | null>;
134
- save(sessionId: string, run: StoredRun): Promise<void>;
135
- loadHistory?(sessionId: string): Promise<StoredRun[]>;
136
- branch?(sessionId: string, runId: string): Promise<string>;
137
- }
138
- interface StoredRun {
139
- readonly runId: string;
140
- readonly sessionId: string;
141
- readonly startedAt: string;
142
- readonly settledAt: string;
143
- readonly phase: 'paused' | 'completed';
144
- readonly initialState: Record<string, unknown>;
145
- readonly finalState: Record<string, unknown>;
146
- readonly signal?: string;
147
- readonly step?: string;
148
- }
149
- type SessionPhase = {
150
- readonly phase: 'fresh';
151
- } | {
152
- readonly phase: 'in-flight';
153
- readonly step: null;
154
- } | {
155
- readonly phase: 'paused';
156
- readonly signal?: string;
157
- readonly step: string;
158
- } | {
159
- readonly phase: 'completed';
160
- readonly signal?: string;
161
- };
162
-
543
+ /**
544
+ * The resolved value of a {@link RunHandle} promise: the final state and the
545
+ * signal that caused the loop to exit.
546
+ *
547
+ * `signal` is `null` when the loop exited via `.end()` without an explicit signal,
548
+ * `'$interrupt'` when paused on an interrupt, or any custom signal string returned
549
+ * by a route function.
550
+ */
163
551
  interface RunOutcome {
164
552
  readonly state: Record<string, unknown>;
165
553
  readonly signal: string | null;
166
554
  }
555
+ /**
556
+ * Handle returned synchronously by {@link Agent.run} and {@link Agent.resume}.
557
+ *
558
+ * Implements `PromiseLike<RunOutcome>` so it can be `await`-ed directly.
559
+ *
560
+ * @example
561
+ * ```ts
562
+ * const handle = agent.run({}, { llm })
563
+ * console.log(handle.sessionId) // available immediately
564
+ * const { state, signal } = await handle
565
+ * ```
566
+ */
167
567
  interface RunHandle extends PromiseLike<RunOutcome> {
168
568
  /** Cancel the in-flight run at the next safe point between steps. Idempotent. */
169
569
  stop(): void;
170
570
  /**
171
- * Provide a response to a pending ctx.interrupt() call.
172
- * Returns a new RunHandle for the resumed execution.
173
- * Stub in F8 — implemented in F9.
571
+ * Provide a response to a pending `ctx.interrupt()` call and start a new run.
572
+ * Returns a new {@link RunHandle} for the resumed execution.
573
+ *
574
+ * Throws {@link NoInterruptError} when the run has not settled on `signal: '$interrupt'`.
575
+ *
576
+ * @param response - The value to deliver as the interrupt response.
577
+ * @param interruptId - The `interruptId` from `state.$interrupt`.
174
578
  */
175
579
  resume(response: unknown, interruptId: string): RunHandle;
176
580
  /** The session identity for this run. */
177
581
  readonly sessionId: string;
582
+ /**
583
+ * Unique identifier for this specific invocation.
584
+ * One UUID per `agent.run()` or `agent.resume()` call.
585
+ */
586
+ readonly runId: string;
178
587
  /**
179
588
  * The name of the step currently executing in this process.
180
- * null before the first step runs, after execution settles, and always when inspected cross-process.
589
+ * `null` before the first step runs, after execution settles, and always
590
+ * when inspected cross-process.
181
591
  */
182
592
  readonly currentStep: string | null;
183
593
  }
184
594
 
595
+ /** Options passed to createAgent(). */
596
+ interface AgentOptions {
597
+ /**
598
+ * Stable identifier for the process or replica running this agent instance.
599
+ * Written to `StoredRun.metadata` on every save and propagated to observers
600
+ * via `RunContext.instanceId`.
601
+ */
602
+ readonly instanceId?: string;
603
+ }
185
604
  interface Agent {
605
+ /** The agent's unique identifier, as provided to createAgent(). */
606
+ readonly id: string;
186
607
  /**
187
608
  * Start a new run. Returns a RunHandle synchronously before execution begins.
188
609
  */
@@ -190,30 +611,197 @@ interface Agent {
190
611
  /**
191
612
  * Cross-process entry point for responding to a pending interrupt.
192
613
  * Returns a RunHandle synchronously; the execution promise performs the resume.
614
+ *
615
+ * The optional fourth argument `resources` accepts:
616
+ * - `observer?: Observer` — structured telemetry for the resumed run (see Observability)
617
+ * - `events?.onStoreError?` — raw store-error callback (unchanged from prior shape)
193
618
  */
194
- resume(response: unknown, sessionId: string, interruptId: string): RunHandle;
619
+ resume(response: unknown, sessionId: string, interruptId: string, resources?: Record<string, unknown>): RunHandle;
195
620
  /**
196
621
  * Query the session store for the current phase of a session.
197
622
  */
198
623
  status(sessionId: string): Promise<SessionPhase>;
199
624
  }
200
- declare function createAgent<Ctx, State, Req extends keyof Ctx, Run extends keyof Ctx>(h: Harness<Ctx, State, Req, Run>, slots: Pick<Ctx, Req>): Agent;
625
+ /**
626
+ * Instantiate an agent from a fully-configured {@link Harness}.
627
+ *
628
+ * Validates that all `required()` slots are present in `slots`, that no
629
+ * `runtime()` slots are passed here, and that the harness has a loop defined.
630
+ *
631
+ * @param id - A stable, human-readable identifier for this agent (used as the
632
+ * first argument to every store operation).
633
+ * @param h - The harness produced by `createHarness()...loop()`.
634
+ * @param slots - Values for every slot declared with `required()` in the harness.
635
+ * @param options - Optional agent-level options (e.g., instanceId).
636
+ *
637
+ * @throws {@link MissingLoopError} when `h.loop()` was never called.
638
+ * @throws {@link MissingSlotError} when a `required()` slot is absent from `slots`.
639
+ * @throws {@link RuntimeSlotInAgentError} when a `runtime()` slot is passed in `slots`.
640
+ * @throws {@link UnknownSlotError} when `slots` contains a key not declared in the harness.
641
+ *
642
+ * @example
643
+ * ```ts
644
+ * const agent = createAgent('my-agent', h, { llm: new Claude('claude-3-5-haiku-20241022') })
645
+ * const handle = agent.run({}, {})
646
+ * const { state, signal } = await handle
647
+ * ```
648
+ */
649
+ 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;
201
650
 
651
+ /**
652
+ * Thrown by {@link RunHandle.resume} or {@link Agent.resume} when the session
653
+ * is not currently paused on a matching interrupt.
654
+ *
655
+ * Common causes: the run has not yet settled, the session does not exist,
656
+ * or the supplied `interruptId` does not match the pending interrupt.
657
+ */
202
658
  declare class NoInterruptError extends Error {
203
659
  constructor();
204
660
  }
205
661
 
662
+ /**
663
+ * Thrown by {@link Agent.run} or {@link Agent.resume} when the given session
664
+ * is already executing a run in the same process.
665
+ *
666
+ * Wait for the in-flight {@link RunHandle} to settle before starting a new run.
667
+ */
206
668
  declare class SessionInFlightError extends Error {
669
+ /** The session that was already in-flight. */
207
670
  readonly sessionId: string;
208
671
  constructor(sessionId: string);
209
672
  }
673
+ /**
674
+ * Thrown by {@link Agent.run} when the session is paused on an unanswered
675
+ * interrupt. Call {@link Agent.resume} (or `handle.resume()`) instead.
676
+ */
210
677
  declare class SessionPendingInterruptError extends Error {
678
+ /** The session that is awaiting an interrupt response. */
211
679
  readonly sessionId: string;
212
680
  constructor(sessionId: string);
213
681
  }
682
+ /**
683
+ * Thrown by the harness when the session store's `load()` call rejects.
684
+ * The original error is available on the `cause` property.
685
+ */
214
686
  declare class StoreLoadError extends Error {
215
687
  readonly cause: unknown;
216
688
  constructor(cause: unknown);
217
689
  }
218
690
 
219
- export { type Agent, type DeepWithMarkers, type FieldDefinition, type Harness, NoInterruptError, REQUIRED_TAG, RUNTIME_TAG, type RequiredMarker, type RuntimeMarker, SessionInFlightError, SessionPendingInterruptError, type SessionStore, type StateFromSchema, StoreLoadError, type StoredRun, createAgent, createHarness, field, isRequiredMarker, isRuntimeMarker, required, runtime };
691
+ /**
692
+ * Identifies the agent and session for run-level observer callbacks.
693
+ */
694
+ interface RunContext {
695
+ readonly agentId: string;
696
+ readonly sessionId: string;
697
+ /**
698
+ * The UUID for this specific run invocation. Always present in RunContext
699
+ * passed to observer methods.
700
+ */
701
+ readonly runId: string;
702
+ /**
703
+ * Optional parent run UUID for cross-process trace correlation. Present
704
+ * when parentRunId was passed to agent.run() resources. Absent for top-level
705
+ * runs or when not provided by the caller.
706
+ */
707
+ readonly parentRunId?: string;
708
+ /**
709
+ * The physical instance ID of the agent process, if configured via
710
+ * `createAgent()` options. Absent when `instanceId` was not provided.
711
+ */
712
+ readonly instanceId?: string;
713
+ }
714
+ /**
715
+ * Identifies the agent, session, and current step for step-level observer callbacks.
716
+ */
717
+ interface StepContext {
718
+ readonly agentId: string;
719
+ readonly sessionId: string;
720
+ readonly stepName: string;
721
+ }
722
+ /**
723
+ * Observability hook interface. All methods are optional — implement only
724
+ * the hooks you need.
725
+ *
726
+ * Pass an `Observer` implementation in `agent.run()` resources under the key
727
+ * `'observer'`, or bind it to an {@link ObserverAware} slot before running.
728
+ *
729
+ * LLM adapters emit `'llm.response'` events via `onEvent` carrying an
730
+ * `LLMUsageEvent` payload for token tracking.
731
+ *
732
+ * @example
733
+ * ```ts
734
+ * const obs: Observer = {
735
+ * onRunStart: (ctx) => console.log('run started', ctx.sessionId),
736
+ * onEvent: (ctx, type, payload) => metrics.record(type, payload),
737
+ * }
738
+ * agent.run({}, { llm, observer: obs })
739
+ * ```
740
+ */
741
+ interface Observer {
742
+ /** Called once when a run begins, before the first step executes. */
743
+ onRunStart?: (ctx: RunContext) => void;
744
+ /** Called once when a run settles (completed or stopped). */
745
+ onRunEnd?: (ctx: RunContext, event: {
746
+ signal: string;
747
+ durationMs: number;
748
+ }) => void;
749
+ /** Called immediately before each step's `run` function is invoked. */
750
+ onStepStart?: (ctx: StepContext) => void;
751
+ /** Called after a step completes successfully. */
752
+ onStepEnd?: (ctx: StepContext, event: {
753
+ durationMs: number;
754
+ }) => void;
755
+ /** Called when a step's `run` function throws. */
756
+ onStepError?: (ctx: StepContext, event: {
757
+ error: unknown;
758
+ durationMs: number;
759
+ }) => void;
760
+ /** Called when a step issues a `ctx.interrupt()`. */
761
+ onInterrupt?: (ctx: StepContext, event: {
762
+ prompt: unknown;
763
+ interruptId: string;
764
+ }) => void;
765
+ /**
766
+ * Called for arbitrary named events emitted by step code via `ctx.emit()` or
767
+ * by LLM adapters (e.g. `'llm.response'`).
768
+ */
769
+ onEvent?: (ctx: StepContext, type: string, payload: unknown) => void;
770
+ }
771
+ /**
772
+ * Implemented by resources (e.g. LLM adapters) that accept an {@link Observer}
773
+ * at run time. The harness calls `bindObserver` on every slot value that
774
+ * implements this interface before the first step runs.
775
+ *
776
+ * Optionally, the harness calls `setStepContext` at the start of each step for
777
+ * every slot that exposes it. Adapters use this to attribute per-step telemetry
778
+ * (e.g. `observer.onEvent`) to the correct step without manual calls from step code.
779
+ */
780
+ interface ObserverAware {
781
+ /**
782
+ * Receive the run's observer. The harness calls this once per `agent.run()`
783
+ * invocation before execution begins.
784
+ */
785
+ bindObserver(observer: Observer): void;
786
+ /**
787
+ * Receive the current step context. The harness calls this at the start of
788
+ * each step for every slot that exposes this method, before calling `step.run`.
789
+ *
790
+ * Adapters that emit `observer.onEvent` in their `invoke()` method store the
791
+ * provided `StepContext` and use it as the first argument to `onEvent`, so
792
+ * events are attributed to the correct step automatically.
793
+ */
794
+ setStepContext?(ctx: StepContext): void;
795
+ }
796
+ /**
797
+ * Combine multiple {@link Observer} instances into one. Each hook on the
798
+ * composite forwards to all constituent observers in order.
799
+ *
800
+ * @example
801
+ * ```ts
802
+ * const observer = composeObservers(otelObserver, metricsObserver)
803
+ * ```
804
+ */
805
+ declare function composeObservers(...observers: Observer[]): Observer;
806
+
807
+ export { type Agent, type ClaimOptions, type DeepWithMarkers, type FieldDefinition, type FrameworkState, type Harness, type Lease, type LoopDefinition, LoopNotDefinedError, NoInterruptError, type Observer, type ObserverAware, REQUIRED_TAG, RUNTIME_TAG, type RequiredMarker, type RouteFn, type RunContext, type RunFn, type RuntimeMarker, SessionInFlightError, SessionPendingInterruptError, type SessionStore, type SignalTransition, type StateFromSchema, type StepContext, type StepDef, type StepState, StoreLoadError, type StoredRun, type StoredRunMetadata, type TransitionTarget, composeObservers, createAgent, createHarness, field, isRequiredMarker, isRuntimeMarker, required, runtime };