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 +76 -0
- package/index.ts +586 -0
- package/openclaw.plugin.json +19 -0
- package/package.json +12 -15
- package/hooks/agentmetrics/HOOK.md +0 -35
- package/hooks/agentmetrics/handler.d.ts +0 -1
- package/hooks/agentmetrics/handler.js +0 -78
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
|
|
4
|
-
"
|
|
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
|
-
"
|
|
17
|
+
"extensions": ["./index.ts"]
|
|
14
18
|
},
|
|
15
19
|
"files": [
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
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": ">=
|
|
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
|
-
}
|