@latitude-data/openclaw-telemetry 0.0.8 → 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 +79 -199
- package/dist/plugin.d.ts +152 -61
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +2122 -588
- package/dist/plugin.js.map +1 -1
- package/openclaw.plugin.json +58 -3
- package/package.json +4 -4
package/dist/plugin.js
CHANGED
|
@@ -1,54 +1,69 @@
|
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
4
|
+
import { readFileSync, statSync } from "node:fs";
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
//#region src/redaction.ts
|
|
7
|
+
function parseRedactConfig(value) {
|
|
8
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
9
|
+
const obj = value;
|
|
10
|
+
const attributes = parseAttributes(obj.attributes);
|
|
11
|
+
if (attributes.length === 0) return void 0;
|
|
12
|
+
return {
|
|
13
|
+
attributes,
|
|
14
|
+
mask: typeof obj.mask === "string" ? obj.mask : "******"
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function redactAttributes(attributes, config) {
|
|
18
|
+
if (!config) return attributes;
|
|
19
|
+
const matchers = config.attributes.map(toMatcher).filter((matcher) => !!matcher);
|
|
20
|
+
if (matchers.length === 0) return attributes;
|
|
21
|
+
return attributes.map((attr) => matchers.some((matches) => matches(attr.key)) ? redactedAttr(attr.key, config.mask) : attr);
|
|
22
|
+
}
|
|
23
|
+
function parseAttributes(value) {
|
|
24
|
+
if (!Array.isArray(value)) return [];
|
|
25
|
+
return value.filter((item) => typeof item === "string" && item.trim() !== "");
|
|
26
|
+
}
|
|
27
|
+
function toMatcher(pattern) {
|
|
28
|
+
if (pattern.startsWith("/") && pattern.lastIndexOf("/") > 0) {
|
|
29
|
+
const end = pattern.lastIndexOf("/");
|
|
30
|
+
try {
|
|
31
|
+
const regex = new RegExp(pattern.slice(1, end), pattern.slice(end + 1));
|
|
32
|
+
return (key) => {
|
|
33
|
+
regex.lastIndex = 0;
|
|
34
|
+
return regex.test(key);
|
|
35
|
+
};
|
|
36
|
+
} catch {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
10
40
|
try {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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);
|
|
41
|
+
const regex = new RegExp(pattern);
|
|
42
|
+
return (key) => {
|
|
43
|
+
regex.lastIndex = 0;
|
|
44
|
+
return key === pattern || regex.test(key);
|
|
45
|
+
};
|
|
46
|
+
} catch {
|
|
47
|
+
return (key) => key === pattern;
|
|
29
48
|
}
|
|
30
49
|
}
|
|
50
|
+
function redactedAttr(key, mask) {
|
|
51
|
+
return {
|
|
52
|
+
key,
|
|
53
|
+
value: { stringValue: mask }
|
|
54
|
+
};
|
|
55
|
+
}
|
|
31
56
|
//#endregion
|
|
32
57
|
//#region src/config.ts
|
|
33
58
|
const DEFAULT_BASE_URL = "https://ingest.latitude.so";
|
|
59
|
+
const DEFAULT_SERVICE_NAME = "openclaw";
|
|
60
|
+
const DEFAULT_MAX_CONTENT_CHARS = 262144;
|
|
34
61
|
/**
|
|
35
62
|
* Build a `Config` from OpenClaw's per-plugin config bucket. The plugin SDK
|
|
36
63
|
* passes `api.pluginConfig` (the user's `plugins.entries[id].config` block)
|
|
37
|
-
* to the registration function
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* were missing from pluginConfig. That fallback is gone deliberately:
|
|
41
|
-
* OpenClaw 2026.4.25's `openclaw plugins install` runs a static-analysis
|
|
42
|
-
* security scan that flags any runtime source combining environment-variable
|
|
43
|
-
* access with a network-send call (we have `fetch(` in postTraces). With
|
|
44
|
-
* the fallback our bundled runtime tripped the scanner. The installer
|
|
45
|
-
* writes credentials to `plugins.entries[id].config` anyway, so the
|
|
46
|
-
* fallback was polish-not-feature — its removal also gives a cleaner
|
|
47
|
-
* privacy story (the runtime can't pick up credentials the operator
|
|
48
|
-
* didn't put in openclaw.json).
|
|
49
|
-
*
|
|
50
|
-
* For dev-time testing with debug logs, set `config.debug = true` in
|
|
51
|
-
* 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.
|
|
52
67
|
*/
|
|
53
68
|
function loadConfig(pluginConfig = void 0) {
|
|
54
69
|
const fromOpts = pluginConfig ?? {};
|
|
@@ -58,13 +73,22 @@ function loadConfig(pluginConfig = void 0) {
|
|
|
58
73
|
const debug = pickBool(fromOpts.debug) ?? false;
|
|
59
74
|
const allowConversationAccess = pickBool(fromOpts.allowConversationAccess) ?? false;
|
|
60
75
|
const explicitlyDisabled = pickBool(fromOpts.enabled) === false;
|
|
76
|
+
const hasCreds = apiKey !== "" && project !== "";
|
|
61
77
|
return {
|
|
62
78
|
apiKey,
|
|
63
79
|
baseUrl,
|
|
64
80
|
project,
|
|
65
81
|
debug,
|
|
66
82
|
allowConversationAccess,
|
|
67
|
-
|
|
83
|
+
redact: parseRedactConfig(fromOpts.redact),
|
|
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
|
|
68
92
|
};
|
|
69
93
|
}
|
|
70
94
|
function pickString(value) {
|
|
@@ -73,24 +97,44 @@ function pickString(value) {
|
|
|
73
97
|
function pickBool(value) {
|
|
74
98
|
return typeof value === "boolean" ? value : void 0;
|
|
75
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
|
+
}
|
|
76
115
|
//#endregion
|
|
77
116
|
//#region src/logger.ts
|
|
78
117
|
const PREFIX = "[latitude-openclaw]";
|
|
79
|
-
function createLogger(debugEnabled) {
|
|
118
|
+
function createLogger(debugEnabled, host) {
|
|
80
119
|
return {
|
|
81
|
-
debug: debugEnabled ? (msg) =>
|
|
82
|
-
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)
|
|
83
122
|
};
|
|
84
123
|
}
|
|
124
|
+
function writeStderr(msg) {
|
|
125
|
+
process.stderr.write(`${PREFIX} ${msg}\n`);
|
|
126
|
+
}
|
|
85
127
|
//#endregion
|
|
86
128
|
//#region src/otlp.ts
|
|
87
129
|
const SCOPE_NAME = "@latitude-data/openclaw-telemetry";
|
|
88
|
-
const SCOPE_VERSION = "0.0
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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)));
|
|
92
136
|
return { resourceSpans: [{
|
|
93
|
-
resource: { attributes: resourceAttrs() },
|
|
137
|
+
resource: { attributes: resourceAttrs(options.serviceName ?? "openclaw") },
|
|
94
138
|
scopeSpans: [{
|
|
95
139
|
scope: {
|
|
96
140
|
name: SCOPE_NAME,
|
|
@@ -101,52 +145,128 @@ function buildOtlpRequest(result, options) {
|
|
|
101
145
|
}] };
|
|
102
146
|
}
|
|
103
147
|
function toOtlpSpan(span, options) {
|
|
104
|
-
const startNs = msToNs(span.startMs);
|
|
105
|
-
const endNs = msToNs(span.endMs ?? span.startMs);
|
|
106
148
|
const attrs = [];
|
|
107
149
|
for (const [rawKey, value] of Object.entries(span.attrs)) {
|
|
108
150
|
if (value === void 0 || value === null) continue;
|
|
109
|
-
const isGated = rawKey.endsWith(
|
|
151
|
+
const isGated = rawKey.endsWith(GATED_SUFFIX);
|
|
110
152
|
if (isGated && !options.allowConversationAccess) continue;
|
|
111
|
-
const kv = encodeAttr(isGated ? rawKey.slice(0, -6) : rawKey, value);
|
|
153
|
+
const kv = encodeAttr(isGated ? rawKey.slice(0, -6) : rawKey, value, options.maxContentChars);
|
|
112
154
|
if (kv !== void 0) attrs.push(kv);
|
|
113
155
|
}
|
|
114
156
|
attrs.push(bool("latitude.captured.content", options.allowConversationAccess));
|
|
115
|
-
if (span.endMs !== void 0) attrs.push(int("openclaw.duration_ms.computed", Math.max(0, span.endMs - span.startMs)));
|
|
116
|
-
const statusCode = span.outcome === "error" ? 2 : 1;
|
|
117
157
|
return {
|
|
118
158
|
traceId: span.traceId,
|
|
119
159
|
spanId: span.spanId,
|
|
120
160
|
parentSpanId: span.parentSpanId,
|
|
121
161
|
name: span.name,
|
|
122
|
-
kind:
|
|
123
|
-
startTimeUnixNano:
|
|
124
|
-
endTimeUnixNano:
|
|
125
|
-
attributes: attrs,
|
|
126
|
-
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 }
|
|
127
167
|
};
|
|
128
168
|
}
|
|
129
|
-
function encodeAttr(key, value) {
|
|
169
|
+
function encodeAttr(key, value, maxChars) {
|
|
130
170
|
if (value === void 0 || value === null) return void 0;
|
|
131
|
-
if (typeof value === "string") return str(key, value);
|
|
171
|
+
if (typeof value === "string") return str$1(key, budget(value, maxChars));
|
|
132
172
|
if (typeof value === "boolean") return bool(key, value);
|
|
133
173
|
if (typeof value === "number") return Number.isInteger(value) ? int(key, value) : {
|
|
134
174
|
key,
|
|
135
175
|
value: { doubleValue: value }
|
|
136
176
|
};
|
|
137
|
-
|
|
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
|
+
};
|
|
138
257
|
}
|
|
139
|
-
function resourceAttrs() {
|
|
258
|
+
function resourceAttrs(serviceName) {
|
|
140
259
|
return [
|
|
141
|
-
str("service.name",
|
|
142
|
-
str("service.version", SCOPE_VERSION),
|
|
143
|
-
str("
|
|
144
|
-
str("host.
|
|
145
|
-
str("
|
|
146
|
-
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())
|
|
147
267
|
];
|
|
148
268
|
}
|
|
149
|
-
function str(key, value) {
|
|
269
|
+
function str$1(key, value) {
|
|
150
270
|
return {
|
|
151
271
|
key,
|
|
152
272
|
value: { stringValue: value }
|
|
@@ -176,6 +296,113 @@ function safeJson$1(value) {
|
|
|
176
296
|
}
|
|
177
297
|
}
|
|
178
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
|
|
179
406
|
//#region src/messages.ts
|
|
180
407
|
const ALLOWED_ROLES = new Set([
|
|
181
408
|
"system",
|
|
@@ -183,26 +410,24 @@ const ALLOWED_ROLES = new Set([
|
|
|
183
410
|
"assistant",
|
|
184
411
|
"tool"
|
|
185
412
|
]);
|
|
186
|
-
/**
|
|
187
|
-
* Normalize a single message of any of the provider shapes we know about.
|
|
188
|
-
* Returns `undefined` for non-objects so the caller can skip them.
|
|
189
|
-
*/
|
|
190
413
|
function normalizeMessage(raw) {
|
|
191
414
|
if (!raw || typeof raw !== "object") return void 0;
|
|
192
415
|
const obj = raw;
|
|
193
|
-
const role = coerceRole(obj.role);
|
|
194
416
|
if (Array.isArray(obj.parts)) {
|
|
195
417
|
const parts = [];
|
|
196
418
|
for (const p of obj.parts) if (p && typeof p === "object") parts.push(p);
|
|
197
419
|
return {
|
|
198
|
-
role,
|
|
420
|
+
role: coerceRole(obj.role),
|
|
199
421
|
parts: parts.length > 0 ? parts : [{
|
|
200
422
|
type: "text",
|
|
201
423
|
content: safeJson(raw)
|
|
202
424
|
}]
|
|
203
425
|
};
|
|
204
426
|
}
|
|
427
|
+
if (obj.role === "toolResult") return normalizeToolResult(obj);
|
|
428
|
+
if (obj.role === "custom") return normalizeCustom(obj);
|
|
205
429
|
const content = obj.content ?? obj.text ?? obj.message;
|
|
430
|
+
const role = isRuntimeContext(obj, content) ? "system" : coerceRole(obj.role);
|
|
206
431
|
if (role === "tool" && obj.tool_call_id !== void 0) return {
|
|
207
432
|
role,
|
|
208
433
|
parts: [{
|
|
@@ -212,11 +437,16 @@ function normalizeMessage(raw) {
|
|
|
212
437
|
}]
|
|
213
438
|
};
|
|
214
439
|
if (typeof content === "string") {
|
|
215
|
-
const parts = [
|
|
440
|
+
const parts = [];
|
|
441
|
+
if (content.length > 0) parts.push({
|
|
216
442
|
type: "text",
|
|
217
443
|
content
|
|
218
|
-
}
|
|
444
|
+
});
|
|
219
445
|
appendToolCalls(parts, obj.tool_calls);
|
|
446
|
+
if (parts.length === 0) parts.push({
|
|
447
|
+
type: "text",
|
|
448
|
+
content: ""
|
|
449
|
+
});
|
|
220
450
|
return {
|
|
221
451
|
role,
|
|
222
452
|
parts
|
|
@@ -246,7 +476,6 @@ function normalizeMessage(raw) {
|
|
|
246
476
|
}]
|
|
247
477
|
};
|
|
248
478
|
}
|
|
249
|
-
/** Normalize an array of provider messages. */
|
|
250
479
|
function normalizeMessages(raw) {
|
|
251
480
|
const out = [];
|
|
252
481
|
for (const m of raw) {
|
|
@@ -255,7 +484,6 @@ function normalizeMessages(raw) {
|
|
|
255
484
|
}
|
|
256
485
|
return out;
|
|
257
486
|
}
|
|
258
|
-
/** Build a single user message from a string prompt. */
|
|
259
487
|
function userMessageFromPrompt(prompt) {
|
|
260
488
|
return {
|
|
261
489
|
role: "user",
|
|
@@ -265,44 +493,73 @@ function userMessageFromPrompt(prompt) {
|
|
|
265
493
|
}]
|
|
266
494
|
};
|
|
267
495
|
}
|
|
268
|
-
/** Build a single assistant message from `assistantTexts` + `lastAssistant` fallback. */
|
|
269
|
-
function assistantMessageFromOutput(assistantTexts, lastAssistant) {
|
|
270
|
-
if (lastAssistant !== void 0) {
|
|
271
|
-
const norm = normalizeMessage(lastAssistant);
|
|
272
|
-
if (norm) return {
|
|
273
|
-
...norm,
|
|
274
|
-
role: "assistant"
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
const parts = [];
|
|
278
|
-
for (const text of assistantTexts) if (text.length > 0) parts.push({
|
|
279
|
-
type: "text",
|
|
280
|
-
content: text
|
|
281
|
-
});
|
|
282
|
-
if (parts.length === 0) parts.push({
|
|
283
|
-
type: "text",
|
|
284
|
-
content: ""
|
|
285
|
-
});
|
|
286
|
-
return {
|
|
287
|
-
role: "assistant",
|
|
288
|
-
parts
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Wrap a system prompt string into the parts-array shape expected for
|
|
293
|
-
* `gen_ai.system_instructions`. Empty string in → single empty text part out
|
|
294
|
-
* (still a valid array, never `undefined`).
|
|
295
|
-
*/
|
|
296
496
|
function systemInstructionsParts(prompt) {
|
|
297
497
|
return [{
|
|
298
498
|
type: "text",
|
|
299
499
|
content: prompt
|
|
300
500
|
}];
|
|
301
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
|
+
}
|
|
302
514
|
function coerceRole(raw) {
|
|
303
515
|
if (typeof raw !== "string") return "user";
|
|
304
516
|
return ALLOWED_ROLES.has(raw) ? raw : "user";
|
|
305
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
|
+
}
|
|
306
563
|
function normalizeBlock(raw) {
|
|
307
564
|
if (typeof raw === "string") return {
|
|
308
565
|
type: "text",
|
|
@@ -310,24 +567,22 @@ function normalizeBlock(raw) {
|
|
|
310
567
|
};
|
|
311
568
|
if (!raw || typeof raw !== "object") return void 0;
|
|
312
569
|
const obj = raw;
|
|
313
|
-
|
|
314
|
-
|
|
570
|
+
const type = typeof obj.type === "string" ? obj.type : "text";
|
|
571
|
+
if (type === "text") {
|
|
572
|
+
if (typeof obj.content === "string") return {
|
|
315
573
|
type: "text",
|
|
316
574
|
content: obj.content
|
|
317
575
|
};
|
|
576
|
+
if (typeof obj.text === "string") return {
|
|
577
|
+
type: "text",
|
|
578
|
+
content: obj.text
|
|
579
|
+
};
|
|
318
580
|
}
|
|
319
|
-
|
|
320
|
-
if (type === "text" && typeof obj.text === "string") return {
|
|
581
|
+
if (type === "toolResult") return {
|
|
321
582
|
type: "text",
|
|
322
|
-
content: obj
|
|
323
|
-
};
|
|
324
|
-
if (type === "tool_use") return {
|
|
325
|
-
type: "tool_call",
|
|
326
|
-
id: typeof obj.id === "string" ? obj.id : "",
|
|
327
|
-
name: typeof obj.name === "string" ? obj.name : "",
|
|
328
|
-
arguments: obj.input ?? {}
|
|
583
|
+
content: blockText(obj) ?? safeJson(raw)
|
|
329
584
|
};
|
|
330
|
-
if (type === "tool_call") return {
|
|
585
|
+
if (type === "toolCall" || type === "tool_use" || type === "tool_call") return {
|
|
331
586
|
type: "tool_call",
|
|
332
587
|
id: typeof obj.id === "string" ? obj.id : "",
|
|
333
588
|
name: typeof obj.name === "string" ? obj.name : "",
|
|
@@ -343,33 +598,46 @@ function normalizeBlock(raw) {
|
|
|
343
598
|
id: typeof obj.id === "string" ? obj.id : "",
|
|
344
599
|
response: obj.response ?? ""
|
|
345
600
|
};
|
|
346
|
-
if (type === "thinking"
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
+
}
|
|
350
612
|
if (type === "reasoning" && typeof obj.content === "string") return {
|
|
351
613
|
type: "reasoning",
|
|
352
614
|
content: obj.content
|
|
353
615
|
};
|
|
354
|
-
if (type === "image"
|
|
355
|
-
const
|
|
356
|
-
const uri = src.url ?? (src.data ? `data:${src.media_type ?? "image/unknown"};base64,${src.data}` : "");
|
|
616
|
+
if (type === "image") {
|
|
617
|
+
const uri = imageUri(obj);
|
|
357
618
|
if (uri) return {
|
|
358
619
|
type: "uri",
|
|
359
620
|
modality: "image",
|
|
360
621
|
uri
|
|
361
622
|
};
|
|
623
|
+
return {
|
|
624
|
+
type: "text",
|
|
625
|
+
content: "[image]"
|
|
626
|
+
};
|
|
362
627
|
}
|
|
363
628
|
return {
|
|
364
629
|
type,
|
|
365
630
|
content: safeJson(raw)
|
|
366
631
|
};
|
|
367
632
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
+
}
|
|
373
641
|
function appendToolCalls(parts, raw) {
|
|
374
642
|
if (!Array.isArray(raw)) return;
|
|
375
643
|
for (const tc of raw) {
|
|
@@ -397,301 +665,1093 @@ function safeJson(value) {
|
|
|
397
665
|
}
|
|
398
666
|
}
|
|
399
667
|
//#endregion
|
|
400
|
-
//#region src/
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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;
|
|
408
689
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
onBeforeAgentStart(evt, ctx) {
|
|
416
|
-
const runId = ctx.runId;
|
|
417
|
-
if (!runId) return;
|
|
418
|
-
if (this.runs.has(runId)) return;
|
|
419
|
-
const link = this.subagentLinks.get(runId);
|
|
420
|
-
const traceId = link?.traceId ?? hashHex(runId, 32);
|
|
421
|
-
const parentSpanId = link?.subagentSpanId ?? "";
|
|
422
|
-
const agent = {
|
|
423
|
-
spanId: hashHex(`${traceId}:${runId}:agent`, 16),
|
|
424
|
-
traceId,
|
|
425
|
-
parentSpanId,
|
|
426
|
-
name: "agent",
|
|
427
|
-
startMs: Date.now(),
|
|
428
|
-
endMs: void 0,
|
|
429
|
-
attrs: {
|
|
430
|
-
...flattenCtx(ctx),
|
|
431
|
-
...latitudeAttrs(ctx),
|
|
432
|
-
...sessionAttrs(ctx),
|
|
433
|
-
"openclaw.run.id": runId,
|
|
434
|
-
"before_agent_start.prompt:gated": evt.prompt,
|
|
435
|
-
"before_agent_start.messages:gated": evt.messages ? normalizeMessages(evt.messages) : void 0
|
|
436
|
-
}
|
|
437
|
-
};
|
|
438
|
-
this.runs.set(runId, {
|
|
439
|
-
agent,
|
|
440
|
-
history: [],
|
|
441
|
-
openModelCalls: /* @__PURE__ */ new Map(),
|
|
442
|
-
openToolCalls: /* @__PURE__ */ new Map(),
|
|
443
|
-
openCompaction: void 0,
|
|
444
|
-
closed: [],
|
|
445
|
-
childSubagentSpans: /* @__PURE__ */ new Map()
|
|
446
|
-
});
|
|
690
|
+
};
|
|
691
|
+
var SessionHistoryStore = class {
|
|
692
|
+
sessions = /* @__PURE__ */ new Map();
|
|
693
|
+
now;
|
|
694
|
+
constructor(now = () => Date.now()) {
|
|
695
|
+
this.now = now;
|
|
447
696
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
* Provider-specific message shapes get normalized into the parts-based
|
|
454
|
-
* GenAI format here — that's the contract Latitude's downstream parser
|
|
455
|
-
* expects on `gen_ai.input.messages` and `gen_ai.system_instructions`.
|
|
456
|
-
*/
|
|
457
|
-
onLlmInput(evt, ctx) {
|
|
458
|
-
const run = this.runs.get(ctx.runId ?? evt.runId);
|
|
459
|
-
if (!run) return;
|
|
460
|
-
const inputMessages = [...normalizeMessages(evt.historyMessages)];
|
|
461
|
-
if (evt.prompt) inputMessages.push(userMessageFromPrompt(evt.prompt));
|
|
462
|
-
Object.assign(run.agent.attrs, {
|
|
463
|
-
"gen_ai.system_instructions:gated": evt.systemPrompt ? systemInstructionsParts(evt.systemPrompt) : void 0,
|
|
464
|
-
"user_prompt:gated": evt.prompt,
|
|
465
|
-
"gen_ai.input.messages:gated": inputMessages,
|
|
466
|
-
"openclaw.images.count": evt.imagesCount,
|
|
467
|
-
"gen_ai.request.model": evt.model,
|
|
468
|
-
"gen_ai.system": evt.provider,
|
|
469
|
-
"openclaw.provider": evt.provider
|
|
470
|
-
});
|
|
471
|
-
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;
|
|
472
702
|
}
|
|
473
|
-
/**
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
*/
|
|
477
|
-
onLlmOutput(evt, ctx) {
|
|
478
|
-
const run = this.runs.get(ctx.runId ?? evt.runId);
|
|
479
|
-
if (!run) return;
|
|
480
|
-
const assistantMessage = assistantMessageFromOutput(evt.assistantTexts, evt.lastAssistant);
|
|
481
|
-
Object.assign(run.agent.attrs, {
|
|
482
|
-
"gen_ai.output.messages:gated": [assistantMessage],
|
|
483
|
-
"openclaw.resolved.ref": evt.resolvedRef,
|
|
484
|
-
"openclaw.harness.id": evt.harnessId,
|
|
485
|
-
"gen_ai.response.model": evt.model,
|
|
486
|
-
...usageAttrs(evt.usage)
|
|
487
|
-
});
|
|
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));
|
|
488
706
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
const span = {
|
|
493
|
-
spanId: hashHex(`${run.agent.traceId}:model_call:${evt.callId}`, 16),
|
|
494
|
-
traceId: run.agent.traceId,
|
|
495
|
-
parentSpanId: run.agent.spanId,
|
|
496
|
-
name: "model_call",
|
|
497
|
-
startMs: Date.now(),
|
|
498
|
-
endMs: void 0,
|
|
499
|
-
attrs: {
|
|
500
|
-
...latitudeAttrs(ctx),
|
|
501
|
-
...sessionAttrs(ctx),
|
|
502
|
-
"openclaw.run.id": evt.runId,
|
|
503
|
-
"openclaw.call.id": evt.callId,
|
|
504
|
-
"gen_ai.system": evt.provider,
|
|
505
|
-
"openclaw.provider": evt.provider,
|
|
506
|
-
"gen_ai.request.model": evt.model,
|
|
507
|
-
"openclaw.api": evt.api,
|
|
508
|
-
"openclaw.transport": evt.transport,
|
|
509
|
-
"gen_ai.input.messages:gated": [...run.history]
|
|
510
|
-
}
|
|
511
|
-
};
|
|
512
|
-
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));
|
|
513
710
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
if (!run) return;
|
|
517
|
-
const span = run.openModelCalls.get(evt.callId);
|
|
518
|
-
if (!span) return;
|
|
519
|
-
span.endMs = Date.now();
|
|
520
|
-
span.outcome = evt.outcome === "completed" ? "ok" : "error";
|
|
521
|
-
span.errorMessage = evt.errorCategory;
|
|
522
|
-
Object.assign(span.attrs, {
|
|
523
|
-
"openclaw.duration_ms": evt.durationMs,
|
|
524
|
-
"openclaw.outcome": evt.outcome,
|
|
525
|
-
"openclaw.error.category": evt.errorCategory,
|
|
526
|
-
"openclaw.failure.kind": evt.failureKind,
|
|
527
|
-
"openclaw.request.payload_bytes": evt.requestPayloadBytes,
|
|
528
|
-
"openclaw.response.stream_bytes": evt.responseStreamBytes,
|
|
529
|
-
"openclaw.ttfb_ms": evt.timeToFirstByteMs,
|
|
530
|
-
"openclaw.upstream.request_id_hash": evt.upstreamRequestIdHash
|
|
531
|
-
});
|
|
532
|
-
run.openModelCalls.delete(evt.callId);
|
|
533
|
-
run.closed.push(span);
|
|
711
|
+
forget(sessionId) {
|
|
712
|
+
this.sessions.delete(sessionId);
|
|
534
713
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
* IMPORTANT: this runs as a `runModifyingHook` in OpenClaw — returning
|
|
541
|
-
* anything other than `undefined`/falsy from this handler blocks the tool.
|
|
542
|
-
* The plugin-side handler enforces a void return; this method's signature
|
|
543
|
-
* already returns `void`.
|
|
544
|
-
*/
|
|
545
|
-
onBeforeToolCall(evt, ctx) {
|
|
546
|
-
if (!evt.runId) return;
|
|
547
|
-
const run = this.runs.get(evt.runId);
|
|
548
|
-
if (!run) return;
|
|
549
|
-
const toolCallId = evt.toolCallId ?? `${evt.toolName}:${randomUUID()}`;
|
|
550
|
-
const span = {
|
|
551
|
-
spanId: hashHex(`${run.agent.traceId}:tool_call:${toolCallId}`, 16),
|
|
552
|
-
traceId: run.agent.traceId,
|
|
553
|
-
parentSpanId: run.agent.spanId,
|
|
554
|
-
name: `tool_call:${evt.toolName}`,
|
|
555
|
-
startMs: Date.now(),
|
|
556
|
-
endMs: void 0,
|
|
557
|
-
attrs: {
|
|
558
|
-
...latitudeAttrs(ctx),
|
|
559
|
-
...sessionAttrs(ctx),
|
|
560
|
-
"openclaw.run.id": evt.runId,
|
|
561
|
-
"gen_ai.tool.name": evt.toolName,
|
|
562
|
-
"gen_ai.tool.call.id": toolCallId,
|
|
563
|
-
"gen_ai.tool.call.arguments:gated": evt.params
|
|
564
|
-
}
|
|
565
|
-
};
|
|
566
|
-
run.openToolCalls.set(toolCallId, span);
|
|
567
|
-
run.history.push({
|
|
568
|
-
role: "assistant",
|
|
569
|
-
parts: [{
|
|
570
|
-
type: "tool_call",
|
|
571
|
-
id: toolCallId,
|
|
572
|
-
name: evt.toolName,
|
|
573
|
-
arguments: evt.params
|
|
574
|
-
}]
|
|
714
|
+
set(sessionId, messages) {
|
|
715
|
+
this.evict();
|
|
716
|
+
this.sessions.set(sessionId, {
|
|
717
|
+
messages,
|
|
718
|
+
updatedAt: this.now()
|
|
575
719
|
});
|
|
576
720
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
const
|
|
580
|
-
if (
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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);
|
|
727
|
+
}
|
|
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);
|
|
597
1422
|
run.history.push({
|
|
598
|
-
role: "
|
|
1423
|
+
role: "assistant",
|
|
599
1424
|
parts: [{
|
|
600
|
-
type: "
|
|
1425
|
+
type: "tool_call",
|
|
601
1426
|
id: toolCallId,
|
|
602
|
-
|
|
1427
|
+
name: evt.toolName,
|
|
1428
|
+
arguments: evt.params
|
|
603
1429
|
}]
|
|
604
1430
|
});
|
|
1431
|
+
if (SPAWN_TOOL_PATTERN.test(evt.toolName)) run.lastSpawnToolSpanId = span.spanId;
|
|
605
1432
|
}
|
|
606
|
-
|
|
607
|
-
const runId = ctx.runId;
|
|
1433
|
+
onAfterToolCall(evt, ctx) {
|
|
1434
|
+
const runId = evt.runId ?? ctx.runId;
|
|
608
1435
|
if (!runId) return;
|
|
609
1436
|
const run = this.runs.get(runId);
|
|
610
1437
|
if (!run) return;
|
|
611
|
-
run.
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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 ?? "",
|
|
615
1491
|
name: "compaction",
|
|
616
|
-
|
|
1492
|
+
kind: KIND_INTERNAL,
|
|
1493
|
+
startMs,
|
|
617
1494
|
endMs: void 0,
|
|
618
1495
|
attrs: {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
"
|
|
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",
|
|
622
1501
|
"openclaw.compaction.message_count.before": evt.messageCount,
|
|
1502
|
+
"openclaw.compaction.compacting_count": evt.compactingCount,
|
|
1503
|
+
"openclaw.compaction.token_count.before": evt.tokenCount,
|
|
623
1504
|
"openclaw.compaction.session_file": evt.sessionFile,
|
|
624
|
-
"
|
|
1505
|
+
"openclaw.session.key": ctx.sessionKey,
|
|
1506
|
+
"gen_ai.input.messages:gated": evt.messages ? normalizeMessages(evt.messages) : void 0
|
|
625
1507
|
}
|
|
626
1508
|
};
|
|
1509
|
+
if (run) {
|
|
1510
|
+
run.openCompaction = span;
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
remember(this.standaloneCompactions, ctx.sessionKey ?? "", span, STANDALONE_COMPACTION_MAX);
|
|
627
1514
|
}
|
|
628
1515
|
onAfterCompaction(evt, ctx) {
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
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;
|
|
635
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);
|
|
636
1526
|
Object.assign(span.attrs, {
|
|
637
1527
|
"openclaw.compaction.message_count.after": evt.messageCount,
|
|
638
1528
|
"openclaw.compaction.compacted_count": evt.compactedCount,
|
|
639
|
-
"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
|
|
640
1540
|
});
|
|
641
|
-
run
|
|
642
|
-
|
|
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);
|
|
643
1631
|
}
|
|
644
1632
|
/**
|
|
645
|
-
*
|
|
646
|
-
*
|
|
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.
|
|
647
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
|
+
}
|
|
648
1688
|
onSubagentSpawned(evt, ctx) {
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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;
|
|
653
1696
|
const span = {
|
|
654
|
-
spanId: hashHex(`${
|
|
655
|
-
traceId
|
|
656
|
-
parentSpanId
|
|
1697
|
+
spanId: hashHex(`${childRunId}:subagent`, 16),
|
|
1698
|
+
traceId,
|
|
1699
|
+
parentSpanId,
|
|
657
1700
|
name: "subagent",
|
|
658
|
-
|
|
1701
|
+
kind: KIND_INTERNAL,
|
|
1702
|
+
startMs: this.now(),
|
|
659
1703
|
endMs: void 0,
|
|
660
1704
|
attrs: {
|
|
661
|
-
...
|
|
662
|
-
|
|
663
|
-
"
|
|
664
|
-
"
|
|
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,
|
|
665
1712
|
"openclaw.subagent.child_session_key": evt.childSessionKey,
|
|
666
1713
|
"openclaw.subagent.agent_id": evt.agentId,
|
|
667
1714
|
"openclaw.subagent.label": evt.label,
|
|
668
1715
|
"openclaw.subagent.mode": evt.mode,
|
|
669
1716
|
"openclaw.subagent.thread_requested": evt.threadRequested,
|
|
1717
|
+
"openclaw.subagent.resolved_model": evt.resolvedModel,
|
|
1718
|
+
"openclaw.subagent.resolved_provider": evt.resolvedProvider,
|
|
670
1719
|
"openclaw.subagent.requester.channel": evt.requester?.channel,
|
|
671
1720
|
"openclaw.subagent.requester.account_id": evt.requester?.accountId,
|
|
672
1721
|
"openclaw.subagent.requester.to": evt.requester?.to,
|
|
673
|
-
"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)
|
|
674
1724
|
}
|
|
675
1725
|
};
|
|
676
|
-
parent
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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()
|
|
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()
|
|
682
1742
|
});
|
|
1743
|
+
const child = this.runs.get(childRunId);
|
|
1744
|
+
if (child) this.applyLink(child);
|
|
683
1745
|
}
|
|
684
1746
|
onSubagentEnded(evt, ctx) {
|
|
685
|
-
const
|
|
686
|
-
if (!parentRunId) return;
|
|
687
|
-
const parent = this.runs.get(parentRunId);
|
|
688
|
-
if (!parent) return;
|
|
689
|
-
const childRunId = evt.runId;
|
|
1747
|
+
const childRunId = evt.runId ?? ctx.runId;
|
|
690
1748
|
if (!childRunId) return;
|
|
691
|
-
const
|
|
692
|
-
if (!
|
|
693
|
-
span
|
|
694
|
-
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";
|
|
695
1755
|
span.errorMessage = evt.error;
|
|
696
1756
|
Object.assign(span.attrs, {
|
|
697
1757
|
"openclaw.subagent.target_session_key": evt.targetSessionKey,
|
|
@@ -699,93 +1759,512 @@ var SpanBuilder = class {
|
|
|
699
1759
|
"openclaw.subagent.reason": evt.reason,
|
|
700
1760
|
"openclaw.subagent.outcome": evt.outcome,
|
|
701
1761
|
"openclaw.subagent.send_farewell": evt.sendFarewell,
|
|
702
|
-
"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
|
|
703
1765
|
});
|
|
704
|
-
|
|
705
|
-
|
|
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
|
+
};
|
|
706
1881
|
}
|
|
707
1882
|
/**
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
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.
|
|
711
1886
|
*/
|
|
712
|
-
|
|
713
|
-
const
|
|
714
|
-
if (!
|
|
715
|
-
const
|
|
716
|
-
if (
|
|
717
|
-
|
|
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";
|
|
718
1894
|
return;
|
|
719
1895
|
}
|
|
720
|
-
const
|
|
721
|
-
run.
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
"
|
|
727
|
-
|
|
728
|
-
|
|
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
|
|
729
2012
|
});
|
|
730
|
-
for (const span of run.openModelCalls.values())
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
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);
|
|
735
2033
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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;
|
|
741
2051
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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";
|
|
747
2072
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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";
|
|
753
2084
|
}
|
|
754
|
-
const spans = [run.agent, ...run.closed];
|
|
755
|
-
this.runs.delete(runId);
|
|
756
|
-
this.subagentLinks.delete(runId);
|
|
757
|
-
return {
|
|
758
|
-
runId,
|
|
759
|
-
spans
|
|
760
|
-
};
|
|
761
2085
|
}
|
|
762
|
-
/**
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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;
|
|
766
2131
|
}
|
|
767
|
-
/**
|
|
768
|
-
|
|
769
|
-
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)];
|
|
770
2135
|
}
|
|
771
2136
|
/**
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
* Two passes: TTL eviction (anything older than `SUBAGENT_LINK_TTL_MS`),
|
|
777
|
-
* then a hard size cap (when we're past `SUBAGENT_LINK_MAX`, drop the
|
|
778
|
-
* 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.
|
|
779
2140
|
*/
|
|
780
|
-
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
if (
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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);
|
|
789
2268
|
}
|
|
790
2269
|
}
|
|
791
2270
|
findOpenToolCallByName(run, toolName) {
|
|
@@ -793,119 +2272,202 @@ var SpanBuilder = class {
|
|
|
793
2272
|
const entries = Array.from(run.openToolCalls.entries());
|
|
794
2273
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
795
2274
|
const entry = entries[i];
|
|
796
|
-
if (
|
|
797
|
-
const [id, span] = entry;
|
|
798
|
-
if (span.name === target) return id;
|
|
2275
|
+
if (entry && entry[1].name === target) return entry[0];
|
|
799
2276
|
}
|
|
800
2277
|
}
|
|
801
2278
|
};
|
|
802
|
-
function
|
|
2279
|
+
function toolCtxToAgentCtx(ctx, runId) {
|
|
803
2280
|
return {
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
"openclaw.channel.id": ctx.channelId,
|
|
813
|
-
"openclaw.cron.job.id": ctx.jobId,
|
|
814
|
-
"openclaw.model.provider.id": ctx.modelProviderId,
|
|
815
|
-
"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
|
|
816
2289
|
};
|
|
817
2290
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
* openclaw-specific code path. Emitted on every span, not just `agent`,
|
|
824
|
-
* so child spans (model_call / tool_call / etc.) inherit the same grouping.
|
|
825
|
-
*/
|
|
826
|
-
function sessionAttrs(ctx) {
|
|
827
|
-
if (!ctx.sessionId) return {};
|
|
828
|
-
return {
|
|
829
|
-
"session.id": ctx.sessionId,
|
|
830
|
-
"gen_ai.session.id": ctx.sessionId
|
|
831
|
-
};
|
|
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";
|
|
832
2296
|
}
|
|
833
|
-
/**
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
*
|
|
838
|
-
* - `latitude.tags` is a JSON-encoded string array (`fromJsonStringArray`
|
|
839
|
-
* in domain/spans/src/otlp/resolvers/enrichment.ts).
|
|
840
|
-
* - `latitude.metadata` is a JSON-encoded string object (`fromJsonString`).
|
|
841
|
-
*
|
|
842
|
-
* Tags = the agent id, the channel id, and the trigger. When trigger is
|
|
843
|
-
* `cron`, the tag becomes `cron:<jobId>` so dashboards can pivot on the
|
|
844
|
-
* specific cron job. Each tag is conditionally included so absent ctx
|
|
845
|
-
* fields don't produce empty entries.
|
|
846
|
-
*
|
|
847
|
-
* Metadata = every ctx field that's set, namespaced under `openclaw.*` so
|
|
848
|
-
* it can't collide with metadata keys other plugins might emit.
|
|
849
|
-
*/
|
|
850
|
-
function latitudeAttrs(ctx) {
|
|
851
|
-
const tags = [];
|
|
852
|
-
if (ctx.agentId) tags.push(ctx.agentId);
|
|
853
|
-
if (ctx.channelId) tags.push(ctx.channelId);
|
|
854
|
-
if (ctx.trigger) tags.push(ctx.trigger === "cron" && ctx.jobId ? `cron:${ctx.jobId}` : ctx.trigger);
|
|
855
|
-
const metadata = {};
|
|
856
|
-
if (ctx.runId) metadata["openclaw.run.id"] = ctx.runId;
|
|
857
|
-
if (ctx.sessionId) metadata["openclaw.session.id"] = ctx.sessionId;
|
|
858
|
-
if (ctx.sessionKey) metadata["openclaw.session.key"] = ctx.sessionKey;
|
|
859
|
-
if (ctx.agentId) metadata["openclaw.agent.id"] = ctx.agentId;
|
|
860
|
-
if (ctx.workspaceDir) metadata["openclaw.workspace.dir"] = ctx.workspaceDir;
|
|
861
|
-
if (ctx.channelId) metadata["openclaw.channel.id"] = ctx.channelId;
|
|
862
|
-
if (ctx.messageProvider) metadata["openclaw.message.provider"] = ctx.messageProvider;
|
|
863
|
-
if (ctx.trigger) metadata["openclaw.trigger"] = ctx.trigger;
|
|
864
|
-
if (ctx.jobId) metadata["openclaw.cron.job.id"] = ctx.jobId;
|
|
865
|
-
if (ctx.modelProviderId) metadata["openclaw.model.provider.id"] = ctx.modelProviderId;
|
|
866
|
-
if (ctx.modelId) metadata["openclaw.model.id"] = ctx.modelId;
|
|
867
|
-
return {
|
|
868
|
-
"latitude.tags": tags.length > 0 ? tags : void 0,
|
|
869
|
-
"latitude.metadata": Object.keys(metadata).length > 0 ? metadata : void 0
|
|
870
|
-
};
|
|
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;
|
|
871
2301
|
}
|
|
872
|
-
|
|
873
|
-
|
|
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 {};
|
|
874
2308
|
return {
|
|
875
|
-
"
|
|
876
|
-
"gen_ai.
|
|
877
|
-
"gen_ai.usage.cache_read_input_tokens": usage.cacheRead,
|
|
878
|
-
"gen_ai.usage.cache_creation_input_tokens": usage.cacheWrite,
|
|
879
|
-
"gen_ai.usage.total_tokens": usage.total
|
|
2309
|
+
"session.id": sessionId,
|
|
2310
|
+
"gen_ai.session.id": sessionId
|
|
880
2311
|
};
|
|
881
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
|
+
}
|
|
882
2328
|
function hashHex(input, length) {
|
|
883
2329
|
return createHash("sha256").update(input).digest("hex").slice(0, length);
|
|
884
2330
|
}
|
|
885
2331
|
//#endregion
|
|
886
|
-
//#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;
|
|
887
2337
|
/**
|
|
888
|
-
*
|
|
889
|
-
*
|
|
890
|
-
*
|
|
891
|
-
*
|
|
892
|
-
* data-only feeds (llm_input/llm_output) that enrich the agent span.
|
|
893
|
-
*
|
|
894
|
-
* Every typed hook on OpenClaw's side fires fire-and-forget for non-modifying
|
|
895
|
-
* hooks; before_tool_call is a `runModifyingHook` where returning anything
|
|
896
|
-
* other than undefined blocks the tool call. Our handler returns nothing —
|
|
897
|
-
* 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.
|
|
898
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";
|
|
899
2423
|
function registerLatitudePlugin(api, opts = {}) {
|
|
900
2424
|
const config = opts.config ?? loadConfig(api.pluginConfig);
|
|
901
|
-
const logger = opts.logger ?? createLogger(config.debug);
|
|
2425
|
+
const logger = opts.logger ?? createLogger(config.debug, api.logger);
|
|
902
2426
|
if (!config.enabled) {
|
|
903
|
-
if (config.apiKey === "") logger.
|
|
904
|
-
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)");
|
|
905
2429
|
return;
|
|
906
2430
|
}
|
|
907
|
-
logger.debug(`enabled: project=${config.project} base=${config.baseUrl}
|
|
908
|
-
|
|
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
|
+
});
|
|
909
2471
|
const wrap = (name, fn) => {
|
|
910
2472
|
return (evt, ctx) => {
|
|
911
2473
|
try {
|
|
@@ -915,61 +2477,33 @@ function registerLatitudePlugin(api, opts = {}) {
|
|
|
915
2477
|
}
|
|
916
2478
|
};
|
|
917
2479
|
};
|
|
918
|
-
api.on("
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
api.on("model_call_started", wrap("model_call_started", (
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
api.on("
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
api.on("
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
api.on("
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
api.on("
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
api.on("after_compaction", wrap("after_compaction", (evt, ctx) => {
|
|
937
|
-
builder.onAfterCompaction(evt, ctx);
|
|
938
|
-
}));
|
|
939
|
-
api.on("subagent_spawned", wrap("subagent_spawned", (evt, ctx) => {
|
|
940
|
-
builder.onSubagentSpawned(evt, ctx);
|
|
941
|
-
}));
|
|
942
|
-
api.on("subagent_ended", wrap("subagent_ended", (evt, ctx) => {
|
|
943
|
-
builder.onSubagentEnded(evt, ctx);
|
|
944
|
-
}));
|
|
945
|
-
api.on("llm_input", wrap("llm_input", (evt, ctx) => {
|
|
946
|
-
builder.onLlmInput(evt, ctx);
|
|
947
|
-
}));
|
|
948
|
-
api.on("llm_output", wrap("llm_output", (evt, ctx) => {
|
|
949
|
-
builder.onLlmOutput(evt, ctx);
|
|
950
|
-
}));
|
|
951
|
-
api.on("agent_end", wrap("agent_end", (evt, ctx) => {
|
|
952
|
-
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) => {
|
|
953
2498
|
try {
|
|
954
|
-
|
|
955
|
-
if (!result) {
|
|
956
|
-
logger.debug("agent_end fired without a matching run in flight");
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
|
-
opts.onEmit?.(result);
|
|
960
|
-
const payload = buildOtlpRequest(result, { allowConversationAccess: config.allowConversationAccess });
|
|
961
|
-
postTraces({
|
|
962
|
-
baseUrl: config.baseUrl,
|
|
963
|
-
apiKey: config.apiKey,
|
|
964
|
-
project: config.project,
|
|
965
|
-
payload,
|
|
966
|
-
logger
|
|
967
|
-
});
|
|
2499
|
+
builder.onAgentEvent(evt);
|
|
968
2500
|
} catch (err) {
|
|
969
|
-
logger.warn(`
|
|
2501
|
+
logger.warn(`agent event handler failed: ${String(err)}`);
|
|
970
2502
|
}
|
|
971
2503
|
});
|
|
972
|
-
})
|
|
2504
|
+
} catch (err) {
|
|
2505
|
+
logger.debug(`agent event stream unavailable: ${String(err)}`);
|
|
2506
|
+
}
|
|
973
2507
|
}
|
|
974
2508
|
//#endregion
|
|
975
2509
|
export { registerLatitudePlugin as default };
|