@noetaris/harness 0.6.0 → 0.8.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 +78 -6
- package/dist/index.d.ts +66 -2
- package/dist/index.js +209 -152
- package/dist/index.js.map +1 -1
- package/package.json +13 -9
package/README.md
CHANGED
|
@@ -249,18 +249,24 @@ paused in its store — clean it up or reuse it the next time the step runs.
|
|
|
249
249
|
An `Observer` receives telemetry hooks for a run. Pass it as `observer` in the resources
|
|
250
250
|
of `agent.run()` or `agent.resume()`. Combine several with `composeObservers([a, b])`;
|
|
251
251
|
a hook that throws is reported to `onObserverError` (or `console.error`) and never stops
|
|
252
|
-
the run.
|
|
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.
|
|
253
254
|
|
|
254
255
|
| Hook | Fires |
|
|
255
256
|
|---|---|
|
|
256
257
|
| `onRunStart(ctx)` | once when a run starts |
|
|
257
|
-
| `onRunEnd(ctx, { signal, durationMs })` | once when a run settles (`$stopped`
|
|
258
|
+
| `onRunEnd(ctx, { signal, durationMs, error? })` | once when a run settles — completed, stopped (`$stopped`), paused, or rejected by a thrown error (`$error`). `error` is set when the run ends with `$error` because of an error no `onError` step handled (paused) or a rejection; a route that returns `'$error'` itself gives no `error`, so test for `error`, not the signal |
|
|
258
259
|
| `onStepStart(ctx)` | before each step |
|
|
259
260
|
| `onStepEnd(ctx, { durationMs })` | after a step's `run` succeeds, before its `route` |
|
|
260
261
|
| `onStepError(ctx, { error, durationMs })` | when a step's `run` throws |
|
|
261
262
|
| `onInterrupt(ctx, { prompt, interruptId })` | when a step calls `ctx.interrupt()` |
|
|
262
263
|
| `onEvent(ctx, type, payload)` | on `ctx.emit()` and adapter events such as `llm.response` |
|
|
263
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.
|
|
264
270
|
|
|
265
271
|
**`onStepSettled`** is the one hook that tells you, for every step, how it ended and
|
|
266
272
|
where the run goes next. It fires exactly once for each `onStepStart`, on every path:
|
|
@@ -271,7 +277,8 @@ onStepStart → onStepEnd | onStepError | onInterrupt → onStepSettled → onRu
|
|
|
271
277
|
|
|
272
278
|
It also fires for a fork node and for each step inside a branch (`ctx.branchPath` names the
|
|
273
279
|
branch). A graph-definition error (`UnknownSignalError`, `NoNextStepError`,
|
|
274
|
-
`MissingReducerError`) settles with `next.kind === 'throw'` before the run rejects
|
|
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
|
|
275
282
|
not fire when a run stops before starting a step, or for a finished branch that a resume
|
|
276
283
|
skips.
|
|
277
284
|
|
|
@@ -307,6 +314,68 @@ await createAgent('demo', h, {}).run({}, { observer: logger })
|
|
|
307
314
|
// decide: ok end (signal done)
|
|
308
315
|
```
|
|
309
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
|
+
|
|
310
379
|
## API
|
|
311
380
|
|
|
312
381
|
| Export | Description |
|
|
@@ -322,9 +391,11 @@ await createAgent('demo', h, {}).run({}, { observer: logger })
|
|
|
322
391
|
| `Observer` | Interface for telemetry hooks on run and step lifecycle events (see [Observers](#observers)). |
|
|
323
392
|
| `StepSettledEvent` | Payload of `Observer.onStepSettled` — outcome, duration, update, signal, next, error, interrupt, fork. |
|
|
324
393
|
| `StepSettledNext` | Where the run goes after a step settles: `step`, `end`, `pause`, or `throw`. |
|
|
325
|
-
| `ObserverAware` | Interface for provider objects that
|
|
326
|
-
| `
|
|
327
|
-
| `
|
|
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?`. |
|
|
328
399
|
| `NoInterruptError` | Thrown when `resume()` is called but the session is not paused on a matching interrupt. |
|
|
329
400
|
| `SessionInFlightError` | Thrown when a session is already running. |
|
|
330
401
|
| `SessionPendingInterruptError` | Thrown when a session is paused on a pending interrupt — use `agent.resume()` instead of `agent.run()`. |
|
|
@@ -353,6 +424,7 @@ See [LIMITATIONS.md](LIMITATIONS.md) for known limitations.
|
|
|
353
424
|
- [`@noetaris/harness-openai`](https://github.com/noetaris-lab/harness-openai) — OpenAI adapter
|
|
354
425
|
- [`@noetaris/harness-google`](https://github.com/noetaris-lab/harness-google) — Google Gemini adapter
|
|
355
426
|
- [`@noetaris/harness-otel`](https://github.com/noetaris-lab/harness-otel) — OpenTelemetry observer bridge
|
|
427
|
+
- [`@noetaris/harness-otel-genai`](https://github.com/noetaris-lab/harness-otel-genai) — GenAI semantic-conventions extension for the bridge
|
|
356
428
|
|
|
357
429
|
## License
|
|
358
430
|
|
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
|
|
@@ -201,6 +208,21 @@ interface StepSettledEvent {
|
|
|
201
208
|
}[];
|
|
202
209
|
};
|
|
203
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
|
+
};
|
|
204
226
|
/**
|
|
205
227
|
* Observability hook interface. All methods are optional — implement only
|
|
206
228
|
* the hooks you need.
|
|
@@ -223,10 +245,21 @@ interface StepSettledEvent {
|
|
|
223
245
|
interface Observer {
|
|
224
246
|
/** Called once when a run begins, before the first step executes. */
|
|
225
247
|
onRunStart?: (ctx: RunContext) => void;
|
|
226
|
-
/**
|
|
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).
|
|
251
|
+
*
|
|
252
|
+
* When the run ends because of an error, `signal` is `'$error'` and `error` holds it: the error
|
|
253
|
+
* a step's `run` or `route` threw, or a failed fork's branch error, when no `onError` step
|
|
254
|
+
* handled it (the run resolves paused); or the original thrown value when the run rejects (it
|
|
255
|
+
* rejects right after). `error` is absent on every other ending. A route may also return
|
|
256
|
+
* `'$error'` as an ordinary signal; that ending has no `error` key, so test for `error`, not
|
|
257
|
+
* for the signal.
|
|
258
|
+
*/
|
|
227
259
|
onRunEnd?: (ctx: RunContext, event: {
|
|
228
260
|
signal: string;
|
|
229
261
|
durationMs: number;
|
|
262
|
+
error?: unknown;
|
|
230
263
|
}) => void;
|
|
231
264
|
/** Called immediately before each step's `run` function is invoked. */
|
|
232
265
|
onStepStart?: (ctx: StepContext) => void;
|
|
@@ -256,6 +289,21 @@ interface Observer {
|
|
|
256
289
|
* (with `ctx.branchPath`). Not fired when a run stops before starting a step.
|
|
257
290
|
*/
|
|
258
291
|
onStepSettled?: (ctx: StepContext, event: StepSettledEvent) => void;
|
|
292
|
+
/**
|
|
293
|
+
* Run `fn` inside this observer's scope — e.g. make a span the active context so code in
|
|
294
|
+
* `fn` (auto-instrumented clients, sub agents) nests under it. The only hook that encloses
|
|
295
|
+
* work rather than being notified about it.
|
|
296
|
+
*
|
|
297
|
+
* Call `fn` at most once and return its promise. The harness never trusts the wrapper:
|
|
298
|
+
* `fn` always runs exactly once, and the caller always gets `fn`'s own result. A wrapper
|
|
299
|
+
* that throws before calling `fn` is reported via `onObserverError` (hook name `'wrap'`) and
|
|
300
|
+
* `fn` runs unwrapped; one that never calls `fn` just loses its scope; one that swallows or
|
|
301
|
+
* replaces `fn`'s result is ignored. A wrapper error after `fn` was called is reported too,
|
|
302
|
+
* unless it is `fn`'s own rejection passed through.
|
|
303
|
+
*
|
|
304
|
+
* Not guarded: a wrapper that never settles hangs the step it encloses.
|
|
305
|
+
*/
|
|
306
|
+
wrap?: <T>(ctx: StepContext, scope: WrapScope, fn: () => Promise<T>) => Promise<T>;
|
|
259
307
|
}
|
|
260
308
|
/**
|
|
261
309
|
* The telemetry a resource needs to attribute an `invoke()` call to the correct
|
|
@@ -266,8 +314,18 @@ interface Observer {
|
|
|
266
314
|
* each call carries its own context by value.
|
|
267
315
|
*/
|
|
268
316
|
interface TelemetryContext {
|
|
317
|
+
/**
|
|
318
|
+
* The run's observer, fault-isolated: a hook that throws is reported via `onObserverError`
|
|
319
|
+
* and never reaches the caller. Not the object passed to `agent.run()` itself.
|
|
320
|
+
*/
|
|
269
321
|
readonly observer: Observer;
|
|
270
322
|
readonly stepContext: StepContext;
|
|
323
|
+
/**
|
|
324
|
+
* Run `fn` inside the observer's scope for the event `type` (e.g. `'llm.request'`), whose
|
|
325
|
+
* `payload` carries that event's id (e.g. `{ callId }`). Bound to this step. A passthrough
|
|
326
|
+
* when the observer has no `wrap`; wrapper faults are isolated as for {@link Observer.wrap}.
|
|
327
|
+
*/
|
|
328
|
+
within<T>(type: string, payload: unknown, fn: () => Promise<T>): Promise<T>;
|
|
271
329
|
}
|
|
272
330
|
/**
|
|
273
331
|
* Implemented by resources (e.g. LLM adapters) that emit per-step telemetry.
|
|
@@ -613,6 +671,12 @@ type RunFn<S, Ctx> = (state: StepState<S>, ctx: Ctx & {
|
|
|
613
671
|
readonly signal: AbortSignal;
|
|
614
672
|
readonly interrupt: (prompt: unknown, id?: string) => Promise<unknown>;
|
|
615
673
|
readonly emit: (name: string, payload?: unknown) => void;
|
|
674
|
+
/**
|
|
675
|
+
* Run `fn` inside the observer's scope for an event this step already emitted — `type` is
|
|
676
|
+
* that event's name (e.g. `'tool.call'`), `payload` carries its id (e.g. `{ toolCallId }`).
|
|
677
|
+
* Returns `fn`'s own result. A passthrough when no observer implements `wrap`.
|
|
678
|
+
*/
|
|
679
|
+
readonly within: <T>(type: string, payload: unknown, fn: () => Promise<T>) => Promise<T>;
|
|
616
680
|
readonly keepAlive: KeepAliveFn;
|
|
617
681
|
}) => Promise<Partial<Omit<S, '$error' | '$interrupt'>>> | Partial<Omit<S, '$error' | '$interrupt'>>;
|
|
618
682
|
/**
|
|
@@ -1046,4 +1110,4 @@ declare class LeaseExpiredError extends Error {
|
|
|
1046
1110
|
constructor(sessionId: string);
|
|
1047
1111
|
}
|
|
1048
1112
|
|
|
1049
|
-
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, composeObservers, createAgent, createHarness, field, isForkCursor, isForkDef, isRequiredMarker, isRuntimeMarker, required, runtime };
|
|
1113
|
+
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 };
|