agentmetrics-openclaw 0.2.4 → 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.
Files changed (2) hide show
  1. package/package.json +54 -40
  2. package/index.ts +0 -1197
package/package.json CHANGED
@@ -1,40 +1,54 @@
1
- {
2
- "name": "agentmetrics-openclaw",
3
- "version": "0.2.4",
4
- "type": "module",
5
- "description": "AgentMetrics observability plugin for OpenClaw agents",
6
- "license": "MIT",
7
- "homepage": "https://github.com/andalabx/agentmetrics",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/andalabx/agentmetrics.git"
11
- },
12
- "keywords": [
13
- "openclaw",
14
- "agentmetrics",
15
- "observability",
16
- "ai-agents",
17
- "monitoring"
18
- ],
19
- "peerDependencies": {
20
- "openclaw": ">=2026.3.2"
21
- },
22
- "peerDependenciesMeta": {
23
- "openclaw": {
24
- "optional": true
25
- }
26
- },
27
- "openclaw": {
28
- "extensions": [
29
- "./index.ts"
30
- ]
31
- },
32
- "files": [
33
- "index.ts",
34
- "openclaw.plugin.json",
35
- "README.md"
36
- ],
37
- "engines": {
38
- "node": ">=22"
39
- }
40
- }
1
+ {
2
+ "name": "agentmetrics-openclaw",
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "description": "AgentMetrics observability plugin for OpenClaw agents",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/andalabx/agentmetrics",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/andalabx/agentmetrics.git"
11
+ },
12
+ "keywords": [
13
+ "openclaw",
14
+ "agentmetrics",
15
+ "observability",
16
+ "ai-agents",
17
+ "monitoring"
18
+ ],
19
+ "exports": {
20
+ ".": "./dist/index.js"
21
+ },
22
+ "openclaw": {
23
+ "extensions": [
24
+ "./dist/index.js"
25
+ ]
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "openclaw.plugin.json",
30
+ "README.md"
31
+ ],
32
+ "scripts": {
33
+ "build": "tsup",
34
+ "prepublishOnly": "npm run build"
35
+ },
36
+ "peerDependencies": {
37
+ "openclaw": ">=2026.3.2"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "openclaw": {
41
+ "optional": true
42
+ }
43
+ },
44
+ "devDependencies": {
45
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
46
+ "@typescript-eslint/parser": "^8.0.0",
47
+ "eslint": "^9.0.0",
48
+ "tsup": "^8.0.0",
49
+ "typescript": "^5.4.0"
50
+ },
51
+ "engines": {
52
+ "node": ">=22"
53
+ }
54
+ }
package/index.ts DELETED
@@ -1,1197 +0,0 @@
1
- import { randomUUID } from "crypto";
2
- import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
- import { gzipSync } from "zlib";
4
- import { dirname, join } from "path";
5
- function _hashName(name: string): string {
6
- let h = 0x811c9dc5;
7
- for (let i = 0; i < name.length; i++) {
8
- h = (Math.imul(h ^ name.charCodeAt(i), 0x01000193)) >>> 0;
9
- }
10
- return `t_${h.toString(16).padStart(8, "0")}`;
11
- }
12
-
13
- const _SECRET_PATTERNS: RegExp[] = [
14
- /sk-[A-Za-z0-9\-_]{20,}/g,
15
- /am_[A-Za-z0-9\-_]{16,}/g,
16
- /\bey[A-Za-z0-9\-_]{20,}\.[A-Za-z0-9\-_]{20,}/g,
17
- /(?:api[_\-]?key|apikey|api[_\-]?token|access[_\-]?token|secret|password|passwd|auth)[=:\s"']+([^\s"'&,\]}\n]{8,})/gi,
18
- ];
19
-
20
- function _scrubSecrets(str: string): string {
21
- let out = str;
22
- for (const re of _SECRET_PATTERNS) {
23
- out = out.replace(re, "[REDACTED]");
24
- }
25
- return out;
26
- }
27
-
28
- const _PRICING: Record<string, [number, number, number?, number?]> = {
29
- "claude-opus-4-7": [15.0, 75.0, 1.50, 18.75],
30
- "claude-opus-4-5": [15.0, 75.0, 1.50, 18.75],
31
- "claude-opus-4": [15.0, 75.0, 1.50, 18.75],
32
- "claude-sonnet-4-6": [ 3.0, 15.0, 0.30, 3.75],
33
- "claude-sonnet-4-5": [ 3.0, 15.0, 0.30, 3.75],
34
- "claude-haiku-4-5": [ 0.8, 4.0, 0.08, 1.00],
35
- "claude-sonnet-3-7": [ 3.0, 15.0, 0.30, 3.75],
36
- "claude-3-5-sonnet-20241022": [ 3.0, 15.0, 0.30, 3.75],
37
- "claude-3-5-sonnet-20240620": [ 3.0, 15.0, 0.30, 3.75],
38
- "claude-3-5-haiku-20241022": [ 0.8, 4.0, 0.08, 1.00],
39
- "claude-3-opus-20240229": [15.0, 75.0, 1.50, 18.75],
40
- "claude-3-sonnet-20240229": [ 3.0, 15.0],
41
- "claude-3-haiku-20240307": [ 0.25, 1.25, 0.03, 0.30],
42
- "gpt-4o": [ 2.5, 10.0],
43
- "gpt-4o-mini": [ 0.15, 0.60],
44
- "gpt-4-turbo": [10.0, 30.0],
45
- "gpt-4": [30.0, 60.0],
46
- "gpt-3.5-turbo": [ 0.50, 1.50],
47
- "gemini-2.0-flash": [ 0.075, 0.30],
48
- "gemini-2.5-pro": [ 1.25, 10.0],
49
- "gemini-1.5-pro": [ 1.25, 5.0],
50
- "gemini-1.5-flash": [ 0.075, 0.30],
51
- };
52
-
53
-
54
- let API_KEY: string | undefined;
55
- let BASE_URL: string;
56
-
57
- let ENABLED = true;
58
- let REDACTION_MODE: "strict" | "moderate" | "debug" = "strict";
59
- let EXPORTED_TOOL_NAMES: "allowlist" | "blocklist" | "hash" | "off" = "blocklist";
60
- let REDACT_TOOL_NAMES: string[] = [];
61
- let DEBUG_EXPIRES_AT: number | null = null;
62
-
63
- let FLUSH_INTERVAL_MS = 10_000;
64
- let MAX_BATCH_SIZE = 100;
65
- let MAX_QUEUE_SIZE = 10_000;
66
- let RETRY_MAX_ATTEMPTS = 5;
67
- let COMPRESS_PAYLOADS = false;
68
-
69
-
70
- const _metrics = { sent: 0, failed: 0, dropped: 0 };
71
-
72
-
73
- interface QueuedEvent {
74
- payload: Record<string, unknown>;
75
- attempt: number;
76
- enqueuedAt: number;
77
- }
78
- const _queue: QueuedEvent[] = [];
79
- const _dlq: QueuedEvent[] = [];
80
-
81
-
82
- const CB_THRESHOLD = 10;
83
- const CB_PROBE_MS = 5 * 60_000;
84
- type CbState = "closed" | "open" | "half-open";
85
- let _cbState: CbState = "closed";
86
- let _cbConsecFails = 0;
87
- let _cbOpenAt: number | null = null;
88
-
89
-
90
- let WAL_PATH: string | null = null;
91
- let _flushTimer: ReturnType<typeof setInterval> | null = null;
92
-
93
- // Duplicate-registration guard
94
- let _registered = false;
95
-
96
-
97
- interface PluginApi {
98
- config: Record<string, unknown>;
99
- pluginConfig?: Record<string, unknown>;
100
- registerAutoEnableProbe?: (probe: () => boolean) => void;
101
- registerCli?: (registrar: {
102
- name: string;
103
- description: string;
104
- commands: Array<{ name: string; description: string; handler: () => void | Promise<void> }>;
105
- }) => void;
106
- on: (hookName: string, handler: (...args: unknown[]) => void) => void;
107
- }
108
-
109
-
110
- type AgentContext = {
111
- runId?: string;
112
- agentId?: string;
113
- sessionKey?: string;
114
- sessionId?: string;
115
- modelId?: string;
116
- modelProviderId?: string;
117
- };
118
-
119
- type SessionContext = {
120
- sessionId: string;
121
- sessionKey?: string;
122
- agentId?: string;
123
- };
124
-
125
- type SessionStartEvent = {
126
- sessionId: string;
127
- sessionKey?: string;
128
- resumedFrom?: string;
129
- };
130
-
131
- type SessionEndEvent = {
132
- sessionId: string;
133
- sessionKey?: string;
134
- durationMs?: number;
135
- messageCount: number;
136
- reason?: string;
137
- transcriptArchived?: boolean;
138
- };
139
-
140
- type LlmInputEvent = {
141
- runId: string;
142
- sessionId: string;
143
- provider: string;
144
- model: string;
145
- systemPrompt?: string;
146
- prompt: string;
147
- historyMessages: unknown[];
148
- imagesCount: number;
149
- };
150
-
151
- type LlmOutputEvent = {
152
- runId: string;
153
- sessionId: string;
154
- provider: string;
155
- model: string;
156
- assistantTexts: string[];
157
- usage?: {
158
- input?: number;
159
- output?: number;
160
- cacheRead?: number;
161
- cacheWrite?: number;
162
- total?: number;
163
- };
164
- };
165
-
166
- type BeforeToolCallEvent = {
167
- toolName: string;
168
- params: Record<string, unknown>;
169
- runId?: string;
170
- toolCallId?: string;
171
- };
172
-
173
- type AfterToolCallEvent = {
174
- toolName: string;
175
- params: Record<string, unknown>;
176
- runId?: string;
177
- toolCallId?: string;
178
- result?: unknown;
179
- error?: string;
180
- durationMs?: number;
181
- };
182
-
183
- type ToolContext = {
184
- agentId?: string;
185
- sessionKey?: string;
186
- sessionId?: string;
187
- runId?: string;
188
- toolName: string;
189
- toolCallId?: string;
190
- };
191
-
192
- type AgentEndEvent = {
193
- messages: unknown[];
194
- success: boolean;
195
- error?: string;
196
- durationMs?: number;
197
- };
198
-
199
- type BeforeAgentStartEvent = {
200
- agentId?: string;
201
- sessionKey?: string;
202
- sessionId?: string;
203
- };
204
-
205
- type SubagentSpawningEvent = {
206
- childSessionKey: string;
207
- agentId: string;
208
- label?: string;
209
- mode: "run" | "session";
210
- threadRequested: boolean;
211
- };
212
-
213
- type SubagentEndedEvent = {
214
- targetSessionKey: string;
215
- reason: string;
216
- runId?: string;
217
- outcome?: "ok" | "error" | "timeout" | "killed" | "reset" | "deleted";
218
- error?: string;
219
- };
220
-
221
- type SubagentContext = {
222
- runId?: string;
223
- childSessionKey?: string;
224
- requesterSessionKey?: string;
225
- };
226
-
227
- type CompactionEvent = {
228
- messageCount: number;
229
- compactingCount?: number;
230
- tokenCount?: number;
231
- sessionFile?: string;
232
- };
233
-
234
- type ResetEvent = {
235
- sessionFile?: string;
236
- reason?: string;
237
- };
238
-
239
- type GatewayStartEvent = { port: number };
240
- type GatewayStopEvent = { reason?: string };
241
- type GatewayContext = { port?: number };
242
-
243
-
244
- interface SessionMeta {
245
- traceId: string;
246
- agentId: string;
247
- startedAt: number;
248
- compactions: number;
249
- resets: number;
250
- // Session-level aggregates accumulated across all runs in this session
251
- runCount: number;
252
- totalInputTokens: number;
253
- totalOutputTokens: number;
254
- totalCacheReadTokens: number;
255
- totalCacheWriteTokens: number;
256
- totalToolCalls: number;
257
- totalEstimatedCostUsd: number;
258
- totalDurationMs: number;
259
- }
260
-
261
- interface RunMeta {
262
- inputTokens: number;
263
- outputTokens: number;
264
- cacheReadTokens: number;
265
- cacheWriteTokens: number;
266
- llmCalls: number;
267
- imagesCount: number;
268
- toolCalls: number;
269
- toolErrors: number;
270
- toolNames: Set<string>;
271
- subagentsSpawned: number;
272
- subagentErrors: number;
273
- model?: string;
274
- provider?: string;
275
- sessionKey?: string;
276
- startedAt: number;
277
- }
278
-
279
- const sessions = new Map<string, SessionMeta>();
280
- const runs = new Map<string, RunMeta>();
281
-
282
-
283
- function _walAppend(payload: Record<string, unknown>): void {
284
- if (!WAL_PATH) return;
285
- try {
286
- appendFileSync(WAL_PATH, JSON.stringify(payload) + "\n", "utf8");
287
- } catch { /* non-fatal */ }
288
- }
289
-
290
- function _walCompact(sentIds: Set<string>): void {
291
- if (!WAL_PATH || !existsSync(WAL_PATH)) return;
292
- try {
293
- const lines = readFileSync(WAL_PATH, "utf8").split("\n").filter(Boolean);
294
- const kept = lines.filter((line) => {
295
- try {
296
- const ev = JSON.parse(line) as Record<string, unknown>;
297
- return !sentIds.has(String(ev.event_id ?? ""));
298
- } catch { return false; }
299
- });
300
- writeFileSync(WAL_PATH, kept.length ? kept.join("\n") + "\n" : "", "utf8");
301
- } catch { /* non-fatal */ }
302
- }
303
-
304
- function _walRecover(): void {
305
- if (!WAL_PATH || !existsSync(WAL_PATH)) return;
306
- try {
307
- const lines = readFileSync(WAL_PATH, "utf8").split("\n").filter(Boolean);
308
- for (const line of lines) {
309
- try {
310
- const payload = JSON.parse(line) as Record<string, unknown>;
311
- _enqueue(payload, true);
312
- } catch { /* skip corrupt lines */ }
313
- }
314
- if (lines.length > 0) {
315
- console.log(`AgentMetrics: recovered ${lines.length} event(s) from WAL`);
316
- }
317
- } catch { /* non-fatal */ }
318
- }
319
-
320
-
321
- function _enqueue(payload: Record<string, unknown>, fromWal = false): void {
322
- if (_queue.length >= MAX_QUEUE_SIZE) {
323
- _queue.shift(); // FIFO: drop oldest on overflow
324
- _metrics.dropped += 1;
325
- }
326
- _queue.push({ payload, attempt: 0, enqueuedAt: Date.now() });
327
- if (!fromWal) _walAppend(payload);
328
- }
329
-
330
-
331
- function _cbIsOpen(): boolean {
332
- if (_cbState === "closed") return false;
333
- if (_cbState === "open") {
334
- if (_cbOpenAt !== null && Date.now() - _cbOpenAt >= CB_PROBE_MS) {
335
- _cbState = "half-open";
336
- return false; // let one probe through
337
- }
338
- return true;
339
- }
340
- return false; // half-open: probe is allowed
341
- }
342
-
343
- function _cbOnSuccess(): void {
344
- if (_cbState !== "closed") {
345
- console.log("AgentMetrics: circuit breaker closed - delivery resumed");
346
- }
347
- _cbState = "closed";
348
- _cbConsecFails = 0;
349
- _cbOpenAt = null;
350
- }
351
-
352
- function _cbOnFailure(): void {
353
- _cbConsecFails += 1;
354
- if (_cbState === "half-open" || _cbConsecFails >= CB_THRESHOLD) {
355
- _cbState = "open";
356
- _cbOpenAt = Date.now();
357
- console.log(
358
- `AgentMetrics: circuit breaker opened after ${_cbConsecFails} consecutive failures - ` +
359
- `probing again in ${CB_PROBE_MS / 60_000}min`,
360
- );
361
- }
362
- }
363
-
364
-
365
- function _buildHeaders(): Record<string, string> {
366
- return {
367
- "Content-Type": "application/json",
368
- "Authorization": `Bearer ${API_KEY}`,
369
- };
370
- }
371
-
372
- function _maybeGzip(body: string): { body: string | Uint8Array; extra: Record<string, string> } {
373
- if (COMPRESS_PAYLOADS && body.length > 1024) {
374
- try {
375
- return {
376
- body: gzipSync(Buffer.from(body, "utf8")),
377
- extra: { "Content-Encoding": "gzip" },
378
- };
379
- } catch { /* fall through */ }
380
- }
381
- return { body, extra: {} };
382
- }
383
-
384
- function _requeue(batch: QueuedEvent[]): void {
385
- for (const item of batch) {
386
- item.attempt += 1;
387
- _metrics.failed += 1;
388
- if (item.attempt >= RETRY_MAX_ATTEMPTS) {
389
- _dlq.push(item);
390
- } else {
391
- _queue.push(item); // back of queue, not front - prevent starvation
392
- }
393
- }
394
- }
395
-
396
- async function _flushBatch(batch: QueuedEvent[]): Promise<void> {
397
- const rawBody = JSON.stringify({ events: batch.map((e) => e.payload) });
398
- const { body, extra } = _maybeGzip(rawBody);
399
- try {
400
- const resp = await fetch(`${BASE_URL}/v1/events/batch`, {
401
- method: "POST",
402
- headers: { ..._buildHeaders(), ...extra },
403
- body,
404
- });
405
- if (resp.ok) {
406
- _cbOnSuccess();
407
- _metrics.sent += batch.length;
408
- _walCompact(new Set(batch.map((e) => String(e.payload.event_id ?? ""))));
409
- } else if (resp.status === 404) {
410
- await _flushIndividual(batch); // batch endpoint not yet available
411
- } else {
412
- _cbOnFailure();
413
- _requeue(batch);
414
- }
415
- } catch {
416
- _cbOnFailure();
417
- _requeue(batch);
418
- }
419
- }
420
-
421
- async function _flushIndividual(batch: QueuedEvent[]): Promise<void> {
422
- for (const item of batch) {
423
- const rawBody = JSON.stringify(item.payload);
424
- const { body, extra } = _maybeGzip(rawBody);
425
- try {
426
- const resp = await fetch(`${BASE_URL}/v1/events`, {
427
- method: "POST",
428
- headers: { ..._buildHeaders(), ...extra },
429
- body,
430
- });
431
- if (resp.ok) {
432
- _cbOnSuccess();
433
- _metrics.sent += 1;
434
- _walCompact(new Set([String(item.payload.event_id ?? "")]));
435
- } else {
436
- _cbOnFailure();
437
- _requeue([item]);
438
- }
439
- } catch {
440
- _cbOnFailure();
441
- _requeue([item]);
442
- }
443
- }
444
- }
445
-
446
- async function _flush(): Promise<void> {
447
- if (!API_KEY || _queue.length === 0 || _cbIsOpen()) return;
448
- const batch = _queue.splice(0, MAX_BATCH_SIZE);
449
- await _flushBatch(batch);
450
- }
451
-
452
-
453
- /** Enqueue a completed run summary; delivered by the flush loop. */
454
- function send(payload: Record<string, unknown>): void {
455
- if (!API_KEY || !ENABLED) return;
456
- _enqueue(payload);
457
- }
458
-
459
- /**
460
- * Send a real-time intermediate event to /v1/activity (in-memory, streamed
461
- * to dashboard via SSE). Fire-and-forget - no retry, no crash on failure.
462
- */
463
- function sendActivity(
464
- type: string,
465
- agentId: string,
466
- sessionKey: string | undefined,
467
- runId: string | undefined,
468
- data?: Record<string, unknown>,
469
- ): void {
470
- if (!API_KEY || !ENABLED) return;
471
- const payload = JSON.stringify({
472
- type,
473
- agent_id: agentId,
474
- session_key: sessionKey,
475
- run_id: runId,
476
- ts: Date.now(),
477
- data: data ?? null,
478
- });
479
- fetch(`${BASE_URL}/v1/activity`, {
480
- method: "POST",
481
- headers: {
482
- "Content-Type": "application/json",
483
- "Authorization": `Bearer ${API_KEY}`,
484
- },
485
- body: payload,
486
- }).catch(() => {});
487
- }
488
-
489
- function emptyRun(sessionKey?: string): RunMeta {
490
- return {
491
- inputTokens: 0, outputTokens: 0,
492
- cacheReadTokens: 0, cacheWriteTokens: 0,
493
- llmCalls: 0, imagesCount: 0,
494
- toolCalls: 0, toolErrors: 0, toolNames: new Set(),
495
- subagentsSpawned: 0, subagentErrors: 0,
496
- sessionKey,
497
- startedAt: Date.now(),
498
- };
499
- }
500
-
501
-
502
-
503
- function _activeMode(): "strict" | "moderate" | "debug" {
504
- if (DEBUG_EXPIRES_AT !== null) {
505
- if (Date.now() < DEBUG_EXPIRES_AT) return "debug";
506
- DEBUG_EXPIRES_AT = null;
507
- console.log("AgentMetrics: debug mode expired - reverting to strict redaction");
508
- }
509
- return REDACTION_MODE;
510
- }
511
-
512
- function _redactError(err: string | undefined, activityStream = false): string | undefined {
513
- if (!err) return err;
514
- const mode = _activeMode();
515
- if (mode === "debug") return err;
516
- const maxLen = (mode === "strict" || activityStream) ? 200 : 500;
517
- return _scrubSecrets(err).slice(0, maxLen);
518
- }
519
-
520
- function _redactToolName(name: string): string {
521
- const mode = _activeMode();
522
- if (mode === "debug") return name;
523
- // allowlist mode: redactToolNames is the permitted list; blocklist mode: it's the deny list
524
- switch (EXPORTED_TOOL_NAMES) {
525
- case "off":
526
- return "[REDACTED]";
527
- case "hash":
528
- return _hashName(name);
529
- case "allowlist":
530
- return REDACT_TOOL_NAMES.includes(name) ? name : `[REDACTED:${_hashName(name)}]`;
531
- case "blocklist":
532
- default:
533
- return REDACT_TOOL_NAMES.includes(name) ? `[REDACTED:${_hashName(name)}]` : name;
534
- }
535
- }
536
-
537
- function _redactToolNames(names: string[]): string[] {
538
- return names.map(_redactToolName);
539
- }
540
-
541
-
542
-
543
- function _estimateCost(
544
- model: string | undefined,
545
- input: number,
546
- output: number,
547
- cacheRead: number,
548
- cacheWrite: number,
549
- ): number | undefined {
550
- if (!model) return undefined;
551
- // Try exact match first, then strip date suffix (e.g. -20241022)
552
- const rates = _PRICING[model.toLowerCase()] ?? _PRICING[model.toLowerCase().replace(/-\d{8}$/, "")];
553
- if (!rates) return undefined;
554
- const M = 1_000_000;
555
- return (
556
- input * rates[0] / M +
557
- output * rates[1] / M +
558
- cacheRead * (rates[2] ?? 0) / M +
559
- cacheWrite * (rates[3] ?? 0) / M
560
- );
561
- }
562
-
563
-
564
- const plugin = {
565
- id: "agentmetrics",
566
- name: "AgentMetrics",
567
- description: "360-degree observability for every OpenClaw agent - real-time streaming, tokens, tools, latency, cost, subagents, and reliability.",
568
- configSchema: {
569
- type: "object",
570
- properties: {
571
- apiKey: {
572
- type: "string",
573
- description: "AgentMetrics API key (overrides AGENTMETRICS_API_KEY env var)",
574
- },
575
- endpoint: {
576
- type: "string",
577
- description: "Custom API endpoint (default: http://localhost:8099)",
578
- },
579
- enabled: {
580
- type: "boolean",
581
- description: "Disable the plugin without removing it (default: true)",
582
- },
583
- flushIntervalSeconds: {
584
- type: "number",
585
- description: "How often to flush the event queue to the API (default: 10)",
586
- },
587
- maxBatchSize: {
588
- type: "number",
589
- description: "Maximum events per batch request (default: 100)",
590
- },
591
- maxQueueSize: {
592
- type: "number",
593
- description: "Maximum in-memory queue depth before FIFO drop (default: 10000)",
594
- },
595
- retryMaxAttempts: {
596
- type: "number",
597
- description: "Max retry attempts before moving event to DLQ (default: 5)",
598
- },
599
- redactionMode: {
600
- type: "string",
601
- enum: ["strict", "moderate", "debug"],
602
- description: "PII redaction level applied to prompts/completions (default: strict)",
603
- },
604
- exportedToolNames: {
605
- type: "string",
606
- enum: ["allowlist", "blocklist", "hash", "off"],
607
- description: "Which tool names to include in exports (default: blocklist)",
608
- },
609
- redactToolNames: {
610
- type: "array",
611
- items: { type: "string" },
612
- description: "Tool names to redact when exportedToolNames is 'blocklist'",
613
- },
614
- compressPayloads: {
615
- type: "boolean",
616
- description: "Gzip-compress batch payloads larger than 1 KB (default: false)",
617
- },
618
- },
619
- additionalProperties: false,
620
- } as const,
621
-
622
- register(api: PluginApi) {
623
- if (_registered) {
624
- console.warn(
625
- "\n AgentMetrics: ⚠ register() called twice - possible duplicate instrumentation.\n" +
626
- " If you have both the plugin and an SDK hook active, remove one to avoid\n" +
627
- " double-counting runs and inflated token/cost totals.\n",
628
- );
629
- }
630
- _registered = true;
631
-
632
- API_KEY = (api.pluginConfig?.apiKey as string | undefined) ?? process.env.AGENTMETRICS_API_KEY;
633
- BASE_URL = (
634
- (api.pluginConfig?.endpoint as string | undefined) ??
635
- process.env.AGENTMETRICS_URL ??
636
- "http://localhost:8099"
637
- ).replace(/\/$/, "");
638
-
639
- ENABLED = (api.pluginConfig?.enabled as boolean | undefined) ?? true;
640
- REDACTION_MODE = (api.pluginConfig?.redactionMode as typeof REDACTION_MODE | undefined) ?? "strict";
641
- EXPORTED_TOOL_NAMES = (api.pluginConfig?.exportedToolNames as typeof EXPORTED_TOOL_NAMES | undefined) ?? "blocklist";
642
- REDACT_TOOL_NAMES = (api.pluginConfig?.redactToolNames as string[] | undefined) ?? [];
643
- FLUSH_INTERVAL_MS = ((api.pluginConfig?.flushIntervalSeconds as number | undefined) ?? 10) * 1000;
644
- MAX_BATCH_SIZE = (api.pluginConfig?.maxBatchSize as number | undefined) ?? 100;
645
- MAX_QUEUE_SIZE = (api.pluginConfig?.maxQueueSize as number | undefined) ?? 10_000;
646
- RETRY_MAX_ATTEMPTS = (api.pluginConfig?.retryMaxAttempts as number | undefined) ?? 5;
647
- COMPRESS_PAYLOADS = (api.pluginConfig?.compressPayloads as boolean | undefined) ?? false;
648
-
649
- if (REDACTION_MODE === "debug") {
650
- DEBUG_EXPIRES_AT = Date.now() + 60 * 60 * 1000;
651
- console.log("AgentMetrics: ⚠ debug redaction mode active - expires in 1 hour");
652
- }
653
-
654
- if (typeof api.registerAutoEnableProbe === "function") {
655
- api.registerAutoEnableProbe(() => !!API_KEY && ENABLED);
656
- }
657
-
658
- if (!ENABLED) {
659
- console.log("\n AgentMetrics: disabled via config (metrics.enabled: false)\n");
660
- return;
661
- }
662
-
663
- if (!API_KEY) {
664
- console.log(
665
- "\n AgentMetrics: no API key found.\n" +
666
- " Your agent runs are not being tracked.\n" +
667
- " Start AgentMetrics (see README) and set AGENTMETRICS_API_KEY.\n" +
668
- " AGENTMETRICS_URL defaults to http://localhost:8099.\n",
669
- );
670
- return;
671
- }
672
-
673
- try {
674
- const home = process.env.HOME ?? process.env.USERPROFILE ?? process.cwd();
675
- WAL_PATH = join(home, ".config", "openclaw", "agentmetrics-wal.jsonl");
676
- mkdirSync(dirname(WAL_PATH), { recursive: true });
677
- try { chmodSync(dirname(WAL_PATH), 0o700); } catch {}
678
- _walRecover();
679
- } catch {
680
- WAL_PATH = null; // WAL unavailable - queue still works in-memory
681
- }
682
-
683
- if (_flushTimer) clearInterval(_flushTimer);
684
- _flushTimer = setInterval(() => { _flush().catch(() => {}); }, FLUSH_INTERVAL_MS);
685
- // Allow process to exit even with an active timer
686
- if (typeof (_flushTimer as unknown as { unref?: () => void }).unref === "function") {
687
- (_flushTimer as unknown as { unref: () => void }).unref();
688
- }
689
-
690
- console.log(
691
- `\n AgentMetrics active - sending data to ${BASE_URL}\n` +
692
- ` Queue: max ${MAX_QUEUE_SIZE} events, batch ${MAX_BATCH_SIZE}, flush every ${FLUSH_INTERVAL_MS / 1000}s\n` +
693
- ` View your dashboard → http://localhost:3099\n`,
694
- );
695
-
696
- if (typeof api.registerCli === "function") {
697
- api.registerCli({
698
- name: "agentmetrics",
699
- description: "AgentMetrics observability commands",
700
- commands: [
701
- {
702
- name: "status",
703
- description: "Show current plugin status, config, delivery counters, and circuit breaker state",
704
- handler() {
705
- const keyPreview = API_KEY
706
- ? `${API_KEY.slice(0, 8)}...${API_KEY.slice(-4)}`
707
- : "(not set)";
708
- const mode = _activeMode();
709
- const cbInfo = _cbState === "open" && _cbOpenAt
710
- ? ` (opens probe at ${new Date(_cbOpenAt + CB_PROBE_MS).toLocaleTimeString()})`
711
- : "";
712
- console.log("AgentMetrics - status");
713
- console.log(` API key : ${keyPreview}`);
714
- console.log(` Endpoint : ${BASE_URL}`);
715
- console.log(` Redaction : ${mode}${mode === "debug" && DEBUG_EXPIRES_AT ? ` (expires ${new Date(DEBUG_EXPIRES_AT).toLocaleTimeString()})` : ""}`);
716
- console.log(` Tool names : ${EXPORTED_TOOL_NAMES}`);
717
- console.log(` Compress payloads: ${COMPRESS_PAYLOADS}`);
718
- console.log(` Flush interval : ${FLUSH_INTERVAL_MS / 1000}s`);
719
- console.log(` WAL path : ${WAL_PATH ?? "(unavailable)"}`);
720
- console.log("");
721
- console.log(` Circuit breaker : ${_cbState}${cbInfo}`);
722
- console.log(` Queue depth : ${_queue.length} / ${MAX_QUEUE_SIZE}`);
723
- console.log(` DLQ depth : ${_dlq.length}`);
724
- console.log(` Sessions tracked : ${sessions.size}`);
725
- console.log(` Runs in flight : ${runs.size}`);
726
- console.log("");
727
- console.log(` Sent : ${_metrics.sent}`);
728
- console.log(` Failed : ${_metrics.failed}`);
729
- console.log(` Dropped (overflow): ${_metrics.dropped}`);
730
- },
731
- },
732
- {
733
- name: "flush",
734
- description: "Force-flush all queued events immediately",
735
- async handler() {
736
- const before = _queue.length;
737
- if (before === 0) {
738
- console.log("AgentMetrics flush - queue empty, nothing to flush");
739
- return;
740
- }
741
- if (_cbIsOpen()) {
742
- console.log(`AgentMetrics flush - circuit breaker is ${_cbState}, skipping`);
743
- return;
744
- }
745
- console.log(`AgentMetrics flush - flushing ${before} event(s)…`);
746
- // Drain the entire queue in batches
747
- while (_queue.length > 0 && !_cbIsOpen()) {
748
- await _flush();
749
- }
750
- console.log(` Done - sent: ${_metrics.sent}, failed: ${_metrics.failed}, queued: ${_queue.length}`);
751
- },
752
- },
753
- {
754
- name: "tail",
755
- description: "Show recent in-flight run state",
756
- handler() {
757
- if (runs.size === 0 && sessions.size === 0) {
758
- console.log("AgentMetrics tail - no active sessions or runs");
759
- return;
760
- }
761
- console.log("AgentMetrics tail - active state");
762
- if (sessions.size > 0) {
763
- console.log(` Sessions (${sessions.size}):`);
764
- for (const [key, s] of sessions) {
765
- console.log(` ${key.slice(0, 12)}… agent=${s.agentId} compactions=${s.compactions} resets=${s.resets}`);
766
- }
767
- }
768
- if (runs.size > 0) {
769
- console.log(` Runs in flight (${runs.size}):`);
770
- for (const [id, r] of runs) {
771
- const age = Math.round((Date.now() - r.startedAt) / 1000);
772
- console.log(` ${id.slice(0, 12)}… llm=${r.llmCalls} tools=${r.toolCalls} ${age}s elapsed`);
773
- }
774
- }
775
- },
776
- },
777
- {
778
- name: "test",
779
- description: "Send a test event and verify end-to-end delivery",
780
- async handler() {
781
- if (!API_KEY) {
782
- console.log("AgentMetrics test - no API key set, cannot send");
783
- return;
784
- }
785
- console.log(`AgentMetrics test - sending to ${BASE_URL}…`);
786
- try {
787
- const resp = await fetch(`${BASE_URL}/v1/events`, {
788
- method: "POST",
789
- headers: {
790
- "Content-Type": "application/json",
791
- "Authorization": `Bearer ${API_KEY}`,
792
- },
793
- body: JSON.stringify({
794
- event_id: randomUUID(),
795
- trace_id: randomUUID(),
796
- agent_id: "agentmetrics-test",
797
- platform: "openclaw",
798
- event_name: "agent_end",
799
- ts: Date.now(),
800
- status: "success",
801
- duration_ms: 1,
802
- redaction_policy_version: `v1-${_activeMode()}`,
803
- }),
804
- });
805
- if (resp.ok) {
806
- console.log(` ✓ Delivered - HTTP ${resp.status}`);
807
- } else {
808
- const body = await resp.text().catch(() => "");
809
- console.log(` ✗ Failed - HTTP ${resp.status} ${body.slice(0, 200)}`);
810
- }
811
- } catch (err) {
812
- console.log(` ✗ Failed - ${err}`);
813
- }
814
- },
815
- },
816
- {
817
- name: "redaction-check",
818
- description: "Show what the current redaction policy does to a sample payload",
819
- handler() {
820
- const mode = _activeMode();
821
- const sampleError = "Connection failed: Bearer sk-ant-abc123exampletoken and api_key=supersecret";
822
- const sampleTools = ["bash", "read_file", "write_file", "send_email"];
823
- const redactedError = _redactError(sampleError);
824
- const redactedTools = _redactToolNames(sampleTools);
825
- console.log("AgentMetrics redaction-check");
826
- console.log(` Mode : ${mode}`);
827
- console.log(` Tool export : ${EXPORTED_TOOL_NAMES}`);
828
- console.log(` Blocked names : ${REDACT_TOOL_NAMES.length ? REDACT_TOOL_NAMES.join(", ") : "(none)"}`);
829
- console.log("");
830
- console.log(" Error sample:");
831
- console.log(` Input : ${sampleError}`);
832
- console.log(` Output : ${redactedError}`);
833
- console.log("");
834
- console.log(" Tool name sample:");
835
- sampleTools.forEach((t, i) =>
836
- console.log(` ${t.padEnd(16)} → ${redactedTools[i]}`),
837
- );
838
- },
839
- },
840
- {
841
- name: "drain",
842
- description: "Retry all events in the dead-letter queue",
843
- async handler() {
844
- if (_dlq.length === 0) {
845
- console.log("AgentMetrics drain - DLQ is empty");
846
- return;
847
- }
848
- const count = _dlq.length;
849
- console.log(`AgentMetrics drain - retrying ${count} DLQ event(s)…`);
850
- // Reset attempts and move DLQ back to main queue
851
- const items = _dlq.splice(0, _dlq.length);
852
- for (const item of items) {
853
- item.attempt = 0;
854
- _queue.push(item);
855
- }
856
- // Flush immediately
857
- while (_queue.length > 0 && !_cbIsOpen()) {
858
- await _flush();
859
- }
860
- console.log(` Done - sent: ${_metrics.sent}, failed: ${_metrics.failed}, remaining DLQ: ${_dlq.length}`);
861
- },
862
- },
863
- ],
864
- });
865
- }
866
-
867
- api.on("gateway_start", (event: GatewayStartEvent, _ctx: GatewayContext) => {
868
- sendActivity("gateway_start", "openclaw-gateway", undefined, undefined, {
869
- port: event.port,
870
- });
871
- });
872
-
873
- api.on("gateway_stop", (event: GatewayStopEvent, _ctx: GatewayContext) => {
874
- sendActivity("gateway_stop", "openclaw-gateway", undefined, undefined, {
875
- reason: event.reason,
876
- });
877
- });
878
-
879
- api.on("session_start", (event: SessionStartEvent, ctx: SessionContext) => {
880
- const key = event.sessionKey ?? event.sessionId;
881
- if (!key || sessions.has(key)) return;
882
-
883
- sessions.set(key, {
884
- traceId: randomUUID(),
885
- agentId: ctx.agentId ?? "openclaw-agent",
886
- startedAt: Date.now(),
887
- compactions: 0,
888
- resets: 0,
889
- runCount: 0,
890
- totalInputTokens: 0,
891
- totalOutputTokens: 0,
892
- totalCacheReadTokens: 0,
893
- totalCacheWriteTokens: 0,
894
- totalToolCalls: 0,
895
- totalEstimatedCostUsd: 0,
896
- totalDurationMs: 0,
897
- });
898
- });
899
-
900
- api.on("before_agent_start", (event: BeforeAgentStartEvent, ctx: AgentContext) => {
901
- const sessionKey = ctx.sessionKey ?? ctx.sessionId;
902
- const agentId = ctx.agentId ?? "openclaw-agent";
903
- const runId = ctx.runId;
904
-
905
- if (!runId && !sessionKey) return;
906
-
907
- sendActivity("run_start", agentId, sessionKey, runId, {
908
- model: ctx.modelId,
909
- provider: ctx.modelProviderId,
910
- });
911
- });
912
-
913
- api.on("llm_input", (event: LlmInputEvent, ctx: AgentContext) => {
914
- const { runId } = event;
915
- if (!runId) return;
916
-
917
- const sessionKey = ctx.sessionKey ?? ctx.sessionId ?? event.sessionId;
918
- const agentId = ctx.agentId ?? sessions.get(sessionKey ?? "")?.agentId ?? "openclaw-agent";
919
- const run = runs.get(runId) ?? emptyRun(sessionKey);
920
-
921
- run.llmCalls += 1;
922
- run.imagesCount += event.imagesCount ?? 0;
923
- if (!run.model && event.model) run.model = event.model;
924
- if (!run.provider && event.provider) run.provider = event.provider;
925
- if (!run.sessionKey) run.sessionKey = sessionKey;
926
- runs.set(runId, run);
927
-
928
- sendActivity("llm_start", agentId, sessionKey, runId, {
929
- model: event.model,
930
- provider: event.provider,
931
- images: event.imagesCount,
932
- history: event.historyMessages?.length ?? 0,
933
- });
934
- });
935
-
936
- api.on("llm_output", (event: LlmOutputEvent, ctx: AgentContext) => {
937
- const { runId } = event;
938
- if (!runId) return;
939
-
940
- const sessionKey = ctx.sessionKey ?? ctx.sessionId ?? event.sessionId;
941
- const agentId = ctx.agentId ?? sessions.get(sessionKey ?? "")?.agentId ?? "openclaw-agent";
942
- const run = runs.get(runId) ?? emptyRun(sessionKey);
943
-
944
- if (event.usage) {
945
- run.inputTokens += event.usage.input ?? 0;
946
- run.outputTokens += event.usage.output ?? 0;
947
- run.cacheReadTokens += event.usage.cacheRead ?? 0;
948
- run.cacheWriteTokens += event.usage.cacheWrite ?? 0;
949
- }
950
- if (event.model) run.model = event.model;
951
- if (event.provider) run.provider = event.provider;
952
- if (!run.sessionKey) run.sessionKey = sessionKey;
953
- runs.set(runId, run);
954
-
955
- sendActivity("llm_end", agentId, sessionKey, runId, {
956
- model: event.model,
957
- provider: event.provider,
958
- input_tokens: event.usage?.input,
959
- output_tokens: event.usage?.output,
960
- cache_read: event.usage?.cacheRead,
961
- cache_write: event.usage?.cacheWrite,
962
- total_input: run.inputTokens,
963
- total_output: run.outputTokens,
964
- });
965
- });
966
-
967
- api.on("before_tool_call", (event: BeforeToolCallEvent, ctx: ToolContext) => {
968
- const runId = event.runId ?? ctx.runId;
969
- const sessionKey = ctx.sessionKey ?? ctx.sessionId;
970
- const agentId = ctx.agentId ?? sessions.get(sessionKey ?? "")?.agentId ?? "openclaw-agent";
971
-
972
- sendActivity("tool_start", agentId, sessionKey, runId, {
973
- tool_name: event.toolName,
974
- });
975
- });
976
-
977
- api.on("after_tool_call", (event: AfterToolCallEvent, ctx: ToolContext) => {
978
- const runId = event.runId ?? ctx.runId;
979
- const sessionKey = ctx.sessionKey ?? ctx.sessionId;
980
- const agentId = ctx.agentId ?? sessions.get(sessionKey ?? "")?.agentId ?? "openclaw-agent";
981
-
982
- if (runId) {
983
- const run = runs.get(runId) ?? emptyRun(sessionKey);
984
- run.toolCalls += 1;
985
- if (event.error) run.toolErrors += 1;
986
- run.toolNames.add(event.toolName);
987
- if (!run.sessionKey) run.sessionKey = sessionKey;
988
- runs.set(runId, run);
989
- }
990
-
991
- sendActivity("tool_end", agentId, sessionKey, runId, {
992
- tool_name: _redactToolName(event.toolName),
993
- duration_ms: event.durationMs,
994
- error: _redactError(event.error, true),
995
- });
996
- });
997
-
998
- api.on("subagent_spawning", (event: SubagentSpawningEvent, ctx: SubagentContext) => {
999
- const runId = ctx.runId;
1000
- const sessionKey = ctx.requesterSessionKey;
1001
- const agentId = sessions.get(sessionKey ?? "")?.agentId ?? "openclaw-agent";
1002
-
1003
- if (runId) {
1004
- const run = runs.get(runId);
1005
- if (run) {
1006
- run.subagentsSpawned += 1;
1007
- runs.set(runId, run);
1008
- }
1009
- }
1010
-
1011
- sendActivity("subagent_start", agentId, sessionKey, runId, {
1012
- child_agent_id: event.agentId,
1013
- child_session_key: event.childSessionKey,
1014
- mode: event.mode,
1015
- label: event.label,
1016
- });
1017
- });
1018
-
1019
- api.on("subagent_ended", (event: SubagentEndedEvent, ctx: SubagentContext) => {
1020
- const runId = event.runId ?? ctx.runId;
1021
- const sessionKey = ctx.requesterSessionKey;
1022
- const agentId = sessions.get(sessionKey ?? "")?.agentId ?? "openclaw-agent";
1023
-
1024
- if (runId) {
1025
- const run = runs.get(runId);
1026
- if (run && event.outcome && event.outcome !== "ok") {
1027
- run.subagentErrors += 1;
1028
- runs.set(runId, run);
1029
- }
1030
- }
1031
-
1032
- sendActivity("subagent_end", agentId, sessionKey, runId, {
1033
- child_session_key: event.targetSessionKey,
1034
- outcome: event.outcome,
1035
- error: _redactError(event.error, true),
1036
- });
1037
- });
1038
-
1039
- api.on("before_compaction", (_event: CompactionEvent, ctx: AgentContext) => {
1040
- const key = ctx.sessionKey ?? ctx.sessionId;
1041
- const agentId = ctx.agentId ?? sessions.get(key ?? "")?.agentId ?? "openclaw-agent";
1042
- if (!key) return;
1043
-
1044
- const session = sessions.get(key);
1045
- if (session) {
1046
- session.compactions += 1;
1047
- sessions.set(key, session);
1048
- }
1049
-
1050
- sendActivity("compaction", agentId, key, ctx.runId);
1051
- });
1052
-
1053
- api.on("before_reset", (event: ResetEvent, ctx: AgentContext) => {
1054
- const key = ctx.sessionKey ?? ctx.sessionId;
1055
- const agentId = ctx.agentId ?? sessions.get(key ?? "")?.agentId ?? "openclaw-agent";
1056
- if (!key) return;
1057
-
1058
- const session = sessions.get(key);
1059
- if (session) {
1060
- session.resets += 1;
1061
- sessions.set(key, session);
1062
- }
1063
-
1064
- sendActivity("reset", agentId, key, ctx.runId, { reason: event.reason });
1065
- });
1066
-
1067
- api.on("agent_end", (event: AgentEndEvent, ctx: AgentContext) => {
1068
- const sessionKey = ctx.sessionKey ?? ctx.sessionId;
1069
- if (!sessionKey) return;
1070
-
1071
- const session = sessions.get(sessionKey);
1072
- const run = ctx.runId ? runs.get(ctx.runId) : undefined;
1073
- const agentId = ctx.agentId ?? session?.agentId ?? "openclaw-agent";
1074
-
1075
- if (session) session.agentId = agentId;
1076
-
1077
- const totalTokens =
1078
- (run?.inputTokens ?? 0) +
1079
- (run?.outputTokens ?? 0) +
1080
- (run?.cacheReadTokens ?? 0) +
1081
- (run?.cacheWriteTokens ?? 0);
1082
-
1083
- const durationMs = event.durationMs ?? (run ? Date.now() - run.startedAt : undefined);
1084
- const redactedError = _redactError(event.error);
1085
-
1086
- const estimatedCostUsd = _estimateCost(
1087
- run?.model,
1088
- run?.inputTokens ?? 0,
1089
- run?.outputTokens ?? 0,
1090
- run?.cacheReadTokens ?? 0,
1091
- run?.cacheWriteTokens ?? 0,
1092
- );
1093
-
1094
- // Accumulate into session-level totals
1095
- if (session) {
1096
- session.runCount += 1;
1097
- session.totalInputTokens += run?.inputTokens ?? 0;
1098
- session.totalOutputTokens += run?.outputTokens ?? 0;
1099
- session.totalCacheReadTokens += run?.cacheReadTokens ?? 0;
1100
- session.totalCacheWriteTokens += run?.cacheWriteTokens ?? 0;
1101
- session.totalToolCalls += run?.toolCalls ?? 0;
1102
- session.totalEstimatedCostUsd += estimatedCostUsd ?? 0;
1103
- session.totalDurationMs += durationMs ?? 0;
1104
- }
1105
-
1106
- sendActivity("run_end", agentId, sessionKey, ctx.runId, {
1107
- status: event.success ? "success" : "failed",
1108
- duration_ms: durationMs,
1109
- total_tokens: totalTokens || undefined,
1110
- tool_calls: run?.toolCalls,
1111
- error: _redactError(event.error, true),
1112
- });
1113
-
1114
- send({
1115
- event_id: randomUUID(),
1116
- trace_id: session?.traceId ?? randomUUID(),
1117
- session_id: sessionKey,
1118
- run_id: ctx.runId,
1119
- agent_id: agentId,
1120
- platform: "openclaw",
1121
- event_name: "agent_end",
1122
- ts: Date.now(),
1123
- redaction_policy_version: `v1-${_activeMode()}`,
1124
- status: event.success ? "success" : "failed",
1125
- duration_ms: durationMs,
1126
- model: run?.model,
1127
- model_provider: run?.provider,
1128
- input_tokens: run?.inputTokens ?? 0,
1129
- output_tokens: run?.outputTokens ?? 0,
1130
- cache_read_tokens: run?.cacheReadTokens ?? 0,
1131
- cache_write_tokens: run?.cacheWriteTokens ?? 0,
1132
- total_tokens: totalTokens || undefined,
1133
- tool_calls: run?.toolCalls ?? 0,
1134
- tool_errors: run?.toolErrors ?? 0,
1135
- tool_names: run ? _redactToolNames([...run.toolNames]) : [],
1136
- step_count: event.messages?.length,
1137
- ...(estimatedCostUsd != null ? { estimated_cost_usd: estimatedCostUsd } : {}),
1138
- ...(redactedError ? { error: redactedError } : {}),
1139
- metadata: {
1140
- llm_calls: run?.llmCalls ?? 0,
1141
- images_count: run?.imagesCount ?? 0,
1142
- subagents_spawned: run?.subagentsSpawned ?? 0,
1143
- subagent_errors: run?.subagentErrors ?? 0,
1144
- compactions: session?.compactions ?? 0,
1145
- resets: session?.resets ?? 0,
1146
- },
1147
- });
1148
-
1149
- if (ctx.runId) runs.delete(ctx.runId);
1150
- });
1151
-
1152
- api.on("session_end", (event: SessionEndEvent, _ctx: SessionContext) => {
1153
- const key = event.sessionKey ?? event.sessionId;
1154
- const session = sessions.get(key);
1155
-
1156
- if (session && session.runCount > 0) {
1157
- const sessionDurationMs = event.durationMs ?? (Date.now() - session.startedAt);
1158
- const totalTokens =
1159
- session.totalInputTokens + session.totalOutputTokens +
1160
- session.totalCacheReadTokens + session.totalCacheWriteTokens;
1161
-
1162
- send({
1163
- event_id: randomUUID(),
1164
- trace_id: session.traceId,
1165
- session_id: key,
1166
- agent_id: session.agentId,
1167
- platform: "openclaw",
1168
- event_name: "session_metrics",
1169
- ts: Date.now(),
1170
- redaction_policy_version: `v1-${_activeMode()}`,
1171
- status: "success",
1172
- duration_ms: sessionDurationMs,
1173
- input_tokens: session.totalInputTokens,
1174
- output_tokens: session.totalOutputTokens,
1175
- cache_read_tokens: session.totalCacheReadTokens,
1176
- cache_write_tokens: session.totalCacheWriteTokens,
1177
- total_tokens: totalTokens || undefined,
1178
- tool_calls: session.totalToolCalls,
1179
- ...(session.totalEstimatedCostUsd > 0
1180
- ? { estimated_cost_usd: session.totalEstimatedCostUsd }
1181
- : {}),
1182
- metadata: {
1183
- run_count: session.runCount,
1184
- compactions: session.compactions,
1185
- resets: session.resets,
1186
- message_count: event.messageCount,
1187
- reason: event.reason,
1188
- },
1189
- });
1190
- }
1191
-
1192
- sessions.delete(key);
1193
- });
1194
- },
1195
- };
1196
-
1197
- export default plugin;