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
JavaScript
/**
* 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