agentfootprint 2.9.0 → 2.10.1

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.
@@ -0,0 +1,229 @@
1
+ /**
2
+ * withCircuitBreaker — provider decorator that fails fast after N
3
+ * consecutive failures.
4
+ *
5
+ * Pattern: Circuit Breaker (Nygard, *Release It!*) — wraps an
6
+ * `LLMProvider` and tracks consecutive failures. After
7
+ * `failureThreshold` failures, the breaker OPENS and
8
+ * rejects all calls without invoking the wrapped provider.
9
+ * After `cooldownMs`, the breaker enters HALF-OPEN and
10
+ * allows probe calls; success closes the breaker, failure
11
+ * re-opens it.
12
+ *
13
+ * Role: Outer ring (Hexagonal). Composes with `withRetry` and
14
+ * `withFallback`:
15
+ *
16
+ * ```
17
+ * withFallback(
18
+ * withCircuitBreaker(anthropic(...)), // ← stop hammering on outage
19
+ * withCircuitBreaker(openai(...)),
20
+ * )
21
+ * ```
22
+ *
23
+ * When Anthropic 503s for the 5th time, the breaker opens
24
+ * and `complete()` throws `CircuitOpenError` immediately —
25
+ * no network round-trip — which `withFallback` then
26
+ * catches and routes to OpenAI. After 30 seconds the
27
+ * breaker probes Anthropic with a single call; if it
28
+ * succeeds, normal operation resumes.
29
+ *
30
+ * Why a circuit breaker on top of `withRetry`?
31
+ * - `withRetry` keeps hammering one provider with exponential
32
+ * backoff — it doesn't know the vendor is down.
33
+ * - During a multi-minute Anthropic outage, every request still
34
+ * burns 3 retries + backoff = ~3 sec of latency before failing
35
+ * to the fallback. Multiplied by your QPS, that's a lot of
36
+ * wasted time + tokens (some retries DO get billed).
37
+ * - The breaker says: "we just saw 5 failures in a row; stop
38
+ * calling for 30 seconds." Subsequent requests fail in <1ms,
39
+ * `withFallback` routes immediately to OpenAI.
40
+ *
41
+ * Three states:
42
+ *
43
+ * CLOSED ──[ N consecutive failures ]──► OPEN
44
+ * ▲ │
45
+ * │ │ [cooldownMs elapsed]
46
+ * │ ▼
47
+ * └──[ M probe successes ]──── HALF-OPEN
48
+ *
49
+ * HALF-OPEN ──[ probe failure ]──► OPEN (cooldown restarts)
50
+ *
51
+ * `stream()` is decorated identically. `name`/`flush`/`stop` pass
52
+ * through unchanged (the consumer's existing observability hooks
53
+ * still see the underlying provider's identity).
54
+ *
55
+ * **Scope: per-instance, NOT distributed.** Each `withCircuitBreaker(...)`
56
+ * call holds its own breaker state in process memory. If you run 100
57
+ * server replicas, each has its own independent breaker — one
58
+ * instance can be CLOSED while another is OPEN. This is intentional
59
+ * (no shared state means no Redis dependency, no SPOF, no
60
+ * partial-cluster-blast-radius surprises) and matches Hystrix's
61
+ * default behavior. For cluster-wide coordination, layer your own
62
+ * Redis-backed counter on top via the `onStateChange` hook +
63
+ * `shouldCount` predicate.
64
+ */
65
+ // ─── Public error type ───────────────────────────────────────────────
66
+ /**
67
+ * Thrown by the wrapped provider when the breaker is OPEN. Carries
68
+ * the underlying root-cause error from the most recent failure so
69
+ * consumers can observe what tripped the breaker.
70
+ */
71
+ export class CircuitOpenError extends Error {
72
+ code = 'ERR_CIRCUIT_OPEN';
73
+ /** The error that tripped the breaker (or the most recent failure
74
+ * during HALF-OPEN that re-opened it). */
75
+ cause;
76
+ /** Wall-clock timestamp at which the breaker may next probe. */
77
+ retryAfter;
78
+ constructor(providerName, cause, retryAfter) {
79
+ super(`[${providerName}] circuit breaker is OPEN — failing fast (next probe at ${new Date(retryAfter).toISOString()}). Underlying error: ${cause?.message ?? String(cause)}`);
80
+ this.name = 'CircuitOpenError';
81
+ this.cause = cause;
82
+ this.retryAfter = retryAfter;
83
+ }
84
+ }
85
+ /**
86
+ * Wrap a provider with a circuit breaker.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * import { anthropic, openai } from 'agentfootprint/llm-providers';
91
+ * import { withCircuitBreaker, withFallback } from 'agentfootprint/resilience';
92
+ *
93
+ * const provider = withFallback(
94
+ * withCircuitBreaker(anthropic({ apiKey }), { failureThreshold: 5, cooldownMs: 30_000 }),
95
+ * withCircuitBreaker(openai({ apiKey })),
96
+ * );
97
+ * ```
98
+ */
99
+ export function withCircuitBreaker(inner, options = {}) {
100
+ const failureThreshold = options.failureThreshold ?? 5;
101
+ const cooldownMs = options.cooldownMs ?? 30_000;
102
+ const halfOpenSuccessThreshold = options.halfOpenSuccessThreshold ?? 2;
103
+ const shouldCount = options.shouldCount ?? defaultShouldCount;
104
+ const onStateChange = options.onStateChange;
105
+ const breaker = {
106
+ state: 'closed',
107
+ consecutiveFailures: 0,
108
+ consecutiveSuccesses: 0,
109
+ openedAt: 0,
110
+ lastError: undefined,
111
+ };
112
+ function transition(next, reason) {
113
+ if (breaker.state === next)
114
+ return;
115
+ breaker.state = next;
116
+ if (next === 'open') {
117
+ breaker.openedAt = Date.now();
118
+ breaker.consecutiveSuccesses = 0;
119
+ }
120
+ else if (next === 'half-open') {
121
+ breaker.consecutiveSuccesses = 0;
122
+ }
123
+ else if (next === 'closed') {
124
+ breaker.consecutiveFailures = 0;
125
+ breaker.consecutiveSuccesses = 0;
126
+ breaker.lastError = undefined;
127
+ }
128
+ onStateChange?.(next, reason);
129
+ }
130
+ /** Decide whether to admit a call. Mutates state if cooldown
131
+ * elapsed (open → half-open). Returns true to admit, false to
132
+ * reject with CircuitOpenError. */
133
+ function admit() {
134
+ if (breaker.state === 'closed' || breaker.state === 'half-open')
135
+ return true;
136
+ // OPEN — check cooldown.
137
+ if (Date.now() - breaker.openedAt >= cooldownMs) {
138
+ transition('half-open', 'cooldown elapsed');
139
+ return true;
140
+ }
141
+ return false;
142
+ }
143
+ function recordSuccess() {
144
+ if (breaker.state === 'half-open') {
145
+ breaker.consecutiveSuccesses += 1;
146
+ if (breaker.consecutiveSuccesses >= halfOpenSuccessThreshold) {
147
+ transition('closed', `${halfOpenSuccessThreshold} probe successes`);
148
+ }
149
+ }
150
+ else if (breaker.state === 'closed') {
151
+ // Successful call resets the failure counter.
152
+ breaker.consecutiveFailures = 0;
153
+ }
154
+ }
155
+ function recordFailure(err) {
156
+ if (!shouldCount(err))
157
+ return;
158
+ breaker.lastError = err;
159
+ if (breaker.state === 'half-open') {
160
+ // Probe failed — re-open the breaker.
161
+ transition('open', 'half-open probe failed');
162
+ return;
163
+ }
164
+ if (breaker.state === 'closed') {
165
+ breaker.consecutiveFailures += 1;
166
+ if (breaker.consecutiveFailures >= failureThreshold) {
167
+ transition('open', `${breaker.consecutiveFailures} consecutive failures`);
168
+ }
169
+ }
170
+ }
171
+ function rejectFastIfOpen() {
172
+ if (!admit()) {
173
+ throw new CircuitOpenError(inner.name, breaker.lastError, breaker.openedAt + cooldownMs);
174
+ }
175
+ }
176
+ const wrapped = {
177
+ name: inner.name,
178
+ async complete(req) {
179
+ rejectFastIfOpen();
180
+ try {
181
+ const res = await inner.complete(req);
182
+ recordSuccess();
183
+ return res;
184
+ }
185
+ catch (err) {
186
+ recordFailure(err);
187
+ throw err;
188
+ }
189
+ },
190
+ // `stream` is optional on `LLMProvider`. Only define our wrapper
191
+ // if the underlying provider supports streaming — otherwise leave
192
+ // it undefined so the consumer's existing capability check
193
+ // (`if (provider.stream)`) still works correctly.
194
+ ...(inner.stream && {
195
+ async *stream(req) {
196
+ rejectFastIfOpen();
197
+ let yieldedAnyChunk = false;
198
+ try {
199
+ for await (const chunk of inner.stream(req)) {
200
+ yieldedAnyChunk = true;
201
+ yield chunk;
202
+ }
203
+ recordSuccess();
204
+ }
205
+ catch (err) {
206
+ // Only count as a breaker-tripping failure if the stream
207
+ // failed BEFORE yielding any tokens. Mid-stream errors are
208
+ // less indicative of vendor health (could be a content-filter
209
+ // trip on this specific request).
210
+ if (!yieldedAnyChunk)
211
+ recordFailure(err);
212
+ throw err;
213
+ }
214
+ },
215
+ }),
216
+ };
217
+ return wrapped;
218
+ }
219
+ // ─── Default predicates ──────────────────────────────────────────────
220
+ function defaultShouldCount(error) {
221
+ // Don't count user cancellations.
222
+ const e = error;
223
+ if (e?.name === 'AbortError')
224
+ return false;
225
+ if (e?.code === 'ABORT_ERR')
226
+ return false;
227
+ return true;
228
+ }
229
+ //# sourceMappingURL=withCircuitBreaker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withCircuitBreaker.js","sourceRoot":"","sources":["../../../src/resilience/withCircuitBreaker.ts"],"names":[],"mappings":"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"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -17,11 +17,14 @@
17
17
  * chain is wrapped in retry with 5 attempts.
18
18
  */
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.fallbackProvider = exports.withFallback = exports.withRetry = void 0;
20
+ exports.CircuitOpenError = exports.withCircuitBreaker = exports.fallbackProvider = exports.withFallback = exports.withRetry = void 0;
21
21
  var withRetry_js_1 = require("./withRetry.js");
22
22
  Object.defineProperty(exports, "withRetry", { enumerable: true, get: function () { return withRetry_js_1.withRetry; } });
23
23
  var withFallback_js_1 = require("./withFallback.js");
24
24
  Object.defineProperty(exports, "withFallback", { enumerable: true, get: function () { return withFallback_js_1.withFallback; } });
25
25
  var fallbackProvider_js_1 = require("./fallbackProvider.js");
26
26
  Object.defineProperty(exports, "fallbackProvider", { enumerable: true, get: function () { return fallbackProvider_js_1.fallbackProvider; } });
27
+ var withCircuitBreaker_js_1 = require("./withCircuitBreaker.js");
28
+ Object.defineProperty(exports, "withCircuitBreaker", { enumerable: true, get: function () { return withCircuitBreaker_js_1.withCircuitBreaker; } });
29
+ Object.defineProperty(exports, "CircuitOpenError", { enumerable: true, get: function () { return withCircuitBreaker_js_1.CircuitOpenError; } });
27
30
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/resilience/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG;;;AAEH,+CAAkE;AAAzD,yGAAA,SAAS,OAAA;AAClB,qDAA2E;AAAlE,+GAAA,YAAY,OAAA;AACrB,6DAAuF;AAA9E,uHAAA,gBAAgB,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/resilience/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG;;;AAEH,+CAAkE;AAAzD,yGAAA,SAAS,OAAA;AAClB,qDAA2E;AAAlE,+GAAA,YAAY,OAAA;AACrB,6DAAuF;AAA9E,uHAAA,gBAAgB,OAAA;AACzB,iEAKiC;AAJ/B,2HAAA,kBAAkB,OAAA;AAClB,yHAAA,gBAAgB,OAAA"}
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ /**
3
+ * withCircuitBreaker — provider decorator that fails fast after N
4
+ * consecutive failures.
5
+ *
6
+ * Pattern: Circuit Breaker (Nygard, *Release It!*) — wraps an
7
+ * `LLMProvider` and tracks consecutive failures. After
8
+ * `failureThreshold` failures, the breaker OPENS and
9
+ * rejects all calls without invoking the wrapped provider.
10
+ * After `cooldownMs`, the breaker enters HALF-OPEN and
11
+ * allows probe calls; success closes the breaker, failure
12
+ * re-opens it.
13
+ *
14
+ * Role: Outer ring (Hexagonal). Composes with `withRetry` and
15
+ * `withFallback`:
16
+ *
17
+ * ```
18
+ * withFallback(
19
+ * withCircuitBreaker(anthropic(...)), // ← stop hammering on outage
20
+ * withCircuitBreaker(openai(...)),
21
+ * )
22
+ * ```
23
+ *
24
+ * When Anthropic 503s for the 5th time, the breaker opens
25
+ * and `complete()` throws `CircuitOpenError` immediately —
26
+ * no network round-trip — which `withFallback` then
27
+ * catches and routes to OpenAI. After 30 seconds the
28
+ * breaker probes Anthropic with a single call; if it
29
+ * succeeds, normal operation resumes.
30
+ *
31
+ * Why a circuit breaker on top of `withRetry`?
32
+ * - `withRetry` keeps hammering one provider with exponential
33
+ * backoff — it doesn't know the vendor is down.
34
+ * - During a multi-minute Anthropic outage, every request still
35
+ * burns 3 retries + backoff = ~3 sec of latency before failing
36
+ * to the fallback. Multiplied by your QPS, that's a lot of
37
+ * wasted time + tokens (some retries DO get billed).
38
+ * - The breaker says: "we just saw 5 failures in a row; stop
39
+ * calling for 30 seconds." Subsequent requests fail in <1ms,
40
+ * `withFallback` routes immediately to OpenAI.
41
+ *
42
+ * Three states:
43
+ *
44
+ * CLOSED ──[ N consecutive failures ]──► OPEN
45
+ * ▲ │
46
+ * │ │ [cooldownMs elapsed]
47
+ * │ ▼
48
+ * └──[ M probe successes ]──── HALF-OPEN
49
+ *
50
+ * HALF-OPEN ──[ probe failure ]──► OPEN (cooldown restarts)
51
+ *
52
+ * `stream()` is decorated identically. `name`/`flush`/`stop` pass
53
+ * through unchanged (the consumer's existing observability hooks
54
+ * still see the underlying provider's identity).
55
+ *
56
+ * **Scope: per-instance, NOT distributed.** Each `withCircuitBreaker(...)`
57
+ * call holds its own breaker state in process memory. If you run 100
58
+ * server replicas, each has its own independent breaker — one
59
+ * instance can be CLOSED while another is OPEN. This is intentional
60
+ * (no shared state means no Redis dependency, no SPOF, no
61
+ * partial-cluster-blast-radius surprises) and matches Hystrix's
62
+ * default behavior. For cluster-wide coordination, layer your own
63
+ * Redis-backed counter on top via the `onStateChange` hook +
64
+ * `shouldCount` predicate.
65
+ */
66
+ Object.defineProperty(exports, "__esModule", { value: true });
67
+ exports.withCircuitBreaker = exports.CircuitOpenError = void 0;
68
+ // ─── Public error type ───────────────────────────────────────────────
69
+ /**
70
+ * Thrown by the wrapped provider when the breaker is OPEN. Carries
71
+ * the underlying root-cause error from the most recent failure so
72
+ * consumers can observe what tripped the breaker.
73
+ */
74
+ class CircuitOpenError extends Error {
75
+ code = 'ERR_CIRCUIT_OPEN';
76
+ /** The error that tripped the breaker (or the most recent failure
77
+ * during HALF-OPEN that re-opened it). */
78
+ cause;
79
+ /** Wall-clock timestamp at which the breaker may next probe. */
80
+ retryAfter;
81
+ constructor(providerName, cause, retryAfter) {
82
+ super(`[${providerName}] circuit breaker is OPEN — failing fast (next probe at ${new Date(retryAfter).toISOString()}). Underlying error: ${cause?.message ?? String(cause)}`);
83
+ this.name = 'CircuitOpenError';
84
+ this.cause = cause;
85
+ this.retryAfter = retryAfter;
86
+ }
87
+ }
88
+ exports.CircuitOpenError = CircuitOpenError;
89
+ /**
90
+ * Wrap a provider with a circuit breaker.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * import { anthropic, openai } from 'agentfootprint/llm-providers';
95
+ * import { withCircuitBreaker, withFallback } from 'agentfootprint/resilience';
96
+ *
97
+ * const provider = withFallback(
98
+ * withCircuitBreaker(anthropic({ apiKey }), { failureThreshold: 5, cooldownMs: 30_000 }),
99
+ * withCircuitBreaker(openai({ apiKey })),
100
+ * );
101
+ * ```
102
+ */
103
+ function withCircuitBreaker(inner, options = {}) {
104
+ const failureThreshold = options.failureThreshold ?? 5;
105
+ const cooldownMs = options.cooldownMs ?? 30_000;
106
+ const halfOpenSuccessThreshold = options.halfOpenSuccessThreshold ?? 2;
107
+ const shouldCount = options.shouldCount ?? defaultShouldCount;
108
+ const onStateChange = options.onStateChange;
109
+ const breaker = {
110
+ state: 'closed',
111
+ consecutiveFailures: 0,
112
+ consecutiveSuccesses: 0,
113
+ openedAt: 0,
114
+ lastError: undefined,
115
+ };
116
+ function transition(next, reason) {
117
+ if (breaker.state === next)
118
+ return;
119
+ breaker.state = next;
120
+ if (next === 'open') {
121
+ breaker.openedAt = Date.now();
122
+ breaker.consecutiveSuccesses = 0;
123
+ }
124
+ else if (next === 'half-open') {
125
+ breaker.consecutiveSuccesses = 0;
126
+ }
127
+ else if (next === 'closed') {
128
+ breaker.consecutiveFailures = 0;
129
+ breaker.consecutiveSuccesses = 0;
130
+ breaker.lastError = undefined;
131
+ }
132
+ onStateChange?.(next, reason);
133
+ }
134
+ /** Decide whether to admit a call. Mutates state if cooldown
135
+ * elapsed (open → half-open). Returns true to admit, false to
136
+ * reject with CircuitOpenError. */
137
+ function admit() {
138
+ if (breaker.state === 'closed' || breaker.state === 'half-open')
139
+ return true;
140
+ // OPEN — check cooldown.
141
+ if (Date.now() - breaker.openedAt >= cooldownMs) {
142
+ transition('half-open', 'cooldown elapsed');
143
+ return true;
144
+ }
145
+ return false;
146
+ }
147
+ function recordSuccess() {
148
+ if (breaker.state === 'half-open') {
149
+ breaker.consecutiveSuccesses += 1;
150
+ if (breaker.consecutiveSuccesses >= halfOpenSuccessThreshold) {
151
+ transition('closed', `${halfOpenSuccessThreshold} probe successes`);
152
+ }
153
+ }
154
+ else if (breaker.state === 'closed') {
155
+ // Successful call resets the failure counter.
156
+ breaker.consecutiveFailures = 0;
157
+ }
158
+ }
159
+ function recordFailure(err) {
160
+ if (!shouldCount(err))
161
+ return;
162
+ breaker.lastError = err;
163
+ if (breaker.state === 'half-open') {
164
+ // Probe failed — re-open the breaker.
165
+ transition('open', 'half-open probe failed');
166
+ return;
167
+ }
168
+ if (breaker.state === 'closed') {
169
+ breaker.consecutiveFailures += 1;
170
+ if (breaker.consecutiveFailures >= failureThreshold) {
171
+ transition('open', `${breaker.consecutiveFailures} consecutive failures`);
172
+ }
173
+ }
174
+ }
175
+ function rejectFastIfOpen() {
176
+ if (!admit()) {
177
+ throw new CircuitOpenError(inner.name, breaker.lastError, breaker.openedAt + cooldownMs);
178
+ }
179
+ }
180
+ const wrapped = {
181
+ name: inner.name,
182
+ async complete(req) {
183
+ rejectFastIfOpen();
184
+ try {
185
+ const res = await inner.complete(req);
186
+ recordSuccess();
187
+ return res;
188
+ }
189
+ catch (err) {
190
+ recordFailure(err);
191
+ throw err;
192
+ }
193
+ },
194
+ // `stream` is optional on `LLMProvider`. Only define our wrapper
195
+ // if the underlying provider supports streaming — otherwise leave
196
+ // it undefined so the consumer's existing capability check
197
+ // (`if (provider.stream)`) still works correctly.
198
+ ...(inner.stream && {
199
+ async *stream(req) {
200
+ rejectFastIfOpen();
201
+ let yieldedAnyChunk = false;
202
+ try {
203
+ for await (const chunk of inner.stream(req)) {
204
+ yieldedAnyChunk = true;
205
+ yield chunk;
206
+ }
207
+ recordSuccess();
208
+ }
209
+ catch (err) {
210
+ // Only count as a breaker-tripping failure if the stream
211
+ // failed BEFORE yielding any tokens. Mid-stream errors are
212
+ // less indicative of vendor health (could be a content-filter
213
+ // trip on this specific request).
214
+ if (!yieldedAnyChunk)
215
+ recordFailure(err);
216
+ throw err;
217
+ }
218
+ },
219
+ }),
220
+ };
221
+ return wrapped;
222
+ }
223
+ exports.withCircuitBreaker = withCircuitBreaker;
224
+ // ─── Default predicates ──────────────────────────────────────────────
225
+ function defaultShouldCount(error) {
226
+ // Don't count user cancellations.
227
+ const e = error;
228
+ if (e?.name === 'AbortError')
229
+ return false;
230
+ if (e?.code === 'ABORT_ERR')
231
+ return false;
232
+ return true;
233
+ }
234
+ //# sourceMappingURL=withCircuitBreaker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withCircuitBreaker.js","sourceRoot":"","sources":["../../src/resilience/withCircuitBreaker.ts"],"names":[],"mappings":"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"}
@@ -20,6 +20,7 @@ import type { LLMProvider, PermissionChecker, PricingTable } from '../adapters/t
20
20
  import type { MemoryIdentity } from '../memory/identity/index.js';
21
21
  import type { MemoryDefinition } from '../memory/define.types.js';
22
22
  import type { Injection } from '../lib/injection-engine/types.js';
23
+ import { type OutputFallbackOptions, type ResolvedOutputFallback } from './outputFallback.js';
23
24
  import { type OutputSchemaOptions, type OutputSchemaParser } from './outputSchema.js';
24
25
  import { RunnerBase } from './RunnerBase.js';
25
26
  import type { Tool, ToolRegistryEntry } from './tools.js';
@@ -169,6 +170,13 @@ export declare class Agent extends RunnerBase<AgentInput, AgentOutput> {
169
170
  * raw string; consumers opt into typed mode explicitly.
170
171
  */
171
172
  private readonly outputSchemaParser?;
173
+ /**
174
+ * Optional 3-tier degradation for output-schema validation
175
+ * failures. Set via the builder's `.outputFallback({...})`. When
176
+ * present, `parseOutput()` and `runTyped()` fall through:
177
+ * primary → fallback → canned (in order; canned guarantees no-throw).
178
+ */
179
+ private readonly outputFallbackCfg?;
172
180
  /**
173
181
  * Optional `ToolProvider` set via the builder's `.toolProvider()`.
174
182
  * When present, the Tools slot subflow consults it per iteration
@@ -183,7 +191,7 @@ export declare class Agent extends RunnerBase<AgentInput, AgentOutput> {
183
191
  readonly appName: string;
184
192
  readonly commentaryTemplates: Readonly<Record<string, string>>;
185
193
  readonly thinkingTemplates: Readonly<Record<string, string>>;
186
- }, injections?: readonly Injection[], memories?: readonly MemoryDefinition[], outputSchemaParser?: OutputSchemaParser<unknown>, toolProvider?: ToolProvider, systemPromptCachePolicy?: CachePolicy, cachingDisabled?: boolean, cacheStrategy?: CacheStrategy);
194
+ }, injections?: readonly Injection[], memories?: readonly MemoryDefinition[], outputSchemaParser?: OutputSchemaParser<unknown>, toolProvider?: ToolProvider, systemPromptCachePolicy?: CachePolicy, cachingDisabled?: boolean, cacheStrategy?: CacheStrategy, outputFallbackCfg?: ResolvedOutputFallback<unknown>);
187
195
  static create(opts: AgentOptions): AgentBuilder;
188
196
  toFlowChart(): FlowChart;
189
197
  /**
@@ -232,11 +240,25 @@ export declare class Agent extends RunnerBase<AgentInput, AgentOutput> {
232
240
  * layer; otherwise prefer `agent.runTyped()`.
233
241
  */
234
242
  parseOutput<T = unknown>(raw: string): T;
243
+ /**
244
+ * Async sister of `parseOutput()`. When the agent is configured
245
+ * with `.outputFallback({...})`, this is the version that engages
246
+ * the 3-tier degradation chain on validation failure (the sync
247
+ * `parseOutput` always throws on failure for back-compat).
248
+ *
249
+ * Without `outputFallback`, behaves identically to `parseOutput`
250
+ * — returns sync-style on the happy path, throws OutputSchemaError
251
+ * on validation failure.
252
+ */
253
+ parseOutputAsync<T = unknown>(raw: string): Promise<T>;
235
254
  /**
236
255
  * Run the agent and return the schema-validated typed output.
237
- * Convenience over `parseOutput(await agent.run({...}))`.
256
+ * Convenience over `parseOutputAsync(await agent.run({...}))`.
257
+ *
258
+ * Throws `OutputSchemaError` on parse / validation failure UNLESS
259
+ * `.outputFallback({...})` is configured, in which case the
260
+ * 3-tier degradation chain (primary → fallback → canned) engages.
238
261
  *
239
- * Throws `OutputSchemaError` on parse / validation failure.
240
262
  * Throws if the agent has no outputSchema set or if the run
241
263
  * pauses (use `run()` directly when pauses are expected).
242
264
  */
@@ -282,6 +304,9 @@ export declare class AgentBuilder {
282
304
  * builder, propagated to the Agent at `.build()` time.
283
305
  */
284
306
  private outputSchemaParser?;
307
+ /** 3-tier output fallback chain — set via `.outputFallback({...})`.
308
+ * Optional; absent = current throw-on-validation-failure behavior. */
309
+ private outputFallbackCfg?;
285
310
  /**
286
311
  * Optional `ToolProvider` set via `.toolProvider()`. Propagated to
287
312
  * the Agent's Tools slot subflow + tool-call dispatcher; consulted
@@ -537,6 +562,41 @@ export declare class AgentBuilder {
537
562
  * typed.status; // narrowed to 'ok' | 'err'
538
563
  */
539
564
  outputSchema<T>(parser: OutputSchemaParser<T>, opts?: OutputSchemaOptions): this;
565
+ /**
566
+ * 3-tier degradation for output-schema validation failures. Pairs
567
+ * with `.outputSchema()` — calling `.outputFallback()` without an
568
+ * `outputSchema` first throws (the fallback has nothing to validate).
569
+ *
570
+ * Three tiers:
571
+ *
572
+ * 1. **Primary** — LLM emitted schema-valid JSON. Caller gets it.
573
+ * 2. **Fallback** — `OutputSchemaError` thrown. The async
574
+ * `fallback(error, raw)` runs; its return is re-validated.
575
+ * 3. **Canned** — static safety-net value. NEVER throws when set.
576
+ *
577
+ * `canned` is validated against the schema at builder time —
578
+ * fail-fast on misconfig (a `canned` that doesn't validate would
579
+ * defeat the fail-open guarantee).
580
+ *
581
+ * Two typed events fire on tier transitions for observability:
582
+ * - `agentfootprint.resilience.output_fallback_triggered`
583
+ * - `agentfootprint.resilience.output_canned_used`
584
+ *
585
+ * @example
586
+ * ```ts
587
+ * import { z } from 'zod';
588
+ * const Refund = z.object({ amount: z.number(), reason: z.string() });
589
+ *
590
+ * const agent = Agent.create({...})
591
+ * .outputSchema(Refund)
592
+ * .outputFallback({
593
+ * fallback: async (err, raw) => ({ amount: 0, reason: 'manual review' }),
594
+ * canned: { amount: 0, reason: 'unable to process' },
595
+ * })
596
+ * .build();
597
+ * ```
598
+ */
599
+ outputFallback<T>(options: OutputFallbackOptions<T>): this;
540
600
  build(): Agent;
541
601
  }
542
602
  //# sourceMappingURL=Agent.d.ts.map