agentmetrics-openclaw 0.1.0 → 0.2.1

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/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # agentmetrics-openclaw
2
+
3
+ Full observability for every OpenClaw agent. Tokens, tools, latency, cost, and reliability — automatically.
4
+
5
+ ## What it captures
6
+
7
+ Every agent run produces one event in AgentMetrics with:
8
+
9
+ | Field | Source |
10
+ |---|---|
11
+ | Status (success / failed) | `agent_end.success` |
12
+ | Duration | `agent_end.durationMs` |
13
+ | Model & provider | `llm_output` |
14
+ | Input tokens | `llm_output.usage.input` |
15
+ | Output tokens | `llm_output.usage.output` |
16
+ | Cache read tokens | `llm_output.usage.cacheRead` |
17
+ | Cache write tokens | `llm_output.usage.cacheWrite` |
18
+ | Tool call count | `after_tool_call` (counted) |
19
+ | Tool error count | `after_tool_call.error` (counted) |
20
+ | Step count | `agent_end.messages.length` |
21
+ | Error message | `agent_end.error` |
22
+
23
+ All token buckets are accumulated across the full run — multi-turn, multi-LLM-call runs are handled correctly.
24
+
25
+ ## Setup
26
+
27
+ ### 1. Install
28
+
29
+ ```sh
30
+ openclaw plugins install agentmetrics-openclaw
31
+ ```
32
+
33
+ ### 2. Set your API key
34
+
35
+ ```sh
36
+ export AGENTMETRICS_API_KEY=am_your_key_here
37
+ ```
38
+
39
+ Add this to your shell profile (`.bashrc`, `.zshrc`, etc.) to persist it.
40
+
41
+ ### 3. Restart the gateway
42
+
43
+ ```sh
44
+ openclaw gateway restart
45
+ ```
46
+
47
+ That's it. Sessions appear in your [AgentMetrics dashboard](https://agentmetrics.dev) within seconds.
48
+
49
+ ## Configuration (optional)
50
+
51
+ You can also set credentials in `openclaw.yml` instead of environment variables:
52
+
53
+ ```yaml
54
+ plugins:
55
+ agentmetrics:
56
+ apiKey: am_your_key_here
57
+ endpoint: https://api.agentmetrics.dev # optional
58
+ ```
59
+
60
+ ## Auto-enable
61
+
62
+ If `AGENTMETRICS_API_KEY` is present in your environment, the plugin enables itself automatically — no manual `openclaw plugins enable agentmetrics` needed.
63
+
64
+ ## CLI
65
+
66
+ ```sh
67
+ openclaw agentmetrics status
68
+ ```
69
+
70
+ Shows the active API key, endpoint, and current session/run counts.
71
+
72
+ ## Links
73
+
74
+ - Dashboard: [agentmetrics.dev](https://agentmetrics.dev)
75
+ - Docs: [agentmetrics.dev/docs](https://agentmetrics.dev/docs)
76
+ - GitHub: [github.com/andausman/agentmetrics](https://github.com/andausman/agentmetrics)
package/index.ts ADDED
@@ -0,0 +1,586 @@
1
+ import { randomUUID } from "crypto";
2
+
3
+ // Resolved once in register() — pluginConfig overrides env vars
4
+ let API_KEY: string | undefined;
5
+ let BASE_URL: string;
6
+
7
+ // ─── Types (sourced from openclaw/src/plugins/hook-types.ts) ──────────────────
8
+
9
+ type AgentContext = {
10
+ runId?: string;
11
+ agentId?: string;
12
+ sessionKey?: string;
13
+ sessionId?: string;
14
+ modelId?: string;
15
+ modelProviderId?: string;
16
+ };
17
+
18
+ type SessionContext = {
19
+ sessionId: string;
20
+ sessionKey?: string;
21
+ agentId?: string;
22
+ };
23
+
24
+ type SessionStartEvent = {
25
+ sessionId: string;
26
+ sessionKey?: string;
27
+ resumedFrom?: string;
28
+ };
29
+
30
+ type SessionEndEvent = {
31
+ sessionId: string;
32
+ sessionKey?: string;
33
+ durationMs?: number;
34
+ messageCount: number;
35
+ reason?: string;
36
+ transcriptArchived?: boolean;
37
+ };
38
+
39
+ type LlmInputEvent = {
40
+ runId: string;
41
+ sessionId: string;
42
+ provider: string;
43
+ model: string;
44
+ systemPrompt?: string;
45
+ prompt: string;
46
+ historyMessages: unknown[];
47
+ imagesCount: number;
48
+ };
49
+
50
+ type LlmOutputEvent = {
51
+ runId: string;
52
+ sessionId: string;
53
+ provider: string;
54
+ model: string;
55
+ assistantTexts: string[];
56
+ usage?: {
57
+ input?: number;
58
+ output?: number;
59
+ cacheRead?: number;
60
+ cacheWrite?: number;
61
+ total?: number;
62
+ };
63
+ };
64
+
65
+ type BeforeToolCallEvent = {
66
+ toolName: string;
67
+ params: Record<string, unknown>;
68
+ runId?: string;
69
+ toolCallId?: string;
70
+ };
71
+
72
+ type AfterToolCallEvent = {
73
+ toolName: string;
74
+ params: Record<string, unknown>;
75
+ runId?: string;
76
+ toolCallId?: string;
77
+ result?: unknown;
78
+ error?: string;
79
+ durationMs?: number;
80
+ };
81
+
82
+ type ToolContext = {
83
+ agentId?: string;
84
+ sessionKey?: string;
85
+ sessionId?: string;
86
+ runId?: string;
87
+ toolName: string;
88
+ toolCallId?: string;
89
+ };
90
+
91
+ type AgentEndEvent = {
92
+ messages: unknown[];
93
+ success: boolean;
94
+ error?: string;
95
+ durationMs?: number;
96
+ };
97
+
98
+ type BeforeAgentStartEvent = {
99
+ agentId?: string;
100
+ sessionKey?: string;
101
+ sessionId?: string;
102
+ };
103
+
104
+ type SubagentSpawningEvent = {
105
+ childSessionKey: string;
106
+ agentId: string;
107
+ label?: string;
108
+ mode: "run" | "session";
109
+ threadRequested: boolean;
110
+ };
111
+
112
+ type SubagentEndedEvent = {
113
+ targetSessionKey: string;
114
+ reason: string;
115
+ runId?: string;
116
+ outcome?: "ok" | "error" | "timeout" | "killed" | "reset" | "deleted";
117
+ error?: string;
118
+ };
119
+
120
+ type SubagentContext = {
121
+ runId?: string;
122
+ childSessionKey?: string;
123
+ requesterSessionKey?: string;
124
+ };
125
+
126
+ type CompactionEvent = {
127
+ messageCount: number;
128
+ compactingCount?: number;
129
+ tokenCount?: number;
130
+ sessionFile?: string;
131
+ };
132
+
133
+ type ResetEvent = {
134
+ sessionFile?: string;
135
+ reason?: string;
136
+ };
137
+
138
+ type GatewayStartEvent = { port: number };
139
+ type GatewayStopEvent = { reason?: string };
140
+ type GatewayContext = { port?: number };
141
+
142
+ // ─── Per-session and per-run state ────────────────────────────────────────────
143
+
144
+ interface SessionMeta {
145
+ traceId: string;
146
+ agentId: string;
147
+ startedAt: number;
148
+ compactions: number;
149
+ resets: number;
150
+ }
151
+
152
+ interface RunMeta {
153
+ inputTokens: number;
154
+ outputTokens: number;
155
+ cacheReadTokens: number;
156
+ cacheWriteTokens: number;
157
+ llmCalls: number;
158
+ imagesCount: number;
159
+ toolCalls: number;
160
+ toolErrors: number;
161
+ toolNames: Set<string>;
162
+ subagentsSpawned: number;
163
+ subagentErrors: number;
164
+ model?: string;
165
+ provider?: string;
166
+ sessionKey?: string;
167
+ }
168
+
169
+ const sessions = new Map<string, SessionMeta>();
170
+ const runs = new Map<string, RunMeta>();
171
+
172
+ // ─── HTTP helpers ─────────────────────────────────────────────────────────────
173
+
174
+ /** Send a completed run summary to /v1/events (persisted to DB). */
175
+ async function send(payload: Record<string, unknown>): Promise<void> {
176
+ if (!API_KEY) return;
177
+ try {
178
+ await fetch(`${BASE_URL}/v1/events`, {
179
+ method: "POST",
180
+ headers: {
181
+ "Content-Type": "application/json",
182
+ "Authorization": `Bearer ${API_KEY}`,
183
+ },
184
+ body: JSON.stringify(payload),
185
+ });
186
+ } catch {
187
+ // Never crash the agent on observability failure
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Send a real-time intermediate event to /v1/activity (in-memory, streamed
193
+ * to dashboard via SSE). Fire-and-forget — no retry, no crash on failure.
194
+ */
195
+ function sendActivity(
196
+ type: string,
197
+ agentId: string,
198
+ sessionKey: string | undefined,
199
+ runId: string | undefined,
200
+ data?: Record<string, unknown>,
201
+ ): void {
202
+ if (!API_KEY) return;
203
+ const payload = JSON.stringify({
204
+ type,
205
+ agent_id: agentId,
206
+ session_key: sessionKey,
207
+ run_id: runId,
208
+ ts: Date.now(),
209
+ data: data ?? null,
210
+ });
211
+ fetch(`${BASE_URL}/v1/activity`, {
212
+ method: "POST",
213
+ headers: {
214
+ "Content-Type": "application/json",
215
+ "Authorization": `Bearer ${API_KEY}`,
216
+ },
217
+ body: payload,
218
+ }).catch(() => {});
219
+ }
220
+
221
+ function emptyRun(sessionKey?: string): RunMeta {
222
+ return {
223
+ inputTokens: 0, outputTokens: 0,
224
+ cacheReadTokens: 0, cacheWriteTokens: 0,
225
+ llmCalls: 0, imagesCount: 0,
226
+ toolCalls: 0, toolErrors: 0, toolNames: new Set(),
227
+ subagentsSpawned: 0, subagentErrors: 0,
228
+ sessionKey,
229
+ };
230
+ }
231
+
232
+ // ─── Plugin ───────────────────────────────────────────────────────────────────
233
+
234
+ const plugin = {
235
+ id: "agentmetrics",
236
+ name: "AgentMetrics",
237
+ description: "360-degree observability for every OpenClaw agent — real-time streaming, tokens, tools, latency, cost, subagents, and reliability.",
238
+ configSchema: {
239
+ type: "object",
240
+ properties: {
241
+ apiKey: {
242
+ type: "string",
243
+ description: "AgentMetrics API key (overrides AGENTMETRICS_API_KEY env var)",
244
+ },
245
+ endpoint: {
246
+ type: "string",
247
+ description: "Custom API endpoint (default: https://api.agentmetrics.dev)",
248
+ },
249
+ },
250
+ additionalProperties: false,
251
+ } as const,
252
+
253
+ register(api: any) {
254
+ // Config: pluginConfig > env vars
255
+ API_KEY = api.config?.apiKey ?? process.env.AGENTMETRICS_API_KEY;
256
+ BASE_URL = (
257
+ api.config?.endpoint ??
258
+ process.env.AGENTMETRICS_URL ??
259
+ "https://api.agentmetrics.dev"
260
+ ).replace(/\/$/, "");
261
+
262
+ // Auto-enable when a key is available (env var or plugin config)
263
+ if (typeof api.registerAutoEnableProbe === "function") {
264
+ api.registerAutoEnableProbe(() => !!API_KEY);
265
+ }
266
+
267
+ if (!API_KEY) return;
268
+
269
+ // CLI: openclaw agentmetrics status
270
+ if (typeof api.registerCli === "function") {
271
+ api.registerCli({
272
+ name: "agentmetrics",
273
+ description: "AgentMetrics observability commands",
274
+ commands: [
275
+ {
276
+ name: "status",
277
+ description: "Show current AgentMetrics plugin status",
278
+ handler() {
279
+ const keyPreview = API_KEY
280
+ ? `${API_KEY.slice(0, 8)}...${API_KEY.slice(-4)}`
281
+ : "(not set)";
282
+ console.log("AgentMetrics — active");
283
+ console.log(` API key : ${keyPreview}`);
284
+ console.log(` Endpoint : ${BASE_URL}`);
285
+ console.log(` Sessions : ${sessions.size} tracked`);
286
+ console.log(` Runs : ${runs.size} in flight`);
287
+ },
288
+ },
289
+ ],
290
+ });
291
+ }
292
+
293
+ // ── gateway_start ─────────────────────────────────────────────────────
294
+ api.on("gateway_start", (event: GatewayStartEvent, _ctx: GatewayContext) => {
295
+ sendActivity("gateway_start", "openclaw-gateway", undefined, undefined, {
296
+ port: event.port,
297
+ });
298
+ send({
299
+ trace_id: randomUUID(),
300
+ agent_id: "openclaw-gateway",
301
+ status: "success",
302
+ metadata: { event_type: "gateway_start", port: event.port },
303
+ });
304
+ });
305
+
306
+ // ── gateway_stop ──────────────────────────────────────────────────────
307
+ api.on("gateway_stop", (event: GatewayStopEvent, _ctx: GatewayContext) => {
308
+ sendActivity("gateway_stop", "openclaw-gateway", undefined, undefined, {
309
+ reason: event.reason,
310
+ });
311
+ send({
312
+ trace_id: randomUUID(),
313
+ agent_id: "openclaw-gateway",
314
+ status: "success",
315
+ metadata: { event_type: "gateway_stop", reason: event.reason },
316
+ });
317
+ });
318
+
319
+ // ── session_start ─────────────────────────────────────────────────────
320
+ api.on("session_start", (event: SessionStartEvent, ctx: SessionContext) => {
321
+ const key = event.sessionKey ?? event.sessionId;
322
+ if (!key || sessions.has(key)) return;
323
+
324
+ sessions.set(key, {
325
+ traceId: randomUUID(),
326
+ agentId: ctx.agentId ?? "openclaw-agent",
327
+ startedAt: Date.now(),
328
+ compactions: 0,
329
+ resets: 0,
330
+ });
331
+ });
332
+
333
+ // ── before_agent_start ────────────────────────────────────────────────
334
+ // Fires at the start of each run. Sends run_start for real-time UI.
335
+ api.on("before_agent_start", (event: BeforeAgentStartEvent, ctx: AgentContext) => {
336
+ const sessionKey = ctx.sessionKey ?? ctx.sessionId;
337
+ const agentId = ctx.agentId ?? "openclaw-agent";
338
+ const runId = ctx.runId;
339
+
340
+ if (!runId && !sessionKey) return;
341
+
342
+ sendActivity("run_start", agentId, sessionKey, runId, {
343
+ model: ctx.modelId,
344
+ provider: ctx.modelProviderId,
345
+ });
346
+ });
347
+
348
+ // ── llm_input ─────────────────────────────────────────────────────────
349
+ // Fires before each LLM call. Sends llm_start for real-time "thinking"
350
+ // indicator. Also accumulates LLM call count and image count.
351
+ api.on("llm_input", (event: LlmInputEvent, ctx: AgentContext) => {
352
+ const { runId } = event;
353
+ if (!runId) return;
354
+
355
+ const sessionKey = ctx.sessionKey ?? ctx.sessionId ?? event.sessionId;
356
+ const agentId = ctx.agentId ?? sessions.get(sessionKey ?? "")?.agentId ?? "openclaw-agent";
357
+ const run = runs.get(runId) ?? emptyRun(sessionKey);
358
+
359
+ run.llmCalls += 1;
360
+ run.imagesCount += event.imagesCount ?? 0;
361
+ if (!run.model && event.model) run.model = event.model;
362
+ if (!run.provider && event.provider) run.provider = event.provider;
363
+ if (!run.sessionKey) run.sessionKey = sessionKey;
364
+ runs.set(runId, run);
365
+
366
+ sendActivity("llm_start", agentId, sessionKey, runId, {
367
+ model: event.model,
368
+ provider: event.provider,
369
+ images: event.imagesCount,
370
+ history: event.historyMessages?.length ?? 0,
371
+ });
372
+ });
373
+
374
+ // ── llm_output ────────────────────────────────────────────────────────
375
+ // Fires after each LLM response. Accumulates tokens and sends llm_end
376
+ // so the dashboard can show token counts updating in real time.
377
+ api.on("llm_output", (event: LlmOutputEvent, ctx: AgentContext) => {
378
+ const { runId } = event;
379
+ if (!runId) return;
380
+
381
+ const sessionKey = ctx.sessionKey ?? ctx.sessionId ?? event.sessionId;
382
+ const agentId = ctx.agentId ?? sessions.get(sessionKey ?? "")?.agentId ?? "openclaw-agent";
383
+ const run = runs.get(runId) ?? emptyRun(sessionKey);
384
+
385
+ if (event.usage) {
386
+ run.inputTokens += event.usage.input ?? 0;
387
+ run.outputTokens += event.usage.output ?? 0;
388
+ run.cacheReadTokens += event.usage.cacheRead ?? 0;
389
+ run.cacheWriteTokens += event.usage.cacheWrite ?? 0;
390
+ }
391
+ if (event.model) run.model = event.model;
392
+ if (event.provider) run.provider = event.provider;
393
+ if (!run.sessionKey) run.sessionKey = sessionKey;
394
+ runs.set(runId, run);
395
+
396
+ sendActivity("llm_end", agentId, sessionKey, runId, {
397
+ model: event.model,
398
+ provider: event.provider,
399
+ input_tokens: event.usage?.input,
400
+ output_tokens: event.usage?.output,
401
+ cache_read: event.usage?.cacheRead,
402
+ cache_write: event.usage?.cacheWrite,
403
+ // Running totals for the dashboard
404
+ total_input: run.inputTokens,
405
+ total_output: run.outputTokens,
406
+ });
407
+ });
408
+
409
+ // ── before_tool_call ──────────────────────────────────────────────────
410
+ // Fires before each tool execution. Sends tool_start immediately so
411
+ // the live visualizer shows tool activity in real time.
412
+ api.on("before_tool_call", (event: BeforeToolCallEvent, ctx: ToolContext) => {
413
+ const runId = event.runId ?? ctx.runId;
414
+ const sessionKey = ctx.sessionKey ?? ctx.sessionId;
415
+ const agentId = ctx.agentId ?? sessions.get(sessionKey ?? "")?.agentId ?? "openclaw-agent";
416
+
417
+ sendActivity("tool_start", agentId, sessionKey, runId, {
418
+ tool_name: event.toolName,
419
+ });
420
+ });
421
+
422
+ // ── after_tool_call ───────────────────────────────────────────────────
423
+ // Fires after every tool execution. Counts calls/errors and sends
424
+ // tool_end with outcome and duration.
425
+ api.on("after_tool_call", (event: AfterToolCallEvent, ctx: ToolContext) => {
426
+ const runId = event.runId ?? ctx.runId;
427
+ const sessionKey = ctx.sessionKey ?? ctx.sessionId;
428
+ const agentId = ctx.agentId ?? sessions.get(sessionKey ?? "")?.agentId ?? "openclaw-agent";
429
+
430
+ if (runId) {
431
+ const run = runs.get(runId) ?? emptyRun(sessionKey);
432
+ run.toolCalls += 1;
433
+ if (event.error) run.toolErrors += 1;
434
+ run.toolNames.add(event.toolName);
435
+ if (!run.sessionKey) run.sessionKey = sessionKey;
436
+ runs.set(runId, run);
437
+ }
438
+
439
+ sendActivity("tool_end", agentId, sessionKey, runId, {
440
+ tool_name: event.toolName,
441
+ duration_ms: event.durationMs,
442
+ error: event.error?.slice(0, 200),
443
+ });
444
+ });
445
+
446
+ // ── subagent_spawning ─────────────────────────────────────────────────
447
+ api.on("subagent_spawning", (event: SubagentSpawningEvent, ctx: SubagentContext) => {
448
+ const runId = ctx.runId;
449
+ const sessionKey = ctx.requesterSessionKey;
450
+ const agentId = sessions.get(sessionKey ?? "")?.agentId ?? "openclaw-agent";
451
+
452
+ if (runId) {
453
+ const run = runs.get(runId);
454
+ if (run) {
455
+ run.subagentsSpawned += 1;
456
+ runs.set(runId, run);
457
+ }
458
+ }
459
+
460
+ sendActivity("subagent_start", agentId, sessionKey, runId, {
461
+ child_agent_id: event.agentId,
462
+ child_session_key: event.childSessionKey,
463
+ mode: event.mode,
464
+ label: event.label,
465
+ });
466
+ });
467
+
468
+ // ── subagent_ended ────────────────────────────────────────────────────
469
+ api.on("subagent_ended", (event: SubagentEndedEvent, ctx: SubagentContext) => {
470
+ const runId = event.runId ?? ctx.runId;
471
+ const sessionKey = ctx.requesterSessionKey;
472
+ const agentId = sessions.get(sessionKey ?? "")?.agentId ?? "openclaw-agent";
473
+
474
+ if (runId) {
475
+ const run = runs.get(runId);
476
+ if (run && event.outcome && event.outcome !== "ok") {
477
+ run.subagentErrors += 1;
478
+ runs.set(runId, run);
479
+ }
480
+ }
481
+
482
+ sendActivity("subagent_end", agentId, sessionKey, runId, {
483
+ child_session_key: event.targetSessionKey,
484
+ outcome: event.outcome,
485
+ error: event.error?.slice(0, 200),
486
+ });
487
+ });
488
+
489
+ // ── before_compaction ─────────────────────────────────────────────────
490
+ api.on("before_compaction", (_event: CompactionEvent, ctx: AgentContext) => {
491
+ const key = ctx.sessionKey ?? ctx.sessionId;
492
+ const agentId = ctx.agentId ?? sessions.get(key ?? "")?.agentId ?? "openclaw-agent";
493
+ if (!key) return;
494
+
495
+ const session = sessions.get(key);
496
+ if (session) {
497
+ session.compactions += 1;
498
+ sessions.set(key, session);
499
+ }
500
+
501
+ sendActivity("compaction", agentId, key, ctx.runId);
502
+ });
503
+
504
+ // ── before_reset ──────────────────────────────────────────────────────
505
+ api.on("before_reset", (event: ResetEvent, ctx: AgentContext) => {
506
+ const key = ctx.sessionKey ?? ctx.sessionId;
507
+ const agentId = ctx.agentId ?? sessions.get(key ?? "")?.agentId ?? "openclaw-agent";
508
+ if (!key) return;
509
+
510
+ const session = sessions.get(key);
511
+ if (session) {
512
+ session.resets += 1;
513
+ sessions.set(key, session);
514
+ }
515
+
516
+ sendActivity("reset", agentId, key, ctx.runId, { reason: event.reason });
517
+ });
518
+
519
+ // ── agent_end ─────────────────────────────────────────────────────────
520
+ // Fires when the agent finishes a run. Sends run_end for real-time UI,
521
+ // then the full persisted event to /v1/events with all accumulated data.
522
+ api.on("agent_end", (event: AgentEndEvent, ctx: AgentContext) => {
523
+ const sessionKey = ctx.sessionKey ?? ctx.sessionId;
524
+ if (!sessionKey) return;
525
+
526
+ const session = sessions.get(sessionKey);
527
+ const run = ctx.runId ? runs.get(ctx.runId) : undefined;
528
+ const agentId = ctx.agentId ?? session?.agentId ?? "openclaw-agent";
529
+
530
+ if (session) session.agentId = agentId;
531
+
532
+ const totalTokens =
533
+ (run?.inputTokens ?? 0) +
534
+ (run?.outputTokens ?? 0) +
535
+ (run?.cacheReadTokens ?? 0) +
536
+ (run?.cacheWriteTokens ?? 0);
537
+
538
+ // Real-time: notify dashboard the run finished
539
+ sendActivity("run_end", agentId, sessionKey, ctx.runId, {
540
+ status: event.success ? "success" : "failed",
541
+ duration_ms: event.durationMs ?? (session ? Date.now() - session.startedAt : undefined),
542
+ total_tokens: totalTokens || undefined,
543
+ tool_calls: run?.toolCalls,
544
+ error: event.error?.slice(0, 200),
545
+ });
546
+
547
+ // Persistent: full summary stored in DB
548
+ send({
549
+ trace_id: session?.traceId ?? randomUUID(),
550
+ agent_id: agentId,
551
+ status: event.success ? "success" : "failed",
552
+ duration_ms: event.durationMs ?? (session ? Date.now() - session.startedAt : undefined),
553
+ model: run?.model,
554
+ model_provider: run?.provider,
555
+ input_tokens: run?.inputTokens ?? 0,
556
+ output_tokens: run?.outputTokens ?? 0,
557
+ cache_read_tokens: run?.cacheReadTokens ?? 0,
558
+ cache_write_tokens: run?.cacheWriteTokens ?? 0,
559
+ total_tokens: totalTokens || undefined,
560
+ tool_calls: run?.toolCalls ?? 0,
561
+ tool_errors: run?.toolErrors ?? 0,
562
+ step_count: event.messages?.length,
563
+ ...(event.error ? { error: event.error.slice(0, 500) } : {}),
564
+ metadata: {
565
+ llm_calls: run?.llmCalls ?? 0,
566
+ images_count: run?.imagesCount ?? 0,
567
+ subagents_spawned: run?.subagentsSpawned ?? 0,
568
+ subagent_errors: run?.subagentErrors ?? 0,
569
+ compactions: session?.compactions ?? 0,
570
+ resets: session?.resets ?? 0,
571
+ tool_names: run ? [...run.toolNames] : [],
572
+ },
573
+ });
574
+
575
+ if (ctx.runId) runs.delete(ctx.runId);
576
+ });
577
+
578
+ // ── session_end ───────────────────────────────────────────────────────
579
+ api.on("session_end", (event: SessionEndEvent, _ctx: SessionContext) => {
580
+ const key = event.sessionKey ?? event.sessionId;
581
+ sessions.delete(key);
582
+ });
583
+ },
584
+ };
585
+
586
+ export default plugin;
@@ -0,0 +1,19 @@
1
+ {
2
+ "id": "agentmetrics",
3
+ "name": "AgentMetrics",
4
+ "description": "360-degree observability for every OpenClaw agent — tokens, tools, latency, cost, subagents, context health, and reliability.",
5
+ "configSchema": {
6
+ "type": "object",
7
+ "properties": {
8
+ "apiKey": {
9
+ "type": "string",
10
+ "description": "AgentMetrics API key (overrides AGENTMETRICS_API_KEY env var)"
11
+ },
12
+ "endpoint": {
13
+ "type": "string",
14
+ "description": "Custom API endpoint (default: https://api.agentmetrics.dev)"
15
+ }
16
+ },
17
+ "additionalProperties": false
18
+ }
19
+ }
package/package.json CHANGED
@@ -1,30 +1,27 @@
1
1
  {
2
2
  "name": "agentmetrics-openclaw",
3
- "version": "0.1.0",
4
- "description": "AgentMetrics observability hook for OpenClaw agents",
3
+ "version": "0.2.1",
4
+ "type": "module",
5
+ "description": "AgentMetrics observability plugin for OpenClaw agents",
5
6
  "license": "MIT",
6
7
  "homepage": "https://agentmetrics.dev",
7
8
  "repository": {
8
9
  "type": "git",
9
- "url": "https://github.com/andausman/agentmetrics"
10
+ "url": "git+https://github.com/andausman/agentmetrics.git"
10
11
  },
11
12
  "keywords": ["openclaw", "agentmetrics", "observability", "ai-agents", "monitoring"],
13
+ "peerDependencies": {
14
+ "openclaw": ">=2026.3.2"
15
+ },
12
16
  "openclaw": {
13
- "hooks": ["hooks/agentmetrics"]
17
+ "extensions": ["./index.ts"]
14
18
  },
15
19
  "files": [
16
- "hooks/agentmetrics/HOOK.md",
17
- "hooks/agentmetrics/handler.js",
18
- "hooks/agentmetrics/handler.d.ts"
20
+ "index.ts",
21
+ "openclaw.plugin.json",
22
+ "README.md"
19
23
  ],
20
- "scripts": {
21
- "build": "tsc",
22
- "prepublishOnly": "npm run build"
23
- },
24
- "devDependencies": {
25
- "typescript": "^5.4.0"
26
- },
27
24
  "engines": {
28
- "node": ">=18"
25
+ "node": ">=22"
29
26
  }
30
27
  }
@@ -1,35 +0,0 @@
1
- ---
2
- name: agentmetrics
3
- description: "Send every OpenClaw session to AgentMetrics for full observability — latency, failures, cost, and model usage."
4
- metadata:
5
- openclaw:
6
- emoji: "📊"
7
- events:
8
- - agent:bootstrap
9
- - acp:session:complete
10
- - command:stop
11
- requires:
12
- env:
13
- - AGENTMETRICS_API_KEY
14
- ---
15
-
16
- # AgentMetrics
17
-
18
- Instruments every OpenClaw session automatically.
19
-
20
- Each session becomes one event in AgentMetrics, capturing:
21
-
22
- - Session duration
23
- - Success or failure status
24
- - Model used
25
- - Agent identity
26
-
27
- No code changes needed. Works with any agent, any model, any platform OpenClaw supports.
28
-
29
- ## Setup
30
-
31
- 1. Install: `openclaw plugins install agentmetrics-openclaw`
32
- 2. Set env var: `AGENTMETRICS_API_KEY=am_your_key_here`
33
- 3. Run your agent as normal.
34
-
35
- Sessions appear in your [AgentMetrics dashboard](https://app.agentmetrics.dev) within seconds.
@@ -1 +0,0 @@
1
- export default function handler(event: Record<string, unknown>): Promise<void>;
@@ -1,78 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = handler;
4
- const crypto_1 = require("crypto");
5
- const sessions = new Map();
6
- const API_KEY = process.env.AGENTMETRICS_API_KEY;
7
- const BASE_URL = (process.env.AGENTMETRICS_URL ?? "https://api.agentmetrics.dev").replace(/\/$/, "");
8
- async function send(payload) {
9
- if (!API_KEY)
10
- return;
11
- try {
12
- await fetch(`${BASE_URL}/events`, {
13
- method: "POST",
14
- headers: {
15
- "Content-Type": "application/json",
16
- "Authorization": `Bearer ${API_KEY}`,
17
- },
18
- body: JSON.stringify(payload),
19
- });
20
- }
21
- catch {
22
- // Never crash the agent on observability failure
23
- }
24
- }
25
- function resolveAgentId(event) {
26
- // Prefer explicit name from config, fall back to session label, then generic
27
- const ctx = event["context"];
28
- const cfg = ctx?.["cfg"];
29
- return (cfg?.["name"] ??
30
- ctx?.["label"] ??
31
- event["sessionKey"] ??
32
- "openclaw-agent");
33
- }
34
- async function handler(event) {
35
- if (!API_KEY)
36
- return;
37
- const type = event["type"];
38
- const action = event["action"];
39
- const key = event["sessionKey"];
40
- // ── Session start ────────────────────────────────────────────────────────
41
- if (type === "agent" && action === "bootstrap" && key) {
42
- sessions.set(key, {
43
- traceId: (0, crypto_1.randomUUID)(),
44
- agentId: resolveAgentId(event),
45
- startedAt: Date.now(),
46
- });
47
- return;
48
- }
49
- // ── Session end (ACP) ────────────────────────────────────────────────────
50
- if (type === "acp" && action === "session:complete" && key) {
51
- const meta = sessions.get(key);
52
- sessions.delete(key);
53
- const ctx = event["context"];
54
- const status = ctx?.["status"] === "success" ? "success" : "failed";
55
- const error = status === "failed" ? ctx?.["lastOutput"] : undefined;
56
- await send({
57
- trace_id: meta?.traceId ?? (0, crypto_1.randomUUID)(),
58
- agent_id: meta?.agentId ?? resolveAgentId(event),
59
- status,
60
- duration_ms: meta ? Date.now() - meta.startedAt : undefined,
61
- error: error?.slice(0, 500),
62
- });
63
- return;
64
- }
65
- // ── Session end (manual /stop) ───────────────────────────────────────────
66
- if (type === "command" && action === "stop" && key) {
67
- const meta = sessions.get(key);
68
- sessions.delete(key);
69
- if (!meta)
70
- return;
71
- await send({
72
- trace_id: meta.traceId,
73
- agent_id: meta.agentId,
74
- status: "success",
75
- duration_ms: Date.now() - meta.startedAt,
76
- });
77
- }
78
- }