@directive-run/knowledge 1.14.0 → 1.16.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/README.md +50 -15
- package/ai/ai-adapters.md +7 -0
- package/ai/ai-agents-streaming.md +187 -149
- package/ai/ai-budget-resilience.md +305 -132
- package/ai/ai-communication.md +220 -197
- package/ai/ai-debug-observability.md +259 -173
- package/ai/ai-guardrails-memory.md +191 -153
- package/ai/ai-mcp-rag.md +204 -199
- package/ai/ai-multi-agent.md +254 -153
- package/ai/ai-orchestrator.md +353 -114
- package/ai/ai-security.md +287 -180
- package/ai/ai-tasks.md +8 -1
- package/ai/ai-testing-evals.md +363 -256
- package/core/anti-patterns.md +18 -2
- package/core/constraints.md +13 -2
- package/core/core-patterns.md +12 -0
- package/core/error-boundaries.md +8 -0
- package/core/history.md +7 -0
- package/core/multi-module.md +15 -3
- package/core/naming.md +128 -90
- package/core/plugins.md +7 -0
- package/core/react-adapter.md +256 -174
- package/core/resolvers.md +10 -0
- package/core/schema-types.md +8 -0
- package/core/system-api.md +10 -0
- package/core/testing.md +257 -143
- package/examples/checkers.ts +15 -16
- package/examples/contact-form.ts +2 -2
- package/examples/counter-react.ts +1 -1
- package/examples/counter-svelte.ts +1 -1
- package/examples/counter-vue.ts +1 -1
- package/examples/counter.ts +1 -1
- package/examples/data-triggers.ts +4 -4
- package/examples/feature-flags.ts +2 -2
- package/examples/form-wizard.ts +2 -2
- package/examples/newsletter.ts +2 -2
- package/examples/server.ts +2 -2
- package/examples/shopping-cart.ts +1 -1
- package/examples/topic-guard.ts +1 -1
- package/package.json +3 -3
- package/sitemap.md +17 -11
- package/examples/debounce-constraints.ts +0 -95
- package/examples/multi-module.ts +0 -58
|
@@ -1,235 +1,408 @@
|
|
|
1
|
-
# AI
|
|
1
|
+
# AI budget + resilience
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Covers `@directive-run/ai` — `withBudget`, `withRetry`, `withFallback`, `createCircuitBreaker`, `createHealthMonitor`, `createSemanticCache`, `createConstraintRouter`.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Runner wrappers and supporting utilities for cost control, retry, fallback, circuit breaking, fleet health tracking, semantic caching, and constraint-driven provider routing. Compose these around any `AgentRunner` before passing it to an orchestrator.
|
|
6
|
+
|
|
7
|
+
## Decision tree
|
|
6
8
|
|
|
7
9
|
```
|
|
8
10
|
What failure mode are you guarding against?
|
|
9
|
-
├── Cost overruns
|
|
10
|
-
├── Transient errors
|
|
11
|
-
├── Provider outage
|
|
12
|
-
├── Repeated
|
|
13
|
-
├── Fleet
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
├── Yes, similar inputs → createSemanticCache({ embedder, similarity })
|
|
17
|
-
│
|
|
18
|
-
Need dynamic provider selection?
|
|
19
|
-
└── Yes → createConstraintRouter({ providers: [...], constraints: [...] })
|
|
11
|
+
├── Cost overruns → withBudget(runner, config)
|
|
12
|
+
├── Transient errors → withRetry(runner, config)
|
|
13
|
+
├── Provider outage → withFallback([primary, backup], config?)
|
|
14
|
+
├── Repeated cascading fails → createCircuitBreaker(config) + breaker.execute(fn)
|
|
15
|
+
├── Fleet-wide health → createHealthMonitor() + recordSuccess/Failure
|
|
16
|
+
├── Redundant LLM calls → createSemanticCache(config) + createSemanticCacheGuardrail
|
|
17
|
+
└── Dynamic provider pick → createConstraintRouter({ providers, constraints })
|
|
20
18
|
```
|
|
21
19
|
|
|
22
|
-
##
|
|
20
|
+
## `withBudget(runner, config)` — cost caps
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
`BudgetConfig` controls per-call and per-window cost limits. The `onBudgetExceeded` callback fires before the wrapper throws `BudgetExceededError`. There is NO `budgetWarningThreshold` or `onWarning` option on this wrapper — for percentage-based warnings, use `onBudgetWarning` on `createAgentOrchestrator` instead.
|
|
25
23
|
|
|
26
24
|
```typescript
|
|
27
25
|
import { withBudget } from "@directive-run/ai";
|
|
28
26
|
|
|
27
|
+
const pricing = { inputPerMillion: 3, outputPerMillion: 15 };
|
|
28
|
+
|
|
29
29
|
const budgetRunner = withBudget(baseRunner, {
|
|
30
|
-
// Hard cap per single LLM call
|
|
31
30
|
maxCostPerCall: 0.10,
|
|
32
|
-
|
|
33
|
-
//
|
|
31
|
+
pricing, // required when maxCostPerCall is set
|
|
32
|
+
estimatedOutputMultiplier: 1.0, // for summarization use 0.3; for generation use 3.0
|
|
33
|
+
charsPerToken: 4,
|
|
34
34
|
budgets: [
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
maxCost: 1.0,
|
|
38
|
-
pricing: { inputPerMillion: 3, outputPerMillion: 15 },
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
window: "day",
|
|
42
|
-
maxCost: 10.0,
|
|
43
|
-
pricing: { inputPerMillion: 3, outputPerMillion: 15 },
|
|
44
|
-
},
|
|
35
|
+
{ window: "hour", maxCost: 5.00, pricing },
|
|
36
|
+
{ window: "day", maxCost: 50.00, pricing },
|
|
45
37
|
],
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
budgetWarningThreshold: 0.8,
|
|
49
|
-
onWarning: (usage) => {
|
|
50
|
-
console.warn(`Budget at ${(usage.percentage * 100).toFixed(0)}%`);
|
|
38
|
+
onBudgetExceeded: ({ estimated, remaining, window }) => {
|
|
39
|
+
console.warn(`[budget] ${window} would overshoot — est $${estimated.toFixed(4)}, remaining $${remaining.toFixed(4)}`);
|
|
51
40
|
},
|
|
52
41
|
});
|
|
53
42
|
```
|
|
54
43
|
|
|
55
|
-
|
|
44
|
+
Catching the throw:
|
|
56
45
|
|
|
57
46
|
```typescript
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
budgets: [{ window: "hour", maxCost: 1.0, pricing: { inputPerMillion: 3, outputPerMillion: 15 } }],
|
|
68
|
-
});
|
|
47
|
+
import { BudgetExceededError } from "@directive-run/ai";
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
await budgetRunner(agent, prompt);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (err instanceof BudgetExceededError) {
|
|
53
|
+
console.log(err.window, err.estimated, err.remaining);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
69
56
|
```
|
|
70
57
|
|
|
71
|
-
##
|
|
58
|
+
## `withRetry(runner, config)` — transient-error retry
|
|
72
59
|
|
|
73
|
-
|
|
60
|
+
Exponential backoff with jitter is hard-coded; `backoff: "linear"` / `"none"` do NOT exist. The retry decision goes through built-in HTTP-status checks (non-retryable: 400 / 401 / 403 / 404 / 422) then your `isRetryable` predicate.
|
|
74
61
|
|
|
75
62
|
```typescript
|
|
76
63
|
import { withRetry } from "@directive-run/ai";
|
|
77
64
|
|
|
78
65
|
const retryRunner = withRetry(baseRunner, {
|
|
79
|
-
maxRetries: 3,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
66
|
+
maxRetries: 3, // default 3
|
|
67
|
+
baseDelayMs: 1000, // default 1000
|
|
68
|
+
maxDelayMs: 30_000, // default 30000
|
|
69
|
+
isRetryable: (error) => {
|
|
70
|
+
const msg = error.message.toLowerCase();
|
|
71
|
+
return msg.includes("rate_limit") || msg.includes("503") || msg.includes("504");
|
|
72
|
+
},
|
|
73
|
+
onRetry: (attempt, error, delayMs) => {
|
|
74
|
+
console.log(`retry ${attempt} in ${delayMs}ms — ${error.message}`);
|
|
86
75
|
},
|
|
87
76
|
});
|
|
88
77
|
```
|
|
89
78
|
|
|
90
|
-
|
|
79
|
+
Catching the exhausted throw:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { RetryExhaustedError } from "@directive-run/ai";
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await retryRunner(agent, prompt);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (err instanceof RetryExhaustedError) {
|
|
88
|
+
console.log(`gave up after ${err.retryCount} retries; last: ${err.lastError.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## `withFallback(runners[], config?)` — provider chain
|
|
91
94
|
|
|
92
|
-
|
|
95
|
+
**`withFallback` takes an ARRAY of runners as positional arg 1**, then an optional config. The chain tries each runner in order; the first success wins. When all fail, `AllProvidersFailedError` is thrown carrying all per-runner errors.
|
|
93
96
|
|
|
94
97
|
```typescript
|
|
95
|
-
import { withFallback } from "@directive-run/ai";
|
|
98
|
+
import { withFallback, withRetry } from "@directive-run/ai";
|
|
96
99
|
import { createAnthropicRunner } from "@directive-run/ai/anthropic";
|
|
97
100
|
import { createOpenAIRunner } from "@directive-run/ai/openai";
|
|
101
|
+
import { createOllamaRunner } from "@directive-run/ai/ollama";
|
|
98
102
|
|
|
99
|
-
const
|
|
100
|
-
|
|
103
|
+
const resilient = withFallback(
|
|
104
|
+
[
|
|
105
|
+
withRetry(createAnthropicRunner({ apiKey: process.env.ANTHROPIC_API_KEY! }), { maxRetries: 2 }),
|
|
106
|
+
withRetry(createOpenAIRunner({ apiKey: process.env.OPENAI_API_KEY! }), { maxRetries: 2 }),
|
|
107
|
+
createOllamaRunner({ baseUrl: "http://localhost:11434" }),
|
|
108
|
+
],
|
|
109
|
+
{
|
|
110
|
+
shouldFallback: (error) => !error.message.includes("invalid_api_key"),
|
|
111
|
+
onFallback: (fromIndex, toIndex, error) => {
|
|
112
|
+
console.log(`falling back from runner ${fromIndex} to ${toIndex}: ${error.message}`);
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
```
|
|
101
117
|
|
|
102
|
-
|
|
103
|
-
|
|
118
|
+
Catching all-failed:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { AllProvidersFailedError } from "@directive-run/ai";
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await resilient(agent, prompt);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
if (err instanceof AllProvidersFailedError) {
|
|
127
|
+
err.errors.forEach((e, i) => console.log(`[${i}] ${e.message}`));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
104
130
|
```
|
|
105
131
|
|
|
106
|
-
##
|
|
132
|
+
## `createCircuitBreaker(config)` — failure isolation
|
|
107
133
|
|
|
108
|
-
|
|
134
|
+
The circuit breaker is from `@directive-run/core` (it's not AI-specific). Use `breaker.execute(fn)` to run an arbitrary async closure inside the breaker; there is NO `breaker.wrap(runner)` shortcut.
|
|
109
135
|
|
|
110
136
|
```typescript
|
|
111
|
-
import { createCircuitBreaker } from "@directive-run/
|
|
137
|
+
import { createCircuitBreaker, CircuitBreakerOpenError } from "@directive-run/core";
|
|
112
138
|
|
|
113
139
|
const breaker = createCircuitBreaker({
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
140
|
+
name: "openai",
|
|
141
|
+
failureThreshold: 5, // default 5
|
|
142
|
+
recoveryTimeMs: 30_000, // default 30000
|
|
143
|
+
halfOpenMaxRequests: 3, // default 3 — concurrent test requests when half-open
|
|
144
|
+
failureWindowMs: 60_000, // default 60000
|
|
117
145
|
});
|
|
118
146
|
|
|
119
|
-
// Use with a runner
|
|
120
|
-
|
|
147
|
+
// Use with a runner: wrap the call inside execute()
|
|
148
|
+
async function callAgent(agent: AgentLike, input: string) {
|
|
149
|
+
return await breaker.execute(() => baseRunner(agent, input));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Inspect state
|
|
153
|
+
console.log(breaker.getState()); // "CLOSED" | "OPEN" | "HALF_OPEN"
|
|
154
|
+
console.log(breaker.getStats()); // { successCount, failureCount, lastFailureTime, ... }
|
|
155
|
+
console.log(breaker.isAllowed()); // boolean — would a request be allowed now?
|
|
121
156
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
breaker.reset(); // Force back to closed
|
|
157
|
+
breaker.reset(); // force back to CLOSED, clear stats
|
|
158
|
+
breaker.forceState("OPEN"); // testing helper
|
|
125
159
|
```
|
|
126
160
|
|
|
127
|
-
|
|
161
|
+
Catching the OPEN throw:
|
|
128
162
|
|
|
129
163
|
```typescript
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
164
|
+
try {
|
|
165
|
+
await breaker.execute(() => baseRunner(agent, prompt));
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (err instanceof CircuitBreakerOpenError) {
|
|
168
|
+
console.log(err.code); // "CIRCUIT_OPEN"
|
|
169
|
+
console.log(err.state); // "OPEN" | "HALF_OPEN"
|
|
170
|
+
console.log(err.retryAfterMs); // wait this long before next attempt
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
134
174
|
|
|
135
|
-
|
|
136
|
-
const researchBreaker = createCircuitBreaker({ failureThreshold: 3, resetTimeout: 30000 });
|
|
137
|
-
const writerBreaker = createCircuitBreaker({ failureThreshold: 3, resetTimeout: 30000 });
|
|
175
|
+
### Don't share a breaker across unrelated runners
|
|
138
176
|
|
|
139
|
-
|
|
140
|
-
|
|
177
|
+
```typescript
|
|
178
|
+
// WRONG — one failing runner trips the breaker for all of them
|
|
179
|
+
const shared = createCircuitBreaker({ name: "shared" });
|
|
180
|
+
async function research(agent, input) { return shared.execute(() => researchRunner(agent, input)); }
|
|
181
|
+
async function write(agent, input) { return shared.execute(() => writerRunner(agent, input)); }
|
|
182
|
+
|
|
183
|
+
// CORRECT — one breaker per failure domain
|
|
184
|
+
const researchBreaker = createCircuitBreaker({ name: "anthropic-research" });
|
|
185
|
+
const writerBreaker = createCircuitBreaker({ name: "openai-writer" });
|
|
141
186
|
```
|
|
142
187
|
|
|
143
|
-
##
|
|
188
|
+
## `createHealthMonitor(config?)` — fleet health metrics
|
|
144
189
|
|
|
145
|
-
|
|
190
|
+
`createHealthMonitor` is a metrics tracker: you record successes/failures into it; it computes per-agent health scores you can use to drive routing or observability. It is NOT an auto-polling daemon. There is no `monitor.start()`, no `monitor.stop()`, no `monitor.getReport()`, no `agents:` config, no `checkInterval`, and no `onStatusChange` callback — those don't exist.
|
|
146
191
|
|
|
147
192
|
```typescript
|
|
148
193
|
import { createHealthMonitor } from "@directive-run/ai";
|
|
149
194
|
|
|
150
195
|
const monitor = createHealthMonitor({
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
onStatusChange: (agent, status) => {
|
|
158
|
-
console.log(`${agent}: ${status}`); // "healthy" | "degraded" | "unhealthy"
|
|
196
|
+
windowMs: 60_000, // rolling window for metrics (default 60s)
|
|
197
|
+
maxNormalLatencyMs: 5000, // above this counts as "slow" for the latency component
|
|
198
|
+
weights: { // weights for the composite health score
|
|
199
|
+
successRate: 0.5,
|
|
200
|
+
latency: 0.3,
|
|
201
|
+
circuitState: 0.2,
|
|
159
202
|
},
|
|
160
203
|
});
|
|
161
204
|
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
205
|
+
// Record outcomes from your runner calls
|
|
206
|
+
const start = Date.now();
|
|
207
|
+
try {
|
|
208
|
+
const result = await baseRunner(agent, input);
|
|
209
|
+
monitor.recordSuccess("researcher", Date.now() - start);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
monitor.recordFailure("researcher", Date.now() - start, err as Error);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// If you also use a circuit breaker, mirror its state into the monitor
|
|
215
|
+
monitor.updateCircuitState("researcher", breaker.getState() === "OPEN" ? "open" : "closed");
|
|
216
|
+
|
|
217
|
+
// Read
|
|
218
|
+
console.log(monitor.getHealthScore("researcher")); // 0-100 (50 when no data)
|
|
219
|
+
console.log(monitor.getMetrics("researcher")); // { successRate, avgLatency, ... }
|
|
220
|
+
console.log(monitor.getAllMetrics()); // Record<agentId, metrics>
|
|
221
|
+
|
|
222
|
+
monitor.reset();
|
|
165
223
|
```
|
|
166
224
|
|
|
167
|
-
##
|
|
225
|
+
## `createSemanticCache(config)` — embedding-based response cache
|
|
168
226
|
|
|
169
|
-
|
|
227
|
+
Semantic caching avoids redundant LLM calls when a new input is close enough to one already seen. Wire it into an orchestrator via the cache guardrail factory — there is NO `cache.wrap(runner)` method.
|
|
170
228
|
|
|
171
229
|
```typescript
|
|
172
|
-
import { createSemanticCache } from "@directive-run/ai";
|
|
173
|
-
|
|
230
|
+
import { createSemanticCache, createSemanticCacheGuardrail, type EmbedderFn } from "@directive-run/ai";
|
|
231
|
+
|
|
232
|
+
// You supply the embedder — no createOpenAIEmbedder / createAnthropicEmbedder exists.
|
|
233
|
+
// `EmbedderFn = (text: string) => Promise<number[]>`
|
|
234
|
+
const embedder: EmbedderFn = async (text) => {
|
|
235
|
+
// call your embedding model (OpenAI, Voyage, local model, …)
|
|
236
|
+
return await myEmbedAPI(text);
|
|
237
|
+
};
|
|
174
238
|
|
|
175
239
|
const cache = createSemanticCache({
|
|
176
|
-
embedder
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
240
|
+
embedder,
|
|
241
|
+
similarityThreshold: 0.95,
|
|
242
|
+
maxCacheSize: 1000,
|
|
243
|
+
ttlMs: 60 * 60 * 1000, // 1 hour
|
|
244
|
+
onHit: (entry, sim) => console.log(`cache hit @ ${(sim * 100).toFixed(1)}%`),
|
|
245
|
+
onMiss: (query) => console.log(`miss: ${query.slice(0, 40)}…`),
|
|
180
246
|
});
|
|
181
247
|
|
|
182
|
-
|
|
183
|
-
|
|
248
|
+
const cacheGuardrail = createSemanticCacheGuardrail({ cache });
|
|
249
|
+
|
|
250
|
+
const orchestrator = createAgentOrchestrator({
|
|
251
|
+
runner: baseRunner,
|
|
252
|
+
guardrails: {
|
|
253
|
+
input: [cacheGuardrail], // short-circuits with the cached response when a hit fires
|
|
254
|
+
},
|
|
255
|
+
});
|
|
184
256
|
```
|
|
185
257
|
|
|
186
|
-
|
|
258
|
+
For testing, a `createTestEmbedder` is exported from `@directive-run/ai` that produces deterministic embeddings without API calls.
|
|
259
|
+
|
|
260
|
+
## `createConstraintRouter(config)` — constraint-driven provider selection
|
|
187
261
|
|
|
188
|
-
|
|
262
|
+
Routes each runner call to a provider based on runtime context.
|
|
189
263
|
|
|
190
264
|
```typescript
|
|
191
265
|
import { createConstraintRouter } from "@directive-run/ai";
|
|
192
266
|
|
|
193
267
|
const router = createConstraintRouter({
|
|
194
268
|
providers: [
|
|
269
|
+
{ name: "ollama", runner: ollamaRunner, costPerMillion: 0 },
|
|
195
270
|
{ name: "anthropic", runner: anthropicRunner, costPerMillion: 3 },
|
|
196
|
-
{ name: "openai",
|
|
197
|
-
{ name: "ollama", runner: ollamaRunner, costPerMillion: 0 },
|
|
271
|
+
{ name: "openai", runner: openaiRunner, costPerMillion: 5 },
|
|
198
272
|
],
|
|
199
273
|
constraints: [
|
|
200
|
-
|
|
201
|
-
{ when: (
|
|
202
|
-
// Use best provider for high-priority tasks
|
|
203
|
-
{ when: (context) => context.priority === "high", prefer: "anthropic" },
|
|
274
|
+
{ when: (ctx) => ctx.budgetRemaining < 1.0, prefer: "ollama" },
|
|
275
|
+
{ when: (ctx) => ctx.priority === "high", prefer: "anthropic" },
|
|
204
276
|
],
|
|
205
277
|
});
|
|
206
278
|
```
|
|
207
279
|
|
|
208
|
-
|
|
280
|
+
`router` returns an `AgentRunner` you pass directly to an orchestrator. The router's context fields depend on your config; check the `ConstraintRouterConfig` type for the full surface.
|
|
209
281
|
|
|
210
|
-
|
|
282
|
+
## Composing wrappers
|
|
283
|
+
|
|
284
|
+
Wrappers are pure functions — compose them inside-out (innermost runs first per call).
|
|
211
285
|
|
|
212
286
|
```typescript
|
|
213
287
|
import { withBudget, withRetry, withFallback } from "@directive-run/ai";
|
|
214
288
|
|
|
215
|
-
//
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
289
|
+
// Per call: retry first → budget cap on retried call → fallback to backup if all retries fail
|
|
290
|
+
const resilient = withFallback(
|
|
291
|
+
[
|
|
292
|
+
withBudget(
|
|
293
|
+
withRetry(primaryRunner, { maxRetries: 3 }),
|
|
294
|
+
{
|
|
295
|
+
pricing: { inputPerMillion: 3, outputPerMillion: 15 },
|
|
296
|
+
budgets: [{ window: "hour", maxCost: 5.0, pricing: { inputPerMillion: 3, outputPerMillion: 15 } }],
|
|
297
|
+
},
|
|
298
|
+
),
|
|
299
|
+
fallbackRunner,
|
|
300
|
+
],
|
|
222
301
|
);
|
|
223
302
|
```
|
|
224
303
|
|
|
225
|
-
##
|
|
304
|
+
## Anti-patterns
|
|
305
|
+
|
|
306
|
+
### `withFallback(primary, backup)` (two positional args)
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
// WRONG — withFallback takes a single ARRAY of runners, then an optional config
|
|
310
|
+
withFallback(primary, backup)
|
|
311
|
+
|
|
312
|
+
// CORRECT
|
|
313
|
+
withFallback([primary, backup], { onFallback: (from, to, err) => {} })
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### `withRetry` with `backoff: "linear" | "none"` or `shouldRetry`
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
// WRONG — backoff is always exponential+jitter (not configurable); shouldRetry is named isRetryable
|
|
320
|
+
withRetry(runner, { maxRetries: 3, backoff: "exponential", shouldRetry: (e) => true })
|
|
321
|
+
|
|
322
|
+
// CORRECT
|
|
323
|
+
withRetry(runner, { maxRetries: 3, baseDelayMs: 500, isRetryable: (e) => /5\d{2}/.test(e.message) })
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### `createCircuitBreaker({ resetTimeout, halfOpenMaxAttempts })`
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
// WRONG — option names from a different library
|
|
330
|
+
createCircuitBreaker({ resetTimeout: 30_000, halfOpenMaxAttempts: 1 })
|
|
331
|
+
|
|
332
|
+
// CORRECT
|
|
333
|
+
createCircuitBreaker({ recoveryTimeMs: 30_000, halfOpenMaxRequests: 1 })
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### `breaker.wrap(runner)` / `breaker.state`
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// WRONG — these don't exist on CircuitBreaker
|
|
340
|
+
const protected = breaker.wrap(baseRunner);
|
|
341
|
+
console.log(breaker.state);
|
|
342
|
+
|
|
343
|
+
// CORRECT
|
|
344
|
+
const result = await breaker.execute(() => baseRunner(agent, input));
|
|
345
|
+
console.log(breaker.getState());
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### `createHealthMonitor({ agents, checkInterval, onStatusChange })` / `monitor.start()` / `monitor.getReport()`
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
// WRONG — none of these exist
|
|
352
|
+
const monitor = createHealthMonitor({
|
|
353
|
+
agents: { researcher: { runner, circuitBreaker } },
|
|
354
|
+
checkInterval: 60_000,
|
|
355
|
+
onStatusChange: (id, s) => {},
|
|
356
|
+
});
|
|
357
|
+
monitor.start();
|
|
358
|
+
const report = monitor.getReport();
|
|
359
|
+
|
|
360
|
+
// CORRECT — push metrics in from your call sites
|
|
361
|
+
const monitor = createHealthMonitor({ windowMs: 60_000 });
|
|
362
|
+
monitor.recordSuccess("researcher", latencyMs);
|
|
363
|
+
monitor.recordFailure("researcher", latencyMs, error);
|
|
364
|
+
const score = monitor.getHealthScore("researcher");
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### `createOpenAIEmbedder` / `createAnthropicEmbedder`
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
// WRONG — no embedder factories ship in @directive-run/ai
|
|
371
|
+
import { createOpenAIEmbedder } from "@directive-run/ai/openai";
|
|
372
|
+
|
|
373
|
+
// CORRECT — pass your own EmbedderFn
|
|
374
|
+
const embedder: EmbedderFn = async (text) => {
|
|
375
|
+
const res = await myEmbedAPI.embed(text);
|
|
376
|
+
return res.embedding;
|
|
377
|
+
};
|
|
378
|
+
const cache = createSemanticCache({ embedder, similarityThreshold: 0.95 });
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### `cache.wrap(runner)`
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// WRONG — SemanticCache doesn't have a wrap() method
|
|
385
|
+
const cachedRunner = cache.wrap(baseRunner);
|
|
386
|
+
|
|
387
|
+
// CORRECT — wire it in as a guardrail
|
|
388
|
+
const cacheGuardrail = createSemanticCacheGuardrail({ cache });
|
|
389
|
+
const orchestrator = createAgentOrchestrator({ runner: baseRunner, guardrails: { input: [cacheGuardrail] } });
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Quick reference
|
|
226
393
|
|
|
227
|
-
| Utility |
|
|
394
|
+
| Utility | Shape | Throws |
|
|
228
395
|
|---|---|---|
|
|
229
|
-
| `withBudget` |
|
|
230
|
-
| `withRetry` |
|
|
231
|
-
| `withFallback` |
|
|
232
|
-
| `createCircuitBreaker` |
|
|
233
|
-
| `createHealthMonitor` |
|
|
234
|
-
| `createSemanticCache` |
|
|
235
|
-
| `createConstraintRouter` |
|
|
396
|
+
| `withBudget(runner, BudgetConfig)` | wraps an AgentRunner | `BudgetExceededError` |
|
|
397
|
+
| `withRetry(runner, RetryConfig)` | wraps an AgentRunner | `RetryExhaustedError` |
|
|
398
|
+
| `withFallback(runners[], config?)` | wraps an array of AgentRunners | `AllProvidersFailedError` |
|
|
399
|
+
| `createCircuitBreaker(config)` | returns `CircuitBreaker` with `execute(fn)` | `CircuitBreakerOpenError` |
|
|
400
|
+
| `createHealthMonitor(config?)` | returns `HealthMonitor`; push outcomes in via `recordSuccess/Failure` | — |
|
|
401
|
+
| `createSemanticCache(config)` | returns `SemanticCache`; use via `createSemanticCacheGuardrail({ cache })` | — |
|
|
402
|
+
| `createConstraintRouter(config)` | returns an `AgentRunner` that picks providers at call time | — |
|
|
403
|
+
|
|
404
|
+
## See also
|
|
405
|
+
|
|
406
|
+
- [`ai-orchestrator.md`](./ai-orchestrator.md) — `circuitBreaker` / `selfHealing` / `agentRetry` / `maxTokenBudget` options on `createAgentOrchestrator`
|
|
407
|
+
- [`ai-guardrails-memory.md`](./ai-guardrails-memory.md) — `createSemanticCacheGuardrail({ cache })` wires this file's `SemanticCache` into the orchestrator
|
|
408
|
+
- [`ai-debug-observability.md`](./ai-debug-observability.md) — recording retry storms, circuit-breaker state changes, and budget warnings in the debug timeline
|