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