@latitude-data/openclaw-telemetry 0.0.3 → 0.0.5
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 +94 -40
- package/dist/cli.js +297 -111
- package/dist/cli.js.map +1 -1
- package/dist/plugin.d.ts +55 -65
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +732 -444
- package/dist/plugin.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/plugin.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
2
|
import { arch, hostname, platform, release } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
6
|
//#region src/client.ts
|
|
4
7
|
async function postTraces({ baseUrl, apiKey, project, payload, logger, timeoutMs = 1e4 }) {
|
|
5
8
|
const url = `${baseUrl.replace(/\/+$/, "")}/v1/traces`;
|
|
@@ -74,10 +77,10 @@ function createLogger(debugEnabled) {
|
|
|
74
77
|
//#endregion
|
|
75
78
|
//#region src/otlp.ts
|
|
76
79
|
const SCOPE_NAME = "@latitude-data/openclaw-telemetry";
|
|
77
|
-
const SCOPE_VERSION =
|
|
80
|
+
const SCOPE_VERSION = readScopeVersion();
|
|
78
81
|
/** Build an OTLP export request for a single completed agent run. */
|
|
79
|
-
function buildOtlpRequest(
|
|
80
|
-
const spans =
|
|
82
|
+
function buildOtlpRequest(result, options) {
|
|
83
|
+
const spans = result.spans.map((span) => toOtlpSpan(span, options));
|
|
81
84
|
return { resourceSpans: [{
|
|
82
85
|
resource: { attributes: resourceAttrs() },
|
|
83
86
|
scopeSpans: [{
|
|
@@ -89,210 +92,158 @@ function buildOtlpRequest(run, options) {
|
|
|
89
92
|
}]
|
|
90
93
|
}] };
|
|
91
94
|
}
|
|
92
|
-
function
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
out.push(buildToolSpan(traceId, interactionSpanId, toolSpanId, tool, run, options));
|
|
107
|
-
});
|
|
108
|
-
return out;
|
|
109
|
-
}
|
|
110
|
-
function buildInteractionSpan(traceId, spanId, run, options) {
|
|
111
|
-
const startNs = msToNs(run.startMs);
|
|
112
|
-
const endNs = msToNs(run.endMs ?? run.startMs);
|
|
113
|
-
const totalTools = run.llmCalls.reduce((sum, c) => sum + c.toolCalls.length, 0) + run.orphanTools.length;
|
|
114
|
-
const totalUsage = aggregateUsage(run.llmCalls);
|
|
95
|
+
function toOtlpSpan(span, options) {
|
|
96
|
+
const startNs = msToNs(span.startMs);
|
|
97
|
+
const endNs = msToNs(span.endMs ?? span.startMs);
|
|
98
|
+
const attrs = [];
|
|
99
|
+
for (const [rawKey, value] of Object.entries(span.attrs)) {
|
|
100
|
+
if (value === void 0 || value === null) continue;
|
|
101
|
+
const isGated = rawKey.endsWith(":gated");
|
|
102
|
+
if (isGated && !options.allowConversationAccess) continue;
|
|
103
|
+
const kv = encodeAttr(isGated ? rawKey.slice(0, -6) : rawKey, value);
|
|
104
|
+
if (kv !== void 0) attrs.push(kv);
|
|
105
|
+
}
|
|
106
|
+
attrs.push(bool("latitude.captured.content", options.allowConversationAccess));
|
|
107
|
+
if (span.endMs !== void 0) attrs.push(int("openclaw.duration_ms.computed", Math.max(0, span.endMs - span.startMs)));
|
|
108
|
+
const statusCode = span.outcome === "error" ? 2 : 1;
|
|
115
109
|
return {
|
|
116
|
-
traceId,
|
|
117
|
-
spanId,
|
|
118
|
-
parentSpanId:
|
|
119
|
-
name:
|
|
110
|
+
traceId: span.traceId,
|
|
111
|
+
spanId: span.spanId,
|
|
112
|
+
parentSpanId: span.parentSpanId,
|
|
113
|
+
name: span.name,
|
|
120
114
|
kind: 1,
|
|
121
115
|
startTimeUnixNano: startNs,
|
|
122
116
|
endTimeUnixNano: endNs,
|
|
123
|
-
attributes:
|
|
124
|
-
|
|
125
|
-
str("interaction.kind", "agent_run"),
|
|
126
|
-
str("openclaw.run.id", run.runId),
|
|
127
|
-
run.sessionId ? str("openclaw.session.id", run.sessionId) : void 0,
|
|
128
|
-
run.sessionId ? str("session.id", run.sessionId) : void 0,
|
|
129
|
-
run.sessionKey ? str("openclaw.session.key", run.sessionKey) : void 0,
|
|
130
|
-
run.agentId ? str("openclaw.agent.id", run.agentId) : void 0,
|
|
131
|
-
run.agentId ? str("openclaw.agent.name", run.agentId) : void 0,
|
|
132
|
-
run.workspaceDir ? str("openclaw.workspace.dir", run.workspaceDir) : void 0,
|
|
133
|
-
run.messageProvider ? str("openclaw.message.provider", run.messageProvider) : void 0,
|
|
134
|
-
run.channelId ? str("openclaw.channel.id", run.channelId) : void 0,
|
|
135
|
-
run.trigger ? str("openclaw.trigger", run.trigger) : void 0,
|
|
136
|
-
run.modelProviderId ? str("openclaw.model.provider.id", run.modelProviderId) : void 0,
|
|
137
|
-
run.modelId ? str("openclaw.model.id", run.modelId) : void 0,
|
|
138
|
-
int("interaction.duration_ms", durationMs(run.startMs, run.endMs)),
|
|
139
|
-
int("interaction.call_count", run.llmCalls.length),
|
|
140
|
-
int("interaction.tool_call_count", totalTools),
|
|
141
|
-
run.success !== void 0 ? bool("openclaw.run.success", run.success) : void 0,
|
|
142
|
-
run.error ? str("openclaw.run.error", run.error) : void 0,
|
|
143
|
-
totalUsage.input !== void 0 ? int("gen_ai.usage.input_tokens", totalUsage.input) : void 0,
|
|
144
|
-
totalUsage.output !== void 0 ? int("gen_ai.usage.output_tokens", totalUsage.output) : void 0,
|
|
145
|
-
totalUsage.cacheRead !== void 0 ? int("gen_ai.usage.cache_read_input_tokens", totalUsage.cacheRead) : void 0,
|
|
146
|
-
totalUsage.cacheWrite !== void 0 ? int("gen_ai.usage.cache_creation_input_tokens", totalUsage.cacheWrite) : void 0,
|
|
147
|
-
totalUsage.total !== void 0 ? int("gen_ai.usage.total_tokens", totalUsage.total) : void 0,
|
|
148
|
-
options.allowConversationAccess && run.llmCalls[0]?.prompt ? str("user_prompt", run.llmCalls[0].prompt) : void 0,
|
|
149
|
-
bool("latitude.captured.content", options.allowConversationAccess)
|
|
150
|
-
]),
|
|
151
|
-
status: { code: run.success === false ? 2 : 1 }
|
|
117
|
+
attributes: attrs,
|
|
118
|
+
status: { code: statusCode }
|
|
152
119
|
};
|
|
153
120
|
}
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
121
|
+
function encodeAttr(key, value) {
|
|
122
|
+
if (value === void 0 || value === null) return void 0;
|
|
123
|
+
if (typeof value === "string") return str(key, value);
|
|
124
|
+
if (typeof value === "boolean") return bool(key, value);
|
|
125
|
+
if (typeof value === "number") return Number.isInteger(value) ? int(key, value) : {
|
|
126
|
+
key,
|
|
127
|
+
value: { doubleValue: value }
|
|
128
|
+
};
|
|
129
|
+
return str(key, safeJson$1(value));
|
|
130
|
+
}
|
|
131
|
+
function resourceAttrs() {
|
|
132
|
+
return [
|
|
133
|
+
str("service.name", "openclaw"),
|
|
134
|
+
str("service.version", SCOPE_VERSION),
|
|
135
|
+
str("host.name", hostname()),
|
|
136
|
+
str("host.arch", arch()),
|
|
137
|
+
str("os.type", platform()),
|
|
138
|
+
str("os.version", release())
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
function str(key, value) {
|
|
164
142
|
return {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
parentSpanId,
|
|
168
|
-
name: "llm_request",
|
|
169
|
-
kind: 3,
|
|
170
|
-
startTimeUnixNano: startNs,
|
|
171
|
-
endTimeUnixNano: endNs,
|
|
172
|
-
attributes: stripUndef([
|
|
173
|
-
str("span.type", "llm_request"),
|
|
174
|
-
str("gen_ai.operation.name", "chat"),
|
|
175
|
-
str("llm_request.context", "interaction"),
|
|
176
|
-
int("llm_request.call_index", callIdx),
|
|
177
|
-
str("gen_ai.system", call.provider),
|
|
178
|
-
str("openclaw.provider", call.provider),
|
|
179
|
-
str("gen_ai.request.model", call.requestModel),
|
|
180
|
-
str("model", call.requestModel),
|
|
181
|
-
call.responseModel ? str("gen_ai.response.model", call.responseModel) : void 0,
|
|
182
|
-
call.resolvedRef ? str("openclaw.resolved.ref", call.resolvedRef) : void 0,
|
|
183
|
-
run.sessionId ? str("session.id", run.sessionId) : void 0,
|
|
184
|
-
run.sessionKey ? str("openclaw.session.key", run.sessionKey) : void 0,
|
|
185
|
-
str("openclaw.run.id", call.runId),
|
|
186
|
-
call.agentId ? str("openclaw.agent.id", call.agentId) : void 0,
|
|
187
|
-
call.agentId ? str("openclaw.agent.name", call.agentId) : void 0,
|
|
188
|
-
call.usage?.input !== void 0 ? int("gen_ai.usage.input_tokens", call.usage.input) : void 0,
|
|
189
|
-
call.usage?.input !== void 0 ? int("input_tokens", call.usage.input) : void 0,
|
|
190
|
-
call.usage?.output !== void 0 ? int("gen_ai.usage.output_tokens", call.usage.output) : void 0,
|
|
191
|
-
call.usage?.output !== void 0 ? int("output_tokens", call.usage.output) : void 0,
|
|
192
|
-
call.usage?.cacheRead !== void 0 ? int("gen_ai.usage.cache_read_input_tokens", call.usage.cacheRead) : void 0,
|
|
193
|
-
call.usage?.cacheRead !== void 0 ? int("cache_read_tokens", call.usage.cacheRead) : void 0,
|
|
194
|
-
call.usage?.cacheWrite !== void 0 ? int("gen_ai.usage.cache_creation_input_tokens", call.usage.cacheWrite) : void 0,
|
|
195
|
-
call.usage?.cacheWrite !== void 0 ? int("cache_creation_tokens", call.usage.cacheWrite) : void 0,
|
|
196
|
-
call.usage?.total !== void 0 ? int("gen_ai.usage.total_tokens", call.usage.total) : void 0,
|
|
197
|
-
systemInstructions ? str("gen_ai.system_instructions", systemInstructions) : void 0,
|
|
198
|
-
inputMessages ? str("gen_ai.input.messages", JSON.stringify(inputMessages)) : void 0,
|
|
199
|
-
outputMessages ? str("gen_ai.output.messages", JSON.stringify(outputMessages)) : void 0,
|
|
200
|
-
bool("latitude.captured.content", captureContent),
|
|
201
|
-
int("openclaw.images.count", call.imagesCount),
|
|
202
|
-
int("llm_request.tool_call_count", call.toolCalls.length),
|
|
203
|
-
int("llm_request.duration_ms", durationMs(call.startMs, call.endMs)),
|
|
204
|
-
call.error ? str("error.type", "llm_error") : void 0,
|
|
205
|
-
call.error ? str("error.message", call.error) : void 0,
|
|
206
|
-
str("success", call.error ? "false" : "true"),
|
|
207
|
-
str("llm_request.captured", "true")
|
|
208
|
-
]),
|
|
209
|
-
status: { code: call.error ? 2 : 1 }
|
|
143
|
+
key,
|
|
144
|
+
value: { stringValue: value }
|
|
210
145
|
};
|
|
211
146
|
}
|
|
212
|
-
function
|
|
213
|
-
const startNs = msToNs(tool.startMs);
|
|
214
|
-
const endNs = msToNs(tool.endMs ?? tool.startMs);
|
|
215
|
-
const isError = Boolean(tool.error);
|
|
216
|
-
const captureContent = options.allowConversationAccess;
|
|
147
|
+
function int(key, value) {
|
|
217
148
|
return {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
parentSpanId,
|
|
221
|
-
name: `tool:${tool.toolName}`,
|
|
222
|
-
kind: 1,
|
|
223
|
-
startTimeUnixNano: startNs,
|
|
224
|
-
endTimeUnixNano: endNs,
|
|
225
|
-
attributes: stripUndef([
|
|
226
|
-
str("span.type", "tool_execution"),
|
|
227
|
-
str("gen_ai.operation.name", "execute_tool"),
|
|
228
|
-
str("gen_ai.tool.name", tool.toolName),
|
|
229
|
-
str("gen_ai.tool.call.id", tool.toolCallId),
|
|
230
|
-
captureContent ? str("gen_ai.tool.call.arguments", safeJson(tool.params)) : void 0,
|
|
231
|
-
captureContent && tool.result !== void 0 ? str("gen_ai.tool.call.result", safeJson(tool.result)) : void 0,
|
|
232
|
-
bool("latitude.captured.content", captureContent),
|
|
233
|
-
isError ? str("error.type", "tool_error") : void 0,
|
|
234
|
-
isError ? str("error.message", tool.error ?? "") : void 0,
|
|
235
|
-
bool("tool.is_error", isError),
|
|
236
|
-
str("success", isError ? "false" : "true"),
|
|
237
|
-
tool.durationMs !== void 0 ? int("tool.duration_ms", tool.durationMs) : void 0,
|
|
238
|
-
run.sessionId ? str("session.id", run.sessionId) : void 0,
|
|
239
|
-
run.sessionKey ? str("openclaw.session.key", run.sessionKey) : void 0,
|
|
240
|
-
str("openclaw.run.id", run.runId),
|
|
241
|
-
tool.agentId ? str("openclaw.agent.id", tool.agentId) : void 0,
|
|
242
|
-
tool.agentId ? str("openclaw.agent.name", tool.agentId) : void 0
|
|
243
|
-
]),
|
|
244
|
-
status: { code: isError ? 2 : 1 }
|
|
149
|
+
key,
|
|
150
|
+
value: { intValue: String(Math.trunc(value)) }
|
|
245
151
|
};
|
|
246
152
|
}
|
|
247
|
-
function
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
153
|
+
function bool(key, value) {
|
|
154
|
+
return {
|
|
155
|
+
key,
|
|
156
|
+
value: { boolValue: value }
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function msToNs(ms) {
|
|
160
|
+
return (BigInt(Math.trunc(ms)) * 1000000n).toString();
|
|
161
|
+
}
|
|
162
|
+
function safeJson$1(value) {
|
|
163
|
+
try {
|
|
164
|
+
if (typeof value === "string") return value;
|
|
165
|
+
return JSON.stringify(value);
|
|
166
|
+
} catch {
|
|
167
|
+
return "";
|
|
252
168
|
}
|
|
253
|
-
if (call.prompt.length > 0) out.push({
|
|
254
|
-
role: "user",
|
|
255
|
-
parts: [{
|
|
256
|
-
type: "text",
|
|
257
|
-
content: call.prompt
|
|
258
|
-
}]
|
|
259
|
-
});
|
|
260
|
-
return out;
|
|
261
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Runtime-read package version, so OTLP `scope.version` and `service.version`
|
|
172
|
+
* always reflect what's actually installed. Read once at module load and
|
|
173
|
+
* cached. Falls back to `"unknown"` if the read fails.
|
|
174
|
+
*
|
|
175
|
+
* Same import.meta.url + ../package.json pattern used by `cli.ts` for the
|
|
176
|
+
* `--version` flag — single source of truth in package.json.
|
|
177
|
+
*/
|
|
178
|
+
function readScopeVersion() {
|
|
179
|
+
try {
|
|
180
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
181
|
+
return JSON.parse(readFileSync(pkgPath, "utf-8")).version ?? "unknown";
|
|
182
|
+
} catch {
|
|
183
|
+
return "unknown";
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
//#region src/messages.ts
|
|
262
188
|
const ALLOWED_ROLES = new Set([
|
|
263
189
|
"system",
|
|
264
190
|
"user",
|
|
265
191
|
"assistant",
|
|
266
192
|
"tool"
|
|
267
193
|
]);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
function
|
|
194
|
+
/**
|
|
195
|
+
* Normalize a single message of any of the provider shapes we know about.
|
|
196
|
+
* Returns `undefined` for non-objects so the caller can skip them.
|
|
197
|
+
*/
|
|
198
|
+
function normalizeMessage(raw) {
|
|
273
199
|
if (!raw || typeof raw !== "object") return void 0;
|
|
274
200
|
const obj = raw;
|
|
275
|
-
const role =
|
|
201
|
+
const role = coerceRole(obj.role);
|
|
202
|
+
if (Array.isArray(obj.parts)) {
|
|
203
|
+
const parts = [];
|
|
204
|
+
for (const p of obj.parts) if (p && typeof p === "object") parts.push(p);
|
|
205
|
+
return {
|
|
206
|
+
role,
|
|
207
|
+
parts: parts.length > 0 ? parts : [{
|
|
208
|
+
type: "text",
|
|
209
|
+
content: safeJson(raw)
|
|
210
|
+
}]
|
|
211
|
+
};
|
|
212
|
+
}
|
|
276
213
|
const content = obj.content ?? obj.text ?? obj.message;
|
|
277
|
-
if (
|
|
214
|
+
if (role === "tool" && obj.tool_call_id !== void 0) return {
|
|
278
215
|
role,
|
|
279
216
|
parts: [{
|
|
280
|
-
type: "
|
|
281
|
-
|
|
217
|
+
type: "tool_call_response",
|
|
218
|
+
id: typeof obj.tool_call_id === "string" ? obj.tool_call_id : "",
|
|
219
|
+
response: content ?? safeJson(obj)
|
|
282
220
|
}]
|
|
283
221
|
};
|
|
222
|
+
if (typeof content === "string") {
|
|
223
|
+
const parts = [{
|
|
224
|
+
type: "text",
|
|
225
|
+
content
|
|
226
|
+
}];
|
|
227
|
+
appendToolCalls(parts, obj.tool_calls);
|
|
228
|
+
return {
|
|
229
|
+
role,
|
|
230
|
+
parts
|
|
231
|
+
};
|
|
232
|
+
}
|
|
284
233
|
if (Array.isArray(content)) {
|
|
285
234
|
const parts = [];
|
|
286
235
|
for (const block of content) {
|
|
287
|
-
const part =
|
|
236
|
+
const part = normalizeBlock(block);
|
|
288
237
|
if (part) parts.push(part);
|
|
289
238
|
}
|
|
239
|
+
appendToolCalls(parts, obj.tool_calls);
|
|
240
|
+
if (parts.length === 0) parts.push({
|
|
241
|
+
type: "text",
|
|
242
|
+
content: safeJson(content)
|
|
243
|
+
});
|
|
290
244
|
return {
|
|
291
245
|
role,
|
|
292
|
-
parts
|
|
293
|
-
type: "text",
|
|
294
|
-
content: JSON.stringify(content)
|
|
295
|
-
}]
|
|
246
|
+
parts
|
|
296
247
|
};
|
|
297
248
|
}
|
|
298
249
|
return {
|
|
@@ -303,13 +254,76 @@ function normalizeHistoryMessage(raw) {
|
|
|
303
254
|
}]
|
|
304
255
|
};
|
|
305
256
|
}
|
|
306
|
-
|
|
257
|
+
/** Normalize an array of provider messages. */
|
|
258
|
+
function normalizeMessages(raw) {
|
|
259
|
+
const out = [];
|
|
260
|
+
for (const m of raw) {
|
|
261
|
+
const norm = normalizeMessage(m);
|
|
262
|
+
if (norm) out.push(norm);
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
/** Build a single user message from a string prompt. */
|
|
267
|
+
function userMessageFromPrompt(prompt) {
|
|
268
|
+
return {
|
|
269
|
+
role: "user",
|
|
270
|
+
parts: [{
|
|
271
|
+
type: "text",
|
|
272
|
+
content: prompt
|
|
273
|
+
}]
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/** Build a single assistant message from `assistantTexts` + `lastAssistant` fallback. */
|
|
277
|
+
function assistantMessageFromOutput(assistantTexts, lastAssistant) {
|
|
278
|
+
if (lastAssistant !== void 0) {
|
|
279
|
+
const norm = normalizeMessage(lastAssistant);
|
|
280
|
+
if (norm) return {
|
|
281
|
+
...norm,
|
|
282
|
+
role: "assistant"
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const parts = [];
|
|
286
|
+
for (const text of assistantTexts) if (text.length > 0) parts.push({
|
|
287
|
+
type: "text",
|
|
288
|
+
content: text
|
|
289
|
+
});
|
|
290
|
+
if (parts.length === 0) parts.push({
|
|
291
|
+
type: "text",
|
|
292
|
+
content: ""
|
|
293
|
+
});
|
|
294
|
+
return {
|
|
295
|
+
role: "assistant",
|
|
296
|
+
parts
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Wrap a system prompt string into the parts-array shape expected for
|
|
301
|
+
* `gen_ai.system_instructions`. Empty string in → single empty text part out
|
|
302
|
+
* (still a valid array, never `undefined`).
|
|
303
|
+
*/
|
|
304
|
+
function systemInstructionsParts(prompt) {
|
|
305
|
+
return [{
|
|
306
|
+
type: "text",
|
|
307
|
+
content: prompt
|
|
308
|
+
}];
|
|
309
|
+
}
|
|
310
|
+
function coerceRole(raw) {
|
|
311
|
+
if (typeof raw !== "string") return "user";
|
|
312
|
+
return ALLOWED_ROLES.has(raw) ? raw : "user";
|
|
313
|
+
}
|
|
314
|
+
function normalizeBlock(raw) {
|
|
307
315
|
if (typeof raw === "string") return {
|
|
308
316
|
type: "text",
|
|
309
317
|
content: raw
|
|
310
318
|
};
|
|
311
319
|
if (!raw || typeof raw !== "object") return void 0;
|
|
312
320
|
const obj = raw;
|
|
321
|
+
if (typeof obj.type === "string" && (typeof obj.content === "string" || obj.content === void 0)) {
|
|
322
|
+
if (obj.type === "text" && typeof obj.content === "string") return {
|
|
323
|
+
type: "text",
|
|
324
|
+
content: obj.content
|
|
325
|
+
};
|
|
326
|
+
}
|
|
313
327
|
const type = typeof obj.type === "string" ? obj.type : "text";
|
|
314
328
|
if (type === "text" && typeof obj.text === "string") return {
|
|
315
329
|
type: "text",
|
|
@@ -321,104 +335,66 @@ function normalizeContentBlock(raw) {
|
|
|
321
335
|
name: typeof obj.name === "string" ? obj.name : "",
|
|
322
336
|
arguments: obj.input ?? {}
|
|
323
337
|
};
|
|
338
|
+
if (type === "tool_call") return {
|
|
339
|
+
type: "tool_call",
|
|
340
|
+
id: typeof obj.id === "string" ? obj.id : "",
|
|
341
|
+
name: typeof obj.name === "string" ? obj.name : "",
|
|
342
|
+
arguments: obj.arguments ?? obj.input ?? {}
|
|
343
|
+
};
|
|
324
344
|
if (type === "tool_result") return {
|
|
325
345
|
type: "tool_call_response",
|
|
326
346
|
id: typeof obj.tool_use_id === "string" ? obj.tool_use_id : "",
|
|
327
347
|
response: obj.content ?? ""
|
|
328
348
|
};
|
|
329
|
-
if (type === "
|
|
330
|
-
type: "
|
|
331
|
-
|
|
332
|
-
|
|
349
|
+
if (type === "tool_call_response") return {
|
|
350
|
+
type: "tool_call_response",
|
|
351
|
+
id: typeof obj.id === "string" ? obj.id : "",
|
|
352
|
+
response: obj.response ?? ""
|
|
333
353
|
};
|
|
334
|
-
return {
|
|
335
|
-
type,
|
|
336
|
-
content:
|
|
354
|
+
if (type === "thinking" && typeof obj.thinking === "string") return {
|
|
355
|
+
type: "reasoning",
|
|
356
|
+
content: obj.thinking
|
|
337
357
|
};
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
for (const text of call.assistantTexts) if (text.length > 0) parts.push({
|
|
342
|
-
type: "text",
|
|
343
|
-
content: text
|
|
344
|
-
});
|
|
345
|
-
for (const tool of call.toolCalls) parts.push({
|
|
346
|
-
type: "tool_call",
|
|
347
|
-
id: tool.toolCallId,
|
|
348
|
-
name: tool.toolName,
|
|
349
|
-
arguments: tool.params
|
|
350
|
-
});
|
|
351
|
-
if (parts.length === 0 && call.lastAssistant !== void 0) parts.push({
|
|
352
|
-
type: "text",
|
|
353
|
-
content: safeJson(call.lastAssistant)
|
|
354
|
-
});
|
|
355
|
-
return [{
|
|
356
|
-
role: "assistant",
|
|
357
|
-
parts
|
|
358
|
-
}];
|
|
359
|
-
}
|
|
360
|
-
function aggregateUsage(calls) {
|
|
361
|
-
const agg = {
|
|
362
|
-
input: void 0,
|
|
363
|
-
output: void 0,
|
|
364
|
-
cacheRead: void 0,
|
|
365
|
-
cacheWrite: void 0,
|
|
366
|
-
total: void 0
|
|
358
|
+
if (type === "reasoning" && typeof obj.content === "string") return {
|
|
359
|
+
type: "reasoning",
|
|
360
|
+
content: obj.content
|
|
367
361
|
};
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
add("cacheRead", c.usage.cacheRead);
|
|
377
|
-
add("cacheWrite", c.usage.cacheWrite);
|
|
378
|
-
add("total", c.usage.total);
|
|
362
|
+
if (type === "image" && obj.source && typeof obj.source === "object") {
|
|
363
|
+
const src = obj.source;
|
|
364
|
+
const uri = src.url ?? (src.data ? `data:${src.media_type ?? "image/unknown"};base64,${src.data}` : "");
|
|
365
|
+
if (uri) return {
|
|
366
|
+
type: "uri",
|
|
367
|
+
modality: "image",
|
|
368
|
+
uri
|
|
369
|
+
};
|
|
379
370
|
}
|
|
380
|
-
return agg;
|
|
381
|
-
}
|
|
382
|
-
function resourceAttrs() {
|
|
383
|
-
return [
|
|
384
|
-
str("service.name", "openclaw"),
|
|
385
|
-
str("service.version", SCOPE_VERSION),
|
|
386
|
-
str("host.name", hostname()),
|
|
387
|
-
str("host.arch", arch()),
|
|
388
|
-
str("os.type", platform()),
|
|
389
|
-
str("os.version", release())
|
|
390
|
-
];
|
|
391
|
-
}
|
|
392
|
-
function str(key, value) {
|
|
393
371
|
return {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
function int(key, value) {
|
|
399
|
-
return {
|
|
400
|
-
key,
|
|
401
|
-
value: { intValue: String(Math.trunc(value)) }
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
function bool(key, value) {
|
|
405
|
-
return {
|
|
406
|
-
key,
|
|
407
|
-
value: { boolValue: value }
|
|
372
|
+
type,
|
|
373
|
+
content: safeJson(raw)
|
|
408
374
|
};
|
|
409
375
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
376
|
+
/**
|
|
377
|
+
* OpenAI assistant messages put tool calls in a separate `tool_calls` array
|
|
378
|
+
* alongside string content. Append them as parts so the trace shows what the
|
|
379
|
+
* model emitted in that turn.
|
|
380
|
+
*/
|
|
381
|
+
function appendToolCalls(parts, raw) {
|
|
382
|
+
if (!Array.isArray(raw)) return;
|
|
383
|
+
for (const tc of raw) {
|
|
384
|
+
if (!tc || typeof tc !== "object") continue;
|
|
385
|
+
const t = tc;
|
|
386
|
+
const fn = t.function;
|
|
387
|
+
let parsedArgs = fn?.arguments;
|
|
388
|
+
if (typeof parsedArgs === "string") try {
|
|
389
|
+
parsedArgs = JSON.parse(parsedArgs);
|
|
390
|
+
} catch {}
|
|
391
|
+
parts.push({
|
|
392
|
+
type: "tool_call",
|
|
393
|
+
id: typeof t.id === "string" ? t.id : "",
|
|
394
|
+
name: fn?.name ?? "",
|
|
395
|
+
arguments: parsedArgs ?? {}
|
|
396
|
+
});
|
|
397
|
+
}
|
|
422
398
|
}
|
|
423
399
|
function safeJson(value) {
|
|
424
400
|
try {
|
|
@@ -429,179 +405,484 @@ function safeJson(value) {
|
|
|
429
405
|
}
|
|
430
406
|
}
|
|
431
407
|
//#endregion
|
|
432
|
-
//#region src/
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
* and non-blocking so the hook runner can stay fire-and-forget.
|
|
437
|
-
*
|
|
438
|
-
* Event ordering assumptions (verified against OpenClaw
|
|
439
|
-
* src/agents/pi-embedded-runner/run/attempt.ts):
|
|
440
|
-
*
|
|
441
|
-
* session_start? -> [ llm_input -> (before_tool_call -> after_tool_call)* -> llm_output ]+ -> agent_end
|
|
442
|
-
*
|
|
443
|
-
* Tool calls arriving between an `llm_input` and its `llm_output` are attached
|
|
444
|
-
* to the currently-open LLM call. Tools arriving outside that window (e.g.
|
|
445
|
-
* `after_tool_call` fires after `llm_output` has already closed the call) are
|
|
446
|
-
* stored on the run's `orphanTools` list so we don't drop them.
|
|
447
|
-
*/
|
|
448
|
-
var TurnBuilder = class {
|
|
408
|
+
//#region src/span-builder.ts
|
|
409
|
+
const SUBAGENT_LINK_TTL_MS = 3600 * 1e3;
|
|
410
|
+
const SUBAGENT_LINK_MAX = 1e3;
|
|
411
|
+
var SpanBuilder = class {
|
|
449
412
|
runs = /* @__PURE__ */ new Map();
|
|
450
|
-
|
|
413
|
+
subagentLinks = /* @__PURE__ */ new Map();
|
|
414
|
+
inflightCount() {
|
|
415
|
+
return this.runs.size;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Open the root `agent` span. If the runId was previously registered as a
|
|
419
|
+
* subagent's child, propagate the parent's traceId and parent the new span
|
|
420
|
+
* under the parent's `subagent` span — so the entire subagent's work nests
|
|
421
|
+
* inside the parent's trace as one waterfall.
|
|
422
|
+
*/
|
|
423
|
+
onBeforeAgentStart(evt, ctx) {
|
|
424
|
+
const runId = ctx.runId;
|
|
425
|
+
if (!runId) return;
|
|
426
|
+
if (this.runs.has(runId)) return;
|
|
427
|
+
const link = this.subagentLinks.get(runId);
|
|
428
|
+
const traceId = link?.traceId ?? hashHex(runId, 32);
|
|
429
|
+
const parentSpanId = link?.subagentSpanId ?? "";
|
|
430
|
+
const agent = {
|
|
431
|
+
spanId: hashHex(`${traceId}:${runId}:agent`, 16),
|
|
432
|
+
traceId,
|
|
433
|
+
parentSpanId,
|
|
434
|
+
name: "agent",
|
|
435
|
+
startMs: Date.now(),
|
|
436
|
+
endMs: void 0,
|
|
437
|
+
attrs: {
|
|
438
|
+
...flattenCtx(ctx),
|
|
439
|
+
...latitudeAttrs(ctx),
|
|
440
|
+
"openclaw.run.id": runId,
|
|
441
|
+
"before_agent_start.prompt:gated": evt.prompt,
|
|
442
|
+
"before_agent_start.messages:gated": evt.messages ? normalizeMessages(evt.messages) : void 0
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
this.runs.set(runId, {
|
|
446
|
+
agent,
|
|
447
|
+
history: [],
|
|
448
|
+
openModelCalls: /* @__PURE__ */ new Map(),
|
|
449
|
+
openToolCalls: /* @__PURE__ */ new Map(),
|
|
450
|
+
openCompaction: void 0,
|
|
451
|
+
closed: [],
|
|
452
|
+
childSubagentSpans: /* @__PURE__ */ new Map()
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Enrich the open `agent` span with content + identity from the LLM input.
|
|
457
|
+
* Also seeds the rolling history snapshot used by per-call `model_call`
|
|
458
|
+
* input attributes.
|
|
459
|
+
*
|
|
460
|
+
* Provider-specific message shapes get normalized into the parts-based
|
|
461
|
+
* GenAI format here — that's the contract Latitude's downstream parser
|
|
462
|
+
* expects on `gen_ai.input.messages` and `gen_ai.system_instructions`.
|
|
463
|
+
*/
|
|
451
464
|
onLlmInput(evt, ctx) {
|
|
452
|
-
const run = this.
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
465
|
+
const run = this.runs.get(ctx.runId ?? evt.runId);
|
|
466
|
+
if (!run) return;
|
|
467
|
+
const inputMessages = [...normalizeMessages(evt.historyMessages)];
|
|
468
|
+
if (evt.prompt) inputMessages.push(userMessageFromPrompt(evt.prompt));
|
|
469
|
+
Object.assign(run.agent.attrs, {
|
|
470
|
+
"gen_ai.system_instructions:gated": evt.systemPrompt ? systemInstructionsParts(evt.systemPrompt) : void 0,
|
|
471
|
+
"user_prompt:gated": evt.prompt,
|
|
472
|
+
"gen_ai.input.messages:gated": inputMessages,
|
|
473
|
+
"openclaw.images.count": evt.imagesCount,
|
|
474
|
+
"gen_ai.request.model": evt.model,
|
|
475
|
+
"gen_ai.system": evt.provider,
|
|
476
|
+
"openclaw.provider": evt.provider
|
|
477
|
+
});
|
|
478
|
+
run.history = inputMessages;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Enrich the agent span with attempt-aggregate output + token usage.
|
|
482
|
+
* (Per-call usage isn't surfaced by OpenClaw today — see PR #2986.)
|
|
483
|
+
*/
|
|
484
|
+
onLlmOutput(evt, ctx) {
|
|
485
|
+
const run = this.runs.get(ctx.runId ?? evt.runId);
|
|
486
|
+
if (!run) return;
|
|
487
|
+
const assistantMessage = assistantMessageFromOutput(evt.assistantTexts, evt.lastAssistant);
|
|
488
|
+
Object.assign(run.agent.attrs, {
|
|
489
|
+
"gen_ai.output.messages:gated": [assistantMessage],
|
|
490
|
+
"openclaw.resolved.ref": evt.resolvedRef,
|
|
491
|
+
"openclaw.harness.id": evt.harnessId,
|
|
492
|
+
"gen_ai.response.model": evt.model,
|
|
493
|
+
...usageAttrs(evt.usage)
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
onModelCallStarted(evt, ctx) {
|
|
497
|
+
const run = this.runs.get(evt.runId);
|
|
498
|
+
if (!run) return;
|
|
499
|
+
const span = {
|
|
500
|
+
spanId: hashHex(`${run.agent.traceId}:model_call:${evt.callId}`, 16),
|
|
501
|
+
traceId: run.agent.traceId,
|
|
502
|
+
parentSpanId: run.agent.spanId,
|
|
503
|
+
name: "model_call",
|
|
469
504
|
startMs: Date.now(),
|
|
470
505
|
endMs: void 0,
|
|
471
|
-
|
|
472
|
-
|
|
506
|
+
attrs: {
|
|
507
|
+
...latitudeAttrs(ctx),
|
|
508
|
+
"openclaw.run.id": evt.runId,
|
|
509
|
+
"openclaw.call.id": evt.callId,
|
|
510
|
+
"gen_ai.system": evt.provider,
|
|
511
|
+
"openclaw.provider": evt.provider,
|
|
512
|
+
"gen_ai.request.model": evt.model,
|
|
513
|
+
"openclaw.api": evt.api,
|
|
514
|
+
"openclaw.transport": evt.transport,
|
|
515
|
+
"gen_ai.input.messages:gated": [...run.history]
|
|
516
|
+
}
|
|
473
517
|
};
|
|
474
|
-
run.
|
|
475
|
-
|
|
518
|
+
run.openModelCalls.set(evt.callId, span);
|
|
519
|
+
}
|
|
520
|
+
onModelCallEnded(evt, _ctx) {
|
|
521
|
+
const run = this.runs.get(evt.runId);
|
|
522
|
+
if (!run) return;
|
|
523
|
+
const span = run.openModelCalls.get(evt.callId);
|
|
524
|
+
if (!span) return;
|
|
525
|
+
span.endMs = Date.now();
|
|
526
|
+
span.outcome = evt.outcome === "completed" ? "ok" : "error";
|
|
527
|
+
span.errorMessage = evt.errorCategory;
|
|
528
|
+
Object.assign(span.attrs, {
|
|
529
|
+
"openclaw.duration_ms": evt.durationMs,
|
|
530
|
+
"openclaw.outcome": evt.outcome,
|
|
531
|
+
"openclaw.error.category": evt.errorCategory,
|
|
532
|
+
"openclaw.failure.kind": evt.failureKind,
|
|
533
|
+
"openclaw.request.payload_bytes": evt.requestPayloadBytes,
|
|
534
|
+
"openclaw.response.stream_bytes": evt.responseStreamBytes,
|
|
535
|
+
"openclaw.ttfb_ms": evt.timeToFirstByteMs,
|
|
536
|
+
"openclaw.upstream.request_id_hash": evt.upstreamRequestIdHash
|
|
537
|
+
});
|
|
538
|
+
run.openModelCalls.delete(evt.callId);
|
|
539
|
+
run.closed.push(span);
|
|
476
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* Open a `tool_call` span as a sibling of the agent span. Also append a
|
|
543
|
+
* synthetic assistant `tool_call` part to the rolling history so the NEXT
|
|
544
|
+
* model_call's input snapshot reflects what the model emitted.
|
|
545
|
+
*
|
|
546
|
+
* IMPORTANT: this runs as a `runModifyingHook` in OpenClaw — returning
|
|
547
|
+
* anything other than `undefined`/falsy from this handler blocks the tool.
|
|
548
|
+
* The plugin-side handler enforces a void return; this method's signature
|
|
549
|
+
* already returns `void`.
|
|
550
|
+
*/
|
|
477
551
|
onBeforeToolCall(evt, ctx) {
|
|
478
552
|
if (!evt.runId) return;
|
|
479
|
-
const run = this.runs.get(evt.runId)
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
553
|
+
const run = this.runs.get(evt.runId);
|
|
554
|
+
if (!run) return;
|
|
555
|
+
const toolCallId = evt.toolCallId ?? `${evt.toolName}:${randomUUID()}`;
|
|
556
|
+
const span = {
|
|
557
|
+
spanId: hashHex(`${run.agent.traceId}:tool_call:${toolCallId}`, 16),
|
|
558
|
+
traceId: run.agent.traceId,
|
|
559
|
+
parentSpanId: run.agent.spanId,
|
|
560
|
+
name: `tool_call:${evt.toolName}`,
|
|
486
561
|
startMs: Date.now(),
|
|
487
562
|
endMs: void 0,
|
|
488
|
-
|
|
489
|
-
|
|
563
|
+
attrs: {
|
|
564
|
+
...latitudeAttrs(ctx),
|
|
565
|
+
"openclaw.run.id": evt.runId,
|
|
566
|
+
"gen_ai.tool.name": evt.toolName,
|
|
567
|
+
"gen_ai.tool.call.id": toolCallId,
|
|
568
|
+
"gen_ai.tool.call.arguments:gated": evt.params
|
|
569
|
+
}
|
|
490
570
|
};
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
571
|
+
run.openToolCalls.set(toolCallId, span);
|
|
572
|
+
run.history.push({
|
|
573
|
+
role: "assistant",
|
|
574
|
+
parts: [{
|
|
575
|
+
type: "tool_call",
|
|
576
|
+
id: toolCallId,
|
|
577
|
+
name: evt.toolName,
|
|
578
|
+
arguments: evt.params
|
|
579
|
+
}]
|
|
580
|
+
});
|
|
494
581
|
}
|
|
495
582
|
onAfterToolCall(evt, _ctx) {
|
|
496
583
|
if (!evt.runId) return;
|
|
497
584
|
const run = this.runs.get(evt.runId);
|
|
498
585
|
if (!run) return;
|
|
499
|
-
|
|
500
|
-
if (!
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
586
|
+
let resolvedId = evt.toolCallId && run.openToolCalls.has(evt.toolCallId) ? evt.toolCallId : void 0;
|
|
587
|
+
if (!resolvedId) resolvedId = this.findOpenToolCallByName(run, evt.toolName);
|
|
588
|
+
if (!resolvedId) return;
|
|
589
|
+
const span = run.openToolCalls.get(resolvedId);
|
|
590
|
+
if (!span) return;
|
|
591
|
+
const toolCallId = resolvedId;
|
|
592
|
+
span.endMs = Date.now();
|
|
593
|
+
span.outcome = Boolean(evt.error) ? "error" : "ok";
|
|
594
|
+
span.errorMessage = evt.error;
|
|
595
|
+
Object.assign(span.attrs, {
|
|
596
|
+
"gen_ai.tool.call.result:gated": evt.result,
|
|
597
|
+
"openclaw.error.message:gated": evt.error,
|
|
598
|
+
"openclaw.duration_ms": evt.durationMs
|
|
599
|
+
});
|
|
600
|
+
run.openToolCalls.delete(toolCallId);
|
|
601
|
+
run.closed.push(span);
|
|
602
|
+
run.history.push({
|
|
603
|
+
role: "tool",
|
|
604
|
+
parts: [{
|
|
605
|
+
type: "tool_call_response",
|
|
606
|
+
id: toolCallId,
|
|
607
|
+
response: evt.result ?? evt.error ?? ""
|
|
608
|
+
}]
|
|
609
|
+
});
|
|
505
610
|
}
|
|
506
|
-
|
|
507
|
-
const
|
|
508
|
-
if (!
|
|
509
|
-
const
|
|
510
|
-
if (!
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
611
|
+
onBeforeCompaction(evt, ctx) {
|
|
612
|
+
const runId = ctx.runId;
|
|
613
|
+
if (!runId) return;
|
|
614
|
+
const run = this.runs.get(runId);
|
|
615
|
+
if (!run) return;
|
|
616
|
+
run.openCompaction = {
|
|
617
|
+
spanId: hashHex(`${run.agent.traceId}:compaction:${run.closed.length}`, 16),
|
|
618
|
+
traceId: run.agent.traceId,
|
|
619
|
+
parentSpanId: run.agent.spanId,
|
|
620
|
+
name: "compaction",
|
|
621
|
+
startMs: Date.now(),
|
|
622
|
+
endMs: void 0,
|
|
623
|
+
attrs: {
|
|
624
|
+
...latitudeAttrs(ctx),
|
|
625
|
+
"openclaw.run.id": runId,
|
|
626
|
+
"openclaw.compaction.message_count.before": evt.messageCount,
|
|
627
|
+
"openclaw.compaction.session_file": evt.sessionFile,
|
|
628
|
+
"before_compaction.messages:gated": evt.messages ? normalizeMessages(evt.messages) : void 0
|
|
629
|
+
}
|
|
630
|
+
};
|
|
518
631
|
}
|
|
632
|
+
onAfterCompaction(evt, ctx) {
|
|
633
|
+
const runId = ctx.runId;
|
|
634
|
+
if (!runId) return;
|
|
635
|
+
const run = this.runs.get(runId);
|
|
636
|
+
if (!run?.openCompaction) return;
|
|
637
|
+
const span = run.openCompaction;
|
|
638
|
+
span.endMs = Date.now();
|
|
639
|
+
span.outcome = "ok";
|
|
640
|
+
Object.assign(span.attrs, {
|
|
641
|
+
"openclaw.compaction.message_count.after": evt.messageCount,
|
|
642
|
+
"openclaw.compaction.compacted_count": evt.compactedCount,
|
|
643
|
+
"openclaw.compaction.token_count": evt.tokenCount
|
|
644
|
+
});
|
|
645
|
+
run.openCompaction = void 0;
|
|
646
|
+
run.closed.push(span);
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Open a `subagent` span on the parent run AND register the cross-run link
|
|
650
|
+
* so the child's `before_agent_start` can find us.
|
|
651
|
+
*/
|
|
652
|
+
onSubagentSpawned(evt, ctx) {
|
|
653
|
+
const parentRunId = ctx.runId;
|
|
654
|
+
if (!parentRunId) return;
|
|
655
|
+
const parent = this.runs.get(parentRunId);
|
|
656
|
+
if (!parent) return;
|
|
657
|
+
const span = {
|
|
658
|
+
spanId: hashHex(`${parent.agent.traceId}:subagent:${evt.runId}`, 16),
|
|
659
|
+
traceId: parent.agent.traceId,
|
|
660
|
+
parentSpanId: parent.agent.spanId,
|
|
661
|
+
name: "subagent",
|
|
662
|
+
startMs: Date.now(),
|
|
663
|
+
endMs: void 0,
|
|
664
|
+
attrs: {
|
|
665
|
+
...latitudeAttrs(ctx),
|
|
666
|
+
"openclaw.parent.run.id": parentRunId,
|
|
667
|
+
"openclaw.run.id": evt.runId,
|
|
668
|
+
"openclaw.subagent.child_session_key": evt.childSessionKey,
|
|
669
|
+
"openclaw.subagent.agent_id": evt.agentId,
|
|
670
|
+
"openclaw.subagent.label": evt.label,
|
|
671
|
+
"openclaw.subagent.mode": evt.mode,
|
|
672
|
+
"openclaw.subagent.thread_requested": evt.threadRequested,
|
|
673
|
+
"openclaw.subagent.requester.channel": evt.requester?.channel,
|
|
674
|
+
"openclaw.subagent.requester.account_id": evt.requester?.accountId,
|
|
675
|
+
"openclaw.subagent.requester.to": evt.requester?.to,
|
|
676
|
+
"openclaw.subagent.requester.thread_id": typeof evt.requester?.threadId === "string" || typeof evt.requester?.threadId === "number" ? String(evt.requester.threadId) : void 0
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
parent.childSubagentSpans.set(evt.runId, span);
|
|
680
|
+
this.evictStaleSubagentLinks();
|
|
681
|
+
this.subagentLinks.set(evt.runId, {
|
|
682
|
+
traceId: parent.agent.traceId,
|
|
683
|
+
subagentSpanId: span.spanId,
|
|
684
|
+
createdAt: Date.now()
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
onSubagentEnded(evt, ctx) {
|
|
688
|
+
const parentRunId = ctx.runId;
|
|
689
|
+
if (!parentRunId) return;
|
|
690
|
+
const parent = this.runs.get(parentRunId);
|
|
691
|
+
if (!parent) return;
|
|
692
|
+
const childRunId = evt.runId;
|
|
693
|
+
if (!childRunId) return;
|
|
694
|
+
const span = parent.childSubagentSpans.get(childRunId);
|
|
695
|
+
if (!span) return;
|
|
696
|
+
span.endMs = Date.now();
|
|
697
|
+
span.outcome = evt.outcome === "error" || Boolean(evt.error) ? "error" : "ok";
|
|
698
|
+
span.errorMessage = evt.error;
|
|
699
|
+
Object.assign(span.attrs, {
|
|
700
|
+
"openclaw.subagent.target_session_key": evt.targetSessionKey,
|
|
701
|
+
"openclaw.subagent.target_kind": evt.targetKind,
|
|
702
|
+
"openclaw.subagent.reason": evt.reason,
|
|
703
|
+
"openclaw.subagent.outcome": evt.outcome,
|
|
704
|
+
"openclaw.subagent.send_farewell": evt.sendFarewell,
|
|
705
|
+
"openclaw.subagent.account_id": evt.accountId
|
|
706
|
+
});
|
|
707
|
+
parent.childSubagentSpans.delete(childRunId);
|
|
708
|
+
parent.closed.push(span);
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Close out the run: finish the agent span, abandon any still-open
|
|
712
|
+
* model_calls / tool_calls / compactions, and return everything ready to
|
|
713
|
+
* emit. Removes the subagent link if this was a child run.
|
|
714
|
+
*/
|
|
519
715
|
onAgentEnd(evt, ctx) {
|
|
520
716
|
const runId = ctx.runId;
|
|
521
717
|
if (!runId) return void 0;
|
|
522
718
|
const run = this.runs.get(runId);
|
|
523
|
-
if (!run)
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
719
|
+
if (!run) {
|
|
720
|
+
this.subagentLinks.delete(runId);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const now = Date.now();
|
|
724
|
+
run.agent.endMs = now;
|
|
725
|
+
run.agent.outcome = evt.success ? "ok" : "error";
|
|
726
|
+
run.agent.errorMessage = evt.error;
|
|
727
|
+
Object.assign(run.agent.attrs, {
|
|
728
|
+
"openclaw.duration_ms": evt.durationMs,
|
|
729
|
+
"openclaw.run.success": evt.success,
|
|
730
|
+
"openclaw.error.message:gated": evt.error,
|
|
731
|
+
"agent_end.messages:gated": normalizeMessages(evt.messages)
|
|
732
|
+
});
|
|
733
|
+
for (const span of run.openModelCalls.values()) {
|
|
734
|
+
span.endMs = now;
|
|
735
|
+
span.outcome = "error";
|
|
736
|
+
span.attrs["openclaw.outcome"] = "abandoned";
|
|
737
|
+
run.closed.push(span);
|
|
738
|
+
}
|
|
739
|
+
for (const span of run.openToolCalls.values()) {
|
|
740
|
+
span.endMs = now;
|
|
741
|
+
span.outcome = "error";
|
|
742
|
+
span.attrs["openclaw.outcome"] = "abandoned";
|
|
743
|
+
run.closed.push(span);
|
|
533
744
|
}
|
|
534
|
-
|
|
745
|
+
if (run.openCompaction) {
|
|
746
|
+
run.openCompaction.endMs = now;
|
|
747
|
+
run.openCompaction.outcome = "error";
|
|
748
|
+
run.openCompaction.attrs["openclaw.outcome"] = "abandoned";
|
|
749
|
+
run.closed.push(run.openCompaction);
|
|
750
|
+
}
|
|
751
|
+
for (const span of run.childSubagentSpans.values()) {
|
|
752
|
+
span.endMs = now;
|
|
753
|
+
span.outcome = "error";
|
|
754
|
+
span.attrs["openclaw.subagent.outcome"] = "abandoned";
|
|
755
|
+
run.closed.push(span);
|
|
756
|
+
}
|
|
757
|
+
const spans = [run.agent, ...run.closed];
|
|
535
758
|
this.runs.delete(runId);
|
|
536
|
-
|
|
759
|
+
this.subagentLinks.delete(runId);
|
|
760
|
+
return {
|
|
761
|
+
runId,
|
|
762
|
+
spans
|
|
763
|
+
};
|
|
537
764
|
}
|
|
538
765
|
/** Drop a run without emitting — used on errors from the emit path. */
|
|
539
766
|
abandon(runId) {
|
|
540
767
|
this.runs.delete(runId);
|
|
768
|
+
this.subagentLinks.delete(runId);
|
|
541
769
|
}
|
|
542
|
-
/**
|
|
543
|
-
|
|
544
|
-
return this.
|
|
770
|
+
/** Test-only: how many cross-run subagent links we're holding. */
|
|
771
|
+
subagentLinkCount() {
|
|
772
|
+
return this.subagentLinks.size;
|
|
545
773
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
llmCalls: [],
|
|
565
|
-
orphanTools: []
|
|
566
|
-
};
|
|
567
|
-
this.runs.set(runId, run);
|
|
568
|
-
return run;
|
|
569
|
-
}
|
|
570
|
-
currentOpenCall(run) {
|
|
571
|
-
for (let i = run.llmCalls.length - 1; i >= 0; i--) {
|
|
572
|
-
const call = run.llmCalls[i];
|
|
573
|
-
if (call && call.endMs === void 0) return call;
|
|
774
|
+
/**
|
|
775
|
+
* Drop any subagent links whose child run never reached `agent_end`. Called
|
|
776
|
+
* before every `subagent_spawned` insert so the map stays bounded even when
|
|
777
|
+
* children crash mid-spawn or the plugin reloads.
|
|
778
|
+
*
|
|
779
|
+
* Two passes: TTL eviction (anything older than `SUBAGENT_LINK_TTL_MS`),
|
|
780
|
+
* then a hard size cap (when we're past `SUBAGENT_LINK_MAX`, drop the
|
|
781
|
+
* oldest until we're under).
|
|
782
|
+
*/
|
|
783
|
+
evictStaleSubagentLinks() {
|
|
784
|
+
const now = Date.now();
|
|
785
|
+
for (const [runId, link] of this.subagentLinks) if (now - link.createdAt > SUBAGENT_LINK_TTL_MS) this.subagentLinks.delete(runId);
|
|
786
|
+
if (this.subagentLinks.size <= SUBAGENT_LINK_MAX) return;
|
|
787
|
+
const sorted = Array.from(this.subagentLinks.entries()).sort((a, b) => a[1].createdAt - b[1].createdAt);
|
|
788
|
+
const toRemove = this.subagentLinks.size - SUBAGENT_LINK_MAX;
|
|
789
|
+
for (let i = 0; i < toRemove; i++) {
|
|
790
|
+
const entry = sorted[i];
|
|
791
|
+
if (entry) this.subagentLinks.delete(entry[0]);
|
|
574
792
|
}
|
|
575
793
|
}
|
|
576
|
-
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
for (
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const t = call.toolCalls[j];
|
|
585
|
-
if (t && t.toolName === toolName && t.endMs === void 0) return t;
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
for (let i = run.orphanTools.length - 1; i >= 0; i--) {
|
|
589
|
-
const t = run.orphanTools[i];
|
|
590
|
-
if (t && t.toolName === toolName && t.endMs === void 0) return t;
|
|
794
|
+
findOpenToolCallByName(run, toolName) {
|
|
795
|
+
const target = `tool_call:${toolName}`;
|
|
796
|
+
const entries = Array.from(run.openToolCalls.entries());
|
|
797
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
798
|
+
const entry = entries[i];
|
|
799
|
+
if (!entry) continue;
|
|
800
|
+
const [id, span] = entry;
|
|
801
|
+
if (span.name === target) return id;
|
|
591
802
|
}
|
|
592
803
|
}
|
|
593
804
|
};
|
|
805
|
+
function flattenCtx(ctx) {
|
|
806
|
+
return {
|
|
807
|
+
"openclaw.run.id": ctx.runId,
|
|
808
|
+
"openclaw.session.id": ctx.sessionId,
|
|
809
|
+
"openclaw.session.key": ctx.sessionKey,
|
|
810
|
+
"openclaw.agent.id": ctx.agentId,
|
|
811
|
+
"openclaw.agent.name": ctx.agentId,
|
|
812
|
+
"openclaw.workspace.dir": ctx.workspaceDir,
|
|
813
|
+
"openclaw.message.provider": ctx.messageProvider,
|
|
814
|
+
"openclaw.trigger": ctx.trigger,
|
|
815
|
+
"openclaw.channel.id": ctx.channelId,
|
|
816
|
+
"openclaw.cron.job.id": ctx.jobId,
|
|
817
|
+
"openclaw.model.provider.id": ctx.modelProviderId,
|
|
818
|
+
"openclaw.model.id": ctx.modelId
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Build `latitude.tags` and `latitude.metadata` attrs from the hook context.
|
|
823
|
+
* The OTLP encoder JSON-stringifies arrays/objects, which is the encoding
|
|
824
|
+
* Latitude's resolver expects:
|
|
825
|
+
*
|
|
826
|
+
* - `latitude.tags` is a JSON-encoded string array (`fromJsonStringArray`
|
|
827
|
+
* in domain/spans/src/otlp/resolvers/enrichment.ts).
|
|
828
|
+
* - `latitude.metadata` is a JSON-encoded string object (`fromJsonString`).
|
|
829
|
+
*
|
|
830
|
+
* Tags = the agent id, the channel id, and the trigger. When trigger is
|
|
831
|
+
* `cron`, the tag becomes `cron:<jobId>` so dashboards can pivot on the
|
|
832
|
+
* specific cron job. Each tag is conditionally included so absent ctx
|
|
833
|
+
* fields don't produce empty entries.
|
|
834
|
+
*
|
|
835
|
+
* Metadata = every ctx field that's set, namespaced under `openclaw.*` so
|
|
836
|
+
* it can't collide with metadata keys other plugins might emit.
|
|
837
|
+
*/
|
|
838
|
+
function latitudeAttrs(ctx) {
|
|
839
|
+
const tags = [];
|
|
840
|
+
if (ctx.agentId) tags.push(ctx.agentId);
|
|
841
|
+
if (ctx.channelId) tags.push(ctx.channelId);
|
|
842
|
+
if (ctx.trigger) tags.push(ctx.trigger === "cron" && ctx.jobId ? `cron:${ctx.jobId}` : ctx.trigger);
|
|
843
|
+
const metadata = {};
|
|
844
|
+
if (ctx.runId) metadata["openclaw.run.id"] = ctx.runId;
|
|
845
|
+
if (ctx.sessionId) metadata["openclaw.session.id"] = ctx.sessionId;
|
|
846
|
+
if (ctx.sessionKey) metadata["openclaw.session.key"] = ctx.sessionKey;
|
|
847
|
+
if (ctx.agentId) metadata["openclaw.agent.id"] = ctx.agentId;
|
|
848
|
+
if (ctx.workspaceDir) metadata["openclaw.workspace.dir"] = ctx.workspaceDir;
|
|
849
|
+
if (ctx.channelId) metadata["openclaw.channel.id"] = ctx.channelId;
|
|
850
|
+
if (ctx.messageProvider) metadata["openclaw.message.provider"] = ctx.messageProvider;
|
|
851
|
+
if (ctx.trigger) metadata["openclaw.trigger"] = ctx.trigger;
|
|
852
|
+
if (ctx.jobId) metadata["openclaw.cron.job.id"] = ctx.jobId;
|
|
853
|
+
if (ctx.modelProviderId) metadata["openclaw.model.provider.id"] = ctx.modelProviderId;
|
|
854
|
+
if (ctx.modelId) metadata["openclaw.model.id"] = ctx.modelId;
|
|
855
|
+
return {
|
|
856
|
+
"latitude.tags": tags.length > 0 ? tags : void 0,
|
|
857
|
+
"latitude.metadata": Object.keys(metadata).length > 0 ? metadata : void 0
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
function usageAttrs(usage) {
|
|
861
|
+
if (!usage) return {};
|
|
862
|
+
return {
|
|
863
|
+
"gen_ai.usage.input_tokens": usage.input,
|
|
864
|
+
"gen_ai.usage.output_tokens": usage.output,
|
|
865
|
+
"gen_ai.usage.cache_read_input_tokens": usage.cacheRead,
|
|
866
|
+
"gen_ai.usage.cache_creation_input_tokens": usage.cacheWrite,
|
|
867
|
+
"gen_ai.usage.total_tokens": usage.total
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
function hashHex(input, length) {
|
|
871
|
+
return createHash("sha256").update(input).digest("hex").slice(0, length);
|
|
872
|
+
}
|
|
594
873
|
//#endregion
|
|
595
874
|
//#region src/plugin.ts
|
|
596
875
|
/**
|
|
597
876
|
* Register the Latitude plugin against an OpenClaw plugin API. OpenClaw calls
|
|
598
|
-
* this once at plugin activation; we wire up
|
|
599
|
-
*
|
|
877
|
+
* this once at plugin activation; we wire up the granular paired hooks
|
|
878
|
+
* (model_call_started/_ended, before_/after_tool_call, before_/after_compaction,
|
|
879
|
+
* subagent_spawned/_ended, before_agent_start/agent_end) plus the
|
|
880
|
+
* data-only feeds (llm_input/llm_output) that enrich the agent span.
|
|
600
881
|
*
|
|
601
|
-
* Every
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
882
|
+
* Every typed hook on OpenClaw's side fires fire-and-forget for non-modifying
|
|
883
|
+
* hooks; before_tool_call is a `runModifyingHook` where returning anything
|
|
884
|
+
* other than undefined blocks the tool call. Our handler returns nothing —
|
|
885
|
+
* keep it that way.
|
|
605
886
|
*/
|
|
606
887
|
function registerLatitudePlugin(api, opts = {}) {
|
|
607
888
|
const config = opts.config ?? loadConfig(api.pluginConfig);
|
|
@@ -612,58 +893,65 @@ function registerLatitudePlugin(api, opts = {}) {
|
|
|
612
893
|
return;
|
|
613
894
|
}
|
|
614
895
|
logger.debug(`enabled: project=${config.project} base=${config.baseUrl} allowConversationAccess=${config.allowConversationAccess}`);
|
|
615
|
-
const builder = new
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
} catch (err) {
|
|
623
|
-
logger.warn(`llm_input handler failed: ${String(err)}`);
|
|
624
|
-
}
|
|
625
|
-
});
|
|
626
|
-
api.on("before_tool_call", (evt, ctx) => {
|
|
627
|
-
try {
|
|
628
|
-
builder.onBeforeToolCall(evt, ctx);
|
|
629
|
-
} catch (err) {
|
|
630
|
-
logger.warn(`before_tool_call handler failed: ${String(err)}`);
|
|
631
|
-
}
|
|
632
|
-
});
|
|
633
|
-
api.on("after_tool_call", (evt, ctx) => {
|
|
634
|
-
try {
|
|
635
|
-
builder.onAfterToolCall(evt, ctx);
|
|
636
|
-
} catch (err) {
|
|
637
|
-
logger.warn(`after_tool_call handler failed: ${String(err)}`);
|
|
638
|
-
}
|
|
639
|
-
});
|
|
640
|
-
api.on("llm_output", (evt, ctx) => {
|
|
641
|
-
try {
|
|
642
|
-
builder.onLlmOutput(evt, ctx);
|
|
643
|
-
} catch (err) {
|
|
644
|
-
logger.warn(`llm_output handler failed: ${String(err)}`);
|
|
645
|
-
}
|
|
646
|
-
});
|
|
647
|
-
api.on("agent_end", (evt, ctx) => {
|
|
648
|
-
try {
|
|
649
|
-
const run = builder.onAgentEnd(evt, ctx);
|
|
650
|
-
if (!run) {
|
|
651
|
-
logger.debug("agent_end fired without a matching run in flight");
|
|
652
|
-
return;
|
|
896
|
+
const builder = new SpanBuilder();
|
|
897
|
+
const wrap = (name, fn) => {
|
|
898
|
+
return (evt, ctx) => {
|
|
899
|
+
try {
|
|
900
|
+
fn(evt, ctx);
|
|
901
|
+
} catch (err) {
|
|
902
|
+
logger.warn(`${name} handler failed: ${String(err)}`);
|
|
653
903
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
904
|
+
};
|
|
905
|
+
};
|
|
906
|
+
api.on("before_agent_start", wrap("before_agent_start", (evt, ctx) => {
|
|
907
|
+
builder.onBeforeAgentStart(evt, ctx);
|
|
908
|
+
}));
|
|
909
|
+
api.on("model_call_started", wrap("model_call_started", (evt, ctx) => {
|
|
910
|
+
builder.onModelCallStarted(evt, ctx);
|
|
911
|
+
}));
|
|
912
|
+
api.on("model_call_ended", wrap("model_call_ended", (evt, ctx) => {
|
|
913
|
+
builder.onModelCallEnded(evt, ctx);
|
|
914
|
+
}));
|
|
915
|
+
api.on("before_tool_call", wrap("before_tool_call", (evt, ctx) => {
|
|
916
|
+
builder.onBeforeToolCall(evt, ctx);
|
|
917
|
+
}));
|
|
918
|
+
api.on("after_tool_call", wrap("after_tool_call", (evt, ctx) => {
|
|
919
|
+
builder.onAfterToolCall(evt, ctx);
|
|
920
|
+
}));
|
|
921
|
+
api.on("before_compaction", wrap("before_compaction", (evt, ctx) => {
|
|
922
|
+
builder.onBeforeCompaction(evt, ctx);
|
|
923
|
+
}));
|
|
924
|
+
api.on("after_compaction", wrap("after_compaction", (evt, ctx) => {
|
|
925
|
+
builder.onAfterCompaction(evt, ctx);
|
|
926
|
+
}));
|
|
927
|
+
api.on("subagent_spawned", wrap("subagent_spawned", (evt, ctx) => {
|
|
928
|
+
builder.onSubagentSpawned(evt, ctx);
|
|
929
|
+
}));
|
|
930
|
+
api.on("subagent_ended", wrap("subagent_ended", (evt, ctx) => {
|
|
931
|
+
builder.onSubagentEnded(evt, ctx);
|
|
932
|
+
}));
|
|
933
|
+
api.on("llm_input", wrap("llm_input", (evt, ctx) => {
|
|
934
|
+
builder.onLlmInput(evt, ctx);
|
|
935
|
+
}));
|
|
936
|
+
api.on("llm_output", wrap("llm_output", (evt, ctx) => {
|
|
937
|
+
builder.onLlmOutput(evt, ctx);
|
|
938
|
+
}));
|
|
939
|
+
api.on("agent_end", wrap("agent_end", (evt, ctx) => {
|
|
940
|
+
const result = builder.onAgentEnd(evt, ctx);
|
|
941
|
+
if (!result) {
|
|
942
|
+
logger.debug("agent_end fired without a matching run in flight");
|
|
943
|
+
return;
|
|
665
944
|
}
|
|
666
|
-
|
|
945
|
+
opts.onEmit?.(result);
|
|
946
|
+
const payload = buildOtlpRequest(result, { allowConversationAccess: config.allowConversationAccess });
|
|
947
|
+
postTraces({
|
|
948
|
+
baseUrl: config.baseUrl,
|
|
949
|
+
apiKey: config.apiKey,
|
|
950
|
+
project: config.project,
|
|
951
|
+
payload,
|
|
952
|
+
logger
|
|
953
|
+
});
|
|
954
|
+
}));
|
|
667
955
|
}
|
|
668
956
|
//#endregion
|
|
669
957
|
export { registerLatitudePlugin as default };
|