agentmetrics-openclaw 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -19
- package/openclaw.plugin.json +50 -2
- package/package.json +35 -8
- package/index.ts +0 -604
package/README.md
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
# agentmetrics-openclaw
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/agentmetrics-openclaw)
|
|
4
|
+
[](../LICENSE)
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
→ **[agentmetrics.dev/docs/integrations/openclaw](https://agentmetrics.dev/docs/integrations/openclaw)**
|
|
6
|
+
AgentMetrics integration for [OpenClaw](https://openclaw.dev). Install the plugin, point it at your server, and every agent session reports back to your dashboard automatically showing tokens, cost, tools, subagents, context health, and reliability, all without changing your agent code.
|
|
9
7
|
|
|
10
8
|
---
|
|
11
9
|
|
|
@@ -13,7 +11,7 @@ Zero-code observability for [OpenClaw](https://openclaw.dev) agents. Install the
|
|
|
13
11
|
|
|
14
12
|
- OpenClaw 2026.3.2 or later
|
|
15
13
|
- Node.js 22 or later
|
|
16
|
-
-
|
|
14
|
+
- A running AgentMetrics server (see the [main README](../../../README.md) for setup)
|
|
17
15
|
|
|
18
16
|
---
|
|
19
17
|
|
|
@@ -27,35 +25,60 @@ openclaw plugins install agentmetrics-openclaw
|
|
|
27
25
|
|
|
28
26
|
## Setup
|
|
29
27
|
|
|
30
|
-
**1.
|
|
28
|
+
**1. Start AgentMetrics** (if not already running)
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Docker
|
|
32
|
+
docker compose up
|
|
33
|
+
|
|
34
|
+
# Or Python CLI
|
|
35
|
+
pip install agentmetrics
|
|
36
|
+
agentmetrics dashboard
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**2. Set the server URL** (if not running on localhost)
|
|
31
40
|
|
|
32
41
|
```bash
|
|
33
|
-
# macOS / Linux
|
|
34
|
-
echo 'export
|
|
42
|
+
# macOS / Linux, permanent
|
|
43
|
+
echo 'export AGENTMETRICS_BASE_URL=http://your-server:8099' >> ~/.bashrc && source ~/.bashrc
|
|
35
44
|
|
|
36
45
|
# Windows (PowerShell)
|
|
37
|
-
$Env:
|
|
46
|
+
$Env:AGENTMETRICS_BASE_URL = "http://your-server:8099"
|
|
38
47
|
```
|
|
39
48
|
|
|
40
|
-
|
|
49
|
+
Omit this step if your server runs on `http://localhost:8099` (the default).
|
|
50
|
+
|
|
51
|
+
**3. Trust the plugin** (silences the security scan advisory)
|
|
41
52
|
|
|
42
53
|
```bash
|
|
43
54
|
openclaw config set plugins.allow '["agentmetrics"]'
|
|
44
55
|
```
|
|
45
56
|
|
|
46
|
-
**
|
|
57
|
+
**4. Restart the gateway**
|
|
47
58
|
|
|
48
59
|
```bash
|
|
49
60
|
openclaw gateway restart
|
|
50
61
|
```
|
|
51
62
|
|
|
52
|
-
**
|
|
63
|
+
**5. Verify**
|
|
53
64
|
|
|
54
65
|
```bash
|
|
55
66
|
openclaw plugins list
|
|
56
67
|
# agentmetrics loaded
|
|
57
68
|
```
|
|
58
69
|
|
|
70
|
+
**6. Set your agent name** (recommended)
|
|
71
|
+
|
|
72
|
+
In your agent's `openclaw.json`:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"name": "my-agent"
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The `name` field becomes the agent ID in your dashboard. Give each agent a distinct name.
|
|
81
|
+
|
|
59
82
|
---
|
|
60
83
|
|
|
61
84
|
## What gets tracked
|
|
@@ -76,15 +99,15 @@ Every agent session reports automatically:
|
|
|
76
99
|
|
|
77
100
|
## Troubleshooting
|
|
78
101
|
|
|
79
|
-
**"dangerous code patterns" warning on install**
|
|
80
|
-
Safe to ignore. The plugin reads `
|
|
102
|
+
**"dangerous code patterns" warning on install**
|
|
103
|
+
Safe to ignore. The plugin reads `AGENTMETRICS_BASE_URL` and makes network calls to the AgentMetrics API. Add `agentmetrics` to `plugins.allow` to suppress it permanently.
|
|
81
104
|
|
|
82
|
-
**"manifest id does not match package name" warning**
|
|
105
|
+
**"manifest id does not match package name" warning**
|
|
83
106
|
Not an error. The plugin's internal manifest id is `agentmetrics`; the npm package name is `agentmetrics-openclaw`. Use `agentmetrics` (not the npm name) in `plugins.allow`.
|
|
84
107
|
|
|
85
|
-
**Runs not appearing in the dashboard**
|
|
86
|
-
1.
|
|
87
|
-
2.
|
|
108
|
+
**Runs not appearing in the dashboard**
|
|
109
|
+
1. Verify the plugin loads: `openclaw plugins list`
|
|
110
|
+
2. Check the server is reachable: `openclaw agentmetrics test`
|
|
88
111
|
3. Restart the gateway after any env var change
|
|
89
112
|
4. Confirm your `openclaw.json` has a `name` field
|
|
90
113
|
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "agentmetrics",
|
|
3
3
|
"name": "AgentMetrics",
|
|
4
|
-
"description": "360-degree observability for every OpenClaw agent
|
|
4
|
+
"description": "360-degree observability for every OpenClaw agent - tokens, tools, latency, cost, subagents, context health, and reliability.",
|
|
5
5
|
"configSchema": {
|
|
6
6
|
"type": "object",
|
|
7
7
|
"properties": {
|
|
@@ -11,7 +11,55 @@
|
|
|
11
11
|
},
|
|
12
12
|
"endpoint": {
|
|
13
13
|
"type": "string",
|
|
14
|
-
"description": "Custom API endpoint (default:
|
|
14
|
+
"description": "Custom API endpoint (default: http://localhost:8099)"
|
|
15
|
+
},
|
|
16
|
+
"enabled": {
|
|
17
|
+
"type": "boolean",
|
|
18
|
+
"description": "Master toggle - set to false to disable all telemetry without uninstalling (default: true)",
|
|
19
|
+
"default": true
|
|
20
|
+
},
|
|
21
|
+
"flushIntervalSeconds": {
|
|
22
|
+
"type": "integer",
|
|
23
|
+
"description": "How often to flush the event batch to the API in seconds (default: 10)",
|
|
24
|
+
"default": 10
|
|
25
|
+
},
|
|
26
|
+
"maxBatchSize": {
|
|
27
|
+
"type": "integer",
|
|
28
|
+
"description": "Maximum number of events per batch request (default: 100)",
|
|
29
|
+
"default": 100
|
|
30
|
+
},
|
|
31
|
+
"maxQueueSize": {
|
|
32
|
+
"type": "integer",
|
|
33
|
+
"description": "Maximum number of events to hold in the local queue before dropping (FIFO, default: 10000)",
|
|
34
|
+
"default": 10000
|
|
35
|
+
},
|
|
36
|
+
"retryMaxAttempts": {
|
|
37
|
+
"type": "integer",
|
|
38
|
+
"description": "Maximum retry attempts per event batch before moving to dead-letter queue (default: 5)",
|
|
39
|
+
"default": 5
|
|
40
|
+
},
|
|
41
|
+
"redactionMode": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"enum": ["strict", "moderate", "debug"],
|
|
44
|
+
"description": "Redaction policy: strict = all sensitive fields scrubbed (default); moderate = errors and secrets only; debug = opt-in full detail, auto-expires after 1h",
|
|
45
|
+
"default": "strict"
|
|
46
|
+
},
|
|
47
|
+
"toolNameExport": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"enum": ["allowlist", "blocklist", "hash", "off"],
|
|
50
|
+
"description": "How tool names are exported: blocklist = export all except redactToolNames (default); allowlist = export only listed; hash = pseudonymise; off = never export",
|
|
51
|
+
"default": "blocklist"
|
|
52
|
+
},
|
|
53
|
+
"redactToolNames": {
|
|
54
|
+
"type": "array",
|
|
55
|
+
"items": { "type": "string" },
|
|
56
|
+
"description": "Tool names to always redact regardless of toolNameExport mode (default: [])",
|
|
57
|
+
"default": []
|
|
58
|
+
},
|
|
59
|
+
"compressPayloads": {
|
|
60
|
+
"type": "boolean",
|
|
61
|
+
"description": "Gzip-compress batch payloads before sending (requires server-side Content-Encoding: gzip support, default: false)",
|
|
62
|
+
"default": false
|
|
15
63
|
}
|
|
16
64
|
},
|
|
17
65
|
"additionalProperties": false
|
package/package.json
CHANGED
|
@@ -1,26 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentmetrics-openclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgentMetrics observability plugin for OpenClaw agents",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"homepage": "https://agentmetrics
|
|
7
|
+
"homepage": "https://github.com/andalabx/agentmetrics",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/andalabx/agentmetrics
|
|
10
|
+
"url": "git+https://github.com/andalabx/agentmetrics.git"
|
|
11
11
|
},
|
|
12
|
-
"keywords": [
|
|
13
|
-
|
|
14
|
-
"
|
|
12
|
+
"keywords": [
|
|
13
|
+
"openclaw",
|
|
14
|
+
"agentmetrics",
|
|
15
|
+
"observability",
|
|
16
|
+
"ai-agents",
|
|
17
|
+
"monitoring"
|
|
18
|
+
],
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./dist/index.js"
|
|
15
21
|
},
|
|
16
22
|
"openclaw": {
|
|
17
|
-
"extensions": [
|
|
23
|
+
"extensions": [
|
|
24
|
+
"./dist/index.js"
|
|
25
|
+
]
|
|
18
26
|
},
|
|
19
27
|
"files": [
|
|
20
|
-
"
|
|
28
|
+
"dist",
|
|
21
29
|
"openclaw.plugin.json",
|
|
22
30
|
"README.md"
|
|
23
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
|
+
},
|
|
24
51
|
"engines": {
|
|
25
52
|
"node": ">=22"
|
|
26
53
|
}
|
package/index.ts
DELETED
|
@@ -1,604 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "crypto";
|
|
2
|
-
|
|
3
|
-
// pluginConfig overrides env vars — set once in register()
|
|
4
|
-
let API_KEY: string | undefined;
|
|
5
|
-
let BASE_URL: string;
|
|
6
|
-
|
|
7
|
-
interface PluginApi {
|
|
8
|
-
config: Record<string, unknown>;
|
|
9
|
-
pluginConfig?: Record<string, unknown>;
|
|
10
|
-
registerAutoEnableProbe?: (probe: () => boolean) => void;
|
|
11
|
-
registerCli?: (registrar: {
|
|
12
|
-
name: string;
|
|
13
|
-
description: string;
|
|
14
|
-
commands: Array<{ name: string; description: string; handler: () => void }>;
|
|
15
|
-
}) => void;
|
|
16
|
-
on: (hookName: string, handler: (...args: unknown[]) => void) => void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// ─── Types (sourced from openclaw/src/plugins/hook-types.ts) ──────────────────
|
|
20
|
-
|
|
21
|
-
type AgentContext = {
|
|
22
|
-
runId?: string;
|
|
23
|
-
agentId?: string;
|
|
24
|
-
sessionKey?: string;
|
|
25
|
-
sessionId?: string;
|
|
26
|
-
modelId?: string;
|
|
27
|
-
modelProviderId?: string;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
type SessionContext = {
|
|
31
|
-
sessionId: string;
|
|
32
|
-
sessionKey?: string;
|
|
33
|
-
agentId?: string;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
type SessionStartEvent = {
|
|
37
|
-
sessionId: string;
|
|
38
|
-
sessionKey?: string;
|
|
39
|
-
resumedFrom?: string;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
type SessionEndEvent = {
|
|
43
|
-
sessionId: string;
|
|
44
|
-
sessionKey?: string;
|
|
45
|
-
durationMs?: number;
|
|
46
|
-
messageCount: number;
|
|
47
|
-
reason?: string;
|
|
48
|
-
transcriptArchived?: boolean;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
type LlmInputEvent = {
|
|
52
|
-
runId: string;
|
|
53
|
-
sessionId: string;
|
|
54
|
-
provider: string;
|
|
55
|
-
model: string;
|
|
56
|
-
systemPrompt?: string;
|
|
57
|
-
prompt: string;
|
|
58
|
-
historyMessages: unknown[];
|
|
59
|
-
imagesCount: number;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
type LlmOutputEvent = {
|
|
63
|
-
runId: string;
|
|
64
|
-
sessionId: string;
|
|
65
|
-
provider: string;
|
|
66
|
-
model: string;
|
|
67
|
-
assistantTexts: string[];
|
|
68
|
-
usage?: {
|
|
69
|
-
input?: number;
|
|
70
|
-
output?: number;
|
|
71
|
-
cacheRead?: number;
|
|
72
|
-
cacheWrite?: number;
|
|
73
|
-
total?: number;
|
|
74
|
-
};
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
type BeforeToolCallEvent = {
|
|
78
|
-
toolName: string;
|
|
79
|
-
params: Record<string, unknown>;
|
|
80
|
-
runId?: string;
|
|
81
|
-
toolCallId?: string;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
type AfterToolCallEvent = {
|
|
85
|
-
toolName: string;
|
|
86
|
-
params: Record<string, unknown>;
|
|
87
|
-
runId?: string;
|
|
88
|
-
toolCallId?: string;
|
|
89
|
-
result?: unknown;
|
|
90
|
-
error?: string;
|
|
91
|
-
durationMs?: number;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
type ToolContext = {
|
|
95
|
-
agentId?: string;
|
|
96
|
-
sessionKey?: string;
|
|
97
|
-
sessionId?: string;
|
|
98
|
-
runId?: string;
|
|
99
|
-
toolName: string;
|
|
100
|
-
toolCallId?: string;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
type AgentEndEvent = {
|
|
104
|
-
messages: unknown[];
|
|
105
|
-
success: boolean;
|
|
106
|
-
error?: string;
|
|
107
|
-
durationMs?: number;
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
type BeforeAgentStartEvent = {
|
|
111
|
-
agentId?: string;
|
|
112
|
-
sessionKey?: string;
|
|
113
|
-
sessionId?: string;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
type SubagentSpawningEvent = {
|
|
117
|
-
childSessionKey: string;
|
|
118
|
-
agentId: string;
|
|
119
|
-
label?: string;
|
|
120
|
-
mode: "run" | "session";
|
|
121
|
-
threadRequested: boolean;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
type SubagentEndedEvent = {
|
|
125
|
-
targetSessionKey: string;
|
|
126
|
-
reason: string;
|
|
127
|
-
runId?: string;
|
|
128
|
-
outcome?: "ok" | "error" | "timeout" | "killed" | "reset" | "deleted";
|
|
129
|
-
error?: string;
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
type SubagentContext = {
|
|
133
|
-
runId?: string;
|
|
134
|
-
childSessionKey?: string;
|
|
135
|
-
requesterSessionKey?: string;
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
type CompactionEvent = {
|
|
139
|
-
messageCount: number;
|
|
140
|
-
compactingCount?: number;
|
|
141
|
-
tokenCount?: number;
|
|
142
|
-
sessionFile?: string;
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
type ResetEvent = {
|
|
146
|
-
sessionFile?: string;
|
|
147
|
-
reason?: string;
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
type GatewayStartEvent = { port: number };
|
|
151
|
-
type GatewayStopEvent = { reason?: string };
|
|
152
|
-
type GatewayContext = { port?: number };
|
|
153
|
-
|
|
154
|
-
// ─── Per-session and per-run state ────────────────────────────────────────────
|
|
155
|
-
|
|
156
|
-
interface SessionMeta {
|
|
157
|
-
traceId: string;
|
|
158
|
-
agentId: string;
|
|
159
|
-
startedAt: number;
|
|
160
|
-
compactions: number;
|
|
161
|
-
resets: number;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
interface RunMeta {
|
|
165
|
-
inputTokens: number;
|
|
166
|
-
outputTokens: number;
|
|
167
|
-
cacheReadTokens: number;
|
|
168
|
-
cacheWriteTokens: number;
|
|
169
|
-
llmCalls: number;
|
|
170
|
-
imagesCount: number;
|
|
171
|
-
toolCalls: number;
|
|
172
|
-
toolErrors: number;
|
|
173
|
-
toolNames: Set<string>;
|
|
174
|
-
subagentsSpawned: number;
|
|
175
|
-
subagentErrors: number;
|
|
176
|
-
model?: string;
|
|
177
|
-
provider?: string;
|
|
178
|
-
sessionKey?: string;
|
|
179
|
-
startedAt: number; // run-level start time for accurate duration
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const sessions = new Map<string, SessionMeta>();
|
|
183
|
-
const runs = new Map<string, RunMeta>();
|
|
184
|
-
|
|
185
|
-
// ─── HTTP helpers ─────────────────────────────────────────────────────────────
|
|
186
|
-
|
|
187
|
-
/** Send a completed run summary to /v1/events (persisted to DB). */
|
|
188
|
-
async function send(payload: Record<string, unknown>): Promise<void> {
|
|
189
|
-
if (!API_KEY) return;
|
|
190
|
-
try {
|
|
191
|
-
await fetch(`${BASE_URL}/v1/events`, {
|
|
192
|
-
method: "POST",
|
|
193
|
-
headers: {
|
|
194
|
-
"Content-Type": "application/json",
|
|
195
|
-
"Authorization": `Bearer ${API_KEY}`,
|
|
196
|
-
},
|
|
197
|
-
body: JSON.stringify(payload),
|
|
198
|
-
});
|
|
199
|
-
} catch {
|
|
200
|
-
// Never crash the agent on observability failure
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Send a real-time intermediate event to /v1/activity (in-memory, streamed
|
|
206
|
-
* to dashboard via SSE). Fire-and-forget — no retry, no crash on failure.
|
|
207
|
-
*/
|
|
208
|
-
function sendActivity(
|
|
209
|
-
type: string,
|
|
210
|
-
agentId: string,
|
|
211
|
-
sessionKey: string | undefined,
|
|
212
|
-
runId: string | undefined,
|
|
213
|
-
data?: Record<string, unknown>,
|
|
214
|
-
): void {
|
|
215
|
-
if (!API_KEY) return;
|
|
216
|
-
const payload = JSON.stringify({
|
|
217
|
-
type,
|
|
218
|
-
agent_id: agentId,
|
|
219
|
-
session_key: sessionKey,
|
|
220
|
-
run_id: runId,
|
|
221
|
-
ts: Date.now(),
|
|
222
|
-
data: data ?? null,
|
|
223
|
-
});
|
|
224
|
-
fetch(`${BASE_URL}/v1/activity`, {
|
|
225
|
-
method: "POST",
|
|
226
|
-
headers: {
|
|
227
|
-
"Content-Type": "application/json",
|
|
228
|
-
"Authorization": `Bearer ${API_KEY}`,
|
|
229
|
-
},
|
|
230
|
-
body: payload,
|
|
231
|
-
}).catch(() => {});
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function emptyRun(sessionKey?: string): RunMeta {
|
|
235
|
-
return {
|
|
236
|
-
inputTokens: 0, outputTokens: 0,
|
|
237
|
-
cacheReadTokens: 0, cacheWriteTokens: 0,
|
|
238
|
-
llmCalls: 0, imagesCount: 0,
|
|
239
|
-
toolCalls: 0, toolErrors: 0, toolNames: new Set(),
|
|
240
|
-
subagentsSpawned: 0, subagentErrors: 0,
|
|
241
|
-
sessionKey,
|
|
242
|
-
startedAt: Date.now(),
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// ─── Plugin ───────────────────────────────────────────────────────────────────
|
|
247
|
-
|
|
248
|
-
const plugin = {
|
|
249
|
-
id: "agentmetrics",
|
|
250
|
-
name: "AgentMetrics",
|
|
251
|
-
description: "360-degree observability for every OpenClaw agent — real-time streaming, tokens, tools, latency, cost, subagents, and reliability.",
|
|
252
|
-
configSchema: {
|
|
253
|
-
type: "object",
|
|
254
|
-
properties: {
|
|
255
|
-
apiKey: {
|
|
256
|
-
type: "string",
|
|
257
|
-
description: "AgentMetrics API key (overrides AGENTMETRICS_API_KEY env var)",
|
|
258
|
-
},
|
|
259
|
-
endpoint: {
|
|
260
|
-
type: "string",
|
|
261
|
-
description: "Custom API endpoint (default: https://api.agentmetrics.dev)",
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
|
-
additionalProperties: false,
|
|
265
|
-
} as const,
|
|
266
|
-
|
|
267
|
-
register(api: PluginApi) {
|
|
268
|
-
// Config: pluginConfig > env vars
|
|
269
|
-
API_KEY = (api.pluginConfig?.apiKey as string | undefined) ?? process.env.AGENTMETRICS_API_KEY;
|
|
270
|
-
BASE_URL = (
|
|
271
|
-
(api.pluginConfig?.endpoint as string | undefined) ??
|
|
272
|
-
process.env.AGENTMETRICS_URL ??
|
|
273
|
-
"https://api.agentmetrics.dev"
|
|
274
|
-
).replace(/\/$/, "");
|
|
275
|
-
|
|
276
|
-
// Auto-enable when a key is available (env var or plugin config)
|
|
277
|
-
if (typeof api.registerAutoEnableProbe === "function") {
|
|
278
|
-
api.registerAutoEnableProbe(() => !!API_KEY);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (!API_KEY) {
|
|
282
|
-
console.log(
|
|
283
|
-
"\n AgentMetrics: no API key found.\n" +
|
|
284
|
-
" Your agent runs are not being tracked.\n" +
|
|
285
|
-
" Get an API key at https://agentmetrics.dev and set AGENTMETRICS_API_KEY.\n"
|
|
286
|
-
);
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
console.log(
|
|
291
|
-
`\n AgentMetrics active — sending data to ${BASE_URL}\n` +
|
|
292
|
-
` View your dashboard → https://agentmetrics.dev\n`
|
|
293
|
-
);
|
|
294
|
-
|
|
295
|
-
// CLI: openclaw agentmetrics status
|
|
296
|
-
if (typeof api.registerCli === "function") {
|
|
297
|
-
api.registerCli({
|
|
298
|
-
name: "agentmetrics",
|
|
299
|
-
description: "AgentMetrics observability commands",
|
|
300
|
-
commands: [
|
|
301
|
-
{
|
|
302
|
-
name: "status",
|
|
303
|
-
description: "Show current AgentMetrics plugin status",
|
|
304
|
-
handler() {
|
|
305
|
-
const keyPreview = API_KEY
|
|
306
|
-
? `${API_KEY.slice(0, 8)}...${API_KEY.slice(-4)}`
|
|
307
|
-
: "(not set)";
|
|
308
|
-
console.log("AgentMetrics — active");
|
|
309
|
-
console.log(` API key : ${keyPreview}`);
|
|
310
|
-
console.log(` Endpoint : ${BASE_URL}`);
|
|
311
|
-
console.log(` Sessions : ${sessions.size} tracked`);
|
|
312
|
-
console.log(` Runs : ${runs.size} in flight`);
|
|
313
|
-
},
|
|
314
|
-
},
|
|
315
|
-
],
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// ── gateway_start ─────────────────────────────────────────────────────
|
|
320
|
-
// Activity only — gateway events are infrastructure, not agent runs.
|
|
321
|
-
// We do NOT call send() here to avoid polluting the agents list.
|
|
322
|
-
api.on("gateway_start", (event: GatewayStartEvent, _ctx: GatewayContext) => {
|
|
323
|
-
sendActivity("gateway_start", "openclaw-gateway", undefined, undefined, {
|
|
324
|
-
port: event.port,
|
|
325
|
-
});
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
// ── gateway_stop ──────────────────────────────────────────────────────
|
|
329
|
-
api.on("gateway_stop", (event: GatewayStopEvent, _ctx: GatewayContext) => {
|
|
330
|
-
sendActivity("gateway_stop", "openclaw-gateway", undefined, undefined, {
|
|
331
|
-
reason: event.reason,
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
// ── session_start ─────────────────────────────────────────────────────
|
|
336
|
-
api.on("session_start", (event: SessionStartEvent, ctx: SessionContext) => {
|
|
337
|
-
const key = event.sessionKey ?? event.sessionId;
|
|
338
|
-
if (!key || sessions.has(key)) return;
|
|
339
|
-
|
|
340
|
-
sessions.set(key, {
|
|
341
|
-
traceId: randomUUID(),
|
|
342
|
-
agentId: ctx.agentId ?? "openclaw-agent",
|
|
343
|
-
startedAt: Date.now(),
|
|
344
|
-
compactions: 0,
|
|
345
|
-
resets: 0,
|
|
346
|
-
});
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
// ── before_agent_start ────────────────────────────────────────────────
|
|
350
|
-
// Fires at the start of each run. Sends run_start for real-time UI.
|
|
351
|
-
api.on("before_agent_start", (event: BeforeAgentStartEvent, ctx: AgentContext) => {
|
|
352
|
-
const sessionKey = ctx.sessionKey ?? ctx.sessionId;
|
|
353
|
-
const agentId = ctx.agentId ?? "openclaw-agent";
|
|
354
|
-
const runId = ctx.runId;
|
|
355
|
-
|
|
356
|
-
if (!runId && !sessionKey) return;
|
|
357
|
-
|
|
358
|
-
sendActivity("run_start", agentId, sessionKey, runId, {
|
|
359
|
-
model: ctx.modelId,
|
|
360
|
-
provider: ctx.modelProviderId,
|
|
361
|
-
});
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
// ── llm_input ─────────────────────────────────────────────────────────
|
|
365
|
-
// Fires before each LLM call. Sends llm_start for real-time "thinking"
|
|
366
|
-
// indicator. Also accumulates LLM call count and image count.
|
|
367
|
-
api.on("llm_input", (event: LlmInputEvent, ctx: AgentContext) => {
|
|
368
|
-
const { runId } = event;
|
|
369
|
-
if (!runId) return;
|
|
370
|
-
|
|
371
|
-
const sessionKey = ctx.sessionKey ?? ctx.sessionId ?? event.sessionId;
|
|
372
|
-
const agentId = ctx.agentId ?? sessions.get(sessionKey ?? "")?.agentId ?? "openclaw-agent";
|
|
373
|
-
const run = runs.get(runId) ?? emptyRun(sessionKey);
|
|
374
|
-
|
|
375
|
-
run.llmCalls += 1;
|
|
376
|
-
run.imagesCount += event.imagesCount ?? 0;
|
|
377
|
-
if (!run.model && event.model) run.model = event.model;
|
|
378
|
-
if (!run.provider && event.provider) run.provider = event.provider;
|
|
379
|
-
if (!run.sessionKey) run.sessionKey = sessionKey;
|
|
380
|
-
runs.set(runId, run);
|
|
381
|
-
|
|
382
|
-
sendActivity("llm_start", agentId, sessionKey, runId, {
|
|
383
|
-
model: event.model,
|
|
384
|
-
provider: event.provider,
|
|
385
|
-
images: event.imagesCount,
|
|
386
|
-
history: event.historyMessages?.length ?? 0,
|
|
387
|
-
});
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
// ── llm_output ────────────────────────────────────────────────────────
|
|
391
|
-
// Fires after each LLM response. Accumulates tokens and sends llm_end
|
|
392
|
-
// so the dashboard can show token counts updating in real time.
|
|
393
|
-
api.on("llm_output", (event: LlmOutputEvent, ctx: AgentContext) => {
|
|
394
|
-
const { runId } = event;
|
|
395
|
-
if (!runId) return;
|
|
396
|
-
|
|
397
|
-
const sessionKey = ctx.sessionKey ?? ctx.sessionId ?? event.sessionId;
|
|
398
|
-
const agentId = ctx.agentId ?? sessions.get(sessionKey ?? "")?.agentId ?? "openclaw-agent";
|
|
399
|
-
const run = runs.get(runId) ?? emptyRun(sessionKey);
|
|
400
|
-
|
|
401
|
-
if (event.usage) {
|
|
402
|
-
run.inputTokens += event.usage.input ?? 0;
|
|
403
|
-
run.outputTokens += event.usage.output ?? 0;
|
|
404
|
-
run.cacheReadTokens += event.usage.cacheRead ?? 0;
|
|
405
|
-
run.cacheWriteTokens += event.usage.cacheWrite ?? 0;
|
|
406
|
-
}
|
|
407
|
-
if (event.model) run.model = event.model;
|
|
408
|
-
if (event.provider) run.provider = event.provider;
|
|
409
|
-
if (!run.sessionKey) run.sessionKey = sessionKey;
|
|
410
|
-
runs.set(runId, run);
|
|
411
|
-
|
|
412
|
-
sendActivity("llm_end", agentId, sessionKey, runId, {
|
|
413
|
-
model: event.model,
|
|
414
|
-
provider: event.provider,
|
|
415
|
-
input_tokens: event.usage?.input,
|
|
416
|
-
output_tokens: event.usage?.output,
|
|
417
|
-
cache_read: event.usage?.cacheRead,
|
|
418
|
-
cache_write: event.usage?.cacheWrite,
|
|
419
|
-
total_input: run.inputTokens,
|
|
420
|
-
total_output: run.outputTokens,
|
|
421
|
-
});
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
// ── before_tool_call ──────────────────────────────────────────────────
|
|
425
|
-
// Fires before each tool execution. Sends tool_start immediately so
|
|
426
|
-
// the live visualizer shows tool activity in real time.
|
|
427
|
-
api.on("before_tool_call", (event: BeforeToolCallEvent, ctx: ToolContext) => {
|
|
428
|
-
const runId = event.runId ?? ctx.runId;
|
|
429
|
-
const sessionKey = ctx.sessionKey ?? ctx.sessionId;
|
|
430
|
-
const agentId = ctx.agentId ?? sessions.get(sessionKey ?? "")?.agentId ?? "openclaw-agent";
|
|
431
|
-
|
|
432
|
-
sendActivity("tool_start", agentId, sessionKey, runId, {
|
|
433
|
-
tool_name: event.toolName,
|
|
434
|
-
});
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
// ── after_tool_call ───────────────────────────────────────────────────
|
|
438
|
-
// Fires after every tool execution. Counts calls/errors and sends
|
|
439
|
-
// tool_end with outcome and duration.
|
|
440
|
-
api.on("after_tool_call", (event: AfterToolCallEvent, ctx: ToolContext) => {
|
|
441
|
-
const runId = event.runId ?? ctx.runId;
|
|
442
|
-
const sessionKey = ctx.sessionKey ?? ctx.sessionId;
|
|
443
|
-
const agentId = ctx.agentId ?? sessions.get(sessionKey ?? "")?.agentId ?? "openclaw-agent";
|
|
444
|
-
|
|
445
|
-
if (runId) {
|
|
446
|
-
const run = runs.get(runId) ?? emptyRun(sessionKey);
|
|
447
|
-
run.toolCalls += 1;
|
|
448
|
-
if (event.error) run.toolErrors += 1;
|
|
449
|
-
run.toolNames.add(event.toolName);
|
|
450
|
-
if (!run.sessionKey) run.sessionKey = sessionKey;
|
|
451
|
-
runs.set(runId, run);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
sendActivity("tool_end", agentId, sessionKey, runId, {
|
|
455
|
-
tool_name: event.toolName,
|
|
456
|
-
duration_ms: event.durationMs,
|
|
457
|
-
error: event.error?.slice(0, 200),
|
|
458
|
-
});
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
// ── subagent_spawning ─────────────────────────────────────────────────
|
|
462
|
-
api.on("subagent_spawning", (event: SubagentSpawningEvent, ctx: SubagentContext) => {
|
|
463
|
-
const runId = ctx.runId;
|
|
464
|
-
const sessionKey = ctx.requesterSessionKey;
|
|
465
|
-
const agentId = sessions.get(sessionKey ?? "")?.agentId ?? "openclaw-agent";
|
|
466
|
-
|
|
467
|
-
if (runId) {
|
|
468
|
-
const run = runs.get(runId);
|
|
469
|
-
if (run) {
|
|
470
|
-
run.subagentsSpawned += 1;
|
|
471
|
-
runs.set(runId, run);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
sendActivity("subagent_start", agentId, sessionKey, runId, {
|
|
476
|
-
child_agent_id: event.agentId,
|
|
477
|
-
child_session_key: event.childSessionKey,
|
|
478
|
-
mode: event.mode,
|
|
479
|
-
label: event.label,
|
|
480
|
-
});
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
// ── subagent_ended ────────────────────────────────────────────────────
|
|
484
|
-
api.on("subagent_ended", (event: SubagentEndedEvent, ctx: SubagentContext) => {
|
|
485
|
-
const runId = event.runId ?? ctx.runId;
|
|
486
|
-
const sessionKey = ctx.requesterSessionKey;
|
|
487
|
-
const agentId = sessions.get(sessionKey ?? "")?.agentId ?? "openclaw-agent";
|
|
488
|
-
|
|
489
|
-
if (runId) {
|
|
490
|
-
const run = runs.get(runId);
|
|
491
|
-
if (run && event.outcome && event.outcome !== "ok") {
|
|
492
|
-
run.subagentErrors += 1;
|
|
493
|
-
runs.set(runId, run);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
sendActivity("subagent_end", agentId, sessionKey, runId, {
|
|
498
|
-
child_session_key: event.targetSessionKey,
|
|
499
|
-
outcome: event.outcome,
|
|
500
|
-
error: event.error?.slice(0, 200),
|
|
501
|
-
});
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
// ── before_compaction ─────────────────────────────────────────────────
|
|
505
|
-
api.on("before_compaction", (_event: CompactionEvent, 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.compactions += 1;
|
|
513
|
-
sessions.set(key, session);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
sendActivity("compaction", agentId, key, ctx.runId);
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
// ── before_reset ──────────────────────────────────────────────────────
|
|
520
|
-
api.on("before_reset", (event: ResetEvent, ctx: AgentContext) => {
|
|
521
|
-
const key = ctx.sessionKey ?? ctx.sessionId;
|
|
522
|
-
const agentId = ctx.agentId ?? sessions.get(key ?? "")?.agentId ?? "openclaw-agent";
|
|
523
|
-
if (!key) return;
|
|
524
|
-
|
|
525
|
-
const session = sessions.get(key);
|
|
526
|
-
if (session) {
|
|
527
|
-
session.resets += 1;
|
|
528
|
-
sessions.set(key, session);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
sendActivity("reset", agentId, key, ctx.runId, { reason: event.reason });
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
// ── agent_end ─────────────────────────────────────────────────────────
|
|
535
|
-
// Fires when the agent finishes a run. Sends run_end for real-time UI,
|
|
536
|
-
// then the full persisted event to /v1/events with all accumulated data.
|
|
537
|
-
api.on("agent_end", (event: AgentEndEvent, ctx: AgentContext) => {
|
|
538
|
-
const sessionKey = ctx.sessionKey ?? ctx.sessionId;
|
|
539
|
-
if (!sessionKey) return;
|
|
540
|
-
|
|
541
|
-
const session = sessions.get(sessionKey);
|
|
542
|
-
const run = ctx.runId ? runs.get(ctx.runId) : undefined;
|
|
543
|
-
const agentId = ctx.agentId ?? session?.agentId ?? "openclaw-agent";
|
|
544
|
-
|
|
545
|
-
if (session) session.agentId = agentId;
|
|
546
|
-
|
|
547
|
-
const totalTokens =
|
|
548
|
-
(run?.inputTokens ?? 0) +
|
|
549
|
-
(run?.outputTokens ?? 0) +
|
|
550
|
-
(run?.cacheReadTokens ?? 0) +
|
|
551
|
-
(run?.cacheWriteTokens ?? 0);
|
|
552
|
-
|
|
553
|
-
// Use run-level duration (accurate) — fall back to session duration only if unavailable
|
|
554
|
-
const durationMs = event.durationMs ?? (run ? Date.now() - run.startedAt : undefined);
|
|
555
|
-
|
|
556
|
-
// Real-time: notify dashboard the run finished
|
|
557
|
-
sendActivity("run_end", agentId, sessionKey, ctx.runId, {
|
|
558
|
-
status: event.success ? "success" : "failed",
|
|
559
|
-
duration_ms: durationMs,
|
|
560
|
-
total_tokens: totalTokens || undefined,
|
|
561
|
-
tool_calls: run?.toolCalls,
|
|
562
|
-
error: event.error?.slice(0, 200),
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
// Persistent: full summary stored in DB
|
|
566
|
-
send({
|
|
567
|
-
trace_id: session?.traceId ?? randomUUID(),
|
|
568
|
-
agent_id: agentId,
|
|
569
|
-
status: event.success ? "success" : "failed",
|
|
570
|
-
duration_ms: durationMs,
|
|
571
|
-
model: run?.model,
|
|
572
|
-
model_provider: run?.provider,
|
|
573
|
-
input_tokens: run?.inputTokens ?? 0,
|
|
574
|
-
output_tokens: run?.outputTokens ?? 0,
|
|
575
|
-
cache_read_tokens: run?.cacheReadTokens ?? 0,
|
|
576
|
-
cache_write_tokens: run?.cacheWriteTokens ?? 0,
|
|
577
|
-
total_tokens: totalTokens || undefined,
|
|
578
|
-
tool_calls: run?.toolCalls ?? 0,
|
|
579
|
-
tool_errors: run?.toolErrors ?? 0,
|
|
580
|
-
step_count: event.messages?.length,
|
|
581
|
-
...(event.error ? { error: event.error.slice(0, 500) } : {}),
|
|
582
|
-
metadata: {
|
|
583
|
-
llm_calls: run?.llmCalls ?? 0,
|
|
584
|
-
images_count: run?.imagesCount ?? 0,
|
|
585
|
-
subagents_spawned: run?.subagentsSpawned ?? 0,
|
|
586
|
-
subagent_errors: run?.subagentErrors ?? 0,
|
|
587
|
-
compactions: session?.compactions ?? 0,
|
|
588
|
-
resets: session?.resets ?? 0,
|
|
589
|
-
tool_names: run ? [...run.toolNames] : [],
|
|
590
|
-
},
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
if (ctx.runId) runs.delete(ctx.runId);
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
// ── session_end ───────────────────────────────────────────────────────
|
|
597
|
-
api.on("session_end", (event: SessionEndEvent, _ctx: SessionContext) => {
|
|
598
|
-
const key = event.sessionKey ?? event.sessionId;
|
|
599
|
-
sessions.delete(key);
|
|
600
|
-
});
|
|
601
|
-
},
|
|
602
|
-
};
|
|
603
|
-
|
|
604
|
-
export default plugin;
|