UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

90 lines • 4.5 kB
/** * resilienceHooks — turns a resilience decorator's `ResilienceReport` * into an already-declared typed event, emitted from inside the run. * * Pattern: Adapter (GoF) — one translation function, fact → event. * Role: The seam between `src/resilience/` (which knows nothing about * runs, scopes or events) and the in-run LLM call sites (which * hold a live scope). The decorator REPORTS; this maps the * report to `typedEmit`, so correlation ids come from * footprintjs's traversal rather than being synthesized. * Emits: agentfootprint.fallback.triggered * agentfootprint.error.retried * agentfootprint.error.recovered * agentfootprint.error.circuit_changed (9.32.0) * * Why here and not in the decorators: a decorator is constructed by the * consumer BEFORE any run exists, so it can never reach a scope. Passing * the hooks object per call is what lets the report cross that boundary. * * Every declared payload field is filled from a report field of the same * name — nothing is synthesized. `FallbackTriggeredPayload.kind` is the * literal `'provider'`; its `'tool'`/`'skill'` arms stay unused. */ import { typedEmit } from './typedEmit.js'; import { replayPreparedHook } from './replayHooks.js'; /** * Build the per-call hooks object for one stage. Hoist it once per stage * rather than per attempt — it is stateless and safe to reuse across * reliability-loop iterations. * * @example * const providerHooks = resilienceHooks(scope); * const response = await provider.complete(request, providerHooks); */ export function resilienceHooks(scope, receiptMinted = false, onReplayPersisted) { return { onReplayPrepared: replayPreparedHook(scope, receiptMinted, onReplayPersisted), onResilience(report) { // The library owns THIS sink, and it is the only one that runs // in-run — so guard it once here. Telemetry must never break an // LLM call (footprintjs's "recorder errors never abort traversal" // invariant). A CONSUMER-supplied onResilience is deliberately // unguarded, matching onRetry/onFallback/onStateChange. try { switch (report.kind) { case 'fell-back': typedEmit(scope, 'agentfootprint.fallback.triggered', { kind: 'provider', primary: report.primary, fallback: report.fallback, reason: report.reason, }); return; case 'retried': typedEmit(scope, 'agentfootprint.error.retried', { attempt: report.attempt, maxAttempts: report.maxAttempts, lastError: report.lastError, backoffMs: report.backoffMs, reason: report.reason, ...(report.statedWaitMs !== undefined && { statedWaitMs: report.statedWaitMs }), }); return; case 'recovered': typedEmit(scope, 'agentfootprint.error.recovered', { attempt: report.attempt, totalDurationMs: report.totalDurationMs, }); return; case 'circuit-changed': // 9.32.0 — the transition happened INSIDE this call, so the ids // footprintjs stamped before the stage ran are the right ones and // nothing here is synthesized. That is the whole reason the report // rides the per-call hooks rather than the breaker's own // `onStateChange`, which fires wherever the breaker lives. typedEmit(scope, 'agentfootprint.error.circuit_changed', { state: report.state, reason: report.reason, providerName: report.providerName, }); return; } } catch { // Swallowed by design — see the comment above. } }, }; } //# sourceMappingURL=resilienceHooks.js.map