@animalabs/agent-framework 0.1.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/dist/src/framework.d.ts +13 -0
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +133 -10
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +5 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +104 -6
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/types.d.ts +31 -0
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +16 -2
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +101 -6
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/push-handler.js +1 -1
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +17 -2
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +66 -3
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-policy.d.ts +24 -0
- package/dist/src/mcpl/tool-policy.d.ts.map +1 -0
- package/dist/src/mcpl/tool-policy.js +46 -0
- package/dist/src/mcpl/tool-policy.js.map +1 -0
- package/dist/src/mcpl/types.d.ts +31 -0
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +43 -1
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +246 -32
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/sync.d.ts +7 -2
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +2 -2
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +55 -3
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js +8 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +40 -3
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +80 -13
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/types/trace.d.ts +20 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/src/usage/index.d.ts +3 -0
- package/dist/src/usage/index.d.ts.map +1 -0
- package/dist/src/usage/index.js +2 -0
- package/dist/src/usage/index.js.map +1 -0
- package/dist/src/usage/types.d.ts +28 -0
- package/dist/src/usage/types.d.ts.map +1 -0
- package/dist/src/usage/types.js +2 -0
- package/dist/src/usage/types.js.map +1 -0
- package/dist/src/usage/usage-tracker.d.ts +33 -0
- package/dist/src/usage/usage-tracker.d.ts.map +1 -0
- package/dist/src/usage/usage-tracker.js +115 -0
- package/dist/src/usage/usage-tracker.js.map +1 -0
- package/dist/test/channel-registry-typing.test.d.ts +2 -0
- package/dist/test/channel-registry-typing.test.d.ts.map +1 -0
- package/dist/test/channel-registry-typing.test.js +82 -0
- package/dist/test/channel-registry-typing.test.js.map +1 -0
- package/dist/test/event-gate.test.js +135 -16
- package/dist/test/event-gate.test.js.map +1 -1
- package/dist/test/mcpl-gate-roundtrip.test.d.ts +2 -0
- package/dist/test/mcpl-gate-roundtrip.test.d.ts.map +1 -0
- package/dist/test/mcpl-gate-roundtrip.test.js +140 -0
- package/dist/test/mcpl-gate-roundtrip.test.js.map +1 -0
- package/dist/test/tool-policy.test.d.ts +2 -0
- package/dist/test/tool-policy.test.d.ts.map +1 -0
- package/dist/test/tool-policy.test.js +51 -0
- package/dist/test/tool-policy.test.js.map +1 -0
- package/dist/test/usage-tracker.test.d.ts +2 -0
- package/dist/test/usage-tracker.test.d.ts.map +1 -0
- package/dist/test/usage-tracker.test.js +129 -0
- package/dist/test/usage-tracker.test.js.map +1 -0
- package/dist/test/workspace-watcher.test.d.ts +2 -0
- package/dist/test/workspace-watcher.test.d.ts.map +1 -0
- package/dist/test/workspace-watcher.test.js +117 -0
- package/dist/test/workspace-watcher.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/framework.ts +148 -10
- package/src/gate/event-gate.ts +106 -6
- package/src/gate/types.ts +28 -0
- package/src/index.ts +7 -0
- package/src/mcpl/channel-registry.ts +120 -8
- package/src/mcpl/push-handler.ts +1 -1
- package/src/mcpl/server-connection.ts +78 -3
- package/src/mcpl/tool-policy.ts +52 -0
- package/src/mcpl/types.ts +34 -0
- package/src/modules/workspace/index.ts +269 -38
- package/src/modules/workspace/sync.ts +11 -4
- package/src/modules/workspace/types.ts +70 -3
- package/src/modules/workspace/watcher.ts +104 -15
- package/src/types/trace.ts +26 -1
- package/src/usage/index.ts +2 -0
- package/src/usage/types.ts +27 -0
- package/src/usage/usage-tracker.ts +141 -0
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Filesystem watcher with debounce, ignore patterns, and suppression.
|
|
3
|
+
*
|
|
4
|
+
* Events are carried through with their op type (created/modified/deleted) so
|
|
5
|
+
* downstream wake policies can key on creation vs. modification vs. deletion.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
8
|
import { watch, type FSWatcher } from 'chokidar';
|
|
9
|
+
import { mkdirSync } from 'node:fs';
|
|
6
10
|
import { type MountConfig } from './types.js';
|
|
7
11
|
|
|
12
|
+
export type FsOp = 'created' | 'modified' | 'deleted';
|
|
13
|
+
|
|
14
|
+
export interface FsChange {
|
|
15
|
+
path: string;
|
|
16
|
+
op: FsOp;
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
export interface WatcherEvents {
|
|
9
|
-
onChange(
|
|
20
|
+
onChange(changes: FsChange[]): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Optional lifecycle callbacks. Without these, chokidar's `ready` and `error`
|
|
25
|
+
* events are silently dropped — which is how a failed attach can look
|
|
26
|
+
* identical to an empty directory in the recorded state.
|
|
27
|
+
*/
|
|
28
|
+
export interface WatcherLifecycle {
|
|
29
|
+
/** Fires once, when chokidar completes its initial scan. */
|
|
30
|
+
onReady?: () => void;
|
|
31
|
+
/** Fires on any chokidar-reported error (ENOENT, EACCES, platform faults). */
|
|
32
|
+
onError?: (err: Error) => void;
|
|
10
33
|
}
|
|
11
34
|
|
|
12
35
|
/**
|
|
@@ -15,21 +38,26 @@ export interface WatcherEvents {
|
|
|
15
38
|
*/
|
|
16
39
|
export class MountWatcher {
|
|
17
40
|
private watcher: FSWatcher | null = null;
|
|
18
|
-
|
|
41
|
+
// Most recent op seen per path within the current debounce window.
|
|
42
|
+
// create -> modify collapses to 'created' (single batch); delete always wins.
|
|
43
|
+
private pendingChanges = new Map<string, FsOp>();
|
|
19
44
|
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
20
45
|
private suppressedPaths = new Set<string>();
|
|
21
46
|
private suppressionTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
22
47
|
private readonly debounceMs: number;
|
|
23
48
|
private readonly config: MountConfig;
|
|
24
|
-
private readonly onChangeCallback: (
|
|
49
|
+
private readonly onChangeCallback: (changes: FsChange[]) => void;
|
|
50
|
+
private readonly lifecycle: WatcherLifecycle;
|
|
25
51
|
|
|
26
52
|
constructor(
|
|
27
53
|
config: MountConfig,
|
|
28
|
-
onChange: (
|
|
54
|
+
onChange: (changes: FsChange[]) => void,
|
|
55
|
+
lifecycle: WatcherLifecycle = {},
|
|
29
56
|
) {
|
|
30
57
|
this.config = config;
|
|
31
58
|
this.debounceMs = config.watchDebounceMs ?? 300;
|
|
32
59
|
this.onChangeCallback = onChange;
|
|
60
|
+
this.lifecycle = lifecycle;
|
|
33
61
|
}
|
|
34
62
|
|
|
35
63
|
/**
|
|
@@ -38,6 +66,21 @@ export class MountWatcher {
|
|
|
38
66
|
start(): void {
|
|
39
67
|
if (this.watcher) return;
|
|
40
68
|
|
|
69
|
+
// Read-write mounts: ensure the path exists before chokidar attaches.
|
|
70
|
+
// chokidar 4's fs.watch-based backend silently fails to fire events when
|
|
71
|
+
// the watched path OR any of its parent directories don't exist at
|
|
72
|
+
// subscribe time (see case C in the WSL/Linux repro). `ready` still fires,
|
|
73
|
+
// so the failure is indistinguishable from an empty directory without
|
|
74
|
+
// this defensive mkdir. Read-only mounts are left alone — creating
|
|
75
|
+
// directories a user asked us only to read would be surprising.
|
|
76
|
+
if (this.config.mode === 'read-write') {
|
|
77
|
+
try {
|
|
78
|
+
mkdirSync(this.config.path, { recursive: true });
|
|
79
|
+
} catch (err) {
|
|
80
|
+
this.lifecycle.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
41
84
|
const ignored = this.config.ignore ?? [];
|
|
42
85
|
|
|
43
86
|
this.watcher = watch(this.config.path, {
|
|
@@ -51,21 +94,31 @@ export class MountWatcher {
|
|
|
51
94
|
},
|
|
52
95
|
});
|
|
53
96
|
|
|
54
|
-
const handleEvent = (filePath: string) => {
|
|
55
|
-
// Convert absolute path to mount-relative
|
|
97
|
+
const handleEvent = (op: FsOp) => (filePath: string) => {
|
|
56
98
|
const relative = this.toRelative(filePath);
|
|
57
99
|
if (!relative) return;
|
|
58
100
|
|
|
59
|
-
// Skip if this path is suppressed (we just wrote it)
|
|
101
|
+
// Skip if this path is suppressed (we just wrote it).
|
|
102
|
+
// Suppression covers all ops — a materialize-then-delete by the module
|
|
103
|
+
// should not echo either the add or the unlink.
|
|
60
104
|
if (this.suppressedPaths.has(relative)) return;
|
|
61
105
|
|
|
62
|
-
this.
|
|
106
|
+
this.mergeOp(relative, op);
|
|
63
107
|
this.scheduleFire();
|
|
64
108
|
};
|
|
65
109
|
|
|
66
|
-
this.watcher.on('add', handleEvent);
|
|
67
|
-
this.watcher.on('change', handleEvent);
|
|
68
|
-
this.watcher.on('unlink', handleEvent);
|
|
110
|
+
this.watcher.on('add', handleEvent('created'));
|
|
111
|
+
this.watcher.on('change', handleEvent('modified'));
|
|
112
|
+
this.watcher.on('unlink', handleEvent('deleted'));
|
|
113
|
+
|
|
114
|
+
if (this.lifecycle.onReady) {
|
|
115
|
+
this.watcher.once('ready', this.lifecycle.onReady);
|
|
116
|
+
}
|
|
117
|
+
if (this.lifecycle.onError) {
|
|
118
|
+
this.watcher.on('error', (err) => {
|
|
119
|
+
this.lifecycle.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
120
|
+
});
|
|
121
|
+
}
|
|
69
122
|
}
|
|
70
123
|
|
|
71
124
|
/**
|
|
@@ -92,12 +145,10 @@ export class MountWatcher {
|
|
|
92
145
|
/**
|
|
93
146
|
* Suppress watcher events for a path temporarily.
|
|
94
147
|
* Used after materializing to avoid echo events.
|
|
95
|
-
* After the cooldown, the path is re-checked via the recheckCallback.
|
|
96
148
|
*/
|
|
97
149
|
suppress(relativePath: string, cooldownMs = 500): void {
|
|
98
150
|
this.suppressedPaths.add(relativePath);
|
|
99
151
|
|
|
100
|
-
// Clear existing timer for this path
|
|
101
152
|
const existing = this.suppressionTimers.get(relativePath);
|
|
102
153
|
if (existing) clearTimeout(existing);
|
|
103
154
|
|
|
@@ -116,6 +167,44 @@ export class MountWatcher {
|
|
|
116
167
|
return this.suppressedPaths.has(relativePath);
|
|
117
168
|
}
|
|
118
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Merge a new op for a path into the pending batch. Chokidar can fire
|
|
172
|
+
* multiple events per path inside one debounce window; collapse them to the
|
|
173
|
+
* single op that reflects the net effect at end-of-window.
|
|
174
|
+
*
|
|
175
|
+
* Transitions (prev → op → result):
|
|
176
|
+
* ∅ → * → op
|
|
177
|
+
* * → deleted → deleted (trailing delete wins)
|
|
178
|
+
* deleted → created → modified (atomic save: unlink+rename;
|
|
179
|
+
* file exists with new contents)
|
|
180
|
+
* created → modified → created (still net-new this window)
|
|
181
|
+
* created → created → created
|
|
182
|
+
* modified → created → created (shouldn't happen, but keep
|
|
183
|
+
* created to avoid a false
|
|
184
|
+
* delete->recreate signal)
|
|
185
|
+
* modified → modified → modified
|
|
186
|
+
*/
|
|
187
|
+
private mergeOp(path: string, op: FsOp): void {
|
|
188
|
+
const prev = this.pendingChanges.get(path);
|
|
189
|
+
if (!prev) {
|
|
190
|
+
this.pendingChanges.set(path, op);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (op === 'deleted') {
|
|
194
|
+
this.pendingChanges.set(path, 'deleted');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (prev === 'deleted' && op === 'created') {
|
|
198
|
+
this.pendingChanges.set(path, 'modified');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (prev === 'created') {
|
|
202
|
+
this.pendingChanges.set(path, 'created');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
this.pendingChanges.set(path, op);
|
|
206
|
+
}
|
|
207
|
+
|
|
119
208
|
private scheduleFire(): void {
|
|
120
209
|
if (this.debounceTimer) {
|
|
121
210
|
clearTimeout(this.debounceTimer);
|
|
@@ -123,9 +212,9 @@ export class MountWatcher {
|
|
|
123
212
|
this.debounceTimer = setTimeout(() => {
|
|
124
213
|
this.debounceTimer = null;
|
|
125
214
|
if (this.pendingChanges.size > 0) {
|
|
126
|
-
const
|
|
215
|
+
const changes: FsChange[] = [...this.pendingChanges].map(([path, op]) => ({ path, op }));
|
|
127
216
|
this.pendingChanges.clear();
|
|
128
|
-
this.onChangeCallback(
|
|
217
|
+
this.onChangeCallback(changes);
|
|
129
218
|
}
|
|
130
219
|
}, this.debounceMs);
|
|
131
220
|
}
|
package/src/types/trace.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ProcessEvent } from './events.js';
|
|
2
2
|
import type { ModuleProcessResponse } from './framework.js';
|
|
3
|
+
import type { SessionUsage } from '../usage/types.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Base for all trace events.
|
|
@@ -75,7 +76,7 @@ export type TraceEvent =
|
|
|
75
76
|
| (TraceEventBase & {
|
|
76
77
|
type: 'inference:usage';
|
|
77
78
|
agentName: string;
|
|
78
|
-
tokenUsage: { input: number; output: number };
|
|
79
|
+
tokenUsage: { input: number; output: number; cacheCreation?: number; cacheRead?: number };
|
|
79
80
|
})
|
|
80
81
|
| (TraceEventBase & {
|
|
81
82
|
type: 'inference:stream_resumed';
|
|
@@ -174,6 +175,23 @@ export type TraceEvent =
|
|
|
174
175
|
configPath: string;
|
|
175
176
|
policyCount: number;
|
|
176
177
|
})
|
|
178
|
+
| (TraceEventBase & {
|
|
179
|
+
type: 'gate:decision';
|
|
180
|
+
eventType: string;
|
|
181
|
+
serverId?: string;
|
|
182
|
+
channelId?: string;
|
|
183
|
+
matchedPolicy: string | null;
|
|
184
|
+
trigger: boolean;
|
|
185
|
+
behavior: string;
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
// Usage lifecycle
|
|
189
|
+
| (TraceEventBase & {
|
|
190
|
+
type: 'usage:updated';
|
|
191
|
+
totals: SessionUsage;
|
|
192
|
+
agentName: string;
|
|
193
|
+
inferenceCount: number;
|
|
194
|
+
})
|
|
177
195
|
|
|
178
196
|
// Undo/redo lifecycle
|
|
179
197
|
| (TraceEventBase & {
|
|
@@ -188,6 +206,13 @@ export type TraceEvent =
|
|
|
188
206
|
agentName: string;
|
|
189
207
|
fromBranch: string;
|
|
190
208
|
toBranch: string;
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// MCPL subprocess stderr (one trace per line, for receipts when things break)
|
|
212
|
+
| (TraceEventBase & {
|
|
213
|
+
type: 'mcpl:server-stderr';
|
|
214
|
+
serverId: string;
|
|
215
|
+
line: string;
|
|
191
216
|
});
|
|
192
217
|
|
|
193
218
|
/**
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface SessionUsage {
|
|
2
|
+
inputTokens: number;
|
|
3
|
+
outputTokens: number;
|
|
4
|
+
cacheCreationTokens: number;
|
|
5
|
+
cacheReadTokens: number;
|
|
6
|
+
estimatedCost?: { total: number; currency: string };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AgentUsage {
|
|
10
|
+
agentName: string;
|
|
11
|
+
usage: SessionUsage;
|
|
12
|
+
inferenceCount: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SessionUsageSnapshot {
|
|
16
|
+
totals: SessionUsage;
|
|
17
|
+
byAgent: AgentUsage[];
|
|
18
|
+
inferenceCount: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Trace event emitted by UsageTracker after each inference completion. */
|
|
22
|
+
export interface UsageUpdatedEvent {
|
|
23
|
+
type: 'usage:updated';
|
|
24
|
+
totals: SessionUsage;
|
|
25
|
+
agentName: string;
|
|
26
|
+
inferenceCount: number;
|
|
27
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { SessionUsage, SessionUsageSnapshot, UsageUpdatedEvent } from './types.js';
|
|
2
|
+
|
|
3
|
+
function snapshotUsage(usage: SessionUsage): SessionUsage {
|
|
4
|
+
return {
|
|
5
|
+
...usage,
|
|
6
|
+
estimatedCost: usage.estimatedCost ? { ...usage.estimatedCost } : undefined,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function accumulateUsage(
|
|
11
|
+
target: SessionUsage,
|
|
12
|
+
tokens: { inputTokens: number; outputTokens: number; cacheCreationTokens: number; cacheReadTokens: number },
|
|
13
|
+
estimatedCost?: { total: number; currency: string }
|
|
14
|
+
): void {
|
|
15
|
+
target.inputTokens += tokens.inputTokens;
|
|
16
|
+
target.outputTokens += tokens.outputTokens;
|
|
17
|
+
target.cacheCreationTokens += tokens.cacheCreationTokens;
|
|
18
|
+
target.cacheReadTokens += tokens.cacheReadTokens;
|
|
19
|
+
if (estimatedCost) {
|
|
20
|
+
if (!target.estimatedCost) {
|
|
21
|
+
target.estimatedCost = { total: 0, currency: estimatedCost.currency };
|
|
22
|
+
} else if (target.estimatedCost.currency !== estimatedCost.currency) {
|
|
23
|
+
// Mixed currencies — drop cost tracking rather than produce garbage
|
|
24
|
+
target.estimatedCost = undefined;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
target.estimatedCost.total += estimatedCost.total;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseUsage(raw: unknown): SessionUsage | null {
|
|
32
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
33
|
+
const o = raw as Record<string, unknown>;
|
|
34
|
+
if (typeof o.inputTokens !== 'number' || typeof o.outputTokens !== 'number') return null;
|
|
35
|
+
return {
|
|
36
|
+
inputTokens: o.inputTokens,
|
|
37
|
+
outputTokens: o.outputTokens as number,
|
|
38
|
+
cacheCreationTokens: typeof o.cacheCreationTokens === 'number' ? o.cacheCreationTokens : 0,
|
|
39
|
+
cacheReadTokens: typeof o.cacheReadTokens === 'number' ? o.cacheReadTokens : 0,
|
|
40
|
+
estimatedCost: o.estimatedCost && typeof o.estimatedCost === 'object'
|
|
41
|
+
? { total: (o.estimatedCost as any).total, currency: (o.estimatedCost as any).currency }
|
|
42
|
+
: undefined,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PersistedUsageState {
|
|
47
|
+
totals: SessionUsage;
|
|
48
|
+
byAgent: Array<{ agentName: string; usage: SessionUsage; inferenceCount: number }>;
|
|
49
|
+
inferenceCount: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class UsageTracker {
|
|
53
|
+
private totals: SessionUsage = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
|
|
54
|
+
private byAgent: Map<string, { usage: SessionUsage; inferenceCount: number }> = new Map();
|
|
55
|
+
private inferenceCount = 0;
|
|
56
|
+
private emitTrace: (event: UsageUpdatedEvent) => void;
|
|
57
|
+
|
|
58
|
+
constructor(opts: {
|
|
59
|
+
emitTrace: (event: UsageUpdatedEvent) => void;
|
|
60
|
+
restored?: PersistedUsageState;
|
|
61
|
+
}) {
|
|
62
|
+
this.emitTrace = opts.emitTrace;
|
|
63
|
+
if (opts.restored) {
|
|
64
|
+
this.restoreFrom(opts.restored);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private restoreFrom(state: PersistedUsageState): void {
|
|
69
|
+
const totals = parseUsage(state.totals);
|
|
70
|
+
if (!totals) return;
|
|
71
|
+
this.totals = totals;
|
|
72
|
+
this.inferenceCount = typeof state.inferenceCount === 'number' ? state.inferenceCount : 0;
|
|
73
|
+
if (Array.isArray(state.byAgent)) {
|
|
74
|
+
for (const entry of state.byAgent) {
|
|
75
|
+
const usage = parseUsage(entry.usage);
|
|
76
|
+
if (usage && typeof entry.agentName === 'string') {
|
|
77
|
+
this.byAgent.set(entry.agentName, {
|
|
78
|
+
usage,
|
|
79
|
+
inferenceCount: typeof entry.inferenceCount === 'number' ? entry.inferenceCount : 0,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
toJSON(): PersistedUsageState {
|
|
87
|
+
return {
|
|
88
|
+
totals: snapshotUsage(this.totals),
|
|
89
|
+
byAgent: [...this.byAgent.entries()].map(([name, data]) => ({
|
|
90
|
+
agentName: name,
|
|
91
|
+
usage: snapshotUsage(data.usage),
|
|
92
|
+
inferenceCount: data.inferenceCount,
|
|
93
|
+
})),
|
|
94
|
+
inferenceCount: this.inferenceCount,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
onInferenceCompleted(agentName: string, usage: {
|
|
99
|
+
inputTokens: number;
|
|
100
|
+
outputTokens: number;
|
|
101
|
+
cacheCreationTokens?: number;
|
|
102
|
+
cacheReadTokens?: number;
|
|
103
|
+
}, estimatedCost?: { total: number; currency: string }): void {
|
|
104
|
+
const tokens = {
|
|
105
|
+
inputTokens: usage.inputTokens,
|
|
106
|
+
outputTokens: usage.outputTokens,
|
|
107
|
+
cacheCreationTokens: usage.cacheCreationTokens ?? 0,
|
|
108
|
+
cacheReadTokens: usage.cacheReadTokens ?? 0,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
accumulateUsage(this.totals, tokens, estimatedCost);
|
|
112
|
+
this.inferenceCount++;
|
|
113
|
+
|
|
114
|
+
let agent = this.byAgent.get(agentName);
|
|
115
|
+
if (!agent) {
|
|
116
|
+
agent = { usage: { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 }, inferenceCount: 0 };
|
|
117
|
+
this.byAgent.set(agentName, agent);
|
|
118
|
+
}
|
|
119
|
+
accumulateUsage(agent.usage, tokens, estimatedCost);
|
|
120
|
+
agent.inferenceCount++;
|
|
121
|
+
|
|
122
|
+
this.emitTrace({
|
|
123
|
+
type: 'usage:updated',
|
|
124
|
+
totals: snapshotUsage(this.totals),
|
|
125
|
+
agentName,
|
|
126
|
+
inferenceCount: this.inferenceCount,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getSnapshot(): SessionUsageSnapshot {
|
|
131
|
+
return {
|
|
132
|
+
totals: snapshotUsage(this.totals),
|
|
133
|
+
byAgent: [...this.byAgent.entries()].map(([name, data]) => ({
|
|
134
|
+
agentName: name,
|
|
135
|
+
usage: snapshotUsage(data.usage),
|
|
136
|
+
inferenceCount: data.inferenceCount,
|
|
137
|
+
})),
|
|
138
|
+
inferenceCount: this.inferenceCount,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|