outcome-cli 1.0.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 (113) hide show
  1. package/README.md +261 -0
  2. package/package.json +95 -0
  3. package/src/agents/README.md +139 -0
  4. package/src/agents/adapters/anthropic.adapter.ts +166 -0
  5. package/src/agents/adapters/dalle.adapter.ts +145 -0
  6. package/src/agents/adapters/gemini.adapter.ts +134 -0
  7. package/src/agents/adapters/imagen.adapter.ts +106 -0
  8. package/src/agents/adapters/nano-banana.adapter.ts +129 -0
  9. package/src/agents/adapters/openai.adapter.ts +165 -0
  10. package/src/agents/adapters/veo.adapter.ts +130 -0
  11. package/src/agents/agent.schema.property.test.ts +379 -0
  12. package/src/agents/agent.schema.test.ts +148 -0
  13. package/src/agents/agent.schema.ts +263 -0
  14. package/src/agents/index.ts +60 -0
  15. package/src/agents/registered-agent.schema.ts +356 -0
  16. package/src/agents/registry.ts +97 -0
  17. package/src/agents/tournament-configs.property.test.ts +266 -0
  18. package/src/cli/README.md +145 -0
  19. package/src/cli/commands/define.ts +79 -0
  20. package/src/cli/commands/list.ts +46 -0
  21. package/src/cli/commands/logs.ts +83 -0
  22. package/src/cli/commands/run.ts +416 -0
  23. package/src/cli/commands/verify.ts +110 -0
  24. package/src/cli/index.ts +81 -0
  25. package/src/config/README.md +128 -0
  26. package/src/config/env.ts +262 -0
  27. package/src/config/index.ts +19 -0
  28. package/src/eval/README.md +318 -0
  29. package/src/eval/ai-judge.test.ts +435 -0
  30. package/src/eval/ai-judge.ts +368 -0
  31. package/src/eval/code-validators.ts +414 -0
  32. package/src/eval/evaluateOutcome.property.test.ts +1174 -0
  33. package/src/eval/evaluateOutcome.ts +591 -0
  34. package/src/eval/immigration-validators.ts +122 -0
  35. package/src/eval/index.ts +90 -0
  36. package/src/eval/judge-cache.ts +402 -0
  37. package/src/eval/tournament-validators.property.test.ts +439 -0
  38. package/src/eval/validators.property.test.ts +1118 -0
  39. package/src/eval/validators.ts +1199 -0
  40. package/src/eval/weighted-scorer.ts +285 -0
  41. package/src/index.ts +17 -0
  42. package/src/league/README.md +188 -0
  43. package/src/league/health-check.ts +353 -0
  44. package/src/league/index.ts +93 -0
  45. package/src/league/killAgent.ts +151 -0
  46. package/src/league/league.test.ts +1151 -0
  47. package/src/league/runLeague.ts +843 -0
  48. package/src/league/scoreAgent.ts +175 -0
  49. package/src/modules/omnibridge/__tests__/.gitkeep +1 -0
  50. package/src/modules/omnibridge/__tests__/auth-tunnel.property.test.ts +524 -0
  51. package/src/modules/omnibridge/__tests__/deterministic-logger.property.test.ts +965 -0
  52. package/src/modules/omnibridge/__tests__/ghost-api.property.test.ts +461 -0
  53. package/src/modules/omnibridge/__tests__/omnibridge-integration.test.ts +542 -0
  54. package/src/modules/omnibridge/__tests__/parallel-executor.property.test.ts +671 -0
  55. package/src/modules/omnibridge/__tests__/semantic-normalizer.property.test.ts +521 -0
  56. package/src/modules/omnibridge/__tests__/semantic-normalizer.test.ts +254 -0
  57. package/src/modules/omnibridge/__tests__/session-vault.property.test.ts +367 -0
  58. package/src/modules/omnibridge/__tests__/shadow-session.property.test.ts +523 -0
  59. package/src/modules/omnibridge/__tests__/triangulation-engine.property.test.ts +292 -0
  60. package/src/modules/omnibridge/__tests__/verification-engine.property.test.ts +769 -0
  61. package/src/modules/omnibridge/api/.gitkeep +1 -0
  62. package/src/modules/omnibridge/api/ghost-api.ts +1087 -0
  63. package/src/modules/omnibridge/auth/.gitkeep +1 -0
  64. package/src/modules/omnibridge/auth/auth-tunnel.ts +843 -0
  65. package/src/modules/omnibridge/auth/session-vault.ts +577 -0
  66. package/src/modules/omnibridge/core/.gitkeep +1 -0
  67. package/src/modules/omnibridge/core/semantic-normalizer.ts +702 -0
  68. package/src/modules/omnibridge/core/triangulation-engine.ts +530 -0
  69. package/src/modules/omnibridge/core/types.ts +610 -0
  70. package/src/modules/omnibridge/execution/.gitkeep +1 -0
  71. package/src/modules/omnibridge/execution/deterministic-logger.ts +629 -0
  72. package/src/modules/omnibridge/execution/parallel-executor.ts +542 -0
  73. package/src/modules/omnibridge/execution/shadow-session.ts +794 -0
  74. package/src/modules/omnibridge/index.ts +212 -0
  75. package/src/modules/omnibridge/omnibridge.ts +510 -0
  76. package/src/modules/omnibridge/verification/.gitkeep +1 -0
  77. package/src/modules/omnibridge/verification/verification-engine.ts +783 -0
  78. package/src/outcomes/README.md +75 -0
  79. package/src/outcomes/acquire-pilot-customer.ts +297 -0
  80. package/src/outcomes/code-delivery-outcomes.ts +89 -0
  81. package/src/outcomes/code-outcomes.ts +256 -0
  82. package/src/outcomes/code_review_battle.test.ts +135 -0
  83. package/src/outcomes/code_review_battle.ts +135 -0
  84. package/src/outcomes/cold_email_battle.ts +97 -0
  85. package/src/outcomes/content_creation_battle.ts +160 -0
  86. package/src/outcomes/f1_stem_opt_compliance.ts +61 -0
  87. package/src/outcomes/index.ts +107 -0
  88. package/src/outcomes/lead_gen_battle.test.ts +113 -0
  89. package/src/outcomes/lead_gen_battle.ts +99 -0
  90. package/src/outcomes/outcome.schema.property.test.ts +229 -0
  91. package/src/outcomes/outcome.schema.ts +187 -0
  92. package/src/outcomes/qualified_sales_interest.ts +118 -0
  93. package/src/outcomes/swarm_planner.property.test.ts +370 -0
  94. package/src/outcomes/swarm_planner.ts +96 -0
  95. package/src/outcomes/web_extraction.ts +234 -0
  96. package/src/runtime/README.md +220 -0
  97. package/src/runtime/agentRunner.test.ts +341 -0
  98. package/src/runtime/agentRunner.ts +746 -0
  99. package/src/runtime/claudeAdapter.ts +232 -0
  100. package/src/runtime/costTracker.ts +123 -0
  101. package/src/runtime/index.ts +34 -0
  102. package/src/runtime/modelAdapter.property.test.ts +305 -0
  103. package/src/runtime/modelAdapter.ts +144 -0
  104. package/src/runtime/openaiAdapter.ts +235 -0
  105. package/src/utils/README.md +122 -0
  106. package/src/utils/command-runner.ts +134 -0
  107. package/src/utils/cost-guard.ts +379 -0
  108. package/src/utils/errors.test.ts +290 -0
  109. package/src/utils/errors.ts +442 -0
  110. package/src/utils/index.ts +37 -0
  111. package/src/utils/logger.test.ts +361 -0
  112. package/src/utils/logger.ts +419 -0
  113. package/src/utils/output-parsers.ts +216 -0
@@ -0,0 +1,379 @@
1
+ /**
2
+ * Cost Guard System - Prevents Surprise API Bills
3
+ *
4
+ * Implements strict cost controls and monitoring to prevent unexpected charges.
5
+ * Extended for battle routing integration with platform vs user cost attribution.
6
+ *
7
+ * @module utils/cost-guard
8
+ */
9
+
10
+ export interface CostLimits {
11
+ dailyLimit: number;
12
+ monthlyLimit: number;
13
+ perRequestLimit: number;
14
+ emergencyStop: boolean;
15
+ /** Platform-specific limits for founder accounts */
16
+ platformDailyLimit?: number;
17
+ platformMonthlyLimit?: number;
18
+ }
19
+
20
+ export interface UsageTracker {
21
+ dailySpent: number;
22
+ monthlySpent: number;
23
+ requestCount: number;
24
+ lastReset: string;
25
+ /** Platform usage tracking for founder accounts */
26
+ platformDailySpent: number;
27
+ platformMonthlySpent: number;
28
+ platformRequestCount: number;
29
+ /** Mock battle tracking (always zero cost) */
30
+ mockBattleCount: number;
31
+ /** Real battle tracking */
32
+ realBattleCount: number;
33
+ }
34
+
35
+ /**
36
+ * Battle cost record for tracking and attribution
37
+ */
38
+ export interface BattleCostRecord {
39
+ battleId: string;
40
+ userId: string;
41
+ cost: number;
42
+ battleMode: 'mock' | 'real';
43
+ costCategory: 'user' | 'platform' | 'mock';
44
+ timestamp: string;
45
+ tokenUsage?: { input: number; output: number };
46
+ }
47
+
48
+ /**
49
+ * Cost attribution result
50
+ */
51
+ export interface CostAttribution {
52
+ category: 'user' | 'platform' | 'mock';
53
+ cost: number;
54
+ isFounderAccount: boolean;
55
+ userId: string;
56
+ }
57
+
58
+
59
+ const DEFAULT_LIMITS: CostLimits = {
60
+ dailyLimit: 10.00, // $10/day max
61
+ monthlyLimit: 100.00, // $100/month max
62
+ perRequestLimit: 2.00, // $2 per request max
63
+ emergencyStop: false,
64
+ platformDailyLimit: 50.00, // $50/day for platform/founder usage
65
+ platformMonthlyLimit: 500.00, // $500/month for platform/founder usage
66
+ };
67
+
68
+ class CostGuard {
69
+ private limits: CostLimits;
70
+ private usage: UsageTracker;
71
+ private battleCostHistory: BattleCostRecord[] = [];
72
+
73
+ constructor(limits: Partial<CostLimits> = {}) {
74
+ this.limits = { ...DEFAULT_LIMITS, ...limits };
75
+ this.usage = this.loadUsage();
76
+ this.resetIfNeeded();
77
+ }
78
+
79
+ /**
80
+ * Check if a request is allowed based on cost limits
81
+ */
82
+ canMakeRequest(estimatedCost: number): { allowed: boolean; reason?: string } {
83
+ if (this.limits.emergencyStop) {
84
+ return { allowed: false, reason: 'Emergency stop activated' };
85
+ }
86
+
87
+ if (estimatedCost > this.limits.perRequestLimit) {
88
+ return {
89
+ allowed: false,
90
+ reason: `Request cost ${estimatedCost} exceeds limit ${this.limits.perRequestLimit}`
91
+ };
92
+ }
93
+
94
+ if (this.usage.dailySpent + estimatedCost > this.limits.dailyLimit) {
95
+ return {
96
+ allowed: false,
97
+ reason: `Would exceed daily limit: ${this.usage.dailySpent + estimatedCost} > ${this.limits.dailyLimit}`
98
+ };
99
+ }
100
+
101
+ if (this.usage.monthlySpent + estimatedCost > this.limits.monthlyLimit) {
102
+ return {
103
+ allowed: false,
104
+ reason: `Would exceed monthly limit: ${this.usage.monthlySpent + estimatedCost} > ${this.limits.monthlyLimit}`
105
+ };
106
+ }
107
+
108
+ return { allowed: true };
109
+ }
110
+
111
+ /**
112
+ * Check if a platform/founder request is allowed
113
+ */
114
+ canMakePlatformRequest(estimatedCost: number): { allowed: boolean; reason?: string } {
115
+ if (this.limits.emergencyStop) {
116
+ return { allowed: false, reason: 'Emergency stop activated' };
117
+ }
118
+
119
+ const platformDailyLimit = this.limits.platformDailyLimit || this.limits.dailyLimit * 5;
120
+ const platformMonthlyLimit = this.limits.platformMonthlyLimit || this.limits.monthlyLimit * 5;
121
+
122
+ if (this.usage.platformDailySpent + estimatedCost > platformDailyLimit) {
123
+ return {
124
+ allowed: false,
125
+ reason: `Would exceed platform daily limit: ${this.usage.platformDailySpent + estimatedCost} > ${platformDailyLimit}`
126
+ };
127
+ }
128
+
129
+ if (this.usage.platformMonthlySpent + estimatedCost > platformMonthlyLimit) {
130
+ return {
131
+ allowed: false,
132
+ reason: `Would exceed platform monthly limit: ${this.usage.platformMonthlySpent + estimatedCost} > ${platformMonthlyLimit}`
133
+ };
134
+ }
135
+
136
+ return { allowed: true };
137
+ }
138
+
139
+ /**
140
+ * Record actual cost after API call
141
+ */
142
+ recordCost(actualCost: number): void {
143
+ this.usage.dailySpent += actualCost;
144
+ this.usage.monthlySpent += actualCost;
145
+ this.usage.requestCount += 1;
146
+ this.saveUsage();
147
+
148
+ if (this.usage.dailySpent > this.limits.dailyLimit * 0.9) {
149
+ console.warn(`⚠️ Approaching daily limit: ${this.usage.dailySpent}/${this.limits.dailyLimit}`);
150
+ }
151
+
152
+ if (this.usage.monthlySpent > this.limits.monthlyLimit * 0.9) {
153
+ console.warn(`⚠️ Approaching monthly limit: ${this.usage.monthlySpent}/${this.limits.monthlyLimit}`);
154
+ }
155
+ }
156
+
157
+
158
+ /**
159
+ * Record battle cost with attribution
160
+ */
161
+ recordBattleCost(record: BattleCostRecord): void {
162
+ this.battleCostHistory.push(record);
163
+
164
+ if (this.battleCostHistory.length > 1000) {
165
+ this.battleCostHistory = this.battleCostHistory.slice(-1000);
166
+ }
167
+
168
+ if (record.battleMode === 'mock') {
169
+ this.usage.mockBattleCount += 1;
170
+ console.log(`🎭 Mock Battle Recorded: ${record.battleId} (zero cost)`);
171
+ } else {
172
+ this.usage.realBattleCount += 1;
173
+
174
+ if (record.costCategory === 'platform') {
175
+ this.usage.platformDailySpent += record.cost;
176
+ this.usage.platformMonthlySpent += record.cost;
177
+ this.usage.platformRequestCount += 1;
178
+ console.log(`🏢 Platform Battle Cost: $${record.cost.toFixed(4)} for ${record.battleId}`);
179
+ } else if (record.costCategory === 'user') {
180
+ this.usage.dailySpent += record.cost;
181
+ this.usage.monthlySpent += record.cost;
182
+ this.usage.requestCount += 1;
183
+ console.log(`👤 User Battle Cost: $${record.cost.toFixed(4)} for ${record.battleId} (user: ${record.userId})`);
184
+ }
185
+ }
186
+
187
+ this.saveUsage();
188
+ this.checkAlerts();
189
+ }
190
+
191
+ /**
192
+ * Record zero cost for mock battles
193
+ */
194
+ recordMockBattle(battleId: string, userId: string): void {
195
+ this.recordBattleCost({
196
+ battleId,
197
+ userId,
198
+ cost: 0,
199
+ battleMode: 'mock',
200
+ costCategory: 'mock',
201
+ timestamp: new Date().toISOString(),
202
+ });
203
+ }
204
+
205
+ /**
206
+ * Get cost attribution for a battle
207
+ */
208
+ getCostAttribution(userId: string, isFounderAccount: boolean, cost: number): CostAttribution {
209
+ if (cost === 0) {
210
+ return { category: 'mock', cost: 0, isFounderAccount, userId };
211
+ }
212
+ return {
213
+ category: isFounderAccount ? 'platform' : 'user',
214
+ cost,
215
+ isFounderAccount,
216
+ userId,
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Get current usage status
222
+ */
223
+ getUsageStatus(): {
224
+ daily: { spent: number; limit: number; remaining: number };
225
+ monthly: { spent: number; limit: number; remaining: number };
226
+ platform: { dailySpent: number; monthlySpent: number; dailyLimit: number; monthlyLimit: number };
227
+ requests: number;
228
+ battles: { mock: number; real: number };
229
+ emergencyStop: boolean;
230
+ } {
231
+ const platformDailyLimit = this.limits.platformDailyLimit || this.limits.dailyLimit * 5;
232
+ const platformMonthlyLimit = this.limits.platformMonthlyLimit || this.limits.monthlyLimit * 5;
233
+
234
+ return {
235
+ daily: {
236
+ spent: this.usage.dailySpent,
237
+ limit: this.limits.dailyLimit,
238
+ remaining: Math.max(0, this.limits.dailyLimit - this.usage.dailySpent)
239
+ },
240
+ monthly: {
241
+ spent: this.usage.monthlySpent,
242
+ limit: this.limits.monthlyLimit,
243
+ remaining: Math.max(0, this.limits.monthlyLimit - this.usage.monthlySpent)
244
+ },
245
+ platform: {
246
+ dailySpent: this.usage.platformDailySpent,
247
+ monthlySpent: this.usage.platformMonthlySpent,
248
+ dailyLimit: platformDailyLimit,
249
+ monthlyLimit: platformMonthlyLimit,
250
+ },
251
+ requests: this.usage.requestCount,
252
+ battles: {
253
+ mock: this.usage.mockBattleCount,
254
+ real: this.usage.realBattleCount,
255
+ },
256
+ emergencyStop: this.limits.emergencyStop
257
+ };
258
+ }
259
+
260
+ getBattleCostHistory(limit: number = 100): BattleCostRecord[] {
261
+ return this.battleCostHistory.slice(-limit);
262
+ }
263
+
264
+ enableEmergencyStop(): void {
265
+ this.limits.emergencyStop = true;
266
+ console.log('🚨 EMERGENCY STOP ACTIVATED - All API calls blocked');
267
+ }
268
+
269
+ disableEmergencyStop(): void {
270
+ this.limits.emergencyStop = false;
271
+ console.log('✅ Emergency stop disabled - API calls resumed');
272
+ }
273
+
274
+ /**
275
+ * Check and trigger alerts for approaching limits
276
+ */
277
+ private checkAlerts(): void {
278
+ const platformDailyLimit = this.limits.platformDailyLimit || this.limits.dailyLimit * 5;
279
+ const platformMonthlyLimit = this.limits.platformMonthlyLimit || this.limits.monthlyLimit * 5;
280
+
281
+ // User limit alerts
282
+ if (this.usage.dailySpent > this.limits.dailyLimit * 0.9) {
283
+ console.warn(`⚠️ Approaching daily limit: ${this.usage.dailySpent.toFixed(2)}/${this.limits.dailyLimit}`);
284
+ }
285
+ if (this.usage.monthlySpent > this.limits.monthlyLimit * 0.9) {
286
+ console.warn(`⚠️ Approaching monthly limit: ${this.usage.monthlySpent.toFixed(2)}/${this.limits.monthlyLimit}`);
287
+ }
288
+
289
+ // Platform limit alerts
290
+ if (this.usage.platformDailySpent > platformDailyLimit * 0.9) {
291
+ console.warn(`🏢 ⚠️ Approaching platform daily limit: ${this.usage.platformDailySpent.toFixed(2)}/${platformDailyLimit}`);
292
+ }
293
+ if (this.usage.platformMonthlySpent > platformMonthlyLimit * 0.9) {
294
+ console.warn(`🏢 ⚠️ Approaching platform monthly limit: ${this.usage.platformMonthlySpent.toFixed(2)}/${platformMonthlyLimit}`);
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Load usage from storage (in-memory for now)
300
+ */
301
+ private loadUsage(): UsageTracker {
302
+ return {
303
+ dailySpent: 0,
304
+ monthlySpent: 0,
305
+ requestCount: 0,
306
+ lastReset: new Date().toISOString(),
307
+ platformDailySpent: 0,
308
+ platformMonthlySpent: 0,
309
+ platformRequestCount: 0,
310
+ mockBattleCount: 0,
311
+ realBattleCount: 0,
312
+ };
313
+ }
314
+
315
+ /**
316
+ * Save usage to storage (in-memory for now)
317
+ */
318
+ private saveUsage(): void {
319
+ // In-memory storage - usage is already in this.usage
320
+ // Future: persist to database or file
321
+ }
322
+
323
+ /**
324
+ * Reset daily/monthly counters if needed
325
+ */
326
+ private resetIfNeeded(): void {
327
+ const now = new Date();
328
+ const lastReset = new Date(this.usage.lastReset);
329
+
330
+ // Reset daily counters if new day
331
+ if (now.toDateString() !== lastReset.toDateString()) {
332
+ this.usage.dailySpent = 0;
333
+ this.usage.platformDailySpent = 0;
334
+ console.log('📅 Daily usage counters reset');
335
+ }
336
+
337
+ // Reset monthly counters if new month
338
+ if (now.getMonth() !== lastReset.getMonth() || now.getFullYear() !== lastReset.getFullYear()) {
339
+ this.usage.monthlySpent = 0;
340
+ this.usage.platformMonthlySpent = 0;
341
+ console.log('📅 Monthly usage counters reset');
342
+ }
343
+
344
+ this.usage.lastReset = now.toISOString();
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Estimated costs per 1K tokens for different models
350
+ */
351
+ export const ESTIMATED_COSTS: Record<string, { input: number; output: number }> = {
352
+ 'gpt-4-turbo': { input: 0.01, output: 0.03 },
353
+ 'gpt-4o': { input: 0.005, output: 0.015 },
354
+ 'gpt-4o-mini': { input: 0.00015, output: 0.0006 },
355
+ 'gpt-3.5-turbo': { input: 0.0005, output: 0.0015 },
356
+ 'claude-3-opus': { input: 0.015, output: 0.075 },
357
+ 'claude-3-sonnet': { input: 0.003, output: 0.015 },
358
+ 'claude-3-haiku': { input: 0.00025, output: 0.00125 },
359
+ 'gemini-pro': { input: 0.00025, output: 0.0005 },
360
+ 'gemini-1.5-pro': { input: 0.00125, output: 0.005 },
361
+ };
362
+
363
+ /**
364
+ * Estimate request cost based on model and token counts
365
+ */
366
+ export function estimateRequestCost(
367
+ model: string,
368
+ inputTokens: number,
369
+ outputTokens: number
370
+ ): number {
371
+ const costs = ESTIMATED_COSTS[model] || { input: 0.01, output: 0.03 }; // Default to GPT-4 pricing
372
+ return (inputTokens / 1000) * costs.input + (outputTokens / 1000) * costs.output;
373
+ }
374
+
375
+ // Singleton instance
376
+ export const costGuard = new CostGuard();
377
+
378
+ // Export class for testing
379
+ export { CostGuard };
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Error Types Tests
3
+ *
4
+ * Tests for custom error classes and error handling utilities.
5
+ *
6
+ * @module utils/errors.test
7
+ */
8
+
9
+ import { describe, it, expect } from 'vitest';
10
+ import {
11
+ ErrorCode,
12
+ ValidationError,
13
+ ExecutionError,
14
+ LimitError,
15
+ SystemError,
16
+ isEarndError,
17
+ isValidationError,
18
+ isExecutionError,
19
+ isLimitError,
20
+ isSystemError,
21
+ toErrorResponse,
22
+ type ErrorResponse,
23
+ } from './errors.js';
24
+
25
+ describe('ValidationError', () => {
26
+ it('should create error with message and code', () => {
27
+ const error = new ValidationError('Test error');
28
+
29
+ expect(error.message).toBe('Test error');
30
+ expect(error.code).toBe(ErrorCode.VALIDATION_SCHEMA_INVALID);
31
+ expect(error.recoverable).toBe(false);
32
+ expect(error.name).toBe('ValidationError');
33
+ });
34
+
35
+ it('should create error with custom code and details', () => {
36
+ const error = new ValidationError(
37
+ 'Field error',
38
+ ErrorCode.VALIDATION_FIELD_INVALID,
39
+ { field: 'email' }
40
+ );
41
+
42
+ expect(error.code).toBe(ErrorCode.VALIDATION_FIELD_INVALID);
43
+ expect(error.details).toEqual({ field: 'email' });
44
+ });
45
+
46
+ it('missingField should create proper error', () => {
47
+ const error = ValidationError.missingField('name', 'AgentConfig');
48
+
49
+ expect(error.message).toContain('Missing required field: name');
50
+ expect(error.message).toContain('AgentConfig');
51
+ expect(error.code).toBe(ErrorCode.VALIDATION_FIELD_MISSING);
52
+ expect(error.details?.field).toBe('name');
53
+ });
54
+
55
+ it('invalidField should create proper error', () => {
56
+ const error = ValidationError.invalidField('age', 'must be positive', -5);
57
+
58
+ expect(error.message).toContain('Invalid field "age"');
59
+ expect(error.message).toContain('must be positive');
60
+ expect(error.code).toBe(ErrorCode.VALIDATION_FIELD_INVALID);
61
+ expect(error.details?.value).toBe(-5);
62
+ });
63
+
64
+ it('malformedData should create proper error', () => {
65
+ const error = ValidationError.malformedData('Invalid JSON', { raw: '{bad' });
66
+
67
+ expect(error.message).toBe('Invalid JSON');
68
+ expect(error.code).toBe(ErrorCode.VALIDATION_DATA_MALFORMED);
69
+ });
70
+
71
+ it('toResponse should return ErrorResponse', () => {
72
+ const error = new ValidationError('Test', ErrorCode.VALIDATION_FIELD_MISSING, { x: 1 });
73
+ const response = error.toResponse();
74
+
75
+ expect(response.code).toBe(ErrorCode.VALIDATION_FIELD_MISSING);
76
+ expect(response.message).toBe('Test');
77
+ expect(response.recoverable).toBe(false);
78
+ expect(response.details).toEqual({ x: 1 });
79
+ });
80
+ });
81
+
82
+ describe('ExecutionError', () => {
83
+ it('should create error with default recoverable true', () => {
84
+ const error = new ExecutionError('API failed');
85
+
86
+ expect(error.message).toBe('API failed');
87
+ expect(error.code).toBe(ErrorCode.EXECUTION_MODEL_FAILURE);
88
+ expect(error.recoverable).toBe(true);
89
+ });
90
+
91
+ it('modelFailure should create proper error', () => {
92
+ const error = ExecutionError.modelFailure('claude', 'Rate limited');
93
+
94
+ expect(error.message).toContain('claude');
95
+ expect(error.message).toContain('Rate limited');
96
+ expect(error.code).toBe(ErrorCode.EXECUTION_MODEL_FAILURE);
97
+ expect(error.recoverable).toBe(true);
98
+ });
99
+
100
+ it('timeout should create proper error', () => {
101
+ const error = ExecutionError.timeout('model.complete', 30000);
102
+
103
+ expect(error.message).toContain('timed out');
104
+ expect(error.message).toContain('30000ms');
105
+ expect(error.code).toBe(ErrorCode.EXECUTION_TIMEOUT);
106
+ expect(error.details?.durationMs).toBe(30000);
107
+ });
108
+
109
+ it('networkError should create proper error', () => {
110
+ const error = ExecutionError.networkError('Connection refused');
111
+
112
+ expect(error.message).toContain('Network error');
113
+ expect(error.code).toBe(ErrorCode.EXECUTION_NETWORK_ERROR);
114
+ expect(error.recoverable).toBe(true);
115
+ });
116
+
117
+ it('retryExhausted should create non-recoverable error', () => {
118
+ const error = ExecutionError.retryExhausted('API call', 3, 'Connection reset');
119
+
120
+ expect(error.message).toContain('Retries exhausted');
121
+ expect(error.message).toContain('3 attempts');
122
+ expect(error.code).toBe(ErrorCode.EXECUTION_RETRY_EXHAUSTED);
123
+ expect(error.recoverable).toBe(false);
124
+ });
125
+ });
126
+
127
+ describe('LimitError', () => {
128
+ it('should create error with recoverable false', () => {
129
+ const error = new LimitError('Limit exceeded');
130
+
131
+ expect(error.recoverable).toBe(false);
132
+ expect(error.code).toBe(ErrorCode.LIMIT_COST_EXCEEDED);
133
+ });
134
+
135
+ it('costExceeded should create proper error', () => {
136
+ const error = LimitError.costExceeded('agent-1', 15000, 10000);
137
+
138
+ expect(error.message).toContain('agent-1');
139
+ expect(error.message).toContain('15000');
140
+ expect(error.message).toContain('10000');
141
+ expect(error.code).toBe(ErrorCode.LIMIT_COST_EXCEEDED);
142
+ expect(error.details?.spent).toBe(15000);
143
+ expect(error.details?.ceiling).toBe(10000);
144
+ });
145
+
146
+ it('attemptsExceeded should create proper error', () => {
147
+ const error = LimitError.attemptsExceeded('agent-2', 5, 5);
148
+
149
+ expect(error.message).toContain('attempt limit');
150
+ expect(error.code).toBe(ErrorCode.LIMIT_ATTEMPTS_EXCEEDED);
151
+ expect(error.details?.attempts).toBe(5);
152
+ });
153
+
154
+ it('timeExceeded should create proper error', () => {
155
+ const error = LimitError.timeExceeded('agent-3', 60000, 30000);
156
+
157
+ expect(error.message).toContain('time limit');
158
+ expect(error.code).toBe(ErrorCode.LIMIT_TIME_EXCEEDED);
159
+ expect(error.details?.elapsedMs).toBe(60000);
160
+ });
161
+
162
+ it('globalSpendExceeded should create proper error', () => {
163
+ const error = LimitError.globalSpendExceeded(100000, 50000);
164
+
165
+ expect(error.message).toContain('Global spend ceiling');
166
+ expect(error.code).toBe(ErrorCode.LIMIT_GLOBAL_SPEND_EXCEEDED);
167
+ });
168
+ });
169
+
170
+ describe('SystemError', () => {
171
+ it('should create error with recoverable false', () => {
172
+ const error = new SystemError('System failure');
173
+
174
+ expect(error.recoverable).toBe(false);
175
+ expect(error.code).toBe(ErrorCode.SYSTEM_UNKNOWN_ERROR);
176
+ });
177
+
178
+ it('infrastructureFailure should create proper error', () => {
179
+ const error = SystemError.infrastructureFailure('DurableObject', 'Storage unavailable');
180
+
181
+ expect(error.message).toContain('DurableObject');
182
+ expect(error.message).toContain('Storage unavailable');
183
+ expect(error.code).toBe(ErrorCode.SYSTEM_INFRASTRUCTURE_FAILURE);
184
+ });
185
+
186
+ it('stateCorruption should create proper error', () => {
187
+ const error = SystemError.stateCorruption('Agent state mismatch');
188
+
189
+ expect(error.message).toContain('State corruption');
190
+ expect(error.code).toBe(ErrorCode.SYSTEM_STATE_CORRUPTION);
191
+ });
192
+
193
+ it('fromUnknown should handle Error objects', () => {
194
+ const originalError = new Error('Original message');
195
+ const error = SystemError.fromUnknown(originalError);
196
+
197
+ expect(error.message).toContain('Original message');
198
+ expect(error.code).toBe(ErrorCode.SYSTEM_UNKNOWN_ERROR);
199
+ expect(error.details?.stack).toBeDefined();
200
+ });
201
+
202
+ it('fromUnknown should handle non-Error values', () => {
203
+ const error = SystemError.fromUnknown('String error');
204
+
205
+ expect(error.message).toContain('String error');
206
+ expect(error.code).toBe(ErrorCode.SYSTEM_UNKNOWN_ERROR);
207
+ });
208
+ });
209
+
210
+ describe('Type Guards', () => {
211
+ it('isEarndError should identify EarndError subclasses', () => {
212
+ expect(isEarndError(new ValidationError('test'))).toBe(true);
213
+ expect(isEarndError(new ExecutionError('test'))).toBe(true);
214
+ expect(isEarndError(new LimitError('test'))).toBe(true);
215
+ expect(isEarndError(new SystemError('test'))).toBe(true);
216
+ expect(isEarndError(new Error('test'))).toBe(false);
217
+ expect(isEarndError('string')).toBe(false);
218
+ });
219
+
220
+ it('isValidationError should identify ValidationError', () => {
221
+ expect(isValidationError(new ValidationError('test'))).toBe(true);
222
+ expect(isValidationError(new ExecutionError('test'))).toBe(false);
223
+ expect(isValidationError(new Error('test'))).toBe(false);
224
+ });
225
+
226
+ it('isExecutionError should identify ExecutionError', () => {
227
+ expect(isExecutionError(new ExecutionError('test'))).toBe(true);
228
+ expect(isExecutionError(new ValidationError('test'))).toBe(false);
229
+ });
230
+
231
+ it('isLimitError should identify LimitError', () => {
232
+ expect(isLimitError(new LimitError('test'))).toBe(true);
233
+ expect(isLimitError(new ValidationError('test'))).toBe(false);
234
+ });
235
+
236
+ it('isSystemError should identify SystemError', () => {
237
+ expect(isSystemError(new SystemError('test'))).toBe(true);
238
+ expect(isSystemError(new ValidationError('test'))).toBe(false);
239
+ });
240
+ });
241
+
242
+ describe('toErrorResponse', () => {
243
+ it('should convert EarndError to ErrorResponse', () => {
244
+ const error = new ValidationError('Test', ErrorCode.VALIDATION_FIELD_INVALID, { x: 1 });
245
+ const response = toErrorResponse(error);
246
+
247
+ expect(response.code).toBe(ErrorCode.VALIDATION_FIELD_INVALID);
248
+ expect(response.message).toBe('Test');
249
+ expect(response.recoverable).toBe(false);
250
+ expect(response.details).toEqual({ x: 1 });
251
+ });
252
+
253
+ it('should convert standard Error to ErrorResponse', () => {
254
+ const error = new Error('Standard error');
255
+ const response = toErrorResponse(error);
256
+
257
+ expect(response.code).toBe(ErrorCode.SYSTEM_UNKNOWN_ERROR);
258
+ expect(response.message).toBe('Standard error');
259
+ expect(response.recoverable).toBe(false);
260
+ expect(response.details?.name).toBe('Error');
261
+ });
262
+
263
+ it('should convert non-Error to ErrorResponse', () => {
264
+ const response = toErrorResponse('String error');
265
+
266
+ expect(response.code).toBe(ErrorCode.SYSTEM_UNKNOWN_ERROR);
267
+ expect(response.message).toBe('String error');
268
+ expect(response.recoverable).toBe(false);
269
+ });
270
+
271
+ it('should handle null/undefined', () => {
272
+ expect(toErrorResponse(null).message).toBe('null');
273
+ expect(toErrorResponse(undefined).message).toBe('undefined');
274
+ });
275
+ });
276
+
277
+ describe('Error Inheritance', () => {
278
+ it('all errors should be instances of Error', () => {
279
+ expect(new ValidationError('test') instanceof Error).toBe(true);
280
+ expect(new ExecutionError('test') instanceof Error).toBe(true);
281
+ expect(new LimitError('test') instanceof Error).toBe(true);
282
+ expect(new SystemError('test') instanceof Error).toBe(true);
283
+ });
284
+
285
+ it('errors should have proper stack traces', () => {
286
+ const error = new ValidationError('test');
287
+ expect(error.stack).toBeDefined();
288
+ expect(error.stack).toContain('ValidationError');
289
+ });
290
+ });