@latitude-data/openclaw-telemetry 0.0.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/LICENSE +157 -0
- package/README.md +141 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +272 -0
- package/dist/cli.js.map +1 -0
- package/dist/plugin.d.ts +117 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +646 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +65 -0
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { arch, hostname, platform, release } from "node:os";
|
|
3
|
+
//#region src/client.ts
|
|
4
|
+
async function postTraces({ baseUrl, apiKey, project, payload, logger, timeoutMs = 1e4 }) {
|
|
5
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/v1/traces`;
|
|
6
|
+
const bodyText = JSON.stringify(payload);
|
|
7
|
+
logger.debug(`POST ${url} (project=${project}, ${bodyText.length} bytes)`);
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
10
|
+
try {
|
|
11
|
+
const res = await fetch(url, {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: {
|
|
14
|
+
"Content-Type": "application/json",
|
|
15
|
+
Authorization: `Bearer ${apiKey}`,
|
|
16
|
+
"X-Latitude-Project": project
|
|
17
|
+
},
|
|
18
|
+
body: bodyText,
|
|
19
|
+
signal: controller.signal
|
|
20
|
+
});
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
const text = await res.text().catch(() => "");
|
|
23
|
+
logger.warn(`ingest HTTP ${res.status}: ${text.slice(0, 500)}`);
|
|
24
|
+
} else logger.debug(`ingest HTTP ${res.status}`);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
logger.warn(`ingest failed: ${String(err)}`);
|
|
27
|
+
} finally {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/config.ts
|
|
33
|
+
function loadConfig(env = process.env) {
|
|
34
|
+
const apiKey = env.LATITUDE_API_KEY ?? "";
|
|
35
|
+
const baseUrl = env.LATITUDE_BASE_URL ?? "https://ingest.latitude.so";
|
|
36
|
+
const project = env.LATITUDE_PROJECT ?? "";
|
|
37
|
+
return {
|
|
38
|
+
apiKey,
|
|
39
|
+
baseUrl,
|
|
40
|
+
project,
|
|
41
|
+
enabled: (env.LATITUDE_OPENCLAW_ENABLED ?? "1") !== "0" && apiKey !== "" && project !== "",
|
|
42
|
+
debug: env.LATITUDE_DEBUG === "1"
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/logger.ts
|
|
47
|
+
const PREFIX = "[latitude-openclaw]";
|
|
48
|
+
function createLogger(debugEnabled) {
|
|
49
|
+
return {
|
|
50
|
+
debug: debugEnabled ? (msg) => process.stderr.write(`${PREFIX} ${msg}\n`) : () => {},
|
|
51
|
+
warn: (msg) => process.stderr.write(`${PREFIX} ${msg}\n`)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/otlp.ts
|
|
56
|
+
const SCOPE_NAME = "@latitude-data/openclaw-telemetry";
|
|
57
|
+
const SCOPE_VERSION = "0.0.1";
|
|
58
|
+
/** Build an OTLP export request for a single completed agent run. */
|
|
59
|
+
function buildOtlpRequest(run) {
|
|
60
|
+
const spans = buildRunSpans(run);
|
|
61
|
+
return { resourceSpans: [{
|
|
62
|
+
resource: { attributes: resourceAttrs() },
|
|
63
|
+
scopeSpans: [{
|
|
64
|
+
scope: {
|
|
65
|
+
name: SCOPE_NAME,
|
|
66
|
+
version: SCOPE_VERSION
|
|
67
|
+
},
|
|
68
|
+
spans
|
|
69
|
+
}]
|
|
70
|
+
}] };
|
|
71
|
+
}
|
|
72
|
+
function buildRunSpans(run) {
|
|
73
|
+
const traceId = hashHex(`${run.sessionId ?? "session"}:${run.runId}`, 32);
|
|
74
|
+
const interactionSpanId = hashHex(`${traceId}:run`, 16);
|
|
75
|
+
const out = [buildInteractionSpan(traceId, interactionSpanId, run)];
|
|
76
|
+
run.llmCalls.forEach((call, idx) => {
|
|
77
|
+
const callSpanId = hashHex(`${traceId}:call:${idx}`, 16);
|
|
78
|
+
out.push(buildLlmSpan(traceId, interactionSpanId, callSpanId, call, idx, run));
|
|
79
|
+
call.toolCalls.forEach((tool, tIdx) => {
|
|
80
|
+
const toolSpanId = hashHex(`${traceId}:call:${idx}:tool:${tIdx}`, 16);
|
|
81
|
+
out.push(buildToolSpan(traceId, interactionSpanId, toolSpanId, tool, run));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
run.orphanTools.forEach((tool, idx) => {
|
|
85
|
+
const toolSpanId = hashHex(`${traceId}:orphan-tool:${idx}`, 16);
|
|
86
|
+
out.push(buildToolSpan(traceId, interactionSpanId, toolSpanId, tool, run));
|
|
87
|
+
});
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
function buildInteractionSpan(traceId, spanId, run) {
|
|
91
|
+
const startNs = msToNs(run.startMs);
|
|
92
|
+
const endNs = msToNs(run.endMs ?? run.startMs);
|
|
93
|
+
const totalTools = run.llmCalls.reduce((sum, c) => sum + c.toolCalls.length, 0) + run.orphanTools.length;
|
|
94
|
+
const totalUsage = aggregateUsage(run.llmCalls);
|
|
95
|
+
return {
|
|
96
|
+
traceId,
|
|
97
|
+
spanId,
|
|
98
|
+
parentSpanId: "",
|
|
99
|
+
name: "interaction",
|
|
100
|
+
kind: 1,
|
|
101
|
+
startTimeUnixNano: startNs,
|
|
102
|
+
endTimeUnixNano: endNs,
|
|
103
|
+
attributes: stripUndef([
|
|
104
|
+
str("span.type", "interaction"),
|
|
105
|
+
str("interaction.kind", "agent_run"),
|
|
106
|
+
str("openclaw.run.id", run.runId),
|
|
107
|
+
run.sessionId ? str("openclaw.session.id", run.sessionId) : void 0,
|
|
108
|
+
run.sessionId ? str("session.id", run.sessionId) : void 0,
|
|
109
|
+
run.sessionKey ? str("openclaw.session.key", run.sessionKey) : void 0,
|
|
110
|
+
run.agentId ? str("openclaw.agent.id", run.agentId) : void 0,
|
|
111
|
+
run.agentId ? str("openclaw.agent.name", run.agentId) : void 0,
|
|
112
|
+
run.workspaceDir ? str("openclaw.workspace.dir", run.workspaceDir) : void 0,
|
|
113
|
+
run.messageProvider ? str("openclaw.message.provider", run.messageProvider) : void 0,
|
|
114
|
+
run.channelId ? str("openclaw.channel.id", run.channelId) : void 0,
|
|
115
|
+
run.trigger ? str("openclaw.trigger", run.trigger) : void 0,
|
|
116
|
+
run.modelProviderId ? str("openclaw.model.provider.id", run.modelProviderId) : void 0,
|
|
117
|
+
run.modelId ? str("openclaw.model.id", run.modelId) : void 0,
|
|
118
|
+
int("interaction.duration_ms", durationMs(run.startMs, run.endMs)),
|
|
119
|
+
int("interaction.call_count", run.llmCalls.length),
|
|
120
|
+
int("interaction.tool_call_count", totalTools),
|
|
121
|
+
run.success !== void 0 ? bool("openclaw.run.success", run.success) : void 0,
|
|
122
|
+
run.error ? str("openclaw.run.error", run.error) : void 0,
|
|
123
|
+
totalUsage.input !== void 0 ? int("gen_ai.usage.input_tokens", totalUsage.input) : void 0,
|
|
124
|
+
totalUsage.output !== void 0 ? int("gen_ai.usage.output_tokens", totalUsage.output) : void 0,
|
|
125
|
+
totalUsage.cacheRead !== void 0 ? int("gen_ai.usage.cache_read_input_tokens", totalUsage.cacheRead) : void 0,
|
|
126
|
+
totalUsage.cacheWrite !== void 0 ? int("gen_ai.usage.cache_creation_input_tokens", totalUsage.cacheWrite) : void 0,
|
|
127
|
+
totalUsage.total !== void 0 ? int("gen_ai.usage.total_tokens", totalUsage.total) : void 0,
|
|
128
|
+
run.llmCalls[0]?.prompt ? str("user_prompt", run.llmCalls[0].prompt) : void 0
|
|
129
|
+
]),
|
|
130
|
+
status: { code: run.success === false ? 2 : 1 }
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function buildLlmSpan(traceId, parentSpanId, spanId, call, callIdx, run) {
|
|
134
|
+
const startNs = msToNs(call.startMs);
|
|
135
|
+
const endNs = msToNs(call.endMs ?? call.startMs);
|
|
136
|
+
const inputMessages = buildInputMessages(call);
|
|
137
|
+
const outputMessages = buildOutputMessages(call);
|
|
138
|
+
const systemInstructions = call.systemPrompt ? JSON.stringify([{
|
|
139
|
+
type: "text",
|
|
140
|
+
content: call.systemPrompt
|
|
141
|
+
}]) : void 0;
|
|
142
|
+
return {
|
|
143
|
+
traceId,
|
|
144
|
+
spanId,
|
|
145
|
+
parentSpanId,
|
|
146
|
+
name: "llm_request",
|
|
147
|
+
kind: 3,
|
|
148
|
+
startTimeUnixNano: startNs,
|
|
149
|
+
endTimeUnixNano: endNs,
|
|
150
|
+
attributes: stripUndef([
|
|
151
|
+
str("span.type", "llm_request"),
|
|
152
|
+
str("gen_ai.operation.name", "chat"),
|
|
153
|
+
str("llm_request.context", "interaction"),
|
|
154
|
+
int("llm_request.call_index", callIdx),
|
|
155
|
+
str("gen_ai.system", call.provider),
|
|
156
|
+
str("openclaw.provider", call.provider),
|
|
157
|
+
str("gen_ai.request.model", call.requestModel),
|
|
158
|
+
str("model", call.requestModel),
|
|
159
|
+
call.responseModel ? str("gen_ai.response.model", call.responseModel) : void 0,
|
|
160
|
+
call.resolvedRef ? str("openclaw.resolved.ref", call.resolvedRef) : void 0,
|
|
161
|
+
run.sessionId ? str("session.id", run.sessionId) : void 0,
|
|
162
|
+
run.sessionKey ? str("openclaw.session.key", run.sessionKey) : void 0,
|
|
163
|
+
str("openclaw.run.id", call.runId),
|
|
164
|
+
call.agentId ? str("openclaw.agent.id", call.agentId) : void 0,
|
|
165
|
+
call.agentId ? str("openclaw.agent.name", call.agentId) : void 0,
|
|
166
|
+
call.usage?.input !== void 0 ? int("gen_ai.usage.input_tokens", call.usage.input) : void 0,
|
|
167
|
+
call.usage?.input !== void 0 ? int("input_tokens", call.usage.input) : void 0,
|
|
168
|
+
call.usage?.output !== void 0 ? int("gen_ai.usage.output_tokens", call.usage.output) : void 0,
|
|
169
|
+
call.usage?.output !== void 0 ? int("output_tokens", call.usage.output) : void 0,
|
|
170
|
+
call.usage?.cacheRead !== void 0 ? int("gen_ai.usage.cache_read_input_tokens", call.usage.cacheRead) : void 0,
|
|
171
|
+
call.usage?.cacheRead !== void 0 ? int("cache_read_tokens", call.usage.cacheRead) : void 0,
|
|
172
|
+
call.usage?.cacheWrite !== void 0 ? int("gen_ai.usage.cache_creation_input_tokens", call.usage.cacheWrite) : void 0,
|
|
173
|
+
call.usage?.cacheWrite !== void 0 ? int("cache_creation_tokens", call.usage.cacheWrite) : void 0,
|
|
174
|
+
call.usage?.total !== void 0 ? int("gen_ai.usage.total_tokens", call.usage.total) : void 0,
|
|
175
|
+
systemInstructions ? str("gen_ai.system_instructions", systemInstructions) : void 0,
|
|
176
|
+
str("gen_ai.input.messages", JSON.stringify(inputMessages)),
|
|
177
|
+
str("gen_ai.output.messages", JSON.stringify(outputMessages)),
|
|
178
|
+
int("openclaw.images.count", call.imagesCount),
|
|
179
|
+
int("llm_request.tool_call_count", call.toolCalls.length),
|
|
180
|
+
int("llm_request.duration_ms", durationMs(call.startMs, call.endMs)),
|
|
181
|
+
call.error ? str("error.type", "llm_error") : void 0,
|
|
182
|
+
call.error ? str("error.message", call.error) : void 0,
|
|
183
|
+
str("success", call.error ? "false" : "true"),
|
|
184
|
+
str("llm_request.captured", "true")
|
|
185
|
+
]),
|
|
186
|
+
status: { code: call.error ? 2 : 1 }
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function buildToolSpan(traceId, parentSpanId, spanId, tool, run) {
|
|
190
|
+
const startNs = msToNs(tool.startMs);
|
|
191
|
+
const endNs = msToNs(tool.endMs ?? tool.startMs);
|
|
192
|
+
const isError = Boolean(tool.error);
|
|
193
|
+
return {
|
|
194
|
+
traceId,
|
|
195
|
+
spanId,
|
|
196
|
+
parentSpanId,
|
|
197
|
+
name: `tool:${tool.toolName}`,
|
|
198
|
+
kind: 1,
|
|
199
|
+
startTimeUnixNano: startNs,
|
|
200
|
+
endTimeUnixNano: endNs,
|
|
201
|
+
attributes: stripUndef([
|
|
202
|
+
str("span.type", "tool_execution"),
|
|
203
|
+
str("gen_ai.operation.name", "execute_tool"),
|
|
204
|
+
str("gen_ai.tool.name", tool.toolName),
|
|
205
|
+
str("gen_ai.tool.call.id", tool.toolCallId),
|
|
206
|
+
str("gen_ai.tool.call.arguments", safeJson(tool.params)),
|
|
207
|
+
tool.result !== void 0 ? str("gen_ai.tool.call.result", safeJson(tool.result)) : void 0,
|
|
208
|
+
isError ? str("error.type", "tool_error") : void 0,
|
|
209
|
+
isError ? str("error.message", tool.error ?? "") : void 0,
|
|
210
|
+
bool("tool.is_error", isError),
|
|
211
|
+
str("success", isError ? "false" : "true"),
|
|
212
|
+
tool.durationMs !== void 0 ? int("tool.duration_ms", tool.durationMs) : void 0,
|
|
213
|
+
run.sessionId ? str("session.id", run.sessionId) : void 0,
|
|
214
|
+
run.sessionKey ? str("openclaw.session.key", run.sessionKey) : void 0,
|
|
215
|
+
str("openclaw.run.id", run.runId),
|
|
216
|
+
tool.agentId ? str("openclaw.agent.id", tool.agentId) : void 0,
|
|
217
|
+
tool.agentId ? str("openclaw.agent.name", tool.agentId) : void 0
|
|
218
|
+
]),
|
|
219
|
+
status: { code: isError ? 2 : 1 }
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function buildInputMessages(call) {
|
|
223
|
+
const out = [];
|
|
224
|
+
for (const msg of call.historyMessages) {
|
|
225
|
+
const normalized = normalizeHistoryMessage(msg);
|
|
226
|
+
if (normalized) out.push(normalized);
|
|
227
|
+
}
|
|
228
|
+
if (call.prompt.length > 0) out.push({
|
|
229
|
+
role: "user",
|
|
230
|
+
parts: [{
|
|
231
|
+
type: "text",
|
|
232
|
+
content: call.prompt
|
|
233
|
+
}]
|
|
234
|
+
});
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
const ALLOWED_ROLES = new Set([
|
|
238
|
+
"system",
|
|
239
|
+
"user",
|
|
240
|
+
"assistant",
|
|
241
|
+
"tool"
|
|
242
|
+
]);
|
|
243
|
+
function normalizeRole(raw) {
|
|
244
|
+
if (typeof raw !== "string") return "user";
|
|
245
|
+
return ALLOWED_ROLES.has(raw) ? raw : "user";
|
|
246
|
+
}
|
|
247
|
+
function normalizeHistoryMessage(raw) {
|
|
248
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
249
|
+
const obj = raw;
|
|
250
|
+
const role = normalizeRole(obj.role);
|
|
251
|
+
const content = obj.content ?? obj.text ?? obj.message;
|
|
252
|
+
if (typeof content === "string") return {
|
|
253
|
+
role,
|
|
254
|
+
parts: [{
|
|
255
|
+
type: "text",
|
|
256
|
+
content
|
|
257
|
+
}]
|
|
258
|
+
};
|
|
259
|
+
if (Array.isArray(content)) {
|
|
260
|
+
const parts = [];
|
|
261
|
+
for (const block of content) {
|
|
262
|
+
const part = normalizeContentBlock(block);
|
|
263
|
+
if (part) parts.push(part);
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
role,
|
|
267
|
+
parts: parts.length > 0 ? parts : [{
|
|
268
|
+
type: "text",
|
|
269
|
+
content: JSON.stringify(content)
|
|
270
|
+
}]
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
role,
|
|
275
|
+
parts: [{
|
|
276
|
+
type: "text",
|
|
277
|
+
content: safeJson(raw)
|
|
278
|
+
}]
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function normalizeContentBlock(raw) {
|
|
282
|
+
if (typeof raw === "string") return {
|
|
283
|
+
type: "text",
|
|
284
|
+
content: raw
|
|
285
|
+
};
|
|
286
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
287
|
+
const obj = raw;
|
|
288
|
+
const type = typeof obj.type === "string" ? obj.type : "text";
|
|
289
|
+
if (type === "text" && typeof obj.text === "string") return {
|
|
290
|
+
type: "text",
|
|
291
|
+
content: obj.text
|
|
292
|
+
};
|
|
293
|
+
if (type === "tool_use") return {
|
|
294
|
+
type: "tool_call",
|
|
295
|
+
id: typeof obj.id === "string" ? obj.id : "",
|
|
296
|
+
name: typeof obj.name === "string" ? obj.name : "",
|
|
297
|
+
arguments: obj.input ?? {}
|
|
298
|
+
};
|
|
299
|
+
if (type === "tool_result") return {
|
|
300
|
+
type: "tool_call_response",
|
|
301
|
+
id: typeof obj.tool_use_id === "string" ? obj.tool_use_id : "",
|
|
302
|
+
response: obj.content ?? ""
|
|
303
|
+
};
|
|
304
|
+
if (type === "image") return {
|
|
305
|
+
type: "uri",
|
|
306
|
+
modality: "image",
|
|
307
|
+
uri: safeJson(obj.source ?? obj)
|
|
308
|
+
};
|
|
309
|
+
return {
|
|
310
|
+
type,
|
|
311
|
+
content: safeJson(raw)
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function buildOutputMessages(call) {
|
|
315
|
+
const parts = [];
|
|
316
|
+
for (const text of call.assistantTexts) if (text.length > 0) parts.push({
|
|
317
|
+
type: "text",
|
|
318
|
+
content: text
|
|
319
|
+
});
|
|
320
|
+
for (const tool of call.toolCalls) parts.push({
|
|
321
|
+
type: "tool_call",
|
|
322
|
+
id: tool.toolCallId,
|
|
323
|
+
name: tool.toolName,
|
|
324
|
+
arguments: tool.params
|
|
325
|
+
});
|
|
326
|
+
if (parts.length === 0 && call.lastAssistant !== void 0) parts.push({
|
|
327
|
+
type: "text",
|
|
328
|
+
content: safeJson(call.lastAssistant)
|
|
329
|
+
});
|
|
330
|
+
return [{
|
|
331
|
+
role: "assistant",
|
|
332
|
+
parts
|
|
333
|
+
}];
|
|
334
|
+
}
|
|
335
|
+
function aggregateUsage(calls) {
|
|
336
|
+
const agg = {
|
|
337
|
+
input: void 0,
|
|
338
|
+
output: void 0,
|
|
339
|
+
cacheRead: void 0,
|
|
340
|
+
cacheWrite: void 0,
|
|
341
|
+
total: void 0
|
|
342
|
+
};
|
|
343
|
+
const add = (k, v) => {
|
|
344
|
+
if (v === void 0) return;
|
|
345
|
+
agg[k] = (agg[k] ?? 0) + v;
|
|
346
|
+
};
|
|
347
|
+
for (const c of calls) {
|
|
348
|
+
if (!c.usage) continue;
|
|
349
|
+
add("input", c.usage.input);
|
|
350
|
+
add("output", c.usage.output);
|
|
351
|
+
add("cacheRead", c.usage.cacheRead);
|
|
352
|
+
add("cacheWrite", c.usage.cacheWrite);
|
|
353
|
+
add("total", c.usage.total);
|
|
354
|
+
}
|
|
355
|
+
return agg;
|
|
356
|
+
}
|
|
357
|
+
function resourceAttrs() {
|
|
358
|
+
return [
|
|
359
|
+
str("service.name", "openclaw"),
|
|
360
|
+
str("service.version", SCOPE_VERSION),
|
|
361
|
+
str("host.name", hostname()),
|
|
362
|
+
str("host.arch", arch()),
|
|
363
|
+
str("os.type", platform()),
|
|
364
|
+
str("os.version", release())
|
|
365
|
+
];
|
|
366
|
+
}
|
|
367
|
+
function str(key, value) {
|
|
368
|
+
return {
|
|
369
|
+
key,
|
|
370
|
+
value: { stringValue: value }
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function int(key, value) {
|
|
374
|
+
return {
|
|
375
|
+
key,
|
|
376
|
+
value: { intValue: String(Math.trunc(value)) }
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function bool(key, value) {
|
|
380
|
+
return {
|
|
381
|
+
key,
|
|
382
|
+
value: { boolValue: value }
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function stripUndef(items) {
|
|
386
|
+
return items.filter((x) => x !== void 0);
|
|
387
|
+
}
|
|
388
|
+
function hashHex(input, length) {
|
|
389
|
+
return createHash("sha256").update(input).digest("hex").slice(0, length);
|
|
390
|
+
}
|
|
391
|
+
function msToNs(ms) {
|
|
392
|
+
return (BigInt(Math.trunc(ms)) * 1000000n).toString();
|
|
393
|
+
}
|
|
394
|
+
function durationMs(startMs, endMs) {
|
|
395
|
+
if (endMs === void 0) return 0;
|
|
396
|
+
return Math.max(0, endMs - startMs);
|
|
397
|
+
}
|
|
398
|
+
function safeJson(value) {
|
|
399
|
+
try {
|
|
400
|
+
if (typeof value === "string") return value;
|
|
401
|
+
return JSON.stringify(value);
|
|
402
|
+
} catch {
|
|
403
|
+
return "";
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
//#endregion
|
|
407
|
+
//#region src/turn-builder.ts
|
|
408
|
+
/**
|
|
409
|
+
* Accumulates OpenClaw hook events per agent run (keyed by `runId`) into a
|
|
410
|
+
* `RunRecord` ready to be converted to OTLP spans. All mutation is synchronous
|
|
411
|
+
* and non-blocking so the hook runner can stay fire-and-forget.
|
|
412
|
+
*
|
|
413
|
+
* Event ordering assumptions (verified against OpenClaw
|
|
414
|
+
* src/agents/pi-embedded-runner/run/attempt.ts):
|
|
415
|
+
*
|
|
416
|
+
* session_start? -> [ llm_input -> (before_tool_call -> after_tool_call)* -> llm_output ]+ -> agent_end
|
|
417
|
+
*
|
|
418
|
+
* Tool calls arriving between an `llm_input` and its `llm_output` are attached
|
|
419
|
+
* to the currently-open LLM call. Tools arriving outside that window (e.g.
|
|
420
|
+
* `after_tool_call` fires after `llm_output` has already closed the call) are
|
|
421
|
+
* stored on the run's `orphanTools` list so we don't drop them.
|
|
422
|
+
*/
|
|
423
|
+
var TurnBuilder = class {
|
|
424
|
+
runs = /* @__PURE__ */ new Map();
|
|
425
|
+
onSessionStart(_evt, _ctx) {}
|
|
426
|
+
onLlmInput(evt, ctx) {
|
|
427
|
+
const run = this.ensureRun(evt.runId, ctx);
|
|
428
|
+
const call = {
|
|
429
|
+
runId: evt.runId,
|
|
430
|
+
sessionId: evt.sessionId,
|
|
431
|
+
sessionKey: ctx.sessionKey,
|
|
432
|
+
agentId: ctx.agentId,
|
|
433
|
+
provider: evt.provider,
|
|
434
|
+
requestModel: evt.model,
|
|
435
|
+
responseModel: void 0,
|
|
436
|
+
resolvedRef: void 0,
|
|
437
|
+
systemPrompt: evt.systemPrompt,
|
|
438
|
+
prompt: evt.prompt,
|
|
439
|
+
historyMessages: evt.historyMessages,
|
|
440
|
+
imagesCount: evt.imagesCount,
|
|
441
|
+
assistantTexts: [],
|
|
442
|
+
lastAssistant: void 0,
|
|
443
|
+
usage: void 0,
|
|
444
|
+
startMs: Date.now(),
|
|
445
|
+
endMs: void 0,
|
|
446
|
+
error: void 0,
|
|
447
|
+
toolCalls: []
|
|
448
|
+
};
|
|
449
|
+
run.llmCalls.push(call);
|
|
450
|
+
return call;
|
|
451
|
+
}
|
|
452
|
+
onBeforeToolCall(evt, ctx) {
|
|
453
|
+
if (!evt.runId) return;
|
|
454
|
+
const run = this.runs.get(evt.runId) ?? this.ensureRun(evt.runId, ctx);
|
|
455
|
+
const tool = {
|
|
456
|
+
toolCallId: evt.toolCallId ?? `${evt.toolName}:${randomUUID()}`,
|
|
457
|
+
toolName: evt.toolName,
|
|
458
|
+
params: evt.params,
|
|
459
|
+
result: void 0,
|
|
460
|
+
error: void 0,
|
|
461
|
+
startMs: Date.now(),
|
|
462
|
+
endMs: void 0,
|
|
463
|
+
durationMs: void 0,
|
|
464
|
+
agentId: ctx.agentId
|
|
465
|
+
};
|
|
466
|
+
const openCall = this.currentOpenCall(run);
|
|
467
|
+
if (openCall) openCall.toolCalls.push(tool);
|
|
468
|
+
else run.orphanTools.push(tool);
|
|
469
|
+
}
|
|
470
|
+
onAfterToolCall(evt, _ctx) {
|
|
471
|
+
if (!evt.runId) return;
|
|
472
|
+
const run = this.runs.get(evt.runId);
|
|
473
|
+
if (!run) return;
|
|
474
|
+
const tool = this.findToolRecord(run, evt.toolCallId, evt.toolName);
|
|
475
|
+
if (!tool) return;
|
|
476
|
+
tool.result = evt.result;
|
|
477
|
+
tool.error = evt.error;
|
|
478
|
+
tool.durationMs = evt.durationMs;
|
|
479
|
+
tool.endMs = Date.now();
|
|
480
|
+
}
|
|
481
|
+
onLlmOutput(evt, _ctx) {
|
|
482
|
+
const run = this.runs.get(evt.runId);
|
|
483
|
+
if (!run) return void 0;
|
|
484
|
+
const openCall = this.currentOpenCall(run);
|
|
485
|
+
if (!openCall) return void 0;
|
|
486
|
+
openCall.endMs = Date.now();
|
|
487
|
+
openCall.assistantTexts = evt.assistantTexts;
|
|
488
|
+
openCall.lastAssistant = evt.lastAssistant;
|
|
489
|
+
openCall.usage = evt.usage;
|
|
490
|
+
openCall.responseModel = evt.model;
|
|
491
|
+
openCall.resolvedRef = evt.resolvedRef;
|
|
492
|
+
return openCall;
|
|
493
|
+
}
|
|
494
|
+
onAgentEnd(evt, ctx) {
|
|
495
|
+
const runId = ctx.runId;
|
|
496
|
+
if (!runId) return void 0;
|
|
497
|
+
const run = this.runs.get(runId);
|
|
498
|
+
if (!run) return void 0;
|
|
499
|
+
run.endMs = Date.now();
|
|
500
|
+
run.success = evt.success;
|
|
501
|
+
run.error = evt.error;
|
|
502
|
+
for (const call of run.llmCalls) {
|
|
503
|
+
if (call.endMs === void 0) {
|
|
504
|
+
call.endMs = run.endMs;
|
|
505
|
+
if (evt.error && call.error === void 0) call.error = evt.error;
|
|
506
|
+
}
|
|
507
|
+
for (const tool of call.toolCalls) if (tool.endMs === void 0) tool.endMs = run.endMs;
|
|
508
|
+
}
|
|
509
|
+
for (const tool of run.orphanTools) if (tool.endMs === void 0) tool.endMs = run.endMs;
|
|
510
|
+
this.runs.delete(runId);
|
|
511
|
+
return run;
|
|
512
|
+
}
|
|
513
|
+
/** Drop a run without emitting — used on errors from the emit path. */
|
|
514
|
+
abandon(runId) {
|
|
515
|
+
this.runs.delete(runId);
|
|
516
|
+
}
|
|
517
|
+
/** Active runs count, for debug logging. */
|
|
518
|
+
inflightCount() {
|
|
519
|
+
return this.runs.size;
|
|
520
|
+
}
|
|
521
|
+
ensureRun(runId, ctx) {
|
|
522
|
+
let run = this.runs.get(runId);
|
|
523
|
+
if (run) return run;
|
|
524
|
+
run = {
|
|
525
|
+
runId,
|
|
526
|
+
sessionId: ctx.sessionId,
|
|
527
|
+
sessionKey: ctx.sessionKey,
|
|
528
|
+
agentId: ctx.agentId,
|
|
529
|
+
workspaceDir: ctx.workspaceDir,
|
|
530
|
+
messageProvider: ctx.messageProvider,
|
|
531
|
+
trigger: ctx.trigger,
|
|
532
|
+
channelId: ctx.channelId,
|
|
533
|
+
modelProviderId: ctx.modelProviderId,
|
|
534
|
+
modelId: ctx.modelId,
|
|
535
|
+
startMs: Date.now(),
|
|
536
|
+
endMs: void 0,
|
|
537
|
+
success: void 0,
|
|
538
|
+
error: void 0,
|
|
539
|
+
llmCalls: [],
|
|
540
|
+
orphanTools: []
|
|
541
|
+
};
|
|
542
|
+
this.runs.set(runId, run);
|
|
543
|
+
return run;
|
|
544
|
+
}
|
|
545
|
+
currentOpenCall(run) {
|
|
546
|
+
for (let i = run.llmCalls.length - 1; i >= 0; i--) {
|
|
547
|
+
const call = run.llmCalls[i];
|
|
548
|
+
if (call && call.endMs === void 0) return call;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
findToolRecord(run, toolCallId, toolName) {
|
|
552
|
+
const matchesId = (t) => Boolean(toolCallId && t.toolCallId === toolCallId);
|
|
553
|
+
for (const call of run.llmCalls) for (const t of call.toolCalls) if (matchesId(t)) return t;
|
|
554
|
+
for (const t of run.orphanTools) if (matchesId(t)) return t;
|
|
555
|
+
for (let i = run.llmCalls.length - 1; i >= 0; i--) {
|
|
556
|
+
const call = run.llmCalls[i];
|
|
557
|
+
if (!call) continue;
|
|
558
|
+
for (let j = call.toolCalls.length - 1; j >= 0; j--) {
|
|
559
|
+
const t = call.toolCalls[j];
|
|
560
|
+
if (t && t.toolName === toolName && t.endMs === void 0) return t;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
for (let i = run.orphanTools.length - 1; i >= 0; i--) {
|
|
564
|
+
const t = run.orphanTools[i];
|
|
565
|
+
if (t && t.toolName === toolName && t.endMs === void 0) return t;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
//#endregion
|
|
570
|
+
//#region src/plugin.ts
|
|
571
|
+
/**
|
|
572
|
+
* Register the Latitude plugin against an OpenClaw plugin API. OpenClaw calls
|
|
573
|
+
* this once at plugin activation; we wire up `llm_input`, `llm_output`, tool
|
|
574
|
+
* and lifecycle hooks to stream traces to Latitude.
|
|
575
|
+
*
|
|
576
|
+
* Every handler is fire-and-forget on OpenClaw's side (see
|
|
577
|
+
* `src/plugins/hooks.ts` — runLlmInput/runLlmOutput are documented as
|
|
578
|
+
* parallel and wrapped with `.catch()` at the call site in attempt.ts), so
|
|
579
|
+
* nothing we do here can slow the agent loop.
|
|
580
|
+
*/
|
|
581
|
+
function registerLatitudePlugin(api, opts = {}) {
|
|
582
|
+
const config = opts.config ?? loadConfig();
|
|
583
|
+
const logger = opts.logger ?? createLogger(config.debug);
|
|
584
|
+
if (!config.enabled) {
|
|
585
|
+
if (config.apiKey === "") logger.debug("disabled: LATITUDE_API_KEY is empty");
|
|
586
|
+
if (config.project === "") logger.debug("disabled: LATITUDE_PROJECT is empty");
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
logger.debug(`enabled: project=${config.project} base=${config.baseUrl}`);
|
|
590
|
+
const builder = new TurnBuilder();
|
|
591
|
+
api.on("session_start", (evt, ctx) => {
|
|
592
|
+
builder.onSessionStart(evt, ctx);
|
|
593
|
+
});
|
|
594
|
+
api.on("llm_input", (evt, ctx) => {
|
|
595
|
+
try {
|
|
596
|
+
builder.onLlmInput(evt, ctx);
|
|
597
|
+
} catch (err) {
|
|
598
|
+
logger.warn(`llm_input handler failed: ${String(err)}`);
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
api.on("before_tool_call", (evt, ctx) => {
|
|
602
|
+
try {
|
|
603
|
+
builder.onBeforeToolCall(evt, ctx);
|
|
604
|
+
} catch (err) {
|
|
605
|
+
logger.warn(`before_tool_call handler failed: ${String(err)}`);
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
api.on("after_tool_call", (evt, ctx) => {
|
|
609
|
+
try {
|
|
610
|
+
builder.onAfterToolCall(evt, ctx);
|
|
611
|
+
} catch (err) {
|
|
612
|
+
logger.warn(`after_tool_call handler failed: ${String(err)}`);
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
api.on("llm_output", (evt, ctx) => {
|
|
616
|
+
try {
|
|
617
|
+
builder.onLlmOutput(evt, ctx);
|
|
618
|
+
} catch (err) {
|
|
619
|
+
logger.warn(`llm_output handler failed: ${String(err)}`);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
api.on("agent_end", (evt, ctx) => {
|
|
623
|
+
try {
|
|
624
|
+
const run = builder.onAgentEnd(evt, ctx);
|
|
625
|
+
if (!run) {
|
|
626
|
+
logger.debug("agent_end fired without a matching run in flight");
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
opts.onEmit?.(run);
|
|
630
|
+
const payload = buildOtlpRequest(run);
|
|
631
|
+
postTraces({
|
|
632
|
+
baseUrl: config.baseUrl,
|
|
633
|
+
apiKey: config.apiKey,
|
|
634
|
+
project: config.project,
|
|
635
|
+
payload,
|
|
636
|
+
logger
|
|
637
|
+
});
|
|
638
|
+
} catch (err) {
|
|
639
|
+
logger.warn(`agent_end handler failed: ${String(err)}`);
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
//#endregion
|
|
644
|
+
export { registerLatitudePlugin as default };
|
|
645
|
+
|
|
646
|
+
//# sourceMappingURL=plugin.js.map
|