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.
- package/README.md +261 -0
- package/package.json +95 -0
- package/src/agents/README.md +139 -0
- package/src/agents/adapters/anthropic.adapter.ts +166 -0
- package/src/agents/adapters/dalle.adapter.ts +145 -0
- package/src/agents/adapters/gemini.adapter.ts +134 -0
- package/src/agents/adapters/imagen.adapter.ts +106 -0
- package/src/agents/adapters/nano-banana.adapter.ts +129 -0
- package/src/agents/adapters/openai.adapter.ts +165 -0
- package/src/agents/adapters/veo.adapter.ts +130 -0
- package/src/agents/agent.schema.property.test.ts +379 -0
- package/src/agents/agent.schema.test.ts +148 -0
- package/src/agents/agent.schema.ts +263 -0
- package/src/agents/index.ts +60 -0
- package/src/agents/registered-agent.schema.ts +356 -0
- package/src/agents/registry.ts +97 -0
- package/src/agents/tournament-configs.property.test.ts +266 -0
- package/src/cli/README.md +145 -0
- package/src/cli/commands/define.ts +79 -0
- package/src/cli/commands/list.ts +46 -0
- package/src/cli/commands/logs.ts +83 -0
- package/src/cli/commands/run.ts +416 -0
- package/src/cli/commands/verify.ts +110 -0
- package/src/cli/index.ts +81 -0
- package/src/config/README.md +128 -0
- package/src/config/env.ts +262 -0
- package/src/config/index.ts +19 -0
- package/src/eval/README.md +318 -0
- package/src/eval/ai-judge.test.ts +435 -0
- package/src/eval/ai-judge.ts +368 -0
- package/src/eval/code-validators.ts +414 -0
- package/src/eval/evaluateOutcome.property.test.ts +1174 -0
- package/src/eval/evaluateOutcome.ts +591 -0
- package/src/eval/immigration-validators.ts +122 -0
- package/src/eval/index.ts +90 -0
- package/src/eval/judge-cache.ts +402 -0
- package/src/eval/tournament-validators.property.test.ts +439 -0
- package/src/eval/validators.property.test.ts +1118 -0
- package/src/eval/validators.ts +1199 -0
- package/src/eval/weighted-scorer.ts +285 -0
- package/src/index.ts +17 -0
- package/src/league/README.md +188 -0
- package/src/league/health-check.ts +353 -0
- package/src/league/index.ts +93 -0
- package/src/league/killAgent.ts +151 -0
- package/src/league/league.test.ts +1151 -0
- package/src/league/runLeague.ts +843 -0
- package/src/league/scoreAgent.ts +175 -0
- package/src/modules/omnibridge/__tests__/.gitkeep +1 -0
- package/src/modules/omnibridge/__tests__/auth-tunnel.property.test.ts +524 -0
- package/src/modules/omnibridge/__tests__/deterministic-logger.property.test.ts +965 -0
- package/src/modules/omnibridge/__tests__/ghost-api.property.test.ts +461 -0
- package/src/modules/omnibridge/__tests__/omnibridge-integration.test.ts +542 -0
- package/src/modules/omnibridge/__tests__/parallel-executor.property.test.ts +671 -0
- package/src/modules/omnibridge/__tests__/semantic-normalizer.property.test.ts +521 -0
- package/src/modules/omnibridge/__tests__/semantic-normalizer.test.ts +254 -0
- package/src/modules/omnibridge/__tests__/session-vault.property.test.ts +367 -0
- package/src/modules/omnibridge/__tests__/shadow-session.property.test.ts +523 -0
- package/src/modules/omnibridge/__tests__/triangulation-engine.property.test.ts +292 -0
- package/src/modules/omnibridge/__tests__/verification-engine.property.test.ts +769 -0
- package/src/modules/omnibridge/api/.gitkeep +1 -0
- package/src/modules/omnibridge/api/ghost-api.ts +1087 -0
- package/src/modules/omnibridge/auth/.gitkeep +1 -0
- package/src/modules/omnibridge/auth/auth-tunnel.ts +843 -0
- package/src/modules/omnibridge/auth/session-vault.ts +577 -0
- package/src/modules/omnibridge/core/.gitkeep +1 -0
- package/src/modules/omnibridge/core/semantic-normalizer.ts +702 -0
- package/src/modules/omnibridge/core/triangulation-engine.ts +530 -0
- package/src/modules/omnibridge/core/types.ts +610 -0
- package/src/modules/omnibridge/execution/.gitkeep +1 -0
- package/src/modules/omnibridge/execution/deterministic-logger.ts +629 -0
- package/src/modules/omnibridge/execution/parallel-executor.ts +542 -0
- package/src/modules/omnibridge/execution/shadow-session.ts +794 -0
- package/src/modules/omnibridge/index.ts +212 -0
- package/src/modules/omnibridge/omnibridge.ts +510 -0
- package/src/modules/omnibridge/verification/.gitkeep +1 -0
- package/src/modules/omnibridge/verification/verification-engine.ts +783 -0
- package/src/outcomes/README.md +75 -0
- package/src/outcomes/acquire-pilot-customer.ts +297 -0
- package/src/outcomes/code-delivery-outcomes.ts +89 -0
- package/src/outcomes/code-outcomes.ts +256 -0
- package/src/outcomes/code_review_battle.test.ts +135 -0
- package/src/outcomes/code_review_battle.ts +135 -0
- package/src/outcomes/cold_email_battle.ts +97 -0
- package/src/outcomes/content_creation_battle.ts +160 -0
- package/src/outcomes/f1_stem_opt_compliance.ts +61 -0
- package/src/outcomes/index.ts +107 -0
- package/src/outcomes/lead_gen_battle.test.ts +113 -0
- package/src/outcomes/lead_gen_battle.ts +99 -0
- package/src/outcomes/outcome.schema.property.test.ts +229 -0
- package/src/outcomes/outcome.schema.ts +187 -0
- package/src/outcomes/qualified_sales_interest.ts +118 -0
- package/src/outcomes/swarm_planner.property.test.ts +370 -0
- package/src/outcomes/swarm_planner.ts +96 -0
- package/src/outcomes/web_extraction.ts +234 -0
- package/src/runtime/README.md +220 -0
- package/src/runtime/agentRunner.test.ts +341 -0
- package/src/runtime/agentRunner.ts +746 -0
- package/src/runtime/claudeAdapter.ts +232 -0
- package/src/runtime/costTracker.ts +123 -0
- package/src/runtime/index.ts +34 -0
- package/src/runtime/modelAdapter.property.test.ts +305 -0
- package/src/runtime/modelAdapter.ts +144 -0
- package/src/runtime/openaiAdapter.ts +235 -0
- package/src/utils/README.md +122 -0
- package/src/utils/command-runner.ts +134 -0
- package/src/utils/cost-guard.ts +379 -0
- package/src/utils/errors.test.ts +290 -0
- package/src/utils/errors.ts +442 -0
- package/src/utils/index.ts +37 -0
- package/src/utils/logger.test.ts +361 -0
- package/src/utils/logger.ts +419 -0
- 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
|
+
});
|