@latitude-data/openclaw-telemetry 0.0.9 → 0.1.0
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 +78 -218
- package/dist/plugin.d.ts +145 -61
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +2078 -601
- package/dist/plugin.js.map +1 -1
- package/openclaw.plugin.json +58 -3
- package/package.json +3 -3
package/dist/plugin.js
CHANGED
|
@@ -1,34 +1,8 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { arch, hostname, platform, release } from "node:os";
|
|
2
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const url = `${baseUrl.replace(/\/+$/, "")}/v1/traces`;
|
|
6
|
-
const bodyText = JSON.stringify(payload);
|
|
7
|
-
logger.debug(`POST ${url} (project=${project}, ${bodyText.length} bytes)`);
|
|
8
|
-
const controller = new AbortController();
|
|
9
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
10
|
-
try {
|
|
11
|
-
const res = await fetch(url, {
|
|
12
|
-
method: "POST",
|
|
13
|
-
headers: {
|
|
14
|
-
"Content-Type": "application/json",
|
|
15
|
-
Authorization: `Bearer ${apiKey}`,
|
|
16
|
-
"X-Latitude-Project": project
|
|
17
|
-
},
|
|
18
|
-
body: bodyText,
|
|
19
|
-
signal: controller.signal
|
|
20
|
-
});
|
|
21
|
-
if (!res.ok) {
|
|
22
|
-
const text = await res.text().catch(() => "");
|
|
23
|
-
logger.warn(`ingest HTTP ${res.status}: ${text.slice(0, 500)}`);
|
|
24
|
-
} else logger.debug(`ingest HTTP ${res.status}`);
|
|
25
|
-
} catch (err) {
|
|
26
|
-
logger.warn(`ingest failed: ${String(err)}`);
|
|
27
|
-
} finally {
|
|
28
|
-
clearTimeout(timer);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
//#endregion
|
|
4
|
+
import { readFileSync, statSync } from "node:fs";
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
32
6
|
//#region src/redaction.ts
|
|
33
7
|
function parseRedactConfig(value) {
|
|
34
8
|
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
@@ -82,24 +56,14 @@ function redactedAttr(key, mask) {
|
|
|
82
56
|
//#endregion
|
|
83
57
|
//#region src/config.ts
|
|
84
58
|
const DEFAULT_BASE_URL = "https://ingest.latitude.so";
|
|
59
|
+
const DEFAULT_SERVICE_NAME = "openclaw";
|
|
60
|
+
const DEFAULT_MAX_CONTENT_CHARS = 262144;
|
|
85
61
|
/**
|
|
86
62
|
* Build a `Config` from OpenClaw's per-plugin config bucket. The plugin SDK
|
|
87
63
|
* passes `api.pluginConfig` (the user's `plugins.entries[id].config` block)
|
|
88
|
-
* to the registration function
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* were missing from pluginConfig. That fallback is gone deliberately:
|
|
92
|
-
* OpenClaw 2026.4.25's `openclaw plugins install` runs a static-analysis
|
|
93
|
-
* security scan that flags any runtime source combining environment-variable
|
|
94
|
-
* access with a network-send call (we have `fetch(` in postTraces). With
|
|
95
|
-
* the fallback our bundled runtime tripped the scanner. The installer
|
|
96
|
-
* writes credentials to `plugins.entries[id].config` anyway, so the
|
|
97
|
-
* fallback was polish-not-feature — its removal also gives a cleaner
|
|
98
|
-
* privacy story (the runtime can't pick up credentials the operator
|
|
99
|
-
* didn't put in openclaw.json).
|
|
100
|
-
*
|
|
101
|
-
* For dev-time testing with debug logs, set `config.debug = true` in
|
|
102
|
-
* openclaw.json directly.
|
|
64
|
+
* to the registration function; that is the only source. There is no
|
|
65
|
+
* environment fallback: the runtime must not combine env reads with the
|
|
66
|
+
* network send, and the installer always writes credentials here.
|
|
103
67
|
*/
|
|
104
68
|
function loadConfig(pluginConfig = void 0) {
|
|
105
69
|
const fromOpts = pluginConfig ?? {};
|
|
@@ -117,7 +81,14 @@ function loadConfig(pluginConfig = void 0) {
|
|
|
117
81
|
debug,
|
|
118
82
|
allowConversationAccess,
|
|
119
83
|
redact: parseRedactConfig(fromOpts.redact),
|
|
120
|
-
enabled: hasCreds && !explicitlyDisabled
|
|
84
|
+
enabled: hasCreds && !explicitlyDisabled,
|
|
85
|
+
serviceName: pickString(fromOpts.serviceName) ?? DEFAULT_SERVICE_NAME,
|
|
86
|
+
tags: pickStringList(fromOpts.tags),
|
|
87
|
+
metadata: pickStringMap(fromOpts.metadata),
|
|
88
|
+
memory: pickBool(fromOpts.memory) ?? true,
|
|
89
|
+
memoryContent: pickBool(fromOpts.memoryContent) ?? true,
|
|
90
|
+
toolDefinitions: pickBool(fromOpts.toolDefinitions) ?? true,
|
|
91
|
+
maxContentChars: pickPositiveInt(fromOpts.maxContentChars) ?? DEFAULT_MAX_CONTENT_CHARS
|
|
121
92
|
};
|
|
122
93
|
}
|
|
123
94
|
function pickString(value) {
|
|
@@ -126,24 +97,44 @@ function pickString(value) {
|
|
|
126
97
|
function pickBool(value) {
|
|
127
98
|
return typeof value === "boolean" ? value : void 0;
|
|
128
99
|
}
|
|
100
|
+
function pickPositiveInt(value) {
|
|
101
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
|
|
102
|
+
}
|
|
103
|
+
function pickStringList(value) {
|
|
104
|
+
if (typeof value === "string") return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
105
|
+
if (!Array.isArray(value)) return [];
|
|
106
|
+
return value.filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim());
|
|
107
|
+
}
|
|
108
|
+
function pickStringMap(value) {
|
|
109
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
110
|
+
const out = {};
|
|
111
|
+
for (const [k, v] of Object.entries(value)) if (typeof v === "string") out[k] = v;
|
|
112
|
+
else if (typeof v === "number" || typeof v === "boolean") out[k] = String(v);
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
129
115
|
//#endregion
|
|
130
116
|
//#region src/logger.ts
|
|
131
117
|
const PREFIX = "[latitude-openclaw]";
|
|
132
|
-
function createLogger(debugEnabled) {
|
|
118
|
+
function createLogger(debugEnabled, host) {
|
|
133
119
|
return {
|
|
134
|
-
debug: debugEnabled ? (msg) =>
|
|
135
|
-
warn: (msg) =>
|
|
120
|
+
debug: debugEnabled ? host ? (msg) => host.info(`${PREFIX} ${msg}`) : (msg) => writeStderr(msg) : () => {},
|
|
121
|
+
warn: host ? (msg) => host.warn(`${PREFIX} ${msg}`) : (msg) => writeStderr(msg)
|
|
136
122
|
};
|
|
137
123
|
}
|
|
124
|
+
function writeStderr(msg) {
|
|
125
|
+
process.stderr.write(`${PREFIX} ${msg}\n`);
|
|
126
|
+
}
|
|
138
127
|
//#endregion
|
|
139
128
|
//#region src/otlp.ts
|
|
140
129
|
const SCOPE_NAME = "@latitude-data/openclaw-telemetry";
|
|
141
|
-
const SCOPE_VERSION = "0.0
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
130
|
+
const SCOPE_VERSION = "0.1.0";
|
|
131
|
+
const GATED_SUFFIX = ":gated";
|
|
132
|
+
const ARRAY_VALUE_KEYS = new Set(["gen_ai.response.finish_reasons"]);
|
|
133
|
+
const TRUNCATION_MARKER = "\n…[truncated by latitude-openclaw]…\n";
|
|
134
|
+
function buildOtlpRequest(results, options) {
|
|
135
|
+
const spans = results.flatMap((result) => result.spans.map((span) => toOtlpSpan(span, options)));
|
|
145
136
|
return { resourceSpans: [{
|
|
146
|
-
resource: { attributes: resourceAttrs() },
|
|
137
|
+
resource: { attributes: resourceAttrs(options.serviceName ?? "openclaw") },
|
|
147
138
|
scopeSpans: [{
|
|
148
139
|
scope: {
|
|
149
140
|
name: SCOPE_NAME,
|
|
@@ -154,53 +145,128 @@ function buildOtlpRequest(result, options) {
|
|
|
154
145
|
}] };
|
|
155
146
|
}
|
|
156
147
|
function toOtlpSpan(span, options) {
|
|
157
|
-
const startNs = msToNs(span.startMs);
|
|
158
|
-
const endNs = msToNs(span.endMs ?? span.startMs);
|
|
159
148
|
const attrs = [];
|
|
160
149
|
for (const [rawKey, value] of Object.entries(span.attrs)) {
|
|
161
150
|
if (value === void 0 || value === null) continue;
|
|
162
|
-
const isGated = rawKey.endsWith(
|
|
151
|
+
const isGated = rawKey.endsWith(GATED_SUFFIX);
|
|
163
152
|
if (isGated && !options.allowConversationAccess) continue;
|
|
164
|
-
const kv = encodeAttr(isGated ? rawKey.slice(0, -6) : rawKey, value);
|
|
153
|
+
const kv = encodeAttr(isGated ? rawKey.slice(0, -6) : rawKey, value, options.maxContentChars);
|
|
165
154
|
if (kv !== void 0) attrs.push(kv);
|
|
166
155
|
}
|
|
167
156
|
attrs.push(bool("latitude.captured.content", options.allowConversationAccess));
|
|
168
|
-
if (span.endMs !== void 0) attrs.push(int("openclaw.duration_ms.computed", Math.max(0, span.endMs - span.startMs)));
|
|
169
|
-
const redactedAttrs = redactAttributes(attrs, options.redact);
|
|
170
|
-
const statusCode = span.outcome === "error" ? 2 : 1;
|
|
171
157
|
return {
|
|
172
158
|
traceId: span.traceId,
|
|
173
159
|
spanId: span.spanId,
|
|
174
160
|
parentSpanId: span.parentSpanId,
|
|
175
161
|
name: span.name,
|
|
176
|
-
kind:
|
|
177
|
-
startTimeUnixNano:
|
|
178
|
-
endTimeUnixNano:
|
|
179
|
-
attributes:
|
|
180
|
-
status: { code:
|
|
162
|
+
kind: span.kind,
|
|
163
|
+
startTimeUnixNano: msToNs(span.startMs),
|
|
164
|
+
endTimeUnixNano: msToNs(span.endMs ?? span.startMs),
|
|
165
|
+
attributes: redactAttributes(attrs, options.redact),
|
|
166
|
+
status: { code: span.outcome === "error" ? 2 : 1 }
|
|
181
167
|
};
|
|
182
168
|
}
|
|
183
|
-
function encodeAttr(key, value) {
|
|
169
|
+
function encodeAttr(key, value, maxChars) {
|
|
184
170
|
if (value === void 0 || value === null) return void 0;
|
|
185
|
-
if (typeof value === "string") return str(key, value);
|
|
171
|
+
if (typeof value === "string") return str$1(key, budget(value, maxChars));
|
|
186
172
|
if (typeof value === "boolean") return bool(key, value);
|
|
187
173
|
if (typeof value === "number") return Number.isInteger(value) ? int(key, value) : {
|
|
188
174
|
key,
|
|
189
175
|
value: { doubleValue: value }
|
|
190
176
|
};
|
|
191
|
-
|
|
177
|
+
if (ARRAY_VALUE_KEYS.has(key) && Array.isArray(value)) return {
|
|
178
|
+
key,
|
|
179
|
+
value: { arrayValue: { values: value.map((v) => ({ stringValue: String(v) })) } }
|
|
180
|
+
};
|
|
181
|
+
return str$1(key, budgetJson(value, maxChars));
|
|
182
|
+
}
|
|
183
|
+
function budget(value, maxChars) {
|
|
184
|
+
if (!maxChars || value.length <= maxChars) return value;
|
|
185
|
+
const keep = Math.max(0, Math.floor((maxChars - 36) / 2));
|
|
186
|
+
let headEnd = keep;
|
|
187
|
+
if (headEnd > 0 && isHighSurrogate(value.charCodeAt(headEnd - 1))) headEnd--;
|
|
188
|
+
let tailStart = value.length - keep;
|
|
189
|
+
if (tailStart < value.length && isLowSurrogate(value.charCodeAt(tailStart))) tailStart++;
|
|
190
|
+
return `${value.slice(0, headEnd)}${TRUNCATION_MARKER}${value.slice(tailStart)}`;
|
|
191
|
+
}
|
|
192
|
+
function isHighSurrogate(code) {
|
|
193
|
+
return code >= 55296 && code <= 56319;
|
|
194
|
+
}
|
|
195
|
+
function isLowSurrogate(code) {
|
|
196
|
+
return code >= 56320 && code <= 57343;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* A structured value must still parse after the budget, since Latitude reads
|
|
200
|
+
* messages, tool definitions and memory records as JSON: long strings inside
|
|
201
|
+
* it are truncated first, then whole items are shed from the middle of an
|
|
202
|
+
* array. Slicing the serialized text is the last resort, for a lone object.
|
|
203
|
+
*/
|
|
204
|
+
function budgetJson(value, maxChars) {
|
|
205
|
+
const json = safeJson$1(value);
|
|
206
|
+
if (!maxChars || json.length <= maxChars) return json;
|
|
207
|
+
const trimmed = budgetStrings(value, Math.max(1, Math.floor(maxChars / 4)));
|
|
208
|
+
const trimmedJson = safeJson$1(trimmed);
|
|
209
|
+
if (trimmedJson.length <= maxChars) return trimmedJson;
|
|
210
|
+
if (Array.isArray(trimmed)) return safeJson$1(shedItems(trimmed, maxChars));
|
|
211
|
+
return budget(trimmedJson, maxChars);
|
|
212
|
+
}
|
|
213
|
+
function budgetStrings(value, maxChars) {
|
|
214
|
+
if (typeof value === "string") return budget(value, maxChars);
|
|
215
|
+
if (Array.isArray(value)) return value.map((item) => budgetStrings(item, maxChars));
|
|
216
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, budgetStrings(v, maxChars)]));
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
const OMISSION_RESERVE = 160;
|
|
220
|
+
/** Keeps the head and tail of an array within the budget; a message list gets a marker for what was dropped. */
|
|
221
|
+
function shedItems(items, maxChars) {
|
|
222
|
+
const sizes = items.map((item) => safeJson$1(item).length + 1);
|
|
223
|
+
const head = [];
|
|
224
|
+
const tail = [];
|
|
225
|
+
let budgetLeft = maxChars - OMISSION_RESERVE;
|
|
226
|
+
let low = 0;
|
|
227
|
+
let high = items.length - 1;
|
|
228
|
+
while (low <= high) {
|
|
229
|
+
const fromHead = head.length <= tail.length;
|
|
230
|
+
const size = fromHead ? sizes[low] : sizes[high];
|
|
231
|
+
if (budgetLeft - size < 0) break;
|
|
232
|
+
budgetLeft -= size;
|
|
233
|
+
if (fromHead) head.push(items[low++]);
|
|
234
|
+
else tail.unshift(items[high--]);
|
|
235
|
+
}
|
|
236
|
+
const omitted = items.length - head.length - tail.length;
|
|
237
|
+
if (omitted <= 0) return [...items];
|
|
238
|
+
const marker = isMessageList(items) ? [omissionMessage(omitted)] : [];
|
|
239
|
+
return [
|
|
240
|
+
...head,
|
|
241
|
+
...marker,
|
|
242
|
+
...tail
|
|
243
|
+
];
|
|
244
|
+
}
|
|
245
|
+
function isMessageList(items) {
|
|
246
|
+
const first = items[0];
|
|
247
|
+
return !!first && typeof first === "object" && typeof first.role === "string";
|
|
248
|
+
}
|
|
249
|
+
function omissionMessage(count) {
|
|
250
|
+
return {
|
|
251
|
+
role: "system",
|
|
252
|
+
parts: [{
|
|
253
|
+
type: "text",
|
|
254
|
+
content: `[… ${count} message(s) omitted by latitude-openclaw …]`
|
|
255
|
+
}]
|
|
256
|
+
};
|
|
192
257
|
}
|
|
193
|
-
function resourceAttrs() {
|
|
258
|
+
function resourceAttrs(serviceName) {
|
|
194
259
|
return [
|
|
195
|
-
str("service.name",
|
|
196
|
-
str("service.version", SCOPE_VERSION),
|
|
197
|
-
str("
|
|
198
|
-
str("host.
|
|
199
|
-
str("
|
|
200
|
-
str("os.
|
|
260
|
+
str$1("service.name", serviceName),
|
|
261
|
+
str$1("service.version", SCOPE_VERSION),
|
|
262
|
+
str$1("telemetry.sdk.name", SCOPE_NAME),
|
|
263
|
+
str$1("host.name", hostname()),
|
|
264
|
+
str$1("host.arch", arch()),
|
|
265
|
+
str$1("os.type", platform()),
|
|
266
|
+
str$1("os.version", release())
|
|
201
267
|
];
|
|
202
268
|
}
|
|
203
|
-
function str(key, value) {
|
|
269
|
+
function str$1(key, value) {
|
|
204
270
|
return {
|
|
205
271
|
key,
|
|
206
272
|
value: { stringValue: value }
|
|
@@ -230,6 +296,113 @@ function safeJson$1(value) {
|
|
|
230
296
|
}
|
|
231
297
|
}
|
|
232
298
|
//#endregion
|
|
299
|
+
//#region src/context.ts
|
|
300
|
+
const TAG_MAX_CHARS = 64;
|
|
301
|
+
const METADATA_VALUE_MAX_CHARS = 1024;
|
|
302
|
+
const METADATA_MAX_KEYS = 64;
|
|
303
|
+
/**
|
|
304
|
+
* Derived tags: `openclaw`, the channel, the agent id, `cron:<job>` (or the
|
|
305
|
+
* bare trigger for other non-user triggers) and `subagent:<agent>` on a run
|
|
306
|
+
* that spawned one. Operator tags are appended. Tags are what `queryAnalytics`
|
|
307
|
+
* can break down on, so they stay low-cardinality; ids go in metadata.
|
|
308
|
+
*/
|
|
309
|
+
function deriveEnrichment(run, operator) {
|
|
310
|
+
const { ctx } = run;
|
|
311
|
+
const tags = ["openclaw"];
|
|
312
|
+
if (ctx.channel) tags.push(ctx.channel);
|
|
313
|
+
if (ctx.agentId) tags.push(ctx.agentId);
|
|
314
|
+
if (ctx.trigger === "cron") tags.push(run.cron ? `cron:${run.cron.id}` : "cron");
|
|
315
|
+
else if (ctx.trigger && ctx.trigger !== "user") tags.push(ctx.trigger);
|
|
316
|
+
for (const id of run.subagentIds) tags.push(`subagent:${id}`);
|
|
317
|
+
for (const tag of operator.tags) tags.push(tag);
|
|
318
|
+
const metadata = {};
|
|
319
|
+
for (const [k, v] of Object.entries(operator.metadata)) {
|
|
320
|
+
if (k.startsWith("openclaw.")) continue;
|
|
321
|
+
metadata[k] = v;
|
|
322
|
+
}
|
|
323
|
+
const derived = {
|
|
324
|
+
"openclaw.run.id": ctx.runId,
|
|
325
|
+
"openclaw.session.id": ctx.sessionId,
|
|
326
|
+
"openclaw.session.key": ctx.sessionKey,
|
|
327
|
+
"openclaw.agent.id": ctx.agentId,
|
|
328
|
+
"openclaw.workspace.dir": ctx.workspaceDir,
|
|
329
|
+
"openclaw.channel": ctx.channel,
|
|
330
|
+
"openclaw.channel.id": ctx.channelId,
|
|
331
|
+
"openclaw.account.id": ctx.accountId,
|
|
332
|
+
"openclaw.message.provider": ctx.messageProvider,
|
|
333
|
+
"openclaw.trigger": ctx.trigger,
|
|
334
|
+
"openclaw.model.provider.id": ctx.modelProviderId,
|
|
335
|
+
"openclaw.model.id": ctx.modelId,
|
|
336
|
+
"openclaw.cron.job.id": run.cron?.id,
|
|
337
|
+
"openclaw.cron.job.name": run.cron?.name,
|
|
338
|
+
"openclaw.sender.id": run.sender?.id,
|
|
339
|
+
"openclaw.sender.name": run.sender?.name,
|
|
340
|
+
"openclaw.sender.username": run.sender?.username,
|
|
341
|
+
"openclaw.trace.id": ctx.trace?.traceId,
|
|
342
|
+
"openclaw.plugin.version": run.pluginVersion
|
|
343
|
+
};
|
|
344
|
+
for (const [k, v] of Object.entries(derived)) if (v !== void 0 && v !== "") metadata[k] = v;
|
|
345
|
+
return {
|
|
346
|
+
tags: capTags(tags),
|
|
347
|
+
metadata: capMetadata(metadata)
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function capTags(tags) {
|
|
351
|
+
const seen = /* @__PURE__ */ new Set();
|
|
352
|
+
const out = [];
|
|
353
|
+
for (const tag of tags) {
|
|
354
|
+
const trimmed = tag.trim();
|
|
355
|
+
if (trimmed.length === 0 || trimmed.length > TAG_MAX_CHARS || seen.has(trimmed)) continue;
|
|
356
|
+
seen.add(trimmed);
|
|
357
|
+
out.push(trimmed);
|
|
358
|
+
if (out.length >= 32) break;
|
|
359
|
+
}
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
function capMetadata(metadata) {
|
|
363
|
+
const out = {};
|
|
364
|
+
let count = 0;
|
|
365
|
+
for (const [k, v] of Object.entries(metadata)) {
|
|
366
|
+
if (v.length > METADATA_VALUE_MAX_CHARS) continue;
|
|
367
|
+
out[k] = v;
|
|
368
|
+
count++;
|
|
369
|
+
if (count >= METADATA_MAX_KEYS) break;
|
|
370
|
+
}
|
|
371
|
+
return out;
|
|
372
|
+
}
|
|
373
|
+
const CTX_MARKER = "⟦openclaw:ctx⟧";
|
|
374
|
+
/**
|
|
375
|
+
* OpenClaw prefixes channel prompts with a fenced JSON block after
|
|
376
|
+
* `⟦openclaw:ctx⟧` that carries the sender's id and display name; the only
|
|
377
|
+
* place the name reaches a run when `message_received` did not.
|
|
378
|
+
*/
|
|
379
|
+
function senderFromPrompt(prompt) {
|
|
380
|
+
if (!prompt) return void 0;
|
|
381
|
+
const marker = prompt.indexOf(CTX_MARKER);
|
|
382
|
+
if (marker < 0) return void 0;
|
|
383
|
+
const fenceStart = prompt.indexOf("```json", marker);
|
|
384
|
+
if (fenceStart < 0) return void 0;
|
|
385
|
+
const bodyStart = prompt.indexOf("\n", fenceStart);
|
|
386
|
+
const fenceEnd = prompt.indexOf("```", bodyStart + 1);
|
|
387
|
+
if (bodyStart < 0 || fenceEnd < 0) return void 0;
|
|
388
|
+
try {
|
|
389
|
+
const sender = JSON.parse(prompt.slice(bodyStart + 1, fenceEnd)).sender;
|
|
390
|
+
if (!sender || typeof sender.id !== "string" || sender.id.length === 0) return void 0;
|
|
391
|
+
return {
|
|
392
|
+
id: sender.id,
|
|
393
|
+
name: typeof sender.name === "string" && sender.name.length > 0 ? sender.name : void 0,
|
|
394
|
+
username: typeof sender.username === "string" && sender.username.length > 0 ? sender.username : void 0
|
|
395
|
+
};
|
|
396
|
+
} catch {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/** `agent:<agentId>:cron:<jobId>[:run:<runId>]` is the isolated cron session key shape. */
|
|
401
|
+
function cronJobFromSessionKey(sessionKey) {
|
|
402
|
+
if (!sessionKey) return void 0;
|
|
403
|
+
return /^agent:[^:]+:cron:([^:]+)/.exec(sessionKey)?.[1];
|
|
404
|
+
}
|
|
405
|
+
//#endregion
|
|
233
406
|
//#region src/messages.ts
|
|
234
407
|
const ALLOWED_ROLES = new Set([
|
|
235
408
|
"system",
|
|
@@ -237,26 +410,24 @@ const ALLOWED_ROLES = new Set([
|
|
|
237
410
|
"assistant",
|
|
238
411
|
"tool"
|
|
239
412
|
]);
|
|
240
|
-
/**
|
|
241
|
-
* Normalize a single message of any of the provider shapes we know about.
|
|
242
|
-
* Returns `undefined` for non-objects so the caller can skip them.
|
|
243
|
-
*/
|
|
244
413
|
function normalizeMessage(raw) {
|
|
245
414
|
if (!raw || typeof raw !== "object") return void 0;
|
|
246
415
|
const obj = raw;
|
|
247
|
-
const role = coerceRole(obj.role);
|
|
248
416
|
if (Array.isArray(obj.parts)) {
|
|
249
417
|
const parts = [];
|
|
250
418
|
for (const p of obj.parts) if (p && typeof p === "object") parts.push(p);
|
|
251
419
|
return {
|
|
252
|
-
role,
|
|
420
|
+
role: coerceRole(obj.role),
|
|
253
421
|
parts: parts.length > 0 ? parts : [{
|
|
254
422
|
type: "text",
|
|
255
423
|
content: safeJson(raw)
|
|
256
424
|
}]
|
|
257
425
|
};
|
|
258
426
|
}
|
|
427
|
+
if (obj.role === "toolResult") return normalizeToolResult(obj);
|
|
428
|
+
if (obj.role === "custom") return normalizeCustom(obj);
|
|
259
429
|
const content = obj.content ?? obj.text ?? obj.message;
|
|
430
|
+
const role = isRuntimeContext(obj, content) ? "system" : coerceRole(obj.role);
|
|
260
431
|
if (role === "tool" && obj.tool_call_id !== void 0) return {
|
|
261
432
|
role,
|
|
262
433
|
parts: [{
|
|
@@ -266,11 +437,16 @@ function normalizeMessage(raw) {
|
|
|
266
437
|
}]
|
|
267
438
|
};
|
|
268
439
|
if (typeof content === "string") {
|
|
269
|
-
const parts = [
|
|
440
|
+
const parts = [];
|
|
441
|
+
if (content.length > 0) parts.push({
|
|
270
442
|
type: "text",
|
|
271
443
|
content
|
|
272
|
-
}
|
|
444
|
+
});
|
|
273
445
|
appendToolCalls(parts, obj.tool_calls);
|
|
446
|
+
if (parts.length === 0) parts.push({
|
|
447
|
+
type: "text",
|
|
448
|
+
content: ""
|
|
449
|
+
});
|
|
274
450
|
return {
|
|
275
451
|
role,
|
|
276
452
|
parts
|
|
@@ -300,7 +476,6 @@ function normalizeMessage(raw) {
|
|
|
300
476
|
}]
|
|
301
477
|
};
|
|
302
478
|
}
|
|
303
|
-
/** Normalize an array of provider messages. */
|
|
304
479
|
function normalizeMessages(raw) {
|
|
305
480
|
const out = [];
|
|
306
481
|
for (const m of raw) {
|
|
@@ -309,7 +484,6 @@ function normalizeMessages(raw) {
|
|
|
309
484
|
}
|
|
310
485
|
return out;
|
|
311
486
|
}
|
|
312
|
-
/** Build a single user message from a string prompt. */
|
|
313
487
|
function userMessageFromPrompt(prompt) {
|
|
314
488
|
return {
|
|
315
489
|
role: "user",
|
|
@@ -319,44 +493,73 @@ function userMessageFromPrompt(prompt) {
|
|
|
319
493
|
}]
|
|
320
494
|
};
|
|
321
495
|
}
|
|
322
|
-
/** Build a single assistant message from `assistantTexts` + `lastAssistant` fallback. */
|
|
323
|
-
function assistantMessageFromOutput(assistantTexts, lastAssistant) {
|
|
324
|
-
if (lastAssistant !== void 0) {
|
|
325
|
-
const norm = normalizeMessage(lastAssistant);
|
|
326
|
-
if (norm) return {
|
|
327
|
-
...norm,
|
|
328
|
-
role: "assistant"
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
const parts = [];
|
|
332
|
-
for (const text of assistantTexts) if (text.length > 0) parts.push({
|
|
333
|
-
type: "text",
|
|
334
|
-
content: text
|
|
335
|
-
});
|
|
336
|
-
if (parts.length === 0) parts.push({
|
|
337
|
-
type: "text",
|
|
338
|
-
content: ""
|
|
339
|
-
});
|
|
340
|
-
return {
|
|
341
|
-
role: "assistant",
|
|
342
|
-
parts
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Wrap a system prompt string into the parts-array shape expected for
|
|
347
|
-
* `gen_ai.system_instructions`. Empty string in → single empty text part out
|
|
348
|
-
* (still a valid array, never `undefined`).
|
|
349
|
-
*/
|
|
350
496
|
function systemInstructionsParts(prompt) {
|
|
351
497
|
return [{
|
|
352
498
|
type: "text",
|
|
353
499
|
content: prompt
|
|
354
500
|
}];
|
|
355
501
|
}
|
|
502
|
+
const RUNTIME_CONTEXT_PREFIX = "[openclaw.runtime-context]";
|
|
503
|
+
/**
|
|
504
|
+
* OpenClaw injects its per-turn context as an extra user message right before
|
|
505
|
+
* the prompt. It is instructions rather than the user's words, so it renders
|
|
506
|
+
* as a system message instead of a second user bubble.
|
|
507
|
+
*/
|
|
508
|
+
function isRuntimeContext(obj, content) {
|
|
509
|
+
if (obj.role !== "user") return false;
|
|
510
|
+
if (obj.runtimeContext) return true;
|
|
511
|
+
const text = typeof content === "string" ? content : Array.isArray(content) ? blockText(content[0]) : void 0;
|
|
512
|
+
return typeof text === "string" && text.trimStart().startsWith(RUNTIME_CONTEXT_PREFIX);
|
|
513
|
+
}
|
|
356
514
|
function coerceRole(raw) {
|
|
357
515
|
if (typeof raw !== "string") return "user";
|
|
358
516
|
return ALLOWED_ROLES.has(raw) ? raw : "user";
|
|
359
517
|
}
|
|
518
|
+
function normalizeToolResult(obj) {
|
|
519
|
+
const content = obj.content;
|
|
520
|
+
let response;
|
|
521
|
+
if (typeof content === "string") response = content;
|
|
522
|
+
else if (Array.isArray(content)) {
|
|
523
|
+
const texts = [];
|
|
524
|
+
let hasNonText = false;
|
|
525
|
+
for (const block of content) {
|
|
526
|
+
const text = blockText(block);
|
|
527
|
+
if (text !== void 0) texts.push(text);
|
|
528
|
+
else hasNonText = true;
|
|
529
|
+
}
|
|
530
|
+
response = hasNonText ? content.map(normalizeBlock).filter(Boolean) : texts.join("\n");
|
|
531
|
+
} else response = content ?? "";
|
|
532
|
+
return {
|
|
533
|
+
role: "tool",
|
|
534
|
+
parts: [{
|
|
535
|
+
type: "tool_call_response",
|
|
536
|
+
id: typeof obj.toolCallId === "string" ? obj.toolCallId : "",
|
|
537
|
+
name: typeof obj.toolName === "string" ? obj.toolName : void 0,
|
|
538
|
+
response,
|
|
539
|
+
...obj.isError === true ? { is_error: true } : {}
|
|
540
|
+
}]
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
/** Text of a plain text block, or of the `toolResult` block the Codex harness wraps tool output in. */
|
|
544
|
+
function blockText(block) {
|
|
545
|
+
if (!block || typeof block !== "object") return void 0;
|
|
546
|
+
const b = block;
|
|
547
|
+
if (b.type === "text" && typeof b.text === "string") return b.text;
|
|
548
|
+
if (b.type === "toolResult") {
|
|
549
|
+
if (typeof b.text === "string") return b.text;
|
|
550
|
+
if (typeof b.content === "string") return b.content;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function normalizeCustom(obj) {
|
|
554
|
+
const text = typeof obj.content === "string" ? obj.content : safeJson(obj);
|
|
555
|
+
return {
|
|
556
|
+
role: "user",
|
|
557
|
+
parts: [{
|
|
558
|
+
type: "text",
|
|
559
|
+
content: `[${typeof obj.customType === "string" ? obj.customType : "custom"}] ${text}`
|
|
560
|
+
}]
|
|
561
|
+
};
|
|
562
|
+
}
|
|
360
563
|
function normalizeBlock(raw) {
|
|
361
564
|
if (typeof raw === "string") return {
|
|
362
565
|
type: "text",
|
|
@@ -364,24 +567,22 @@ function normalizeBlock(raw) {
|
|
|
364
567
|
};
|
|
365
568
|
if (!raw || typeof raw !== "object") return void 0;
|
|
366
569
|
const obj = raw;
|
|
367
|
-
|
|
368
|
-
|
|
570
|
+
const type = typeof obj.type === "string" ? obj.type : "text";
|
|
571
|
+
if (type === "text") {
|
|
572
|
+
if (typeof obj.content === "string") return {
|
|
369
573
|
type: "text",
|
|
370
574
|
content: obj.content
|
|
371
575
|
};
|
|
576
|
+
if (typeof obj.text === "string") return {
|
|
577
|
+
type: "text",
|
|
578
|
+
content: obj.text
|
|
579
|
+
};
|
|
372
580
|
}
|
|
373
|
-
|
|
374
|
-
if (type === "text" && typeof obj.text === "string") return {
|
|
581
|
+
if (type === "toolResult") return {
|
|
375
582
|
type: "text",
|
|
376
|
-
content: obj
|
|
377
|
-
};
|
|
378
|
-
if (type === "tool_use") return {
|
|
379
|
-
type: "tool_call",
|
|
380
|
-
id: typeof obj.id === "string" ? obj.id : "",
|
|
381
|
-
name: typeof obj.name === "string" ? obj.name : "",
|
|
382
|
-
arguments: obj.input ?? {}
|
|
583
|
+
content: blockText(obj) ?? safeJson(raw)
|
|
383
584
|
};
|
|
384
|
-
if (type === "tool_call") return {
|
|
585
|
+
if (type === "toolCall" || type === "tool_use" || type === "tool_call") return {
|
|
385
586
|
type: "tool_call",
|
|
386
587
|
id: typeof obj.id === "string" ? obj.id : "",
|
|
387
588
|
name: typeof obj.name === "string" ? obj.name : "",
|
|
@@ -397,33 +598,46 @@ function normalizeBlock(raw) {
|
|
|
397
598
|
id: typeof obj.id === "string" ? obj.id : "",
|
|
398
599
|
response: obj.response ?? ""
|
|
399
600
|
};
|
|
400
|
-
if (type === "thinking"
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
601
|
+
if (type === "thinking") {
|
|
602
|
+
if (typeof obj.thinking === "string" && obj.thinking.length > 0) return {
|
|
603
|
+
type: "reasoning",
|
|
604
|
+
content: obj.thinking
|
|
605
|
+
};
|
|
606
|
+
if (obj.redacted === true) return {
|
|
607
|
+
type: "reasoning",
|
|
608
|
+
content: "[redacted]"
|
|
609
|
+
};
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
404
612
|
if (type === "reasoning" && typeof obj.content === "string") return {
|
|
405
613
|
type: "reasoning",
|
|
406
614
|
content: obj.content
|
|
407
615
|
};
|
|
408
|
-
if (type === "image"
|
|
409
|
-
const
|
|
410
|
-
const uri = src.url ?? (src.data ? `data:${src.media_type ?? "image/unknown"};base64,${src.data}` : "");
|
|
616
|
+
if (type === "image") {
|
|
617
|
+
const uri = imageUri(obj);
|
|
411
618
|
if (uri) return {
|
|
412
619
|
type: "uri",
|
|
413
620
|
modality: "image",
|
|
414
621
|
uri
|
|
415
622
|
};
|
|
623
|
+
return {
|
|
624
|
+
type: "text",
|
|
625
|
+
content: "[image]"
|
|
626
|
+
};
|
|
416
627
|
}
|
|
417
628
|
return {
|
|
418
629
|
type,
|
|
419
630
|
content: safeJson(raw)
|
|
420
631
|
};
|
|
421
632
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
633
|
+
function imageUri(obj) {
|
|
634
|
+
if (typeof obj.data === "string" && obj.data.length > 0) return `data:${typeof obj.mimeType === "string" ? obj.mimeType : "image/unknown"};base64,${obj.data}`;
|
|
635
|
+
if (obj.source && typeof obj.source === "object") {
|
|
636
|
+
const src = obj.source;
|
|
637
|
+
if (src.url) return src.url;
|
|
638
|
+
if (src.data) return `data:${src.media_type ?? "image/unknown"};base64,${src.data}`;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
427
641
|
function appendToolCalls(parts, raw) {
|
|
428
642
|
if (!Array.isArray(raw)) return;
|
|
429
643
|
for (const tc of raw) {
|
|
@@ -451,301 +665,1093 @@ function safeJson(value) {
|
|
|
451
665
|
}
|
|
452
666
|
}
|
|
453
667
|
//#endregion
|
|
454
|
-
//#region src/
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
668
|
+
//#region src/history.ts
|
|
669
|
+
/**
|
|
670
|
+
* Conversation history for harnesses that do not hand it to plugins. The
|
|
671
|
+
* embedded runner passes the whole session on `llm_input.historyMessages` and
|
|
672
|
+
* on `agent_end.messages`; the Codex harness owns the thread itself and passes
|
|
673
|
+
* an empty history and a per-turn transcript. The plugin then rebuilds the
|
|
674
|
+
* session from the turns it has seen, and on a cold start from OpenClaw's own
|
|
675
|
+
* transcript store: the per-agent SQLite database (2026.9+) or the older
|
|
676
|
+
* JSONL file under `<stateDir>/agents/<agent>/sessions/`.
|
|
677
|
+
*/
|
|
678
|
+
const MAX_MESSAGES = 400;
|
|
679
|
+
const MAX_SESSIONS = 200;
|
|
680
|
+
const SESSION_TTL_MS = 1440 * 60 * 1e3;
|
|
681
|
+
const MAX_FILE_BYTES$1 = 8 * 1024 * 1024;
|
|
682
|
+
const CURRENT_PROMPT_WINDOW_MS = 6e4;
|
|
683
|
+
const defaultTranscriptReader = (path) => {
|
|
684
|
+
try {
|
|
685
|
+
if (statSync(path).size > MAX_FILE_BYTES$1) return void 0;
|
|
686
|
+
return readFileSync(path, "utf8");
|
|
687
|
+
} catch {
|
|
688
|
+
return;
|
|
462
689
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
onBeforeAgentStart(evt, ctx) {
|
|
470
|
-
const runId = ctx.runId;
|
|
471
|
-
if (!runId) return;
|
|
472
|
-
if (this.runs.has(runId)) return;
|
|
473
|
-
const link = this.subagentLinks.get(runId);
|
|
474
|
-
const traceId = link?.traceId ?? hashHex(runId, 32);
|
|
475
|
-
const parentSpanId = link?.subagentSpanId ?? "";
|
|
476
|
-
const agent = {
|
|
477
|
-
spanId: hashHex(`${traceId}:${runId}:agent`, 16),
|
|
478
|
-
traceId,
|
|
479
|
-
parentSpanId,
|
|
480
|
-
name: "agent",
|
|
481
|
-
startMs: Date.now(),
|
|
482
|
-
endMs: void 0,
|
|
483
|
-
attrs: {
|
|
484
|
-
...flattenCtx(ctx),
|
|
485
|
-
...latitudeAttrs(ctx),
|
|
486
|
-
...sessionAttrs(ctx),
|
|
487
|
-
"openclaw.run.id": runId,
|
|
488
|
-
"before_agent_start.prompt:gated": evt.prompt,
|
|
489
|
-
"before_agent_start.messages:gated": evt.messages ? normalizeMessages(evt.messages) : void 0
|
|
490
|
-
}
|
|
491
|
-
};
|
|
492
|
-
this.runs.set(runId, {
|
|
493
|
-
agent,
|
|
494
|
-
history: [],
|
|
495
|
-
openModelCalls: /* @__PURE__ */ new Map(),
|
|
496
|
-
openToolCalls: /* @__PURE__ */ new Map(),
|
|
497
|
-
openCompaction: void 0,
|
|
498
|
-
closed: [],
|
|
499
|
-
childSubagentSpans: /* @__PURE__ */ new Map()
|
|
500
|
-
});
|
|
690
|
+
};
|
|
691
|
+
var SessionHistoryStore = class {
|
|
692
|
+
sessions = /* @__PURE__ */ new Map();
|
|
693
|
+
now;
|
|
694
|
+
constructor(now = () => Date.now()) {
|
|
695
|
+
this.now = now;
|
|
501
696
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
* Provider-specific message shapes get normalized into the parts-based
|
|
508
|
-
* GenAI format here — that's the contract Latitude's downstream parser
|
|
509
|
-
* expects on `gen_ai.input.messages` and `gen_ai.system_instructions`.
|
|
510
|
-
*/
|
|
511
|
-
onLlmInput(evt, ctx) {
|
|
512
|
-
const run = this.runs.get(ctx.runId ?? evt.runId);
|
|
513
|
-
if (!run) return;
|
|
514
|
-
const inputMessages = [...normalizeMessages(evt.historyMessages)];
|
|
515
|
-
if (evt.prompt) inputMessages.push(userMessageFromPrompt(evt.prompt));
|
|
516
|
-
Object.assign(run.agent.attrs, {
|
|
517
|
-
"gen_ai.system_instructions:gated": evt.systemPrompt ? systemInstructionsParts(evt.systemPrompt) : void 0,
|
|
518
|
-
"user_prompt:gated": evt.prompt,
|
|
519
|
-
"gen_ai.input.messages:gated": inputMessages,
|
|
520
|
-
"openclaw.images.count": evt.imagesCount,
|
|
521
|
-
"gen_ai.request.model": evt.model,
|
|
522
|
-
"gen_ai.system": evt.provider,
|
|
523
|
-
"openclaw.provider": evt.provider
|
|
524
|
-
});
|
|
525
|
-
run.history = inputMessages;
|
|
697
|
+
get(sessionId) {
|
|
698
|
+
const entry = this.sessions.get(sessionId);
|
|
699
|
+
if (!entry) return void 0;
|
|
700
|
+
entry.updatedAt = this.now();
|
|
701
|
+
return entry.messages;
|
|
526
702
|
}
|
|
527
|
-
/**
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
*/
|
|
531
|
-
onLlmOutput(evt, ctx) {
|
|
532
|
-
const run = this.runs.get(ctx.runId ?? evt.runId);
|
|
533
|
-
if (!run) return;
|
|
534
|
-
const assistantMessage = assistantMessageFromOutput(evt.assistantTexts, evt.lastAssistant);
|
|
535
|
-
Object.assign(run.agent.attrs, {
|
|
536
|
-
"gen_ai.output.messages:gated": [assistantMessage],
|
|
537
|
-
"openclaw.resolved.ref": evt.resolvedRef,
|
|
538
|
-
"openclaw.harness.id": evt.harnessId,
|
|
539
|
-
"gen_ai.response.model": evt.model,
|
|
540
|
-
...usageAttrs(evt.usage)
|
|
541
|
-
});
|
|
703
|
+
/** Replace the session's history with a full transcript the harness supplied. */
|
|
704
|
+
replace(sessionId, messages) {
|
|
705
|
+
this.set(sessionId, messages.slice(-MAX_MESSAGES));
|
|
542
706
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const span = {
|
|
547
|
-
spanId: hashHex(`${run.agent.traceId}:model_call:${evt.callId}`, 16),
|
|
548
|
-
traceId: run.agent.traceId,
|
|
549
|
-
parentSpanId: run.agent.spanId,
|
|
550
|
-
name: "model_call",
|
|
551
|
-
startMs: Date.now(),
|
|
552
|
-
endMs: void 0,
|
|
553
|
-
attrs: {
|
|
554
|
-
...latitudeAttrs(ctx),
|
|
555
|
-
...sessionAttrs(ctx),
|
|
556
|
-
"openclaw.run.id": evt.runId,
|
|
557
|
-
"openclaw.call.id": evt.callId,
|
|
558
|
-
"gen_ai.system": evt.provider,
|
|
559
|
-
"openclaw.provider": evt.provider,
|
|
560
|
-
"gen_ai.request.model": evt.model,
|
|
561
|
-
"openclaw.api": evt.api,
|
|
562
|
-
"openclaw.transport": evt.transport,
|
|
563
|
-
"gen_ai.input.messages:gated": [...run.history]
|
|
564
|
-
}
|
|
565
|
-
};
|
|
566
|
-
run.openModelCalls.set(evt.callId, span);
|
|
707
|
+
/** Extend the session's history with one turn's new messages. */
|
|
708
|
+
append(sessionId, base, turn) {
|
|
709
|
+
this.set(sessionId, [...base, ...turn].slice(-MAX_MESSAGES));
|
|
567
710
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if (!run) return;
|
|
571
|
-
const span = run.openModelCalls.get(evt.callId);
|
|
572
|
-
if (!span) return;
|
|
573
|
-
span.endMs = Date.now();
|
|
574
|
-
span.outcome = evt.outcome === "completed" ? "ok" : "error";
|
|
575
|
-
span.errorMessage = evt.errorCategory;
|
|
576
|
-
Object.assign(span.attrs, {
|
|
577
|
-
"openclaw.duration_ms": evt.durationMs,
|
|
578
|
-
"openclaw.outcome": evt.outcome,
|
|
579
|
-
"openclaw.error.category": evt.errorCategory,
|
|
580
|
-
"openclaw.failure.kind": evt.failureKind,
|
|
581
|
-
"openclaw.request.payload_bytes": evt.requestPayloadBytes,
|
|
582
|
-
"openclaw.response.stream_bytes": evt.responseStreamBytes,
|
|
583
|
-
"openclaw.ttfb_ms": evt.timeToFirstByteMs,
|
|
584
|
-
"openclaw.upstream.request_id_hash": evt.upstreamRequestIdHash
|
|
585
|
-
});
|
|
586
|
-
run.openModelCalls.delete(evt.callId);
|
|
587
|
-
run.closed.push(span);
|
|
711
|
+
forget(sessionId) {
|
|
712
|
+
this.sessions.delete(sessionId);
|
|
588
713
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
* IMPORTANT: this runs as a `runModifyingHook` in OpenClaw — returning
|
|
595
|
-
* anything other than `undefined`/falsy from this handler blocks the tool.
|
|
596
|
-
* The plugin-side handler enforces a void return; this method's signature
|
|
597
|
-
* already returns `void`.
|
|
598
|
-
*/
|
|
599
|
-
onBeforeToolCall(evt, ctx) {
|
|
600
|
-
if (!evt.runId) return;
|
|
601
|
-
const run = this.runs.get(evt.runId);
|
|
602
|
-
if (!run) return;
|
|
603
|
-
const toolCallId = evt.toolCallId ?? `${evt.toolName}:${randomUUID()}`;
|
|
604
|
-
const span = {
|
|
605
|
-
spanId: hashHex(`${run.agent.traceId}:tool_call:${toolCallId}`, 16),
|
|
606
|
-
traceId: run.agent.traceId,
|
|
607
|
-
parentSpanId: run.agent.spanId,
|
|
608
|
-
name: `tool_call:${evt.toolName}`,
|
|
609
|
-
startMs: Date.now(),
|
|
610
|
-
endMs: void 0,
|
|
611
|
-
attrs: {
|
|
612
|
-
...latitudeAttrs(ctx),
|
|
613
|
-
...sessionAttrs(ctx),
|
|
614
|
-
"openclaw.run.id": evt.runId,
|
|
615
|
-
"gen_ai.tool.name": evt.toolName,
|
|
616
|
-
"gen_ai.tool.call.id": toolCallId,
|
|
617
|
-
"gen_ai.tool.call.arguments:gated": evt.params
|
|
618
|
-
}
|
|
619
|
-
};
|
|
620
|
-
run.openToolCalls.set(toolCallId, span);
|
|
621
|
-
run.history.push({
|
|
622
|
-
role: "assistant",
|
|
623
|
-
parts: [{
|
|
624
|
-
type: "tool_call",
|
|
625
|
-
id: toolCallId,
|
|
626
|
-
name: evt.toolName,
|
|
627
|
-
arguments: evt.params
|
|
628
|
-
}]
|
|
714
|
+
set(sessionId, messages) {
|
|
715
|
+
this.evict();
|
|
716
|
+
this.sessions.set(sessionId, {
|
|
717
|
+
messages,
|
|
718
|
+
updatedAt: this.now()
|
|
629
719
|
});
|
|
630
720
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
const
|
|
634
|
-
if (
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
if (!resolvedId) return;
|
|
638
|
-
const span = run.openToolCalls.get(resolvedId);
|
|
639
|
-
if (!span) return;
|
|
640
|
-
const toolCallId = resolvedId;
|
|
641
|
-
span.endMs = Date.now();
|
|
642
|
-
span.outcome = Boolean(evt.error) ? "error" : "ok";
|
|
643
|
-
span.errorMessage = evt.error;
|
|
644
|
-
Object.assign(span.attrs, {
|
|
645
|
-
"gen_ai.tool.call.result:gated": evt.result,
|
|
646
|
-
"openclaw.error.message:gated": evt.error,
|
|
647
|
-
"openclaw.duration_ms": evt.durationMs
|
|
648
|
-
});
|
|
649
|
-
run.openToolCalls.delete(toolCallId);
|
|
650
|
-
run.closed.push(span);
|
|
651
|
-
run.history.push({
|
|
652
|
-
role: "tool",
|
|
653
|
-
parts: [{
|
|
654
|
-
type: "tool_call_response",
|
|
655
|
-
id: toolCallId,
|
|
656
|
-
response: evt.result ?? evt.error ?? ""
|
|
657
|
-
}]
|
|
658
|
-
});
|
|
721
|
+
evict() {
|
|
722
|
+
const now = this.now();
|
|
723
|
+
for (const [id, entry] of this.sessions) if (now - entry.updatedAt > SESSION_TTL_MS) this.sessions.delete(id);
|
|
724
|
+
if (this.sessions.size < MAX_SESSIONS) return;
|
|
725
|
+
const oldest = Array.from(this.sessions.entries()).sort((a, b) => a[1].updatedAt - b[1].updatedAt);
|
|
726
|
+
for (const [id] of oldest.slice(0, this.sessions.size - MAX_SESSIONS + 1)) this.sessions.delete(id);
|
|
659
727
|
}
|
|
660
|
-
|
|
661
|
-
|
|
728
|
+
};
|
|
729
|
+
/** The default workspace is `<stateDir>/workspace`, which is the only structural clue a run carries. */
|
|
730
|
+
function stateDirFromWorkspace(workspaceDir) {
|
|
731
|
+
if (!workspaceDir) return void 0;
|
|
732
|
+
return basename(workspaceDir) === "workspace" ? dirname(workspaceDir) : void 0;
|
|
733
|
+
}
|
|
734
|
+
function sessionTranscriptPath(stateDir, agentId, sessionId) {
|
|
735
|
+
return join(stateDir, "agents", agentId, "sessions", `${sessionId}.jsonl`);
|
|
736
|
+
}
|
|
737
|
+
function agentDatabasePath(stateDir, agentId) {
|
|
738
|
+
return join(stateDir, "agents", agentId, "agent", "openclaw-agent.sqlite");
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Reads through `node:sqlite` (Node 22.13+), loaded lazily so a host without
|
|
742
|
+
* it degrades to "no history" instead of failing to load the plugin. The
|
|
743
|
+
* database is opened read-only; the gateway keeps its own writer open.
|
|
744
|
+
*/
|
|
745
|
+
const defaultTranscriptRowsReader = (dbPath, sessionId) => {
|
|
746
|
+
let db;
|
|
747
|
+
try {
|
|
748
|
+
db = new (createRequire(import.meta.url)("node:sqlite")).DatabaseSync(dbPath, { readOnly: true });
|
|
749
|
+
} catch {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
try {
|
|
753
|
+
const active = db.prepare(`SELECT e.event_json AS event_json
|
|
754
|
+
FROM session_transcript_active_events a
|
|
755
|
+
JOIN transcript_events e ON e.session_id = a.session_id AND e.seq = a.event_seq
|
|
756
|
+
WHERE a.session_id = ?
|
|
757
|
+
ORDER BY a.active_position`).all(sessionId);
|
|
758
|
+
return (active.length > 0 ? active : db.prepare("SELECT event_json FROM transcript_events WHERE session_id = ? ORDER BY seq").all(sessionId)).map((row) => row && typeof row === "object" ? row.event_json : void 0).filter((v) => typeof v === "string");
|
|
759
|
+
} catch {
|
|
760
|
+
return;
|
|
761
|
+
} finally {
|
|
762
|
+
try {
|
|
763
|
+
db.close();
|
|
764
|
+
} catch {}
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
/**
|
|
768
|
+
* The latest compaction entry of a session. `after_compaction` reports only
|
|
769
|
+
* counts; the summary that replaced the compacted messages lives in the
|
|
770
|
+
* transcript store, written before the hook fires.
|
|
771
|
+
*/
|
|
772
|
+
function readLatestCompaction(dbPath, sessionId, readRows = defaultTranscriptRowsReader) {
|
|
773
|
+
const rows = readRows(dbPath, sessionId);
|
|
774
|
+
return rows ? latestCompactionFromEntries(rows) : void 0;
|
|
775
|
+
}
|
|
776
|
+
function readLatestCompactionFromFile(path, read = defaultTranscriptReader) {
|
|
777
|
+
const raw = read(path);
|
|
778
|
+
return raw === void 0 ? void 0 : latestCompactionFromEntries(raw.split("\n"));
|
|
779
|
+
}
|
|
780
|
+
function latestCompactionFromEntries(lines) {
|
|
781
|
+
let latest;
|
|
782
|
+
for (const line of lines) {
|
|
783
|
+
if (!line.includes("\"compaction\"")) continue;
|
|
784
|
+
let entry;
|
|
785
|
+
try {
|
|
786
|
+
entry = JSON.parse(line);
|
|
787
|
+
} catch {
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
if (!entry || entry.type !== "compaction" || typeof entry.summary !== "string") continue;
|
|
791
|
+
latest = {
|
|
792
|
+
summary: entry.summary,
|
|
793
|
+
tokensBefore: typeof entry.tokensBefore === "number" ? entry.tokensBefore : void 0,
|
|
794
|
+
tokensAfter: typeof entry.tokensAfter === "number" ? entry.tokensAfter : void 0
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
return latest;
|
|
798
|
+
}
|
|
799
|
+
/** The session's visible conversation from the agent database, oldest first. */
|
|
800
|
+
function readSessionTranscriptFromDatabase(dbPath, sessionId, readRows = defaultTranscriptRowsReader) {
|
|
801
|
+
const rows = readRows(dbPath, sessionId);
|
|
802
|
+
if (!rows) return void 0;
|
|
803
|
+
return messagesFromEntries(rows);
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* The current prompt is usually persisted before `llm_input` fires, so the
|
|
807
|
+
* stored transcript ends with it. Channels wrap the prompt in an envelope
|
|
808
|
+
* (conversation info, system lines) before the model sees it, so the stored
|
|
809
|
+
* text is matched by containment; a very recent trailing user message with no
|
|
810
|
+
* text match is dropped as well.
|
|
811
|
+
*/
|
|
812
|
+
function withoutCurrentPrompt(stored, prompt, runStartMs) {
|
|
813
|
+
const kept = [...stored];
|
|
814
|
+
for (let i = 0; i < 3 && kept.length > 0; i++) {
|
|
815
|
+
const last = kept[kept.length - 1];
|
|
816
|
+
if (last.message.role !== "user") break;
|
|
817
|
+
const text = last.message.parts.map((p) => typeof p.content === "string" ? p.content : "").join("\n").trim();
|
|
818
|
+
const textMatches = text.length > 0 && prompt !== void 0 && (prompt === text || prompt.includes(text));
|
|
819
|
+
const recent = last.timestamp !== void 0 && Math.abs(runStartMs - last.timestamp) <= CURRENT_PROMPT_WINDOW_MS;
|
|
820
|
+
if (!textMatches && !recent) break;
|
|
821
|
+
kept.pop();
|
|
822
|
+
}
|
|
823
|
+
return kept.map((m) => m.message);
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* OpenClaw's transcript is a JSONL tree of entries; the visible conversation is
|
|
827
|
+
* the parent chain of the last message entry (compaction and reset entries
|
|
828
|
+
* branch it). Returns the normalized messages, oldest first.
|
|
829
|
+
*/
|
|
830
|
+
function readSessionTranscript(path, read = defaultTranscriptReader) {
|
|
831
|
+
const raw = read(path);
|
|
832
|
+
if (raw === void 0) return void 0;
|
|
833
|
+
return messagesFromEntries(raw.split("\n"));
|
|
834
|
+
}
|
|
835
|
+
function messagesFromEntries(lines) {
|
|
836
|
+
const byId = /* @__PURE__ */ new Map();
|
|
837
|
+
let last;
|
|
838
|
+
for (const line of lines) {
|
|
839
|
+
if (line.trim().length === 0) continue;
|
|
840
|
+
let entry;
|
|
841
|
+
try {
|
|
842
|
+
entry = JSON.parse(line);
|
|
843
|
+
} catch {
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
if (!entry || typeof entry !== "object") continue;
|
|
847
|
+
if (typeof entry.id === "string") byId.set(entry.id, entry);
|
|
848
|
+
if (entry.type === "message" && entry.message) last = entry;
|
|
849
|
+
}
|
|
850
|
+
if (!last) return [];
|
|
851
|
+
const chain = [];
|
|
852
|
+
const seen = /* @__PURE__ */ new Set();
|
|
853
|
+
let cursor = last;
|
|
854
|
+
while (cursor && chain.length < MAX_MESSAGES) {
|
|
855
|
+
if (cursor.type === "message" && cursor.message) chain.push(cursor);
|
|
856
|
+
if (cursor.type === "compaction") {
|
|
857
|
+
if (typeof cursor.summary === "string" && cursor.summary.length > 0) chain.push({
|
|
858
|
+
type: "message",
|
|
859
|
+
message: {
|
|
860
|
+
role: "user",
|
|
861
|
+
content: `[compaction summary] ${cursor.summary}`
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
const parentId = cursor.parentId;
|
|
867
|
+
if (!parentId || seen.has(parentId)) break;
|
|
868
|
+
seen.add(parentId);
|
|
869
|
+
cursor = byId.get(parentId);
|
|
870
|
+
}
|
|
871
|
+
chain.reverse();
|
|
872
|
+
const out = [];
|
|
873
|
+
for (const entry of chain) {
|
|
874
|
+
const [message] = normalizeMessages([entry.message]);
|
|
875
|
+
if (message) out.push({
|
|
876
|
+
message,
|
|
877
|
+
timestamp: entryTimestamp(entry)
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
return out;
|
|
881
|
+
}
|
|
882
|
+
function entryTimestamp(entry) {
|
|
883
|
+
const inner = entry.message?.timestamp;
|
|
884
|
+
if (typeof inner === "number") return inner;
|
|
885
|
+
if (typeof entry.timestamp === "string") {
|
|
886
|
+
const parsed = Date.parse(entry.timestamp);
|
|
887
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
//#endregion
|
|
891
|
+
//#region src/memory.ts
|
|
892
|
+
const MAX_FILE_BYTES = 1024 * 1024;
|
|
893
|
+
const MEMORY_ROOT_FILES = new Set([
|
|
894
|
+
"MEMORY.md",
|
|
895
|
+
"memory.md",
|
|
896
|
+
"USER.md",
|
|
897
|
+
"user.md"
|
|
898
|
+
]);
|
|
899
|
+
const MEMORY_DIR = "memory";
|
|
900
|
+
function memoryStoreId(agentId) {
|
|
901
|
+
return `openclaw/${agentId ?? "main"}`;
|
|
902
|
+
}
|
|
903
|
+
const defaultFileReader = (path) => {
|
|
904
|
+
try {
|
|
905
|
+
if (statSync(path).size > MAX_FILE_BYTES) return void 0;
|
|
906
|
+
return readFileSync(path, "utf8");
|
|
907
|
+
} catch {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
/** Workspace-relative record id for a path inside the memory scope, else undefined. */
|
|
912
|
+
function memoryRecordId(path, workspaceDir) {
|
|
913
|
+
if (!path) return void 0;
|
|
914
|
+
const abs = workspaceDir ? resolve(workspaceDir, path) : isAbsolute(path) ? path : void 0;
|
|
915
|
+
if (!abs) return void 0;
|
|
916
|
+
const rel = workspaceDir ? relative(workspaceDir, abs) : abs;
|
|
917
|
+
if (rel.startsWith("..")) return void 0;
|
|
918
|
+
const normalized = rel.split(sep).join("/");
|
|
919
|
+
if (MEMORY_ROOT_FILES.has(normalized)) return normalized;
|
|
920
|
+
if (normalized.startsWith(`${MEMORY_DIR}/`) && normalized.endsWith(".md")) return normalized;
|
|
921
|
+
}
|
|
922
|
+
/** Classify a finished tool call as memory operations; empty when it touched none. */
|
|
923
|
+
function memoryEventsFromToolCall(call, readFile = defaultFileReader) {
|
|
924
|
+
if (call.error) return [];
|
|
925
|
+
if (call.toolName === "apply_patch") return patchEvents(call, memoryStoreId(call.agentId), readFile);
|
|
926
|
+
const single = singleMemoryEvent(call, readFile);
|
|
927
|
+
return single ? [single] : [];
|
|
928
|
+
}
|
|
929
|
+
function singleMemoryEvent(call, readFile) {
|
|
930
|
+
const storeId = memoryStoreId(call.agentId);
|
|
931
|
+
switch (call.toolName) {
|
|
932
|
+
case "memory_search": return {
|
|
933
|
+
operation: "search_memory",
|
|
934
|
+
storeId,
|
|
935
|
+
queryText: str(call.params.query),
|
|
936
|
+
records: searchRecords(call.result)
|
|
937
|
+
};
|
|
938
|
+
case "memory_get": {
|
|
939
|
+
const path = str(call.params.path);
|
|
940
|
+
const text = resultText(call.result);
|
|
941
|
+
return {
|
|
942
|
+
operation: "search_memory",
|
|
943
|
+
storeId,
|
|
944
|
+
recordId: path,
|
|
945
|
+
records: text !== void 0 ? [{
|
|
946
|
+
id: path,
|
|
947
|
+
content: text
|
|
948
|
+
}] : []
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
case "memory_recall": return {
|
|
952
|
+
operation: "search_memory",
|
|
953
|
+
storeId,
|
|
954
|
+
queryText: str(call.params.query),
|
|
955
|
+
records: searchRecords(call.result)
|
|
956
|
+
};
|
|
957
|
+
case "memory_store": {
|
|
958
|
+
const text = str(call.params.text) ?? str(call.params.content);
|
|
959
|
+
return {
|
|
960
|
+
operation: "upsert_memory",
|
|
961
|
+
storeId,
|
|
962
|
+
records: text !== void 0 ? [{ content: text }] : []
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
case "memory_forget": return {
|
|
966
|
+
operation: "delete_memory",
|
|
967
|
+
storeId,
|
|
968
|
+
recordId: str(call.params.id) ?? str(call.params.query),
|
|
969
|
+
records: []
|
|
970
|
+
};
|
|
971
|
+
case "write":
|
|
972
|
+
case "edit": return fileWriteEvent(call, storeId, readFile);
|
|
973
|
+
default: return;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
function fileWriteEvent(call, storeId, readFile) {
|
|
977
|
+
const path = str(call.params.path);
|
|
978
|
+
const recordId = memoryRecordId(path, call.workspaceDir);
|
|
979
|
+
if (!recordId || !path) return void 0;
|
|
980
|
+
const abs = call.workspaceDir ? resolve(call.workspaceDir, path) : path;
|
|
981
|
+
return writeEvent(storeId, recordId, call.toolName === "write" ? str(call.params.content) : readFile(abs));
|
|
982
|
+
}
|
|
983
|
+
function writeEvent(storeId, recordId, body) {
|
|
984
|
+
if (body === void 0) return {
|
|
985
|
+
operation: "upsert_memory",
|
|
986
|
+
storeId,
|
|
987
|
+
recordId,
|
|
988
|
+
records: [],
|
|
989
|
+
bodyUnavailable: true
|
|
990
|
+
};
|
|
991
|
+
if (body.trim().length === 0) return {
|
|
992
|
+
operation: "delete_memory",
|
|
993
|
+
storeId,
|
|
994
|
+
recordId,
|
|
995
|
+
records: []
|
|
996
|
+
};
|
|
997
|
+
return {
|
|
998
|
+
operation: "upsert_memory",
|
|
999
|
+
storeId,
|
|
1000
|
+
recordId,
|
|
1001
|
+
records: [{
|
|
1002
|
+
id: recordId,
|
|
1003
|
+
content: body
|
|
1004
|
+
}]
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
const PATCH_FILE_LINE = /^\*\*\* (Update|Add|Delete) File: (.+)$/gm;
|
|
1008
|
+
/**
|
|
1009
|
+
* Codex's `apply_patch` takes one patch text touching any number of files
|
|
1010
|
+
* (`*** Update File: <path>`). Each memory-scoped file becomes its own event,
|
|
1011
|
+
* with the body read back from disk after the patch landed.
|
|
1012
|
+
*/
|
|
1013
|
+
function patchEvents(call, storeId, readFile) {
|
|
1014
|
+
const patch = str(call.params.command) ?? str(call.params.input) ?? str(call.params.patch);
|
|
1015
|
+
if (!patch) return [];
|
|
1016
|
+
const events = [];
|
|
1017
|
+
for (const match of patch.matchAll(PATCH_FILE_LINE)) {
|
|
1018
|
+
const action = match[1];
|
|
1019
|
+
const path = match[2]?.trim();
|
|
1020
|
+
const recordId = memoryRecordId(path, call.workspaceDir);
|
|
1021
|
+
if (!recordId || !path) continue;
|
|
1022
|
+
if (action === "Delete") {
|
|
1023
|
+
events.push({
|
|
1024
|
+
operation: "delete_memory",
|
|
1025
|
+
storeId,
|
|
1026
|
+
recordId,
|
|
1027
|
+
records: []
|
|
1028
|
+
});
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
const abs = call.workspaceDir ? resolve(call.workspaceDir, path) : path;
|
|
1032
|
+
events.push(writeEvent(storeId, recordId, readFile(abs)));
|
|
1033
|
+
}
|
|
1034
|
+
return events;
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* The frozen snapshot injected at session start: every non-empty built-in
|
|
1038
|
+
* store file, read once per session. Returns undefined when nothing exists.
|
|
1039
|
+
*/
|
|
1040
|
+
function memorySnapshot(workspaceDir, agentId, readFile = defaultFileReader, today = /* @__PURE__ */ new Date()) {
|
|
1041
|
+
if (!workspaceDir) return void 0;
|
|
1042
|
+
const candidates = [
|
|
1043
|
+
"MEMORY.md",
|
|
1044
|
+
"USER.md",
|
|
1045
|
+
`${MEMORY_DIR}/${today.toISOString().slice(0, 10)}.md`
|
|
1046
|
+
];
|
|
1047
|
+
const records = [];
|
|
1048
|
+
for (const rel of candidates) {
|
|
1049
|
+
const body = readFile(resolve(workspaceDir, rel));
|
|
1050
|
+
if (body !== void 0 && body.trim().length > 0) records.push({
|
|
1051
|
+
id: rel,
|
|
1052
|
+
content: body
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
if (records.length === 0) return void 0;
|
|
1056
|
+
return {
|
|
1057
|
+
operation: "search_memory",
|
|
1058
|
+
storeId: memoryStoreId(agentId),
|
|
1059
|
+
records
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
function searchRecords(result) {
|
|
1063
|
+
const details = detailsOf(result);
|
|
1064
|
+
const list = Array.isArray(details?.results) ? details.results : void 0;
|
|
1065
|
+
if (list) {
|
|
1066
|
+
const out = [];
|
|
1067
|
+
for (const item of list) {
|
|
1068
|
+
if (!item || typeof item !== "object") continue;
|
|
1069
|
+
const r = item;
|
|
1070
|
+
const path = str(r.path);
|
|
1071
|
+
const line = typeof r.startLine === "number" ? `#${r.startLine}` : "";
|
|
1072
|
+
out.push({
|
|
1073
|
+
...path ? { id: `${path}${line}` } : {},
|
|
1074
|
+
content: str(r.snippet) ?? str(r.text) ?? str(r.content) ?? "",
|
|
1075
|
+
...typeof r.score === "number" ? { score: r.score } : {}
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
return out;
|
|
1079
|
+
}
|
|
1080
|
+
const text = resultText(result);
|
|
1081
|
+
return text !== void 0 && text.length > 0 ? [{ content: text }] : [];
|
|
1082
|
+
}
|
|
1083
|
+
function detailsOf(result) {
|
|
1084
|
+
if (!result || typeof result !== "object") return void 0;
|
|
1085
|
+
const details = result.details;
|
|
1086
|
+
return details && typeof details === "object" ? details : void 0;
|
|
1087
|
+
}
|
|
1088
|
+
function resultText(result) {
|
|
1089
|
+
if (typeof result === "string") return result;
|
|
1090
|
+
if (!result || typeof result !== "object") return void 0;
|
|
1091
|
+
const details = detailsOf(result);
|
|
1092
|
+
if (details && typeof details.text === "string") return details.text;
|
|
1093
|
+
const content = result.content;
|
|
1094
|
+
if (typeof content === "string") return content;
|
|
1095
|
+
if (Array.isArray(content)) {
|
|
1096
|
+
const texts = content.map((b) => b && typeof b === "object" && typeof b.text === "string" ? b.text : "").filter((t) => t.length > 0);
|
|
1097
|
+
if (texts.length > 0) return texts.join("\n");
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
function str(value) {
|
|
1101
|
+
return typeof value === "string" ? value : void 0;
|
|
1102
|
+
}
|
|
1103
|
+
//#endregion
|
|
1104
|
+
//#region src/tools.ts
|
|
1105
|
+
/**
|
|
1106
|
+
* `llm_input.tools` is the post-policy tool list offered to the model, as
|
|
1107
|
+
* pi-ai `Tool` objects (`name`, `description`, `parameters` JSON schema) plus
|
|
1108
|
+
* runtime fields such as `execute` that never leave the process.
|
|
1109
|
+
*/
|
|
1110
|
+
function toolDefinitionsFrom(raw) {
|
|
1111
|
+
if (!raw || raw.length === 0) return void 0;
|
|
1112
|
+
const out = [];
|
|
1113
|
+
for (const item of raw) {
|
|
1114
|
+
if (!item || typeof item !== "object") continue;
|
|
1115
|
+
const obj = item;
|
|
1116
|
+
const fn = obj.function && typeof obj.function === "object" ? obj.function : obj;
|
|
1117
|
+
if (typeof fn.name !== "string" || fn.name.length === 0) continue;
|
|
1118
|
+
const parameters = fn.parameters ?? fn.inputSchema ?? fn.input_schema;
|
|
1119
|
+
out.push({
|
|
1120
|
+
type: "function",
|
|
1121
|
+
name: fn.name,
|
|
1122
|
+
description: typeof fn.description === "string" ? fn.description : "",
|
|
1123
|
+
...parameters !== void 0 ? { parameters: plainSchema(parameters) } : {}
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
return out.length > 0 ? out : void 0;
|
|
1127
|
+
}
|
|
1128
|
+
function plainSchema(schema) {
|
|
1129
|
+
try {
|
|
1130
|
+
return JSON.parse(JSON.stringify(schema));
|
|
1131
|
+
} catch {
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
//#endregion
|
|
1136
|
+
//#region src/usage.ts
|
|
1137
|
+
function isTranscriptAssistant(raw) {
|
|
1138
|
+
return !!raw && typeof raw === "object" && raw.role === "assistant";
|
|
1139
|
+
}
|
|
1140
|
+
const FINISH_REASONS = {
|
|
1141
|
+
stop: "stop",
|
|
1142
|
+
length: "length",
|
|
1143
|
+
toolUse: "tool_calls",
|
|
1144
|
+
error: "error",
|
|
1145
|
+
aborted: "cancelled"
|
|
1146
|
+
};
|
|
1147
|
+
function finishReason(stopReason) {
|
|
1148
|
+
if (!stopReason) return void 0;
|
|
1149
|
+
return FINISH_REASONS[stopReason] ?? stopReason;
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* `gen_ai.usage.*` from a transcript message. `output_tokens` stays inclusive
|
|
1153
|
+
* of reasoning: Latitude's resolver subtracts `reasoning_tokens` itself.
|
|
1154
|
+
*/
|
|
1155
|
+
function usageAttrsFromTranscript(usage) {
|
|
1156
|
+
if (!usage) return {};
|
|
1157
|
+
const input = num(usage.input);
|
|
1158
|
+
const output = num(usage.output);
|
|
1159
|
+
const cacheRead = num(usage.cacheRead);
|
|
1160
|
+
const cacheWrite = num(usage.cacheWrite);
|
|
1161
|
+
const total = num(usage.totalTokens) ?? sumDefined(input, output, cacheRead, cacheWrite);
|
|
1162
|
+
return {
|
|
1163
|
+
"gen_ai.usage.input_tokens": input,
|
|
1164
|
+
"gen_ai.usage.output_tokens": output,
|
|
1165
|
+
"gen_ai.usage.cache_read.input_tokens": cacheRead,
|
|
1166
|
+
"gen_ai.usage.cache_creation.input_tokens": cacheWrite,
|
|
1167
|
+
"gen_ai.usage.reasoning_tokens": num(usage.reasoningTokens),
|
|
1168
|
+
"gen_ai.usage.total_tokens": total,
|
|
1169
|
+
...costAttrs(usage.cost)
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
/** Attempt-aggregate usage from `llm_output`, same keys. */
|
|
1173
|
+
function usageAttrsFromAggregate(usage) {
|
|
1174
|
+
if (!usage) return {};
|
|
1175
|
+
const input = num(usage.input);
|
|
1176
|
+
const output = num(usage.output);
|
|
1177
|
+
const cacheRead = num(usage.cacheRead);
|
|
1178
|
+
const cacheWrite = num(usage.cacheWrite);
|
|
1179
|
+
return {
|
|
1180
|
+
"gen_ai.usage.input_tokens": input,
|
|
1181
|
+
"gen_ai.usage.output_tokens": output,
|
|
1182
|
+
"gen_ai.usage.cache_read.input_tokens": cacheRead,
|
|
1183
|
+
"gen_ai.usage.cache_creation.input_tokens": cacheWrite,
|
|
1184
|
+
"gen_ai.usage.total_tokens": num(usage.total) ?? sumDefined(input, output, cacheRead, cacheWrite)
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* OpenClaw prices every call from its own model catalog (or the provider's
|
|
1189
|
+
* bill when `totalOrigin` says so). A positive total is reported as the span's
|
|
1190
|
+
* cost so a model missing from Latitude's catalog still carries a price.
|
|
1191
|
+
*/
|
|
1192
|
+
function costAttrs(cost) {
|
|
1193
|
+
if (!cost) return {};
|
|
1194
|
+
const total = num(cost.total);
|
|
1195
|
+
if (total === void 0 || total <= 0) return {};
|
|
1196
|
+
return {
|
|
1197
|
+
"gen_ai.usage.cost": total,
|
|
1198
|
+
"gen_ai.usage.input_cost": sumDefined(num(cost.input), num(cost.cacheRead), num(cost.cacheWrite)),
|
|
1199
|
+
"gen_ai.usage.output_cost": num(cost.output),
|
|
1200
|
+
"openclaw.cost.origin": cost.totalOrigin === "provider-billed" ? "provider-billed" : "catalog"
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
function num(value) {
|
|
1204
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1205
|
+
}
|
|
1206
|
+
function sumDefined(...values) {
|
|
1207
|
+
const defined = values.filter((v) => v !== void 0);
|
|
1208
|
+
if (defined.length === 0) return void 0;
|
|
1209
|
+
return defined.reduce((a, b) => a + b, 0);
|
|
1210
|
+
}
|
|
1211
|
+
//#endregion
|
|
1212
|
+
//#region src/span-builder.ts
|
|
1213
|
+
const GRACE_MS = 1500;
|
|
1214
|
+
const RUN_TTL_MS = 7200 * 1e3;
|
|
1215
|
+
const LINK_TTL_MS = 3600 * 1e3;
|
|
1216
|
+
const LINK_MAX = 1e3;
|
|
1217
|
+
const SESSION_INDEX_MAX = 2e3;
|
|
1218
|
+
const STANDALONE_COMPACTION_MAX = 100;
|
|
1219
|
+
const SPAWN_TOOL_PATTERN = /spawn/i;
|
|
1220
|
+
const MAX_DELTAS = 256;
|
|
1221
|
+
const MAX_THINKING_CHARS = 64 * 1024;
|
|
1222
|
+
const KIND_INTERNAL = 1;
|
|
1223
|
+
const KIND_CLIENT = 3;
|
|
1224
|
+
var SpanBuilder = class {
|
|
1225
|
+
runs = /* @__PURE__ */ new Map();
|
|
1226
|
+
runsBySession = /* @__PURE__ */ new Map();
|
|
1227
|
+
subagentLinks = /* @__PURE__ */ new Map();
|
|
1228
|
+
pendingSubagents = /* @__PURE__ */ new Map();
|
|
1229
|
+
sessionIds = /* @__PURE__ */ new Map();
|
|
1230
|
+
modelBySession = /* @__PURE__ */ new Map();
|
|
1231
|
+
workspaceBySession = /* @__PURE__ */ new Map();
|
|
1232
|
+
memorySnapshotSessions = /* @__PURE__ */ new Set();
|
|
1233
|
+
senders = /* @__PURE__ */ new Map();
|
|
1234
|
+
sendersById = /* @__PURE__ */ new Map();
|
|
1235
|
+
cronStarted = /* @__PURE__ */ new Map();
|
|
1236
|
+
sessionHistory;
|
|
1237
|
+
standaloneCompactions = /* @__PURE__ */ new Map();
|
|
1238
|
+
emit;
|
|
1239
|
+
now;
|
|
1240
|
+
schedule;
|
|
1241
|
+
readFile;
|
|
1242
|
+
readTranscript;
|
|
1243
|
+
readTranscriptRows;
|
|
1244
|
+
stateDir;
|
|
1245
|
+
log;
|
|
1246
|
+
pluginVersion;
|
|
1247
|
+
operatorTags;
|
|
1248
|
+
operatorMetadata;
|
|
1249
|
+
memoryEnabled;
|
|
1250
|
+
memoryContent;
|
|
1251
|
+
toolDefinitionsEnabled;
|
|
1252
|
+
constructor(options) {
|
|
1253
|
+
this.emit = options.emit;
|
|
1254
|
+
this.now = options.now ?? (() => Date.now());
|
|
1255
|
+
this.schedule = options.schedule ?? ((fn, ms) => {
|
|
1256
|
+
const timer = setTimeout(fn, ms);
|
|
1257
|
+
timer.unref?.();
|
|
1258
|
+
return () => clearTimeout(timer);
|
|
1259
|
+
});
|
|
1260
|
+
this.readFile = options.readFile;
|
|
1261
|
+
this.readTranscript = options.readTranscript;
|
|
1262
|
+
this.readTranscriptRows = options.readTranscriptRows;
|
|
1263
|
+
this.stateDir = options.stateDir;
|
|
1264
|
+
this.sessionHistory = new SessionHistoryStore(this.now);
|
|
1265
|
+
this.log = options.log ?? (() => {});
|
|
1266
|
+
this.pluginVersion = options.pluginVersion;
|
|
1267
|
+
this.operatorTags = options.tags ?? [];
|
|
1268
|
+
this.operatorMetadata = options.metadata ?? {};
|
|
1269
|
+
this.memoryEnabled = options.memory ?? true;
|
|
1270
|
+
this.memoryContent = options.memoryContent ?? true;
|
|
1271
|
+
this.toolDefinitionsEnabled = options.toolDefinitions ?? true;
|
|
1272
|
+
}
|
|
1273
|
+
inflightCount() {
|
|
1274
|
+
return this.runs.size;
|
|
1275
|
+
}
|
|
1276
|
+
subagentLinkCount() {
|
|
1277
|
+
return this.subagentLinks.size;
|
|
1278
|
+
}
|
|
1279
|
+
onLlmInput(evt, ctx) {
|
|
1280
|
+
const run = this.ensureRun(evt.runId, ctx);
|
|
1281
|
+
if (!run) return;
|
|
1282
|
+
const harnessHistory = normalizeMessages(evt.historyMessages);
|
|
1283
|
+
if (harnessHistory.length > 0) {
|
|
1284
|
+
run.inheritedHistory = [];
|
|
1285
|
+
run.historySource = "harness";
|
|
1286
|
+
} else this.inheritHistory(run, evt.prompt);
|
|
1287
|
+
run.history = [...run.inheritedHistory, ...harnessHistory];
|
|
1288
|
+
if (evt.prompt) run.history.push(userMessageFromPrompt(evt.prompt));
|
|
1289
|
+
run.inputMessages = [...run.history];
|
|
1290
|
+
run.prompt = evt.prompt;
|
|
1291
|
+
run.systemPrompt = evt.systemPrompt;
|
|
1292
|
+
if (run.ctx.sessionKey) {
|
|
1293
|
+
remember(this.modelBySession, run.ctx.sessionKey, {
|
|
1294
|
+
provider: evt.provider,
|
|
1295
|
+
model: evt.model
|
|
1296
|
+
}, SESSION_INDEX_MAX);
|
|
1297
|
+
if (run.ctx.workspaceDir) remember(this.workspaceBySession, run.ctx.sessionKey, run.ctx.workspaceDir, SESSION_INDEX_MAX);
|
|
1298
|
+
}
|
|
1299
|
+
if (this.toolDefinitionsEnabled) run.toolDefinitions = toolDefinitionsFrom(evt.tools) ?? run.toolDefinitions;
|
|
1300
|
+
Object.assign(run.root.attrs, {
|
|
1301
|
+
"gen_ai.request.model": evt.model,
|
|
1302
|
+
"openclaw.provider": evt.provider,
|
|
1303
|
+
"openclaw.images.count": evt.imagesCount,
|
|
1304
|
+
"openclaw.tool_count": evt.tools?.length
|
|
1305
|
+
});
|
|
1306
|
+
this.recordMemorySnapshot(run);
|
|
1307
|
+
}
|
|
1308
|
+
onLlmOutput(evt, ctx) {
|
|
1309
|
+
const run = this.runs.get(ctx.runId ?? evt.runId);
|
|
1310
|
+
if (!run) return;
|
|
1311
|
+
this.mergeCtx(run, ctx);
|
|
1312
|
+
run.outputSeen = true;
|
|
1313
|
+
run.aggregateUsage = usageAttrsFromAggregate(evt.usage);
|
|
1314
|
+
run.output = {
|
|
1315
|
+
resolvedRef: evt.resolvedRef,
|
|
1316
|
+
harnessId: evt.harnessId,
|
|
1317
|
+
reasoningEffort: evt.reasoningEffort,
|
|
1318
|
+
lastAssistant: evt.lastAssistant
|
|
1319
|
+
};
|
|
1320
|
+
Object.assign(run.root.attrs, {
|
|
1321
|
+
"gen_ai.response.model": evt.model,
|
|
1322
|
+
"openclaw.resolved.ref": evt.resolvedRef,
|
|
1323
|
+
"openclaw.harness.id": evt.harnessId,
|
|
1324
|
+
"openclaw.reasoning.effort": evt.reasoningEffort
|
|
1325
|
+
});
|
|
1326
|
+
if (run.endEvent) this.finalize(run);
|
|
1327
|
+
}
|
|
1328
|
+
onAgentEnd(evt, ctx) {
|
|
1329
|
+
const runId = ctx.runId ?? evt.runId;
|
|
1330
|
+
if (!runId) return;
|
|
1331
|
+
const run = this.runs.get(runId);
|
|
1332
|
+
if (!run) {
|
|
1333
|
+
this.subagentLinks.delete(runId);
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
this.mergeCtx(run, ctx);
|
|
1337
|
+
run.endEvent = evt;
|
|
1338
|
+
if (run.outputSeen) {
|
|
1339
|
+
this.finalize(run);
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
run.cancelGrace = this.schedule(() => this.finalize(run), GRACE_MS);
|
|
1343
|
+
}
|
|
1344
|
+
onModelCallStarted(evt, ctx) {
|
|
1345
|
+
const run = this.ensureRun(evt.runId, {
|
|
1346
|
+
...ctx,
|
|
1347
|
+
sessionId: ctx.sessionId ?? evt.sessionId,
|
|
1348
|
+
sessionKey: ctx.sessionKey ?? evt.sessionKey
|
|
1349
|
+
});
|
|
1350
|
+
if (!run) return;
|
|
1351
|
+
const span = {
|
|
1352
|
+
spanId: hashHex(`${run.runId}:llm_request:${evt.callId}`, 16),
|
|
1353
|
+
traceId: run.traceId,
|
|
1354
|
+
parentSpanId: run.root.spanId,
|
|
1355
|
+
name: "llm_request",
|
|
1356
|
+
kind: KIND_INTERNAL,
|
|
1357
|
+
startMs: this.now(),
|
|
1358
|
+
endMs: void 0,
|
|
1359
|
+
attrs: {
|
|
1360
|
+
"gen_ai.operation.name": "chat",
|
|
1361
|
+
"gen_ai.provider.name": evt.provider,
|
|
1362
|
+
"gen_ai.system": evt.provider,
|
|
1363
|
+
"gen_ai.request.model": evt.model,
|
|
1364
|
+
"gen_ai.request.stream": true,
|
|
1365
|
+
"openclaw.call.id": evt.callId,
|
|
1366
|
+
"openclaw.api": evt.api,
|
|
1367
|
+
"openclaw.transport": evt.transport,
|
|
1368
|
+
"openclaw.context.token_budget": evt.contextTokenBudget,
|
|
1369
|
+
"llm_request.call_index": run.llmCalls.length + run.openModelCalls.size,
|
|
1370
|
+
"gen_ai.input.messages:gated": [...run.history],
|
|
1371
|
+
"gen_ai.system_instructions:gated": run.systemPrompt ? systemInstructionsParts(run.systemPrompt) : void 0,
|
|
1372
|
+
"gen_ai.tool.definitions:gated": run.toolDefinitions
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
run.openModelCalls.set(evt.callId, span);
|
|
1376
|
+
}
|
|
1377
|
+
onModelCallEnded(evt, _ctx) {
|
|
1378
|
+
const run = this.runs.get(evt.runId);
|
|
1379
|
+
if (!run) return;
|
|
1380
|
+
const span = run.openModelCalls.get(evt.callId);
|
|
1381
|
+
if (!span) return;
|
|
1382
|
+
span.endMs = this.now();
|
|
1383
|
+
span.outcome = evt.outcome === "completed" ? "ok" : "error";
|
|
1384
|
+
span.errorMessage = evt.errorCategory;
|
|
1385
|
+
Object.assign(span.attrs, {
|
|
1386
|
+
"openclaw.duration_ms": evt.durationMs,
|
|
1387
|
+
"openclaw.outcome": evt.outcome,
|
|
1388
|
+
"openclaw.error.category": evt.errorCategory,
|
|
1389
|
+
"openclaw.failure.kind": evt.failureKind,
|
|
1390
|
+
"error.type": evt.outcome === "error" ? evt.errorCategory ?? evt.failureKind ?? "error" : void 0,
|
|
1391
|
+
"openclaw.request.payload_bytes": evt.requestPayloadBytes,
|
|
1392
|
+
"openclaw.response.stream_bytes": evt.responseStreamBytes,
|
|
1393
|
+
"openclaw.ttfb_ms": evt.timeToFirstByteMs,
|
|
1394
|
+
"gen_ai.server.time_to_first_token": evt.timeToFirstByteMs !== void 0 && evt.timeToFirstByteMs > 0 ? Math.round(evt.timeToFirstByteMs * 1e6) : void 0,
|
|
1395
|
+
"openclaw.upstream.request_id_hash": evt.upstreamRequestIdHash
|
|
1396
|
+
});
|
|
1397
|
+
run.openModelCalls.delete(evt.callId);
|
|
1398
|
+
run.llmCalls.push(span);
|
|
1399
|
+
}
|
|
1400
|
+
onBeforeToolCall(evt, ctx) {
|
|
1401
|
+
const runId = evt.runId ?? ctx.runId;
|
|
1402
|
+
if (!runId) return;
|
|
1403
|
+
const run = this.ensureRun(runId, toolCtxToAgentCtx(ctx, runId));
|
|
1404
|
+
if (!run) return;
|
|
1405
|
+
const toolCallId = evt.toolCallId ?? ctx.toolCallId ?? `${evt.toolName}:${randomUUID()}`;
|
|
1406
|
+
const span = {
|
|
1407
|
+
spanId: hashHex(`${run.runId}:tool_call:${toolCallId}`, 16),
|
|
1408
|
+
traceId: run.traceId,
|
|
1409
|
+
parentSpanId: run.root.spanId,
|
|
1410
|
+
name: `tool_call:${evt.toolName}`,
|
|
1411
|
+
kind: KIND_CLIENT,
|
|
1412
|
+
startMs: this.now(),
|
|
1413
|
+
endMs: void 0,
|
|
1414
|
+
attrs: {
|
|
1415
|
+
"gen_ai.operation.name": "execute_tool",
|
|
1416
|
+
"gen_ai.tool.name": evt.toolName,
|
|
1417
|
+
"gen_ai.tool.call.id": toolCallId,
|
|
1418
|
+
"gen_ai.tool.call.arguments:gated": evt.params
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
run.openToolCalls.set(toolCallId, span);
|
|
1422
|
+
run.history.push({
|
|
1423
|
+
role: "assistant",
|
|
1424
|
+
parts: [{
|
|
1425
|
+
type: "tool_call",
|
|
1426
|
+
id: toolCallId,
|
|
1427
|
+
name: evt.toolName,
|
|
1428
|
+
arguments: evt.params
|
|
1429
|
+
}]
|
|
1430
|
+
});
|
|
1431
|
+
if (SPAWN_TOOL_PATTERN.test(evt.toolName)) run.lastSpawnToolSpanId = span.spanId;
|
|
1432
|
+
}
|
|
1433
|
+
onAfterToolCall(evt, ctx) {
|
|
1434
|
+
const runId = evt.runId ?? ctx.runId;
|
|
662
1435
|
if (!runId) return;
|
|
663
1436
|
const run = this.runs.get(runId);
|
|
664
1437
|
if (!run) return;
|
|
665
|
-
run.
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
1438
|
+
let resolvedId = evt.toolCallId && run.openToolCalls.has(evt.toolCallId) ? evt.toolCallId : void 0;
|
|
1439
|
+
if (!resolvedId) resolvedId = this.findOpenToolCallByName(run, evt.toolName);
|
|
1440
|
+
if (!resolvedId) return;
|
|
1441
|
+
const span = run.openToolCalls.get(resolvedId);
|
|
1442
|
+
if (!span) return;
|
|
1443
|
+
span.endMs = this.now();
|
|
1444
|
+
const isError = Boolean(evt.error);
|
|
1445
|
+
span.outcome = isError ? "error" : "ok";
|
|
1446
|
+
span.errorMessage = evt.error;
|
|
1447
|
+
Object.assign(span.attrs, {
|
|
1448
|
+
"gen_ai.tool.call.result:gated": evt.result,
|
|
1449
|
+
"tool.is_error": isError,
|
|
1450
|
+
"error.type": isError ? "tool_error" : void 0,
|
|
1451
|
+
"error.message:gated": evt.error,
|
|
1452
|
+
"openclaw.duration_ms": evt.durationMs
|
|
1453
|
+
});
|
|
1454
|
+
run.openToolCalls.delete(resolvedId);
|
|
1455
|
+
run.closed.push(span);
|
|
1456
|
+
run.history.push({
|
|
1457
|
+
role: "tool",
|
|
1458
|
+
parts: [{
|
|
1459
|
+
type: "tool_call_response",
|
|
1460
|
+
id: resolvedId,
|
|
1461
|
+
name: evt.toolName,
|
|
1462
|
+
response: evt.result ?? evt.error ?? ""
|
|
1463
|
+
}]
|
|
1464
|
+
});
|
|
1465
|
+
if (this.memoryEnabled) {
|
|
1466
|
+
const events = memoryEventsFromToolCall({
|
|
1467
|
+
toolName: evt.toolName,
|
|
1468
|
+
params: evt.params,
|
|
1469
|
+
result: evt.result,
|
|
1470
|
+
error: evt.error,
|
|
1471
|
+
workspaceDir: run.ctx.workspaceDir,
|
|
1472
|
+
agentId: run.ctx.agentId
|
|
1473
|
+
}, this.readFile);
|
|
1474
|
+
for (const memory of events) run.closed.push(this.memorySpan(run, memory, span));
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* A compaction is a model call of its own: the compacted messages go in and
|
|
1479
|
+
* a summary comes out, so it is exported as a `chat` span. OpenClaw fires no
|
|
1480
|
+
* per-call hooks for the summarizer, so its usage is unreported.
|
|
1481
|
+
*/
|
|
1482
|
+
onBeforeCompaction(evt, ctx) {
|
|
1483
|
+
const run = this.openRunForSession(ctx.sessionKey);
|
|
1484
|
+
const startMs = this.now();
|
|
1485
|
+
const traceId = run?.traceId ?? hashHex(`${ctx.sessionKey ?? "unknown"}:compaction:${startMs}`, 32);
|
|
1486
|
+
const model = ctx.sessionKey ? this.modelBySession.get(ctx.sessionKey) : void 0;
|
|
1487
|
+
const span = {
|
|
1488
|
+
spanId: hashHex(`${traceId}:compaction:${startMs}`, 16),
|
|
1489
|
+
traceId,
|
|
1490
|
+
parentSpanId: run?.root.spanId ?? "",
|
|
669
1491
|
name: "compaction",
|
|
670
|
-
|
|
1492
|
+
kind: KIND_INTERNAL,
|
|
1493
|
+
startMs,
|
|
671
1494
|
endMs: void 0,
|
|
672
1495
|
attrs: {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
"
|
|
1496
|
+
"gen_ai.operation.name": "chat",
|
|
1497
|
+
"gen_ai.provider.name": model?.provider,
|
|
1498
|
+
"gen_ai.system": model?.provider,
|
|
1499
|
+
"gen_ai.request.model": model?.model,
|
|
1500
|
+
"openclaw.usage.state": "unreported",
|
|
676
1501
|
"openclaw.compaction.message_count.before": evt.messageCount,
|
|
1502
|
+
"openclaw.compaction.compacting_count": evt.compactingCount,
|
|
1503
|
+
"openclaw.compaction.token_count.before": evt.tokenCount,
|
|
677
1504
|
"openclaw.compaction.session_file": evt.sessionFile,
|
|
678
|
-
"
|
|
1505
|
+
"openclaw.session.key": ctx.sessionKey,
|
|
1506
|
+
"gen_ai.input.messages:gated": evt.messages ? normalizeMessages(evt.messages) : void 0
|
|
679
1507
|
}
|
|
680
1508
|
};
|
|
1509
|
+
if (run) {
|
|
1510
|
+
run.openCompaction = span;
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
remember(this.standaloneCompactions, ctx.sessionKey ?? "", span, STANDALONE_COMPACTION_MAX);
|
|
681
1514
|
}
|
|
682
1515
|
onAfterCompaction(evt, ctx) {
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
span.endMs = Date.now();
|
|
1516
|
+
const run = this.openRunForSession(ctx.sessionKey);
|
|
1517
|
+
const span = run?.openCompaction ?? this.standaloneCompactions.get(ctx.sessionKey ?? "");
|
|
1518
|
+
if (!span) return;
|
|
1519
|
+
const endMs = this.now();
|
|
1520
|
+
span.endMs = endMs;
|
|
689
1521
|
span.outcome = "ok";
|
|
1522
|
+
const sessionId = ctx.sessionId ?? run?.ctx.sessionId ?? (ctx.sessionKey ? this.sessionIds.get(ctx.sessionKey) : void 0);
|
|
1523
|
+
const agentId = ctx.agentId ?? run?.ctx.agentId ?? agentIdFromSessionKey(ctx.sessionKey);
|
|
1524
|
+
const workspaceDir = run?.ctx.workspaceDir ?? (ctx.sessionKey ? this.workspaceBySession.get(ctx.sessionKey) : void 0);
|
|
1525
|
+
const record = this.latestCompaction(sessionId, agentId, workspaceDir);
|
|
690
1526
|
Object.assign(span.attrs, {
|
|
691
1527
|
"openclaw.compaction.message_count.after": evt.messageCount,
|
|
692
1528
|
"openclaw.compaction.compacted_count": evt.compactedCount,
|
|
693
|
-
"openclaw.compaction.token_count": evt.tokenCount
|
|
1529
|
+
"openclaw.compaction.token_count.after": evt.tokenCount ?? record?.tokensAfter,
|
|
1530
|
+
"openclaw.compaction.token_count.before": span.attrs["openclaw.compaction.token_count.before"] ?? record?.tokensBefore,
|
|
1531
|
+
"openclaw.compaction.previous_session_id": evt.previousSessionId,
|
|
1532
|
+
"openclaw.compaction.summary_chars": record?.summary.length,
|
|
1533
|
+
"gen_ai.output.messages:gated": record ? [{
|
|
1534
|
+
role: "assistant",
|
|
1535
|
+
parts: [{
|
|
1536
|
+
type: "text",
|
|
1537
|
+
content: record.summary
|
|
1538
|
+
}]
|
|
1539
|
+
}] : void 0
|
|
694
1540
|
});
|
|
695
|
-
run
|
|
696
|
-
|
|
1541
|
+
if (run?.openCompaction === span) {
|
|
1542
|
+
run.openCompaction = void 0;
|
|
1543
|
+
run.closed.push(span);
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
this.standaloneCompactions.delete(ctx.sessionKey ?? "");
|
|
1547
|
+
const enrichment = deriveEnrichment({
|
|
1548
|
+
ctx: {
|
|
1549
|
+
agentId,
|
|
1550
|
+
sessionKey: ctx.sessionKey,
|
|
1551
|
+
sessionId,
|
|
1552
|
+
workspaceDir
|
|
1553
|
+
},
|
|
1554
|
+
subagentIds: [],
|
|
1555
|
+
pluginVersion: this.pluginVersion
|
|
1556
|
+
}, {
|
|
1557
|
+
tags: this.operatorTags,
|
|
1558
|
+
metadata: this.operatorMetadata
|
|
1559
|
+
});
|
|
1560
|
+
const compacted = evt.compactedCount ?? 0;
|
|
1561
|
+
const root = {
|
|
1562
|
+
spanId: hashHex(`${span.traceId}:compaction-root`, 16),
|
|
1563
|
+
traceId: span.traceId,
|
|
1564
|
+
parentSpanId: "",
|
|
1565
|
+
name: "compaction",
|
|
1566
|
+
kind: KIND_INTERNAL,
|
|
1567
|
+
startMs: span.startMs,
|
|
1568
|
+
endMs,
|
|
1569
|
+
outcome: "ok",
|
|
1570
|
+
attrs: {
|
|
1571
|
+
"gen_ai.operation.name": "invoke_agent",
|
|
1572
|
+
"interaction.kind": "compaction",
|
|
1573
|
+
"interaction.duration_ms": endMs - span.startMs,
|
|
1574
|
+
"user_prompt:gated": `[compaction] ${compacted} messages summarized, ${evt.messageCount} kept`,
|
|
1575
|
+
"gen_ai.output.messages:gated": span.attrs["gen_ai.output.messages:gated"],
|
|
1576
|
+
"openclaw.outcome": "completed",
|
|
1577
|
+
"openclaw.llm_calls": 1,
|
|
1578
|
+
"openclaw.tool_calls": 0
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
span.parentSpanId = root.spanId;
|
|
1582
|
+
const common = {
|
|
1583
|
+
...sessionAttrs(sessionId),
|
|
1584
|
+
"openclaw.session.id": sessionId,
|
|
1585
|
+
"openclaw.session.key": ctx.sessionKey,
|
|
1586
|
+
"openclaw.agent.id": agentId,
|
|
1587
|
+
"gen_ai.agent.name": agentId,
|
|
1588
|
+
"openclaw.workspace.dir": workspaceDir,
|
|
1589
|
+
"latitude.tags": enrichment.tags,
|
|
1590
|
+
"latitude.metadata": enrichment.metadata
|
|
1591
|
+
};
|
|
1592
|
+
for (const target of [root, span]) for (const [k, v] of Object.entries(common)) if (target.attrs[k] === void 0) target.attrs[k] = v;
|
|
1593
|
+
this.emit({
|
|
1594
|
+
runId: `compaction:${span.spanId}`,
|
|
1595
|
+
spans: [root, span]
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
latestCompaction(sessionId, agentId, workspaceDir) {
|
|
1599
|
+
const stateDir = this.stateDir ?? stateDirFromWorkspace(workspaceDir);
|
|
1600
|
+
if (!sessionId || !agentId || !stateDir) return void 0;
|
|
1601
|
+
return readLatestCompaction(agentDatabasePath(stateDir, agentId), sessionId, this.readTranscriptRows) ?? readLatestCompactionFromFile(sessionTranscriptPath(stateDir, agentId, sessionId), this.readTranscript);
|
|
1602
|
+
}
|
|
1603
|
+
onSessionStart(evt, _ctx) {
|
|
1604
|
+
if (evt.sessionKey) {
|
|
1605
|
+
remember(this.sessionIds, evt.sessionKey, evt.sessionId, SESSION_INDEX_MAX);
|
|
1606
|
+
this.memorySnapshotSessions.delete(evt.sessionKey);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
onSessionEnd(sessionKey, sessionId) {
|
|
1610
|
+
if (sessionId) this.sessionHistory.forget(sessionId);
|
|
1611
|
+
if (!sessionKey) return;
|
|
1612
|
+
this.memorySnapshotSessions.delete(sessionKey);
|
|
1613
|
+
this.senders.delete(sessionKey);
|
|
1614
|
+
}
|
|
1615
|
+
onMessageReceived(evt, ctx) {
|
|
1616
|
+
const sessionKey = evt.sessionKey ?? ctx.sessionKey;
|
|
1617
|
+
const id = evt.senderId ?? evt.metadata?.senderId ?? ctx.senderId;
|
|
1618
|
+
if (!sessionKey || !id) return;
|
|
1619
|
+
let bySender = this.senders.get(sessionKey);
|
|
1620
|
+
if (!bySender) {
|
|
1621
|
+
bySender = /* @__PURE__ */ new Map();
|
|
1622
|
+
this.senders.set(sessionKey, bySender);
|
|
1623
|
+
}
|
|
1624
|
+
const sender = {
|
|
1625
|
+
id,
|
|
1626
|
+
name: evt.metadata?.senderName,
|
|
1627
|
+
username: evt.metadata?.senderUsername
|
|
1628
|
+
};
|
|
1629
|
+
bySender.set(id, sender);
|
|
1630
|
+
if (sender.name || sender.username) this.sendersById.set(id, sender);
|
|
697
1631
|
}
|
|
698
1632
|
/**
|
|
699
|
-
*
|
|
700
|
-
*
|
|
1633
|
+
* Streamed agent events: `lifecycle` start gives the run's true start before
|
|
1634
|
+
* any hook fires, and the first `assistant` / `thinking` delta after a model
|
|
1635
|
+
* call started is its time to first token, which the Codex harness reports
|
|
1636
|
+
* nowhere else.
|
|
701
1637
|
*/
|
|
1638
|
+
onAgentEvent(evt) {
|
|
1639
|
+
if (!evt.runId || !evt.stream) return;
|
|
1640
|
+
if (evt.stream === "lifecycle") {
|
|
1641
|
+
if (evt.data?.phase !== "start") return;
|
|
1642
|
+
const startedAt = typeof evt.data.startedAt === "number" ? evt.data.startedAt : evt.ts;
|
|
1643
|
+
const run = this.ensureRun(evt.runId, {
|
|
1644
|
+
runId: evt.runId,
|
|
1645
|
+
sessionKey: evt.sessionKey,
|
|
1646
|
+
sessionId: evt.sessionId,
|
|
1647
|
+
agentId: evt.agentId
|
|
1648
|
+
});
|
|
1649
|
+
if (run && startedAt !== void 0 && startedAt < run.startedAt) {
|
|
1650
|
+
run.startedAt = startedAt;
|
|
1651
|
+
run.root.startMs = startedAt;
|
|
1652
|
+
}
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
if (evt.stream !== "assistant" && evt.stream !== "thinking") return;
|
|
1656
|
+
const delta = evt.data?.delta;
|
|
1657
|
+
if (typeof delta !== "string" || delta.length === 0) return;
|
|
1658
|
+
const run = this.runs.get(evt.runId);
|
|
1659
|
+
if (!run) return;
|
|
1660
|
+
const ts = evt.ts ?? this.now();
|
|
1661
|
+
if (run.deltas.length < MAX_DELTAS) run.deltas.push(ts);
|
|
1662
|
+
if (evt.stream === "thinking" && run.thinkingChars < MAX_THINKING_CHARS) {
|
|
1663
|
+
run.thinking.push({
|
|
1664
|
+
ts,
|
|
1665
|
+
text: delta
|
|
1666
|
+
});
|
|
1667
|
+
run.thinkingChars += delta.length;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
onCronChanged(evt) {
|
|
1671
|
+
const agentId = evt.agentId ?? evt.job?.agentId ?? "main";
|
|
1672
|
+
const job = {
|
|
1673
|
+
id: evt.jobId,
|
|
1674
|
+
name: evt.job?.name
|
|
1675
|
+
};
|
|
1676
|
+
if (evt.action === "started" || evt.action === "added" || evt.action === "updated") this.cronStarted.set(`job:${evt.jobId}`, job);
|
|
1677
|
+
if (evt.action === "started") {
|
|
1678
|
+
this.cronStarted.set(agentId, job);
|
|
1679
|
+
if (evt.sessionKey) this.cronStarted.set(`session:${evt.sessionKey}`, job);
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
if (evt.action === "finished") {
|
|
1683
|
+
if (this.cronStarted.get(agentId)?.id === evt.jobId) this.cronStarted.delete(agentId);
|
|
1684
|
+
if (evt.sessionKey) this.cronStarted.delete(`session:${evt.sessionKey}`);
|
|
1685
|
+
}
|
|
1686
|
+
if (evt.action === "removed") this.cronStarted.delete(`job:${evt.jobId}`);
|
|
1687
|
+
}
|
|
702
1688
|
onSubagentSpawned(evt, ctx) {
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
1689
|
+
const parent = this.openRunForSession(ctx.requesterSessionKey);
|
|
1690
|
+
const childRunId = evt.runId ?? ctx.runId;
|
|
1691
|
+
if (!childRunId) return;
|
|
1692
|
+
this.evictStale();
|
|
1693
|
+
const traceId = parent?.traceId ?? hashHex(childRunId, 32);
|
|
1694
|
+
const parentSpanId = parent ? parent.lastSpawnToolSpanId ?? parent.root.spanId : "";
|
|
1695
|
+
const agentName = evt.label ?? evt.agentId;
|
|
707
1696
|
const span = {
|
|
708
|
-
spanId: hashHex(`${
|
|
709
|
-
traceId
|
|
710
|
-
parentSpanId
|
|
1697
|
+
spanId: hashHex(`${childRunId}:subagent`, 16),
|
|
1698
|
+
traceId,
|
|
1699
|
+
parentSpanId,
|
|
711
1700
|
name: "subagent",
|
|
712
|
-
|
|
1701
|
+
kind: KIND_INTERNAL,
|
|
1702
|
+
startMs: this.now(),
|
|
713
1703
|
endMs: void 0,
|
|
714
1704
|
attrs: {
|
|
715
|
-
...
|
|
716
|
-
|
|
717
|
-
"
|
|
718
|
-
"
|
|
1705
|
+
...parent ? this.commonAttrs(parent) : {},
|
|
1706
|
+
"gen_ai.agent.name": agentName,
|
|
1707
|
+
"subagent.name": agentName,
|
|
1708
|
+
"subagent.type": evt.agentId,
|
|
1709
|
+
"subagent.id": `${evt.agentId}:${childRunId}`,
|
|
1710
|
+
"openclaw.run.id": childRunId,
|
|
1711
|
+
"openclaw.parent.run.id": parent?.runId,
|
|
719
1712
|
"openclaw.subagent.child_session_key": evt.childSessionKey,
|
|
720
1713
|
"openclaw.subagent.agent_id": evt.agentId,
|
|
721
1714
|
"openclaw.subagent.label": evt.label,
|
|
722
1715
|
"openclaw.subagent.mode": evt.mode,
|
|
723
1716
|
"openclaw.subagent.thread_requested": evt.threadRequested,
|
|
1717
|
+
"openclaw.subagent.resolved_model": evt.resolvedModel,
|
|
1718
|
+
"openclaw.subagent.resolved_provider": evt.resolvedProvider,
|
|
724
1719
|
"openclaw.subagent.requester.channel": evt.requester?.channel,
|
|
725
1720
|
"openclaw.subagent.requester.account_id": evt.requester?.accountId,
|
|
726
1721
|
"openclaw.subagent.requester.to": evt.requester?.to,
|
|
727
|
-
"openclaw.subagent.requester.thread_id":
|
|
1722
|
+
"openclaw.subagent.requester.thread_id": evt.requester?.threadId !== void 0 ? String(evt.requester.threadId) : void 0,
|
|
1723
|
+
...sessionAttrs(parent?.ctx.sessionId)
|
|
728
1724
|
}
|
|
729
1725
|
};
|
|
730
|
-
parent
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1726
|
+
if (parent) {
|
|
1727
|
+
parent.subagentIds.push(evt.agentId);
|
|
1728
|
+
parent.lastSpawnToolSpanId = void 0;
|
|
1729
|
+
}
|
|
1730
|
+
this.pendingSubagents.set(childRunId, {
|
|
1731
|
+
span,
|
|
1732
|
+
parentRunId: parent?.runId ?? "",
|
|
1733
|
+
createdAt: this.now()
|
|
736
1734
|
});
|
|
1735
|
+
this.subagentLinks.set(childRunId, {
|
|
1736
|
+
traceId,
|
|
1737
|
+
parentSpanId: span.spanId,
|
|
1738
|
+
parentRunId: parent?.runId ?? "",
|
|
1739
|
+
sessionId: parent?.ctx.sessionId,
|
|
1740
|
+
agentName,
|
|
1741
|
+
createdAt: this.now()
|
|
1742
|
+
});
|
|
1743
|
+
const child = this.runs.get(childRunId);
|
|
1744
|
+
if (child) this.applyLink(child);
|
|
737
1745
|
}
|
|
738
1746
|
onSubagentEnded(evt, ctx) {
|
|
739
|
-
const
|
|
740
|
-
if (!parentRunId) return;
|
|
741
|
-
const parent = this.runs.get(parentRunId);
|
|
742
|
-
if (!parent) return;
|
|
743
|
-
const childRunId = evt.runId;
|
|
1747
|
+
const childRunId = evt.runId ?? ctx.runId;
|
|
744
1748
|
if (!childRunId) return;
|
|
745
|
-
const
|
|
746
|
-
if (!
|
|
747
|
-
span
|
|
748
|
-
span.
|
|
1749
|
+
const pending = this.pendingSubagents.get(childRunId);
|
|
1750
|
+
if (!pending) return;
|
|
1751
|
+
const span = pending.span;
|
|
1752
|
+
span.endMs = evt.endedAt ?? this.now();
|
|
1753
|
+
const isError = evt.outcome === "error" || evt.outcome === "timeout" || Boolean(evt.error);
|
|
1754
|
+
span.outcome = isError ? "error" : "ok";
|
|
749
1755
|
span.errorMessage = evt.error;
|
|
750
1756
|
Object.assign(span.attrs, {
|
|
751
1757
|
"openclaw.subagent.target_session_key": evt.targetSessionKey,
|
|
@@ -753,93 +1759,512 @@ var SpanBuilder = class {
|
|
|
753
1759
|
"openclaw.subagent.reason": evt.reason,
|
|
754
1760
|
"openclaw.subagent.outcome": evt.outcome,
|
|
755
1761
|
"openclaw.subagent.send_farewell": evt.sendFarewell,
|
|
756
|
-
"openclaw.subagent.account_id": evt.accountId
|
|
1762
|
+
"openclaw.subagent.account_id": evt.accountId,
|
|
1763
|
+
"error.type": isError ? evt.outcome ?? "error" : void 0,
|
|
1764
|
+
"error.message:gated": evt.error
|
|
757
1765
|
});
|
|
758
|
-
|
|
759
|
-
|
|
1766
|
+
this.pendingSubagents.delete(childRunId);
|
|
1767
|
+
this.emitSubagentSpan(span, pending.parentRunId);
|
|
1768
|
+
}
|
|
1769
|
+
ensureRun(runId, ctx) {
|
|
1770
|
+
const existing = this.runs.get(runId);
|
|
1771
|
+
if (existing) {
|
|
1772
|
+
this.mergeCtx(existing, ctx);
|
|
1773
|
+
return existing;
|
|
1774
|
+
}
|
|
1775
|
+
this.evictStale();
|
|
1776
|
+
const startedAt = this.now();
|
|
1777
|
+
const link = this.subagentLinks.get(runId);
|
|
1778
|
+
const traceId = link?.traceId ?? hashHex(runId, 32);
|
|
1779
|
+
const root = {
|
|
1780
|
+
spanId: hashHex(`${runId}:interaction`, 16),
|
|
1781
|
+
traceId,
|
|
1782
|
+
parentSpanId: link?.parentSpanId ?? "",
|
|
1783
|
+
name: "interaction",
|
|
1784
|
+
kind: KIND_INTERNAL,
|
|
1785
|
+
startMs: startedAt,
|
|
1786
|
+
endMs: void 0,
|
|
1787
|
+
attrs: {
|
|
1788
|
+
"gen_ai.operation.name": "invoke_agent",
|
|
1789
|
+
"openclaw.run.id": runId
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
const run = {
|
|
1793
|
+
runId,
|
|
1794
|
+
ctx: {
|
|
1795
|
+
...ctx,
|
|
1796
|
+
runId
|
|
1797
|
+
},
|
|
1798
|
+
traceId,
|
|
1799
|
+
root,
|
|
1800
|
+
startedAt,
|
|
1801
|
+
history: [],
|
|
1802
|
+
inputMessages: [],
|
|
1803
|
+
inheritedHistory: [],
|
|
1804
|
+
historySource: "none",
|
|
1805
|
+
deltas: [],
|
|
1806
|
+
thinking: [],
|
|
1807
|
+
thinkingChars: 0,
|
|
1808
|
+
prompt: void 0,
|
|
1809
|
+
systemPrompt: void 0,
|
|
1810
|
+
toolDefinitions: void 0,
|
|
1811
|
+
llmCalls: [],
|
|
1812
|
+
openModelCalls: /* @__PURE__ */ new Map(),
|
|
1813
|
+
openToolCalls: /* @__PURE__ */ new Map(),
|
|
1814
|
+
openCompaction: void 0,
|
|
1815
|
+
closed: [],
|
|
1816
|
+
lastSpawnToolSpanId: void 0,
|
|
1817
|
+
subagentIds: [],
|
|
1818
|
+
sender: void 0,
|
|
1819
|
+
cron: void 0,
|
|
1820
|
+
link,
|
|
1821
|
+
aggregateUsage: {},
|
|
1822
|
+
output: void 0,
|
|
1823
|
+
endEvent: void 0,
|
|
1824
|
+
outputSeen: false,
|
|
1825
|
+
finalized: false,
|
|
1826
|
+
cancelGrace: void 0
|
|
1827
|
+
};
|
|
1828
|
+
this.runs.set(runId, run);
|
|
1829
|
+
this.indexSession(run);
|
|
1830
|
+
this.resolveSender(run);
|
|
1831
|
+
this.resolveCron(run);
|
|
1832
|
+
return run;
|
|
1833
|
+
}
|
|
1834
|
+
mergeCtx(run, ctx) {
|
|
1835
|
+
let changed = false;
|
|
1836
|
+
for (const [key, value] of Object.entries(ctx)) {
|
|
1837
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
1838
|
+
if (run.ctx[key] === void 0) {
|
|
1839
|
+
run.ctx[key] = value;
|
|
1840
|
+
changed = true;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
if (!changed) return;
|
|
1844
|
+
this.indexSession(run);
|
|
1845
|
+
this.resolveSender(run);
|
|
1846
|
+
this.resolveCron(run);
|
|
1847
|
+
}
|
|
1848
|
+
indexSession(run) {
|
|
1849
|
+
const key = run.ctx.sessionKey;
|
|
1850
|
+
if (!key) return;
|
|
1851
|
+
if (run.ctx.sessionId) remember(this.sessionIds, key, run.ctx.sessionId, SESSION_INDEX_MAX);
|
|
1852
|
+
const list = this.runsBySession.get(key) ?? [];
|
|
1853
|
+
if (!list.includes(run.runId)) {
|
|
1854
|
+
list.push(run.runId);
|
|
1855
|
+
this.runsBySession.set(key, list);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
resolveSender(run) {
|
|
1859
|
+
if (run.sender?.name) return;
|
|
1860
|
+
const id = run.ctx.senderId;
|
|
1861
|
+
if (!id) return;
|
|
1862
|
+
const cached = (run.ctx.sessionKey ? this.senders.get(run.ctx.sessionKey)?.get(id) : void 0) ?? this.sendersById.get(id);
|
|
1863
|
+
if (cached?.name) {
|
|
1864
|
+
run.sender = cached;
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
const fromPrompt = senderFromPrompt(run.prompt);
|
|
1868
|
+
run.sender = fromPrompt && fromPrompt.id === id ? fromPrompt : cached ?? { id };
|
|
1869
|
+
if (run.sender.name) this.sendersById.set(id, run.sender);
|
|
1870
|
+
}
|
|
1871
|
+
resolveCron(run) {
|
|
1872
|
+
if (run.cron?.name || run.ctx.trigger !== "cron") return;
|
|
1873
|
+
const fromKey = cronJobFromSessionKey(run.ctx.sessionKey);
|
|
1874
|
+
const bySession = run.ctx.sessionKey ? this.cronStarted.get(`session:${run.ctx.sessionKey}`) : void 0;
|
|
1875
|
+
const job = fromKey ? this.cronStarted.get(`job:${fromKey}`) ?? { id: fromKey } : bySession ?? this.cronStarted.get(run.ctx.agentId ?? "main");
|
|
1876
|
+
if (!job) return;
|
|
1877
|
+
run.cron = job.name ? job : {
|
|
1878
|
+
id: job.id,
|
|
1879
|
+
name: cronNameFromPrompt(run.prompt, job.id)
|
|
1880
|
+
};
|
|
760
1881
|
}
|
|
761
1882
|
/**
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
1883
|
+
* History the harness withheld. The Codex harness passes an empty history
|
|
1884
|
+
* and only this turn's transcript, so the session is rebuilt from the turns
|
|
1885
|
+
* this process has seen, or from OpenClaw's transcript mirror on a cold start.
|
|
765
1886
|
*/
|
|
766
|
-
|
|
767
|
-
const
|
|
768
|
-
if (!
|
|
769
|
-
const
|
|
770
|
-
if (
|
|
771
|
-
|
|
1887
|
+
inheritHistory(run, prompt) {
|
|
1888
|
+
const sessionId = run.ctx.sessionId;
|
|
1889
|
+
if (!sessionId) return;
|
|
1890
|
+
const remembered = this.sessionHistory.get(sessionId);
|
|
1891
|
+
if (remembered) {
|
|
1892
|
+
run.inheritedHistory = [...remembered];
|
|
1893
|
+
run.historySource = "memory";
|
|
772
1894
|
return;
|
|
773
1895
|
}
|
|
774
|
-
const
|
|
775
|
-
run.
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
"
|
|
781
|
-
|
|
782
|
-
|
|
1896
|
+
const stateDir = this.stateDir ?? stateDirFromWorkspace(run.ctx.workspaceDir);
|
|
1897
|
+
const agentId = run.ctx.agentId;
|
|
1898
|
+
if (!stateDir || !agentId) return;
|
|
1899
|
+
const fromDatabase = readSessionTranscriptFromDatabase(agentDatabasePath(stateDir, agentId), sessionId, this.readTranscriptRows);
|
|
1900
|
+
const stored = fromDatabase && fromDatabase.length > 0 ? {
|
|
1901
|
+
messages: fromDatabase,
|
|
1902
|
+
source: "sqlite"
|
|
1903
|
+
} : {
|
|
1904
|
+
messages: readSessionTranscript(sessionTranscriptPath(stateDir, agentId, sessionId), this.readTranscript),
|
|
1905
|
+
source: "file"
|
|
1906
|
+
};
|
|
1907
|
+
if (!stored.messages) return;
|
|
1908
|
+
run.inheritedHistory = withoutCurrentPrompt(stored.messages, prompt, run.startedAt);
|
|
1909
|
+
run.historySource = stored.source;
|
|
1910
|
+
}
|
|
1911
|
+
openRunForSession(sessionKey) {
|
|
1912
|
+
if (!sessionKey) return void 0;
|
|
1913
|
+
const ids = this.runsBySession.get(sessionKey);
|
|
1914
|
+
if (!ids) return void 0;
|
|
1915
|
+
for (let i = ids.length - 1; i >= 0; i--) {
|
|
1916
|
+
const run = this.runs.get(ids[i]);
|
|
1917
|
+
if (run && !run.finalized) return run;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
recordMemorySnapshot(run) {
|
|
1921
|
+
if (!this.memoryEnabled) return;
|
|
1922
|
+
const key = run.ctx.sessionKey ?? run.ctx.sessionId;
|
|
1923
|
+
if (!key || this.memorySnapshotSessions.has(key)) return;
|
|
1924
|
+
this.memorySnapshotSessions.add(key);
|
|
1925
|
+
const snapshot = memorySnapshot(run.ctx.workspaceDir, run.ctx.agentId, this.readFile);
|
|
1926
|
+
if (!snapshot) return;
|
|
1927
|
+
const span = this.memorySpan(run, snapshot, void 0);
|
|
1928
|
+
span.attrs["openclaw.memory.source"] = "session_snapshot";
|
|
1929
|
+
run.closed.push(span);
|
|
1930
|
+
}
|
|
1931
|
+
memorySpan(run, memory, parent) {
|
|
1932
|
+
const at = this.now();
|
|
1933
|
+
return {
|
|
1934
|
+
spanId: hashHex(`${run.runId}:memory:${memory.operation}:${memory.recordId ?? ""}:${at}:${run.closed.length}`, 16),
|
|
1935
|
+
traceId: run.traceId,
|
|
1936
|
+
parentSpanId: parent?.spanId ?? run.root.spanId,
|
|
1937
|
+
name: memory.operation,
|
|
1938
|
+
kind: KIND_CLIENT,
|
|
1939
|
+
startMs: parent?.startMs ?? at,
|
|
1940
|
+
endMs: parent?.endMs ?? at,
|
|
1941
|
+
outcome: "ok",
|
|
1942
|
+
attrs: {
|
|
1943
|
+
"gen_ai.operation.name": memory.operation,
|
|
1944
|
+
"gen_ai.provider.name": "openclaw",
|
|
1945
|
+
"gen_ai.memory.store.id": memory.storeId,
|
|
1946
|
+
"gen_ai.memory.record.id": memory.recordId,
|
|
1947
|
+
"gen_ai.memory.record.count": memory.records.length,
|
|
1948
|
+
"gen_ai.memory.query.text:gated": this.memoryContent ? memory.queryText : void 0,
|
|
1949
|
+
"gen_ai.memory.records:gated": this.memoryContent && memory.records.length > 0 ? memory.records : void 0,
|
|
1950
|
+
"openclaw.memory.body_unavailable": memory.bodyUnavailable ? true : void 0
|
|
1951
|
+
}
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
applyLink(run) {
|
|
1955
|
+
const link = this.subagentLinks.get(run.runId);
|
|
1956
|
+
if (!link) return;
|
|
1957
|
+
run.link = link;
|
|
1958
|
+
run.traceId = link.traceId;
|
|
1959
|
+
run.root.parentSpanId = link.parentSpanId;
|
|
1960
|
+
for (const span of [
|
|
1961
|
+
run.root,
|
|
1962
|
+
...run.llmCalls,
|
|
1963
|
+
...run.closed,
|
|
1964
|
+
...run.openModelCalls.values(),
|
|
1965
|
+
...run.openToolCalls.values()
|
|
1966
|
+
]) span.traceId = link.traceId;
|
|
1967
|
+
if (run.openCompaction) run.openCompaction.traceId = link.traceId;
|
|
1968
|
+
}
|
|
1969
|
+
finalize(run) {
|
|
1970
|
+
if (run.finalized) return;
|
|
1971
|
+
run.finalized = true;
|
|
1972
|
+
run.cancelGrace?.();
|
|
1973
|
+
run.cancelGrace = void 0;
|
|
1974
|
+
this.applyLink(run);
|
|
1975
|
+
const evt = run.endEvent;
|
|
1976
|
+
const now = this.now();
|
|
1977
|
+
const endMs = now;
|
|
1978
|
+
const startMs = evt?.durationMs !== void 0 ? Math.min(run.startedAt, endMs - evt.durationMs) : run.startedAt;
|
|
1979
|
+
run.root.startMs = startMs;
|
|
1980
|
+
run.root.endMs = endMs;
|
|
1981
|
+
const transcript = evt?.messages ?? [];
|
|
1982
|
+
this.resolveSender(run);
|
|
1983
|
+
this.resolveCron(run);
|
|
1984
|
+
const assistants = this.attributeCalls(run, transcript, startMs);
|
|
1985
|
+
this.applyTimeToFirstToken(run);
|
|
1986
|
+
this.applyStreamedReasoning(run);
|
|
1987
|
+
this.rememberHistory(run, transcript, startMs);
|
|
1988
|
+
const lastAssistant = assistants[assistants.length - 1]?.message ?? run.output?.lastAssistant;
|
|
1989
|
+
const outputMessage = lastAssistant !== void 0 ? normalizeMessage(lastAssistant) : void 0;
|
|
1990
|
+
const success = evt ? evt.success : false;
|
|
1991
|
+
run.root.outcome = success ? "ok" : "error";
|
|
1992
|
+
run.root.errorMessage = evt?.error ?? (evt ? void 0 : "abandoned");
|
|
1993
|
+
Object.assign(run.root.attrs, {
|
|
1994
|
+
"user_prompt:gated": run.prompt,
|
|
1995
|
+
"gen_ai.input.messages:gated": run.inputMessages.length > 0 ? run.inputMessages : void 0,
|
|
1996
|
+
"gen_ai.output.messages:gated": outputMessage ? [{
|
|
1997
|
+
...outputMessage,
|
|
1998
|
+
role: "assistant"
|
|
1999
|
+
}] : void 0,
|
|
2000
|
+
"gen_ai.system_instructions:gated": run.systemPrompt ? systemInstructionsParts(run.systemPrompt) : void 0,
|
|
2001
|
+
"openclaw.run.success": success,
|
|
2002
|
+
"openclaw.duration_ms": evt?.durationMs,
|
|
2003
|
+
"openclaw.outcome": evt ? success ? "completed" : "error" : "abandoned",
|
|
2004
|
+
"error.type": success ? void 0 : evt ? "run_error" : "abandoned",
|
|
2005
|
+
"error.message:gated": evt?.error,
|
|
2006
|
+
"openclaw.llm_calls": run.llmCalls.length,
|
|
2007
|
+
"openclaw.tool_calls": run.closed.filter((s) => s.name.startsWith("tool_call:")).length,
|
|
2008
|
+
"interaction.kind": interactionKind(run),
|
|
2009
|
+
"interaction.duration_ms": endMs - startMs,
|
|
2010
|
+
"openclaw.history.source": run.historySource,
|
|
2011
|
+
"openclaw.history.messages": run.inheritedHistory.length
|
|
783
2012
|
});
|
|
784
|
-
for (const span of run.openModelCalls.values())
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
run.
|
|
2013
|
+
for (const span of run.openModelCalls.values()) abandon(span, now);
|
|
2014
|
+
for (const span of run.openToolCalls.values()) abandon(span, now);
|
|
2015
|
+
if (run.openCompaction) abandon(run.openCompaction, now);
|
|
2016
|
+
const spans = [
|
|
2017
|
+
run.root,
|
|
2018
|
+
...run.llmCalls,
|
|
2019
|
+
...run.closed,
|
|
2020
|
+
...run.openModelCalls.values(),
|
|
2021
|
+
...run.openToolCalls.values()
|
|
2022
|
+
];
|
|
2023
|
+
if (run.openCompaction) spans.push(run.openCompaction);
|
|
2024
|
+
const common = this.commonAttrs(run);
|
|
2025
|
+
for (const span of spans) for (const [k, v] of Object.entries(common)) if (span.attrs[k] === void 0) span.attrs[k] = v;
|
|
2026
|
+
this.runs.delete(run.runId);
|
|
2027
|
+
this.subagentLinks.delete(run.runId);
|
|
2028
|
+
const key = run.ctx.sessionKey;
|
|
2029
|
+
if (key) {
|
|
2030
|
+
const ids = (this.runsBySession.get(key) ?? []).filter((id) => id !== run.runId);
|
|
2031
|
+
if (ids.length === 0) this.runsBySession.delete(key);
|
|
2032
|
+
else this.runsBySession.set(key, ids);
|
|
789
2033
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
2034
|
+
this.log(`run ${run.runId}: ${spans.length} spans, ${run.llmCalls.length} calls, history=${run.historySource}:${run.inheritedHistory.length}, transcript=${transcript.length}, deltas=${run.deltas.length}, thinking=${run.thinkingChars}`);
|
|
2035
|
+
this.emit({
|
|
2036
|
+
runId: run.runId,
|
|
2037
|
+
spans
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
/**
|
|
2041
|
+
* Keep the session's conversation for the next turn of a harness that does
|
|
2042
|
+
* not pass history. A harness that passes the whole session on `agent_end`
|
|
2043
|
+
* replaces the remembered copy outright.
|
|
2044
|
+
*/
|
|
2045
|
+
rememberHistory(run, transcript, runStartMs) {
|
|
2046
|
+
const sessionId = run.ctx.sessionId;
|
|
2047
|
+
if (!sessionId || transcript.length === 0) return;
|
|
2048
|
+
if (run.historySource === "harness") {
|
|
2049
|
+
this.sessionHistory.replace(sessionId, normalizeMessages(transcript));
|
|
2050
|
+
return;
|
|
795
2051
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
2052
|
+
const turn = transcript.filter((m) => {
|
|
2053
|
+
const ts = m.timestamp;
|
|
2054
|
+
return typeof ts !== "number" || ts >= runStartMs - 5e3;
|
|
2055
|
+
});
|
|
2056
|
+
this.sessionHistory.append(sessionId, run.inheritedHistory, normalizeMessages(turn));
|
|
2057
|
+
}
|
|
2058
|
+
/** Reasoning streamed during a call's window, when its transcript message carries none. */
|
|
2059
|
+
applyStreamedReasoning(run) {
|
|
2060
|
+
if (run.thinking.length === 0) return;
|
|
2061
|
+
for (const call of run.llmCalls) {
|
|
2062
|
+
const endMs = call.endMs ?? call.startMs;
|
|
2063
|
+
const text = run.thinking.filter((t) => t.ts > call.startMs - 1e3 && t.ts <= endMs + 1e3).map((t) => t.text).join("").trim();
|
|
2064
|
+
if (text.length === 0) continue;
|
|
2065
|
+
const message = call.attrs["gen_ai.output.messages:gated"]?.[0];
|
|
2066
|
+
if (!message || message.parts.some((p) => p.type === "reasoning")) continue;
|
|
2067
|
+
message.parts.unshift({
|
|
2068
|
+
type: "reasoning",
|
|
2069
|
+
content: text
|
|
2070
|
+
});
|
|
2071
|
+
call.attrs["openclaw.reasoning.source"] = "stream";
|
|
801
2072
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
2073
|
+
}
|
|
2074
|
+
/** The first streamed delta inside a call's window, when the harness reported no TTFB. */
|
|
2075
|
+
applyTimeToFirstToken(run) {
|
|
2076
|
+
if (run.deltas.length === 0) return;
|
|
2077
|
+
for (const call of run.llmCalls) {
|
|
2078
|
+
if (call.attrs["gen_ai.server.time_to_first_token"] !== void 0) continue;
|
|
2079
|
+
const endMs = call.endMs ?? call.startMs;
|
|
2080
|
+
const first = run.deltas.find((ts) => ts > call.startMs && ts <= endMs + 1e3);
|
|
2081
|
+
if (first === void 0) continue;
|
|
2082
|
+
call.attrs["gen_ai.server.time_to_first_token"] = (first - call.startMs) * 1e6;
|
|
2083
|
+
call.attrs["openclaw.ttft.source"] = "stream";
|
|
807
2084
|
}
|
|
808
|
-
const spans = [run.agent, ...run.closed];
|
|
809
|
-
this.runs.delete(runId);
|
|
810
|
-
this.subagentLinks.delete(runId);
|
|
811
|
-
return {
|
|
812
|
-
runId,
|
|
813
|
-
spans
|
|
814
|
-
};
|
|
815
2085
|
}
|
|
816
|
-
/**
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
2086
|
+
/**
|
|
2087
|
+
* Per-call usage, cost and output come from the transcript's assistant
|
|
2088
|
+
* messages, each of which is one provider response. Calls and messages are
|
|
2089
|
+
* both chronological, so a message is matched to the call whose window
|
|
2090
|
+
* contains its timestamp, falling back to order when the counts line up.
|
|
2091
|
+
* A harness that never fires the per-call hooks still leaves its responses
|
|
2092
|
+
* in the transcript, so those become `llm_request` spans of their own.
|
|
2093
|
+
*/
|
|
2094
|
+
attributeCalls(run, transcript, runStartMs) {
|
|
2095
|
+
const assistants = [];
|
|
2096
|
+
transcript.forEach((m, index) => {
|
|
2097
|
+
if (!isTranscriptAssistant(m)) return;
|
|
2098
|
+
const ts = typeof m.timestamp === "number" ? m.timestamp : void 0;
|
|
2099
|
+
if (ts !== void 0 && ts < runStartMs - 5e3) return;
|
|
2100
|
+
assistants.push({
|
|
2101
|
+
message: m,
|
|
2102
|
+
index
|
|
2103
|
+
});
|
|
2104
|
+
});
|
|
2105
|
+
if (run.llmCalls.length === 0) {
|
|
2106
|
+
for (const [i, entry] of assistants.entries()) run.llmCalls.push(this.synthesizeCall(run, entry, i, transcript, runStartMs));
|
|
2107
|
+
this.applyAggregateIfUnreported(run);
|
|
2108
|
+
return assistants;
|
|
2109
|
+
}
|
|
2110
|
+
const unmatched = [...assistants];
|
|
2111
|
+
let matchedAny = false;
|
|
2112
|
+
for (const [callIndex, call] of run.llmCalls.entries()) {
|
|
2113
|
+
const remainingCalls = run.llmCalls.length - callIndex;
|
|
2114
|
+
let picked = unmatched.findIndex(({ message }) => {
|
|
2115
|
+
const ts = message.timestamp;
|
|
2116
|
+
return ts !== void 0 && ts >= call.startMs - 2e3 && ts <= (call.endMs ?? call.startMs) + 2e3;
|
|
2117
|
+
});
|
|
2118
|
+
if (picked < 0 && unmatched.length === remainingCalls) picked = 0;
|
|
2119
|
+
if (picked < 0) continue;
|
|
2120
|
+
const [entry] = unmatched.splice(picked, 1);
|
|
2121
|
+
if (!entry) continue;
|
|
2122
|
+
matchedAny = true;
|
|
2123
|
+
this.enrichCall(call, entry.message, this.inputBefore(run, transcript, entry.index));
|
|
2124
|
+
}
|
|
2125
|
+
if (!matchedAny) {
|
|
2126
|
+
const last = run.llmCalls[run.llmCalls.length - 1];
|
|
2127
|
+
if (last) Object.assign(last.attrs, run.aggregateUsage, { "openclaw.usage.scope": "attempt" });
|
|
2128
|
+
}
|
|
2129
|
+
this.applyAggregateIfUnreported(run);
|
|
2130
|
+
return assistants;
|
|
820
2131
|
}
|
|
821
|
-
/**
|
|
822
|
-
|
|
823
|
-
return
|
|
2132
|
+
/** Everything the model saw before a given transcript index, including history the harness withheld. */
|
|
2133
|
+
inputBefore(run, transcript, index) {
|
|
2134
|
+
return [...run.inheritedHistory, ...transcript.slice(0, index)];
|
|
824
2135
|
}
|
|
825
2136
|
/**
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
829
|
-
*
|
|
830
|
-
* Two passes: TTL eviction (anything older than `SUBAGENT_LINK_TTL_MS`),
|
|
831
|
-
* then a hard size cap (when we're past `SUBAGENT_LINK_MAX`, drop the
|
|
832
|
-
* oldest until we're under).
|
|
2137
|
+
* A harness that reports usage only on the turn's final message leaves a
|
|
2138
|
+
* turn ending in a tool call with no usage at all; the attempt aggregate
|
|
2139
|
+
* from `llm_output` then goes on the last call.
|
|
833
2140
|
*/
|
|
834
|
-
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
if (
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
2141
|
+
applyAggregateIfUnreported(run) {
|
|
2142
|
+
const last = run.llmCalls[run.llmCalls.length - 1];
|
|
2143
|
+
if (!last || run.aggregateUsage["gen_ai.usage.total_tokens"] === void 0) return;
|
|
2144
|
+
if (run.llmCalls.some((call) => Number(call.attrs["gen_ai.usage.total_tokens"] ?? 0) > 0)) return;
|
|
2145
|
+
Object.assign(last.attrs, run.aggregateUsage, { "openclaw.usage.scope": "attempt" });
|
|
2146
|
+
}
|
|
2147
|
+
synthesizeCall(run, entry, index, transcript, runStartMs) {
|
|
2148
|
+
const endMs = entry.message.timestamp ?? run.startedAt;
|
|
2149
|
+
const previous = transcript[entry.index - 1];
|
|
2150
|
+
const previousTs = typeof previous?.timestamp === "number" ? previous.timestamp : void 0;
|
|
2151
|
+
const startMs = previousTs !== void 0 && previousTs <= endMs ? Math.max(previousTs, runStartMs) : Math.min(runStartMs, endMs);
|
|
2152
|
+
const span = {
|
|
2153
|
+
spanId: hashHex(`${run.runId}:llm_request:transcript:${index}`, 16),
|
|
2154
|
+
traceId: run.traceId,
|
|
2155
|
+
parentSpanId: run.root.spanId,
|
|
2156
|
+
name: "llm_request",
|
|
2157
|
+
kind: KIND_INTERNAL,
|
|
2158
|
+
startMs,
|
|
2159
|
+
endMs,
|
|
2160
|
+
outcome: "ok",
|
|
2161
|
+
attrs: {
|
|
2162
|
+
"gen_ai.operation.name": "chat",
|
|
2163
|
+
"gen_ai.provider.name": entry.message.provider ?? run.root.attrs["openclaw.provider"],
|
|
2164
|
+
"gen_ai.system": entry.message.provider ?? run.root.attrs["openclaw.provider"],
|
|
2165
|
+
"gen_ai.request.model": entry.message.model ?? run.root.attrs["gen_ai.request.model"],
|
|
2166
|
+
"llm_request.call_index": index,
|
|
2167
|
+
"openclaw.call.source": "transcript",
|
|
2168
|
+
"gen_ai.system_instructions:gated": run.systemPrompt ? systemInstructionsParts(run.systemPrompt) : void 0,
|
|
2169
|
+
"gen_ai.tool.definitions:gated": run.toolDefinitions
|
|
2170
|
+
}
|
|
2171
|
+
};
|
|
2172
|
+
this.enrichCall(span, entry.message, this.inputBefore(run, transcript, entry.index));
|
|
2173
|
+
return span;
|
|
2174
|
+
}
|
|
2175
|
+
enrichCall(call, message, before) {
|
|
2176
|
+
const output = normalizeMessage(message);
|
|
2177
|
+
const usage = message.usage;
|
|
2178
|
+
const reason = finishReason(message.stopReason);
|
|
2179
|
+
Object.assign(call.attrs, {
|
|
2180
|
+
"gen_ai.input.messages:gated": before.length > 0 ? normalizeMessages(before) : call.attrs["gen_ai.input.messages:gated"],
|
|
2181
|
+
"gen_ai.output.messages:gated": output ? [{
|
|
2182
|
+
...output,
|
|
2183
|
+
role: "assistant"
|
|
2184
|
+
}] : void 0,
|
|
2185
|
+
"gen_ai.response.model": message.responseModel ?? message.model,
|
|
2186
|
+
"gen_ai.response.id": message.responseId,
|
|
2187
|
+
"gen_ai.response.finish_reasons": reason ? [reason] : void 0,
|
|
2188
|
+
"openclaw.stop_reason": message.stopReason,
|
|
2189
|
+
...usageAttrsFromTranscript(usage)
|
|
2190
|
+
});
|
|
2191
|
+
if (message.stopReason === "error" && message.errorMessage) {
|
|
2192
|
+
call.outcome = "error";
|
|
2193
|
+
call.errorMessage = message.errorMessage;
|
|
2194
|
+
call.attrs["error.type"] = call.attrs["error.type"] ?? "provider_error";
|
|
2195
|
+
call.attrs["error.message:gated"] = message.errorMessage;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
commonAttrs(run) {
|
|
2199
|
+
const sessionId = run.link?.sessionId ?? run.ctx.sessionId;
|
|
2200
|
+
const enrichment = deriveEnrichment({
|
|
2201
|
+
ctx: run.ctx,
|
|
2202
|
+
sender: run.sender,
|
|
2203
|
+
cron: run.cron,
|
|
2204
|
+
subagentIds: run.subagentIds,
|
|
2205
|
+
pluginVersion: this.pluginVersion
|
|
2206
|
+
}, {
|
|
2207
|
+
tags: this.operatorTags,
|
|
2208
|
+
metadata: this.operatorMetadata
|
|
2209
|
+
});
|
|
2210
|
+
return {
|
|
2211
|
+
...sessionAttrs(sessionId),
|
|
2212
|
+
"openclaw.session.id": run.ctx.sessionId,
|
|
2213
|
+
"openclaw.session.key": run.ctx.sessionKey,
|
|
2214
|
+
"openclaw.run.id": run.runId,
|
|
2215
|
+
"openclaw.agent.id": run.ctx.agentId,
|
|
2216
|
+
"gen_ai.agent.name": run.link?.agentName ?? run.ctx.agentId,
|
|
2217
|
+
"openclaw.workspace.dir": run.ctx.workspaceDir,
|
|
2218
|
+
"openclaw.channel": run.ctx.channel,
|
|
2219
|
+
"openclaw.channel.id": run.ctx.channelId,
|
|
2220
|
+
"openclaw.message.provider": run.ctx.messageProvider,
|
|
2221
|
+
"openclaw.trigger": run.ctx.trigger,
|
|
2222
|
+
"openclaw.cron.job.id": run.cron?.id,
|
|
2223
|
+
"openclaw.parent.run.id": run.link?.parentRunId || void 0,
|
|
2224
|
+
"user.id": run.sender?.id,
|
|
2225
|
+
"latitude.tags": enrichment.tags,
|
|
2226
|
+
"latitude.metadata": enrichment.metadata
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
emitSubagentSpan(span, parentRunId) {
|
|
2230
|
+
const parent = this.runs.get(parentRunId);
|
|
2231
|
+
if (parent && !parent.finalized) {
|
|
2232
|
+
parent.closed.push(span);
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
if (span.attrs["latitude.tags"] === void 0) {
|
|
2236
|
+
const enrichment = deriveEnrichment({
|
|
2237
|
+
ctx: {},
|
|
2238
|
+
subagentIds: [],
|
|
2239
|
+
pluginVersion: this.pluginVersion
|
|
2240
|
+
}, {
|
|
2241
|
+
tags: this.operatorTags,
|
|
2242
|
+
metadata: this.operatorMetadata
|
|
2243
|
+
});
|
|
2244
|
+
span.attrs["latitude.tags"] = enrichment.tags;
|
|
2245
|
+
span.attrs["latitude.metadata"] = enrichment.metadata;
|
|
2246
|
+
}
|
|
2247
|
+
this.emit({
|
|
2248
|
+
runId: `subagent:${span.spanId}`,
|
|
2249
|
+
spans: [span]
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
evictStale() {
|
|
2253
|
+
const now = this.now();
|
|
2254
|
+
for (const [runId, link] of this.subagentLinks) if (now - link.createdAt > LINK_TTL_MS) this.subagentLinks.delete(runId);
|
|
2255
|
+
if (this.subagentLinks.size > LINK_MAX) {
|
|
2256
|
+
const sorted = Array.from(this.subagentLinks.entries()).sort((a, b) => a[1].createdAt - b[1].createdAt);
|
|
2257
|
+
for (const [runId] of sorted.slice(0, this.subagentLinks.size - LINK_MAX)) this.subagentLinks.delete(runId);
|
|
2258
|
+
}
|
|
2259
|
+
for (const [runId, pending] of this.pendingSubagents) if (now - pending.createdAt > LINK_TTL_MS) {
|
|
2260
|
+
abandon(pending.span, now);
|
|
2261
|
+
pending.span.attrs["openclaw.subagent.outcome"] = "abandoned";
|
|
2262
|
+
this.pendingSubagents.delete(runId);
|
|
2263
|
+
this.emitSubagentSpan(pending.span, pending.parentRunId);
|
|
2264
|
+
}
|
|
2265
|
+
for (const run of Array.from(this.runs.values())) if (now - run.startedAt > RUN_TTL_MS) {
|
|
2266
|
+
this.log(`run ${run.runId} never ended; exporting as abandoned`);
|
|
2267
|
+
this.finalize(run);
|
|
843
2268
|
}
|
|
844
2269
|
}
|
|
845
2270
|
findOpenToolCallByName(run, toolName) {
|
|
@@ -847,119 +2272,202 @@ var SpanBuilder = class {
|
|
|
847
2272
|
const entries = Array.from(run.openToolCalls.entries());
|
|
848
2273
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
849
2274
|
const entry = entries[i];
|
|
850
|
-
if (
|
|
851
|
-
const [id, span] = entry;
|
|
852
|
-
if (span.name === target) return id;
|
|
2275
|
+
if (entry && entry[1].name === target) return entry[0];
|
|
853
2276
|
}
|
|
854
2277
|
}
|
|
855
2278
|
};
|
|
856
|
-
function
|
|
2279
|
+
function toolCtxToAgentCtx(ctx, runId) {
|
|
857
2280
|
return {
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
"openclaw.channel.id": ctx.channelId,
|
|
867
|
-
"openclaw.cron.job.id": ctx.jobId,
|
|
868
|
-
"openclaw.model.provider.id": ctx.modelProviderId,
|
|
869
|
-
"openclaw.model.id": ctx.modelId
|
|
2281
|
+
runId,
|
|
2282
|
+
agentId: ctx.agentId,
|
|
2283
|
+
sessionKey: ctx.sessionKey,
|
|
2284
|
+
sessionId: ctx.sessionId,
|
|
2285
|
+
channelId: ctx.channelId,
|
|
2286
|
+
channel: ctx.requester?.channel,
|
|
2287
|
+
accountId: ctx.requester?.accountId,
|
|
2288
|
+
senderId: ctx.requester?.senderId
|
|
870
2289
|
};
|
|
871
2290
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
* openclaw-specific code path. Emitted on every span, not just `agent`,
|
|
878
|
-
* so child spans (model_call / tool_call / etc.) inherit the same grouping.
|
|
879
|
-
*/
|
|
880
|
-
function sessionAttrs(ctx) {
|
|
881
|
-
if (!ctx.sessionId) return {};
|
|
882
|
-
return {
|
|
883
|
-
"session.id": ctx.sessionId,
|
|
884
|
-
"gen_ai.session.id": ctx.sessionId
|
|
885
|
-
};
|
|
2291
|
+
function interactionKind(run) {
|
|
2292
|
+
if (run.link) return "subagent";
|
|
2293
|
+
if (run.runId.startsWith("announce:")) return "announce";
|
|
2294
|
+
if (run.ctx.trigger === "cron") return "cron";
|
|
2295
|
+
return run.ctx.trigger ?? "user";
|
|
886
2296
|
}
|
|
887
|
-
/**
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
*
|
|
892
|
-
* - `latitude.tags` is a JSON-encoded string array (`fromJsonStringArray`
|
|
893
|
-
* in domain/spans/src/otlp/resolvers/enrichment.ts).
|
|
894
|
-
* - `latitude.metadata` is a JSON-encoded string object (`fromJsonString`).
|
|
895
|
-
*
|
|
896
|
-
* Tags = the agent id, the channel id, and the trigger. When trigger is
|
|
897
|
-
* `cron`, the tag becomes `cron:<jobId>` so dashboards can pivot on the
|
|
898
|
-
* specific cron job. Each tag is conditionally included so absent ctx
|
|
899
|
-
* fields don't produce empty entries.
|
|
900
|
-
*
|
|
901
|
-
* Metadata = every ctx field that's set, namespaced under `openclaw.*` so
|
|
902
|
-
* it can't collide with metadata keys other plugins might emit.
|
|
903
|
-
*/
|
|
904
|
-
function latitudeAttrs(ctx) {
|
|
905
|
-
const tags = [];
|
|
906
|
-
if (ctx.agentId) tags.push(ctx.agentId);
|
|
907
|
-
if (ctx.channelId) tags.push(ctx.channelId);
|
|
908
|
-
if (ctx.trigger) tags.push(ctx.trigger === "cron" && ctx.jobId ? `cron:${ctx.jobId}` : ctx.trigger);
|
|
909
|
-
const metadata = {};
|
|
910
|
-
if (ctx.runId) metadata["openclaw.run.id"] = ctx.runId;
|
|
911
|
-
if (ctx.sessionId) metadata["openclaw.session.id"] = ctx.sessionId;
|
|
912
|
-
if (ctx.sessionKey) metadata["openclaw.session.key"] = ctx.sessionKey;
|
|
913
|
-
if (ctx.agentId) metadata["openclaw.agent.id"] = ctx.agentId;
|
|
914
|
-
if (ctx.workspaceDir) metadata["openclaw.workspace.dir"] = ctx.workspaceDir;
|
|
915
|
-
if (ctx.channelId) metadata["openclaw.channel.id"] = ctx.channelId;
|
|
916
|
-
if (ctx.messageProvider) metadata["openclaw.message.provider"] = ctx.messageProvider;
|
|
917
|
-
if (ctx.trigger) metadata["openclaw.trigger"] = ctx.trigger;
|
|
918
|
-
if (ctx.jobId) metadata["openclaw.cron.job.id"] = ctx.jobId;
|
|
919
|
-
if (ctx.modelProviderId) metadata["openclaw.model.provider.id"] = ctx.modelProviderId;
|
|
920
|
-
if (ctx.modelId) metadata["openclaw.model.id"] = ctx.modelId;
|
|
921
|
-
return {
|
|
922
|
-
"latitude.tags": tags.length > 0 ? tags : void 0,
|
|
923
|
-
"latitude.metadata": Object.keys(metadata).length > 0 ? metadata : void 0
|
|
924
|
-
};
|
|
2297
|
+
/** Cron prompts open with `[cron:<jobId> <job name>]`, the only place the name reaches a run. */
|
|
2298
|
+
function cronNameFromPrompt(prompt, jobId) {
|
|
2299
|
+
if (!prompt) return void 0;
|
|
2300
|
+
return new RegExp(`^\\[cron:${jobId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} ([^\\]]+)\\]`).exec(prompt)?.[1]?.trim() || void 0;
|
|
925
2301
|
}
|
|
926
|
-
|
|
927
|
-
|
|
2302
|
+
/** Session keys are `agent:<agentId>:<rest>`. */
|
|
2303
|
+
function agentIdFromSessionKey(sessionKey) {
|
|
2304
|
+
return (sessionKey ? /^agent:([^:]+):/.exec(sessionKey) : null)?.[1];
|
|
2305
|
+
}
|
|
2306
|
+
function sessionAttrs(sessionId) {
|
|
2307
|
+
if (!sessionId) return {};
|
|
928
2308
|
return {
|
|
929
|
-
"
|
|
930
|
-
"gen_ai.
|
|
931
|
-
"gen_ai.usage.cache_read_input_tokens": usage.cacheRead,
|
|
932
|
-
"gen_ai.usage.cache_creation_input_tokens": usage.cacheWrite,
|
|
933
|
-
"gen_ai.usage.total_tokens": usage.total
|
|
2309
|
+
"session.id": sessionId,
|
|
2310
|
+
"gen_ai.session.id": sessionId
|
|
934
2311
|
};
|
|
935
2312
|
}
|
|
2313
|
+
function abandon(span, now) {
|
|
2314
|
+
span.endMs = now;
|
|
2315
|
+
span.outcome = "error";
|
|
2316
|
+
span.attrs["openclaw.outcome"] = "abandoned";
|
|
2317
|
+
span.attrs["error.type"] = "abandoned";
|
|
2318
|
+
}
|
|
2319
|
+
/** Insertion-ordered cache: re-setting a key moves it to the back, and the oldest keys go once `max` is exceeded. */
|
|
2320
|
+
function remember(map, key, value, max) {
|
|
2321
|
+
map.delete(key);
|
|
2322
|
+
map.set(key, value);
|
|
2323
|
+
for (const oldest of map.keys()) {
|
|
2324
|
+
if (map.size <= max) break;
|
|
2325
|
+
map.delete(oldest);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
936
2328
|
function hashHex(input, length) {
|
|
937
2329
|
return createHash("sha256").update(input).digest("hex").slice(0, length);
|
|
938
2330
|
}
|
|
939
2331
|
//#endregion
|
|
940
|
-
//#region src/
|
|
2332
|
+
//#region src/transport.ts
|
|
2333
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
2334
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
2335
|
+
const RETRY_BASE_MS = 500;
|
|
2336
|
+
const RETRY_AFTER_MAX_MS = 3e4;
|
|
941
2337
|
/**
|
|
942
|
-
*
|
|
943
|
-
*
|
|
944
|
-
*
|
|
945
|
-
*
|
|
946
|
-
* data-only feeds (llm_input/llm_output) that enrich the agent span.
|
|
947
|
-
*
|
|
948
|
-
* Every typed hook on OpenClaw's side fires fire-and-forget for non-modifying
|
|
949
|
-
* hooks; before_tool_call is a `runModifyingHook` where returning anything
|
|
950
|
-
* other than undefined blocks the tool call. Our handler returns nothing —
|
|
951
|
-
* keep it that way.
|
|
2338
|
+
* Ships OTLP requests sequentially with bounded retries. Every span is sent
|
|
2339
|
+
* exactly once on success: Latitude's trace and session rollups add per
|
|
2340
|
+
* insert, so a duplicate would inflate counts; a `4xx` other than `429` is
|
|
2341
|
+
* therefore final, while `429`, `5xx` and network errors retry with backoff.
|
|
952
2342
|
*/
|
|
2343
|
+
var Transport = class {
|
|
2344
|
+
url;
|
|
2345
|
+
opts;
|
|
2346
|
+
chain = Promise.resolve();
|
|
2347
|
+
pending = 0;
|
|
2348
|
+
constructor(opts) {
|
|
2349
|
+
this.opts = opts;
|
|
2350
|
+
this.url = `${withoutTrailingSlashes(opts.baseUrl)}/v1/traces`;
|
|
2351
|
+
}
|
|
2352
|
+
enqueue(payload) {
|
|
2353
|
+
this.pending++;
|
|
2354
|
+
this.chain = this.chain.then(() => this.send(payload)).catch((err) => this.opts.logger.warn(`export failed: ${String(err)}`)).finally(() => {
|
|
2355
|
+
this.pending--;
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
/** Resolves when everything queued so far has been sent or given up on, or the budget elapses. */
|
|
2359
|
+
async flush(budgetMs) {
|
|
2360
|
+
if (this.pending === 0) return;
|
|
2361
|
+
await Promise.race([this.chain, sleep(budgetMs)]);
|
|
2362
|
+
}
|
|
2363
|
+
async send(payload) {
|
|
2364
|
+
const body = JSON.stringify(payload);
|
|
2365
|
+
const spanCount = payload.resourceSpans.reduce((n, rs) => n + rs.scopeSpans.reduce((m, ss) => m + ss.spans.length, 0), 0);
|
|
2366
|
+
const maxAttempts = this.opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
2367
|
+
const doFetch = this.opts.fetchImpl ?? fetch;
|
|
2368
|
+
const wait = this.opts.sleep ?? sleep;
|
|
2369
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
2370
|
+
const controller = new AbortController();
|
|
2371
|
+
const timer = setTimeout(() => controller.abort(), this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
2372
|
+
try {
|
|
2373
|
+
const res = await doFetch(this.url, {
|
|
2374
|
+
method: "POST",
|
|
2375
|
+
headers: {
|
|
2376
|
+
"Content-Type": "application/json",
|
|
2377
|
+
Authorization: `Bearer ${this.opts.apiKey}`,
|
|
2378
|
+
"X-Latitude-Project": this.opts.project
|
|
2379
|
+
},
|
|
2380
|
+
body,
|
|
2381
|
+
signal: controller.signal
|
|
2382
|
+
});
|
|
2383
|
+
if (res.ok) {
|
|
2384
|
+
this.opts.logger.debug(`exported ${spanCount} spans (${body.length} bytes) HTTP ${res.status}`);
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
const text = await res.text().catch(() => "");
|
|
2388
|
+
if (!(res.status === 429 || res.status >= 500) || attempt === maxAttempts) {
|
|
2389
|
+
this.opts.logger.warn(`ingest HTTP ${res.status} (final): ${text.slice(0, 300)}`);
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
const retryAfter = Number(res.headers.get("retry-after"));
|
|
2393
|
+
await wait((Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter * 1e3, RETRY_AFTER_MAX_MS) : 0) || backoff(attempt));
|
|
2394
|
+
} catch (err) {
|
|
2395
|
+
if (attempt === maxAttempts) {
|
|
2396
|
+
this.opts.logger.warn(`ingest unreachable after ${attempt} attempts: ${String(err)}`);
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
await wait(backoff(attempt));
|
|
2400
|
+
} finally {
|
|
2401
|
+
clearTimeout(timer);
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
};
|
|
2406
|
+
function backoff(attempt) {
|
|
2407
|
+
return RETRY_BASE_MS * 2 ** (attempt - 1) + Math.random() * RETRY_BASE_MS;
|
|
2408
|
+
}
|
|
2409
|
+
function withoutTrailingSlashes(url) {
|
|
2410
|
+
let end = url.length;
|
|
2411
|
+
while (end > 0 && url[end - 1] === "/") end--;
|
|
2412
|
+
return url.slice(0, end);
|
|
2413
|
+
}
|
|
2414
|
+
function sleep(ms) {
|
|
2415
|
+
return new Promise((resolve) => {
|
|
2416
|
+
setTimeout(resolve, ms).unref?.();
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
//#endregion
|
|
2420
|
+
//#region src/plugin.ts
|
|
2421
|
+
const STOP_FLUSH_BUDGET_MS = 4e3;
|
|
2422
|
+
const PLUGIN_ID = "@latitude-data/openclaw-telemetry";
|
|
953
2423
|
function registerLatitudePlugin(api, opts = {}) {
|
|
954
2424
|
const config = opts.config ?? loadConfig(api.pluginConfig);
|
|
955
|
-
const logger = opts.logger ?? createLogger(config.debug);
|
|
2425
|
+
const logger = opts.logger ?? createLogger(config.debug, api.logger);
|
|
956
2426
|
if (!config.enabled) {
|
|
957
|
-
if (config.apiKey === "") logger.
|
|
958
|
-
if (config.project === "") logger.
|
|
2427
|
+
if (config.apiKey === "") logger.warn("disabled: apiKey is empty (set plugins.entries[id].config.apiKey)");
|
|
2428
|
+
if (config.project === "") logger.warn("disabled: project is empty (set plugins.entries[id].config.project)");
|
|
959
2429
|
return;
|
|
960
2430
|
}
|
|
961
|
-
logger.debug(`enabled: project=${config.project} base=${config.baseUrl}
|
|
962
|
-
|
|
2431
|
+
logger.debug(`enabled v${SCOPE_VERSION}: project=${config.project} base=${config.baseUrl} content=${config.allowConversationAccess}`);
|
|
2432
|
+
if (api.config?.plugins?.entries && api.config.plugins.entries[PLUGIN_ID]?.hooks?.allowConversationAccess !== true) logger.warn(`plugins.entries["${PLUGIN_ID}"].hooks.allowConversationAccess is not true; OpenClaw will not deliver the conversation hooks and no traces will be exported. Run: openclaw config set 'plugins.entries["${PLUGIN_ID}"].hooks.allowConversationAccess' true && openclaw gateway restart`);
|
|
2433
|
+
const transport = opts.transport ?? new Transport({
|
|
2434
|
+
baseUrl: config.baseUrl,
|
|
2435
|
+
apiKey: config.apiKey,
|
|
2436
|
+
project: config.project,
|
|
2437
|
+
logger
|
|
2438
|
+
});
|
|
2439
|
+
let stateDir;
|
|
2440
|
+
try {
|
|
2441
|
+
stateDir = api.runtime?.state?.resolveStateDir?.();
|
|
2442
|
+
} catch {
|
|
2443
|
+
stateDir = void 0;
|
|
2444
|
+
}
|
|
2445
|
+
const builder = new SpanBuilder({
|
|
2446
|
+
pluginVersion: SCOPE_VERSION,
|
|
2447
|
+
stateDir,
|
|
2448
|
+
tags: config.tags,
|
|
2449
|
+
metadata: config.metadata,
|
|
2450
|
+
memory: config.memory,
|
|
2451
|
+
memoryContent: config.memoryContent,
|
|
2452
|
+
toolDefinitions: config.toolDefinitions,
|
|
2453
|
+
now: opts.now,
|
|
2454
|
+
schedule: opts.schedule,
|
|
2455
|
+
log: (msg) => logger.debug(msg),
|
|
2456
|
+
emit: (result) => {
|
|
2457
|
+
try {
|
|
2458
|
+
opts.onEmit?.(result);
|
|
2459
|
+
logger.debug(`run ${result.runId}: ${result.spans.length} spans ready`);
|
|
2460
|
+
transport.enqueue(buildOtlpRequest([result], {
|
|
2461
|
+
allowConversationAccess: config.allowConversationAccess,
|
|
2462
|
+
redact: config.redact,
|
|
2463
|
+
serviceName: config.serviceName,
|
|
2464
|
+
maxContentChars: config.maxContentChars
|
|
2465
|
+
}));
|
|
2466
|
+
} catch (err) {
|
|
2467
|
+
logger.warn(`export of run ${result.runId} failed: ${String(err)}`);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
});
|
|
963
2471
|
const wrap = (name, fn) => {
|
|
964
2472
|
return (evt, ctx) => {
|
|
965
2473
|
try {
|
|
@@ -969,64 +2477,33 @@ function registerLatitudePlugin(api, opts = {}) {
|
|
|
969
2477
|
}
|
|
970
2478
|
};
|
|
971
2479
|
};
|
|
972
|
-
api.on("
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
api.on("model_call_started", wrap("model_call_started", (
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
api.on("
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
api.on("
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
api.on("
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
api.on("
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
api.on("after_compaction", wrap("after_compaction", (evt, ctx) => {
|
|
991
|
-
builder.onAfterCompaction(evt, ctx);
|
|
992
|
-
}));
|
|
993
|
-
api.on("subagent_spawned", wrap("subagent_spawned", (evt, ctx) => {
|
|
994
|
-
builder.onSubagentSpawned(evt, ctx);
|
|
995
|
-
}));
|
|
996
|
-
api.on("subagent_ended", wrap("subagent_ended", (evt, ctx) => {
|
|
997
|
-
builder.onSubagentEnded(evt, ctx);
|
|
998
|
-
}));
|
|
999
|
-
api.on("llm_input", wrap("llm_input", (evt, ctx) => {
|
|
1000
|
-
builder.onLlmInput(evt, ctx);
|
|
1001
|
-
}));
|
|
1002
|
-
api.on("llm_output", wrap("llm_output", (evt, ctx) => {
|
|
1003
|
-
builder.onLlmOutput(evt, ctx);
|
|
1004
|
-
}));
|
|
1005
|
-
api.on("agent_end", wrap("agent_end", (evt, ctx) => {
|
|
1006
|
-
queueMicrotask(() => {
|
|
2480
|
+
api.on("llm_input", wrap("llm_input", (e, c) => builder.onLlmInput(e, c)));
|
|
2481
|
+
api.on("llm_output", wrap("llm_output", (e, c) => builder.onLlmOutput(e, c)));
|
|
2482
|
+
api.on("agent_end", wrap("agent_end", (e, c) => builder.onAgentEnd(e, c)));
|
|
2483
|
+
api.on("model_call_started", wrap("model_call_started", (e, c) => builder.onModelCallStarted(e, c)));
|
|
2484
|
+
api.on("model_call_ended", wrap("model_call_ended", (e, c) => builder.onModelCallEnded(e, c)));
|
|
2485
|
+
api.on("before_tool_call", wrap("before_tool_call", (e, c) => builder.onBeforeToolCall(e, c)));
|
|
2486
|
+
api.on("after_tool_call", wrap("after_tool_call", (e, c) => builder.onAfterToolCall(e, c)));
|
|
2487
|
+
api.on("before_compaction", wrap("before_compaction", (e, c) => builder.onBeforeCompaction(e, c)));
|
|
2488
|
+
api.on("after_compaction", wrap("after_compaction", (e, c) => builder.onAfterCompaction(e, c)));
|
|
2489
|
+
api.on("subagent_spawned", wrap("subagent_spawned", (e, c) => builder.onSubagentSpawned(e, c)));
|
|
2490
|
+
api.on("subagent_ended", wrap("subagent_ended", (e, c) => builder.onSubagentEnded(e, c)));
|
|
2491
|
+
api.on("session_start", wrap("session_start", (e, c) => builder.onSessionStart(e, c)));
|
|
2492
|
+
api.on("session_end", wrap("session_end", (e, c) => builder.onSessionEnd(e.sessionKey ?? c.sessionKey, e.sessionId ?? c.sessionId)));
|
|
2493
|
+
api.on("message_received", wrap("message_received", (e, c) => builder.onMessageReceived(e, c)));
|
|
2494
|
+
api.on("cron_changed", wrap("cron_changed", (e) => builder.onCronChanged(e)));
|
|
2495
|
+
api.on("gateway_stop", (_evt, _ctx) => transport.flush(STOP_FLUSH_BUDGET_MS));
|
|
2496
|
+
try {
|
|
2497
|
+
api.runtime?.events?.onAgentEvent?.((evt) => {
|
|
1007
2498
|
try {
|
|
1008
|
-
|
|
1009
|
-
if (!result) {
|
|
1010
|
-
logger.debug("agent_end fired without a matching run in flight");
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
opts.onEmit?.(result);
|
|
1014
|
-
const payload = buildOtlpRequest(result, {
|
|
1015
|
-
allowConversationAccess: config.allowConversationAccess,
|
|
1016
|
-
redact: config.redact
|
|
1017
|
-
});
|
|
1018
|
-
postTraces({
|
|
1019
|
-
baseUrl: config.baseUrl,
|
|
1020
|
-
apiKey: config.apiKey,
|
|
1021
|
-
project: config.project,
|
|
1022
|
-
payload,
|
|
1023
|
-
logger
|
|
1024
|
-
});
|
|
2499
|
+
builder.onAgentEvent(evt);
|
|
1025
2500
|
} catch (err) {
|
|
1026
|
-
logger.warn(`
|
|
2501
|
+
logger.warn(`agent event handler failed: ${String(err)}`);
|
|
1027
2502
|
}
|
|
1028
2503
|
});
|
|
1029
|
-
})
|
|
2504
|
+
} catch (err) {
|
|
2505
|
+
logger.debug(`agent event stream unavailable: ${String(err)}`);
|
|
2506
|
+
}
|
|
1030
2507
|
}
|
|
1031
2508
|
//#endregion
|
|
1032
2509
|
export { registerLatitudePlugin as default };
|