@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/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