agentfootprint 2.8.3 → 2.10.0
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/adapters/observability/otel.js +294 -0
- package/dist/adapters/observability/otel.js.map +1 -0
- package/dist/esm/adapters/observability/otel.js +290 -0
- package/dist/esm/adapters/observability/otel.js.map +1 -0
- package/dist/esm/observability-providers.js +9 -3
- package/dist/esm/observability-providers.js.map +1 -1
- package/dist/esm/resilience/index.js +1 -0
- package/dist/esm/resilience/index.js.map +1 -1
- package/dist/esm/resilience/withCircuitBreaker.js +219 -0
- package/dist/esm/resilience/withCircuitBreaker.js.map +1 -0
- package/dist/observability-providers.js +11 -4
- package/dist/observability-providers.js.map +1 -1
- package/dist/resilience/index.js +4 -1
- package/dist/resilience/index.js.map +1 -1
- package/dist/resilience/withCircuitBreaker.js +224 -0
- package/dist/resilience/withCircuitBreaker.js.map +1 -0
- package/dist/types/adapters/observability/otel.d.ts +115 -0
- package/dist/types/adapters/observability/otel.d.ts.map +1 -0
- package/dist/types/observability-providers.d.ts +9 -3
- package/dist/types/observability-providers.d.ts.map +1 -1
- package/dist/types/resilience/index.d.ts +1 -0
- package/dist/types/resilience/index.d.ts.map +1 -1
- package/dist/types/resilience/withCircuitBreaker.d.ts +104 -0
- package/dist/types/resilience/withCircuitBreaker.d.ts.map +1 -0
- package/package.json +5 -1
|
@@ -0,0 +1,219 @@
|
|
|
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
|
+
// ─── Public error type ───────────────────────────────────────────────
|
|
56
|
+
/**
|
|
57
|
+
* Thrown by the wrapped provider when the breaker is OPEN. Carries
|
|
58
|
+
* the underlying root-cause error from the most recent failure so
|
|
59
|
+
* consumers can observe what tripped the breaker.
|
|
60
|
+
*/
|
|
61
|
+
export class CircuitOpenError extends Error {
|
|
62
|
+
code = 'ERR_CIRCUIT_OPEN';
|
|
63
|
+
/** The error that tripped the breaker (or the most recent failure
|
|
64
|
+
* during HALF-OPEN that re-opened it). */
|
|
65
|
+
cause;
|
|
66
|
+
/** Wall-clock timestamp at which the breaker may next probe. */
|
|
67
|
+
retryAfter;
|
|
68
|
+
constructor(providerName, cause, retryAfter) {
|
|
69
|
+
super(`[${providerName}] circuit breaker is OPEN — failing fast (next probe at ${new Date(retryAfter).toISOString()}). Underlying error: ${cause?.message ?? String(cause)}`);
|
|
70
|
+
this.name = 'CircuitOpenError';
|
|
71
|
+
this.cause = cause;
|
|
72
|
+
this.retryAfter = retryAfter;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Wrap a provider with a circuit breaker.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* import { anthropic, openai } from 'agentfootprint/llm-providers';
|
|
81
|
+
* import { withCircuitBreaker, withFallback } from 'agentfootprint/resilience';
|
|
82
|
+
*
|
|
83
|
+
* const provider = withFallback(
|
|
84
|
+
* withCircuitBreaker(anthropic({ apiKey }), { failureThreshold: 5, cooldownMs: 30_000 }),
|
|
85
|
+
* withCircuitBreaker(openai({ apiKey })),
|
|
86
|
+
* );
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function withCircuitBreaker(inner, options = {}) {
|
|
90
|
+
const failureThreshold = options.failureThreshold ?? 5;
|
|
91
|
+
const cooldownMs = options.cooldownMs ?? 30_000;
|
|
92
|
+
const halfOpenSuccessThreshold = options.halfOpenSuccessThreshold ?? 2;
|
|
93
|
+
const shouldCount = options.shouldCount ?? defaultShouldCount;
|
|
94
|
+
const onStateChange = options.onStateChange;
|
|
95
|
+
const breaker = {
|
|
96
|
+
state: 'closed',
|
|
97
|
+
consecutiveFailures: 0,
|
|
98
|
+
consecutiveSuccesses: 0,
|
|
99
|
+
openedAt: 0,
|
|
100
|
+
lastError: undefined,
|
|
101
|
+
};
|
|
102
|
+
function transition(next, reason) {
|
|
103
|
+
if (breaker.state === next)
|
|
104
|
+
return;
|
|
105
|
+
breaker.state = next;
|
|
106
|
+
if (next === 'open') {
|
|
107
|
+
breaker.openedAt = Date.now();
|
|
108
|
+
breaker.consecutiveSuccesses = 0;
|
|
109
|
+
}
|
|
110
|
+
else if (next === 'half-open') {
|
|
111
|
+
breaker.consecutiveSuccesses = 0;
|
|
112
|
+
}
|
|
113
|
+
else if (next === 'closed') {
|
|
114
|
+
breaker.consecutiveFailures = 0;
|
|
115
|
+
breaker.consecutiveSuccesses = 0;
|
|
116
|
+
breaker.lastError = undefined;
|
|
117
|
+
}
|
|
118
|
+
onStateChange?.(next, reason);
|
|
119
|
+
}
|
|
120
|
+
/** Decide whether to admit a call. Mutates state if cooldown
|
|
121
|
+
* elapsed (open → half-open). Returns true to admit, false to
|
|
122
|
+
* reject with CircuitOpenError. */
|
|
123
|
+
function admit() {
|
|
124
|
+
if (breaker.state === 'closed' || breaker.state === 'half-open')
|
|
125
|
+
return true;
|
|
126
|
+
// OPEN — check cooldown.
|
|
127
|
+
if (Date.now() - breaker.openedAt >= cooldownMs) {
|
|
128
|
+
transition('half-open', 'cooldown elapsed');
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
function recordSuccess() {
|
|
134
|
+
if (breaker.state === 'half-open') {
|
|
135
|
+
breaker.consecutiveSuccesses += 1;
|
|
136
|
+
if (breaker.consecutiveSuccesses >= halfOpenSuccessThreshold) {
|
|
137
|
+
transition('closed', `${halfOpenSuccessThreshold} probe successes`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else if (breaker.state === 'closed') {
|
|
141
|
+
// Successful call resets the failure counter.
|
|
142
|
+
breaker.consecutiveFailures = 0;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function recordFailure(err) {
|
|
146
|
+
if (!shouldCount(err))
|
|
147
|
+
return;
|
|
148
|
+
breaker.lastError = err;
|
|
149
|
+
if (breaker.state === 'half-open') {
|
|
150
|
+
// Probe failed — re-open the breaker.
|
|
151
|
+
transition('open', 'half-open probe failed');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (breaker.state === 'closed') {
|
|
155
|
+
breaker.consecutiveFailures += 1;
|
|
156
|
+
if (breaker.consecutiveFailures >= failureThreshold) {
|
|
157
|
+
transition('open', `${breaker.consecutiveFailures} consecutive failures`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function rejectFastIfOpen() {
|
|
162
|
+
if (!admit()) {
|
|
163
|
+
throw new CircuitOpenError(inner.name, breaker.lastError, breaker.openedAt + cooldownMs);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const wrapped = {
|
|
167
|
+
name: inner.name,
|
|
168
|
+
async complete(req) {
|
|
169
|
+
rejectFastIfOpen();
|
|
170
|
+
try {
|
|
171
|
+
const res = await inner.complete(req);
|
|
172
|
+
recordSuccess();
|
|
173
|
+
return res;
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
recordFailure(err);
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
// `stream` is optional on `LLMProvider`. Only define our wrapper
|
|
181
|
+
// if the underlying provider supports streaming — otherwise leave
|
|
182
|
+
// it undefined so the consumer's existing capability check
|
|
183
|
+
// (`if (provider.stream)`) still works correctly.
|
|
184
|
+
...(inner.stream && {
|
|
185
|
+
async *stream(req) {
|
|
186
|
+
rejectFastIfOpen();
|
|
187
|
+
let yieldedAnyChunk = false;
|
|
188
|
+
try {
|
|
189
|
+
for await (const chunk of inner.stream(req)) {
|
|
190
|
+
yieldedAnyChunk = true;
|
|
191
|
+
yield chunk;
|
|
192
|
+
}
|
|
193
|
+
recordSuccess();
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
// Only count as a breaker-tripping failure if the stream
|
|
197
|
+
// failed BEFORE yielding any tokens. Mid-stream errors are
|
|
198
|
+
// less indicative of vendor health (could be a content-filter
|
|
199
|
+
// trip on this specific request).
|
|
200
|
+
if (!yieldedAnyChunk)
|
|
201
|
+
recordFailure(err);
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
}),
|
|
206
|
+
};
|
|
207
|
+
return wrapped;
|
|
208
|
+
}
|
|
209
|
+
// ─── Default predicates ──────────────────────────────────────────────
|
|
210
|
+
function defaultShouldCount(error) {
|
|
211
|
+
// Don't count user cancellations.
|
|
212
|
+
const e = error;
|
|
213
|
+
if (e?.name === 'AbortError')
|
|
214
|
+
return false;
|
|
215
|
+
if (e?.code === 'ABORT_ERR')
|
|
216
|
+
return false;
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
//# 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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"}
|
|
@@ -30,16 +30,23 @@
|
|
|
30
30
|
* Roadmap:
|
|
31
31
|
* - agentcoreObservability ← v2.8.1
|
|
32
32
|
* - cloudwatchObservability ← v2.8.2
|
|
33
|
-
* - xrayObservability ← v2.8.3
|
|
34
|
-
* - otelObservability ← v2.9.
|
|
35
|
-
*
|
|
33
|
+
* - xrayObservability ← v2.8.3
|
|
34
|
+
* - otelObservability ← v2.9.0 (this release)
|
|
35
|
+
*
|
|
36
|
+
* Note: `datadogObservability` was on the v2.9 roadmap, but Datadog
|
|
37
|
+
* APM accepts OTLP — point your OTel SDK at Datadog's OTLP endpoint
|
|
38
|
+
* and `otelObservability` covers the Datadog use case. We'll ship a
|
|
39
|
+
* dedicated `dd-trace`-based adapter only if real-world feedback
|
|
40
|
+
* demands the native Datadog APM client.
|
|
36
41
|
*/
|
|
37
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
-
exports.xrayObservability = exports.cloudwatchObservability = exports.agentcoreObservability = void 0;
|
|
43
|
+
exports.otelObservability = exports.xrayObservability = exports.cloudwatchObservability = exports.agentcoreObservability = void 0;
|
|
39
44
|
var agentcore_js_1 = require("./adapters/observability/agentcore.js");
|
|
40
45
|
Object.defineProperty(exports, "agentcoreObservability", { enumerable: true, get: function () { return agentcore_js_1.agentcoreObservability; } });
|
|
41
46
|
var cloudwatch_js_1 = require("./adapters/observability/cloudwatch.js");
|
|
42
47
|
Object.defineProperty(exports, "cloudwatchObservability", { enumerable: true, get: function () { return cloudwatch_js_1.cloudwatchObservability; } });
|
|
43
48
|
var xray_js_1 = require("./adapters/observability/xray.js");
|
|
44
49
|
Object.defineProperty(exports, "xrayObservability", { enumerable: true, get: function () { return xray_js_1.xrayObservability; } });
|
|
50
|
+
var otel_js_1 = require("./adapters/observability/otel.js");
|
|
51
|
+
Object.defineProperty(exports, "otelObservability", { enumerable: true, get: function () { return otel_js_1.otelObservability; } });
|
|
45
52
|
//# sourceMappingURL=observability-providers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observability-providers.js","sourceRoot":"","sources":["../src/observability-providers.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"observability-providers.js","sourceRoot":"","sources":["../src/observability-providers.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;;;AAEH,sEAG+C;AAF7C,sHAAA,sBAAsB,OAAA;AAGxB,wEAGgD;AAF9C,wHAAA,uBAAuB,OAAA;AAGzB,4DAI0C;AAHxC,4GAAA,iBAAiB,OAAA;AAInB,4DAM0C;AALxC,4GAAA,iBAAiB,OAAA"}
|
package/dist/resilience/index.js
CHANGED
|
@@ -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,224 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
+
exports.withCircuitBreaker = exports.CircuitOpenError = void 0;
|
|
58
|
+
// ─── Public error type ───────────────────────────────────────────────
|
|
59
|
+
/**
|
|
60
|
+
* Thrown by the wrapped provider when the breaker is OPEN. Carries
|
|
61
|
+
* the underlying root-cause error from the most recent failure so
|
|
62
|
+
* consumers can observe what tripped the breaker.
|
|
63
|
+
*/
|
|
64
|
+
class CircuitOpenError extends Error {
|
|
65
|
+
code = 'ERR_CIRCUIT_OPEN';
|
|
66
|
+
/** The error that tripped the breaker (or the most recent failure
|
|
67
|
+
* during HALF-OPEN that re-opened it). */
|
|
68
|
+
cause;
|
|
69
|
+
/** Wall-clock timestamp at which the breaker may next probe. */
|
|
70
|
+
retryAfter;
|
|
71
|
+
constructor(providerName, cause, retryAfter) {
|
|
72
|
+
super(`[${providerName}] circuit breaker is OPEN — failing fast (next probe at ${new Date(retryAfter).toISOString()}). Underlying error: ${cause?.message ?? String(cause)}`);
|
|
73
|
+
this.name = 'CircuitOpenError';
|
|
74
|
+
this.cause = cause;
|
|
75
|
+
this.retryAfter = retryAfter;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
exports.CircuitOpenError = CircuitOpenError;
|
|
79
|
+
/**
|
|
80
|
+
* Wrap a provider with a circuit breaker.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* import { anthropic, openai } from 'agentfootprint/llm-providers';
|
|
85
|
+
* import { withCircuitBreaker, withFallback } from 'agentfootprint/resilience';
|
|
86
|
+
*
|
|
87
|
+
* const provider = withFallback(
|
|
88
|
+
* withCircuitBreaker(anthropic({ apiKey }), { failureThreshold: 5, cooldownMs: 30_000 }),
|
|
89
|
+
* withCircuitBreaker(openai({ apiKey })),
|
|
90
|
+
* );
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
function withCircuitBreaker(inner, options = {}) {
|
|
94
|
+
const failureThreshold = options.failureThreshold ?? 5;
|
|
95
|
+
const cooldownMs = options.cooldownMs ?? 30_000;
|
|
96
|
+
const halfOpenSuccessThreshold = options.halfOpenSuccessThreshold ?? 2;
|
|
97
|
+
const shouldCount = options.shouldCount ?? defaultShouldCount;
|
|
98
|
+
const onStateChange = options.onStateChange;
|
|
99
|
+
const breaker = {
|
|
100
|
+
state: 'closed',
|
|
101
|
+
consecutiveFailures: 0,
|
|
102
|
+
consecutiveSuccesses: 0,
|
|
103
|
+
openedAt: 0,
|
|
104
|
+
lastError: undefined,
|
|
105
|
+
};
|
|
106
|
+
function transition(next, reason) {
|
|
107
|
+
if (breaker.state === next)
|
|
108
|
+
return;
|
|
109
|
+
breaker.state = next;
|
|
110
|
+
if (next === 'open') {
|
|
111
|
+
breaker.openedAt = Date.now();
|
|
112
|
+
breaker.consecutiveSuccesses = 0;
|
|
113
|
+
}
|
|
114
|
+
else if (next === 'half-open') {
|
|
115
|
+
breaker.consecutiveSuccesses = 0;
|
|
116
|
+
}
|
|
117
|
+
else if (next === 'closed') {
|
|
118
|
+
breaker.consecutiveFailures = 0;
|
|
119
|
+
breaker.consecutiveSuccesses = 0;
|
|
120
|
+
breaker.lastError = undefined;
|
|
121
|
+
}
|
|
122
|
+
onStateChange?.(next, reason);
|
|
123
|
+
}
|
|
124
|
+
/** Decide whether to admit a call. Mutates state if cooldown
|
|
125
|
+
* elapsed (open → half-open). Returns true to admit, false to
|
|
126
|
+
* reject with CircuitOpenError. */
|
|
127
|
+
function admit() {
|
|
128
|
+
if (breaker.state === 'closed' || breaker.state === 'half-open')
|
|
129
|
+
return true;
|
|
130
|
+
// OPEN — check cooldown.
|
|
131
|
+
if (Date.now() - breaker.openedAt >= cooldownMs) {
|
|
132
|
+
transition('half-open', 'cooldown elapsed');
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
function recordSuccess() {
|
|
138
|
+
if (breaker.state === 'half-open') {
|
|
139
|
+
breaker.consecutiveSuccesses += 1;
|
|
140
|
+
if (breaker.consecutiveSuccesses >= halfOpenSuccessThreshold) {
|
|
141
|
+
transition('closed', `${halfOpenSuccessThreshold} probe successes`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else if (breaker.state === 'closed') {
|
|
145
|
+
// Successful call resets the failure counter.
|
|
146
|
+
breaker.consecutiveFailures = 0;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function recordFailure(err) {
|
|
150
|
+
if (!shouldCount(err))
|
|
151
|
+
return;
|
|
152
|
+
breaker.lastError = err;
|
|
153
|
+
if (breaker.state === 'half-open') {
|
|
154
|
+
// Probe failed — re-open the breaker.
|
|
155
|
+
transition('open', 'half-open probe failed');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (breaker.state === 'closed') {
|
|
159
|
+
breaker.consecutiveFailures += 1;
|
|
160
|
+
if (breaker.consecutiveFailures >= failureThreshold) {
|
|
161
|
+
transition('open', `${breaker.consecutiveFailures} consecutive failures`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function rejectFastIfOpen() {
|
|
166
|
+
if (!admit()) {
|
|
167
|
+
throw new CircuitOpenError(inner.name, breaker.lastError, breaker.openedAt + cooldownMs);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const wrapped = {
|
|
171
|
+
name: inner.name,
|
|
172
|
+
async complete(req) {
|
|
173
|
+
rejectFastIfOpen();
|
|
174
|
+
try {
|
|
175
|
+
const res = await inner.complete(req);
|
|
176
|
+
recordSuccess();
|
|
177
|
+
return res;
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
recordFailure(err);
|
|
181
|
+
throw err;
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
// `stream` is optional on `LLMProvider`. Only define our wrapper
|
|
185
|
+
// if the underlying provider supports streaming — otherwise leave
|
|
186
|
+
// it undefined so the consumer's existing capability check
|
|
187
|
+
// (`if (provider.stream)`) still works correctly.
|
|
188
|
+
...(inner.stream && {
|
|
189
|
+
async *stream(req) {
|
|
190
|
+
rejectFastIfOpen();
|
|
191
|
+
let yieldedAnyChunk = false;
|
|
192
|
+
try {
|
|
193
|
+
for await (const chunk of inner.stream(req)) {
|
|
194
|
+
yieldedAnyChunk = true;
|
|
195
|
+
yield chunk;
|
|
196
|
+
}
|
|
197
|
+
recordSuccess();
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
// Only count as a breaker-tripping failure if the stream
|
|
201
|
+
// failed BEFORE yielding any tokens. Mid-stream errors are
|
|
202
|
+
// less indicative of vendor health (could be a content-filter
|
|
203
|
+
// trip on this specific request).
|
|
204
|
+
if (!yieldedAnyChunk)
|
|
205
|
+
recordFailure(err);
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
}),
|
|
210
|
+
};
|
|
211
|
+
return wrapped;
|
|
212
|
+
}
|
|
213
|
+
exports.withCircuitBreaker = withCircuitBreaker;
|
|
214
|
+
// ─── Default predicates ──────────────────────────────────────────────
|
|
215
|
+
function defaultShouldCount(error) {
|
|
216
|
+
// Don't count user cancellations.
|
|
217
|
+
const e = error;
|
|
218
|
+
if (e?.name === 'AbortError')
|
|
219
|
+
return false;
|
|
220
|
+
if (e?.code === 'ABORT_ERR')
|
|
221
|
+
return false;
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
//# 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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"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* otelObservability — OpenTelemetry distributed-tracing adapter.
|
|
3
|
+
*
|
|
4
|
+
* Ships every agentfootprint event as OpenTelemetry spans + log
|
|
5
|
+
* records via a consumer-supplied OTel API. Same hierarchical
|
|
6
|
+
* mapping as the X-Ray adapter, but the destination is whichever
|
|
7
|
+
* OTel-compat backend the consumer's SDK exports to:
|
|
8
|
+
*
|
|
9
|
+
* - **Honeycomb** (OTLP/HTTP)
|
|
10
|
+
* - **Grafana Cloud / Tempo / Mimir** (OTLP)
|
|
11
|
+
* - **AWS Distro for OTel** → AWS X-Ray (alternative to xrayObservability)
|
|
12
|
+
* - **Datadog APM** (OTLP endpoint)
|
|
13
|
+
* - **Splunk Observability Cloud** (OTLP)
|
|
14
|
+
* - **New Relic** (OTLP endpoint)
|
|
15
|
+
* - **Lightstep / ServiceNow Cloud Observability** (OTLP)
|
|
16
|
+
* - any custom OTel collector / processor pipeline
|
|
17
|
+
*
|
|
18
|
+
* Subpath: `agentfootprint/observability-providers`
|
|
19
|
+
* Peer dep: `@opentelemetry/api` (OPTIONAL — installed only when
|
|
20
|
+
* this adapter is used. The consumer ALSO installs the
|
|
21
|
+
* OTel SDK + exporter of their choice — that's the BYO
|
|
22
|
+
* contract that makes this adapter backend-agnostic.).
|
|
23
|
+
*
|
|
24
|
+
* **Why BYO SDK:** OTel's SDK is heavyweight and exporter-specific
|
|
25
|
+
* (each backend has its own exporter package). Forcing a particular
|
|
26
|
+
* exporter would defeat the "OTel is portable" guarantee. Consumers
|
|
27
|
+
* configure the SDK + exporter once at app startup; we just speak
|
|
28
|
+
* the typed OTel API.
|
|
29
|
+
*
|
|
30
|
+
* Mapping:
|
|
31
|
+
*
|
|
32
|
+
* agent.turn_start ↦ start root span (one trace per turn)
|
|
33
|
+
* agent.turn_end ↦ end root span
|
|
34
|
+
* agent.iteration_start ↦ start child span under root
|
|
35
|
+
* agent.iteration_end ↦ end iteration span
|
|
36
|
+
* stream.llm_start ↦ start child span (model call)
|
|
37
|
+
* stream.llm_end ↦ end llm span
|
|
38
|
+
* stream.tool_start ↦ start child span (tool call)
|
|
39
|
+
* stream.tool_end ↦ end tool span (with `error: true` if errored)
|
|
40
|
+
* cost.tick ↦ setAttribute on topmost active span
|
|
41
|
+
*
|
|
42
|
+
* @example Basic — Honeycomb via OTLP
|
|
43
|
+
* ```ts
|
|
44
|
+
* import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
|
|
45
|
+
* import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
46
|
+
* import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
47
|
+
* import { trace } from '@opentelemetry/api';
|
|
48
|
+
* import { otelObservability } from 'agentfootprint/observability-providers';
|
|
49
|
+
*
|
|
50
|
+
* // Set up OTel ONCE at app startup.
|
|
51
|
+
* const provider = new NodeTracerProvider();
|
|
52
|
+
* provider.addSpanProcessor(new BatchSpanProcessor(new OTLPTraceExporter({
|
|
53
|
+
* url: 'https://api.honeycomb.io/v1/traces',
|
|
54
|
+
* headers: { 'x-honeycomb-team': process.env.HONEYCOMB_KEY },
|
|
55
|
+
* })));
|
|
56
|
+
* provider.register();
|
|
57
|
+
*
|
|
58
|
+
* agent.enable.observability({
|
|
59
|
+
* strategy: otelObservability({
|
|
60
|
+
* serviceName: 'my-agent',
|
|
61
|
+
* // tracer optional — defaults to trace.getTracer('agentfootprint').
|
|
62
|
+
* }),
|
|
63
|
+
* });
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* @example Test injection
|
|
67
|
+
* ```ts
|
|
68
|
+
* otelObservability({
|
|
69
|
+
* serviceName: 'test',
|
|
70
|
+
* tracer: mockTracer, // anything matching the OTel Tracer interface
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
import type { ObservabilityStrategy } from '../../strategies/types.js';
|
|
75
|
+
export interface OtelObservabilityOptions {
|
|
76
|
+
/** Service name on every emitted span. Surfaces in your OTel
|
|
77
|
+
* backend's service map. Required. */
|
|
78
|
+
readonly serviceName: string;
|
|
79
|
+
/** OTel Tracer to use. Defaults to
|
|
80
|
+
* `trace.getTracer('agentfootprint', AGENTFOOTPRINT_VERSION)`
|
|
81
|
+
* (where `trace` is the lazy-imported `@opentelemetry/api`). */
|
|
82
|
+
readonly tracer?: OtelTracerLike;
|
|
83
|
+
/** 0..1 — sample rate for turn-level spans. Default `1.0`.
|
|
84
|
+
* Sampling decisions are normally an OTel SDK concern (via
|
|
85
|
+
* `Sampler`); this option is a per-strategy override for cases
|
|
86
|
+
* where the consumer wants agentfootprint to drop spans BEFORE
|
|
87
|
+
* they reach the SDK (e.g., aggressive cost control). */
|
|
88
|
+
readonly sampleRate?: number;
|
|
89
|
+
}
|
|
90
|
+
/** Subset of `@opentelemetry/api`'s `Tracer` we depend on. */
|
|
91
|
+
export interface OtelTracerLike {
|
|
92
|
+
startSpan(name: string, options?: OtelSpanOptions, context?: unknown): OtelSpanLike;
|
|
93
|
+
}
|
|
94
|
+
/** Subset of `@opentelemetry/api`'s `SpanOptions`. */
|
|
95
|
+
export interface OtelSpanOptions {
|
|
96
|
+
attributes?: Record<string, string | number | boolean>;
|
|
97
|
+
startTime?: number;
|
|
98
|
+
kind?: number;
|
|
99
|
+
}
|
|
100
|
+
/** Subset of `@opentelemetry/api`'s `Span` we depend on. */
|
|
101
|
+
export interface OtelSpanLike {
|
|
102
|
+
setAttribute(key: string, value: string | number | boolean): unknown;
|
|
103
|
+
setStatus(status: {
|
|
104
|
+
code: number;
|
|
105
|
+
message?: string;
|
|
106
|
+
}): unknown;
|
|
107
|
+
end(endTime?: number): void;
|
|
108
|
+
spanContext(): {
|
|
109
|
+
traceId: string;
|
|
110
|
+
spanId: string;
|
|
111
|
+
traceFlags: number;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export declare function otelObservability(opts: OtelObservabilityOptions): ObservabilityStrategy;
|
|
115
|
+
//# sourceMappingURL=otel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otel.d.ts","sourceRoot":"","sources":["../../../../src/adapters/observability/otel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AAIH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAIvE,MAAM,WAAW,wBAAwB;IACvC;2CACuC;IACvC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;qEAEiE;IACjE,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC;IACjC;;;;8DAI0D;IAC1D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAID,8DAA8D;AAC9D,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,YAAY,CAAC;CACrF;AAED,sDAAsD;AACtD,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,4DAA4D;AAC5D,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;IACrE,SAAS,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;IAC/D,GAAG,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CACxE;AAgBD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,wBAAwB,GAAG,qBAAqB,CA0OvF"}
|
|
@@ -29,11 +29,17 @@
|
|
|
29
29
|
* Roadmap:
|
|
30
30
|
* - agentcoreObservability ← v2.8.1
|
|
31
31
|
* - cloudwatchObservability ← v2.8.2
|
|
32
|
-
* - xrayObservability ← v2.8.3
|
|
33
|
-
* - otelObservability ← v2.9.
|
|
34
|
-
*
|
|
32
|
+
* - xrayObservability ← v2.8.3
|
|
33
|
+
* - otelObservability ← v2.9.0 (this release)
|
|
34
|
+
*
|
|
35
|
+
* Note: `datadogObservability` was on the v2.9 roadmap, but Datadog
|
|
36
|
+
* APM accepts OTLP — point your OTel SDK at Datadog's OTLP endpoint
|
|
37
|
+
* and `otelObservability` covers the Datadog use case. We'll ship a
|
|
38
|
+
* dedicated `dd-trace`-based adapter only if real-world feedback
|
|
39
|
+
* demands the native Datadog APM client.
|
|
35
40
|
*/
|
|
36
41
|
export { agentcoreObservability, type AgentcoreObservabilityOptions, } from './adapters/observability/agentcore.js';
|
|
37
42
|
export { cloudwatchObservability, type CloudwatchObservabilityOptions, } from './adapters/observability/cloudwatch.js';
|
|
38
43
|
export { xrayObservability, type XrayObservabilityOptions, type XRayLikeClient, } from './adapters/observability/xray.js';
|
|
44
|
+
export { otelObservability, type OtelObservabilityOptions, type OtelTracerLike, type OtelSpanLike, type OtelSpanOptions, } from './adapters/observability/otel.js';
|
|
39
45
|
//# sourceMappingURL=observability-providers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observability-providers.d.ts","sourceRoot":"","sources":["../../src/observability-providers.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"observability-providers.d.ts","sourceRoot":"","sources":["../../src/observability-providers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,EACL,sBAAsB,EACtB,KAAK,6BAA6B,GACnC,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EACL,uBAAuB,EACvB,KAAK,8BAA8B,GACpC,MAAM,wCAAwC,CAAC;AAChD,OAAO,EACL,iBAAiB,EACjB,KAAK,wBAAwB,EAC7B,KAAK,cAAc,GACpB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,iBAAiB,EACjB,KAAK,wBAAwB,EAC7B,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,eAAe,GACrB,MAAM,kCAAkC,CAAC"}
|