coze_lab 0.1.30 → 0.1.32
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/index.js
CHANGED
|
@@ -5393,6 +5393,12 @@ function getCloudCozeloopApiBaseUrl() {
|
|
|
5393
5393
|
function normalizeCozeloopApiBaseUrl(raw) {
|
|
5394
5394
|
const base = raw.trim().replace(/\/+$/, '');
|
|
5395
5395
|
if (!base) return '';
|
|
5396
|
+
if (base.endsWith('/v1/loop/traces/ingest')) {
|
|
5397
|
+
return base.slice(0, -'/v1/loop/traces/ingest'.length).replace(/\/+$/, '');
|
|
5398
|
+
}
|
|
5399
|
+
if (base.endsWith('/api/v1/loop/traces/ingest')) {
|
|
5400
|
+
return base.slice(0, -'/v1/loop/traces/ingest'.length).replace(/\/+$/, '');
|
|
5401
|
+
}
|
|
5396
5402
|
if (base.endsWith('/v1/loop/opentelemetry/v1/traces')) {
|
|
5397
5403
|
return base.slice(0, -'/v1/loop/opentelemetry/v1/traces'.length).replace(/\/+$/, '');
|
|
5398
5404
|
}
|
|
@@ -5431,6 +5437,14 @@ function getOtelTracesUrl(cloud) {
|
|
|
5431
5437
|
return `${getOtelEndpointBase(cloud).replace(/\/+$/, '')}/v1/traces`;
|
|
5432
5438
|
}
|
|
5433
5439
|
|
|
5440
|
+
function getCozeloopIngestUrlFromBase(apiBase) {
|
|
5441
|
+
return `${String(apiBase || COZE_API).replace(/\/+$/, '')}/v1/loop/traces/ingest`;
|
|
5442
|
+
}
|
|
5443
|
+
|
|
5444
|
+
function getCozeloopIngestUrl(cloud) {
|
|
5445
|
+
return getCozeloopIngestUrlFromBase(getCozeloopApiBaseUrl(cloud));
|
|
5446
|
+
}
|
|
5447
|
+
|
|
5434
5448
|
function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud) {
|
|
5435
5449
|
if (!existing.plugins) existing.plugins = {};
|
|
5436
5450
|
if (!existing.plugins.allow) existing.plugins.allow = [];
|
|
@@ -5450,7 +5464,9 @@ function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud)
|
|
|
5450
5464
|
if (!existing.plugins.entries[PLUGIN].config) existing.plugins.entries[PLUGIN].config = {};
|
|
5451
5465
|
const pcfg = existing.plugins.entries[PLUGIN].config;
|
|
5452
5466
|
pcfg.authorization = `Bearer ${token}`;
|
|
5453
|
-
|
|
5467
|
+
// OpenClaw runtime uses CozeLoop ingest protocol, not OTLP. Keep accepting
|
|
5468
|
+
// OTEL_ENDPOINT in cloud, but normalize it to the API base URL before writing.
|
|
5469
|
+
pcfg.endpoint = getCozeloopApiBaseUrl(cloud);
|
|
5454
5470
|
pcfg.workspaceId = workspaceId;
|
|
5455
5471
|
pcfg.debug = true;
|
|
5456
5472
|
pcfg.disableLocalCredentials = !!cloud;
|
|
@@ -5848,48 +5864,50 @@ async function verifyOpenClawTraceLink(cloud) {
|
|
|
5848
5864
|
return { success: false, status: 0, body: 'plugin authorization missing' };
|
|
5849
5865
|
}
|
|
5850
5866
|
|
|
5851
|
-
// 1) 用插件【实际配置的】token 打一条最小
|
|
5852
|
-
// 不能发空 resourceSpans,CozeLoop 会返回 "unknown event type",导致自检假失败。
|
|
5867
|
+
// 1) 用插件【实际配置的】token 打一条最小 CozeLoop ingest trace ——这才是运行时真实用的那个 token。
|
|
5853
5868
|
const authHeader = pcfg.authorization; // 形如 "Bearer czu_xxx"
|
|
5854
|
-
const tracesUrl = (pcfg.endpoint
|
|
5855
|
-
? `${String(pcfg.endpoint).replace(/\/+$/, '')}/v1/traces`
|
|
5856
|
-
: getOtelTracesUrl(cloud));
|
|
5869
|
+
const tracesUrl = getCozeloopIngestUrlFromBase(normalizeCozeloopApiBaseUrl(pcfg.endpoint || getCozeloopApiBaseUrl(cloud)));
|
|
5857
5870
|
const workspaceId = pcfg.workspaceId || WORKSPACE_ID;
|
|
5858
5871
|
const traceId = crypto.randomBytes(16).toString('hex');
|
|
5859
5872
|
const spanId = crypto.randomBytes(8).toString('hex');
|
|
5860
|
-
const
|
|
5873
|
+
const nowMicros = Date.now() * 1000;
|
|
5861
5874
|
const pair = crypto.randomBytes(6).toString('hex');
|
|
5862
|
-
const
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5875
|
+
const ingestBody = {
|
|
5876
|
+
spans: [{
|
|
5877
|
+
started_at_micros: nowMicros,
|
|
5878
|
+
log_id: '',
|
|
5879
|
+
span_id: spanId,
|
|
5880
|
+
parent_id: '0',
|
|
5881
|
+
trace_id: traceId,
|
|
5882
|
+
duration_micros: 1,
|
|
5883
|
+
service_name: 'cozelab-onboard-openclaw',
|
|
5884
|
+
workspace_id: String(workspaceId),
|
|
5885
|
+
span_name: 'cozelab-onboard-openclaw-selfcheck',
|
|
5886
|
+
span_type: 'main',
|
|
5887
|
+
status_code: 0,
|
|
5888
|
+
input: 'cozelab-onboard-openclaw selfcheck',
|
|
5889
|
+
output: 'ok',
|
|
5890
|
+
object_storage: '',
|
|
5891
|
+
system_tags_string: {
|
|
5892
|
+
runtime: JSON.stringify({ language: 'nodejs', library: 'openclaw', scene: process.env.COZELOOP_SCENE || 'custom' }),
|
|
5868
5893
|
},
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
status: { code: 1 },
|
|
5879
|
-
attributes: [
|
|
5880
|
-
{ key: 'pair_code', value: { stringValue: pair } },
|
|
5881
|
-
{ key: 'source', value: { stringValue: 'cozelab-onboard-openclaw' } },
|
|
5882
|
-
],
|
|
5883
|
-
}],
|
|
5884
|
-
}],
|
|
5894
|
+
system_tags_long: {},
|
|
5895
|
+
system_tags_double: {},
|
|
5896
|
+
tags_string: {
|
|
5897
|
+
pair_code: pair,
|
|
5898
|
+
source: 'cozelab-onboard-openclaw',
|
|
5899
|
+
},
|
|
5900
|
+
tags_long: {},
|
|
5901
|
+
tags_double: {},
|
|
5902
|
+
tags_bool: {},
|
|
5885
5903
|
}],
|
|
5886
5904
|
};
|
|
5887
5905
|
let res;
|
|
5888
5906
|
try {
|
|
5889
5907
|
res = await httpsPost(
|
|
5890
5908
|
tracesUrl,
|
|
5891
|
-
|
|
5892
|
-
{ Authorization: authHeader
|
|
5909
|
+
ingestBody,
|
|
5910
|
+
{ Authorization: authHeader },
|
|
5893
5911
|
);
|
|
5894
5912
|
} catch (e) {
|
|
5895
5913
|
warn(`openclaw 插件 token 上报探测失败: ${e.message}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { trace, context, SpanKind, SpanStatusCode } from "@opentelemetry/api";
|
|
2
2
|
import { BasicTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
3
|
-
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
4
3
|
import { Resource } from "@opentelemetry/resources";
|
|
5
4
|
import { ATTR_SERVICE_NAME, ATTR_SERVICE_INSTANCE_ID } from "@opentelemetry/semantic-conventions";
|
|
6
5
|
import { hostname } from "os";
|
|
@@ -8,6 +7,7 @@ import { basename, join } from "path";
|
|
|
8
7
|
import { createRequire } from "node:module";
|
|
9
8
|
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
10
9
|
import { homedir } from "os";
|
|
10
|
+
import http from "http";
|
|
11
11
|
import https from "https";
|
|
12
12
|
|
|
13
13
|
const require = createRequire(import.meta.url);
|
|
@@ -78,6 +78,238 @@ async function getRefreshedToken(currentAuthorization, opts = {}) {
|
|
|
78
78
|
}
|
|
79
79
|
// ─────────────────────────────────────────────────────────────────────────
|
|
80
80
|
|
|
81
|
+
const EXPORT_SUCCESS = 0;
|
|
82
|
+
const EXPORT_FAILED = 1;
|
|
83
|
+
const INGEST_TRACE_PATH = "/v1/loop/traces/ingest";
|
|
84
|
+
|
|
85
|
+
function normalizeApiBaseUrl(endpoint) {
|
|
86
|
+
const base = String(endpoint || "").replace(/\/+$/, "");
|
|
87
|
+
if (!base)
|
|
88
|
+
return _COZE_API;
|
|
89
|
+
const suffixes = [
|
|
90
|
+
"/v1/loop/traces/ingest",
|
|
91
|
+
"/api/v1/loop/traces/ingest",
|
|
92
|
+
"/v1/loop/opentelemetry/v1/traces",
|
|
93
|
+
"/api/v1/loop/opentelemetry/v1/traces",
|
|
94
|
+
"/v1/loop/opentelemetry",
|
|
95
|
+
"/api/v1/loop/opentelemetry",
|
|
96
|
+
"/v1/traces",
|
|
97
|
+
"/api/v1",
|
|
98
|
+
];
|
|
99
|
+
for (const suffix of suffixes) {
|
|
100
|
+
if (base.endsWith(suffix)) {
|
|
101
|
+
const trimSuffix = suffix.startsWith("/api/") ? suffix.slice("/api".length) : suffix;
|
|
102
|
+
return base.slice(0, -trimSuffix.length).replace(/\/+$/, "");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return base;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeIngestUrl(endpoint) {
|
|
109
|
+
const base = String(endpoint || "").replace(/\/+$/, "");
|
|
110
|
+
if (base.endsWith(INGEST_TRACE_PATH))
|
|
111
|
+
return base;
|
|
112
|
+
return `${normalizeApiBaseUrl(base)}${INGEST_TRACE_PATH}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function hrTimeToUnixMicros(time) {
|
|
116
|
+
if (Array.isArray(time)) {
|
|
117
|
+
return Number(BigInt(time[0]) * 1000000n + BigInt(Math.trunc(time[1] / 1000)));
|
|
118
|
+
}
|
|
119
|
+
const millis = time instanceof Date ? time.getTime() : Number(time || Date.now());
|
|
120
|
+
return Math.trunc(millis * 1000);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function hrDurationToMicros(duration) {
|
|
124
|
+
if (Array.isArray(duration)) {
|
|
125
|
+
return Number(BigInt(duration[0]) * 1000000n + BigInt(Math.trunc(duration[1] / 1000)));
|
|
126
|
+
}
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function safeStringify(value) {
|
|
131
|
+
if (value === undefined || value === null)
|
|
132
|
+
return "";
|
|
133
|
+
if (typeof value === "string")
|
|
134
|
+
return value;
|
|
135
|
+
try {
|
|
136
|
+
return JSON.stringify(value);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return String(value);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function mapSpanType(type) {
|
|
144
|
+
switch (type) {
|
|
145
|
+
case "entry":
|
|
146
|
+
case "gateway":
|
|
147
|
+
case "message":
|
|
148
|
+
case "session":
|
|
149
|
+
return "main";
|
|
150
|
+
case "agent":
|
|
151
|
+
case "model":
|
|
152
|
+
case "tool":
|
|
153
|
+
return type;
|
|
154
|
+
default:
|
|
155
|
+
return type || "main";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function putTypedTag(maps, key, value) {
|
|
160
|
+
if (value === undefined || value === null || key === "")
|
|
161
|
+
return;
|
|
162
|
+
if (key === "cozeloop.span_type" || key === "cozeloop.input" || key === "cozeloop.output" ||
|
|
163
|
+
key === "cozeloop.system_tag_runtime") {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (typeof value === "boolean") {
|
|
167
|
+
maps.bool[key] = value;
|
|
168
|
+
}
|
|
169
|
+
else if (typeof value === "number" && Number.isInteger(value)) {
|
|
170
|
+
maps.long[key] = value;
|
|
171
|
+
}
|
|
172
|
+
else if (typeof value === "number") {
|
|
173
|
+
maps.double[key] = value;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
maps.string[key] = typeof value === "string" ? value : safeStringify(value);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function splitAttributes(attributes) {
|
|
181
|
+
const tags = { string: {}, long: {}, double: {}, bool: {} };
|
|
182
|
+
const systemTags = { string: {}, long: {}, double: {} };
|
|
183
|
+
for (const [key, value] of Object.entries(attributes || {})) {
|
|
184
|
+
if (key === "cozeloop.system_tag_runtime") {
|
|
185
|
+
systemTags.string.runtime = safeStringify(value);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
putTypedTag(tags, key, value);
|
|
189
|
+
}
|
|
190
|
+
return { tags, systemTags };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function spanToUploadSpan(span, config) {
|
|
194
|
+
const spanContext = span.spanContext();
|
|
195
|
+
const attrs = span.attributes || {};
|
|
196
|
+
const { tags, systemTags } = splitAttributes(attrs);
|
|
197
|
+
const resourceAttrs = span.resource?.attributes || {};
|
|
198
|
+
const serviceName = resourceAttrs["service.name"] || config.serviceName || "openclaw-agent";
|
|
199
|
+
const spanType = mapSpanType(attrs["cozeloop.span_type"]);
|
|
200
|
+
const input = attrs["cozeloop.input"] !== undefined ? safeStringify(attrs["cozeloop.input"]) : "";
|
|
201
|
+
const output = attrs["cozeloop.output"] !== undefined ? safeStringify(attrs["cozeloop.output"]) : "";
|
|
202
|
+
const hasError = span.status?.code === SpanStatusCode.ERROR || attrs.error === true || attrs["tool.error"] === true;
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
started_at_micros: hrTimeToUnixMicros(span.startTime),
|
|
206
|
+
log_id: "",
|
|
207
|
+
traceId: spanContext.traceId,
|
|
208
|
+
trace_id: spanContext.traceId,
|
|
209
|
+
span_id: spanContext.spanId,
|
|
210
|
+
parent_id: span.parentSpanId || "0",
|
|
211
|
+
duration_micros: hrDurationToMicros(span.duration),
|
|
212
|
+
service_name: String(serviceName),
|
|
213
|
+
workspace_id: String(config.workspaceId || ""),
|
|
214
|
+
span_name: String(span.name || "unknown"),
|
|
215
|
+
span_type: spanType,
|
|
216
|
+
status_code: hasError ? -1 : 0,
|
|
217
|
+
input,
|
|
218
|
+
output,
|
|
219
|
+
object_storage: "",
|
|
220
|
+
system_tags_string: systemTags.string,
|
|
221
|
+
system_tags_long: systemTags.long,
|
|
222
|
+
system_tags_double: systemTags.double,
|
|
223
|
+
tags_string: tags.string,
|
|
224
|
+
tags_long: tags.long,
|
|
225
|
+
tags_double: tags.double,
|
|
226
|
+
tags_bool: tags.bool,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function postJson(url, body, headers) {
|
|
231
|
+
return new Promise((resolve, reject) => {
|
|
232
|
+
const data = JSON.stringify(body);
|
|
233
|
+
const u = new URL(url);
|
|
234
|
+
const client = u.protocol === "http:" ? http : https;
|
|
235
|
+
const req = client.request({
|
|
236
|
+
protocol: u.protocol,
|
|
237
|
+
hostname: u.hostname,
|
|
238
|
+
port: u.port || undefined,
|
|
239
|
+
path: u.pathname + u.search,
|
|
240
|
+
method: "POST",
|
|
241
|
+
headers: {
|
|
242
|
+
"Content-Type": "application/json",
|
|
243
|
+
"Content-Length": Buffer.byteLength(data),
|
|
244
|
+
...(headers || {}),
|
|
245
|
+
},
|
|
246
|
+
}, (res) => {
|
|
247
|
+
let buf = "";
|
|
248
|
+
res.on("data", c => buf += c);
|
|
249
|
+
res.on("end", () => resolve({ status: res.statusCode || 0, body: buf }));
|
|
250
|
+
});
|
|
251
|
+
req.on("error", reject);
|
|
252
|
+
req.setTimeout(15000, () => req.destroy(new Error("CozeLoop ingest export timed out")));
|
|
253
|
+
req.write(data);
|
|
254
|
+
req.end();
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
class CozeloopIngestExporter {
|
|
259
|
+
constructor(config) {
|
|
260
|
+
this.url = normalizeIngestUrl(config.endpoint || config.url);
|
|
261
|
+
this.headers = config.headers || {};
|
|
262
|
+
this.logger = config.logger;
|
|
263
|
+
this.workspaceId = config.workspaceId;
|
|
264
|
+
this.serviceName = config.serviceName;
|
|
265
|
+
this.shutdownRequested = false;
|
|
266
|
+
}
|
|
267
|
+
export(spans, resultCallback) {
|
|
268
|
+
if (!spans || spans.length === 0 || this.shutdownRequested) {
|
|
269
|
+
resultCallback({ code: EXPORT_SUCCESS });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
this.postSpans(spans)
|
|
273
|
+
.then(() => resultCallback({ code: EXPORT_SUCCESS }))
|
|
274
|
+
.catch((err) => {
|
|
275
|
+
this.logger?.error?.(`[CozeloopTrace] CozeLoop ingest export failed: ${err?.message || err}`);
|
|
276
|
+
resultCallback({ code: EXPORT_FAILED, error: err });
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
async postSpans(spans) {
|
|
280
|
+
const body = {
|
|
281
|
+
spans: spans.map((span) => spanToUploadSpan(span, {
|
|
282
|
+
workspaceId: this.workspaceId,
|
|
283
|
+
serviceName: this.serviceName,
|
|
284
|
+
})),
|
|
285
|
+
};
|
|
286
|
+
const res = await postJson(this.url, body, this.headers);
|
|
287
|
+
if (res.status < 200 || res.status >= 300) {
|
|
288
|
+
const snippet = String(res.body || "").slice(0, 300);
|
|
289
|
+
throw new Error(`HTTP ${res.status}${snippet ? `: ${snippet}` : ""}`);
|
|
290
|
+
}
|
|
291
|
+
if (res.body) {
|
|
292
|
+
try {
|
|
293
|
+
const parsed = JSON.parse(res.body);
|
|
294
|
+
if (parsed && parsed.code !== undefined && parsed.code !== 0) {
|
|
295
|
+
throw new Error(`code ${parsed.code}: ${parsed.msg || res.body.slice(0, 300)}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
if (/^code\s/.test(String(err?.message || ""))) {
|
|
300
|
+
throw err;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async forceFlush() {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
async shutdown() {
|
|
309
|
+
this.shutdownRequested = true;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
81
313
|
export class CozeloopExporter {
|
|
82
314
|
config;
|
|
83
315
|
api;
|
|
@@ -173,11 +405,13 @@ export class CozeloopExporter {
|
|
|
173
405
|
const authorization = this.config.authorization;
|
|
174
406
|
const workspaceId = this.config.workspaceId;
|
|
175
407
|
this.api.logger.info(`[CozeloopTrace] Using authorization, workspaceId=${workspaceId}, tokenLength=${authorization?.length}`);
|
|
176
|
-
const exporter = new
|
|
177
|
-
|
|
408
|
+
const exporter = new CozeloopIngestExporter({
|
|
409
|
+
endpoint: this.config.endpoint,
|
|
410
|
+
logger: this.api.logger,
|
|
411
|
+
workspaceId,
|
|
412
|
+
serviceName: this.config.serviceName,
|
|
178
413
|
headers: {
|
|
179
414
|
"Authorization": authorization,
|
|
180
|
-
"cozeloop-workspace-id": workspaceId,
|
|
181
415
|
"x-tt-env": "ppe_cozelab",
|
|
182
416
|
"x-use-ppe": "1",
|
|
183
417
|
},
|
|
@@ -441,12 +675,22 @@ export class CozeloopExporter {
|
|
|
441
675
|
}
|
|
442
676
|
async flush() {
|
|
443
677
|
if (this.provider) {
|
|
444
|
-
|
|
678
|
+
try {
|
|
679
|
+
await this.provider.forceFlush();
|
|
680
|
+
}
|
|
681
|
+
catch (err) {
|
|
682
|
+
this.api.logger.error(`[CozeloopTrace] Flush failed: ${err?.message || err}`);
|
|
683
|
+
}
|
|
445
684
|
}
|
|
446
685
|
}
|
|
447
686
|
async dispose() {
|
|
448
687
|
if (this.provider) {
|
|
449
|
-
|
|
688
|
+
try {
|
|
689
|
+
await this.provider.shutdown();
|
|
690
|
+
}
|
|
691
|
+
catch (err) {
|
|
692
|
+
this.api.logger.error(`[CozeloopTrace] Shutdown failed: ${err?.message || err}`);
|
|
693
|
+
}
|
|
450
694
|
}
|
|
451
695
|
}
|
|
452
696
|
}
|
|
@@ -476,20 +476,39 @@ function resolveCozeContext(input, sessionId) {
|
|
|
476
476
|
return cozeContextBySession.get(sessionId);
|
|
477
477
|
return lastCozeContext || {};
|
|
478
478
|
}
|
|
479
|
+
function resetRuntimeState() {
|
|
480
|
+
lastUserChannelId = undefined;
|
|
481
|
+
lastUserTraceContext = undefined;
|
|
482
|
+
lastOpenclawSessionId = undefined;
|
|
483
|
+
cozeContextBySession.clear();
|
|
484
|
+
lastCozeContext = undefined;
|
|
485
|
+
activeAgentCtx = undefined;
|
|
486
|
+
activeAgentChannelId = undefined;
|
|
487
|
+
activeAgentContextByKey.clear();
|
|
488
|
+
lastUserInput = undefined;
|
|
489
|
+
lastUserContextByKey.clear();
|
|
490
|
+
pendingToolCallsByKey.clear();
|
|
491
|
+
}
|
|
479
492
|
// Active agent context: set in before_agent_start, cleared in agent_end.
|
|
480
493
|
// All hooks between these two (llm_input, llm_output, tool calls, messages)
|
|
481
494
|
// use this to ensure every span lands in the same Trace.
|
|
482
495
|
let activeAgentCtx;
|
|
483
496
|
let activeAgentChannelId;
|
|
497
|
+
const activeAgentContextByKey = new Map();
|
|
484
498
|
// Latest user input captured from message_received, independent of any ctx.
|
|
485
499
|
// Used by ensureRootSpan as a reliable fallback for the root span's input.
|
|
486
500
|
let lastUserInput;
|
|
487
|
-
|
|
501
|
+
const lastUserContextByKey = new Map();
|
|
502
|
+
const pendingToolCallsByKey = new Map();
|
|
488
503
|
function resolveOpenclawSessionId(hookCtx, eventSessionId) {
|
|
489
504
|
const raw = hookCtx.sessionId?.trim() || eventSessionId?.trim();
|
|
490
|
-
if (
|
|
491
|
-
return
|
|
492
|
-
|
|
505
|
+
if (raw)
|
|
506
|
+
return raw;
|
|
507
|
+
const sessionKey = hookCtx.sessionKey?.trim();
|
|
508
|
+
const match = sessionKey?.match(/^agent:[^:]+:(.+)$/);
|
|
509
|
+
if (match?.[1])
|
|
510
|
+
return match[1];
|
|
511
|
+
return undefined;
|
|
493
512
|
}
|
|
494
513
|
const cozeloopTracePlugin = {
|
|
495
514
|
id: "openclaw-cozeloop-trace",
|
|
@@ -497,6 +516,7 @@ const cozeloopTracePlugin = {
|
|
|
497
516
|
version: PLUGIN_VERSION,
|
|
498
517
|
description: "Report OpenClaw execution traces to CozeLoop via OpenTelemetry",
|
|
499
518
|
activate(api) {
|
|
519
|
+
resetRuntimeState();
|
|
500
520
|
const pluginConfig = api.pluginConfig || {};
|
|
501
521
|
const authorization = pluginConfig.authorization;
|
|
502
522
|
const workspaceId = pluginConfig.workspaceId;
|
|
@@ -505,7 +525,7 @@ const cozeloopTracePlugin = {
|
|
|
505
525
|
return;
|
|
506
526
|
}
|
|
507
527
|
const config = {
|
|
508
|
-
endpoint: pluginConfig.endpoint || "https://api.coze.cn
|
|
528
|
+
endpoint: pluginConfig.endpoint || "https://api.coze.cn",
|
|
509
529
|
authorization,
|
|
510
530
|
workspaceId,
|
|
511
531
|
serviceName: pluginConfig.serviceName || "openclaw-agent",
|
|
@@ -558,6 +578,30 @@ const cozeloopTracePlugin = {
|
|
|
558
578
|
const ctx = contextByRunId.get(runId);
|
|
559
579
|
return ctx?.originalChannelId || ctx?.channelId;
|
|
560
580
|
};
|
|
581
|
+
const sessionIdFromChannelId = (channelId) => {
|
|
582
|
+
const match = String(channelId || "").match(/^agent\/[^:]+:(.+)$/);
|
|
583
|
+
return match?.[1];
|
|
584
|
+
};
|
|
585
|
+
const canFallbackToLastUserContext = (rawChannelId) => {
|
|
586
|
+
const rawSessionId = sessionIdFromChannelId(rawChannelId);
|
|
587
|
+
const lastSessionId = lastUserTraceContext?.openclawSessionId || sessionIdFromChannelId(lastUserChannelId);
|
|
588
|
+
return !rawSessionId || !lastSessionId || rawSessionId === lastSessionId;
|
|
589
|
+
};
|
|
590
|
+
const canFallbackToActiveAgentContext = (rawChannelId) => {
|
|
591
|
+
const rawSessionId = sessionIdFromChannelId(rawChannelId);
|
|
592
|
+
const activeSessionId = activeAgentCtx?.openclawSessionId || sessionIdFromChannelId(activeAgentChannelId);
|
|
593
|
+
return !rawSessionId || !activeSessionId || rawSessionId === activeSessionId;
|
|
594
|
+
};
|
|
595
|
+
const canUseLastUserFallbackFor = (ctx, rawChannelId) => {
|
|
596
|
+
if (!lastUserTraceContext || lastUserTraceContext === ctx)
|
|
597
|
+
return true;
|
|
598
|
+
const rawSessionId = ctx?.openclawSessionId || sessionIdFromChannelId(rawChannelId);
|
|
599
|
+
const lastSessionId = lastUserTraceContext.openclawSessionId || sessionIdFromChannelId(lastUserChannelId);
|
|
600
|
+
return !rawSessionId || !lastSessionId || rawSessionId === lastSessionId;
|
|
601
|
+
};
|
|
602
|
+
const lastUserFallbackFor = (ctx, rawChannelId) => {
|
|
603
|
+
return canUseLastUserFallbackFor(ctx, rawChannelId) ? lastUserTraceContext : undefined;
|
|
604
|
+
};
|
|
561
605
|
const startTurn = (runId, channelId, originalChannelId, openclawSessionId) => {
|
|
562
606
|
const traceId = generateId(32);
|
|
563
607
|
const ctx = {
|
|
@@ -583,7 +627,7 @@ const cozeloopTracePlugin = {
|
|
|
583
627
|
const getOrCreateContext = (rawChannelId, runId, hookName) => {
|
|
584
628
|
let channelId = rawChannelId;
|
|
585
629
|
let activeCtx = getContextByChannel(rawChannelId);
|
|
586
|
-
const effectiveRunId = runId || activeCtx?.runId || `run-${Date.now()}`;
|
|
630
|
+
const effectiveRunId = runId || activeCtx?.runId || `run-${Date.now()}-${generateId(8)}`;
|
|
587
631
|
if (rawChannelId.startsWith("agent/") && effectiveRunId) {
|
|
588
632
|
const originalChannelId = getOriginalChannelId(effectiveRunId);
|
|
589
633
|
if (originalChannelId) {
|
|
@@ -594,7 +638,7 @@ const cozeloopTracePlugin = {
|
|
|
594
638
|
if (!activeCtx) {
|
|
595
639
|
activeCtx = getContextByRun(effectiveRunId);
|
|
596
640
|
}
|
|
597
|
-
if (!activeCtx && rawChannelId.startsWith("agent/") && lastUserTraceContext) {
|
|
641
|
+
if (!activeCtx && rawChannelId.startsWith("agent/") && lastUserTraceContext && canFallbackToLastUserContext(rawChannelId)) {
|
|
598
642
|
activeCtx = lastUserTraceContext;
|
|
599
643
|
channelId = lastUserChannelId || channelId;
|
|
600
644
|
contextByChannelId.set(rawChannelId, activeCtx);
|
|
@@ -616,19 +660,140 @@ const cozeloopTracePlugin = {
|
|
|
616
660
|
}
|
|
617
661
|
return { ctx: activeCtx, channelId, isNew };
|
|
618
662
|
};
|
|
663
|
+
const contextKeys = (hookCtx, channelId, eventSessionId) => {
|
|
664
|
+
const keys = [];
|
|
665
|
+
const add = (value) => {
|
|
666
|
+
const s = String(value || "").trim();
|
|
667
|
+
if (s && !keys.includes(s))
|
|
668
|
+
keys.push(s);
|
|
669
|
+
};
|
|
670
|
+
add(resolveOpenclawSessionId(hookCtx || {}, eventSessionId));
|
|
671
|
+
add(hookCtx?.sessionKey);
|
|
672
|
+
add(hookCtx?.conversationId);
|
|
673
|
+
add(channelId);
|
|
674
|
+
return keys;
|
|
675
|
+
};
|
|
676
|
+
const rememberLastUserContext = (hookCtx, channelId, ctx) => {
|
|
677
|
+
for (const key of contextKeys(hookCtx, channelId)) {
|
|
678
|
+
lastUserContextByKey.set(key, { ctx, channelId });
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
const resolveLastUserContext = (hookCtx, channelId) => {
|
|
682
|
+
for (const key of contextKeys(hookCtx, channelId)) {
|
|
683
|
+
const found = lastUserContextByKey.get(key);
|
|
684
|
+
if (found)
|
|
685
|
+
return found;
|
|
686
|
+
}
|
|
687
|
+
if (lastUserTraceContext) {
|
|
688
|
+
if (canFallbackToLastUserContext(channelId)) {
|
|
689
|
+
return { ctx: lastUserTraceContext, channelId: lastUserChannelId || channelId };
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return undefined;
|
|
693
|
+
};
|
|
694
|
+
const rememberActiveAgentContext = (hookCtx, channelId, ctx) => {
|
|
695
|
+
for (const key of contextKeys(hookCtx, channelId, ctx.openclawSessionId)) {
|
|
696
|
+
activeAgentContextByKey.set(key, { ctx, channelId });
|
|
697
|
+
}
|
|
698
|
+
activeAgentCtx = ctx;
|
|
699
|
+
activeAgentChannelId = channelId;
|
|
700
|
+
};
|
|
701
|
+
const resolveActiveAgentContext = (hookCtx, channelId) => {
|
|
702
|
+
for (const key of contextKeys(hookCtx, channelId)) {
|
|
703
|
+
const found = activeAgentContextByKey.get(key);
|
|
704
|
+
if (found)
|
|
705
|
+
return found;
|
|
706
|
+
}
|
|
707
|
+
if (activeAgentCtx) {
|
|
708
|
+
if (canFallbackToActiveAgentContext(channelId)) {
|
|
709
|
+
return { ctx: activeAgentCtx, channelId: activeAgentChannelId || channelId };
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return undefined;
|
|
713
|
+
};
|
|
714
|
+
const clearActiveAgentContext = (ctx) => {
|
|
715
|
+
for (const [key, found] of activeAgentContextByKey.entries()) {
|
|
716
|
+
if (found.ctx === ctx)
|
|
717
|
+
activeAgentContextByKey.delete(key);
|
|
718
|
+
}
|
|
719
|
+
if (activeAgentCtx === ctx) {
|
|
720
|
+
activeAgentCtx = undefined;
|
|
721
|
+
activeAgentChannelId = undefined;
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
const clearLastUserContext = (ctx) => {
|
|
725
|
+
for (const [key, found] of lastUserContextByKey.entries()) {
|
|
726
|
+
if (found.ctx === ctx)
|
|
727
|
+
lastUserContextByKey.delete(key);
|
|
728
|
+
}
|
|
729
|
+
if (lastUserTraceContext === ctx) {
|
|
730
|
+
lastUserTraceContext = undefined;
|
|
731
|
+
lastUserChannelId = undefined;
|
|
732
|
+
}
|
|
733
|
+
};
|
|
619
734
|
// Resolve context for hooks that fire between before_agent_start and
|
|
620
735
|
// agent_end. When an agent is active, always return that agent's context
|
|
621
736
|
// so every span ends up in the same Trace regardless of channelId drift.
|
|
622
|
-
const resolveActiveContext = (rawChannelId, runId, hookName) => {
|
|
623
|
-
|
|
737
|
+
const resolveActiveContext = (rawChannelId, runId, hookName, hookCtx) => {
|
|
738
|
+
const active = resolveActiveAgentContext(hookCtx || {}, rawChannelId);
|
|
739
|
+
if (active) {
|
|
624
740
|
if (config.debug) {
|
|
625
|
-
api.logger.info(`[CozeloopTrace] Using activeAgentCtx for ${hookName}: traceId=${
|
|
741
|
+
api.logger.info(`[CozeloopTrace] Using activeAgentCtx for ${hookName}: traceId=${active.ctx.traceId}, rootSpanId=${active.ctx.rootSpanId}`);
|
|
626
742
|
}
|
|
627
|
-
return
|
|
743
|
+
return active;
|
|
628
744
|
}
|
|
629
745
|
const { ctx, channelId } = getOrCreateContext(rawChannelId, runId, hookName);
|
|
630
746
|
return { ctx, channelId };
|
|
631
747
|
};
|
|
748
|
+
const getToolCallId = (event) => {
|
|
749
|
+
const candidates = [
|
|
750
|
+
event?.toolCallId,
|
|
751
|
+
event?.tool_call_id,
|
|
752
|
+
event?.callId,
|
|
753
|
+
event?.id,
|
|
754
|
+
event?.requestId,
|
|
755
|
+
event?.invocationId,
|
|
756
|
+
event?.params?.toolCallId,
|
|
757
|
+
event?.params?.tool_call_id,
|
|
758
|
+
];
|
|
759
|
+
const found = candidates.find((value) => value !== undefined && value !== null && String(value).trim());
|
|
760
|
+
return found === undefined ? undefined : String(found);
|
|
761
|
+
};
|
|
762
|
+
const pendingToolKey = (ctx, channelId, toolName, toolCallId) => {
|
|
763
|
+
const traceKey = ctx.rootSpanId || ctx.traceId || channelId;
|
|
764
|
+
if (toolCallId)
|
|
765
|
+
return `${traceKey}:id:${toolCallId}`;
|
|
766
|
+
return `${traceKey}:name:${toolName || "unknown_tool"}`;
|
|
767
|
+
};
|
|
768
|
+
const pushPendingToolCall = (ctx, channelId, pending) => {
|
|
769
|
+
const key = pendingToolKey(ctx, channelId, pending.toolName, pending.toolCallId);
|
|
770
|
+
const queue = pendingToolCallsByKey.get(key) || [];
|
|
771
|
+
queue.push(pending);
|
|
772
|
+
pendingToolCallsByKey.set(key, queue);
|
|
773
|
+
return queue.length;
|
|
774
|
+
};
|
|
775
|
+
const shiftPendingToolCall = (ctx, channelId, toolName, toolCallId) => {
|
|
776
|
+
const key = pendingToolKey(ctx, channelId, toolName, toolCallId);
|
|
777
|
+
const queue = pendingToolCallsByKey.get(key);
|
|
778
|
+
if (!queue || queue.length === 0)
|
|
779
|
+
return undefined;
|
|
780
|
+
const pending = queue.shift();
|
|
781
|
+
if (queue.length === 0) {
|
|
782
|
+
pendingToolCallsByKey.delete(key);
|
|
783
|
+
}
|
|
784
|
+
return pending;
|
|
785
|
+
};
|
|
786
|
+
const clearPendingToolCallsForContext = (ctx) => {
|
|
787
|
+
for (const [key, queue] of pendingToolCallsByKey.entries()) {
|
|
788
|
+
const kept = queue.filter((pending) => pending.traceContext !== ctx);
|
|
789
|
+
if (kept.length === 0) {
|
|
790
|
+
pendingToolCallsByKey.delete(key);
|
|
791
|
+
}
|
|
792
|
+
else if (kept.length !== queue.length) {
|
|
793
|
+
pendingToolCallsByKey.set(key, kept);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
};
|
|
632
797
|
const createSpan = (ctx, channelId, name, type, startTime, endTime, attributes = {}, input, output, parentSpanId) => {
|
|
633
798
|
return {
|
|
634
799
|
name,
|
|
@@ -878,6 +1043,7 @@ const cozeloopTracePlugin = {
|
|
|
878
1043
|
if (role === "user" || !role) {
|
|
879
1044
|
lastUserChannelId = channelId;
|
|
880
1045
|
lastUserTraceContext = ctx;
|
|
1046
|
+
rememberLastUserContext(hookCtx, channelId, ctx);
|
|
881
1047
|
ctx.userInput = event.content;
|
|
882
1048
|
lastUserInput = event.content;
|
|
883
1049
|
rememberCozeContext(event.content, ctx.openclawSessionId || lastOpenclawSessionId);
|
|
@@ -891,20 +1057,22 @@ const cozeloopTracePlugin = {
|
|
|
891
1057
|
if (!lastUserTraceContext) {
|
|
892
1058
|
lastUserTraceContext = ctx;
|
|
893
1059
|
lastUserChannelId = channelId;
|
|
1060
|
+
rememberLastUserContext(hookCtx, channelId, ctx);
|
|
894
1061
|
}
|
|
895
1062
|
});
|
|
896
1063
|
}
|
|
897
1064
|
if (shouldHookEnabled("message_sending")) {
|
|
898
1065
|
on("message_sending", async (event, hookCtx) => {
|
|
899
|
-
|
|
900
|
-
|
|
1066
|
+
const rawChannelId = resolveChannelId(hookCtx, event.to);
|
|
1067
|
+
const lastUser = resolveLastUserContext(hookCtx, rawChannelId);
|
|
1068
|
+
if (lastUser) {
|
|
1069
|
+
lastUser.ctx.lastOutput = event.content;
|
|
901
1070
|
if (config.debug) {
|
|
902
|
-
api.logger.info(`[CozeloopTrace] Captured output for root span: traceId=${
|
|
1071
|
+
api.logger.info(`[CozeloopTrace] Captured output for root span: traceId=${lastUser.ctx.traceId}, content=${typeof event.content === 'string' ? event.content.substring(0, 100) : 'non-string'}`);
|
|
903
1072
|
}
|
|
904
1073
|
}
|
|
905
1074
|
else {
|
|
906
|
-
const
|
|
907
|
-
const { ctx } = resolveActiveContext(rawChannelId, undefined, "message_sending");
|
|
1075
|
+
const { ctx } = resolveActiveContext(rawChannelId, undefined, "message_sending", hookCtx);
|
|
908
1076
|
ctx.lastOutput = event.content;
|
|
909
1077
|
if (config.debug) {
|
|
910
1078
|
api.logger.info(`[CozeloopTrace] Captured output (fallback) for root span: traceId=${ctx.traceId}`);
|
|
@@ -915,15 +1083,16 @@ const cozeloopTracePlugin = {
|
|
|
915
1083
|
if (shouldHookEnabled("message_sent")) {
|
|
916
1084
|
on("message_sent", async (event, hookCtx) => {
|
|
917
1085
|
if (event.content && event.success) {
|
|
918
|
-
|
|
919
|
-
|
|
1086
|
+
const rawChannelId = resolveChannelId(hookCtx, event.to);
|
|
1087
|
+
const lastUser = resolveLastUserContext(hookCtx, rawChannelId);
|
|
1088
|
+
if (lastUser) {
|
|
1089
|
+
lastUser.ctx.lastOutput = event.content;
|
|
920
1090
|
if (config.debug) {
|
|
921
|
-
api.logger.info(`[CozeloopTrace] Captured output from message_sent: traceId=${
|
|
1091
|
+
api.logger.info(`[CozeloopTrace] Captured output from message_sent: traceId=${lastUser.ctx.traceId}`);
|
|
922
1092
|
}
|
|
923
1093
|
}
|
|
924
1094
|
else {
|
|
925
|
-
const
|
|
926
|
-
const { ctx } = resolveActiveContext(rawChannelId, undefined, "message_sent");
|
|
1095
|
+
const { ctx } = resolveActiveContext(rawChannelId, undefined, "message_sent", hookCtx);
|
|
927
1096
|
ctx.lastOutput = event.content;
|
|
928
1097
|
if (config.debug) {
|
|
929
1098
|
api.logger.info(`[CozeloopTrace] Captured output from message_sent (fallback): traceId=${ctx.traceId}`);
|
|
@@ -941,7 +1110,7 @@ const cozeloopTracePlugin = {
|
|
|
941
1110
|
if (config.debug) {
|
|
942
1111
|
api.logger.info(`[CozeloopTrace] llm_input hookCtx: ${JSON.stringify({ channelId: hookCtx.channelId, sessionKey: hookCtx.sessionKey, conversationId: hookCtx.conversationId })}, event.runId=${event.runId}`);
|
|
943
1112
|
}
|
|
944
|
-
const { ctx } = resolveActiveContext(rawChannelId, event.runId, "llm_input");
|
|
1113
|
+
const { ctx, channelId } = resolveActiveContext(rawChannelId, event.runId, "llm_input", hookCtx);
|
|
945
1114
|
const ocSessionId = resolveOpenclawSessionId(hookCtx);
|
|
946
1115
|
if (ocSessionId) {
|
|
947
1116
|
ctx.openclawSessionId = ocSessionId;
|
|
@@ -966,9 +1135,10 @@ const cozeloopTracePlugin = {
|
|
|
966
1135
|
rememberCozeContext(event.prompt, ctx.openclawSessionId || lastOpenclawSessionId);
|
|
967
1136
|
// Fallback: ensure root + agent spans exist in case before_agent_start
|
|
968
1137
|
// was not fired (older OpenClaw versions or resumed sessions).
|
|
969
|
-
const
|
|
1138
|
+
const active = resolveActiveAgentContext(hookCtx, channelId);
|
|
1139
|
+
const channelIdForSpans = active?.channelId || channelId || rawChannelId;
|
|
970
1140
|
await ensureRootSpan(ctx, channelIdForSpans);
|
|
971
|
-
await ensureAgentSpan(ctx, channelIdForSpans);
|
|
1141
|
+
await ensureAgentSpan(ctx, channelIdForSpans, undefined, hookCtx);
|
|
972
1142
|
const messages = [];
|
|
973
1143
|
if (event.systemPrompt) {
|
|
974
1144
|
messages.push({ role: "system", content: safeClone(event.systemPrompt) });
|
|
@@ -1035,14 +1205,15 @@ const cozeloopTracePlugin = {
|
|
|
1035
1205
|
api.logger.info(`[CozeloopTrace][DEBUG] llm_output event keys=${JSON.stringify(Object.keys(event))}`);
|
|
1036
1206
|
api.logger.info(`[CozeloopTrace] llm_output hookCtx: ${JSON.stringify({ channelId: hookCtx.channelId, sessionKey: hookCtx.sessionKey, conversationId: hookCtx.conversationId })}, event.runId=${event.runId}`);
|
|
1037
1207
|
}
|
|
1038
|
-
const { ctx, channelId } = resolveActiveContext(rawChannelId, event.runId, "llm_output");
|
|
1208
|
+
const { ctx, channelId } = resolveActiveContext(rawChannelId, event.runId, "llm_output", hookCtx);
|
|
1039
1209
|
const now = Date.now();
|
|
1040
1210
|
const startTime = ctx.llmStartTime || lastLlmStartTime || now;
|
|
1041
1211
|
if (event.assistantTexts && event.assistantTexts.length > 0) {
|
|
1042
1212
|
const outputText = event.assistantTexts.join("\n");
|
|
1043
1213
|
ctx.lastOutput = outputText;
|
|
1044
|
-
|
|
1045
|
-
|
|
1214
|
+
const lastUser = resolveLastUserContext(hookCtx, channelId);
|
|
1215
|
+
if (lastUser) {
|
|
1216
|
+
lastUser.ctx.lastOutput = outputText;
|
|
1046
1217
|
}
|
|
1047
1218
|
if (config.debug) {
|
|
1048
1219
|
api.logger.info(`[CozeloopTrace] Captured output from llm_output (will use last): traceId=${ctx.traceId}, length=${outputText.length}`);
|
|
@@ -1128,34 +1299,39 @@ const cozeloopTracePlugin = {
|
|
|
1128
1299
|
if (config.debug) {
|
|
1129
1300
|
api.logger.info(`[CozeloopTrace] before_tool_call hookCtx: ${JSON.stringify({ channelId: hookCtx.channelId, sessionKey: hookCtx.sessionKey, conversationId: hookCtx.conversationId })}, toolName=${event.toolName}`);
|
|
1130
1301
|
}
|
|
1131
|
-
const { ctx, channelId } = resolveActiveContext(rawChannelId, undefined, "before_tool_call");
|
|
1132
|
-
pendingToolCall = {
|
|
1302
|
+
const { ctx, channelId } = resolveActiveContext(rawChannelId, undefined, "before_tool_call", hookCtx);
|
|
1303
|
+
const pendingToolCall = {
|
|
1133
1304
|
toolName: event.toolName,
|
|
1305
|
+
toolCallId: getToolCallId(event),
|
|
1134
1306
|
toolSpanId: generateId(16),
|
|
1135
1307
|
toolStartTime: Date.now(),
|
|
1136
1308
|
toolInput: event.params,
|
|
1137
1309
|
traceContext: ctx,
|
|
1138
1310
|
channelId: channelId,
|
|
1139
1311
|
};
|
|
1312
|
+
const pendingCount = pushPendingToolCall(ctx, channelId, pendingToolCall);
|
|
1140
1313
|
ctx.reactCount = (ctx.reactCount || 0) + 1;
|
|
1141
1314
|
if (config.debug) {
|
|
1142
|
-
api.logger.info(`[CozeloopTrace] Tool call started: ${event.toolName}, spanId=${pendingToolCall.toolSpanId}, traceId=${ctx.traceId}`);
|
|
1315
|
+
api.logger.info(`[CozeloopTrace] Tool call started: ${event.toolName}, toolCallId=${pendingToolCall.toolCallId || "none"}, spanId=${pendingToolCall.toolSpanId}, traceId=${ctx.traceId}, pendingCount=${pendingCount}`);
|
|
1143
1316
|
}
|
|
1144
1317
|
});
|
|
1145
1318
|
}
|
|
1146
1319
|
if (shouldHookEnabled("after_tool_call")) {
|
|
1147
1320
|
on("after_tool_call", async (event, hookCtx) => {
|
|
1321
|
+
const rawChannelId = resolveChannelId(hookCtx);
|
|
1322
|
+
const { ctx, channelId } = resolveActiveContext(rawChannelId, undefined, "after_tool_call", hookCtx);
|
|
1148
1323
|
if (config.debug) {
|
|
1149
1324
|
api.logger.info(`[CozeloopTrace] after_tool_call hookCtx: ${JSON.stringify({ channelId: hookCtx.channelId, sessionKey: hookCtx.sessionKey, conversationId: hookCtx.conversationId })}, toolName=${event.toolName}`);
|
|
1150
1325
|
}
|
|
1151
|
-
|
|
1326
|
+
const toolCallId = getToolCallId(event);
|
|
1327
|
+
const pendingToolCall = shiftPendingToolCall(ctx, channelId, event.toolName, toolCallId);
|
|
1328
|
+
if (!pendingToolCall) {
|
|
1152
1329
|
if (config.debug) {
|
|
1153
|
-
api.logger.info(`[CozeloopTrace] Skipping after_tool_call: no pending tool
|
|
1330
|
+
api.logger.info(`[CozeloopTrace] Skipping after_tool_call: no pending tool, toolName=${event.toolName}, toolCallId=${toolCallId || "none"}, pendingKeys=${pendingToolCallsByKey.size}`);
|
|
1154
1331
|
}
|
|
1155
1332
|
return;
|
|
1156
1333
|
}
|
|
1157
1334
|
const { toolName, toolSpanId, toolStartTime, toolInput, traceContext } = pendingToolCall;
|
|
1158
|
-
pendingToolCall = undefined;
|
|
1159
1335
|
const now = Date.now();
|
|
1160
1336
|
if (!traceContext.pendingToolSpans) {
|
|
1161
1337
|
traceContext.pendingToolSpans = [];
|
|
@@ -1177,11 +1353,12 @@ const cozeloopTracePlugin = {
|
|
|
1177
1353
|
// Helper: finalize a trace — end agent span (if open), end root span, flush,
|
|
1178
1354
|
// and clean up all state. Called from agent_end (normal path) and
|
|
1179
1355
|
// session_end (fallback for old OpenClaw versions that don't emit agent_end).
|
|
1180
|
-
|
|
1356
|
+
const finalizingTraces = new Set();
|
|
1181
1357
|
const finalizeTrace = (ctx, channelId, agentEndAttrs, agentOutput) => {
|
|
1182
|
-
|
|
1358
|
+
const finalizeKey = ctx.rootSpanId || ctx.traceId;
|
|
1359
|
+
if (finalizingTraces.has(finalizeKey))
|
|
1183
1360
|
return;
|
|
1184
|
-
|
|
1361
|
+
finalizingTraces.add(finalizeKey);
|
|
1185
1362
|
const now = Date.now();
|
|
1186
1363
|
// End agent span if still open.
|
|
1187
1364
|
if (ctx.agentSpanId) {
|
|
@@ -1194,14 +1371,15 @@ const cozeloopTracePlugin = {
|
|
|
1194
1371
|
}
|
|
1195
1372
|
const rootSpanId = ctx.rootSpanId;
|
|
1196
1373
|
const rootSpanStartTime = ctx.rootSpanStartTime;
|
|
1197
|
-
const
|
|
1374
|
+
const lastUserFallback = lastUserFallbackFor(ctx, channelId);
|
|
1375
|
+
const userInput = ctx.userInput || lastUserFallback?.userInput || (lastUserFallback ? lastUserInput : undefined);
|
|
1198
1376
|
const traceId = ctx.traceId;
|
|
1199
1377
|
const hasRootSpan = !!rootSpanStartTime;
|
|
1200
1378
|
const savedLastUserChannelId = lastUserChannelId;
|
|
1201
1379
|
const originalChannelId = ctx.originalChannelId || channelId;
|
|
1202
1380
|
setTimeout(async () => {
|
|
1203
1381
|
if (hasRootSpan) {
|
|
1204
|
-
const finalOutput = ctx.lastOutput ||
|
|
1382
|
+
const finalOutput = ctx.lastOutput || lastUserFallback?.lastOutput;
|
|
1205
1383
|
if (config.debug) {
|
|
1206
1384
|
api.logger.info(`[CozeloopTrace] Ending root span with input=${userInput ? 'present' : 'missing'}, output=${finalOutput ? 'present' : 'missing'}`);
|
|
1207
1385
|
}
|
|
@@ -1215,20 +1393,17 @@ const cozeloopTracePlugin = {
|
|
|
1215
1393
|
}
|
|
1216
1394
|
await exporter.flush();
|
|
1217
1395
|
exporter.endTrace(rootSpanId);
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
activeAgentChannelId = undefined;
|
|
1221
|
-
}
|
|
1396
|
+
clearPendingToolCallsForContext(ctx);
|
|
1397
|
+
clearActiveAgentContext(ctx);
|
|
1222
1398
|
if (savedLastUserChannelId) {
|
|
1223
1399
|
endTurn(savedLastUserChannelId);
|
|
1224
1400
|
}
|
|
1225
1401
|
if (originalChannelId && originalChannelId !== savedLastUserChannelId) {
|
|
1226
1402
|
endTurn(originalChannelId);
|
|
1227
1403
|
}
|
|
1228
|
-
|
|
1229
|
-
lastUserTraceContext = undefined;
|
|
1404
|
+
clearLastUserContext(ctx);
|
|
1230
1405
|
lastUserInput = undefined;
|
|
1231
|
-
|
|
1406
|
+
finalizingTraces.delete(finalizeKey);
|
|
1232
1407
|
}, 200);
|
|
1233
1408
|
};
|
|
1234
1409
|
// OpenClaw runtime 周期性发送的心跳轮询消息,不是真实对话,整条 trace 丢弃。
|
|
@@ -1249,8 +1424,9 @@ const cozeloopTracePlugin = {
|
|
|
1249
1424
|
// currentRootContext is set and the agent span becomes a proper child.
|
|
1250
1425
|
const ensureRootSpan = async (ctx, channelId) => {
|
|
1251
1426
|
// 心跳轮询消息:不是真实对话,整条 trace 不上报(沿用下方“无 coze-context 即 return”的同款范式)。
|
|
1427
|
+
const lastUserFallback = lastUserFallbackFor(ctx, channelId);
|
|
1252
1428
|
const heartbeatInput = ctx.userInput
|
|
1253
|
-
||
|
|
1429
|
+
|| lastUserFallback?.userInput || (lastUserFallback ? lastUserInput : undefined);
|
|
1254
1430
|
if (isHeartbeatInput(heartbeatInput)) {
|
|
1255
1431
|
if (config.debug) {
|
|
1256
1432
|
api.logger.info(`[CozeloopTrace] skip heartbeat poll trace, traceId=${ctx.traceId}`);
|
|
@@ -1267,7 +1443,7 @@ const cozeloopTracePlugin = {
|
|
|
1267
1443
|
let cozeCtx = resolveCozeContext(ctx.userInput, ocSessionId);
|
|
1268
1444
|
if (Object.keys(cozeCtx).length === 0) {
|
|
1269
1445
|
// Try the fallback user inputs too before giving up.
|
|
1270
|
-
const fallbackInput =
|
|
1446
|
+
const fallbackInput = lastUserFallback?.userInput || (lastUserFallback ? lastUserInput : undefined);
|
|
1271
1447
|
cozeCtx = resolveCozeContext(fallbackInput, ocSessionId);
|
|
1272
1448
|
}
|
|
1273
1449
|
if (Object.keys(cozeCtx).length === 0) {
|
|
@@ -1298,7 +1474,7 @@ const cozeloopTracePlugin = {
|
|
|
1298
1474
|
// Resolve user input: prefer ctx.userInput set by this turn's
|
|
1299
1475
|
// message_received, fall back to lastUserTraceContext, then lastUserInput.
|
|
1300
1476
|
if (!ctx.userInput) {
|
|
1301
|
-
ctx.userInput =
|
|
1477
|
+
ctx.userInput = lastUserFallback?.userInput || (lastUserFallback ? lastUserInput : undefined);
|
|
1302
1478
|
}
|
|
1303
1479
|
const rootSpanData = {
|
|
1304
1480
|
name: "openclaw_request",
|
|
@@ -1322,9 +1498,11 @@ const cozeloopTracePlugin = {
|
|
|
1322
1498
|
};
|
|
1323
1499
|
// Helper: ensure the agent span exists for a given context.
|
|
1324
1500
|
// Safe to call multiple times — only creates the span once.
|
|
1325
|
-
const ensureAgentSpan = async (ctx, channelId, agentId) => {
|
|
1326
|
-
if (ctx.agentSpanId)
|
|
1501
|
+
const ensureAgentSpan = async (ctx, channelId, agentId, hookCtx) => {
|
|
1502
|
+
if (ctx.agentSpanId) {
|
|
1503
|
+
rememberActiveAgentContext(hookCtx || {}, channelId, ctx);
|
|
1327
1504
|
return;
|
|
1505
|
+
}
|
|
1328
1506
|
const effectiveAgentId = agentId || "main";
|
|
1329
1507
|
const now = Date.now();
|
|
1330
1508
|
ctx.agentStartTime = now;
|
|
@@ -1347,8 +1525,7 @@ const cozeloopTracePlugin = {
|
|
|
1347
1525
|
};
|
|
1348
1526
|
await exporter.startSpan(spanData, ctx.agentSpanId);
|
|
1349
1527
|
// Set active agent context so all subsequent hooks use the same Trace.
|
|
1350
|
-
|
|
1351
|
-
activeAgentChannelId = channelId;
|
|
1528
|
+
rememberActiveAgentContext(hookCtx || {}, channelId, ctx);
|
|
1352
1529
|
if (config.debug) {
|
|
1353
1530
|
api.logger.info(`[CozeloopTrace] ensureAgentSpan: created agent span, agentId=${effectiveAgentId}, spanId=${ctx.agentSpanId}, traceId=${ctx.traceId}`);
|
|
1354
1531
|
}
|
|
@@ -1368,7 +1545,7 @@ const cozeloopTracePlugin = {
|
|
|
1368
1545
|
}
|
|
1369
1546
|
ctx.openclawSessionId = ctx.openclawSessionId || lastOpenclawSessionId;
|
|
1370
1547
|
await ensureRootSpan(ctx, channelId);
|
|
1371
|
-
await ensureAgentSpan(ctx, channelId, agentId);
|
|
1548
|
+
await ensureAgentSpan(ctx, channelId, agentId, hookCtx);
|
|
1372
1549
|
});
|
|
1373
1550
|
}
|
|
1374
1551
|
if (shouldHookEnabled("agent_end")) {
|
|
@@ -1378,8 +1555,9 @@ const cozeloopTracePlugin = {
|
|
|
1378
1555
|
api.logger.info(`[CozeloopTrace] agent_end hookCtx: ${JSON.stringify({ channelId: hookCtx.channelId, sessionKey: hookCtx.sessionKey, conversationId: hookCtx.conversationId })}`);
|
|
1379
1556
|
}
|
|
1380
1557
|
// Use activeAgentCtx if available, otherwise fall back to resolution.
|
|
1381
|
-
const
|
|
1382
|
-
const
|
|
1558
|
+
const active = resolveActiveAgentContext(hookCtx, rawChannelId);
|
|
1559
|
+
const ctx = active?.ctx || getOrCreateContext(rawChannelId, undefined, "agent_end").ctx;
|
|
1560
|
+
const channelId = active?.channelId || rawChannelId;
|
|
1383
1561
|
finalizeTrace(ctx, channelId, {
|
|
1384
1562
|
"agent.duration_ms": event.durationMs || 0,
|
|
1385
1563
|
"agent.message_count": event.messageCount || 0,
|
|
@@ -1397,9 +1575,11 @@ const cozeloopTracePlugin = {
|
|
|
1397
1575
|
if (config.debug) {
|
|
1398
1576
|
api.logger.info(`[CozeloopTrace] session_end: ${rawChannelId}`);
|
|
1399
1577
|
}
|
|
1400
|
-
const
|
|
1578
|
+
const active = resolveActiveAgentContext(hookCtx, rawChannelId);
|
|
1579
|
+
const lastUser = resolveLastUserContext(hookCtx, rawChannelId);
|
|
1580
|
+
const ctx = active?.ctx || lastUser?.ctx;
|
|
1401
1581
|
if (ctx && ctx.rootSpanStartTime) {
|
|
1402
|
-
const channelId =
|
|
1582
|
+
const channelId = active?.channelId || lastUser?.channelId || rawChannelId;
|
|
1403
1583
|
if (config.debug) {
|
|
1404
1584
|
api.logger.info(`[CozeloopTrace] session_end: finalizing trace as fallback, traceId=${ctx.traceId}`);
|
|
1405
1585
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-cozeloop-trace",
|
|
3
3
|
"name": "OpenClaw CozeLoop Trace",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.16",
|
|
5
5
|
"description": "Report OpenClaw execution traces to CozeLoop via OpenTelemetry",
|
|
6
6
|
"type": "plugin",
|
|
7
7
|
"entry": "./dist/index.js",
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"properties": {
|
|
11
11
|
"endpoint": {
|
|
12
12
|
"type": "string",
|
|
13
|
-
"default": "https://api.coze.cn
|
|
14
|
-
"description": "CozeLoop
|
|
13
|
+
"default": "https://api.coze.cn",
|
|
14
|
+
"description": "CozeLoop API base URL"
|
|
15
15
|
},
|
|
16
16
|
"authorization": {
|
|
17
17
|
"type": "string",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cozeloop/openclaw-cozeloop-trace",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "OpenClaw Plugin for reporting traces to CozeLoop via OpenTelemetry",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -37,7 +37,6 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@opentelemetry/api": "^1.7.0",
|
|
40
|
-
"@opentelemetry/exporter-trace-otlp-proto": "^0.48.0",
|
|
41
40
|
"@opentelemetry/resources": "^1.22.0",
|
|
42
41
|
"@opentelemetry/sdk-trace-base": "^1.22.0",
|
|
43
42
|
"@opentelemetry/semantic-conventions": "^1.22.0"
|