UNPKG

agentfootprint

Version:

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

110 lines • 6.05 kB
"use strict"; /** * withCredentialRetry — CredentialProvider decorator that retries transient * `getCredential` failures with exponential backoff. * * Pattern: Decorator (GoF) — the credential twin of `withRetry` (resilience) * for LLM providers. SAME option vocabulary (`maxAttempts` / * `initialDelayMs` / `backoffFactor` / `maxDelayMs` / `shouldRetry` / * `onRetry`) and the SAME default transience policy (the shared * `defaultShouldRetry`: skip AbortError + 4xx except 429; retry 5xx, * network errors, unknown shapes). AgentCore's documented transient * errors (`InternalServerException` 500, `ThrottlingException` 429) * retry out of the box. * * Why a decorator and not a reliability rule: the rules-based reliability * subsystem (`Agent.create(...).reliability({...})`) is LLM-call-scoped — * `ReliabilityScope` carries `request: LLMRequest` and the gate chart loops * around the CallLLM stage with provider failover. Credential resolution * happens at a different boundary (the tool-dispatch loop, declare-and-push); * promoting it to a chart-level gate is the deferred `sf-credential` node. * Until that exists, retry is a TRANSPORT property of the credential provider * — exactly like `withRetry` is for LLM transports. * * Semantics: * • Only THROWN errors retry. Both result branches return immediately: * `issued` is success; `authorization-required` is a HUMAN flow (3LO * consent), not a transient fault — retrying it would hammer the IdP * without the user having authorized anything. * • The request's `signal` (`CredentialRequest.signal`) is passed through to * the wrapped provider, ends the backoff wait, and is checked immediately * before every attempt, the first included: a request whose signal has * fired never asks the provider (again), and the call rejects with the * abort. An attempt already running is the provider's to stop. * • After retries exhaust, the LAST error is rethrown — the call site * behaves byte-identically to an unwrapped provider (fail-closed: * `agentfootprint.credential.failed` emit + error tool result; the tool * does NOT run). * • Visibility: the agent trace brackets the whole retried resolution with * `credential.requested` → `credential.acquired` / `credential.failed`. * Per-attempt visibility is consumer-wired via `onRetry` — the established * decorator contract (same as `withRetry`; the `agentfootprint.error.*` * event family is reserved for these decorators, see events/payloads.ts). * No new event types. * * @example * import { agentCoreIdentity, withCredentialRetry } from 'agentfootprint/security'; * * const credentials = withCredentialRetry(agentCoreIdentity({ region: 'us-east-1' }), { * maxAttempts: 3, * onRetry: (err, attempt, ms) => console.warn(`credential retry ${attempt} in ${ms}ms`, err), * }); * const agent = Agent.create({ provider, model, credentials }).tools([...]).build(); */ Object.defineProperty(exports, "__esModule", { value: true }); exports.withCredentialRetry = void 0; const abort_js_1 = require("../lib/abort.js"); const sleep_js_1 = require("../lib/sleep.js"); const withRetry_js_1 = require("../resilience/withRetry.js"); /** * Wrap a {@link CredentialProvider} so transient `getCredential` failures * retry with exponential backoff before failing closed. * * Defaults mirror `withRetry`: 3 attempts total, 200ms → 400ms → 800ms * backoff capped at 10s, retry on 5xx/429/network/unknown, never on * AbortError or other 4xx. */ function withCredentialRetry(provider, options = {}) { const maxAttempts = Math.max(1, options.maxAttempts ?? 3); const initialDelayMs = options.initialDelayMs ?? 200; const backoffFactor = options.backoffFactor ?? 2; const maxDelayMs = options.maxDelayMs ?? 10_000; const shouldRetry = options.shouldRetry ?? withRetry_js_1.defaultShouldRetry; const onRetry = options.onRetry; return { id: `${provider.id}+retry`, async getCredential(req) { let lastError; for (let attempt = 1; attempt <= maxAttempts; attempt++) { // Immediately before EVERY attempt, the first included: a request whose // signal has fired never asks the provider — not before the first // attempt, and not after a wait or an `onRetry` that cancelled it (a // zero wait never consults the signal). (0, abort_js_1.throwIfAborted)(req.signal); try { // Both result branches return as-is (same object — the wrapper never // clones or serializes the credential): `issued` is success; // `authorization-required` is consent, not a fault — never retried. return await provider.getCredential(req); } catch (err) { lastError = err; // A failure that lands after the abort is the abort, whatever // `shouldRetry` says: nobody is waiting for the credential (`lib/abort.ts`). (0, abort_js_1.throwIfAborted)(req.signal); if (attempt >= maxAttempts || !shouldRetry(err, attempt)) { throw err; } const delay = Math.min(maxDelayMs, initialDelayMs * Math.pow(backoffFactor, attempt - 1)); onRetry?.(err, attempt + 1, delay); // The request's signal (`CredentialRequest.signal`) ends the wait too. await (0, sleep_js_1.sleep)(delay, req.signal, abort_js_1.abortErrorOf); } } // Unreachable — the last attempt either returned or threw above. throw lastError; }, }; } exports.withCredentialRetry = withCredentialRetry; //# sourceMappingURL=withCredentialRetry.js.map