@inixiative/foundry-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.
- package/LICENSE +21 -0
- package/package.json +39 -0
- package/src/action-prompt.ts +248 -0
- package/src/adapters/file-memory.ts +718 -0
- package/src/adapters/http-memory.ts +141 -0
- package/src/adapters/index.ts +4 -0
- package/src/adapters/markdown-docs.ts +213 -0
- package/src/adapters/sqlite-memory.ts +188 -0
- package/src/base-agent.ts +137 -0
- package/src/bounded-set.ts +42 -0
- package/src/cache-lifecycle.ts +187 -0
- package/src/capability.ts +252 -0
- package/src/classifier.ts +43 -0
- package/src/context-layer.ts +706 -0
- package/src/context-stack.ts +223 -0
- package/src/credentials.ts +14 -0
- package/src/decider.ts +62 -0
- package/src/delivery-evidence.ts +85 -0
- package/src/event-stream.ts +105 -0
- package/src/executor.ts +185 -0
- package/src/harness.ts +749 -0
- package/src/hooks.ts +133 -0
- package/src/hydrator.ts +148 -0
- package/src/id.ts +39 -0
- package/src/index.ts +273 -0
- package/src/intervention.ts +134 -0
- package/src/messages.ts +315 -0
- package/src/middleware.ts +118 -0
- package/src/native-evidence.ts +140 -0
- package/src/permission-middleware.ts +92 -0
- package/src/retry.ts +91 -0
- package/src/router.ts +41 -0
- package/src/scope.ts +20 -0
- package/src/signal.ts +117 -0
- package/src/thread.ts +528 -0
- package/src/token-counts.ts +30 -0
- package/src/token-tracker.ts +433 -0
- package/src/tools.ts +714 -0
- package/src/trace.ts +261 -0
- package/src/types.ts +167 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { type TokenCounts, sumTokenCounts, totalTokenCount } from "./token-counts";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Token & Cost Tracking
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Accumulated token usage with estimated cost.
|
|
9
|
+
*/
|
|
10
|
+
export interface TokenUsage extends TokenCounts {
|
|
11
|
+
readonly input: number;
|
|
12
|
+
readonly output: number;
|
|
13
|
+
readonly total: number;
|
|
14
|
+
readonly estimatedCost: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A single recorded usage entry — one LLM call.
|
|
19
|
+
*/
|
|
20
|
+
export interface UsageEntry {
|
|
21
|
+
readonly timestamp: number;
|
|
22
|
+
readonly provider: string;
|
|
23
|
+
readonly model: string;
|
|
24
|
+
readonly agentId?: string;
|
|
25
|
+
readonly threadId?: string;
|
|
26
|
+
readonly spanId?: string;
|
|
27
|
+
readonly tokens: Readonly<TokenCounts>;
|
|
28
|
+
readonly cost: number;
|
|
29
|
+
/** False means cost is an unpriced subtotal, not a zero-price acknowledgment. */
|
|
30
|
+
readonly costKnown?: boolean;
|
|
31
|
+
readonly cached?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Per-model pricing: cost in dollars per 1 million tokens.
|
|
36
|
+
*/
|
|
37
|
+
export interface ModelPricing {
|
|
38
|
+
readonly inputPer1M: number;
|
|
39
|
+
readonly outputPer1M: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Nested cost table: provider -> model -> pricing.
|
|
44
|
+
*/
|
|
45
|
+
export interface CostTable {
|
|
46
|
+
readonly [provider: string]: {
|
|
47
|
+
readonly [model: string]: ModelPricing;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Budget enforcement configuration.
|
|
53
|
+
*/
|
|
54
|
+
export interface BudgetConfig {
|
|
55
|
+
/** Maximum total tokens (input + output + cache reads/writes) before budget is exceeded. */
|
|
56
|
+
readonly maxTokens?: number;
|
|
57
|
+
/** Maximum cost in dollars before budget is exceeded. */
|
|
58
|
+
readonly maxCost?: number;
|
|
59
|
+
/** Percentage (0-1) of budget at which to emit a warning. Default 0.8. */
|
|
60
|
+
readonly warnAt?: number;
|
|
61
|
+
/** Percentage (0-1) of budget at which to throw / halt. Default 1.0. */
|
|
62
|
+
readonly haltAt?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Current budget status snapshot.
|
|
67
|
+
*/
|
|
68
|
+
export interface BudgetStatus {
|
|
69
|
+
readonly usedTokens: number;
|
|
70
|
+
readonly usedCost: number;
|
|
71
|
+
readonly limitTokens: number | undefined;
|
|
72
|
+
readonly limitCost: number | undefined;
|
|
73
|
+
readonly percentage: number;
|
|
74
|
+
readonly warning: boolean;
|
|
75
|
+
readonly exceeded: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Breakdown row used inside UsageSummary.
|
|
80
|
+
*/
|
|
81
|
+
export interface UsageBreakdown extends TokenCounts {
|
|
82
|
+
readonly key: string;
|
|
83
|
+
readonly input: number;
|
|
84
|
+
readonly output: number;
|
|
85
|
+
readonly total: number;
|
|
86
|
+
readonly cost: number;
|
|
87
|
+
readonly calls: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Full summary across all dimensions.
|
|
92
|
+
*/
|
|
93
|
+
export interface UsageSummary {
|
|
94
|
+
/** Disjoint input/output/cache totals; tags live on recent entries. */
|
|
95
|
+
readonly tokens: Readonly<TokenCounts>;
|
|
96
|
+
readonly totalInput: number;
|
|
97
|
+
readonly totalOutput: number;
|
|
98
|
+
readonly totalTokens: number;
|
|
99
|
+
readonly totalCost: number;
|
|
100
|
+
readonly totalCalls: number;
|
|
101
|
+
readonly byProvider: ReadonlyArray<UsageBreakdown>;
|
|
102
|
+
readonly byModel: ReadonlyArray<UsageBreakdown>;
|
|
103
|
+
readonly byAgent: ReadonlyArray<UsageBreakdown>;
|
|
104
|
+
readonly byThread: ReadonlyArray<UsageBreakdown>;
|
|
105
|
+
readonly budget: BudgetStatus;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Default cost table (approximate pricing as of early 2026)
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
export const DEFAULT_COST_TABLE: CostTable = {
|
|
113
|
+
anthropic: {
|
|
114
|
+
"claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25 },
|
|
115
|
+
"claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25 },
|
|
116
|
+
"claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15 },
|
|
117
|
+
"claude-haiku-4-5-20251001": { inputPer1M: 1, outputPer1M: 5 },
|
|
118
|
+
},
|
|
119
|
+
openai: {
|
|
120
|
+
"gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15 },
|
|
121
|
+
"gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5 },
|
|
122
|
+
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14 },
|
|
123
|
+
"o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4 }, // retired Feb 2026 — kept for historical cost tracking
|
|
124
|
+
},
|
|
125
|
+
gemini: {
|
|
126
|
+
"gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12 },
|
|
127
|
+
"gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3 },
|
|
128
|
+
"gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5 },
|
|
129
|
+
},
|
|
130
|
+
// Claude Code provider tracks usage via session output, not cost table
|
|
131
|
+
"claude-code": {},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Token estimation
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
/** Heuristic patterns that indicate source code rather than prose. */
|
|
139
|
+
const CODE_SIGNALS = /[{}();=]|=>|function\s|import\s|export\s|const\s|let\s|var\s|class\s|def\s|return\s/;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Estimate the number of tokens in `text` without a full tokenizer.
|
|
143
|
+
*
|
|
144
|
+
* Strategy:
|
|
145
|
+
* - Detect whether the text is predominantly code or natural language.
|
|
146
|
+
* - For code, use ~0.4 tokens per character (code is denser).
|
|
147
|
+
* - For prose, use ~0.75 tokens per whitespace-delimited word.
|
|
148
|
+
* - Falls back gracefully for empty / tiny strings.
|
|
149
|
+
*
|
|
150
|
+
* This is intentionally fast — no WASM, no external deps — but
|
|
151
|
+
* meaningfully more accurate than the naive `Math.ceil(len / 4)`.
|
|
152
|
+
*/
|
|
153
|
+
export function estimateTokens(text: string, _model?: string): number {
|
|
154
|
+
if (!text) return 0;
|
|
155
|
+
|
|
156
|
+
const trimmed = text.trim();
|
|
157
|
+
if (trimmed.length === 0) return 0;
|
|
158
|
+
|
|
159
|
+
// Sample up to the first 2000 chars for the code-detection heuristic
|
|
160
|
+
const sample = trimmed.slice(0, 2000);
|
|
161
|
+
const isCode = CODE_SIGNALS.test(sample);
|
|
162
|
+
|
|
163
|
+
if (isCode) {
|
|
164
|
+
// Code: roughly 0.4 tokens per character
|
|
165
|
+
return Math.max(1, Math.ceil(trimmed.length * 0.4));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Prose: roughly 0.75 tokens per word
|
|
169
|
+
const words = trimmed.split(/\s+/).length;
|
|
170
|
+
return Math.max(1, Math.ceil(words * 0.75));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// TokenTracker
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
type EventHandler = (status: BudgetStatus) => void;
|
|
178
|
+
|
|
179
|
+
export class TokenTracker {
|
|
180
|
+
private readonly _entries: UsageEntry[] = [];
|
|
181
|
+
private readonly _costTable: CostTable;
|
|
182
|
+
private readonly _budget: BudgetConfig;
|
|
183
|
+
private readonly _warningHandlers: EventHandler[] = [];
|
|
184
|
+
private readonly _exceededHandlers: EventHandler[] = [];
|
|
185
|
+
|
|
186
|
+
/** Tracks whether a warning has already been emitted for this budget cycle. */
|
|
187
|
+
private _warningEmitted = false;
|
|
188
|
+
|
|
189
|
+
constructor(opts?: { costTable?: CostTable; budget?: BudgetConfig }) {
|
|
190
|
+
this._costTable = opts?.costTable ?? DEFAULT_COST_TABLE;
|
|
191
|
+
this._budget = opts?.budget ?? {};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// -----------------------------------------------------------------------
|
|
195
|
+
// Recording
|
|
196
|
+
// -----------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Record a token usage entry. Automatically computes cost from the cost
|
|
200
|
+
* table and timestamps the entry. Returns the full entry.
|
|
201
|
+
*
|
|
202
|
+
* Throws if budget.haltAt is exceeded (when configured).
|
|
203
|
+
*/
|
|
204
|
+
record(
|
|
205
|
+
entry: Omit<UsageEntry, "timestamp" | "cost">
|
|
206
|
+
): UsageEntry {
|
|
207
|
+
const cost = this._computeCost(
|
|
208
|
+
entry.provider,
|
|
209
|
+
entry.model,
|
|
210
|
+
entry.tokens.input,
|
|
211
|
+
entry.tokens.output,
|
|
212
|
+
entry.cached
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const full: UsageEntry = {
|
|
216
|
+
...entry,
|
|
217
|
+
timestamp: Date.now(),
|
|
218
|
+
cost,
|
|
219
|
+
costKnown: !!this._costTable[entry.provider]?.[entry.model],
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
this._entries.push(full);
|
|
223
|
+
this._checkBudget();
|
|
224
|
+
return full;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// -----------------------------------------------------------------------
|
|
228
|
+
// Queries
|
|
229
|
+
// -----------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
get totalTokens(): TokenUsage {
|
|
232
|
+
return this._aggregate(this._entries);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
byProvider(provider: string): TokenUsage {
|
|
236
|
+
return this._aggregate(this._entries.filter((e) => e.provider === provider));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
byModel(model: string): TokenUsage {
|
|
240
|
+
return this._aggregate(this._entries.filter((e) => e.model === model));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
byAgent(agentId: string): TokenUsage {
|
|
244
|
+
return this._aggregate(this._entries.filter((e) => e.agentId === agentId));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
byThread(threadId: string): TokenUsage {
|
|
248
|
+
return this._aggregate(this._entries.filter((e) => e.threadId === threadId));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// -----------------------------------------------------------------------
|
|
252
|
+
// Budget
|
|
253
|
+
// -----------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
get budgetStatus(): BudgetStatus {
|
|
256
|
+
return this._buildBudgetStatus();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// -----------------------------------------------------------------------
|
|
260
|
+
// History & summary
|
|
261
|
+
// -----------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
/** Return the most recent entries (default 50). */
|
|
264
|
+
recent(limit = 50): ReadonlyArray<UsageEntry> {
|
|
265
|
+
return this._entries.slice(-limit);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Full usage summary across all dimensions. */
|
|
269
|
+
summary(): UsageSummary {
|
|
270
|
+
const tokens = sumTokenCounts(this._entries.map(e => e.tokens));
|
|
271
|
+
return {
|
|
272
|
+
tokens,
|
|
273
|
+
totalInput: tokens.input,
|
|
274
|
+
totalOutput: tokens.output,
|
|
275
|
+
totalTokens: totalTokenCount(tokens),
|
|
276
|
+
totalCost: this._entries.reduce((s, e) => s + e.cost, 0),
|
|
277
|
+
totalCalls: this._entries.length,
|
|
278
|
+
byProvider: this._groupBy("provider"),
|
|
279
|
+
byModel: this._groupBy("model"),
|
|
280
|
+
byAgent: this._groupBy("agentId"),
|
|
281
|
+
byThread: this._groupBy("threadId"),
|
|
282
|
+
budget: this._buildBudgetStatus(),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Clear all recorded entries and reset warning state. */
|
|
287
|
+
reset(): void {
|
|
288
|
+
this._entries.length = 0;
|
|
289
|
+
this._warningEmitted = false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// -----------------------------------------------------------------------
|
|
293
|
+
// Events
|
|
294
|
+
// -----------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
/** Subscribe to budget warnings. Returns an unsubscribe function. */
|
|
297
|
+
onWarning(handler: EventHandler): () => void {
|
|
298
|
+
this._warningHandlers.push(handler);
|
|
299
|
+
return () => {
|
|
300
|
+
const idx = this._warningHandlers.indexOf(handler);
|
|
301
|
+
if (idx !== -1) this._warningHandlers.splice(idx, 1);
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Subscribe to budget exceeded events. Returns an unsubscribe function. */
|
|
306
|
+
onExceeded(handler: EventHandler): () => void {
|
|
307
|
+
this._exceededHandlers.push(handler);
|
|
308
|
+
return () => {
|
|
309
|
+
const idx = this._exceededHandlers.indexOf(handler);
|
|
310
|
+
if (idx !== -1) this._exceededHandlers.splice(idx, 1);
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// -----------------------------------------------------------------------
|
|
315
|
+
// Private helpers
|
|
316
|
+
// -----------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
private _computeCost(
|
|
319
|
+
provider: string,
|
|
320
|
+
model: string,
|
|
321
|
+
input: number,
|
|
322
|
+
output: number,
|
|
323
|
+
cached?: boolean
|
|
324
|
+
): number {
|
|
325
|
+
const pricing = this._costTable[provider]?.[model];
|
|
326
|
+
if (!pricing) return 0;
|
|
327
|
+
|
|
328
|
+
// Cached input tokens are typically free or heavily discounted
|
|
329
|
+
const effectiveInput = cached ? input * 0.1 : input;
|
|
330
|
+
return (
|
|
331
|
+
(effectiveInput / 1_000_000) * pricing.inputPer1M +
|
|
332
|
+
(output / 1_000_000) * pricing.outputPer1M
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private _aggregate(entries: UsageEntry[]): TokenUsage {
|
|
337
|
+
const tokens = sumTokenCounts(entries.map(e => e.tokens));
|
|
338
|
+
return { ...tokens, total: totalTokenCount(tokens), estimatedCost: entries.reduce((sum, e) => sum + e.cost, 0) };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private _groupBy(
|
|
342
|
+
field: "provider" | "model" | "agentId" | "threadId"
|
|
343
|
+
): UsageBreakdown[] {
|
|
344
|
+
const groups = new Map<string, UsageEntry[]>();
|
|
345
|
+
for (const entry of this._entries) {
|
|
346
|
+
const key = entry[field];
|
|
347
|
+
if (key == null) continue;
|
|
348
|
+
const group = groups.get(key) ?? [];
|
|
349
|
+
group.push(entry);
|
|
350
|
+
groups.set(key, group);
|
|
351
|
+
}
|
|
352
|
+
return [...groups].map(([key, entries]) => {
|
|
353
|
+
const { estimatedCost, ...tokens } = this._aggregate(entries);
|
|
354
|
+
return { key, ...tokens, cost: estimatedCost, calls: entries.length };
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private _buildBudgetStatus(): BudgetStatus {
|
|
359
|
+
const usedTokens = totalTokenCount(sumTokenCounts(this._entries.map(e => e.tokens)));
|
|
360
|
+
const usedCost = this._entries.reduce((s, e) => s + e.cost, 0);
|
|
361
|
+
|
|
362
|
+
const limitTokens = this._budget.maxTokens;
|
|
363
|
+
const limitCost = this._budget.maxCost;
|
|
364
|
+
|
|
365
|
+
// Calculate percentage based on whichever limit is closer to being hit
|
|
366
|
+
let percentage = 0;
|
|
367
|
+
if (limitTokens != null && limitTokens > 0) {
|
|
368
|
+
percentage = Math.max(percentage, usedTokens / limitTokens);
|
|
369
|
+
}
|
|
370
|
+
if (limitCost != null && limitCost > 0) {
|
|
371
|
+
percentage = Math.max(percentage, usedCost / limitCost);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const warnAt = this._budget.warnAt ?? 0.8;
|
|
375
|
+
const haltAt = this._budget.haltAt ?? 1.0;
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
usedTokens,
|
|
379
|
+
usedCost,
|
|
380
|
+
limitTokens,
|
|
381
|
+
limitCost,
|
|
382
|
+
percentage,
|
|
383
|
+
warning: percentage >= warnAt,
|
|
384
|
+
exceeded: percentage >= haltAt,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private _checkBudget(): void {
|
|
389
|
+
const status = this._buildBudgetStatus();
|
|
390
|
+
|
|
391
|
+
if (status.warning && !this._warningEmitted) {
|
|
392
|
+
this._warningEmitted = true;
|
|
393
|
+
for (const handler of [...this._warningHandlers]) {
|
|
394
|
+
try {
|
|
395
|
+
handler(status);
|
|
396
|
+
} catch {
|
|
397
|
+
// Don't let one bad handler break recording
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (status.exceeded) {
|
|
403
|
+
for (const handler of [...this._exceededHandlers]) {
|
|
404
|
+
try {
|
|
405
|
+
handler(status);
|
|
406
|
+
} catch {
|
|
407
|
+
// Don't let one bad handler break recording
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
throw new BudgetExceededError(status);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Thrown when token/cost budget is exceeded (at haltAt threshold).
|
|
417
|
+
*/
|
|
418
|
+
export class BudgetExceededError extends Error {
|
|
419
|
+
readonly status: BudgetStatus;
|
|
420
|
+
|
|
421
|
+
constructor(status: BudgetStatus) {
|
|
422
|
+
const parts: string[] = [];
|
|
423
|
+
if (status.limitTokens != null) {
|
|
424
|
+
parts.push(`${status.usedTokens}/${status.limitTokens} tokens`);
|
|
425
|
+
}
|
|
426
|
+
if (status.limitCost != null) {
|
|
427
|
+
parts.push(`$${status.usedCost.toFixed(4)}/$${status.limitCost.toFixed(2)}`);
|
|
428
|
+
}
|
|
429
|
+
super(`Budget exceeded: ${parts.join(", ")} (${(status.percentage * 100).toFixed(1)}%)`);
|
|
430
|
+
this.name = "BudgetExceededError";
|
|
431
|
+
this.status = status;
|
|
432
|
+
}
|
|
433
|
+
}
|