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.
- package/dist/core/Agent.js +231 -14
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/outputFallback.js +156 -0
- package/dist/core/outputFallback.js.map +1 -0
- package/dist/core/runCheckpoint.js +169 -0
- package/dist/core/runCheckpoint.js.map +1 -0
- package/dist/esm/core/Agent.js +232 -15
- package/dist/esm/core/Agent.js.map +1 -1
- package/dist/esm/core/outputFallback.js +151 -0
- package/dist/esm/core/outputFallback.js.map +1 -0
- package/dist/esm/core/runCheckpoint.js +162 -0
- package/dist/esm/core/runCheckpoint.js.map +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/resilience/withCircuitBreaker.js +10 -0
- package/dist/esm/resilience/withCircuitBreaker.js.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/resilience/withCircuitBreaker.js +10 -0
- package/dist/resilience/withCircuitBreaker.js.map +1 -1
- package/dist/types/core/Agent.d.ts +102 -3
- package/dist/types/core/Agent.d.ts.map +1 -1
- package/dist/types/core/outputFallback.d.ts +140 -0
- package/dist/types/core/outputFallback.d.ts.map +1 -0
- package/dist/types/core/runCheckpoint.d.ts +167 -0
- package/dist/types/core/runCheckpoint.d.ts.map +1 -0
- package/dist/types/events/payloads.d.ts +7 -0
- package/dist/types/events/payloads.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/resilience/withCircuitBreaker.d.ts +10 -0
- package/dist/types/resilience/withCircuitBreaker.d.ts.map +1 -1
- 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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"}
|
package/dist/esm/core/Agent.js
CHANGED
|
@@ -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 {
|
|
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 `
|
|
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.
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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.
|