@noetaris/harness 0.1.0 → 0.2.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 +201 -0
- package/dist/index.d.ts +555 -40
- package/dist/index.js +405 -73
- package/dist/index.js.map +1 -1
- package/package.json +12 -2
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
@@ -117,72 +413,137 @@ interface LoopBuilder<S, Ctx> {
|
|
|
117
413
|
onError(step: string): LoopBuilder<S, Ctx>;
|
|
118
414
|
}
|
|
119
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Immutable builder that accumulates context-slot declarations, store bindings,
|
|
418
|
+
* and the loop definition for an agent. Produced by {@link createHarness} and
|
|
419
|
+
* consumed by {@link createAgent}.
|
|
420
|
+
*
|
|
421
|
+
* The four type parameters track what has been declared so far:
|
|
422
|
+
* - `Ctx` — the full context shape expected by step functions.
|
|
423
|
+
* - `State` — the state record shape derived from the schema passed to `createHarness()`.
|
|
424
|
+
* - `Req` — keys declared as {@link required} (must be supplied to `createAgent`).
|
|
425
|
+
* - `Run` — keys declared as {@link runtime} (must be supplied to `agent.run()`).
|
|
426
|
+
*
|
|
427
|
+
* Each call returns a **new** `Harness` instance; the original is unchanged.
|
|
428
|
+
*
|
|
429
|
+
* @example
|
|
430
|
+
* ```ts
|
|
431
|
+
* const h = createHarness<Ctx>()(schema)
|
|
432
|
+
* .provide('llm', required())
|
|
433
|
+
* .provide('model', 'claude-3-5-haiku-20241022')
|
|
434
|
+
* .store({ session: new InMemorySessionStore() })
|
|
435
|
+
* .loop(l => {
|
|
436
|
+
* l.start().step('run', { run, route }).on('done').end()
|
|
437
|
+
* })
|
|
438
|
+
* ```
|
|
439
|
+
*/
|
|
120
440
|
type Harness<Ctx, State, Req extends keyof Ctx = never, Run extends keyof Ctx = never> = {
|
|
441
|
+
/**
|
|
442
|
+
* Declare a context slot.
|
|
443
|
+
*
|
|
444
|
+
* - Pass {@link required} to require the value at `createAgent()` time.
|
|
445
|
+
* - Pass {@link runtime} to require the value at `agent.run()` time.
|
|
446
|
+
* - Pass any concrete value (or a deep object mixing in markers) to bind it now.
|
|
447
|
+
*/
|
|
121
448
|
provide<K extends keyof Ctx>(key: K, value: RequiredMarker): Harness<Ctx, State, Req | K, Run>;
|
|
122
449
|
provide<K extends keyof Ctx>(key: K, value: RuntimeMarker): Harness<Ctx, State, Req, Run | K>;
|
|
123
450
|
provide<K extends keyof Ctx>(key: K, value: DeepWithMarkers<Ctx[K]>): Harness<Ctx, State, Req, Run>;
|
|
451
|
+
/**
|
|
452
|
+
* Bind session store(s). Pass `{ session: myStore }` to enable persistence and
|
|
453
|
+
* interrupt/resume across processes.
|
|
454
|
+
*/
|
|
124
455
|
store(stores: DeepWithMarkers<{
|
|
125
456
|
session?: unknown;
|
|
126
457
|
} & Record<string, unknown>>): Harness<Ctx, State, Req, Run>;
|
|
458
|
+
/**
|
|
459
|
+
* Define the execution loop using the {@link LoopBuilder} DSL.
|
|
460
|
+
*
|
|
461
|
+
* @param builder - Callback that receives a `LoopBuilder` and uses it to
|
|
462
|
+
* declare steps, transitions, and the entry point.
|
|
463
|
+
*/
|
|
127
464
|
loop(builder: (l: LoopBuilder<State, Ctx>) => void): Harness<Ctx, State, Req, Run>;
|
|
128
465
|
};
|
|
466
|
+
/**
|
|
467
|
+
* Create a harness builder for the given context type.
|
|
468
|
+
*
|
|
469
|
+
* Call the returned function (optionally with a state schema) to get the
|
|
470
|
+
* {@link Harness} builder on which you chain `.provide()`, `.store()`, and `.loop()`.
|
|
471
|
+
*
|
|
472
|
+
* @example
|
|
473
|
+
* ```ts
|
|
474
|
+
* interface Ctx { llm: LLM; model: string }
|
|
475
|
+
* const schema = { messages: field<string[]>({ default: () => [] }) }
|
|
476
|
+
*
|
|
477
|
+
* const h = createHarness<Ctx>()(schema)
|
|
478
|
+
* ```
|
|
479
|
+
*/
|
|
129
480
|
declare function createHarness<Ctx = any>(): <S extends object = {}>(// any: allows createHarness() without an explicit Ctx type parameter
|
|
130
481
|
stateSchema?: S) => Harness<Ctx, StateFromSchema<S>>;
|
|
131
482
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
483
|
+
/**
|
|
484
|
+
* The resolved value of a {@link RunHandle} promise: the final state and the
|
|
485
|
+
* signal that caused the loop to exit.
|
|
486
|
+
*
|
|
487
|
+
* `signal` is `null` when the loop exited via `.end()` without an explicit signal,
|
|
488
|
+
* `'$interrupt'` when paused on an interrupt, or any custom signal string returned
|
|
489
|
+
* by a route function.
|
|
490
|
+
*/
|
|
163
491
|
interface RunOutcome {
|
|
164
492
|
readonly state: Record<string, unknown>;
|
|
165
493
|
readonly signal: string | null;
|
|
166
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* Handle returned synchronously by {@link Agent.run} and {@link Agent.resume}.
|
|
497
|
+
*
|
|
498
|
+
* Implements `PromiseLike<RunOutcome>` so it can be `await`-ed directly.
|
|
499
|
+
*
|
|
500
|
+
* @example
|
|
501
|
+
* ```ts
|
|
502
|
+
* const handle = agent.run({}, { llm })
|
|
503
|
+
* console.log(handle.sessionId) // available immediately
|
|
504
|
+
* const { state, signal } = await handle
|
|
505
|
+
* ```
|
|
506
|
+
*/
|
|
167
507
|
interface RunHandle extends PromiseLike<RunOutcome> {
|
|
168
508
|
/** Cancel the in-flight run at the next safe point between steps. Idempotent. */
|
|
169
509
|
stop(): void;
|
|
170
510
|
/**
|
|
171
|
-
* Provide a response to a pending ctx.interrupt() call.
|
|
172
|
-
* Returns a new RunHandle for the resumed execution.
|
|
173
|
-
*
|
|
511
|
+
* Provide a response to a pending `ctx.interrupt()` call and start a new run.
|
|
512
|
+
* Returns a new {@link RunHandle} for the resumed execution.
|
|
513
|
+
*
|
|
514
|
+
* Throws {@link NoInterruptError} when the run has not settled on `signal: '$interrupt'`.
|
|
515
|
+
*
|
|
516
|
+
* @param response - The value to deliver as the interrupt response.
|
|
517
|
+
* @param interruptId - The `interruptId` from `state.$interrupt`.
|
|
174
518
|
*/
|
|
175
519
|
resume(response: unknown, interruptId: string): RunHandle;
|
|
176
520
|
/** The session identity for this run. */
|
|
177
521
|
readonly sessionId: string;
|
|
522
|
+
/**
|
|
523
|
+
* Unique identifier for this specific invocation.
|
|
524
|
+
* One UUID per `agent.run()` or `agent.resume()` call.
|
|
525
|
+
*/
|
|
526
|
+
readonly runId: string;
|
|
178
527
|
/**
|
|
179
528
|
* The name of the step currently executing in this process.
|
|
180
|
-
* null before the first step runs, after execution settles, and always
|
|
529
|
+
* `null` before the first step runs, after execution settles, and always
|
|
530
|
+
* when inspected cross-process.
|
|
181
531
|
*/
|
|
182
532
|
readonly currentStep: string | null;
|
|
183
533
|
}
|
|
184
534
|
|
|
535
|
+
/** Options passed to createAgent(). */
|
|
536
|
+
interface AgentOptions {
|
|
537
|
+
/**
|
|
538
|
+
* Stable identifier for the process or replica running this agent instance.
|
|
539
|
+
* Written to `StoredRun.metadata` on every save and propagated to observers
|
|
540
|
+
* via `RunContext.instanceId`.
|
|
541
|
+
*/
|
|
542
|
+
readonly instanceId?: string;
|
|
543
|
+
}
|
|
185
544
|
interface Agent {
|
|
545
|
+
/** The agent's unique identifier, as provided to createAgent(). */
|
|
546
|
+
readonly id: string;
|
|
186
547
|
/**
|
|
187
548
|
* Start a new run. Returns a RunHandle synchronously before execution begins.
|
|
188
549
|
*/
|
|
@@ -191,29 +552,183 @@ interface Agent {
|
|
|
191
552
|
* Cross-process entry point for responding to a pending interrupt.
|
|
192
553
|
* Returns a RunHandle synchronously; the execution promise performs the resume.
|
|
193
554
|
*/
|
|
194
|
-
resume(response: unknown, sessionId: string, interruptId: string
|
|
555
|
+
resume(response: unknown, sessionId: string, interruptId: string, options?: {
|
|
556
|
+
events?: {
|
|
557
|
+
onStoreError?: (error: unknown, phase: 'load' | 'persist' | 'claim') => void;
|
|
558
|
+
};
|
|
559
|
+
}): RunHandle;
|
|
195
560
|
/**
|
|
196
561
|
* Query the session store for the current phase of a session.
|
|
197
562
|
*/
|
|
198
563
|
status(sessionId: string): Promise<SessionPhase>;
|
|
199
564
|
}
|
|
200
|
-
|
|
565
|
+
/**
|
|
566
|
+
* Instantiate an agent from a fully-configured {@link Harness}.
|
|
567
|
+
*
|
|
568
|
+
* Validates that all `required()` slots are present in `slots`, that no
|
|
569
|
+
* `runtime()` slots are passed here, and that the harness has a loop defined.
|
|
570
|
+
*
|
|
571
|
+
* @param id - A stable, human-readable identifier for this agent (used as the
|
|
572
|
+
* first argument to every store operation).
|
|
573
|
+
* @param h - The harness produced by `createHarness()...loop()`.
|
|
574
|
+
* @param slots - Values for every slot declared with `required()` in the harness.
|
|
575
|
+
* @param options - Optional agent-level options (e.g., instanceId).
|
|
576
|
+
*
|
|
577
|
+
* @throws {@link MissingLoopError} when `h.loop()` was never called.
|
|
578
|
+
* @throws {@link MissingSlotError} when a `required()` slot is absent from `slots`.
|
|
579
|
+
* @throws {@link RuntimeSlotInAgentError} when a `runtime()` slot is passed in `slots`.
|
|
580
|
+
* @throws {@link UnknownSlotError} when `slots` contains a key not declared in the harness.
|
|
581
|
+
*
|
|
582
|
+
* @example
|
|
583
|
+
* ```ts
|
|
584
|
+
* const agent = createAgent('my-agent', h, { llm: new Claude('claude-3-5-haiku-20241022') })
|
|
585
|
+
* const handle = agent.run({}, {})
|
|
586
|
+
* const { state, signal } = await handle
|
|
587
|
+
* ```
|
|
588
|
+
*/
|
|
589
|
+
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
590
|
|
|
591
|
+
/**
|
|
592
|
+
* Thrown by {@link RunHandle.resume} or {@link Agent.resume} when the session
|
|
593
|
+
* is not currently paused on a matching interrupt.
|
|
594
|
+
*
|
|
595
|
+
* Common causes: the run has not yet settled, the session does not exist,
|
|
596
|
+
* or the supplied `interruptId` does not match the pending interrupt.
|
|
597
|
+
*/
|
|
202
598
|
declare class NoInterruptError extends Error {
|
|
203
599
|
constructor();
|
|
204
600
|
}
|
|
205
601
|
|
|
602
|
+
/**
|
|
603
|
+
* Thrown by {@link Agent.run} or {@link Agent.resume} when the given session
|
|
604
|
+
* is already executing a run in the same process.
|
|
605
|
+
*
|
|
606
|
+
* Wait for the in-flight {@link RunHandle} to settle before starting a new run.
|
|
607
|
+
*/
|
|
206
608
|
declare class SessionInFlightError extends Error {
|
|
609
|
+
/** The session that was already in-flight. */
|
|
207
610
|
readonly sessionId: string;
|
|
208
611
|
constructor(sessionId: string);
|
|
209
612
|
}
|
|
613
|
+
/**
|
|
614
|
+
* Thrown by {@link Agent.run} when the session is paused on an unanswered
|
|
615
|
+
* interrupt. Call {@link Agent.resume} (or `handle.resume()`) instead.
|
|
616
|
+
*/
|
|
210
617
|
declare class SessionPendingInterruptError extends Error {
|
|
618
|
+
/** The session that is awaiting an interrupt response. */
|
|
211
619
|
readonly sessionId: string;
|
|
212
620
|
constructor(sessionId: string);
|
|
213
621
|
}
|
|
622
|
+
/**
|
|
623
|
+
* Thrown by the harness when the session store's `load()` call rejects.
|
|
624
|
+
* The original error is available on the `cause` property.
|
|
625
|
+
*/
|
|
214
626
|
declare class StoreLoadError extends Error {
|
|
215
627
|
readonly cause: unknown;
|
|
216
628
|
constructor(cause: unknown);
|
|
217
629
|
}
|
|
218
630
|
|
|
219
|
-
|
|
631
|
+
/**
|
|
632
|
+
* Identifies the agent and session for run-level observer callbacks.
|
|
633
|
+
*/
|
|
634
|
+
interface RunContext {
|
|
635
|
+
readonly agentId: string;
|
|
636
|
+
readonly sessionId: string;
|
|
637
|
+
/**
|
|
638
|
+
* The UUID for this specific run invocation. Always present in RunContext
|
|
639
|
+
* passed to observer methods.
|
|
640
|
+
*/
|
|
641
|
+
readonly runId: string;
|
|
642
|
+
/**
|
|
643
|
+
* Optional parent run UUID for cross-process trace correlation. Present
|
|
644
|
+
* when parentRunId was passed to agent.run() resources. Absent for top-level
|
|
645
|
+
* runs or when not provided by the caller.
|
|
646
|
+
*/
|
|
647
|
+
readonly parentRunId?: string;
|
|
648
|
+
/**
|
|
649
|
+
* The physical instance ID of the agent process, if configured via
|
|
650
|
+
* `createAgent()` options. Absent when `instanceId` was not provided.
|
|
651
|
+
*/
|
|
652
|
+
readonly instanceId?: string;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Identifies the agent, session, and current step for step-level observer callbacks.
|
|
656
|
+
*/
|
|
657
|
+
interface StepContext {
|
|
658
|
+
readonly agentId: string;
|
|
659
|
+
readonly sessionId: string;
|
|
660
|
+
readonly stepName: string;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Observability hook interface. All methods are optional — implement only
|
|
664
|
+
* the hooks you need.
|
|
665
|
+
*
|
|
666
|
+
* Pass an `Observer` implementation in `agent.run()` resources under the key
|
|
667
|
+
* `'observer'`, or bind it to an {@link ObserverAware} slot before running.
|
|
668
|
+
*
|
|
669
|
+
* LLM adapters emit `'llm.response'` events via `onEvent` carrying an
|
|
670
|
+
* `LLMUsageEvent` payload for token tracking.
|
|
671
|
+
*
|
|
672
|
+
* @example
|
|
673
|
+
* ```ts
|
|
674
|
+
* const obs: Observer = {
|
|
675
|
+
* onRunStart: (ctx) => console.log('run started', ctx.sessionId),
|
|
676
|
+
* onEvent: (ctx, type, payload) => metrics.record(type, payload),
|
|
677
|
+
* }
|
|
678
|
+
* agent.run({}, { llm, observer: obs })
|
|
679
|
+
* ```
|
|
680
|
+
*/
|
|
681
|
+
interface Observer {
|
|
682
|
+
/** Called once when a run begins, before the first step executes. */
|
|
683
|
+
onRunStart?: (ctx: RunContext) => void;
|
|
684
|
+
/** Called once when a run settles (completed or stopped). */
|
|
685
|
+
onRunEnd?: (ctx: RunContext, event: {
|
|
686
|
+
signal: string;
|
|
687
|
+
durationMs: number;
|
|
688
|
+
}) => void;
|
|
689
|
+
/** Called immediately before each step's `run` function is invoked. */
|
|
690
|
+
onStepStart?: (ctx: StepContext) => void;
|
|
691
|
+
/** Called after a step completes successfully. */
|
|
692
|
+
onStepEnd?: (ctx: StepContext, event: {
|
|
693
|
+
durationMs: number;
|
|
694
|
+
}) => void;
|
|
695
|
+
/** Called when a step's `run` function throws. */
|
|
696
|
+
onStepError?: (ctx: StepContext, event: {
|
|
697
|
+
error: unknown;
|
|
698
|
+
durationMs: number;
|
|
699
|
+
}) => void;
|
|
700
|
+
/** Called when a step issues a `ctx.interrupt()`. */
|
|
701
|
+
onInterrupt?: (ctx: StepContext, event: {
|
|
702
|
+
prompt: unknown;
|
|
703
|
+
interruptId: string;
|
|
704
|
+
}) => void;
|
|
705
|
+
/**
|
|
706
|
+
* Called for arbitrary named events emitted by step code via `ctx.emit()` or
|
|
707
|
+
* by LLM adapters (e.g. `'llm.response'`).
|
|
708
|
+
*/
|
|
709
|
+
onEvent?: (ctx: StepContext, type: string, payload: unknown) => void;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Implemented by resources (e.g. LLM adapters) that accept an {@link Observer}
|
|
713
|
+
* at run time. The harness calls `bindObserver` on every slot value that
|
|
714
|
+
* implements this interface before the first step runs.
|
|
715
|
+
*/
|
|
716
|
+
interface ObserverAware {
|
|
717
|
+
/**
|
|
718
|
+
* Receive the run's observer. The harness calls this once per `agent.run()`
|
|
719
|
+
* invocation before execution begins.
|
|
720
|
+
*/
|
|
721
|
+
bindObserver(observer: Observer): void;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Combine multiple {@link Observer} instances into one. Each hook on the
|
|
725
|
+
* composite forwards to all constituent observers in order.
|
|
726
|
+
*
|
|
727
|
+
* @example
|
|
728
|
+
* ```ts
|
|
729
|
+
* const observer = composeObservers(otelObserver, metricsObserver)
|
|
730
|
+
* ```
|
|
731
|
+
*/
|
|
732
|
+
declare function composeObservers(...observers: Observer[]): Observer;
|
|
733
|
+
|
|
734
|
+
export { type Agent, type ClaimOptions, type DeepWithMarkers, type FieldDefinition, type Harness, type Lease, NoInterruptError, type Observer, type ObserverAware, REQUIRED_TAG, RUNTIME_TAG, type RequiredMarker, type RunContext, type RuntimeMarker, SessionInFlightError, SessionPendingInterruptError, type SessionStore, type StateFromSchema, type StepContext, StoreLoadError, type StoredRun, type StoredRunMetadata, composeObservers, createAgent, createHarness, field, isRequiredMarker, isRuntimeMarker, required, runtime };
|