watchmyagents 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/README.md +183 -0
- package/SECURITY.md +78 -0
- package/package.json +66 -0
- package/scripts/fetch-anthropic.js +130 -0
- package/scripts/inspect.js +241 -0
- package/src/adapters/claude.js +46 -0
- package/src/adapters/generic.js +21 -0
- package/src/adapters/langchain.js +42 -0
- package/src/adapters/openai.js +47 -0
- package/src/anonymizer.js +48 -0
- package/src/collector.js +113 -0
- package/src/exporter.js +71 -0
- package/src/index.cjs +36 -0
- package/src/index.js +26 -0
- package/src/logger.js +76 -0
- package/src/sources/anthropic-managed.js +422 -0
- package/src/tokens.js +76 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { WatchMyAgents } from './collector.js';
|
|
2
|
+
import { watch, createGenericMonitor } from './adapters/generic.js';
|
|
3
|
+
import { createClaudeMonitor } from './adapters/claude.js';
|
|
4
|
+
import { createOpenAIMonitor } from './adapters/openai.js';
|
|
5
|
+
import { createLangChainHandler } from './adapters/langchain.js';
|
|
6
|
+
import { anonymize, scrubString, hashId } from './anonymizer.js';
|
|
7
|
+
import { DEFAULT_PRICING, estimateCost, TokenTracker } from './tokens.js';
|
|
8
|
+
import * as anthropicManaged from './sources/anthropic-managed.js';
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
WatchMyAgents,
|
|
12
|
+
watch,
|
|
13
|
+
createGenericMonitor,
|
|
14
|
+
createClaudeMonitor,
|
|
15
|
+
createOpenAIMonitor,
|
|
16
|
+
createLangChainHandler,
|
|
17
|
+
anonymize,
|
|
18
|
+
scrubString,
|
|
19
|
+
hashId,
|
|
20
|
+
DEFAULT_PRICING,
|
|
21
|
+
estimateCost,
|
|
22
|
+
TokenTracker,
|
|
23
|
+
anthropicManaged,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default WatchMyAgents;
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { mkdir, appendFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
const EXPORT_FIELDS = [
|
|
6
|
+
'id', 'agent_id', 'framework', 'timestamp', 'action_type',
|
|
7
|
+
'tool_name', 'duration_ms', 'tokens_used',
|
|
8
|
+
'input_tokens', 'output_tokens', 'cache_read_tokens', 'cache_creation_tokens',
|
|
9
|
+
'cost_usd', 'model',
|
|
10
|
+
'session_tokens', 'session_cost_usd',
|
|
11
|
+
'status', 'error', 'sequence_number', 'session_id',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export class Logger {
|
|
15
|
+
constructor({ logDir, agentId, sessionId, silent }) {
|
|
16
|
+
this.logDir = logDir;
|
|
17
|
+
this.agentId = agentId;
|
|
18
|
+
this.sessionId = sessionId || randomUUID();
|
|
19
|
+
this.silent = silent !== false;
|
|
20
|
+
this.sequence = 0;
|
|
21
|
+
this.currentDay = null;
|
|
22
|
+
this.currentPath = null;
|
|
23
|
+
this.count = 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_pathForToday() {
|
|
27
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
28
|
+
if (day !== this.currentDay) {
|
|
29
|
+
this.currentDay = day;
|
|
30
|
+
this.currentPath = join(this.logDir, this.agentId, `${day}.ndjson`);
|
|
31
|
+
}
|
|
32
|
+
return this.currentPath;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async write(e) {
|
|
36
|
+
const path = this._pathForToday();
|
|
37
|
+
const full = {
|
|
38
|
+
id: e.id || randomUUID(),
|
|
39
|
+
agent_id: this.agentId,
|
|
40
|
+
framework: e.framework || 'generic',
|
|
41
|
+
timestamp: e.timestamp || new Date().toISOString(),
|
|
42
|
+
action_type: e.action_type || 'tool_call',
|
|
43
|
+
tool_name: e.tool_name || null,
|
|
44
|
+
duration_ms: e.duration_ms ?? null,
|
|
45
|
+
model: e.model ?? null,
|
|
46
|
+
tokens_used: e.tokens_used ?? null,
|
|
47
|
+
input_tokens: e.input_tokens ?? null,
|
|
48
|
+
output_tokens: e.output_tokens ?? null,
|
|
49
|
+
cache_read_tokens: e.cache_read_tokens ?? null,
|
|
50
|
+
cache_creation_tokens: e.cache_creation_tokens ?? null,
|
|
51
|
+
cost_usd: e.cost_usd ?? null,
|
|
52
|
+
status: e.status || 'ok',
|
|
53
|
+
error: e.error || null,
|
|
54
|
+
sequence_number: ++this.sequence,
|
|
55
|
+
session_id: this.sessionId,
|
|
56
|
+
session_tokens: e.session_tokens ?? null,
|
|
57
|
+
session_cost_usd: e.session_cost_usd ?? null,
|
|
58
|
+
input: e.input ?? null,
|
|
59
|
+
output: e.output ?? null,
|
|
60
|
+
};
|
|
61
|
+
try {
|
|
62
|
+
await mkdir(join(this.logDir, this.agentId), { recursive: true, mode: 0o700 });
|
|
63
|
+
await appendFile(path, JSON.stringify(full) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
64
|
+
this.count++;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (!this.silent) process.stderr.write(`[wma] log error: ${err.message}\n`);
|
|
67
|
+
}
|
|
68
|
+
return full;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
toExportRecord(entry) {
|
|
72
|
+
const out = {};
|
|
73
|
+
for (const k of EXPORT_FIELDS) out[k] = entry[k];
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
// Anthropic Managed Agents — post-hoc fetcher
|
|
2
|
+
//
|
|
3
|
+
// Verified against docs.claude.com/managed-agents/events-and-streaming
|
|
4
|
+
// (managed-agents-2026-04-01 beta).
|
|
5
|
+
//
|
|
6
|
+
// Mapping policy:
|
|
7
|
+
// span.model_request_end (model_usage) → llm_call entry (tokens + duration)
|
|
8
|
+
// agent.tool_use + agent.tool_result → tool_use entry (duration + error)
|
|
9
|
+
// agent.mcp_tool_use + agent.mcp_tool_result → mcp_tool_use entry
|
|
10
|
+
// agent.custom_tool_use → custom_tool_use entry (no duration —
|
|
11
|
+
// resolution comes from user side)
|
|
12
|
+
// session.error → error entry
|
|
13
|
+
// agent.message / agent.thinking → skipped (token cost is on the
|
|
14
|
+
// model_request_end span anyway,
|
|
15
|
+
// content is the agent's output not
|
|
16
|
+
// an "action" we observe)
|
|
17
|
+
|
|
18
|
+
import { request } from 'node:https';
|
|
19
|
+
import { URLSearchParams } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const API_HOST = 'api.anthropic.com';
|
|
22
|
+
const BETA = 'managed-agents-2026-04-01';
|
|
23
|
+
const VERSION = '2023-06-01';
|
|
24
|
+
|
|
25
|
+
function httpGet(apiKey, path) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const req = request({
|
|
28
|
+
host: API_HOST, port: 443, path, method: 'GET',
|
|
29
|
+
rejectUnauthorized: true,
|
|
30
|
+
headers: {
|
|
31
|
+
'x-api-key': apiKey,
|
|
32
|
+
'anthropic-version': VERSION,
|
|
33
|
+
'anthropic-beta': BETA,
|
|
34
|
+
'accept': 'application/json',
|
|
35
|
+
},
|
|
36
|
+
}, res => {
|
|
37
|
+
const chunks = [];
|
|
38
|
+
res.on('data', c => chunks.push(c));
|
|
39
|
+
res.on('end', () => {
|
|
40
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
41
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
42
|
+
try { resolve(JSON.parse(body)); } catch (e) { reject(e); }
|
|
43
|
+
} else if (res.statusCode === 429) {
|
|
44
|
+
const ra = parseInt(res.headers['retry-after'] || '5', 10);
|
|
45
|
+
const err = new Error(`HTTP 429: ${body}`);
|
|
46
|
+
err.retryAfter = ra; reject(err);
|
|
47
|
+
} else {
|
|
48
|
+
reject(new Error(`HTTP ${res.statusCode}: ${body.slice(0, 500)}`));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
req.on('error', reject);
|
|
53
|
+
req.end();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function getWithRetry(apiKey, path, attempts = 3) {
|
|
58
|
+
let lastErr;
|
|
59
|
+
for (let i = 0; i < attempts; i++) {
|
|
60
|
+
try { return await httpGet(apiKey, path); }
|
|
61
|
+
catch (e) {
|
|
62
|
+
lastErr = e;
|
|
63
|
+
const wait = e.retryAfter ? e.retryAfter * 1000 : 1000 * 2 ** i;
|
|
64
|
+
if (i < attempts - 1) await new Promise(r => setTimeout(r, wait));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
throw lastErr;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function getAgent(apiKey, agentId) {
|
|
71
|
+
return getWithRetry(apiKey, `/v1/agents/${agentId}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function listSessions(apiKey, { agentId, since, limit = 100 } = {}) {
|
|
75
|
+
const sessions = [];
|
|
76
|
+
let after = null;
|
|
77
|
+
while (true) {
|
|
78
|
+
const qs = new URLSearchParams({ limit: String(limit) });
|
|
79
|
+
if (agentId) qs.set('agent_id', agentId);
|
|
80
|
+
if (after) qs.set('after_id', after);
|
|
81
|
+
const data = await getWithRetry(apiKey, `/v1/sessions?${qs}`);
|
|
82
|
+
const page = data.data || [];
|
|
83
|
+
let stop = false;
|
|
84
|
+
for (const s of page) {
|
|
85
|
+
const created = s.created_at ? new Date(s.created_at) : null;
|
|
86
|
+
if (since && created && created < since) { stop = true; break; }
|
|
87
|
+
sessions.push(s);
|
|
88
|
+
}
|
|
89
|
+
if (stop || !data.has_more || page.length === 0) break;
|
|
90
|
+
after = page[page.length - 1]?.id;
|
|
91
|
+
if (!after) break;
|
|
92
|
+
}
|
|
93
|
+
return sessions;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Yields raw events in chronological order. Accepts an optional types filter
|
|
97
|
+
// to reduce payload (server-side `types[]=...&types[]=...`).
|
|
98
|
+
export async function* fetchRawEvents(apiKey, sessionId, { types } = {}) {
|
|
99
|
+
let after = null;
|
|
100
|
+
while (true) {
|
|
101
|
+
const qs = new URLSearchParams({ limit: '1000' });
|
|
102
|
+
if (after) qs.set('after_id', after);
|
|
103
|
+
if (types) for (const t of types) qs.append('types[]', t);
|
|
104
|
+
const data = await getWithRetry(apiKey, `/v1/sessions/${sessionId}/events?${qs}`);
|
|
105
|
+
const page = data.data || [];
|
|
106
|
+
for (const ev of page) yield ev;
|
|
107
|
+
if (!data.has_more || page.length === 0) break;
|
|
108
|
+
after = page[page.length - 1]?.id;
|
|
109
|
+
if (!after) break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const RELEVANT_TYPES = [
|
|
114
|
+
'span.model_request_start', 'span.model_request_end',
|
|
115
|
+
// User events (audit trail of human/orchestrator inputs)
|
|
116
|
+
'user.message', 'user.interrupt',
|
|
117
|
+
'user.tool_confirmation', 'user.custom_tool_result',
|
|
118
|
+
// Agent events
|
|
119
|
+
'agent.message', 'agent.thinking',
|
|
120
|
+
'agent.tool_use', 'agent.tool_result',
|
|
121
|
+
'agent.mcp_tool_use', 'agent.mcp_tool_result',
|
|
122
|
+
'agent.custom_tool_use',
|
|
123
|
+
'agent.thread_context_compacted',
|
|
124
|
+
'agent.thread_message_sent', 'agent.thread_message_received',
|
|
125
|
+
// Session lifecycle (security-critical: config changes, terminations)
|
|
126
|
+
'session.error',
|
|
127
|
+
'session.updated',
|
|
128
|
+
'session.thread_created',
|
|
129
|
+
'session.status_running', 'session.status_idle',
|
|
130
|
+
'session.status_rescheduled', 'session.status_terminated',
|
|
131
|
+
'session.thread_status_running', 'session.thread_status_idle',
|
|
132
|
+
'session.thread_status_terminated',
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const tsMs = ev => Date.parse(ev.processed_at || ev.created_at || '') || null;
|
|
136
|
+
|
|
137
|
+
export async function* fetchSessionEntries({ apiKey, agentId, sessionId, model }) {
|
|
138
|
+
// Pair-tracking maps: event_id of the "start" → its timestamp + metadata
|
|
139
|
+
const pendingModelReq = new Map(); // span.model_request_start.id → ts
|
|
140
|
+
const pendingToolUse = new Map(); // agent.tool_use.id → { ts, name, isMcp, input }
|
|
141
|
+
|
|
142
|
+
const base = { framework: 'anthropic-managed', agent_id: agentId, session_id: sessionId };
|
|
143
|
+
|
|
144
|
+
// No server-side `types[]` filter: the API rejects unknown values, but the
|
|
145
|
+
// exact filterable set is undocumented & evolves. We pull everything and
|
|
146
|
+
// filter here, ensuring future event types are surfaced rather than dropped.
|
|
147
|
+
const RELEVANT = new Set(RELEVANT_TYPES);
|
|
148
|
+
for await (const ev of fetchRawEvents(apiKey, sessionId)) {
|
|
149
|
+
if (!RELEVANT.has(ev.type)) continue;
|
|
150
|
+
const type = ev.type;
|
|
151
|
+
const ts = ev.processed_at || ev.created_at || new Date().toISOString();
|
|
152
|
+
const tsMillis = tsMs(ev);
|
|
153
|
+
|
|
154
|
+
if (type === 'span.model_request_start') {
|
|
155
|
+
pendingModelReq.set(ev.id, tsMillis);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (type === 'span.model_request_end') {
|
|
159
|
+
const startTs = pendingModelReq.get(ev.model_request_start_id);
|
|
160
|
+
pendingModelReq.delete(ev.model_request_start_id);
|
|
161
|
+
const u = ev.model_usage || {};
|
|
162
|
+
const i = u.input_tokens || 0;
|
|
163
|
+
const o = u.output_tokens || 0;
|
|
164
|
+
const cr = u.cache_read_input_tokens || 0;
|
|
165
|
+
const cw = u.cache_creation_input_tokens || 0;
|
|
166
|
+
yield {
|
|
167
|
+
...base,
|
|
168
|
+
action_type: 'llm_call',
|
|
169
|
+
tool_name: null,
|
|
170
|
+
model: model || null,
|
|
171
|
+
timestamp: ts,
|
|
172
|
+
duration_ms: (startTs && tsMillis) ? tsMillis - startTs : null,
|
|
173
|
+
input_tokens: i || null,
|
|
174
|
+
output_tokens: o || null,
|
|
175
|
+
cache_read_tokens: cr || null,
|
|
176
|
+
cache_creation_tokens: cw || null,
|
|
177
|
+
tokens_used: (i + o + cr + cw) || null,
|
|
178
|
+
status: ev.is_error ? 'error' : 'ok',
|
|
179
|
+
};
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (type === 'user.message') {
|
|
184
|
+
yield {
|
|
185
|
+
...base,
|
|
186
|
+
action_type: 'user_message',
|
|
187
|
+
tool_name: null,
|
|
188
|
+
model: model || null,
|
|
189
|
+
timestamp: ts,
|
|
190
|
+
status: 'ok',
|
|
191
|
+
input: { content: ev.content || [] },
|
|
192
|
+
};
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (type === 'user.interrupt') {
|
|
197
|
+
yield {
|
|
198
|
+
...base,
|
|
199
|
+
action_type: 'user_interrupt',
|
|
200
|
+
tool_name: null,
|
|
201
|
+
model: model || null,
|
|
202
|
+
timestamp: ts,
|
|
203
|
+
status: 'ok',
|
|
204
|
+
};
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Audit trail: who approved/denied which tool, with optional deny_message
|
|
209
|
+
if (type === 'user.tool_confirmation') {
|
|
210
|
+
const denied = ev.result === 'deny';
|
|
211
|
+
yield {
|
|
212
|
+
...base,
|
|
213
|
+
action_type: 'tool_confirmation',
|
|
214
|
+
tool_name: null,
|
|
215
|
+
model: model || null,
|
|
216
|
+
timestamp: ts,
|
|
217
|
+
status: denied ? 'error' : 'ok',
|
|
218
|
+
input: { tool_use_id: ev.tool_use_id, result: ev.result },
|
|
219
|
+
output: { deny_message: ev.deny_message ?? null },
|
|
220
|
+
error: denied ? (ev.deny_message || 'denied').slice(0, 500) : null,
|
|
221
|
+
};
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (type === 'user.custom_tool_result') {
|
|
226
|
+
yield {
|
|
227
|
+
...base,
|
|
228
|
+
action_type: 'custom_tool_result',
|
|
229
|
+
tool_name: null,
|
|
230
|
+
model: model || null,
|
|
231
|
+
timestamp: ts,
|
|
232
|
+
status: ev.is_error ? 'error' : 'ok',
|
|
233
|
+
input: { custom_tool_use_id: ev.custom_tool_use_id },
|
|
234
|
+
output: { content: ev.content ?? null },
|
|
235
|
+
};
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (type === 'agent.message') {
|
|
240
|
+
yield {
|
|
241
|
+
...base,
|
|
242
|
+
action_type: 'message',
|
|
243
|
+
tool_name: null,
|
|
244
|
+
model: model || null,
|
|
245
|
+
timestamp: ts,
|
|
246
|
+
status: 'ok',
|
|
247
|
+
output: { content: ev.content || [] },
|
|
248
|
+
};
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (type === 'agent.thinking') {
|
|
253
|
+
yield {
|
|
254
|
+
...base,
|
|
255
|
+
action_type: 'thinking',
|
|
256
|
+
tool_name: null,
|
|
257
|
+
model: model || null,
|
|
258
|
+
timestamp: ts,
|
|
259
|
+
status: 'ok',
|
|
260
|
+
output: { thinking: ev.thinking ?? ev.content ?? null, signature: ev.signature ?? null },
|
|
261
|
+
};
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (type === 'agent.tool_use' || type === 'agent.mcp_tool_use') {
|
|
266
|
+
pendingToolUse.set(ev.id, {
|
|
267
|
+
ts: tsMillis,
|
|
268
|
+
name: ev.name || 'unknown',
|
|
269
|
+
isMcp: type === 'agent.mcp_tool_use',
|
|
270
|
+
input: ev.input ?? null,
|
|
271
|
+
mcpServer: ev.server_name ?? ev.mcp_server_name ?? null,
|
|
272
|
+
});
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (type === 'agent.tool_result' || type === 'agent.mcp_tool_result') {
|
|
276
|
+
const start = pendingToolUse.get(ev.tool_use_id);
|
|
277
|
+
pendingToolUse.delete(ev.tool_use_id);
|
|
278
|
+
const isError = ev.is_error === true;
|
|
279
|
+
yield {
|
|
280
|
+
...base,
|
|
281
|
+
action_type: start?.isMcp ? 'mcp_tool_use' : 'tool_use',
|
|
282
|
+
tool_name: start?.name || 'unknown',
|
|
283
|
+
timestamp: ts,
|
|
284
|
+
duration_ms: (start?.ts && tsMillis) ? tsMillis - start.ts : null,
|
|
285
|
+
status: isError ? 'error' : 'ok',
|
|
286
|
+
error: isError ? extractText(ev.content).slice(0, 500) : null,
|
|
287
|
+
input: start?.input ?? null,
|
|
288
|
+
output: { content: ev.content ?? null, mcp_server: start?.mcpServer ?? undefined },
|
|
289
|
+
};
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (type === 'agent.custom_tool_use') {
|
|
294
|
+
yield {
|
|
295
|
+
...base,
|
|
296
|
+
action_type: 'custom_tool_use',
|
|
297
|
+
tool_name: ev.name || 'unknown',
|
|
298
|
+
timestamp: ts,
|
|
299
|
+
status: 'ok',
|
|
300
|
+
input: ev.input ?? null,
|
|
301
|
+
};
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Context window saturation — historic content may be lost
|
|
306
|
+
if (type === 'agent.thread_context_compacted') {
|
|
307
|
+
yield {
|
|
308
|
+
...base,
|
|
309
|
+
action_type: 'context_compacted',
|
|
310
|
+
tool_name: null,
|
|
311
|
+
model: model || null,
|
|
312
|
+
timestamp: ts,
|
|
313
|
+
status: 'ok',
|
|
314
|
+
output: {
|
|
315
|
+
session_thread_id: ev.session_thread_id ?? null,
|
|
316
|
+
agent_name: ev.agent_name ?? null,
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Multi-agent: orchestrator/sub-agent message passing
|
|
323
|
+
if (type === 'agent.thread_message_sent' || type === 'agent.thread_message_received') {
|
|
324
|
+
const direction = type.endsWith('_sent') ? 'sent' : 'received';
|
|
325
|
+
yield {
|
|
326
|
+
...base,
|
|
327
|
+
action_type: `thread_message_${direction}`,
|
|
328
|
+
tool_name: null,
|
|
329
|
+
model: model || null,
|
|
330
|
+
timestamp: ts,
|
|
331
|
+
status: 'ok',
|
|
332
|
+
output: {
|
|
333
|
+
session_thread_id: ev.session_thread_id ?? null,
|
|
334
|
+
agent_name: ev.agent_name ?? null,
|
|
335
|
+
content: ev.content ?? null,
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Security-critical: session configuration changed mid-flight.
|
|
342
|
+
// Docs say "Includes only the fields that changed."
|
|
343
|
+
if (type === 'session.updated') {
|
|
344
|
+
const { id: _id, type: _type, processed_at: _pa, created_at: _ca, ...changes } = ev;
|
|
345
|
+
yield {
|
|
346
|
+
...base,
|
|
347
|
+
action_type: 'config_change',
|
|
348
|
+
tool_name: null,
|
|
349
|
+
model: model || null,
|
|
350
|
+
timestamp: ts,
|
|
351
|
+
status: 'ok',
|
|
352
|
+
output: { changes },
|
|
353
|
+
};
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (type === 'session.thread_created') {
|
|
358
|
+
yield {
|
|
359
|
+
...base,
|
|
360
|
+
action_type: 'thread_created',
|
|
361
|
+
tool_name: null,
|
|
362
|
+
model: model || null,
|
|
363
|
+
timestamp: ts,
|
|
364
|
+
status: 'ok',
|
|
365
|
+
output: {
|
|
366
|
+
session_thread_id: ev.session_thread_id ?? null,
|
|
367
|
+
agent_name: ev.agent_name ?? null,
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (type === 'session.error') {
|
|
374
|
+
yield {
|
|
375
|
+
...base,
|
|
376
|
+
action_type: 'session_error',
|
|
377
|
+
tool_name: null,
|
|
378
|
+
timestamp: ts,
|
|
379
|
+
status: 'error',
|
|
380
|
+
error: (ev.error?.message || 'session error').slice(0, 500),
|
|
381
|
+
};
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// session.status_{running,idle,rescheduled,terminated},
|
|
386
|
+
// session.thread_status_{running,idle,terminated}
|
|
387
|
+
// → state transitions, useful for security audit (e.g. inspecting
|
|
388
|
+
// stop_reason for refusals, errors, max_tokens; terminated = fatal)
|
|
389
|
+
if (type.startsWith('session.status_') || type.startsWith('session.thread_status_')) {
|
|
390
|
+
const isThread = type.startsWith('session.thread_status_');
|
|
391
|
+
const prefix = isThread ? 'session.thread_status_' : 'session.status_';
|
|
392
|
+
const state = type.slice(prefix.length); // 'running' | 'idle' | 'rescheduled' | 'terminated'
|
|
393
|
+
const fatal = state === 'terminated';
|
|
394
|
+
yield {
|
|
395
|
+
...base,
|
|
396
|
+
action_type: 'state_transition',
|
|
397
|
+
tool_name: null,
|
|
398
|
+
model: model || null,
|
|
399
|
+
timestamp: ts,
|
|
400
|
+
status: fatal ? 'error' : 'ok',
|
|
401
|
+
output: {
|
|
402
|
+
scope: isThread ? 'session_thread' : 'session',
|
|
403
|
+
state,
|
|
404
|
+
stop_reason: ev.stop_reason ?? null,
|
|
405
|
+
agent_name: ev.agent_name ?? null,
|
|
406
|
+
session_thread_id: ev.session_thread_id ?? null,
|
|
407
|
+
},
|
|
408
|
+
error: fatal ? (ev.stop_reason?.message || ev.stop_reason?.type || 'session terminated').slice(0, 500) : null,
|
|
409
|
+
};
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function extractText(content) {
|
|
416
|
+
if (typeof content === 'string') return content;
|
|
417
|
+
if (Array.isArray(content)) {
|
|
418
|
+
return content.map(b => (typeof b === 'string' ? b : (b?.text || JSON.stringify(b)))).join(' ');
|
|
419
|
+
}
|
|
420
|
+
if (content && typeof content === 'object') return content.text || JSON.stringify(content);
|
|
421
|
+
return '';
|
|
422
|
+
}
|
package/src/tokens.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Pricing is intentionally NOT bundled in the SDK: per-customer plans evolve.
|
|
2
|
+
// Supply your own table via WatchMyAgents({ tokenPricing: { 'model-id': { input, output, cache_read, cache_write } } })
|
|
3
|
+
// to get cost_usd populated on entries; otherwise cost stays null and only
|
|
4
|
+
// token counts (split + by-model) are tracked.
|
|
5
|
+
export const DEFAULT_PRICING = {};
|
|
6
|
+
|
|
7
|
+
export function estimateCost(model, t, pricing) {
|
|
8
|
+
if (!model) return null;
|
|
9
|
+
const p = (pricing && pricing[model]) || DEFAULT_PRICING[model];
|
|
10
|
+
if (!p) return null;
|
|
11
|
+
const inT = t.input_tokens || 0;
|
|
12
|
+
const outT = t.output_tokens || 0;
|
|
13
|
+
const cr = t.cache_read_tokens || 0;
|
|
14
|
+
const cw = t.cache_creation_tokens || 0;
|
|
15
|
+
const cost = ((inT * (p.input || 0)) + (outT * (p.output || 0)) +
|
|
16
|
+
(cr * (p.cache_read || 0)) + (cw * (p.cache_write || 0))) / 1_000_000;
|
|
17
|
+
return Math.round(cost * 1_000_000) / 1_000_000;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class TokenTracker {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.total = { input: 0, output: 0, cache_read: 0, cache_creation: 0, sum: 0, cost_usd: 0 };
|
|
23
|
+
this.byTool = new Map();
|
|
24
|
+
this.byAction = new Map();
|
|
25
|
+
this.byModel = new Map();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_bucket(map, key) {
|
|
29
|
+
let b = map.get(key);
|
|
30
|
+
if (!b) { b = { input: 0, output: 0, cache_read: 0, cache_creation: 0, sum: 0, cost_usd: 0, calls: 0 }; map.set(key, b); }
|
|
31
|
+
return b;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
record(entry) {
|
|
35
|
+
// Security-audit principle: count EVERY action, including zero-token ones
|
|
36
|
+
// (tool_use, message, thinking, user_message, errors…). Only skip meta-
|
|
37
|
+
// entries that are not actions themselves.
|
|
38
|
+
if (entry.action_type === 'session_end') return;
|
|
39
|
+
|
|
40
|
+
const t = {
|
|
41
|
+
input: entry.input_tokens || 0,
|
|
42
|
+
output: entry.output_tokens || 0,
|
|
43
|
+
cache_read: entry.cache_read_tokens || 0,
|
|
44
|
+
cache_creation: entry.cache_creation_tokens || 0,
|
|
45
|
+
};
|
|
46
|
+
const sum = entry.tokens_used || (t.input + t.output + t.cache_read + t.cache_creation);
|
|
47
|
+
const cost = entry.cost_usd || 0;
|
|
48
|
+
|
|
49
|
+
this.total.input += t.input;
|
|
50
|
+
this.total.output += t.output;
|
|
51
|
+
this.total.cache_read += t.cache_read;
|
|
52
|
+
this.total.cache_creation += t.cache_creation;
|
|
53
|
+
this.total.sum += sum;
|
|
54
|
+
this.total.cost_usd += cost;
|
|
55
|
+
|
|
56
|
+
for (const [map, key] of [[this.byTool, entry.tool_name], [this.byAction, entry.action_type], [this.byModel, entry.model]]) {
|
|
57
|
+
if (!key) continue;
|
|
58
|
+
const b = this._bucket(map, key);
|
|
59
|
+
b.input += t.input; b.output += t.output;
|
|
60
|
+
b.cache_read += t.cache_read; b.cache_creation += t.cache_creation;
|
|
61
|
+
b.sum += sum; b.cost_usd += cost; b.calls += 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
stats() {
|
|
66
|
+
const toObj = m => Object.fromEntries([...m.entries()].map(([k, v]) => [k, { ...v, cost_usd: round6(v.cost_usd) }]));
|
|
67
|
+
return {
|
|
68
|
+
total: { ...this.total, cost_usd: round6(this.total.cost_usd) },
|
|
69
|
+
by_tool: toObj(this.byTool),
|
|
70
|
+
by_action: toObj(this.byAction),
|
|
71
|
+
by_model: toObj(this.byModel),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function round6(n) { return Math.round(n * 1_000_000) / 1_000_000; }
|