@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,849 @@
1
+ # Examples
2
+
3
+ > Auto-generated from extracted examples. Do not edit manually.
4
+
5
+ ## ai-checkpoint
6
+
7
+ ```typescript
8
+ // Example: ai-checkpoint
9
+ // Source: examples/ai-checkpoint/src/module.ts
10
+ // Pure module file — no DOM wiring
11
+
12
+ /**
13
+ * AI Pipeline Checkpoint — Module Definition
14
+ *
15
+ * 4-stage pipeline (extract → summarize → classify → archive) with checkpoint
16
+ * at every stage. Save/restore/delete checkpoints. Retry with backoff on failures.
17
+ */
18
+
19
+ import {
20
+ type Checkpoint,
21
+ InMemoryCheckpointStore,
22
+ createCheckpointId,
23
+ validateCheckpoint,
24
+ } from "@directive-run/ai";
25
+ import {
26
+ type ModuleSchema,
27
+ createModule,
28
+ createSystem,
29
+ t,
30
+ } from "@directive-run/core";
31
+ import { devtoolsPlugin } from "@directive-run/core/plugins";
32
+
33
+ // ============================================================================
34
+ // Types
35
+ // ============================================================================
36
+
37
+ export type PipelineStage =
38
+ | "idle"
39
+ | "extract"
40
+ | "summarize"
41
+ | "classify"
42
+ | "archive"
43
+ | "done"
44
+ | "error";
45
+
46
+ export interface StageResult {
47
+ stage: string;
48
+ output: string;
49
+ tokens: number;
50
+ durationMs: number;
51
+ }
52
+
53
+ export interface CheckpointEntry {
54
+ id: string;
55
+ label: string;
56
+ createdAt: string;
57
+ stage: PipelineStage;
58
+ }
59
+
60
+ export interface TimelineEntry {
61
+ time: number;
62
+ event: string;
63
+ detail: string;
64
+ type: "stage" | "checkpoint" | "retry" | "error" | "info" | "success";
65
+ }
66
+
67
+ // ============================================================================
68
+ // Constants
69
+ // ============================================================================
70
+
71
+ export const STAGES: PipelineStage[] = [
72
+ "extract",
73
+ "summarize",
74
+ "classify",
75
+ "archive",
76
+ ];
77
+
78
+ const STAGE_CONFIG = {
79
+ extract: {
80
+ tokens: 150,
81
+ baseLatency: 300,
82
+ output: "Extracted 3 sections, 2 tables, 5 figures from document.",
83
+ },
84
+ summarize: {
85
+ tokens: 200,
86
+ baseLatency: 400,
87
+ output:
88
+ "Summary: Key findings include efficiency gains of 23% and cost reduction of $1.2M annually.",
89
+ },
90
+ classify: {
91
+ tokens: 80,
92
+ baseLatency: 200,
93
+ output:
94
+ "Classification: category=research, confidence=0.94, tags=[efficiency, cost, annual-review]",
95
+ },
96
+ archive: {
97
+ tokens: 50,
98
+ baseLatency: 150,
99
+ output: "Archived to /documents/2026/research/efficiency-report.json",
100
+ },
101
+ };
102
+
103
+ // ============================================================================
104
+ // Timeline
105
+ // ============================================================================
106
+
107
+ export const timeline: TimelineEntry[] = [];
108
+
109
+ export function addTimeline(
110
+ event: string,
111
+ detail: string,
112
+ type: TimelineEntry["type"],
113
+ ) {
114
+ timeline.unshift({ time: Date.now(), event, detail, type });
115
+ if (timeline.length > 50) {
116
+ timeline.length = 50;
117
+ }
118
+ }
119
+
120
+ // ============================================================================
121
+ // Checkpoint Store
122
+ // ============================================================================
123
+
124
+ export const checkpointStore = new InMemoryCheckpointStore({
125
+ maxCheckpoints: 20,
126
+ });
127
+
128
+ // ============================================================================
129
+ // Schema
130
+ // ============================================================================
131
+
132
+ export const schema = {
133
+ facts: {
134
+ currentStage: t.string<PipelineStage>(),
135
+ stageResults: t.array<StageResult>(),
136
+ totalTokens: t.number(),
137
+ retryCount: t.number(),
138
+ maxRetries: t.number(),
139
+ failStage: t.string(),
140
+ isRunning: t.boolean(),
141
+ lastError: t.string(),
142
+ checkpoints: t.array<CheckpointEntry>(),
143
+ selectedCheckpoint: t.string(),
144
+ },
145
+ derivations: {
146
+ completionPercentage: t.number(),
147
+ currentStageIndex: t.number(),
148
+ canAdvance: t.boolean(),
149
+ isPipelineDone: t.boolean(),
150
+ stageCount: t.number(),
151
+ },
152
+ events: {
153
+ setFailStage: { value: t.string() },
154
+ setMaxRetries: { value: t.number() },
155
+ selectCheckpoint: { id: t.string() },
156
+ reset: {},
157
+ },
158
+ requirements: {},
159
+ } satisfies ModuleSchema;
160
+
161
+ // ============================================================================
162
+ // Module
163
+ // ============================================================================
164
+
165
+ const pipelineModule = createModule("pipeline", {
166
+ schema,
167
+
168
+ init: (facts) => {
169
+ facts.currentStage = "idle";
170
+ facts.stageResults = [];
171
+ facts.totalTokens = 0;
172
+ facts.retryCount = 0;
173
+ facts.maxRetries = 2;
174
+ facts.failStage = "";
175
+ facts.isRunning = false;
176
+ facts.lastError = "";
177
+ facts.checkpoints = [];
178
+ facts.selectedCheckpoint = "";
179
+ },
180
+
181
+ derive: {
182
+ completionPercentage: (facts) => {
183
+ if (facts.currentStage === "idle") {
184
+ return 0;
185
+ }
186
+ if (facts.currentStage === "done") {
187
+ return 100;
188
+ }
189
+ if (facts.currentStage === "error") {
190
+ const idx = facts.stageResults.length;
191
+
192
+ return Math.round((idx / STAGES.length) * 100);
193
+ }
194
+ const idx = STAGES.indexOf(facts.currentStage);
195
+
196
+ return Math.round((idx / STAGES.length) * 100);
197
+ },
198
+ currentStageIndex: (facts) => {
199
+ if (facts.currentStage === "idle") {
200
+ return -1;
201
+ }
202
+ if (facts.currentStage === "done") {
203
+ return STAGES.length;
204
+ }
205
+
206
+ return STAGES.indexOf(facts.currentStage);
207
+ },
208
+ canAdvance: (facts) => {
209
+ return (
210
+ !facts.isRunning &&
211
+ facts.currentStage !== "done" &&
212
+ facts.currentStage !== "error"
213
+ );
214
+ },
215
+ isPipelineDone: (facts) => facts.currentStage === "done",
216
+ stageCount: () => STAGES.length,
217
+ },
218
+
219
+ events: {
220
+ setFailStage: (facts, { value }) => {
221
+ facts.failStage = value;
222
+ },
223
+ setMaxRetries: (facts, { value }) => {
224
+ facts.maxRetries = value;
225
+ },
226
+ selectCheckpoint: (facts, { id }) => {
227
+ facts.selectedCheckpoint = id;
228
+ },
229
+ reset: (facts) => {
230
+ facts.currentStage = "idle";
231
+ facts.stageResults = [];
232
+ facts.totalTokens = 0;
233
+ facts.retryCount = 0;
234
+ facts.isRunning = false;
235
+ facts.lastError = "";
236
+ facts.selectedCheckpoint = "";
237
+ timeline.length = 0;
238
+ },
239
+ },
240
+ });
241
+
242
+ // ============================================================================
243
+ // System
244
+ // ============================================================================
245
+
246
+ export const system = createSystem({
247
+ module: pipelineModule,
248
+ trace: true,
249
+ plugins: [devtoolsPlugin({ name: "ai-checkpoint" })],
250
+ });
251
+
252
+ // ============================================================================
253
+ // Pipeline Logic
254
+ // ============================================================================
255
+
256
+ async function runStage(stage: PipelineStage): Promise<StageResult> {
257
+ const config = STAGE_CONFIG[stage as keyof typeof STAGE_CONFIG];
258
+ if (!config) {
259
+ throw new Error(`Unknown stage: ${stage}`);
260
+ }
261
+
262
+ // Simulate latency
263
+ const latency = config.baseLatency + Math.random() * 100;
264
+ await new Promise((resolve) => setTimeout(resolve, latency));
265
+
266
+ // Check for injected failure
267
+ if (system.facts.failStage === stage) {
268
+ throw new Error(`${stage}: simulated failure`);
269
+ }
270
+
271
+ return {
272
+ stage,
273
+ output: config.output,
274
+ tokens: config.tokens + Math.floor(Math.random() * 30),
275
+ durationMs: Math.round(latency),
276
+ };
277
+ }
278
+
279
+ export async function runStageWithRetry(
280
+ stage: PipelineStage,
281
+ renderCallback: () => void,
282
+ ): Promise<StageResult> {
283
+ const maxRetries = system.facts.maxRetries;
284
+ let lastError: Error | null = null;
285
+
286
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
287
+ try {
288
+ if (attempt > 0) {
289
+ const delay = Math.min(500 * 2 ** (attempt - 1), 4000);
290
+ const jitter = Math.random() * delay * 0.1;
291
+ system.facts.retryCount = system.facts.retryCount + 1;
292
+ addTimeline(
293
+ "retry",
294
+ `${stage}: attempt ${attempt + 1}/${maxRetries + 1} (delay ${Math.round(delay)}ms)`,
295
+ "retry",
296
+ );
297
+ renderCallback();
298
+ await new Promise((resolve) => setTimeout(resolve, delay + jitter));
299
+ }
300
+
301
+ return await runStage(stage);
302
+ } catch (err) {
303
+ lastError = err instanceof Error ? err : new Error(String(err));
304
+ addTimeline("error", `${stage}: ${lastError.message}`, "error");
305
+ }
306
+ }
307
+
308
+ throw lastError!;
309
+ }
310
+
311
+ export async function advancePipeline(
312
+ renderCallback: () => void,
313
+ ): Promise<void> {
314
+ if (system.facts.isRunning) {
315
+ return;
316
+ }
317
+
318
+ const current = system.facts.currentStage;
319
+ let nextStage: PipelineStage;
320
+
321
+ if (current === "idle") {
322
+ nextStage = STAGES[0]!;
323
+ } else if (current === "done" || current === "error") {
324
+ return;
325
+ } else {
326
+ const idx = STAGES.indexOf(current);
327
+ if (idx < 0 || idx >= STAGES.length - 1) {
328
+ // Current stage should be run first
329
+ nextStage = current;
330
+ } else {
331
+ nextStage = STAGES[idx + 1]!;
332
+ }
333
+ }
334
+
335
+ system.facts.isRunning = true;
336
+ system.facts.currentStage = nextStage;
337
+ addTimeline("stage", `${nextStage}: starting`, "stage");
338
+ renderCallback();
339
+
340
+ try {
341
+ const result = await runStageWithRetry(nextStage, renderCallback);
342
+ const results = [...system.facts.stageResults, result];
343
+ system.facts.stageResults = results;
344
+ system.facts.totalTokens = system.facts.totalTokens + result.tokens;
345
+ addTimeline(
346
+ "success",
347
+ `${nextStage}: complete (${result.tokens} tokens)`,
348
+ "success",
349
+ );
350
+
351
+ const idx = STAGES.indexOf(nextStage);
352
+ if (idx >= STAGES.length - 1) {
353
+ system.facts.currentStage = "done";
354
+ addTimeline("info", "pipeline complete", "info");
355
+ } else {
356
+ system.facts.currentStage = nextStage;
357
+ }
358
+ } catch (err) {
359
+ system.facts.currentStage = "error";
360
+ system.facts.lastError = err instanceof Error ? err.message : String(err);
361
+ addTimeline("error", `pipeline halted: ${system.facts.lastError}`, "error");
362
+ } finally {
363
+ system.facts.isRunning = false;
364
+ }
365
+ }
366
+
367
+ export async function autoRun(renderCallback: () => void): Promise<void> {
368
+ if (system.facts.isRunning) {
369
+ return;
370
+ }
371
+
372
+ system.facts.currentStage = "idle";
373
+ system.facts.stageResults = [];
374
+ system.facts.totalTokens = 0;
375
+ system.facts.retryCount = 0;
376
+ system.facts.lastError = "";
377
+ addTimeline("info", "auto-run started", "info");
378
+
379
+ for (const stage of STAGES) {
380
+ system.facts.isRunning = true;
381
+ system.facts.currentStage = stage;
382
+ addTimeline("stage", `${stage}: starting`, "stage");
383
+ renderCallback();
384
+
385
+ try {
386
+ const result = await runStageWithRetry(stage, renderCallback);
387
+ const results = [...system.facts.stageResults, result];
388
+ system.facts.stageResults = results;
389
+ system.facts.totalTokens = system.facts.totalTokens + result.tokens;
390
+ addTimeline(
391
+ "success",
392
+ `${stage}: complete (${result.tokens} tokens)`,
393
+ "success",
394
+ );
395
+ } catch (err) {
396
+ system.facts.currentStage = "error";
397
+ system.facts.lastError = err instanceof Error ? err.message : String(err);
398
+ system.facts.isRunning = false;
399
+ addTimeline(
400
+ "error",
401
+ `pipeline halted at ${stage}: ${system.facts.lastError}`,
402
+ "error",
403
+ );
404
+
405
+ return;
406
+ }
407
+
408
+ system.facts.isRunning = false;
409
+ }
410
+
411
+ system.facts.currentStage = "done";
412
+ addTimeline("info", "pipeline complete (auto-run)", "info");
413
+ }
414
+
415
+ // ============================================================================
416
+ // Checkpoint Logic
417
+ // ============================================================================
418
+
419
+ export async function saveCheckpoint(): Promise<void> {
420
+ const stage = system.facts.currentStage;
421
+ const id = createCheckpointId();
422
+ const label = `Stage: ${stage} (${new Date().toLocaleTimeString()})`;
423
+
424
+ const checkpoint: Checkpoint = {
425
+ version: 1,
426
+ id,
427
+ createdAt: new Date().toISOString(),
428
+ label,
429
+ systemExport: JSON.stringify({
430
+ currentStage: system.facts.currentStage,
431
+ stageResults: system.facts.stageResults,
432
+ totalTokens: system.facts.totalTokens,
433
+ retryCount: system.facts.retryCount,
434
+ lastError: system.facts.lastError,
435
+ }),
436
+ timelineExport: JSON.stringify(timeline.slice(0, 20)),
437
+ localState: { type: "single" },
438
+ memoryExport: null,
439
+ orchestratorType: "single",
440
+ };
441
+
442
+ await checkpointStore.save(checkpoint);
443
+
444
+ const entry: CheckpointEntry = {
445
+ id,
446
+ label,
447
+ createdAt: checkpoint.createdAt,
448
+ stage,
449
+ };
450
+ system.facts.checkpoints = [...system.facts.checkpoints, entry];
451
+
452
+ addTimeline("checkpoint", `saved: ${label}`, "checkpoint");
453
+ }
454
+
455
+ export async function restoreCheckpoint(checkpointId: string): Promise<void> {
456
+ const checkpoint = await checkpointStore.load(checkpointId);
457
+ if (!checkpoint) {
458
+ addTimeline("error", "checkpoint not found", "error");
459
+
460
+ return;
461
+ }
462
+
463
+ if (!validateCheckpoint(checkpoint)) {
464
+ addTimeline("error", "invalid checkpoint data", "error");
465
+
466
+ return;
467
+ }
468
+
469
+ const saved = JSON.parse(checkpoint.systemExport);
470
+ system.facts.currentStage = saved.currentStage;
471
+ system.facts.stageResults = saved.stageResults;
472
+ system.facts.totalTokens = saved.totalTokens;
473
+ system.facts.retryCount = saved.retryCount;
474
+ system.facts.lastError = saved.lastError;
475
+ system.facts.isRunning = false;
476
+
477
+ if (checkpoint.timelineExport) {
478
+ const savedTimeline = JSON.parse(checkpoint.timelineExport);
479
+ timeline.length = 0;
480
+ for (const entry of savedTimeline) {
481
+ timeline.push(entry);
482
+ }
483
+ }
484
+
485
+ addTimeline("checkpoint", `restored: ${checkpoint.label}`, "checkpoint");
486
+ }
487
+
488
+ export async function deleteCheckpoint(checkpointId: string): Promise<void> {
489
+ const deleted = await checkpointStore.delete(checkpointId);
490
+ if (deleted) {
491
+ const checkpoints = system.facts.checkpoints.filter(
492
+ (c) => c.id !== checkpointId,
493
+ );
494
+ system.facts.checkpoints = checkpoints;
495
+ addTimeline("checkpoint", "deleted checkpoint", "checkpoint");
496
+ }
497
+ }
498
+ ```
499
+
500
+ ## provider-routing
501
+
502
+ ```typescript
503
+ // Example: provider-routing
504
+ // Source: examples/provider-routing/src/module.ts
505
+ // Pure module file — no DOM wiring
506
+
507
+ /**
508
+ * Smart Provider Router — Module Definition
509
+ *
510
+ * 3 mock providers (OpenAI, Anthropic, Ollama). Constraint router selects based
511
+ * on cost, error rates, circuit state. Provider fallback chain.
512
+ */
513
+
514
+ import {
515
+ type ModuleSchema,
516
+ createModule,
517
+ createSystem,
518
+ t,
519
+ } from "@directive-run/core";
520
+ import {
521
+ type CircuitState,
522
+ createCircuitBreaker,
523
+ devtoolsPlugin,
524
+ } from "@directive-run/core/plugins";
525
+
526
+ // ============================================================================
527
+ // Types
528
+ // ============================================================================
529
+
530
+ export interface ProviderStats {
531
+ name: string;
532
+ callCount: number;
533
+ errorCount: number;
534
+ totalCost: number;
535
+ avgLatencyMs: number;
536
+ circuitState: CircuitState;
537
+ }
538
+
539
+ export interface TimelineEntry {
540
+ time: number;
541
+ event: string;
542
+ detail: string;
543
+ type: "route" | "error" | "fallback" | "circuit" | "info" | "success";
544
+ }
545
+
546
+ // ============================================================================
547
+ // Mock Providers
548
+ // ============================================================================
549
+
550
+ const PROVIDERS = {
551
+ openai: { name: "OpenAI", costPer1k: 0.03, baseLatency: 200 },
552
+ anthropic: { name: "Anthropic", costPer1k: 0.025, baseLatency: 250 },
553
+ ollama: { name: "Ollama", costPer1k: 0.001, baseLatency: 400 },
554
+ };
555
+
556
+ const providerErrors: Record<string, boolean> = {
557
+ openai: false,
558
+ anthropic: false,
559
+ ollama: false,
560
+ };
561
+
562
+ export const circuitBreakers = {
563
+ openai: createCircuitBreaker({
564
+ name: "openai",
565
+ failureThreshold: 3,
566
+ recoveryTimeMs: 5000,
567
+ halfOpenMaxRequests: 2,
568
+ onStateChange: (from, to) =>
569
+ addTimeline("circuit", `openai: ${from} → ${to}`, "circuit"),
570
+ }),
571
+ anthropic: createCircuitBreaker({
572
+ name: "anthropic",
573
+ failureThreshold: 3,
574
+ recoveryTimeMs: 5000,
575
+ halfOpenMaxRequests: 2,
576
+ onStateChange: (from, to) =>
577
+ addTimeline("circuit", `anthropic: ${from} → ${to}`, "circuit"),
578
+ }),
579
+ ollama: createCircuitBreaker({
580
+ name: "ollama",
581
+ failureThreshold: 3,
582
+ recoveryTimeMs: 5000,
583
+ halfOpenMaxRequests: 2,
584
+ onStateChange: (from, to) =>
585
+ addTimeline("circuit", `ollama: ${from} → ${to}`, "circuit"),
586
+ }),
587
+ };
588
+
589
+ // ============================================================================
590
+ // Timeline
591
+ // ============================================================================
592
+
593
+ export const timeline: TimelineEntry[] = [];
594
+
595
+ export function addTimeline(
596
+ event: string,
597
+ detail: string,
598
+ type: TimelineEntry["type"],
599
+ ) {
600
+ timeline.unshift({ time: Date.now(), event, detail, type });
601
+ if (timeline.length > 50) {
602
+ timeline.length = 50;
603
+ }
604
+ }
605
+
606
+ // ============================================================================
607
+ // Schema
608
+ // ============================================================================
609
+
610
+ export const schema = {
611
+ facts: {
612
+ openaiStats: t.object<ProviderStats>(),
613
+ anthropicStats: t.object<ProviderStats>(),
614
+ ollamaStats: t.object<ProviderStats>(),
615
+ budgetRemaining: t.number(),
616
+ budgetTotal: t.number(),
617
+ preferCheapest: t.boolean(),
618
+ lastProvider: t.string(),
619
+ totalRequests: t.number(),
620
+ lastError: t.string(),
621
+ },
622
+ derivations: {
623
+ openaiCircuit: t.string<CircuitState>(),
624
+ anthropicCircuit: t.string<CircuitState>(),
625
+ ollamaCircuit: t.string<CircuitState>(),
626
+ cheapestAvailable: t.string(),
627
+ allDown: t.boolean(),
628
+ },
629
+ events: {
630
+ toggleProviderError: { provider: t.string() },
631
+ setBudget: { value: t.number() },
632
+ togglePreferCheapest: {},
633
+ resetStats: {},
634
+ },
635
+ requirements: {},
636
+ } satisfies ModuleSchema;
637
+
638
+ // ============================================================================
639
+ // Module
640
+ // ============================================================================
641
+
642
+ function defaultStats(name: string): ProviderStats {
643
+ return {
644
+ name,
645
+ callCount: 0,
646
+ errorCount: 0,
647
+ totalCost: 0,
648
+ avgLatencyMs: 0,
649
+ circuitState: "CLOSED",
650
+ };
651
+ }
652
+
653
+ const routerModule = createModule("router", {
654
+ schema,
655
+
656
+ init: (facts) => {
657
+ facts.openaiStats = defaultStats("OpenAI");
658
+ facts.anthropicStats = defaultStats("Anthropic");
659
+ facts.ollamaStats = defaultStats("Ollama");
660
+ facts.budgetRemaining = 1.0;
661
+ facts.budgetTotal = 1.0;
662
+ facts.preferCheapest = false;
663
+ facts.lastProvider = "";
664
+ facts.totalRequests = 0;
665
+ facts.lastError = "";
666
+ },
667
+
668
+ derive: {
669
+ openaiCircuit: () => circuitBreakers.openai.getState(),
670
+ anthropicCircuit: () => circuitBreakers.anthropic.getState(),
671
+ ollamaCircuit: () => circuitBreakers.ollama.getState(),
672
+ cheapestAvailable: () => {
673
+ const available: { name: string; cost: number }[] = [];
674
+ for (const [id, config] of Object.entries(PROVIDERS)) {
675
+ const breaker = circuitBreakers[id as keyof typeof circuitBreakers];
676
+ if (breaker.isAllowed()) {
677
+ available.push({ name: id, cost: config.costPer1k });
678
+ }
679
+ }
680
+ available.sort((a, b) => a.cost - b.cost);
681
+
682
+ return available.length > 0 ? available[0]!.name : "none";
683
+ },
684
+ allDown: () =>
685
+ !circuitBreakers.openai.isAllowed() &&
686
+ !circuitBreakers.anthropic.isAllowed() &&
687
+ !circuitBreakers.ollama.isAllowed(),
688
+ },
689
+
690
+ events: {
691
+ toggleProviderError: (_facts, { provider }) => {
692
+ providerErrors[provider] = !providerErrors[provider];
693
+ },
694
+ setBudget: (facts, { value }) => {
695
+ facts.budgetRemaining = value;
696
+ facts.budgetTotal = value;
697
+ },
698
+ togglePreferCheapest: (facts) => {
699
+ facts.preferCheapest = !facts.preferCheapest;
700
+ },
701
+ resetStats: (facts) => {
702
+ facts.openaiStats = defaultStats("OpenAI");
703
+ facts.anthropicStats = defaultStats("Anthropic");
704
+ facts.ollamaStats = defaultStats("Ollama");
705
+ facts.budgetRemaining = facts.budgetTotal;
706
+ facts.lastProvider = "";
707
+ facts.totalRequests = 0;
708
+ facts.lastError = "";
709
+ providerErrors.openai = false;
710
+ providerErrors.anthropic = false;
711
+ providerErrors.ollama = false;
712
+ circuitBreakers.openai.reset();
713
+ circuitBreakers.anthropic.reset();
714
+ circuitBreakers.ollama.reset();
715
+ timeline.length = 0;
716
+ },
717
+ },
718
+ });
719
+
720
+ // ============================================================================
721
+ // System
722
+ // ============================================================================
723
+
724
+ export const system = createSystem({
725
+ module: routerModule,
726
+ trace: true,
727
+ plugins: [devtoolsPlugin({ name: "provider-routing" })],
728
+ });
729
+
730
+ // ============================================================================
731
+ // Routing Logic
732
+ // ============================================================================
733
+
734
+ function selectProvider(): string | null {
735
+ const budget = system.facts.budgetRemaining;
736
+ const preferCheapest = system.facts.preferCheapest;
737
+
738
+ // Collect available providers (circuit breaker allows + within budget)
739
+ const available: { id: string; cost: number }[] = [];
740
+ for (const [id, config] of Object.entries(PROVIDERS)) {
741
+ const breaker = circuitBreakers[id as keyof typeof circuitBreakers];
742
+ if (breaker.isAllowed() && budget >= config.costPer1k) {
743
+ available.push({ id, cost: config.costPer1k });
744
+ }
745
+ }
746
+
747
+ if (available.length === 0) {
748
+ return null;
749
+ }
750
+
751
+ if (preferCheapest) {
752
+ available.sort((a, b) => a.cost - b.cost);
753
+
754
+ return available[0]!.id;
755
+ }
756
+
757
+ // Default: prefer openai > anthropic > ollama
758
+ const priority = ["openai", "anthropic", "ollama"];
759
+ for (const id of priority) {
760
+ if (available.find((a) => a.id === id)) {
761
+ return id;
762
+ }
763
+ }
764
+
765
+ return available[0]!.id;
766
+ }
767
+
768
+ async function executeProvider(providerId: string): Promise<boolean> {
769
+ const breaker = circuitBreakers[providerId as keyof typeof circuitBreakers];
770
+ const config = PROVIDERS[providerId as keyof typeof PROVIDERS]!;
771
+ const statsKey = `${providerId}Stats` as
772
+ | "openaiStats"
773
+ | "anthropicStats"
774
+ | "ollamaStats";
775
+
776
+ try {
777
+ await breaker.execute(async () => {
778
+ await new Promise((resolve) =>
779
+ setTimeout(resolve, config.baseLatency + Math.random() * 100),
780
+ );
781
+
782
+ if (providerErrors[providerId]) {
783
+ throw new Error(`${config.name}: simulated error`);
784
+ }
785
+ });
786
+
787
+ const stats = system.facts[statsKey];
788
+ const cost = config.costPer1k;
789
+ const latency = config.baseLatency + Math.random() * 100;
790
+ system.facts[statsKey] = {
791
+ ...stats,
792
+ callCount: stats.callCount + 1,
793
+ totalCost: Math.round((stats.totalCost + cost) * 1000) / 1000,
794
+ avgLatencyMs: Math.round(
795
+ (stats.avgLatencyMs * stats.callCount + latency) /
796
+ (stats.callCount + 1),
797
+ ),
798
+ circuitState: breaker.getState(),
799
+ };
800
+ system.facts.budgetRemaining =
801
+ Math.round((system.facts.budgetRemaining - cost) * 1000) / 1000;
802
+ system.facts.lastError = "";
803
+ addTimeline("success", `${config.name}: ok ($${cost})`, "success");
804
+
805
+ return true;
806
+ } catch (err) {
807
+ const stats = system.facts[statsKey];
808
+ system.facts[statsKey] = {
809
+ ...stats,
810
+ errorCount: stats.errorCount + 1,
811
+ circuitState: breaker.getState(),
812
+ };
813
+ system.facts.lastError = err instanceof Error ? err.message : String(err);
814
+ addTimeline("error", `${config.name}: ${system.facts.lastError}`, "error");
815
+
816
+ return false;
817
+ }
818
+ }
819
+
820
+ export async function sendRequest(): Promise<void> {
821
+ system.facts.totalRequests = system.facts.totalRequests + 1;
822
+
823
+ const providerId = selectProvider();
824
+ if (!providerId) {
825
+ system.facts.lastError = "All providers unavailable or over budget";
826
+ addTimeline("error", "no available providers", "error");
827
+
828
+ return;
829
+ }
830
+
831
+ system.facts.lastProvider = providerId;
832
+ addTimeline("route", `→ ${providerId}`, "route");
833
+
834
+ const success = await executeProvider(providerId);
835
+ if (success) {
836
+ return;
837
+ }
838
+
839
+ // Primary failed — try fallback
840
+ const fallbackId = selectProvider();
841
+ if (fallbackId && fallbackId !== providerId) {
842
+ addTimeline("fallback", `falling back to ${fallbackId}`, "fallback");
843
+ system.facts.lastProvider = fallbackId;
844
+ await executeProvider(fallbackId);
845
+ }
846
+ }
847
+
848
+ export { providerErrors };
849
+ ```