@directive-run/claude-plugin 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 (83) hide show
  1. package/.claude-plugin/plugin.json +6 -0
  2. package/LICENSE +26 -0
  3. package/README.md +113 -0
  4. package/dist/index.cjs +2 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.cts +66 -0
  7. package/dist/index.d.ts +66 -0
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.map +1 -0
  10. package/package.json +64 -0
  11. package/skills/README.md +9 -0
  12. package/skills/building-ai-agents/SKILL.md +414 -0
  13. package/skills/building-ai-agents/ai-adapters.md +257 -0
  14. package/skills/building-ai-agents/ai-agents-streaming.md +307 -0
  15. package/skills/building-ai-agents/ai-communication.md +304 -0
  16. package/skills/building-ai-agents/api-skeleton.md +5 -0
  17. package/skills/building-ai-agents/examples.md +849 -0
  18. package/skills/building-ai-orchestrators/SKILL.md +401 -0
  19. package/skills/building-ai-orchestrators/ai-multi-agent.md +375 -0
  20. package/skills/building-ai-orchestrators/ai-orchestrator.md +466 -0
  21. package/skills/building-ai-orchestrators/ai-tasks.md +268 -0
  22. package/skills/building-ai-orchestrators/api-skeleton.md +5 -0
  23. package/skills/building-ai-orchestrators/examples.md +1609 -0
  24. package/skills/building-directive-systems/SKILL.md +499 -0
  25. package/skills/building-directive-systems/api-skeleton.md +5 -0
  26. package/skills/building-directive-systems/examples.md +4149 -0
  27. package/skills/building-directive-systems/multi-module.md +327 -0
  28. package/skills/building-directive-systems/plugins.md +361 -0
  29. package/skills/building-directive-systems/react-adapter.md +376 -0
  30. package/skills/building-directive-systems/system-api.md +463 -0
  31. package/skills/getting-started-with-directive/SKILL.md +198 -0
  32. package/skills/getting-started-with-directive/api-skeleton.md +5 -0
  33. package/skills/getting-started-with-directive/core-patterns.md +240 -0
  34. package/skills/getting-started-with-directive/examples.md +87 -0
  35. package/skills/getting-started-with-directive/sitemap.md +202 -0
  36. package/skills/hardening-ai-systems/SKILL.md +377 -0
  37. package/skills/hardening-ai-systems/ai-budget-resilience.md +408 -0
  38. package/skills/hardening-ai-systems/ai-guardrails-memory.md +370 -0
  39. package/skills/hardening-ai-systems/ai-security.md +400 -0
  40. package/skills/hardening-ai-systems/api-skeleton.md +5 -0
  41. package/skills/hardening-ai-systems/examples.md +628 -0
  42. package/skills/migrating-to-directive/SKILL.md +249 -0
  43. package/skills/migrating-to-directive/anti-patterns.md +382 -0
  44. package/skills/migrating-to-directive/api-skeleton.md +5 -0
  45. package/skills/migrating-to-directive/core-patterns.md +240 -0
  46. package/skills/migrating-to-directive/examples.md +511 -0
  47. package/skills/migrating-to-directive/schema-types.md +273 -0
  48. package/skills/reviewing-directive-code/SKILL.md +232 -0
  49. package/skills/reviewing-directive-code/anti-patterns.md +382 -0
  50. package/skills/reviewing-directive-code/api-skeleton.md +5 -0
  51. package/skills/reviewing-directive-code/core-patterns.md +240 -0
  52. package/skills/reviewing-directive-code/examples.md +463 -0
  53. package/skills/reviewing-directive-code/naming.md +321 -0
  54. package/skills/scaffolding-directive-modules/SKILL.md +218 -0
  55. package/skills/scaffolding-directive-modules/api-skeleton.md +5 -0
  56. package/skills/scaffolding-directive-modules/core-patterns.md +240 -0
  57. package/skills/scaffolding-directive-modules/examples.md +1386 -0
  58. package/skills/scaffolding-directive-modules/naming.md +321 -0
  59. package/skills/scaffolding-directive-modules/schema-types.md +273 -0
  60. package/skills/testing-ai-systems/SKILL.md +398 -0
  61. package/skills/testing-ai-systems/ai-debug-observability.md +329 -0
  62. package/skills/testing-ai-systems/ai-mcp-rag.md +293 -0
  63. package/skills/testing-ai-systems/ai-testing-evals.md +485 -0
  64. package/skills/testing-ai-systems/api-skeleton.md +5 -0
  65. package/skills/testing-ai-systems/examples.md +1261 -0
  66. package/skills/testing-directive-code/SKILL.md +479 -0
  67. package/skills/testing-directive-code/api-skeleton.md +5 -0
  68. package/skills/testing-directive-code/examples.md +156 -0
  69. package/skills/testing-directive-code/history.md +351 -0
  70. package/skills/testing-directive-code/testing.md +412 -0
  71. package/skills/writing-directive-constraints/SKILL.md +486 -0
  72. package/skills/writing-directive-constraints/api-skeleton.md +5 -0
  73. package/skills/writing-directive-constraints/constraints.md +274 -0
  74. package/skills/writing-directive-constraints/error-boundaries.md +330 -0
  75. package/skills/writing-directive-constraints/examples.md +1361 -0
  76. package/skills/writing-directive-constraints/resolvers.md +367 -0
  77. package/skills/writing-directive-modules/SKILL.md +342 -0
  78. package/skills/writing-directive-modules/anti-patterns.md +382 -0
  79. package/skills/writing-directive-modules/api-skeleton.md +5 -0
  80. package/skills/writing-directive-modules/core-patterns.md +240 -0
  81. package/skills/writing-directive-modules/examples.md +1639 -0
  82. package/skills/writing-directive-modules/naming.md +321 -0
  83. package/skills/writing-directive-modules/schema-types.md +273 -0
@@ -0,0 +1,408 @@
1
+ # AI budget + resilience
2
+
3
+ > Covers `@directive-run/ai` — `withBudget`, `withRetry`, `withFallback`, `createCircuitBreaker`, `createHealthMonitor`, `createSemanticCache`, `createConstraintRouter`.
4
+
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
8
+
9
+ ```
10
+ What failure mode are you guarding against?
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 })
18
+ ```
19
+
20
+ ## `withBudget(runner, config)` — cost caps
21
+
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.
23
+
24
+ ```typescript
25
+ import { withBudget } from "@directive-run/ai";
26
+
27
+ const pricing = { inputPerMillion: 3, outputPerMillion: 15 };
28
+
29
+ const budgetRunner = withBudget(baseRunner, {
30
+ maxCostPerCall: 0.10,
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
+ budgets: [
35
+ { window: "hour", maxCost: 5.00, pricing },
36
+ { window: "day", maxCost: 50.00, pricing },
37
+ ],
38
+ onBudgetExceeded: ({ estimated, remaining, window }) => {
39
+ console.warn(`[budget] ${window} would overshoot — est $${estimated.toFixed(4)}, remaining $${remaining.toFixed(4)}`);
40
+ },
41
+ });
42
+ ```
43
+
44
+ Catching the throw:
45
+
46
+ ```typescript
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
+ }
56
+ ```
57
+
58
+ ## `withRetry(runner, config)` — transient-error retry
59
+
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.
61
+
62
+ ```typescript
63
+ import { withRetry } from "@directive-run/ai";
64
+
65
+ const retryRunner = withRetry(baseRunner, {
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}`);
75
+ },
76
+ });
77
+ ```
78
+
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
94
+
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.
96
+
97
+ ```typescript
98
+ import { withFallback, withRetry } from "@directive-run/ai";
99
+ import { createAnthropicRunner } from "@directive-run/ai/anthropic";
100
+ import { createOpenAIRunner } from "@directive-run/ai/openai";
101
+ import { createOllamaRunner } from "@directive-run/ai/ollama";
102
+
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
+ ```
117
+
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
+ }
130
+ ```
131
+
132
+ ## `createCircuitBreaker(config)` — failure isolation
133
+
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.
135
+
136
+ ```typescript
137
+ import { createCircuitBreaker, CircuitBreakerOpenError } from "@directive-run/core";
138
+
139
+ const breaker = createCircuitBreaker({
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
145
+ });
146
+
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?
156
+
157
+ breaker.reset(); // force back to CLOSED, clear stats
158
+ breaker.forceState("OPEN"); // testing helper
159
+ ```
160
+
161
+ Catching the OPEN throw:
162
+
163
+ ```typescript
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
+ ```
174
+
175
+ ### Don't share a breaker across unrelated runners
176
+
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" });
186
+ ```
187
+
188
+ ## `createHealthMonitor(config?)` — fleet health metrics
189
+
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.
191
+
192
+ ```typescript
193
+ import { createHealthMonitor } from "@directive-run/ai";
194
+
195
+ const monitor = createHealthMonitor({
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,
202
+ },
203
+ });
204
+
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();
223
+ ```
224
+
225
+ ## `createSemanticCache(config)` — embedding-based response cache
226
+
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.
228
+
229
+ ```typescript
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
+ };
238
+
239
+ const cache = createSemanticCache({
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)}…`),
246
+ });
247
+
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
+ });
256
+ ```
257
+
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
261
+
262
+ Routes each runner call to a provider based on runtime context.
263
+
264
+ ```typescript
265
+ import { createConstraintRouter } from "@directive-run/ai";
266
+
267
+ const router = createConstraintRouter({
268
+ providers: [
269
+ { name: "ollama", runner: ollamaRunner, costPerMillion: 0 },
270
+ { name: "anthropic", runner: anthropicRunner, costPerMillion: 3 },
271
+ { name: "openai", runner: openaiRunner, costPerMillion: 5 },
272
+ ],
273
+ constraints: [
274
+ { when: (ctx) => ctx.budgetRemaining < 1.0, prefer: "ollama" },
275
+ { when: (ctx) => ctx.priority === "high", prefer: "anthropic" },
276
+ ],
277
+ });
278
+ ```
279
+
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.
281
+
282
+ ## Composing wrappers
283
+
284
+ Wrappers are pure functions — compose them inside-out (innermost runs first per call).
285
+
286
+ ```typescript
287
+ import { withBudget, withRetry, withFallback } from "@directive-run/ai";
288
+
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
+ ],
301
+ );
302
+ ```
303
+
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
393
+
394
+ | Utility | Shape | Throws |
395
+ |---|---|---|
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