@inixiative/foundry-core 0.2.0 → 0.4.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/src/thread.ts CHANGED
@@ -1,20 +1,62 @@
1
- import { ContextStack } from "./context-stack";
2
- import type { LayerFilter, ContextStackView } from "./context-stack";
3
- import { CacheLifecycle } from "./cache-lifecycle";
4
- import { BaseAgent, type ExecutionResult } from "./base-agent";
5
- import { MiddlewareChain, type DispatchContext } from "./middleware";
6
- import { SignalBus } from "./signal";
7
- import type { TokenTracker } from "./token-tracker";
8
- import type { ToolCallObservation } from "./tools";
9
- import type { InjectionArtifact } from "./messages";
10
- import { newId } from "./id";
11
-
12
- export type ThreadStatus = "idle" | "active" | "waiting" | "archived";
1
+ import type { BaseAgent, ExecutionResult } from './base-agent';
2
+ import { CacheLifecycle } from './cache-lifecycle';
3
+ import type { ContextStack, ContextStackView, LayerFilter } from './context-stack';
4
+ import { newId } from './id';
5
+ import type { InjectionArtifact } from './messages';
6
+ import { type DispatchContext, MiddlewareChain } from './middleware';
7
+ import { SignalBus } from './signal';
8
+ import type { TokenTracker } from './token-tracker';
9
+ import type { ToolCallObservation } from './tools';
10
+
11
+ export type ThreadStatus = 'idle' | 'active' | 'waiting' | 'archived';
12
+
13
+ /** A thread name and when it was set. */
14
+ export interface ThreadName {
15
+ readonly text: string;
16
+ readonly updatedAt: number;
17
+ }
18
+
19
+ /** Tracked work a thread links to, in Archive's integration-reference shape (`github` `owner/repo#12`, `linear` `ENG-4`). */
20
+ export interface ThreadReference {
21
+ readonly integration: string;
22
+ readonly ref: string;
23
+ readonly url?: string;
24
+ }
25
+
26
+ /** Where a thread's work lives, as the runtime last observed it. */
27
+ export interface ThreadContext {
28
+ /** `owner/repo` of the worktree's origin. */
29
+ readonly repository?: string;
30
+ /** Branch checked out in the worktree now (may differ from the assigned `branch`). */
31
+ readonly branch?: string;
32
+ /** Worktree root containing the thread's cwd. */
33
+ readonly worktree?: string;
34
+ readonly references: readonly ThreadReference[];
35
+ readonly updatedAt: number;
36
+ }
37
+
38
+ /** The name a thread shows: a person's name overrides the agent-maintained one. */
39
+ export function threadTitle(
40
+ meta: Pick<ThreadMeta, 'name' | 'agentName' | 'description'>,
41
+ ): { text: string; source: 'human' | 'agent' | 'description' } | undefined {
42
+ if (meta.name?.text) return { text: meta.name.text, source: 'human' };
43
+ if (meta.agentName?.text) return { text: meta.agentName.text, source: 'agent' };
44
+ return meta.description ? { text: meta.description, source: 'description' } : undefined;
45
+ }
13
46
 
14
47
  export interface ThreadMeta {
15
48
  /** Living description — what this thread is doing right now. */
16
49
  description: string;
17
50
 
51
+ /** Name a person set. A hard override: agents never change it. */
52
+ name?: ThreadName;
53
+
54
+ /** Name an agent maintains while no person has named the thread. */
55
+ agentName?: ThreadName;
56
+
57
+ /** Branch, worktree and linked PRs/tickets, derived by the runtime. */
58
+ context?: ThreadContext;
59
+
18
60
  /** Classification tags — what kind of work this thread handles. */
19
61
  tags: string[];
20
62
 
@@ -53,7 +95,7 @@ export interface Dispatch<T = unknown> {
53
95
 
54
96
  export interface FanResult {
55
97
  readonly agentId: string;
56
- readonly status: "fulfilled" | "rejected";
98
+ readonly status: 'fulfilled' | 'rejected';
57
99
  readonly result?: ExecutionResult;
58
100
  readonly error?: unknown;
59
101
  }
@@ -69,8 +111,10 @@ export interface BackgroundHandle {
69
111
  * observation signal; the thread never invents them.
70
112
  */
71
113
  export interface DispatchOptions {
72
- nativeObservation?: Omit<import("./native-evidence").NativeObservation, "owner"> & { generation: string };
73
- recordNative?: (evidence: import("./native-evidence").NativeEvidence) => void;
114
+ nativeObservation?: Omit<import('./native-evidence').NativeObservation, 'owner'> & {
115
+ generation: string;
116
+ };
117
+ recordNative?: (evidence: import('./native-evidence').NativeEvidence) => void;
74
118
  recordCompleted?: (output: unknown) => void;
75
119
  /** Streaming sink for executor text deltas. */
76
120
  onDelta?: (text: string) => void;
@@ -167,9 +211,9 @@ export class Thread {
167
211
 
168
212
  const now = Date.now();
169
213
  this.meta = {
170
- description: opts?.description ?? "",
214
+ description: opts?.description ?? '',
171
215
  tags: opts?.tags ?? [],
172
- status: "idle",
216
+ status: 'idle',
173
217
  cwd: opts?.cwd,
174
218
  branch: opts?.branch,
175
219
  parentThreadId: opts?.parentThreadId,
@@ -204,6 +248,13 @@ export class Thread {
204
248
  this.meta.description = description;
205
249
  }
206
250
 
251
+ /** Set (or with an empty name, clear) the person-given name. */
252
+ rename(name: string): void {
253
+ const text = name.trim();
254
+ if (!text) delete this.meta.name;
255
+ else if (text !== this.meta.name?.text) this.meta.name = { text, updatedAt: Date.now() };
256
+ }
257
+
207
258
  /** Update tags. */
208
259
  tag(...tags: string[]): void {
209
260
  for (const t of tags) {
@@ -256,7 +307,7 @@ export class Thread {
256
307
 
257
308
  /** Archive this thread. Sets the terminal status, then disposes. */
258
309
  archive(): void {
259
- this.meta.status = "archived";
310
+ this.meta.status = 'archived';
260
311
  this.meta.archivedAt = Date.now();
261
312
  this.dispose();
262
313
  }
@@ -277,7 +328,7 @@ export class Thread {
277
328
  // This dispatch's observation and middleware must retain the same logical
278
329
  // identity even if a caller later reuses/mutates its options object.
279
330
  opts = opts ? { ...opts } : undefined;
280
- if (this.meta.status === "archived") {
331
+ if (this.meta.status === 'archived') {
281
332
  throw new Error(`Thread archived: ${this.id} cannot dispatch "${agentId}"`);
282
333
  }
283
334
  if (this._disposed) {
@@ -287,13 +338,13 @@ export class Thread {
287
338
  if (!agent) throw new Error(`Agent not found: ${agentId}`);
288
339
 
289
340
  this._activeDispatches++;
290
- this.meta.status = "active";
341
+ this.meta.status = 'active';
291
342
  this.meta.lastActiveAt = Date.now();
292
343
 
293
344
  // Explicit identity for this dispatch. Middleware sees it, tool calls
294
345
  // executed on its behalf are observed with it, and the completion
295
346
  // observation carries it, so evidence never drifts to another completion.
296
- const dispatchId = newId("dispatch");
347
+ const dispatchId = newId('dispatch');
297
348
  const ctx: DispatchContext<TPayload> = {
298
349
  agentId,
299
350
  payload,
@@ -315,33 +366,52 @@ export class Thread {
315
366
  messageId: ctx.messageId,
316
367
  dispatchId,
317
368
  observeTool: (observation: ToolCallObservation) => {
318
- void this.signals.emit({
319
- id: newId("sig-tool"),
320
- kind: "tool_observation",
321
- source: `thread:${this.id}`,
322
- content: { threadId: this.id, dispatchId, agentId, ...observation, timestamp: Date.now() },
323
- timestamp: Date.now(),
324
- }).catch((err) => {
325
- console.warn(`[Thread] tool observation failed for "${agentId}":`, (err as Error).message ?? err);
326
- });
369
+ void this.signals
370
+ .emit({
371
+ id: newId('sig-tool'),
372
+ kind: 'tool_observation',
373
+ source: `thread:${this.id}`,
374
+ content: {
375
+ threadId: this.id,
376
+ dispatchId,
377
+ agentId,
378
+ ...observation,
379
+ timestamp: Date.now(),
380
+ },
381
+ timestamp: Date.now(),
382
+ })
383
+ .catch((err) => {
384
+ console.warn(
385
+ `[Thread] tool observation failed for "${agentId}":`,
386
+ (err as Error).message ?? err,
387
+ );
388
+ });
327
389
  },
328
390
  annotations: ctx.annotations,
329
391
  onDelta: opts?.onDelta,
330
392
  recordInjection: opts?.recordInjection,
331
393
  recordNative: opts?.recordNative,
332
394
  recordCompleted: opts?.recordCompleted,
333
- nativeObservation: opts?.nativeObservation ? {
334
- preflight: opts.nativeObservation.preflight,
335
- bridge: opts.nativeObservation.bridge,
336
- register: opts.nativeObservation.register, observe: opts.nativeObservation.observe,
337
- owner: Object.freeze({ threadId: this.id, projectId: this.meta.projectId, generation: opts.nativeObservation.generation,
338
- messageId: ctx.messageId, dispatchId }),
339
- } : undefined,
395
+ nativeObservation: opts?.nativeObservation
396
+ ? {
397
+ preflight: opts.nativeObservation.preflight,
398
+ bridge: opts.nativeObservation.bridge,
399
+ register: opts.nativeObservation.register,
400
+ observe: opts.nativeObservation.observe,
401
+ owner: Object.freeze({
402
+ threadId: this.id,
403
+ projectId: this.meta.projectId,
404
+ generation: opts.nativeObservation.generation,
405
+ messageId: ctx.messageId,
406
+ dispatchId,
407
+ }),
408
+ }
409
+ : undefined,
340
410
  };
341
411
 
342
412
  try {
343
413
  const result = await this.middleware.execute(ctx, () =>
344
- agent.run(payload, filterOverride, meta)
414
+ agent.run(payload, filterOverride, meta),
345
415
  );
346
416
 
347
417
  const durationMs = performance.now() - start;
@@ -350,8 +420,8 @@ export class Thread {
350
420
  if (this._tokenTracker && result.tokens) {
351
421
  const llm = agent.llm;
352
422
  this._tokenTracker.record({
353
- provider: llm?.provider ?? "unknown",
354
- model: llm?.model ?? "unknown",
423
+ provider: llm?.provider ?? 'unknown',
424
+ model: llm?.model ?? 'unknown',
355
425
  agentId,
356
426
  threadId: this.id,
357
427
  tokens: result.tokens,
@@ -389,8 +459,8 @@ export class Thread {
389
459
  this._activeDispatches--;
390
460
  // Only the last finishing dispatch returns the thread to idle, and a
391
461
  // late completion must not resurrect an archived (or paused) thread.
392
- if (this._activeDispatches === 0 && this.meta.status === "active") {
393
- this.meta.status = "idle";
462
+ if (this._activeDispatches === 0 && this.meta.status === 'active') {
463
+ this.meta.status = 'idle';
394
464
  }
395
465
  }
396
466
  }
@@ -405,12 +475,10 @@ export class Thread {
405
475
  filterOverride?: LayerFilter,
406
476
  opts?: DispatchOptions,
407
477
  ): BackgroundHandle {
408
- const promise = this.dispatch(agentId, payload, filterOverride, opts).catch(
409
- async (err) => {
410
- console.warn(`[Thread] background dispatch "${agentId}" failed:`, (err as Error).message);
411
- return { output: null, contextHash: "" } as ExecutionResult;
412
- }
413
- );
478
+ const promise = this.dispatch(agentId, payload, filterOverride, opts).catch(async (err) => {
479
+ console.warn(`[Thread] background dispatch "${agentId}" failed:`, (err as Error).message);
480
+ return { output: null, contextHash: '' } as ExecutionResult;
481
+ });
414
482
  return { agentId, promise };
415
483
  }
416
484
 
@@ -425,14 +493,14 @@ export class Thread {
425
493
  opts?: DispatchOptions,
426
494
  ): Promise<FanResult[]> {
427
495
  const settled = await Promise.allSettled(
428
- agentIds.map((id) => this.dispatch(id, payload, filterOverride, opts))
496
+ agentIds.map((id) => this.dispatch(id, payload, filterOverride, opts)),
429
497
  );
430
498
 
431
499
  return settled.map((s, i) => {
432
- if (s.status === "fulfilled") {
433
- return { agentId: agentIds[i], status: "fulfilled" as const, result: s.value };
500
+ if (s.status === 'fulfilled') {
501
+ return { agentId: agentIds[i], status: 'fulfilled' as const, result: s.value };
434
502
  } else {
435
- return { agentId: agentIds[i], status: "rejected" as const, error: s.reason };
503
+ return { agentId: agentIds[i], status: 'rejected' as const, error: s.reason };
436
504
  }
437
505
  });
438
506
  }
@@ -444,7 +512,7 @@ export class Thread {
444
512
  payload: unknown,
445
513
  opts: DispatchOptions | undefined,
446
514
  dispatchId: string,
447
- outcome: Pick<DispatchObservation, "durationMs" | "ok" | "error" | "contextHash" | "output">,
515
+ outcome: Pick<DispatchObservation, 'durationMs' | 'ok' | 'error' | 'contextHash' | 'output'>,
448
516
  ): Promise<void> {
449
517
  const content: DispatchObservation = {
450
518
  threadId: this.id,
@@ -458,14 +526,17 @@ export class Thread {
458
526
  };
459
527
  try {
460
528
  await this.signals.emit({
461
- id: newId("sig-dispatch"),
462
- kind: "dispatch",
529
+ id: newId('sig-dispatch'),
530
+ kind: 'dispatch',
463
531
  source: opts?.role ? `harness:${agentId}` : `thread:${this.id}`,
464
532
  content,
465
533
  timestamp: Date.now(),
466
534
  });
467
535
  } catch (err) {
468
- console.warn(`[Thread] dispatch observation failed for "${agentId}":`, (err as Error).message ?? err);
536
+ console.warn(
537
+ `[Thread] dispatch observation failed for "${agentId}":`,
538
+ (err as Error).message ?? err,
539
+ );
469
540
  }
470
541
  }
471
542
 
@@ -481,11 +552,15 @@ export class Thread {
481
552
  },
482
553
  getContent: (id) => {
483
554
  const l = stack.getLayer(id);
484
- return l?.isWarm ? l.content : "";
555
+ return l?.isWarm ? l.content : '';
485
556
  },
486
557
  getState: (id) => stack.getLayer(id)?.state,
487
- get layerIds() { return stack.layers.map((l) => l.id); },
488
- get estimatedTokens() { return stack.estimateTokens(); },
558
+ get layerIds() {
559
+ return stack.layers.map((l) => l.id);
560
+ },
561
+ get estimatedTokens() {
562
+ return stack.estimateTokens();
563
+ },
489
564
  };
490
565
  }
491
566
 
@@ -502,7 +577,9 @@ export class Thread {
502
577
  */
503
578
  start(): void {
504
579
  if (this._disposed) {
505
- throw new Error(`Thread disposed: ${this.id} cannot be started; create a new thread to restore it`);
580
+ throw new Error(
581
+ `Thread disposed: ${this.id} cannot be started; create a new thread to restore it`,
582
+ );
506
583
  }
507
584
  this.lifecycle.start();
508
585
  }
@@ -515,7 +592,7 @@ export class Thread {
515
592
  /** Serialize a dispatch payload for observation, bounded in size. */
516
593
  function serializePayload(payload: unknown): string {
517
594
  let text: string;
518
- if (typeof payload === "string") {
595
+ if (typeof payload === 'string') {
519
596
  text = payload;
520
597
  } else {
521
598
  try {
@@ -21,7 +21,15 @@ export function totalTokenCount(tokens: TokenCounts): number {
21
21
  export function sumTokenCounts(entries: Iterable<TokenCounts>): TokenCounts {
22
22
  const total: TokenCounts = { input: 0, output: 0 };
23
23
  for (const entry of entries) {
24
- for (const key of ["input", "output", "cacheRead", "cacheWrite", "cacheWrite5m", "cacheWrite1h", "thinking"] as const) {
24
+ for (const key of [
25
+ 'input',
26
+ 'output',
27
+ 'cacheRead',
28
+ 'cacheWrite',
29
+ 'cacheWrite5m',
30
+ 'cacheWrite1h',
31
+ 'thinking',
32
+ ] as const) {
25
33
  const value = entry[key];
26
34
  if (value !== undefined) total[key] = (total[key] ?? 0) + value;
27
35
  }
@@ -1,4 +1,4 @@
1
- import { type TokenCounts, sumTokenCounts, totalTokenCount } from "./token-counts";
1
+ import { sumTokenCounts, type TokenCounts, totalTokenCount } from './token-counts';
2
2
 
3
3
  // ---------------------------------------------------------------------------
4
4
  // Token & Cost Tracking
@@ -111,24 +111,33 @@ export interface UsageSummary {
111
111
 
112
112
  export const DEFAULT_COST_TABLE: CostTable = {
113
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 },
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
118
  },
119
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
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
124
  },
125
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 },
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
+ meta: {
131
+ // The -contributor ids are the same models, ~21x cheaper because Meta trains
132
+ // on the traffic. Tracking both makes the saving show up next to what bought it.
133
+ 'muse-spark-1.3': { inputPer1M: 1.25, outputPer1M: 4.25 },
134
+ 'muse-spark-1.3-contributor': { inputPer1M: 0.1, outputPer1M: 0.2 },
135
+ 'muse-spark-1.2': { inputPer1M: 1.25, outputPer1M: 4.25 },
136
+ 'muse-spark-1.2-contributor': { inputPer1M: 0.1, outputPer1M: 0.2 },
137
+ 'muse-spark-1.1': { inputPer1M: 1.25, outputPer1M: 4.25 },
129
138
  },
130
139
  // Claude Code provider tracks usage via session output, not cost table
131
- "claude-code": {},
140
+ 'claude-code': {},
132
141
  };
133
142
 
134
143
  // ---------------------------------------------------------------------------
@@ -136,7 +145,8 @@ export const DEFAULT_COST_TABLE: CostTable = {
136
145
  // ---------------------------------------------------------------------------
137
146
 
138
147
  /** 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/;
148
+ const CODE_SIGNALS =
149
+ /[{}();=]|=>|function\s|import\s|export\s|const\s|let\s|var\s|class\s|def\s|return\s/;
140
150
 
141
151
  /**
142
152
  * Estimate the number of tokens in `text` without a full tokenizer.
@@ -201,15 +211,13 @@ export class TokenTracker {
201
211
  *
202
212
  * Throws if budget.haltAt is exceeded (when configured).
203
213
  */
204
- record(
205
- entry: Omit<UsageEntry, "timestamp" | "cost">
206
- ): UsageEntry {
214
+ record(entry: Omit<UsageEntry, 'timestamp' | 'cost'>): UsageEntry {
207
215
  const cost = this._computeCost(
208
216
  entry.provider,
209
217
  entry.model,
210
218
  entry.tokens.input,
211
219
  entry.tokens.output,
212
- entry.cached
220
+ entry.cached,
213
221
  );
214
222
 
215
223
  const full: UsageEntry = {
@@ -267,7 +275,7 @@ export class TokenTracker {
267
275
 
268
276
  /** Full usage summary across all dimensions. */
269
277
  summary(): UsageSummary {
270
- const tokens = sumTokenCounts(this._entries.map(e => e.tokens));
278
+ const tokens = sumTokenCounts(this._entries.map((e) => e.tokens));
271
279
  return {
272
280
  tokens,
273
281
  totalInput: tokens.input,
@@ -275,10 +283,10 @@ export class TokenTracker {
275
283
  totalTokens: totalTokenCount(tokens),
276
284
  totalCost: this._entries.reduce((s, e) => s + e.cost, 0),
277
285
  totalCalls: this._entries.length,
278
- byProvider: this._groupBy("provider"),
279
- byModel: this._groupBy("model"),
280
- byAgent: this._groupBy("agentId"),
281
- byThread: this._groupBy("threadId"),
286
+ byProvider: this._groupBy('provider'),
287
+ byModel: this._groupBy('model'),
288
+ byAgent: this._groupBy('agentId'),
289
+ byThread: this._groupBy('threadId'),
282
290
  budget: this._buildBudgetStatus(),
283
291
  };
284
292
  }
@@ -320,7 +328,7 @@ export class TokenTracker {
320
328
  model: string,
321
329
  input: number,
322
330
  output: number,
323
- cached?: boolean
331
+ cached?: boolean,
324
332
  ): number {
325
333
  const pricing = this._costTable[provider]?.[model];
326
334
  if (!pricing) return 0;
@@ -328,19 +336,20 @@ export class TokenTracker {
328
336
  // Cached input tokens are typically free or heavily discounted
329
337
  const effectiveInput = cached ? input * 0.1 : input;
330
338
  return (
331
- (effectiveInput / 1_000_000) * pricing.inputPer1M +
332
- (output / 1_000_000) * pricing.outputPer1M
339
+ (effectiveInput / 1_000_000) * pricing.inputPer1M + (output / 1_000_000) * pricing.outputPer1M
333
340
  );
334
341
  }
335
342
 
336
343
  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) };
344
+ const tokens = sumTokenCounts(entries.map((e) => e.tokens));
345
+ return {
346
+ ...tokens,
347
+ total: totalTokenCount(tokens),
348
+ estimatedCost: entries.reduce((sum, e) => sum + e.cost, 0),
349
+ };
339
350
  }
340
351
 
341
- private _groupBy(
342
- field: "provider" | "model" | "agentId" | "threadId"
343
- ): UsageBreakdown[] {
352
+ private _groupBy(field: 'provider' | 'model' | 'agentId' | 'threadId'): UsageBreakdown[] {
344
353
  const groups = new Map<string, UsageEntry[]>();
345
354
  for (const entry of this._entries) {
346
355
  const key = entry[field];
@@ -356,7 +365,7 @@ export class TokenTracker {
356
365
  }
357
366
 
358
367
  private _buildBudgetStatus(): BudgetStatus {
359
- const usedTokens = totalTokenCount(sumTokenCounts(this._entries.map(e => e.tokens)));
368
+ const usedTokens = totalTokenCount(sumTokenCounts(this._entries.map((e) => e.tokens)));
360
369
  const usedCost = this._entries.reduce((s, e) => s + e.cost, 0);
361
370
 
362
371
  const limitTokens = this._budget.maxTokens;
@@ -426,8 +435,8 @@ export class BudgetExceededError extends Error {
426
435
  if (status.limitCost != null) {
427
436
  parts.push(`$${status.usedCost.toFixed(4)}/$${status.limitCost.toFixed(2)}`);
428
437
  }
429
- super(`Budget exceeded: ${parts.join(", ")} (${(status.percentage * 100).toFixed(1)}%)`);
430
- this.name = "BudgetExceededError";
438
+ super(`Budget exceeded: ${parts.join(', ')} (${(status.percentage * 100).toFixed(1)}%)`);
439
+ this.name = 'BudgetExceededError';
431
440
  this.status = status;
432
441
  }
433
442
  }