@inixiative/foundry-core 0.2.0 → 0.3.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/package.json +1 -6
- package/src/action-prompt.ts +47 -28
- package/src/adapters/file-memory.ts +314 -122
- package/src/adapters/http-memory.ts +19 -24
- package/src/adapters/index.ts +4 -4
- package/src/adapters/markdown-docs.ts +15 -15
- package/src/adapters/sqlite-memory.ts +29 -27
- package/src/base-agent.ts +5 -7
- package/src/cache-lifecycle.ts +14 -14
- package/src/capability.ts +76 -83
- package/src/clarifier.ts +46 -0
- package/src/classifier.ts +6 -9
- package/src/context-layer.ts +125 -78
- package/src/context-stack.ts +15 -14
- package/src/credentials.ts +4 -1
- package/src/decider.ts +7 -8
- package/src/delivery-evidence.ts +206 -55
- package/src/event-stream.ts +34 -20
- package/src/executor.ts +81 -43
- package/src/harness.ts +225 -126
- package/src/hooks.ts +26 -26
- package/src/hydrator.ts +6 -8
- package/src/id.ts +3 -3
- package/src/index.ts +248 -218
- package/src/intervention.ts +21 -12
- package/src/messages.ts +73 -62
- package/src/middleware.ts +9 -19
- package/src/model-registry.ts +1587 -0
- package/src/native-evidence.ts +124 -30
- package/src/permission-middleware.ts +16 -28
- package/src/retry.ts +6 -6
- package/src/router.ts +5 -5
- package/src/signal.ts +16 -11
- package/src/thread.ts +138 -61
- package/src/token-counts.ts +9 -1
- package/src/token-tracker.ts +43 -34
- package/src/tools.ts +138 -95
- package/src/trace.ts +44 -23
- package/src/types.ts +33 -24
package/src/thread.ts
CHANGED
|
@@ -1,20 +1,62 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import type {
|
|
9
|
-
import type {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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:
|
|
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(
|
|
73
|
-
|
|
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:
|
|
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 =
|
|
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 ===
|
|
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 =
|
|
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(
|
|
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
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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 ??
|
|
354
|
-
model: llm?.model ??
|
|
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 ===
|
|
393
|
-
this.meta.status =
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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 ===
|
|
433
|
-
return { agentId: agentIds[i], status:
|
|
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:
|
|
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,
|
|
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(
|
|
462
|
-
kind:
|
|
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(
|
|
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() {
|
|
488
|
-
|
|
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(
|
|
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 ===
|
|
595
|
+
if (typeof payload === 'string') {
|
|
519
596
|
text = payload;
|
|
520
597
|
} else {
|
|
521
598
|
try {
|
package/src/token-counts.ts
CHANGED
|
@@ -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 [
|
|
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
|
}
|
package/src/token-tracker.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type TokenCounts,
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
279
|
-
byModel: this._groupBy(
|
|
280
|
-
byAgent: this._groupBy(
|
|
281
|
-
byThread: this._groupBy(
|
|
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 {
|
|
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(
|
|
430
|
-
this.name =
|
|
438
|
+
super(`Budget exceeded: ${parts.join(', ')} (${(status.percentage * 100).toFixed(1)}%)`);
|
|
439
|
+
this.name = 'BudgetExceededError';
|
|
431
440
|
this.status = status;
|
|
432
441
|
}
|
|
433
442
|
}
|