confused-ai-core 0.1.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 (114) hide show
  1. package/FEATURES.md +169 -0
  2. package/package.json +119 -0
  3. package/src/agent.ts +187 -0
  4. package/src/agentic/index.ts +87 -0
  5. package/src/agentic/runner.ts +386 -0
  6. package/src/agentic/types.ts +91 -0
  7. package/src/artifacts/artifact.ts +417 -0
  8. package/src/artifacts/index.ts +42 -0
  9. package/src/artifacts/media.ts +304 -0
  10. package/src/cli/index.ts +122 -0
  11. package/src/core/base-agent.ts +151 -0
  12. package/src/core/context-builder.ts +106 -0
  13. package/src/core/index.ts +8 -0
  14. package/src/core/schemas.ts +17 -0
  15. package/src/core/types.ts +158 -0
  16. package/src/create-agent.ts +309 -0
  17. package/src/debug-logger.ts +188 -0
  18. package/src/dx/agent.ts +88 -0
  19. package/src/dx/define-agent.ts +183 -0
  20. package/src/dx/dev-logger.ts +57 -0
  21. package/src/dx/index.ts +11 -0
  22. package/src/errors.ts +175 -0
  23. package/src/execution/engine.ts +522 -0
  24. package/src/execution/graph-builder.ts +362 -0
  25. package/src/execution/index.ts +8 -0
  26. package/src/execution/types.ts +257 -0
  27. package/src/execution/worker-pool.ts +308 -0
  28. package/src/extensions/index.ts +123 -0
  29. package/src/guardrails/allowlist.ts +155 -0
  30. package/src/guardrails/index.ts +17 -0
  31. package/src/guardrails/types.ts +159 -0
  32. package/src/guardrails/validator.ts +265 -0
  33. package/src/index.ts +74 -0
  34. package/src/knowledge/index.ts +5 -0
  35. package/src/knowledge/types.ts +52 -0
  36. package/src/learning/in-memory-store.ts +72 -0
  37. package/src/learning/index.ts +6 -0
  38. package/src/learning/types.ts +42 -0
  39. package/src/llm/cache.ts +300 -0
  40. package/src/llm/index.ts +22 -0
  41. package/src/llm/model-resolver.ts +81 -0
  42. package/src/llm/openai-provider.ts +313 -0
  43. package/src/llm/openrouter-provider.ts +29 -0
  44. package/src/llm/types.ts +131 -0
  45. package/src/memory/in-memory-store.ts +255 -0
  46. package/src/memory/index.ts +7 -0
  47. package/src/memory/types.ts +193 -0
  48. package/src/memory/vector-store.ts +251 -0
  49. package/src/observability/console-logger.ts +123 -0
  50. package/src/observability/index.ts +12 -0
  51. package/src/observability/metrics.ts +85 -0
  52. package/src/observability/otlp-exporter.ts +417 -0
  53. package/src/observability/tracer.ts +105 -0
  54. package/src/observability/types.ts +341 -0
  55. package/src/orchestration/agent-adapter.ts +33 -0
  56. package/src/orchestration/index.ts +34 -0
  57. package/src/orchestration/load-balancer.ts +151 -0
  58. package/src/orchestration/mcp-types.ts +59 -0
  59. package/src/orchestration/message-bus.ts +192 -0
  60. package/src/orchestration/orchestrator.ts +349 -0
  61. package/src/orchestration/pipeline.ts +66 -0
  62. package/src/orchestration/supervisor.ts +107 -0
  63. package/src/orchestration/swarm.ts +1099 -0
  64. package/src/orchestration/toolkit.ts +47 -0
  65. package/src/orchestration/types.ts +339 -0
  66. package/src/planner/classical-planner.ts +383 -0
  67. package/src/planner/index.ts +8 -0
  68. package/src/planner/llm-planner.ts +353 -0
  69. package/src/planner/types.ts +227 -0
  70. package/src/planner/validator.ts +297 -0
  71. package/src/production/circuit-breaker.ts +290 -0
  72. package/src/production/graceful-shutdown.ts +251 -0
  73. package/src/production/health.ts +333 -0
  74. package/src/production/index.ts +57 -0
  75. package/src/production/latency-eval.ts +62 -0
  76. package/src/production/rate-limiter.ts +287 -0
  77. package/src/production/resumable-stream.ts +289 -0
  78. package/src/production/types.ts +81 -0
  79. package/src/sdk/index.ts +374 -0
  80. package/src/session/db-driver.ts +50 -0
  81. package/src/session/in-memory-store.ts +235 -0
  82. package/src/session/index.ts +12 -0
  83. package/src/session/sql-store.ts +315 -0
  84. package/src/session/sqlite-store.ts +61 -0
  85. package/src/session/types.ts +153 -0
  86. package/src/tools/base-tool.ts +223 -0
  87. package/src/tools/browser-tool.ts +123 -0
  88. package/src/tools/calculator-tool.ts +265 -0
  89. package/src/tools/file-tools.ts +394 -0
  90. package/src/tools/github-tool.ts +432 -0
  91. package/src/tools/hackernews-tool.ts +187 -0
  92. package/src/tools/http-tool.ts +118 -0
  93. package/src/tools/index.ts +99 -0
  94. package/src/tools/jira-tool.ts +373 -0
  95. package/src/tools/notion-tool.ts +322 -0
  96. package/src/tools/openai-tool.ts +236 -0
  97. package/src/tools/registry.ts +131 -0
  98. package/src/tools/serpapi-tool.ts +234 -0
  99. package/src/tools/shell-tool.ts +118 -0
  100. package/src/tools/slack-tool.ts +327 -0
  101. package/src/tools/telegram-tool.ts +127 -0
  102. package/src/tools/types.ts +229 -0
  103. package/src/tools/websearch-tool.ts +335 -0
  104. package/src/tools/wikipedia-tool.ts +177 -0
  105. package/src/tools/yfinance-tool.ts +33 -0
  106. package/src/voice/index.ts +17 -0
  107. package/src/voice/voice-provider.ts +228 -0
  108. package/tests/artifact.test.ts +241 -0
  109. package/tests/circuit-breaker.test.ts +171 -0
  110. package/tests/health.test.ts +192 -0
  111. package/tests/llm-cache.test.ts +186 -0
  112. package/tests/rate-limiter.test.ts +161 -0
  113. package/tsconfig.json +29 -0
  114. package/vitest.config.ts +47 -0
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Plan validator implementation
3
+ */
4
+
5
+ import {
6
+ ValidationResult,
7
+ ValidationError,
8
+ Plan,
9
+ Task,
10
+ TaskStatus,
11
+ } from './types.js';
12
+ import type { EntityId } from '../core/types.js';
13
+
14
+ /**
15
+ * Plan validator with comprehensive checks
16
+ */
17
+ export class PlanValidator {
18
+ private customRules: ValidationRule[] = [];
19
+
20
+ /**
21
+ * Validate a plan comprehensively
22
+ */
23
+ validate(plan: Plan): ValidationResult {
24
+ const errors: ValidationError[] = [];
25
+
26
+ // Check for empty plan
27
+ if (plan.tasks.length === 0) {
28
+ errors.push({
29
+ message: 'Plan contains no tasks',
30
+ severity: 'error',
31
+ });
32
+ }
33
+
34
+ // Check for duplicate task IDs
35
+ errors.push(...this.checkDuplicateIds(plan.tasks));
36
+
37
+ // Check for circular dependencies
38
+ errors.push(...this.checkCircularDependencies(plan.tasks));
39
+
40
+ // Check for orphaned tasks
41
+ errors.push(...this.checkOrphanedTasks(plan.tasks));
42
+
43
+ // Check for missing dependencies
44
+ errors.push(...this.checkMissingDependencies(plan.tasks));
45
+
46
+ // Check task validity
47
+ for (const task of plan.tasks) {
48
+ errors.push(...this.validateTask(task));
49
+ }
50
+
51
+ // Apply custom rules
52
+ for (const rule of this.customRules) {
53
+ const ruleErrors = rule(plan);
54
+ errors.push(...ruleErrors);
55
+ }
56
+
57
+ return {
58
+ valid: errors.filter(e => e.severity === 'error').length === 0,
59
+ errors,
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Add a custom validation rule
65
+ */
66
+ addRule(rule: ValidationRule): void {
67
+ this.customRules.push(rule);
68
+ }
69
+
70
+ /**
71
+ * Remove a custom validation rule
72
+ */
73
+ removeRule(rule: ValidationRule): void {
74
+ const index = this.customRules.indexOf(rule);
75
+ if (index > -1) {
76
+ this.customRules.splice(index, 1);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Check for duplicate task IDs
82
+ */
83
+ private checkDuplicateIds(tasks: Task[]): ValidationError[] {
84
+ const errors: ValidationError[] = [];
85
+ const seenIds = new Set<EntityId>();
86
+
87
+ for (const task of tasks) {
88
+ if (seenIds.has(task.id)) {
89
+ errors.push({
90
+ taskId: task.id,
91
+ message: `Duplicate task ID: ${task.id}`,
92
+ severity: 'error',
93
+ });
94
+ }
95
+ seenIds.add(task.id);
96
+ }
97
+
98
+ return errors;
99
+ }
100
+
101
+ /**
102
+ * Check for circular dependencies using DFS
103
+ */
104
+ private checkCircularDependencies(tasks: Task[]): ValidationError[] {
105
+ const errors: ValidationError[] = [];
106
+ const visited = new Set<EntityId>();
107
+ const recursionStack = new Set<EntityId>();
108
+
109
+ const hasCycle = (taskId: EntityId, path: EntityId[], taskMap: Map<EntityId, Task>): boolean => {
110
+ visited.add(taskId);
111
+ recursionStack.add(taskId);
112
+
113
+ const task = taskMap.get(taskId);
114
+ if (task) {
115
+ for (const depId of task.dependencies) {
116
+ if (!visited.has(depId)) {
117
+ if (hasCycle(depId, [...path, depId], taskMap)) {
118
+ return true;
119
+ }
120
+ } else if (recursionStack.has(depId)) {
121
+ // Found cycle
122
+ const cycleStart = path.indexOf(depId);
123
+ const cycle = path.slice(cycleStart).concat([taskId]);
124
+ errors.push({
125
+ taskId,
126
+ message: `Circular dependency detected: ${cycle.join(' -> ')}`,
127
+ severity: 'error',
128
+ });
129
+ return true;
130
+ }
131
+ }
132
+ }
133
+
134
+ recursionStack.delete(taskId);
135
+ return false;
136
+ };
137
+
138
+ for (const task of tasks) {
139
+ if (!visited.has(task.id)) {
140
+ // Create task map for cycle detection
141
+ const taskMapForCycle = new Map(tasks.map(t => [t.id, t]));
142
+ hasCycle(task.id, [task.id], taskMapForCycle);
143
+ }
144
+ }
145
+
146
+ return errors;
147
+ }
148
+
149
+ /**
150
+ * Check for orphaned tasks (tasks not reachable from any root)
151
+ */
152
+ private checkOrphanedTasks(tasks: Task[]): ValidationError[] {
153
+ const errors: ValidationError[] = [];
154
+ const reachable = new Set<EntityId>();
155
+
156
+ // Find all root tasks (no dependencies)
157
+ const roots = tasks.filter(t => t.dependencies.length === 0);
158
+
159
+ // BFS from roots to find all reachable tasks
160
+ const queue = [...roots];
161
+ while (queue.length > 0) {
162
+ const task = queue.shift()!;
163
+ reachable.add(task.id);
164
+
165
+ // Find tasks that depend on this task
166
+ for (const otherTask of tasks) {
167
+ if (otherTask.dependencies.includes(task.id) && !reachable.has(otherTask.id)) {
168
+ queue.push(otherTask);
169
+ }
170
+ }
171
+ }
172
+
173
+ // Check for unreachable tasks
174
+ for (const task of tasks) {
175
+ if (!reachable.has(task.id)) {
176
+ errors.push({
177
+ taskId: task.id,
178
+ message: `Orphaned task: ${task.name} is not reachable from any root task`,
179
+ severity: 'warning',
180
+ });
181
+ }
182
+ }
183
+
184
+ return errors;
185
+ }
186
+
187
+ /**
188
+ * Check for missing dependencies
189
+ */
190
+ private checkMissingDependencies(tasks: Task[]): ValidationError[] {
191
+ const errors: ValidationError[] = [];
192
+ const taskIds = new Set(tasks.map(t => t.id));
193
+
194
+ for (const task of tasks) {
195
+ for (const depId of task.dependencies) {
196
+ if (!taskIds.has(depId)) {
197
+ errors.push({
198
+ taskId: task.id,
199
+ message: `Missing dependency: ${depId} does not exist in the plan`,
200
+ severity: 'error',
201
+ });
202
+ }
203
+ }
204
+ }
205
+
206
+ return errors;
207
+ }
208
+
209
+ /**
210
+ * Validate a single task
211
+ */
212
+ private validateTask(task: Task): ValidationError[] {
213
+ const errors: ValidationError[] = [];
214
+
215
+ // Check name
216
+ if (!task.name || task.name.trim().length === 0) {
217
+ errors.push({
218
+ taskId: task.id,
219
+ message: 'Task name is required',
220
+ severity: 'error',
221
+ });
222
+ } else if (task.name.length > 100) {
223
+ errors.push({
224
+ taskId: task.id,
225
+ message: 'Task name exceeds 100 characters',
226
+ severity: 'warning',
227
+ });
228
+ }
229
+
230
+ // Check description
231
+ if (!task.description || task.description.trim().length === 0) {
232
+ errors.push({
233
+ taskId: task.id,
234
+ message: 'Task description is required',
235
+ severity: 'warning',
236
+ });
237
+ }
238
+
239
+ // Check estimated duration
240
+ if (task.estimatedDurationMs !== undefined) {
241
+ if (task.estimatedDurationMs <= 0) {
242
+ errors.push({
243
+ taskId: task.id,
244
+ message: 'Estimated duration must be positive',
245
+ severity: 'error',
246
+ });
247
+ } else if (task.estimatedDurationMs > 86400000) { // 24 hours
248
+ errors.push({
249
+ taskId: task.id,
250
+ message: 'Estimated duration exceeds 24 hours, consider breaking into smaller tasks',
251
+ severity: 'warning',
252
+ });
253
+ }
254
+ }
255
+
256
+ // Check metadata
257
+ if (task.metadata.maxRetries !== undefined && task.metadata.maxRetries < 0) {
258
+ errors.push({
259
+ taskId: task.id,
260
+ message: 'Max retries must be non-negative',
261
+ severity: 'error',
262
+ });
263
+ }
264
+
265
+ if (task.metadata.timeoutMs !== undefined && task.metadata.timeoutMs <= 0) {
266
+ errors.push({
267
+ taskId: task.id,
268
+ message: 'Timeout must be positive',
269
+ severity: 'error',
270
+ });
271
+ }
272
+
273
+ return errors;
274
+ }
275
+
276
+ /**
277
+ * Validate task execution result
278
+ */
279
+ validateTaskResult(task: Task, result: { status: TaskStatus; error?: { message: string } }): ValidationError[] {
280
+ const errors: ValidationError[] = [];
281
+
282
+ if (result.status === TaskStatus.FAILED && !result.error) {
283
+ errors.push({
284
+ taskId: task.id,
285
+ message: 'Failed task result must include error information',
286
+ severity: 'error',
287
+ });
288
+ }
289
+
290
+ return errors;
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Custom validation rule type
296
+ */
297
+ type ValidationRule = (plan: Plan) => ValidationError[];
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Circuit Breaker Pattern - Agno-style Production Resilience
3
+ *
4
+ * Prevents cascading failures by tracking error rates and temporarily
5
+ * blocking calls to failing services. Supports:
6
+ * - Configurable failure thresholds
7
+ * - Automatic recovery with half-open state
8
+ * - Metrics integration for observability
9
+ * - Event callbacks for monitoring
10
+ */
11
+
12
+ import type { MetricsCollector } from '../observability/types.js';
13
+
14
+ /** Circuit breaker states */
15
+ export enum CircuitState {
16
+ /** Normal operation - requests pass through */
17
+ CLOSED = 'CLOSED',
18
+ /** Circuit tripped - requests are rejected immediately */
19
+ OPEN = 'OPEN',
20
+ /** Testing recovery - limited requests allowed */
21
+ HALF_OPEN = 'HALF_OPEN',
22
+ }
23
+
24
+ /** Circuit breaker configuration */
25
+ export interface CircuitBreakerConfig {
26
+ /** Unique name for this circuit (for metrics/logging) */
27
+ readonly name: string;
28
+ /** Number of failures before opening circuit (default: 5) */
29
+ readonly failureThreshold?: number;
30
+ /** Number of successes in half-open before closing (default: 2) */
31
+ readonly successThreshold?: number;
32
+ /** Time in ms before attempting recovery (default: 30000) */
33
+ readonly resetTimeoutMs?: number;
34
+ /** Time window in ms for counting failures (default: 60000) */
35
+ readonly failureWindowMs?: number;
36
+ /** Optional metrics collector for observability */
37
+ readonly metrics?: MetricsCollector;
38
+ /** Callback when state changes */
39
+ readonly onStateChange?: (from: CircuitState, to: CircuitState) => void;
40
+ }
41
+
42
+ /** Circuit breaker execution result */
43
+ export interface CircuitBreakerResult<T> {
44
+ readonly success: boolean;
45
+ readonly value?: T;
46
+ readonly error?: Error;
47
+ readonly state: CircuitState;
48
+ readonly executionTimeMs: number;
49
+ }
50
+
51
+ /** Error thrown when circuit is open */
52
+ export class CircuitOpenError extends Error {
53
+ readonly circuitName: string;
54
+ readonly state: CircuitState;
55
+ readonly resetAt: Date;
56
+
57
+ constructor(name: string, resetAt: Date) {
58
+ super(`Circuit '${name}' is OPEN. Retry after ${resetAt.toISOString()}`);
59
+ this.name = 'CircuitOpenError';
60
+ this.circuitName = name;
61
+ this.state = CircuitState.OPEN;
62
+ this.resetAt = resetAt;
63
+ Object.setPrototypeOf(this, CircuitOpenError.prototype);
64
+ }
65
+ }
66
+
67
+ /** Failure record for sliding window */
68
+ interface FailureRecord {
69
+ readonly timestamp: number;
70
+ readonly error: Error;
71
+ }
72
+
73
+ /**
74
+ * Circuit Breaker implementation with sliding window failure tracking.
75
+ *
76
+ * @example
77
+ * const breaker = new CircuitBreaker({
78
+ * name: 'openai-api',
79
+ * failureThreshold: 5,
80
+ * resetTimeoutMs: 30000,
81
+ * });
82
+ *
83
+ * const result = await breaker.execute(() => openai.chat(...));
84
+ * if (result.success) {
85
+ * console.log(result.value);
86
+ * } else {
87
+ * console.error('Blocked or failed:', result.error);
88
+ * }
89
+ */
90
+ export class CircuitBreaker {
91
+ private state: CircuitState = CircuitState.CLOSED;
92
+ private failures: FailureRecord[] = [];
93
+ private successCount = 0;
94
+ private lastFailureTime = 0;
95
+ private openedAt = 0;
96
+
97
+ private readonly config: Required<
98
+ Omit<CircuitBreakerConfig, 'metrics' | 'onStateChange'>
99
+ > & Pick<CircuitBreakerConfig, 'metrics' | 'onStateChange'>;
100
+
101
+ constructor(config: CircuitBreakerConfig) {
102
+ this.config = {
103
+ name: config.name,
104
+ failureThreshold: config.failureThreshold ?? 5,
105
+ successThreshold: config.successThreshold ?? 2,
106
+ resetTimeoutMs: config.resetTimeoutMs ?? 30_000,
107
+ failureWindowMs: config.failureWindowMs ?? 60_000,
108
+ metrics: config.metrics,
109
+ onStateChange: config.onStateChange,
110
+ };
111
+ }
112
+
113
+ /** Get current circuit state */
114
+ getState(): CircuitState {
115
+ return this.state;
116
+ }
117
+
118
+ /** Get circuit name */
119
+ getName(): string {
120
+ return this.config.name;
121
+ }
122
+
123
+ /** Check if circuit allows requests */
124
+ isAllowed(): boolean {
125
+ this.checkStateTransition();
126
+ return this.state !== CircuitState.OPEN;
127
+ }
128
+
129
+ /** Get time until circuit resets (if open) */
130
+ getResetTime(): Date | null {
131
+ if (this.state !== CircuitState.OPEN) return null;
132
+ return new Date(this.openedAt + this.config.resetTimeoutMs);
133
+ }
134
+
135
+ /**
136
+ * Execute a function through the circuit breaker.
137
+ * Tracks success/failure and manages state transitions.
138
+ */
139
+ async execute<T>(fn: () => Promise<T>): Promise<CircuitBreakerResult<T>> {
140
+ const startTime = Date.now();
141
+
142
+ // Check if we should allow this request
143
+ this.checkStateTransition();
144
+
145
+ if (this.state === CircuitState.OPEN) {
146
+ const resetAt = this.getResetTime()!;
147
+ this.recordMetric('circuit_rejected', 1);
148
+ return {
149
+ success: false,
150
+ error: new CircuitOpenError(this.config.name, resetAt),
151
+ state: this.state,
152
+ executionTimeMs: Date.now() - startTime,
153
+ };
154
+ }
155
+
156
+ try {
157
+ const value = await fn();
158
+ this.recordSuccess();
159
+ return {
160
+ success: true,
161
+ value,
162
+ state: this.state,
163
+ executionTimeMs: Date.now() - startTime,
164
+ };
165
+ } catch (error) {
166
+ this.recordFailure(error as Error);
167
+ return {
168
+ success: false,
169
+ error: error as Error,
170
+ state: this.state,
171
+ executionTimeMs: Date.now() - startTime,
172
+ };
173
+ }
174
+ }
175
+
176
+ /** Force reset the circuit to closed state */
177
+ reset(): void {
178
+ this.transitionTo(CircuitState.CLOSED);
179
+ this.failures = [];
180
+ this.successCount = 0;
181
+ this.lastFailureTime = 0;
182
+ this.openedAt = 0;
183
+ }
184
+
185
+ /** Get current failure count within window */
186
+ getFailureCount(): number {
187
+ this.pruneOldFailures();
188
+ return this.failures.length;
189
+ }
190
+
191
+ /** Get circuit statistics */
192
+ getStats(): {
193
+ state: CircuitState;
194
+ failureCount: number;
195
+ successCount: number;
196
+ lastFailure: Date | null;
197
+ } {
198
+ return {
199
+ state: this.state,
200
+ failureCount: this.getFailureCount(),
201
+ successCount: this.successCount,
202
+ lastFailure: this.lastFailureTime > 0 ? new Date(this.lastFailureTime) : null,
203
+ };
204
+ }
205
+
206
+ // --- Private methods ---
207
+
208
+ private checkStateTransition(): void {
209
+ const now = Date.now();
210
+
211
+ if (this.state === CircuitState.OPEN) {
212
+ // Check if reset timeout has passed
213
+ if (now - this.openedAt >= this.config.resetTimeoutMs) {
214
+ this.transitionTo(CircuitState.HALF_OPEN);
215
+ this.successCount = 0;
216
+ }
217
+ }
218
+ }
219
+
220
+ private recordSuccess(): void {
221
+ this.recordMetric('circuit_success', 1);
222
+
223
+ if (this.state === CircuitState.HALF_OPEN) {
224
+ this.successCount++;
225
+ if (this.successCount >= this.config.successThreshold) {
226
+ this.transitionTo(CircuitState.CLOSED);
227
+ this.failures = [];
228
+ }
229
+ }
230
+ }
231
+
232
+ private recordFailure(error: Error): void {
233
+ const now = Date.now();
234
+ this.lastFailureTime = now;
235
+ this.failures.push({ timestamp: now, error });
236
+ this.recordMetric('circuit_failure', 1);
237
+
238
+ this.pruneOldFailures();
239
+
240
+ if (this.state === CircuitState.HALF_OPEN) {
241
+ // Any failure in half-open immediately opens
242
+ this.transitionTo(CircuitState.OPEN);
243
+ this.openedAt = now;
244
+ } else if (this.state === CircuitState.CLOSED) {
245
+ if (this.failures.length >= this.config.failureThreshold) {
246
+ this.transitionTo(CircuitState.OPEN);
247
+ this.openedAt = now;
248
+ }
249
+ }
250
+ }
251
+
252
+ private pruneOldFailures(): void {
253
+ const cutoff = Date.now() - this.config.failureWindowMs;
254
+ this.failures = this.failures.filter(f => f.timestamp > cutoff);
255
+ }
256
+
257
+ private transitionTo(newState: CircuitState): void {
258
+ if (this.state === newState) return;
259
+
260
+ const oldState = this.state;
261
+ this.state = newState;
262
+
263
+ this.recordMetric('circuit_state_change', 1, { from: oldState, to: newState });
264
+ this.config.onStateChange?.(oldState, newState);
265
+ }
266
+
267
+ private recordMetric(name: string, value: number, labels: Record<string, string> = {}): void {
268
+ this.config.metrics?.counter(`${this.config.name}.${name}`, value, {
269
+ circuit: this.config.name,
270
+ ...labels,
271
+ });
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Create a circuit breaker with common defaults for LLM providers.
277
+ */
278
+ export function createLLMCircuitBreaker(
279
+ name: string,
280
+ options?: Partial<CircuitBreakerConfig>
281
+ ): CircuitBreaker {
282
+ return new CircuitBreaker({
283
+ name,
284
+ failureThreshold: 3,
285
+ successThreshold: 2,
286
+ resetTimeoutMs: 30_000,
287
+ failureWindowMs: 60_000,
288
+ ...options,
289
+ });
290
+ }