@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.
Files changed (43) hide show
  1. package/README.md +50 -15
  2. package/ai/ai-adapters.md +7 -0
  3. package/ai/ai-agents-streaming.md +187 -149
  4. package/ai/ai-budget-resilience.md +305 -132
  5. package/ai/ai-communication.md +220 -197
  6. package/ai/ai-debug-observability.md +259 -173
  7. package/ai/ai-guardrails-memory.md +191 -153
  8. package/ai/ai-mcp-rag.md +204 -199
  9. package/ai/ai-multi-agent.md +254 -153
  10. package/ai/ai-orchestrator.md +353 -114
  11. package/ai/ai-security.md +287 -180
  12. package/ai/ai-tasks.md +8 -1
  13. package/ai/ai-testing-evals.md +363 -256
  14. package/core/anti-patterns.md +18 -2
  15. package/core/constraints.md +13 -2
  16. package/core/core-patterns.md +12 -0
  17. package/core/error-boundaries.md +8 -0
  18. package/core/history.md +7 -0
  19. package/core/multi-module.md +15 -3
  20. package/core/naming.md +128 -90
  21. package/core/plugins.md +7 -0
  22. package/core/react-adapter.md +256 -174
  23. package/core/resolvers.md +10 -0
  24. package/core/schema-types.md +8 -0
  25. package/core/system-api.md +10 -0
  26. package/core/testing.md +257 -143
  27. package/examples/checkers.ts +15 -16
  28. package/examples/contact-form.ts +2 -2
  29. package/examples/counter-react.ts +1 -1
  30. package/examples/counter-svelte.ts +1 -1
  31. package/examples/counter-vue.ts +1 -1
  32. package/examples/counter.ts +1 -1
  33. package/examples/data-triggers.ts +4 -4
  34. package/examples/feature-flags.ts +2 -2
  35. package/examples/form-wizard.ts +2 -2
  36. package/examples/newsletter.ts +2 -2
  37. package/examples/server.ts +2 -2
  38. package/examples/shopping-cart.ts +1 -1
  39. package/examples/topic-guard.ts +1 -1
  40. package/package.json +3 -3
  41. package/sitemap.md +17 -11
  42. package/examples/debounce-constraints.ts +0 -95
  43. package/examples/multi-module.ts +0 -58
@@ -1,235 +1,408 @@
1
- # AI Budget and Resilience
1
+ # AI budget + resilience
2
2
 
3
- Budget wrappers, retry policies, fallback chains, circuit breakers, health monitors, semantic caching, and constraint-driven provider routing for AI runners.
3
+ > Covers `@directive-run/ai` `withBudget`, `withRetry`, `withFallback`, `createCircuitBreaker`, `createHealthMonitor`, `createSemanticCache`, `createConstraintRouter`.
4
4
 
5
- ## Decision Tree: "How do I protect my AI calls?"
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 → withBudget(runner, { budgets: [...] })
10
- ├── Transient errors → withRetry(runner, { maxRetries, backoff })
11
- ├── Provider outage → withFallback(primaryRunner, fallbackRunner)
12
- ├── Repeated failures → createCircuitBreaker({ failureThreshold })
13
- ├── Fleet monitoring → createHealthMonitor({ agents: {...} })
14
-
15
- Want to avoid redundant LLM calls?
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
- ## Budget Wrapping
20
+ ## `withBudget(runner, config)` — cost caps
23
21
 
24
- Wrap any runner with cost tracking and enforcement per time window:
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
- // Time-window budgets (multiple allowed)
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
- window: "hour",
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
- // Callback when approaching limit (0-1 percentage)
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
- ### Anti-Pattern #29: budgetWarningThreshold out of range
44
+ Catching the throw:
56
45
 
57
46
  ```typescript
58
- // WRONG threshold must be a 0-1 percentage
59
- const budgetRunner = withBudget(baseRunner, {
60
- budgetWarningThreshold: 80, // Not a percentage!
61
- budgets: [{ window: "hour", maxCost: 1.0, pricing: { inputPerMillion: 3, outputPerMillion: 15 } }],
62
- });
63
-
64
- // CORRECT – use a decimal between 0 and 1
65
- const budgetRunner = withBudget(baseRunner, {
66
- budgetWarningThreshold: 0.8, // 80%
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
- ## Retry Policies
58
+ ## `withRetry(runner, config)` — transient-error retry
72
59
 
73
- Wrap a runner with automatic retry on transient failures:
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
- backoff: "exponential", // "exponential" | "linear" | "none"
81
- baseDelayMs: 100,
82
-
83
- // Only retry specific errors
84
- shouldRetry: (error) => {
85
- return error.status === 429 || error.status >= 500;
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
- ## Fallback Chains
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
- Automatically switch to a backup runner when the primary fails:
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 primary = createAnthropicRunner({ apiKey: process.env.ANTHROPIC_API_KEY });
100
- const backup = createOpenAIRunner({ apiKey: process.env.OPENAI_API_KEY });
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
- // Falls back to backup when primary throws
103
- const resilientRunner = withFallback(primary, backup);
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
- ## Circuit Breaker
132
+ ## `createCircuitBreaker(config)` — failure isolation
107
133
 
108
- Prevent repeated calls to a failing provider. Opens after N failures, resets after a timeout:
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/ai";
137
+ import { createCircuitBreaker, CircuitBreakerOpenError } from "@directive-run/core";
112
138
 
113
139
  const breaker = createCircuitBreaker({
114
- failureThreshold: 3, // Open after 3 consecutive failures
115
- resetTimeout: 30000, // Try again after 30s (half-open state)
116
- halfOpenMaxAttempts: 1, // Allow 1 test request in half-open
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
- const protectedRunner = breaker.wrap(baseRunner);
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
- // Check state
123
- console.log(breaker.state); // "closed" | "open" | "half-open"
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
- ### Anti-Pattern #28: Sharing a CircuitBreaker across unrelated agents
161
+ Catching the OPEN throw:
128
162
 
129
163
  ```typescript
130
- // WRONG – one failing agent opens the breaker for all agents
131
- const sharedBreaker = createCircuitBreaker({ failureThreshold: 3, resetTimeout: 30000 });
132
- const researchRunner = sharedBreaker.wrap(baseRunner);
133
- const writerRunner = sharedBreaker.wrap(baseRunner); // Same breaker!
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
- // CORRECT each agent gets its own breaker instance
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
- const researchRunner = researchBreaker.wrap(baseRunner);
140
- const writerRunner = writerBreaker.wrap(baseRunner);
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
- ## Health Monitor
188
+ ## `createHealthMonitor(config?)` — fleet health metrics
144
189
 
145
- Monitor agent health across the system, track circuit breaker states, and report status:
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
- agents: {
152
- researcher: { runner: researchRunner, circuitBreaker: researchBreaker },
153
- writer: { runner: writerRunner, circuitBreaker: writerBreaker },
154
- },
155
- checkInterval: 60000, // Health check every 60s
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
- monitor.start();
163
- const report = monitor.getReport();
164
- monitor.stop();
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
- ## Semantic Cache
225
+ ## `createSemanticCache(config)` — embedding-based response cache
168
226
 
169
- Cache LLM responses by semantic similarity to avoid redundant calls:
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
- import { createOpenAIEmbedder } from "@directive-run/ai/openai";
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: createOpenAIEmbedder({ apiKey: process.env.OPENAI_API_KEY }),
177
- similarity: 0.98, // Minimum cosine similarity to count as a hit
178
- maxSize: 1000, // Max cached entries (LRU eviction)
179
- ttl: 3600000, // Cache entry TTL in ms (1 hour)
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
- // Wrap a runner with caching
183
- const cachedRunner = cache.wrap(baseRunner);
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
- ## Constraint-Driven Provider Routing
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
- Route LLM calls to different providers based on runtime constraints:
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", runner: openaiRunner, costPerMillion: 5 },
197
- { name: "ollama", runner: ollamaRunner, costPerMillion: 0 },
271
+ { name: "openai", runner: openaiRunner, costPerMillion: 5 },
198
272
  ],
199
273
  constraints: [
200
- // Use cheapest provider when budget is low
201
- { when: (context) => context.budgetRemaining < 1.0, prefer: "ollama" },
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
- ## Combining Wrappers
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
- Wrappers compose – apply them inside-out (innermost runs first):
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
- // Order: retry → budget → fallback
216
- const resilientRunner = withFallback(
217
- withBudget(
218
- withRetry(primaryRunner, { maxRetries: 3, backoff: "exponential", baseDelayMs: 100 }),
219
- { budgets: [{ window: "hour", maxCost: 1.0, pricing: { inputPerMillion: 3, outputPerMillion: 15 } }] },
220
- ),
221
- fallbackRunner,
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
- ## Quick Reference
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 | Purpose | Key Options |
394
+ | Utility | Shape | Throws |
228
395
  |---|---|---|
229
- | `withBudget` | Cost caps per time window | `budgets`, `maxCostPerCall` |
230
- | `withRetry` | Retry transient failures | `maxRetries`, `backoff`, `shouldRetry` |
231
- | `withFallback` | Switch to backup runner | primary, fallback runners |
232
- | `createCircuitBreaker` | Stop calling failing providers | `failureThreshold`, `resetTimeout` |
233
- | `createHealthMonitor` | Fleet health tracking | `agents`, `checkInterval` |
234
- | `createSemanticCache` | Avoid redundant LLM calls | `similarity`, `maxSize`, `ttl` |
235
- | `createConstraintRouter` | Dynamic provider selection | `providers`, `constraints` |
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