agentfootprint 2.10.0 → 2.10.2

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.
Files changed (33) hide show
  1. package/dist/core/Agent.js +231 -14
  2. package/dist/core/Agent.js.map +1 -1
  3. package/dist/core/outputFallback.js +156 -0
  4. package/dist/core/outputFallback.js.map +1 -0
  5. package/dist/core/runCheckpoint.js +169 -0
  6. package/dist/core/runCheckpoint.js.map +1 -0
  7. package/dist/esm/core/Agent.js +232 -15
  8. package/dist/esm/core/Agent.js.map +1 -1
  9. package/dist/esm/core/outputFallback.js +151 -0
  10. package/dist/esm/core/outputFallback.js.map +1 -0
  11. package/dist/esm/core/runCheckpoint.js +162 -0
  12. package/dist/esm/core/runCheckpoint.js.map +1 -0
  13. package/dist/esm/index.js +1 -0
  14. package/dist/esm/index.js.map +1 -1
  15. package/dist/esm/resilience/withCircuitBreaker.js +10 -0
  16. package/dist/esm/resilience/withCircuitBreaker.js.map +1 -1
  17. package/dist/index.js +4 -2
  18. package/dist/index.js.map +1 -1
  19. package/dist/resilience/withCircuitBreaker.js +10 -0
  20. package/dist/resilience/withCircuitBreaker.js.map +1 -1
  21. package/dist/types/core/Agent.d.ts +102 -3
  22. package/dist/types/core/Agent.d.ts.map +1 -1
  23. package/dist/types/core/outputFallback.d.ts +140 -0
  24. package/dist/types/core/outputFallback.d.ts.map +1 -0
  25. package/dist/types/core/runCheckpoint.d.ts +167 -0
  26. package/dist/types/core/runCheckpoint.d.ts.map +1 -0
  27. package/dist/types/events/payloads.d.ts +7 -0
  28. package/dist/types/events/payloads.d.ts.map +1 -1
  29. package/dist/types/index.d.ts +2 -0
  30. package/dist/types/index.d.ts.map +1 -1
  31. package/dist/types/resilience/withCircuitBreaker.d.ts +10 -0
  32. package/dist/types/resilience/withCircuitBreaker.d.ts.map +1 -1
  33. package/package.json +1 -1
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ /**
3
+ * outputFallback — 3-tier degradation for structured-output validation
4
+ * failures.
5
+ *
6
+ * Pairs with `outputSchema(parser)`. When the LLM's final answer
7
+ * fails schema validation (after the agent loop has done what it
8
+ * could), instead of throwing `OutputSchemaError` to the caller,
9
+ * the agent falls through:
10
+ *
11
+ * 1. **Primary** — LLM emitted schema-valid JSON. Caller gets the
12
+ * parsed value.
13
+ * 2. **Fallback** — `OutputSchemaError` thrown by the parser. The
14
+ * consumer-supplied async `fallback(error, raw)` runs; its
15
+ * return value is parsed against the same schema. If valid →
16
+ * caller gets it. If `fallback` itself throws OR its return
17
+ * value fails schema → tier 3.
18
+ * 3. **Canned** — static `canned` value (validated against the
19
+ * schema at builder time so it's guaranteed to satisfy). The
20
+ * agent NEVER throws when `canned` is set.
21
+ *
22
+ * Pattern: chain-of-responsibility (GoF) over typed degradation tiers.
23
+ * Same shape as `withRetry` / `withFallback` for LLM
24
+ * providers, but at the SCHEMA layer instead of the network
25
+ * layer.
26
+ *
27
+ * Role: Layer-6 (Agent) — terminal contract failure handler.
28
+ * Composable with `outputSchema` (which it supplements;
29
+ * one without the other is incoherent).
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { z } from 'zod';
34
+ *
35
+ * const Refund = z.object({
36
+ * amount: z.number().nonnegative(),
37
+ * reason: z.string().min(1),
38
+ * });
39
+ *
40
+ * const agent = Agent.create({...})
41
+ * .system('You decide refund amounts.')
42
+ * .outputSchema(Refund)
43
+ * .outputFallback({
44
+ * // Tier 2: try a more permissive prompt; if it also fails,
45
+ * // escalate to a human.
46
+ * fallback: async (err, raw) => ({
47
+ * amount: 0,
48
+ * reason: `manual review required (LLM output: ${raw.slice(0, 200)})`,
49
+ * }),
50
+ * // Tier 3: guaranteed-valid safety net.
51
+ * canned: { amount: 0, reason: 'unable to process — please retry' },
52
+ * })
53
+ * .build();
54
+ *
55
+ * // Caller never sees OutputSchemaError; gets a typed Refund either way.
56
+ * const refund = await agent.runTyped({ message: '...' });
57
+ * ```
58
+ *
59
+ * Why this matters in production:
60
+ * - LLMs occasionally emit prose despite the system prompt asking
61
+ * for JSON ("Sure! Here's your refund: {...}").
62
+ * - Schema-violating outputs are bursty under model load (vendor
63
+ * A/B tests, model rollouts, content-filter trips).
64
+ * - A B2C agent that THROWS on every malformed output cascades
65
+ * into 5xx for the end user; the FAIL-OPEN pattern degrades
66
+ * gracefully and lets you triage offline.
67
+ *
68
+ * Two typed events fire so observability backends can alert on
69
+ * degradation:
70
+ * - `agentfootprint.resilience.output_fallback_triggered`
71
+ * (tier 2 fired)
72
+ * - `agentfootprint.resilience.output_canned_used`
73
+ * (tier 3 fired — fallback also failed; safety net engaged)
74
+ */
75
+ Object.defineProperty(exports, "__esModule", { value: true });
76
+ exports.applyOutputFallback = exports.validateCannedAgainstSchema = void 0;
77
+ // ─── Builder-time validation ─────────────────────────────────────────
78
+ /**
79
+ * Validate the consumer-supplied `canned` value against the schema
80
+ * at builder time. Fail-fast on misconfig — a `canned` value that
81
+ * doesn't satisfy the schema would cascade into runtime errors
82
+ * AFTER the agent loop has already failed, which defeats the
83
+ * fail-open guarantee.
84
+ *
85
+ * Throws `TypeError` with a hint if validation fails.
86
+ */
87
+ function validateCannedAgainstSchema(canned, parser) {
88
+ try {
89
+ parser.parse(canned);
90
+ }
91
+ catch (cause) {
92
+ throw new TypeError(`[outputFallback] canned value does not satisfy outputSchema. ` +
93
+ `The canned value is the safety net — it must always validate. ` +
94
+ `Underlying error: ${cause?.message ?? String(cause)}`);
95
+ }
96
+ }
97
+ exports.validateCannedAgainstSchema = validateCannedAgainstSchema;
98
+ // ─── Runtime application ─────────────────────────────────────────────
99
+ /**
100
+ * The 3-tier resolver. Called by `agent.parseOutput()` /
101
+ * `agent.runTyped()` when an `outputFallback` is configured. Replaces
102
+ * the bare-throw behavior of `applyOutputSchema()`.
103
+ *
104
+ * Returns the typed value from whichever tier wins. Emits typed
105
+ * events at every tier transition so observability backends can
106
+ * alert on degradation.
107
+ *
108
+ * @param raw — the LLM's original final-answer string
109
+ * @param parser — the outputSchema parser
110
+ * @param fallbackCfg — the resolved fallback configuration
111
+ * @param emit — agentfootprint dispatcher's `dispatch()` entry
112
+ * (typed via the runner; we accept a thin
113
+ * function so this module stays import-free of
114
+ * the dispatcher).
115
+ */
116
+ async function applyOutputFallback(raw, parser, fallbackCfg, emit, primaryError) {
117
+ // Tier 2 — fallback function.
118
+ emit('agentfootprint.resilience.output_fallback_triggered', {
119
+ stage: primaryError.stage,
120
+ rawOutputPreview: raw.slice(0, 200),
121
+ primaryErrorMessage: primaryError.message,
122
+ });
123
+ let tier2Value;
124
+ try {
125
+ tier2Value = await fallbackCfg.fallback(primaryError, raw);
126
+ }
127
+ catch (fallbackError) {
128
+ return cannedOrRethrow(parser, fallbackCfg, emit, fallbackError, raw);
129
+ }
130
+ // Validate tier 2's output against the schema.
131
+ try {
132
+ return parser.parse(tier2Value);
133
+ }
134
+ catch (validationError) {
135
+ return cannedOrRethrow(parser, fallbackCfg, emit, validationError, raw);
136
+ }
137
+ }
138
+ exports.applyOutputFallback = applyOutputFallback;
139
+ function cannedOrRethrow(parser, fallbackCfg, emit, failureCause, raw) {
140
+ if (!fallbackCfg.hasCanned) {
141
+ // No safety net — propagate. Consumer chose fail-closed by
142
+ // omitting `canned`.
143
+ if (failureCause instanceof Error)
144
+ throw failureCause;
145
+ throw new Error(String(failureCause));
146
+ }
147
+ emit('agentfootprint.resilience.output_canned_used', {
148
+ rawOutputPreview: raw.slice(0, 200),
149
+ fallbackErrorMessage: failureCause instanceof Error ? failureCause.message : String(failureCause),
150
+ });
151
+ // Re-validate canned defensively. Builder-time validation already
152
+ // ran, but if a consumer mutates the canned object after build,
153
+ // we'd rather throw than corrupt the contract.
154
+ return parser.parse(fallbackCfg.canned);
155
+ }
156
+ //# sourceMappingURL=outputFallback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outputFallback.js","sourceRoot":"","sources":["../../src/core/outputFallback.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;;;AA8CH,wEAAwE;AAExE;;;;;;;;GAQG;AACH,SAAgB,2BAA2B,CAAI,MAAS,EAAE,MAA6B;IACrF,IAAI,CAAC;QACH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACvB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,SAAS,CACjB,+DAA+D;YAC7D,gEAAgE;YAChE,qBAAsB,KAA8B,EAAE,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CACnF,CAAC;IACJ,CAAC;AACH,CAAC;AAVD,kEAUC;AAED,wEAAwE;AAExE;;;;;;;;;;;;;;;;GAgBG;AACI,KAAK,UAAU,mBAAmB,CACvC,GAAW,EACX,MAA6B,EAC7B,WAAsC,EACtC,IAAmE,EACnE,YAA+B;IAE/B,8BAA8B;IAC9B,IAAI,CAAC,qDAAqD,EAAE;QAC1D,KAAK,EAAE,YAAY,CAAC,KAAK;QACzB,gBAAgB,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;QACnC,mBAAmB,EAAE,YAAY,CAAC,OAAO;KAC1C,CAAC,CAAC;IAEH,IAAI,UAAmB,CAAC;IACxB,IAAI,CAAC;QACH,UAAU,GAAG,MAAM,WAAW,CAAC,QAAQ,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;IAC7D,CAAC;IAAC,OAAO,aAAa,EAAE,CAAC;QACvB,OAAO,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,CAAC,CAAC;IACxE,CAAC;IAED,+CAA+C;IAC/C,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IAClC,CAAC;IAAC,OAAO,eAAe,EAAE,CAAC;QACzB,OAAO,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe,EAAE,GAAG,CAAC,CAAC;IAC1E,CAAC;AACH,CAAC;AA3BD,kDA2BC;AAED,SAAS,eAAe,CACtB,MAA6B,EAC7B,WAAsC,EACtC,IAAmE,EACnE,YAAqB,EACrB,GAAW;IAEX,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,CAAC;QAC3B,2DAA2D;QAC3D,qBAAqB;QACrB,IAAI,YAAY,YAAY,KAAK;YAAE,MAAM,YAAY,CAAC;QACtD,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC;IACxC,CAAC;IACD,IAAI,CAAC,8CAA8C,EAAE;QACnD,gBAAgB,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;QACnC,oBAAoB,EAClB,YAAY,YAAY,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC;KAC9E,CAAC,CAAC;IACH,kEAAkE;IAClE,gEAAgE;IAChE,+CAA+C;IAC/C,OAAO,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;AAC1C,CAAC"}
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ /**
3
+ * runCheckpoint — fault-tolerant resume primitives.
4
+ *
5
+ * Today's pause/resume only handles INTENTIONAL pauses (`askHuman`).
6
+ * Errors mid-run (LLM 500s, vendor outages, tool throws, container
7
+ * restarts) propagate all the way up and the consumer must restart
8
+ * from scratch — losing the prior iterations' work.
9
+ *
10
+ * This module adds the third piece of the Reliability subsystem:
11
+ *
12
+ * 1. **`AgentRunCheckpoint`** — JSON-serializable snapshot of an
13
+ * agent run's progress. Captured automatically at each
14
+ * iteration boundary (the natural commit points). Survives
15
+ * process restart — persist to Redis / Postgres / S3 / queue.
16
+ *
17
+ * 2. **`RunCheckpointError`** — wraps the underlying error with
18
+ * the last-known-good checkpoint. Throwing this instead of the
19
+ * raw error lets consumers catch + persist + resume later
20
+ * without losing context.
21
+ *
22
+ * 3. **`agent.resumeOnError(checkpoint)`** — replays the agent run
23
+ * with the checkpointed conversation history restored. The
24
+ * next iteration retries the call that originally failed (with
25
+ * the latest provider state — circuit breaker may have closed,
26
+ * vendor may have recovered, etc.).
27
+ *
28
+ * Design tradeoff: we use a CONVERSATION-HISTORY checkpoint shape
29
+ * rather than a full executor-state checkpoint (which would require
30
+ * footprintjs API surface changes for mid-run snapshotting). The
31
+ * tradeoff:
32
+ *
33
+ * ✅ Survives process restart (JSON-serializable, tiny payload)
34
+ * ✅ Works with any LLM provider — replay starts from history
35
+ * ✅ No footprintjs core changes
36
+ * ⚠️ Loses mid-iteration partial state (acceptable — iterations
37
+ * are atomic; we resume from the last completed boundary)
38
+ * ⚠️ Tool calls inside the failed iteration re-execute (consumer
39
+ * must idempotency-key their tool implementations OR use
40
+ * v2.10.3+ tool-result dedup via toolCallId).
41
+ *
42
+ * Pattern: Memento (GoF) — snapshot of an object's internal state
43
+ * for later restoration. Same shape as `FlowchartCheckpoint`
44
+ * but at the agent layer (one logical iteration vs. one
45
+ * DFS stage).
46
+ */
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.classifyFailurePhase = exports.validateCheckpoint = exports.buildCheckpoint = exports.RunCheckpointError = void 0;
49
+ /**
50
+ * Thrown by `agent.run()` when a fault occurs mid-run. Carries the
51
+ * underlying error AND the last-known-good checkpoint. Catch this
52
+ * specifically to engage the resume-on-error path; let other errors
53
+ * propagate normally.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * import { Agent, RunCheckpointError } from 'agentfootprint';
58
+ *
59
+ * try {
60
+ * const result = await agent.run({ message: 'long task' });
61
+ * } catch (err) {
62
+ * if (err instanceof RunCheckpointError) {
63
+ * await checkpointStore.put(sessionId, err.checkpoint);
64
+ * // hours / restart later:
65
+ * const checkpoint = await checkpointStore.get(sessionId);
66
+ * const result = await agent.resumeOnError(checkpoint);
67
+ * } else {
68
+ * throw err; // not a recoverable error — propagate
69
+ * }
70
+ * }
71
+ * ```
72
+ */
73
+ class RunCheckpointError extends Error {
74
+ code = 'ERR_RUN_CHECKPOINT';
75
+ /** The error that triggered the checkpoint. Inspect for retry
76
+ * decisions ("if cause is CircuitOpenError, wait for cooldown
77
+ * before resuming"). */
78
+ cause;
79
+ /** The last-known-good checkpoint. Persist + pass back to
80
+ * `agent.resumeOnError(checkpoint)` to continue from here. */
81
+ checkpoint;
82
+ constructor(cause, checkpoint) {
83
+ const phase = checkpoint.failurePoint?.phase ?? 'unknown';
84
+ super(`[agent run] failed at iteration ${checkpoint.failurePoint?.iteration ?? '?'} (${phase}). ` +
85
+ `Last-good checkpoint captured at iteration ${checkpoint.lastCompletedIteration}. ` +
86
+ `Pass to agent.resumeOnError(checkpoint) to continue. ` +
87
+ `Underlying error: ${cause.message}`);
88
+ this.name = 'RunCheckpointError';
89
+ this.cause = cause;
90
+ this.checkpoint = checkpoint;
91
+ }
92
+ }
93
+ exports.RunCheckpointError = RunCheckpointError;
94
+ /**
95
+ * Build a JSON-serializable checkpoint from a tracker + failure
96
+ * info. Pure function — no side effects.
97
+ *
98
+ * @internal
99
+ */
100
+ function buildCheckpoint(tracker, failurePoint) {
101
+ return {
102
+ version: 1,
103
+ runId: tracker.runId,
104
+ history: tracker.history,
105
+ lastCompletedIteration: tracker.lastCompletedIteration,
106
+ originalInput: tracker.originalInput,
107
+ checkpointedAt: Date.now(),
108
+ ...(failurePoint && { failurePoint }),
109
+ };
110
+ }
111
+ exports.buildCheckpoint = buildCheckpoint;
112
+ /**
113
+ * Validate a checkpoint at deserialization time. Catches forward-
114
+ * incompatible payloads (someone tries to resume a v3 checkpoint on
115
+ * a v1 runtime, or a corrupted JSON blob).
116
+ *
117
+ * Returns the checkpoint typed-narrowed; throws TypeError on
118
+ * unknown shape.
119
+ */
120
+ function validateCheckpoint(value) {
121
+ if (!value || typeof value !== 'object') {
122
+ throw new TypeError('[resumeOnError] checkpoint is not an object.');
123
+ }
124
+ const c = value;
125
+ if (c.version !== 1) {
126
+ throw new TypeError(`[resumeOnError] unsupported checkpoint version: ${c.version}. ` +
127
+ `This runtime supports version 1; persisted checkpoints from a future ` +
128
+ `agentfootprint version need a matching runtime to resume.`);
129
+ }
130
+ if (typeof c.runId !== 'string' || !Array.isArray(c.history)) {
131
+ throw new TypeError('[resumeOnError] checkpoint missing required fields (runId, history).');
132
+ }
133
+ if (typeof c.lastCompletedIteration !== 'number') {
134
+ throw new TypeError('[resumeOnError] checkpoint missing required field: lastCompletedIteration.');
135
+ }
136
+ if (!c.originalInput || typeof c.originalInput.message !== 'string') {
137
+ throw new TypeError('[resumeOnError] checkpoint missing required field: originalInput.message.');
138
+ }
139
+ return c;
140
+ }
141
+ exports.validateCheckpoint = validateCheckpoint;
142
+ /**
143
+ * Classify a thrown error into one of the failure-point phase
144
+ * buckets. Heuristic — uses error name / code / message inspection.
145
+ * Fast path returns 'unknown' so unrecognized errors still produce
146
+ * a checkpoint (the cause itself is preserved in
147
+ * `RunCheckpointError.cause`).
148
+ */
149
+ function classifyFailurePhase(err) {
150
+ const name = err.name;
151
+ const code = err.code ?? '';
152
+ const msg = err.message ?? '';
153
+ // LLM provider failures: known codes + name patterns.
154
+ if (code === 'ERR_CIRCUIT_OPEN' || // our own circuit breaker
155
+ name === 'AnthropicError' ||
156
+ name === 'OpenAIError' ||
157
+ name === 'BedrockError' ||
158
+ /\b(LLM|provider|anthropic|openai|bedrock)\b/i.test(msg)) {
159
+ return 'llm';
160
+ }
161
+ if (/\b(tool|tool_call)\b/i.test(name) || /\bTool\b/.test(msg)) {
162
+ return 'tool';
163
+ }
164
+ if (/iteration/i.test(msg))
165
+ return 'iteration';
166
+ return 'unknown';
167
+ }
168
+ exports.classifyFailurePhase = classifyFailurePhase;
169
+ //# sourceMappingURL=runCheckpoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runCheckpoint.js","sourceRoot":"","sources":["../../src/core/runCheckpoint.ts"],"names":[],"mappings":"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"}
@@ -47,7 +47,9 @@ import { buildToolsSlot } from './slots/buildToolsSlot.js';
47
47
  import { buildInjectionEngineSubflow } from '../lib/injection-engine/buildInjectionEngineSubflow.js';
48
48
  import { buildReadSkillTool } from '../lib/injection-engine/skillTools.js';
49
49
  import { defineInstruction } from '../lib/injection-engine/factories/defineInstruction.js';
50
- import { applyOutputSchema, buildDefaultInstruction, } from './outputSchema.js';
50
+ import { applyOutputFallback, validateCannedAgainstSchema, } from './outputFallback.js';
51
+ import { buildCheckpoint, classifyFailurePhase, RunCheckpointError, validateCheckpoint, } from './runCheckpoint.js';
52
+ import { applyOutputSchema, buildDefaultInstruction, OutputSchemaError, } from './outputSchema.js';
51
53
  import { RunnerBase, makeRunId } from './RunnerBase.js';
52
54
  export class Agent extends RunnerBase {
53
55
  name;
@@ -133,6 +135,17 @@ export class Agent extends RunnerBase {
133
135
  * raw string; consumers opt into typed mode explicitly.
134
136
  */
135
137
  outputSchemaParser;
138
+ /**
139
+ * Optional 3-tier degradation for output-schema validation
140
+ * failures. Set via the builder's `.outputFallback({...})`. When
141
+ * present, `parseOutput()` and `runTyped()` fall through:
142
+ * primary → fallback → canned (in order; canned guarantees no-throw).
143
+ */
144
+ outputFallbackCfg;
145
+ /** Side-channel for `resumeOnError(...)` — when set, the seed
146
+ * function restores `scope.history` from this instead of starting
147
+ * fresh. Cleared on first read so subsequent runs start clean. */
148
+ pendingResumeHistory;
136
149
  /**
137
150
  * Optional `ToolProvider` set via the builder's `.toolProvider()`.
138
151
  * When present, the Tools slot subflow consults it per iteration
@@ -143,7 +156,7 @@ export class Agent extends RunnerBase {
143
156
  * dispatch correctly when their visible-set changes mid-turn.
144
157
  */
145
158
  externalToolProvider;
146
- constructor(opts, systemPromptValue, registry, voice, injections = [], memories = [], outputSchemaParser, toolProvider, systemPromptCachePolicy = 'always', cachingDisabled = false, cacheStrategy) {
159
+ constructor(opts, systemPromptValue, registry, voice, injections = [], memories = [], outputSchemaParser, toolProvider, systemPromptCachePolicy = 'always', cachingDisabled = false, cacheStrategy, outputFallbackCfg) {
147
160
  super();
148
161
  this.provider = opts.provider;
149
162
  this.name = opts.name ?? 'Agent';
@@ -162,6 +175,7 @@ export class Agent extends RunnerBase {
162
175
  this.injections = injections;
163
176
  this.memories = memories;
164
177
  this.outputSchemaParser = outputSchemaParser;
178
+ this.outputFallbackCfg = outputFallbackCfg;
165
179
  this.externalToolProvider = toolProvider;
166
180
  // Eager validation: tool names must be unique across .tool() +
167
181
  // every Skill.inject.tools — the LLM dispatches by name. Runs in
@@ -247,11 +261,54 @@ export class Agent extends RunnerBase {
247
261
  }
248
262
  return applyOutputSchema(raw, this.outputSchemaParser);
249
263
  }
264
+ /**
265
+ * Async sister of `parseOutput()`. When the agent is configured
266
+ * with `.outputFallback({...})`, this is the version that engages
267
+ * the 3-tier degradation chain on validation failure (the sync
268
+ * `parseOutput` always throws on failure for back-compat).
269
+ *
270
+ * Without `outputFallback`, behaves identically to `parseOutput`
271
+ * — returns sync-style on the happy path, throws OutputSchemaError
272
+ * on validation failure.
273
+ */
274
+ async parseOutputAsync(raw) {
275
+ if (!this.outputSchemaParser) {
276
+ throw new Error(`Agent.parseOutputAsync: this agent has no outputSchema. Use ` +
277
+ `Agent.create({...}).outputSchema(parser).build() to enable typed output.`);
278
+ }
279
+ const parser = this.outputSchemaParser;
280
+ try {
281
+ return applyOutputSchema(raw, parser);
282
+ }
283
+ catch (err) {
284
+ if (!this.outputFallbackCfg || !(err instanceof OutputSchemaError))
285
+ throw err;
286
+ // Engage the 3-tier fallback. The dispatcher gives us the
287
+ // typed-event entry; we synthesize a minimal event shape since
288
+ // these events have no per-stage anchor.
289
+ const emit = (eventType, payload) => {
290
+ try {
291
+ this.dispatcher.dispatch({
292
+ type: eventType,
293
+ timestamp: Date.now(),
294
+ payload,
295
+ });
296
+ }
297
+ catch {
298
+ /* observability errors must not poison the fallback path */
299
+ }
300
+ };
301
+ return applyOutputFallback(raw, parser, this.outputFallbackCfg, emit, err);
302
+ }
303
+ }
250
304
  /**
251
305
  * Run the agent and return the schema-validated typed output.
252
- * Convenience over `parseOutput(await agent.run({...}))`.
306
+ * Convenience over `parseOutputAsync(await agent.run({...}))`.
307
+ *
308
+ * Throws `OutputSchemaError` on parse / validation failure UNLESS
309
+ * `.outputFallback({...})` is configured, in which case the
310
+ * 3-tier degradation chain (primary → fallback → canned) engages.
253
311
  *
254
- * Throws `OutputSchemaError` on parse / validation failure.
255
312
  * Throws if the agent has no outputSchema set or if the run
256
313
  * pauses (use `run()` directly when pauses are expected).
257
314
  */
@@ -265,18 +322,109 @@ export class Agent extends RunnerBase {
265
322
  throw new Error('Agent.runTyped: run paused — typed mode does not support pauses. ' +
266
323
  'Use agent.run() + agent.parseOutput(...) after resume.');
267
324
  }
268
- return this.parseOutput(out);
325
+ return this.parseOutputAsync(out);
269
326
  }
270
327
  async run(input, options) {
328
+ // (helper used in the catch block below — module-private function
329
+ // declared at file end via hoisting)
271
330
  const executor = this.createExecutor();
272
- const result = await executor.run({
273
- input: {
274
- message: input.message,
275
- ...(input.identity !== undefined && { identity: input.identity }),
276
- },
277
- ...(options ?? {}),
278
- });
279
- return this.finalizeResult(executor, result);
331
+ // Auto-checkpoint at iteration boundaries — captures the latest
332
+ // conversation history into a per-run tracker. On error, we
333
+ // wrap the underlying error in `RunCheckpointError` carrying
334
+ // this checkpoint so `agent.resumeOnError(checkpoint)` can
335
+ // continue from the last good iteration.
336
+ const tracker = {
337
+ runId: this.currentRunContext?.runId ?? 'unknown',
338
+ originalInput: { message: input.message },
339
+ history: [],
340
+ lastCompletedIteration: 0,
341
+ };
342
+ const stopTracking = this.installCheckpointTracker(tracker);
343
+ try {
344
+ const result = await executor.run({
345
+ input: {
346
+ message: input.message,
347
+ ...(input.identity !== undefined && { identity: input.identity }),
348
+ },
349
+ ...(options ?? {}),
350
+ });
351
+ return this.finalizeResult(executor, result);
352
+ }
353
+ catch (cause) {
354
+ // Wrap recoverable errors with the last-known-good checkpoint.
355
+ // Pause-signal exceptions are not recoverable in this sense
356
+ // (they're intentional askHuman pauses) — let those propagate.
357
+ if (cause instanceof Error && cause.name !== 'PauseSignal' && tracker.history.length > 0) {
358
+ const checkpoint = buildCheckpoint(tracker, {
359
+ iteration: tracker.inFlightIteration ?? tracker.lastCompletedIteration + 1,
360
+ phase: classifyFailurePhase(cause),
361
+ });
362
+ throw new RunCheckpointError(cause, checkpoint);
363
+ }
364
+ throw cause;
365
+ }
366
+ finally {
367
+ stopTracking();
368
+ }
369
+ }
370
+ /**
371
+ * Resume an agent run from a checkpoint produced by a prior
372
+ * `RunCheckpointError`. Unlike `agent.resume()` (which takes a
373
+ * `FlowchartCheckpoint` from an intentional pause), this takes
374
+ * an `AgentRunCheckpoint` (conversation-history snapshot) and
375
+ * replays the agent run with that history restored.
376
+ *
377
+ * The next iteration retries the call that originally failed —
378
+ * with the latest provider state (circuit breaker may have
379
+ * closed, vendor may have recovered, etc.).
380
+ *
381
+ * @example
382
+ * ```ts
383
+ * try {
384
+ * const result = await agent.run({ message: 'long task' });
385
+ * } catch (err) {
386
+ * if (err instanceof RunCheckpointError) {
387
+ * await checkpointStore.put(sessionId, err.checkpoint);
388
+ * // hours / restart later:
389
+ * const checkpoint = await checkpointStore.get(sessionId);
390
+ * const result = await agent.resumeOnError(checkpoint);
391
+ * }
392
+ * }
393
+ * ```
394
+ */
395
+ async resumeOnError(checkpoint, options) {
396
+ const cp = validateCheckpoint(checkpoint);
397
+ // Stash the checkpointed history on the side channel; the seed
398
+ // function reads + clears it before scope.history initializes.
399
+ this.pendingResumeHistory = cp.history;
400
+ return this.run({ message: cp.originalInput.message }, options);
401
+ }
402
+ /**
403
+ * Install a per-run checkpoint tracker. Listens for the agent's
404
+ * own iteration_end events on `this.dispatcher` and snapshots the
405
+ * conversation history into the tracker. Returns a stop function.
406
+ *
407
+ * @internal
408
+ */
409
+ installCheckpointTracker(tracker) {
410
+ const offIterStart = this.dispatcher.on('agentfootprint.agent.iteration_start', ((event) => {
411
+ const p = event.payload;
412
+ if (typeof p?.iterIndex === 'number')
413
+ tracker.inFlightIteration = p.iterIndex;
414
+ }));
415
+ const offIterEnd = this.dispatcher.on('agentfootprint.agent.iteration_end', ((event) => {
416
+ const p = event.payload;
417
+ if (typeof p?.iterIndex === 'number')
418
+ tracker.lastCompletedIteration = p.iterIndex;
419
+ if (Array.isArray(p?.history)) {
420
+ tracker.history = p.history;
421
+ }
422
+ tracker.inFlightIteration = undefined;
423
+ }));
424
+ return () => {
425
+ offIterStart();
426
+ offIterEnd();
427
+ };
280
428
  }
281
429
  async resume(checkpoint, input, options) {
282
430
  this.emitPauseResume(checkpoint, input);
@@ -360,7 +508,18 @@ export class Agent extends RunnerBase {
360
508
  const seed = (scope) => {
361
509
  const args = scope.$getArgs();
362
510
  scope.userMessage = args.message;
363
- scope.history = [{ role: 'user', content: args.message }];
511
+ // If `resumeOnError(...)` set the side channel, restore the
512
+ // checkpointed conversation history. The next iteration sees
513
+ // the prior messages and continues from the failure point.
514
+ // We always clear the field after reading so subsequent runs
515
+ // (without resumeOnError) start fresh.
516
+ if (this.pendingResumeHistory && this.pendingResumeHistory.length > 0) {
517
+ scope.history = [...this.pendingResumeHistory];
518
+ this.pendingResumeHistory = undefined;
519
+ }
520
+ else {
521
+ scope.history = [{ role: 'user', content: args.message }];
522
+ }
364
523
  // Default identity uses the runId so multi-run isolation works
365
524
  // without consumer changes; explicit identity (multi-tenant)
366
525
  // overrides via `agent.run({ identity })`.
@@ -823,6 +982,7 @@ export class Agent extends RunnerBase {
823
982
  turnIndex: 0,
824
983
  iterIndex: iteration,
825
984
  toolCallCount: toolCalls.length,
985
+ history: scope.history,
826
986
  });
827
987
  scope.iteration = iteration + 1;
828
988
  return undefined; // explicit: no pause, flow continues to loopTo
@@ -857,6 +1017,7 @@ export class Agent extends RunnerBase {
857
1017
  turnIndex: 0,
858
1018
  iterIndex: iteration,
859
1019
  toolCallCount: 1,
1020
+ history: scope.history,
860
1021
  });
861
1022
  scope.iteration = iteration + 1;
862
1023
  // Clear pause checkpoint fields.
@@ -1142,6 +1303,9 @@ export class AgentBuilder {
1142
1303
  * builder, propagated to the Agent at `.build()` time.
1143
1304
  */
1144
1305
  outputSchemaParser;
1306
+ /** 3-tier output fallback chain — set via `.outputFallback({...})`.
1307
+ * Optional; absent = current throw-on-validation-failure behavior. */
1308
+ outputFallbackCfg;
1145
1309
  /**
1146
1310
  * Optional `ToolProvider` set via `.toolProvider()`. Propagated to
1147
1311
  * the Agent's Tools slot subflow + tool-call dispatcher; consulted
@@ -1499,6 +1663,59 @@ export class AgentBuilder {
1499
1663
  }));
1500
1664
  return this;
1501
1665
  }
1666
+ /**
1667
+ * 3-tier degradation for output-schema validation failures. Pairs
1668
+ * with `.outputSchema()` — calling `.outputFallback()` without an
1669
+ * `outputSchema` first throws (the fallback has nothing to validate).
1670
+ *
1671
+ * Three tiers:
1672
+ *
1673
+ * 1. **Primary** — LLM emitted schema-valid JSON. Caller gets it.
1674
+ * 2. **Fallback** — `OutputSchemaError` thrown. The async
1675
+ * `fallback(error, raw)` runs; its return is re-validated.
1676
+ * 3. **Canned** — static safety-net value. NEVER throws when set.
1677
+ *
1678
+ * `canned` is validated against the schema at builder time —
1679
+ * fail-fast on misconfig (a `canned` that doesn't validate would
1680
+ * defeat the fail-open guarantee).
1681
+ *
1682
+ * Two typed events fire on tier transitions for observability:
1683
+ * - `agentfootprint.resilience.output_fallback_triggered`
1684
+ * - `agentfootprint.resilience.output_canned_used`
1685
+ *
1686
+ * @example
1687
+ * ```ts
1688
+ * import { z } from 'zod';
1689
+ * const Refund = z.object({ amount: z.number(), reason: z.string() });
1690
+ *
1691
+ * const agent = Agent.create({...})
1692
+ * .outputSchema(Refund)
1693
+ * .outputFallback({
1694
+ * fallback: async (err, raw) => ({ amount: 0, reason: 'manual review' }),
1695
+ * canned: { amount: 0, reason: 'unable to process' },
1696
+ * })
1697
+ * .build();
1698
+ * ```
1699
+ */
1700
+ outputFallback(options) {
1701
+ if (!this.outputSchemaParser) {
1702
+ throw new Error('AgentBuilder.outputFallback: call .outputSchema(parser) FIRST. ' +
1703
+ 'outputFallback supplements outputSchema; one without the other is incoherent.');
1704
+ }
1705
+ if (this.outputFallbackCfg) {
1706
+ throw new Error('AgentBuilder.outputFallback: already set. Each agent has at most one fallback chain.');
1707
+ }
1708
+ // Build-time validation — canned MUST satisfy the schema.
1709
+ if (options.canned !== undefined) {
1710
+ validateCannedAgainstSchema(options.canned, this.outputSchemaParser);
1711
+ }
1712
+ this.outputFallbackCfg = {
1713
+ fallback: options.fallback,
1714
+ ...(options.canned !== undefined && { canned: options.canned }),
1715
+ hasCanned: options.canned !== undefined,
1716
+ };
1717
+ return this;
1718
+ }
1502
1719
  build() {
1503
1720
  // Resolve the voice config: bundled defaults + consumer overrides.
1504
1721
  // Templates flow through the same barrel exports the rest of the
@@ -1511,7 +1728,7 @@ export class AgentBuilder {
1511
1728
  const opts = this.maxIterationsOverride !== undefined
1512
1729
  ? { ...this.opts, maxIterations: this.maxIterationsOverride }
1513
1730
  : this.opts;
1514
- const agent = new Agent(opts, this.systemPromptValue, this.registry, voice, this.injectionList, this.memoryList, this.outputSchemaParser, this.toolProviderRef, this.systemPromptCachePolicy, this.cachingDisabledValue, this.cacheStrategyOverride);
1731
+ const agent = new Agent(opts, this.systemPromptValue, this.registry, voice, this.injectionList, this.memoryList, this.outputSchemaParser, this.toolProviderRef, this.systemPromptCachePolicy, this.cachingDisabledValue, this.cacheStrategyOverride, this.outputFallbackCfg);
1515
1732
  // Attach builder-collected recorders so they receive events from
1516
1733
  // the very first run. Mirrors what consumers would do post-build
1517
1734
  // via `agent.attach(rec)`; the builder method is purely sugar.