@noetaris/harness 0.5.0 → 0.7.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 +229 -5
- package/dist/index.d.ts +122 -2
- package/dist/index.js +259 -143
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -156,6 +156,226 @@ const resumed = run.resume(response, interruptId)
|
|
|
156
156
|
const resumed = agent.resume(response, sessionId, interruptId)
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
+
**Resume replays the interrupted step from the top.** On resume, the step that called
|
|
160
|
+
`ctx.interrupt()` runs again from its first line; each `ctx.interrupt()` call it reaches
|
|
161
|
+
returns the stored response instead of pausing. State updates from the paused attempt are
|
|
162
|
+
not kept — a step's update is applied only when `run` returns. So any side effect a step
|
|
163
|
+
performs *before* `ctx.interrupt()` (an HTTP call, a DB write, a sub agent run) happens
|
|
164
|
+
again on resume. Making that safe is up to the step: guard the side effect so a replay
|
|
165
|
+
skips or reuses it.
|
|
166
|
+
|
|
167
|
+
**Inside a fork, only paused branches replay.** When a fork pauses, each branch is saved
|
|
168
|
+
separately. On resume, a branch that already finished is not run again — its saved state
|
|
169
|
+
goes straight to the join. Every branch that is still paused runs again from its paused
|
|
170
|
+
step, even if the response you sent was for a different branch; a branch with no response
|
|
171
|
+
yet reaches its `ctx.interrupt()` again and re-pauses. The session stays paused until
|
|
172
|
+
every pending interrupt (see `agent.status(sessionId).pendingInterrupts`) is answered.
|
|
173
|
+
If any branch fails, the fork fails fast: the other branches are aborted and paused
|
|
174
|
+
branches are dropped. The same happens when a branch throws a graph-definition error
|
|
175
|
+
(for example a signal with no matching `.on()`): the other branches are aborted, and the
|
|
176
|
+
run rejects with that error only after they have stopped.
|
|
177
|
+
|
|
178
|
+
**Stopping a fork.** `stop()` while a fork runs stops each branch at its next step
|
|
179
|
+
boundary. What happens next depends on whether any branch is waiting on an interrupt:
|
|
180
|
+
|
|
181
|
+
| At the stop | Run settles with | To continue |
|
|
182
|
+
|---|---|---|
|
|
183
|
+
| No branch is waiting on an interrupt | `signal: null` — a plain stop; `agent.status()` shows `paused` with no pending interrupts | `agent.run()` on the same session continues the fork: finished branches are not run again, stopped branches continue from where they stopped. `resume()` throws `NoInterruptError`. |
|
|
184
|
+
| At least one branch (at any depth) is waiting on an interrupt | `signal: '$interrupt'` | `resume()` answers it and re-runs every paused branch, including the ones that were only stopped |
|
|
185
|
+
|
|
186
|
+
Input passed to the `agent.run()` that continues a stopped fork is merged into the main
|
|
187
|
+
state only. The branches that continue keep their own saved state and do not see it; it
|
|
188
|
+
is visible from the join onward.
|
|
189
|
+
|
|
190
|
+
#### Calling sub agents from a step
|
|
191
|
+
|
|
192
|
+
A sub agent is just another agent with its own session. When it pauses, its `run()`
|
|
193
|
+
resolves with `signal: '$interrupt'` — it does not throw — so the calling step must pass
|
|
194
|
+
the interrupt up with `ctx.interrupt()` and, after resume, call the sub agent's
|
|
195
|
+
`resume()`. Because the step replays, the call must be idempotent. Checking the sub
|
|
196
|
+
agent's session phase is enough:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
async function callSubAgent(agent: Agent, sessionId: string, input: object, ctx: any) {
|
|
200
|
+
const status = await agent.status(sessionId)
|
|
201
|
+
if (status.phase === 'completed') return loadResult(sessionId) // your own lookup — don't run again
|
|
202
|
+
let outcome
|
|
203
|
+
if (status.phase === 'paused') {
|
|
204
|
+
const pending = status.pendingInterrupts[0]!
|
|
205
|
+
const answer = await ctx.interrupt(pending.prompt, `${sessionId}:${pending.interruptId}`)
|
|
206
|
+
outcome = await agent.resume(answer, sessionId, pending.interruptId)
|
|
207
|
+
} else {
|
|
208
|
+
outcome = await agent.run(input, { sessionId })
|
|
209
|
+
}
|
|
210
|
+
if (outcome.signal === '$interrupt') {
|
|
211
|
+
const { interruptId, prompt } = outcome.state.$interrupt
|
|
212
|
+
await ctx.interrupt(prompt, `${sessionId}:${interruptId}`) // pauses the main agent
|
|
213
|
+
}
|
|
214
|
+
return outcome.state
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Use a stable sub agent `sessionId` (for example derived from the main `sessionId`) so a
|
|
219
|
+
replay finds the same session. To run several sub agents in parallel, put each in its own
|
|
220
|
+
fork branch rather than `Promise.all` in one step: then, when one pauses, the finished ones
|
|
221
|
+
are not run again on resume.
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
h.loop(l =>
|
|
225
|
+
l.start()
|
|
226
|
+
.fork('research')
|
|
227
|
+
.branch('web', b => b.start()
|
|
228
|
+
.step('callWeb', {
|
|
229
|
+
run: async (s, ctx) => ({ web: await callSubAgent(webAgent, `${ctx.sessionId}:web`, {}, ctx) }),
|
|
230
|
+
route: () => 'done',
|
|
231
|
+
})
|
|
232
|
+
.on('done').end()) // inside a branch, .end() ends the branch
|
|
233
|
+
.branch('docs', b => b.start()
|
|
234
|
+
.step('callDocs', {
|
|
235
|
+
run: async (s, ctx) => ({ docs: await callSubAgent(docsAgent, `${ctx.sessionId}:docs`, {}, ctx) }),
|
|
236
|
+
route: () => 'done',
|
|
237
|
+
})
|
|
238
|
+
.on('done').end())
|
|
239
|
+
.join('merge', { run: mergeResults, route: () => 'done' })
|
|
240
|
+
.on('done').end()
|
|
241
|
+
)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
If a fork fails fast while a sub agent is paused, that sub agent's own session stays
|
|
245
|
+
paused in its store — clean it up or reuse it the next time the step runs.
|
|
246
|
+
|
|
247
|
+
### Observers
|
|
248
|
+
|
|
249
|
+
An `Observer` receives telemetry hooks for a run. Pass it as `observer` in the resources
|
|
250
|
+
of `agent.run()` or `agent.resume()`. Combine several with `composeObservers([a, b])`;
|
|
251
|
+
a hook that throws is reported to `onObserverError` (or `console.error`) and never stops
|
|
252
|
+
the run. The observer that LLM adapters call is isolated the same way, so an observer that
|
|
253
|
+
throws on an adapter event never fails a step. Every hook is optional.
|
|
254
|
+
|
|
255
|
+
| Hook | Fires |
|
|
256
|
+
|---|---|
|
|
257
|
+
| `onRunStart(ctx)` | once when a run starts |
|
|
258
|
+
| `onRunEnd(ctx, { signal, durationMs })` | once when a run settles — completed, stopped (`$stopped`), paused, or rejected by a thrown error (`$error`) |
|
|
259
|
+
| `onStepStart(ctx)` | before each step |
|
|
260
|
+
| `onStepEnd(ctx, { durationMs })` | after a step's `run` succeeds, before its `route` |
|
|
261
|
+
| `onStepError(ctx, { error, durationMs })` | when a step's `run` throws |
|
|
262
|
+
| `onInterrupt(ctx, { prompt, interruptId })` | when a step calls `ctx.interrupt()` |
|
|
263
|
+
| `onEvent(ctx, type, payload)` | on `ctx.emit()` and adapter events such as `llm.response` |
|
|
264
|
+
| `onStepSettled(ctx, event)` | once per step, when its outcome and destination are final |
|
|
265
|
+
| `wrap(ctx, scope, fn)` | around a step's `run`, and around calls made with `ctx.within` |
|
|
266
|
+
|
|
267
|
+
Step hooks receive a `StepContext`: `agentId`, `sessionId`, `runId`, `stepName` and, inside a
|
|
268
|
+
fork branch, `branchPath`. `runId` is the same value as `RunContext.runId`. Steps inside a branch
|
|
269
|
+
carry the top-level run's id, and a resumed run has a new one.
|
|
270
|
+
|
|
271
|
+
**`onStepSettled`** is the one hook that tells you, for every step, how it ended and
|
|
272
|
+
where the run goes next. It fires exactly once for each `onStepStart`, on every path:
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
onStepStart → onStepEnd | onStepError | onInterrupt → onStepSettled → onRunEnd
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
It also fires for a fork node and for each step inside a branch (`ctx.branchPath` names the
|
|
279
|
+
branch). A graph-definition error (`UnknownSignalError`, `NoNextStepError`,
|
|
280
|
+
`MissingReducerError`) settles with `next.kind === 'throw'` before the run rejects; `onRunEnd`
|
|
281
|
+
then fires with `'$error'` and the run rejects with the same error. It does
|
|
282
|
+
not fire when a run stops before starting a step, or for a finished branch that a resume
|
|
283
|
+
skips.
|
|
284
|
+
|
|
285
|
+
| `event` field | Meaning |
|
|
286
|
+
|---|---|
|
|
287
|
+
| `outcome` | `'ok'`, `'error'`, `'interrupt'`, or `'stopped'` (a fork whose branches were stopped) |
|
|
288
|
+
| `durationMs` | from `onStepStart` until the destination is known — includes `route` |
|
|
289
|
+
| `update` | what `run` returned, present only when it was applied to state. The raw object — copy it if you keep it |
|
|
290
|
+
| `signal` | what `route` returned, when it was called |
|
|
291
|
+
| `next` | `{ kind: 'step', name }`, `{ kind: 'end' }`, `{ kind: 'pause' }`, or `{ kind: 'throw' }` |
|
|
292
|
+
| `error` | present when `outcome` is `'error'` |
|
|
293
|
+
| `interrupt` | `{ interruptId, prompt }`, when a plain step paused on an interrupt |
|
|
294
|
+
| `fork` | on a fork node that ended done or paused: `{ branches: [{ name, status, touchedKeys }] }` |
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
const logger: Observer = {
|
|
298
|
+
onStepSettled(ctx, e) {
|
|
299
|
+
const next = e.next.kind === 'step' ? `→ ${e.next.name}` : e.next.kind
|
|
300
|
+
console.log(`${ctx.stepName}: ${e.outcome} ${next}`, e.signal ? `(signal ${e.signal})` : '')
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const h = createHarness()({ total: field({ default: () => 0 }) }).loop(l =>
|
|
305
|
+
l.start()
|
|
306
|
+
.step('fetch', { run: async () => ({ total: 3 }) })
|
|
307
|
+
.step('decide', { route: s => (s.total > 0 ? 'done' : 'empty') })
|
|
308
|
+
.on('done').end()
|
|
309
|
+
.on('empty').end()
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
await createAgent('demo', h, {}).run({}, { observer: logger })
|
|
313
|
+
// fetch: ok → decide
|
|
314
|
+
// decide: ok end (signal done)
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Running code inside a span
|
|
318
|
+
|
|
319
|
+
Hooks are notifications: they return before the step runs, so they cannot make a span the
|
|
320
|
+
active context for the code inside it. `wrap` is the one hook that encloses work.
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
const timing: Observer = {
|
|
324
|
+
async wrap(ctx, scope, fn) {
|
|
325
|
+
const label = scope.kind === 'step' ? ctx.stepName : `${ctx.stepName}/${scope.type}`
|
|
326
|
+
console.log(`enter ${label}`)
|
|
327
|
+
try {
|
|
328
|
+
return await fn()
|
|
329
|
+
} finally {
|
|
330
|
+
console.log(`exit ${label}`)
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const h = createHarness()({ done: field({ default: () => false }) }).loop(l =>
|
|
336
|
+
l.start()
|
|
337
|
+
.step('work', {
|
|
338
|
+
run: async (_s, ctx) => {
|
|
339
|
+
await ctx.within('tool.call', { toolCallId: 't1' }, async () => { /* call the tool */ })
|
|
340
|
+
return { done: true }
|
|
341
|
+
},
|
|
342
|
+
route: () => 'done',
|
|
343
|
+
})
|
|
344
|
+
.on('done').end()
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
await createAgent('demo', h, {}).run({}, { observer: timing })
|
|
348
|
+
// enter work
|
|
349
|
+
// enter work/tool.call
|
|
350
|
+
// exit work/tool.call
|
|
351
|
+
// exit work
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
- `scope` is `{ kind: 'step' }` for a step's `run`, or `{ kind: 'event', type, payload }` for code
|
|
355
|
+
that asks for a scope with `ctx.within(type, payload, fn)`. `type` is the name of the event you
|
|
356
|
+
emit for that call (for example `'tool.call'`) and `payload` carries its id (`{ toolCallId }`).
|
|
357
|
+
- `ctx.within` only runs `fn` when no observer has `wrap`. For tool calls, `runTool` from
|
|
358
|
+
[`@noetaris/harness-types`](https://github.com/noetaris-lab/harness-types) emits the tool events
|
|
359
|
+
and calls `ctx.within` for you.
|
|
360
|
+
- Only a step's `run` is wrapped. `route` runs after the wrap has exited, and a fork node is not
|
|
361
|
+
wrapped; each step inside a branch is wrapped with its own `ctx.branchPath`.
|
|
362
|
+
- With `composeObservers([a, b])`, `a` is the outer scope.
|
|
363
|
+
- LLM adapters use the same primitive: the `TelemetryContext` an `ObserverAware` slot receives from
|
|
364
|
+
`withTelemetry` carries a `within` bound to the current step.
|
|
365
|
+
|
|
366
|
+
The harness never trusts a wrapper. `fn` always runs exactly once, and the caller always gets
|
|
367
|
+
`fn`'s own result:
|
|
368
|
+
|
|
369
|
+
| The wrapper... | What happens |
|
|
370
|
+
|---|---|
|
|
371
|
+
| throws before calling `fn` | the error goes to `onObserverError`; `fn` runs without the scope |
|
|
372
|
+
| never calls `fn` | `fn` runs without the scope |
|
|
373
|
+
| calls `fn` twice | `fn` runs once |
|
|
374
|
+
| returns another value, or swallows `fn`'s error | ignored; the caller gets `fn`'s result or error |
|
|
375
|
+
| does not await `fn` | the harness still waits for `fn` |
|
|
376
|
+
| throws after `fn` settled | reported to `onObserverError`, unless it is `fn`'s own error passed through |
|
|
377
|
+
| never settles | the step never finishes — a wrapper must settle |
|
|
378
|
+
|
|
159
379
|
## API
|
|
160
380
|
|
|
161
381
|
| Export | Description |
|
|
@@ -165,13 +385,17 @@ const resumed = agent.resume(response, sessionId, interruptId)
|
|
|
165
385
|
| `field<T>(opts)` | Declares a state field with a default and optional reduce function. |
|
|
166
386
|
| `required()` | Marks a provider slot as required at `createAgent()`. |
|
|
167
387
|
| `runtime()` | Marks a provider slot as required at `agent.run()`. |
|
|
168
|
-
| `composeObservers(
|
|
388
|
+
| `composeObservers([a, b], onObserverError?)` | Merges multiple `Observer` instances into one fan-out observer; a throwing observer is isolated and reported to `onObserverError`. |
|
|
169
389
|
| `SessionStore` | Interface for session persistence backends. |
|
|
170
390
|
| `StoredRun` | Type for a persisted run snapshot. Includes `agentId`, `runId`, `sessionId`, `phase`, and state. |
|
|
171
|
-
| `Observer` | Interface for telemetry hooks on run and step lifecycle events. |
|
|
172
|
-
| `
|
|
173
|
-
| `
|
|
174
|
-
| `
|
|
391
|
+
| `Observer` | Interface for telemetry hooks on run and step lifecycle events (see [Observers](#observers)). |
|
|
392
|
+
| `StepSettledEvent` | Payload of `Observer.onStepSettled` — outcome, duration, update, signal, next, error, interrupt, fork. |
|
|
393
|
+
| `StepSettledNext` | Where the run goes after a step settles: `step`, `end`, `pause`, or `throw`. |
|
|
394
|
+
| `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`. |
|
|
395
|
+
| `TelemetryContext` | What `withTelemetry` receives: `observer` (the run's observer, fault-isolated), `stepContext`, and `within(type, payload, fn)` bound to that step. |
|
|
396
|
+
| `WrapScope` | What `Observer.wrap` is asked to enclose: `{ kind: 'step' }` or `{ kind: 'event', type, payload }`. |
|
|
397
|
+
| `RunContext` | Context passed to run-level observer hooks — `agentId`, `sessionId`, `runId`, `parentRunId?`, `instanceId?`, `branchPath?`. |
|
|
398
|
+
| `StepContext` | Context passed to step-level observer hooks — `agentId`, `sessionId`, `runId`, `stepName`, `branchPath?`. |
|
|
175
399
|
| `NoInterruptError` | Thrown when `resume()` is called but the session is not paused on a matching interrupt. |
|
|
176
400
|
| `SessionInFlightError` | Thrown when a session is already running. |
|
|
177
401
|
| `SessionPendingInterruptError` | Thrown when a session is paused on a pending interrupt — use `agent.resume()` instead of `agent.run()`. |
|
package/dist/index.d.ts
CHANGED
|
@@ -137,6 +137,13 @@ interface RunContext {
|
|
|
137
137
|
interface StepContext {
|
|
138
138
|
readonly agentId: string;
|
|
139
139
|
readonly sessionId: string;
|
|
140
|
+
/**
|
|
141
|
+
* The runId of the run this step belongs to — the same value as `RunContext.runId`. Steps
|
|
142
|
+
* inside fork branches carry the top-level run's id (tell branches apart with `branchPath`).
|
|
143
|
+
* A resumed run has a new id. Empty string only when the loop is driven without a runId
|
|
144
|
+
* (direct `runLoop` use), as for `RunContext.runId`.
|
|
145
|
+
*/
|
|
146
|
+
readonly runId: string;
|
|
140
147
|
readonly stepName: string;
|
|
141
148
|
/**
|
|
142
149
|
* Branch-name path (outermost to innermost), present only when this step runs inside a
|
|
@@ -145,6 +152,77 @@ interface StepContext {
|
|
|
145
152
|
*/
|
|
146
153
|
readonly branchPath?: readonly string[];
|
|
147
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Where execution goes after a step settles.
|
|
157
|
+
*
|
|
158
|
+
* - `step` — the next step to run (a route target, `.next()`, the implicit next step, the
|
|
159
|
+
* `l.onError()` fallback, or the join after a fork).
|
|
160
|
+
* - `end` — the loop ended via `.end()` (inside a fork branch: the branch ended).
|
|
161
|
+
* - `pause` — the run paused (interrupt, stop, or an unhandled error) and can be continued later.
|
|
162
|
+
* - `throw` — a graph-definition error (`UnknownSignalError`, `NoNextStepError`,
|
|
163
|
+
* `MissingReducerError`) is about to reject the run.
|
|
164
|
+
*/
|
|
165
|
+
type StepSettledNext = {
|
|
166
|
+
readonly kind: 'step';
|
|
167
|
+
readonly name: string;
|
|
168
|
+
} | {
|
|
169
|
+
readonly kind: 'end';
|
|
170
|
+
} | {
|
|
171
|
+
readonly kind: 'pause';
|
|
172
|
+
} | {
|
|
173
|
+
readonly kind: 'throw';
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Payload of {@link Observer.onStepSettled}: everything known about one step once its
|
|
177
|
+
* outcome and destination are final. Optional keys are omitted, never set to `undefined`.
|
|
178
|
+
*/
|
|
179
|
+
interface StepSettledEvent {
|
|
180
|
+
/** `'stopped'` only occurs on fork nodes whose branches were stopped without an interrupt. */
|
|
181
|
+
readonly outcome: 'ok' | 'error' | 'interrupt' | 'stopped';
|
|
182
|
+
/** From `onStepStart` until the destination is known — includes `route()`. */
|
|
183
|
+
readonly durationMs: number;
|
|
184
|
+
/**
|
|
185
|
+
* The value `run()` returned, present iff it was applied to state. The raw reference,
|
|
186
|
+
* unfiltered — it may be aliased into state, so copy it before the next step if you keep it.
|
|
187
|
+
*/
|
|
188
|
+
readonly update?: Record<string, unknown>;
|
|
189
|
+
/** Present iff `route()` was called and returned a signal. */
|
|
190
|
+
readonly signal?: string;
|
|
191
|
+
readonly next: StepSettledNext;
|
|
192
|
+
/** Present iff `outcome` is `'error'`. */
|
|
193
|
+
readonly error?: Error;
|
|
194
|
+
/** Present iff `outcome` is `'interrupt'` on a plain (non-fork) step. */
|
|
195
|
+
readonly interrupt?: {
|
|
196
|
+
readonly interruptId: string;
|
|
197
|
+
readonly prompt: unknown;
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* Fork nodes only, when every branch ended done or paused (not when the fork failed):
|
|
201
|
+
* each branch in declaration order.
|
|
202
|
+
*/
|
|
203
|
+
readonly fork?: {
|
|
204
|
+
readonly branches: readonly {
|
|
205
|
+
readonly name: string;
|
|
206
|
+
readonly status: 'done' | 'paused';
|
|
207
|
+
readonly touchedKeys: readonly string[];
|
|
208
|
+
}[];
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* What {@link Observer.wrap} is asked to enclose.
|
|
213
|
+
*
|
|
214
|
+
* - `step` — one step's `run()`. Hooks (`onStepStart`, `onStepEnd`, …) and `route()` run outside it.
|
|
215
|
+
* - `event` — a call inside a step that an event already opened, named by that event's `type`
|
|
216
|
+
* (e.g. `'tool.call'`, `'llm.request'`), with a `payload` carrying the id the event used
|
|
217
|
+
* (e.g. `{ toolCallId }`, `{ callId }`). Requested via `ctx.within()` or `TelemetryContext.within()`.
|
|
218
|
+
*/
|
|
219
|
+
type WrapScope = {
|
|
220
|
+
readonly kind: 'step';
|
|
221
|
+
} | {
|
|
222
|
+
readonly kind: 'event';
|
|
223
|
+
readonly type: string;
|
|
224
|
+
readonly payload: unknown;
|
|
225
|
+
};
|
|
148
226
|
/**
|
|
149
227
|
* Observability hook interface. All methods are optional — implement only
|
|
150
228
|
* the hooks you need.
|
|
@@ -167,7 +245,11 @@ interface StepContext {
|
|
|
167
245
|
interface Observer {
|
|
168
246
|
/** Called once when a run begins, before the first step executes. */
|
|
169
247
|
onRunStart?: (ctx: RunContext) => void;
|
|
170
|
-
/**
|
|
248
|
+
/**
|
|
249
|
+
* Called once when a run settles — completed, stopped, paused, or rejected by a thrown error
|
|
250
|
+
* (a graph-definition error or a throwing callback). On a rejection `signal` is `'$error'` and
|
|
251
|
+
* the run rejects right after, with the original error.
|
|
252
|
+
*/
|
|
171
253
|
onRunEnd?: (ctx: RunContext, event: {
|
|
172
254
|
signal: string;
|
|
173
255
|
durationMs: number;
|
|
@@ -193,6 +275,28 @@ interface Observer {
|
|
|
193
275
|
* by LLM adapters (e.g. `'llm.response'`).
|
|
194
276
|
*/
|
|
195
277
|
onEvent?: (ctx: StepContext, type: string, payload: unknown) => void;
|
|
278
|
+
/**
|
|
279
|
+
* Called exactly once per step, on every path, once the step's outcome and destination
|
|
280
|
+
* are final: after `onStepEnd` / `onStepError` / `onInterrupt` (when that hook fires on the
|
|
281
|
+
* path) and before `onRunEnd`. Also fires for fork nodes and for steps inside fork branches
|
|
282
|
+
* (with `ctx.branchPath`). Not fired when a run stops before starting a step.
|
|
283
|
+
*/
|
|
284
|
+
onStepSettled?: (ctx: StepContext, event: StepSettledEvent) => void;
|
|
285
|
+
/**
|
|
286
|
+
* Run `fn` inside this observer's scope — e.g. make a span the active context so code in
|
|
287
|
+
* `fn` (auto-instrumented clients, sub agents) nests under it. The only hook that encloses
|
|
288
|
+
* work rather than being notified about it.
|
|
289
|
+
*
|
|
290
|
+
* Call `fn` at most once and return its promise. The harness never trusts the wrapper:
|
|
291
|
+
* `fn` always runs exactly once, and the caller always gets `fn`'s own result. A wrapper
|
|
292
|
+
* that throws before calling `fn` is reported via `onObserverError` (hook name `'wrap'`) and
|
|
293
|
+
* `fn` runs unwrapped; one that never calls `fn` just loses its scope; one that swallows or
|
|
294
|
+
* replaces `fn`'s result is ignored. A wrapper error after `fn` was called is reported too,
|
|
295
|
+
* unless it is `fn`'s own rejection passed through.
|
|
296
|
+
*
|
|
297
|
+
* Not guarded: a wrapper that never settles hangs the step it encloses.
|
|
298
|
+
*/
|
|
299
|
+
wrap?: <T>(ctx: StepContext, scope: WrapScope, fn: () => Promise<T>) => Promise<T>;
|
|
196
300
|
}
|
|
197
301
|
/**
|
|
198
302
|
* The telemetry a resource needs to attribute an `invoke()` call to the correct
|
|
@@ -203,8 +307,18 @@ interface Observer {
|
|
|
203
307
|
* each call carries its own context by value.
|
|
204
308
|
*/
|
|
205
309
|
interface TelemetryContext {
|
|
310
|
+
/**
|
|
311
|
+
* The run's observer, fault-isolated: a hook that throws is reported via `onObserverError`
|
|
312
|
+
* and never reaches the caller. Not the object passed to `agent.run()` itself.
|
|
313
|
+
*/
|
|
206
314
|
readonly observer: Observer;
|
|
207
315
|
readonly stepContext: StepContext;
|
|
316
|
+
/**
|
|
317
|
+
* Run `fn` inside the observer's scope for the event `type` (e.g. `'llm.request'`), whose
|
|
318
|
+
* `payload` carries that event's id (e.g. `{ callId }`). Bound to this step. A passthrough
|
|
319
|
+
* when the observer has no `wrap`; wrapper faults are isolated as for {@link Observer.wrap}.
|
|
320
|
+
*/
|
|
321
|
+
within<T>(type: string, payload: unknown, fn: () => Promise<T>): Promise<T>;
|
|
208
322
|
}
|
|
209
323
|
/**
|
|
210
324
|
* Implemented by resources (e.g. LLM adapters) that emit per-step telemetry.
|
|
@@ -550,6 +664,12 @@ type RunFn<S, Ctx> = (state: StepState<S>, ctx: Ctx & {
|
|
|
550
664
|
readonly signal: AbortSignal;
|
|
551
665
|
readonly interrupt: (prompt: unknown, id?: string) => Promise<unknown>;
|
|
552
666
|
readonly emit: (name: string, payload?: unknown) => void;
|
|
667
|
+
/**
|
|
668
|
+
* Run `fn` inside the observer's scope for an event this step already emitted — `type` is
|
|
669
|
+
* that event's name (e.g. `'tool.call'`), `payload` carries its id (e.g. `{ toolCallId }`).
|
|
670
|
+
* Returns `fn`'s own result. A passthrough when no observer implements `wrap`.
|
|
671
|
+
*/
|
|
672
|
+
readonly within: <T>(type: string, payload: unknown, fn: () => Promise<T>) => Promise<T>;
|
|
553
673
|
readonly keepAlive: KeepAliveFn;
|
|
554
674
|
}) => Promise<Partial<Omit<S, '$error' | '$interrupt'>>> | Partial<Omit<S, '$error' | '$interrupt'>>;
|
|
555
675
|
/**
|
|
@@ -983,4 +1103,4 @@ declare class LeaseExpiredError extends Error {
|
|
|
983
1103
|
constructor(sessionId: string);
|
|
984
1104
|
}
|
|
985
1105
|
|
|
986
|
-
export { type Agent, type BranchCursor, type BranchDef, type ClaimOptions, type Cursor, type DeepWithMarkers, type FieldDefinition, type ForkCursor, type ForkDef, type FrameworkState, type Harness, type Lease, LeaseExpiredError, type LoopDefinition, type LoopNode, LoopNotDefinedError, NoInterruptError, type Observer, type ObserverAware, type ObserverErrorContext, type ObserverErrorSink, REQUIRED_TAG, RUNTIME_TAG, type RequiredMarker, type RouteFn, type RunContext, type RunFn, type RuntimeMarker, SessionBusyError, SessionInFlightError, SessionPendingInterruptError, type SessionStore, type SignalTransition, type StateFromSchema, type StepContext, type StepDef, type StepState, StoreLoadError, type StoredRun, type StoredRunMetadata, type TelemetryContext, type TransitionTarget, composeObservers, createAgent, createHarness, field, isForkCursor, isForkDef, isRequiredMarker, isRuntimeMarker, required, runtime };
|
|
1106
|
+
export { type Agent, type BranchCursor, type BranchDef, type ClaimOptions, type Cursor, type DeepWithMarkers, type FieldDefinition, type ForkCursor, type ForkDef, type FrameworkState, type Harness, type Lease, LeaseExpiredError, type LoopDefinition, type LoopNode, LoopNotDefinedError, NoInterruptError, type Observer, type ObserverAware, type ObserverErrorContext, type ObserverErrorSink, REQUIRED_TAG, RUNTIME_TAG, type RequiredMarker, type RouteFn, type RunContext, type RunFn, type RuntimeMarker, SessionBusyError, SessionInFlightError, SessionPendingInterruptError, type SessionStore, type SignalTransition, type StateFromSchema, type StepContext, type StepDef, type StepSettledEvent, type StepSettledNext, type StepState, StoreLoadError, type StoredRun, type StoredRunMetadata, type TelemetryContext, type TransitionTarget, type WrapScope, composeObservers, createAgent, createHarness, field, isForkCursor, isForkDef, isRequiredMarker, isRuntimeMarker, required, runtime };
|