@latitude-data/openclaw-telemetry 0.0.4 → 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 +40 -14
- package/dist/plugin.d.ts +55 -65
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +728 -460
- package/dist/plugin.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -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`;
|
|
@@ -77,27 +77,10 @@ function createLogger(debugEnabled) {
|
|
|
77
77
|
//#endregion
|
|
78
78
|
//#region src/otlp.ts
|
|
79
79
|
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
80
|
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
81
|
/** Build an OTLP export request for a single completed agent run. */
|
|
99
|
-
function buildOtlpRequest(
|
|
100
|
-
const spans =
|
|
82
|
+
function buildOtlpRequest(result, options) {
|
|
83
|
+
const spans = result.spans.map((span) => toOtlpSpan(span, options));
|
|
101
84
|
return { resourceSpans: [{
|
|
102
85
|
resource: { attributes: resourceAttrs() },
|
|
103
86
|
scopeSpans: [{
|
|
@@ -109,210 +92,158 @@ function buildOtlpRequest(run, options) {
|
|
|
109
92
|
}]
|
|
110
93
|
}] };
|
|
111
94
|
}
|
|
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);
|
|
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;
|
|
135
109
|
return {
|
|
136
|
-
traceId,
|
|
137
|
-
spanId,
|
|
138
|
-
parentSpanId:
|
|
139
|
-
name:
|
|
110
|
+
traceId: span.traceId,
|
|
111
|
+
spanId: span.spanId,
|
|
112
|
+
parentSpanId: span.parentSpanId,
|
|
113
|
+
name: span.name,
|
|
140
114
|
kind: 1,
|
|
141
115
|
startTimeUnixNano: startNs,
|
|
142
116
|
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 }
|
|
117
|
+
attributes: attrs,
|
|
118
|
+
status: { code: statusCode }
|
|
172
119
|
};
|
|
173
120
|
}
|
|
174
|
-
function
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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) {
|
|
184
142
|
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 }
|
|
143
|
+
key,
|
|
144
|
+
value: { stringValue: value }
|
|
230
145
|
};
|
|
231
146
|
}
|
|
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;
|
|
147
|
+
function int(key, value) {
|
|
237
148
|
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 }
|
|
149
|
+
key,
|
|
150
|
+
value: { intValue: String(Math.trunc(value)) }
|
|
265
151
|
};
|
|
266
152
|
}
|
|
267
|
-
function
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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 "";
|
|
168
|
+
}
|
|
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";
|
|
272
184
|
}
|
|
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
185
|
}
|
|
186
|
+
//#endregion
|
|
187
|
+
//#region src/messages.ts
|
|
282
188
|
const ALLOWED_ROLES = new Set([
|
|
283
189
|
"system",
|
|
284
190
|
"user",
|
|
285
191
|
"assistant",
|
|
286
192
|
"tool"
|
|
287
193
|
]);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
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) {
|
|
293
199
|
if (!raw || typeof raw !== "object") return void 0;
|
|
294
200
|
const obj = raw;
|
|
295
|
-
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
|
+
}
|
|
296
213
|
const content = obj.content ?? obj.text ?? obj.message;
|
|
297
|
-
if (
|
|
214
|
+
if (role === "tool" && obj.tool_call_id !== void 0) return {
|
|
298
215
|
role,
|
|
299
216
|
parts: [{
|
|
300
|
-
type: "
|
|
301
|
-
|
|
217
|
+
type: "tool_call_response",
|
|
218
|
+
id: typeof obj.tool_call_id === "string" ? obj.tool_call_id : "",
|
|
219
|
+
response: content ?? safeJson(obj)
|
|
302
220
|
}]
|
|
303
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
|
+
}
|
|
304
233
|
if (Array.isArray(content)) {
|
|
305
234
|
const parts = [];
|
|
306
235
|
for (const block of content) {
|
|
307
|
-
const part =
|
|
236
|
+
const part = normalizeBlock(block);
|
|
308
237
|
if (part) parts.push(part);
|
|
309
238
|
}
|
|
239
|
+
appendToolCalls(parts, obj.tool_calls);
|
|
240
|
+
if (parts.length === 0) parts.push({
|
|
241
|
+
type: "text",
|
|
242
|
+
content: safeJson(content)
|
|
243
|
+
});
|
|
310
244
|
return {
|
|
311
245
|
role,
|
|
312
|
-
parts
|
|
313
|
-
type: "text",
|
|
314
|
-
content: JSON.stringify(content)
|
|
315
|
-
}]
|
|
246
|
+
parts
|
|
316
247
|
};
|
|
317
248
|
}
|
|
318
249
|
return {
|
|
@@ -323,13 +254,76 @@ function normalizeHistoryMessage(raw) {
|
|
|
323
254
|
}]
|
|
324
255
|
};
|
|
325
256
|
}
|
|
326
|
-
|
|
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) {
|
|
327
315
|
if (typeof raw === "string") return {
|
|
328
316
|
type: "text",
|
|
329
317
|
content: raw
|
|
330
318
|
};
|
|
331
319
|
if (!raw || typeof raw !== "object") return void 0;
|
|
332
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
|
+
}
|
|
333
327
|
const type = typeof obj.type === "string" ? obj.type : "text";
|
|
334
328
|
if (type === "text" && typeof obj.text === "string") return {
|
|
335
329
|
type: "text",
|
|
@@ -341,104 +335,66 @@ function normalizeContentBlock(raw) {
|
|
|
341
335
|
name: typeof obj.name === "string" ? obj.name : "",
|
|
342
336
|
arguments: obj.input ?? {}
|
|
343
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
|
+
};
|
|
344
344
|
if (type === "tool_result") return {
|
|
345
345
|
type: "tool_call_response",
|
|
346
346
|
id: typeof obj.tool_use_id === "string" ? obj.tool_use_id : "",
|
|
347
347
|
response: obj.content ?? ""
|
|
348
348
|
};
|
|
349
|
-
if (type === "
|
|
350
|
-
type: "
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
};
|
|
354
|
-
return {
|
|
355
|
-
type,
|
|
356
|
-
content: safeJson(raw)
|
|
349
|
+
if (type === "tool_call_response") return {
|
|
350
|
+
type: "tool_call_response",
|
|
351
|
+
id: typeof obj.id === "string" ? obj.id : "",
|
|
352
|
+
response: obj.response ?? ""
|
|
357
353
|
};
|
|
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
|
|
354
|
+
if (type === "thinking" && typeof obj.thinking === "string") return {
|
|
355
|
+
type: "reasoning",
|
|
356
|
+
content: obj.thinking
|
|
387
357
|
};
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
358
|
+
if (type === "reasoning" && typeof obj.content === "string") return {
|
|
359
|
+
type: "reasoning",
|
|
360
|
+
content: obj.content
|
|
391
361
|
};
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
+
};
|
|
399
370
|
}
|
|
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
371
|
return {
|
|
426
|
-
|
|
427
|
-
|
|
372
|
+
type,
|
|
373
|
+
content: safeJson(raw)
|
|
428
374
|
};
|
|
429
375
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
+
}
|
|
442
398
|
}
|
|
443
399
|
function safeJson(value) {
|
|
444
400
|
try {
|
|
@@ -449,179 +405,484 @@ function safeJson(value) {
|
|
|
449
405
|
}
|
|
450
406
|
}
|
|
451
407
|
//#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 {
|
|
408
|
+
//#region src/span-builder.ts
|
|
409
|
+
const SUBAGENT_LINK_TTL_MS = 3600 * 1e3;
|
|
410
|
+
const SUBAGENT_LINK_MAX = 1e3;
|
|
411
|
+
var SpanBuilder = class {
|
|
469
412
|
runs = /* @__PURE__ */ new Map();
|
|
470
|
-
|
|
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
|
+
*/
|
|
471
464
|
onLlmInput(evt, ctx) {
|
|
472
|
-
const run = this.
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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",
|
|
489
504
|
startMs: Date.now(),
|
|
490
505
|
endMs: void 0,
|
|
491
|
-
|
|
492
|
-
|
|
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
|
+
}
|
|
493
517
|
};
|
|
494
|
-
run.
|
|
495
|
-
|
|
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);
|
|
496
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
|
+
*/
|
|
497
551
|
onBeforeToolCall(evt, ctx) {
|
|
498
552
|
if (!evt.runId) return;
|
|
499
|
-
const run = this.runs.get(evt.runId)
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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}`,
|
|
506
561
|
startMs: Date.now(),
|
|
507
562
|
endMs: void 0,
|
|
508
|
-
|
|
509
|
-
|
|
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
|
+
}
|
|
510
570
|
};
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
+
});
|
|
514
581
|
}
|
|
515
582
|
onAfterToolCall(evt, _ctx) {
|
|
516
583
|
if (!evt.runId) return;
|
|
517
584
|
const run = this.runs.get(evt.runId);
|
|
518
585
|
if (!run) return;
|
|
519
|
-
|
|
520
|
-
if (!
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
+
});
|
|
610
|
+
}
|
|
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
|
+
};
|
|
525
631
|
}
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
if (!
|
|
529
|
-
const
|
|
530
|
-
if (!
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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);
|
|
538
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
|
+
*/
|
|
539
715
|
onAgentEnd(evt, ctx) {
|
|
540
716
|
const runId = ctx.runId;
|
|
541
717
|
if (!runId) return void 0;
|
|
542
718
|
const run = this.runs.get(runId);
|
|
543
|
-
if (!run)
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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);
|
|
744
|
+
}
|
|
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);
|
|
553
750
|
}
|
|
554
|
-
for (const
|
|
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];
|
|
555
758
|
this.runs.delete(runId);
|
|
556
|
-
|
|
759
|
+
this.subagentLinks.delete(runId);
|
|
760
|
+
return {
|
|
761
|
+
runId,
|
|
762
|
+
spans
|
|
763
|
+
};
|
|
557
764
|
}
|
|
558
765
|
/** Drop a run without emitting — used on errors from the emit path. */
|
|
559
766
|
abandon(runId) {
|
|
560
767
|
this.runs.delete(runId);
|
|
768
|
+
this.subagentLinks.delete(runId);
|
|
561
769
|
}
|
|
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;
|
|
770
|
+
/** Test-only: how many cross-run subagent links we're holding. */
|
|
771
|
+
subagentLinkCount() {
|
|
772
|
+
return this.subagentLinks.size;
|
|
589
773
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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]);
|
|
594
792
|
}
|
|
595
793
|
}
|
|
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;
|
|
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;
|
|
611
802
|
}
|
|
612
803
|
}
|
|
613
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
|
+
}
|
|
614
873
|
//#endregion
|
|
615
874
|
//#region src/plugin.ts
|
|
616
875
|
/**
|
|
617
876
|
* Register the Latitude plugin against an OpenClaw plugin API. OpenClaw calls
|
|
618
|
-
* this once at plugin activation; we wire up
|
|
619
|
-
*
|
|
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.
|
|
620
881
|
*
|
|
621
|
-
* Every
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
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.
|
|
625
886
|
*/
|
|
626
887
|
function registerLatitudePlugin(api, opts = {}) {
|
|
627
888
|
const config = opts.config ?? loadConfig(api.pluginConfig);
|
|
@@ -632,58 +893,65 @@ function registerLatitudePlugin(api, opts = {}) {
|
|
|
632
893
|
return;
|
|
633
894
|
}
|
|
634
895
|
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;
|
|
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)}`);
|
|
673
903
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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;
|
|
685
944
|
}
|
|
686
|
-
|
|
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
|
+
}));
|
|
687
955
|
}
|
|
688
956
|
//#endregion
|
|
689
957
|
export { registerLatitudePlugin as default };
|