coze_lab 0.1.30 → 0.1.31
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/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,193 @@ async function getRefreshedToken(currentAuthorization, opts = {}) {
|
|
|
78
78
|
}
|
|
79
79
|
// ─────────────────────────────────────────────────────────────────────────
|
|
80
80
|
|
|
81
|
+
const EXPORT_SUCCESS = 0;
|
|
82
|
+
const EXPORT_FAILED = 1;
|
|
83
|
+
|
|
84
|
+
function normalizeTraceUrl(endpoint) {
|
|
85
|
+
const base = String(endpoint || "").replace(/\/+$/, "");
|
|
86
|
+
return base.endsWith("/v1/traces") ? base : `${base}/v1/traces`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function hrTimeToUnixNano(time) {
|
|
90
|
+
if (Array.isArray(time)) {
|
|
91
|
+
return (BigInt(time[0]) * 1000000000n + BigInt(time[1])).toString();
|
|
92
|
+
}
|
|
93
|
+
const millis = time instanceof Date ? time.getTime() : Number(time || Date.now());
|
|
94
|
+
return (BigInt(Math.trunc(millis)) * 1000000n).toString();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function spanKindToOtlp(kind) {
|
|
98
|
+
switch (kind) {
|
|
99
|
+
case SpanKind.INTERNAL: return 1;
|
|
100
|
+
case SpanKind.SERVER: return 2;
|
|
101
|
+
case SpanKind.CLIENT: return 3;
|
|
102
|
+
case SpanKind.PRODUCER: return 4;
|
|
103
|
+
case SpanKind.CONSUMER: return 5;
|
|
104
|
+
default: return 0;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function scalarToAnyValue(value) {
|
|
109
|
+
if (value === undefined || value === null)
|
|
110
|
+
return undefined;
|
|
111
|
+
if (typeof value === "string")
|
|
112
|
+
return { stringValue: value };
|
|
113
|
+
if (typeof value === "boolean")
|
|
114
|
+
return { boolValue: value };
|
|
115
|
+
if (typeof value === "number") {
|
|
116
|
+
if (Number.isInteger(value))
|
|
117
|
+
return { intValue: String(value) };
|
|
118
|
+
return { doubleValue: value };
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
return { stringValue: JSON.stringify(value) };
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return { stringValue: String(value) };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function valueToAnyValue(value) {
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
const values = value.map(scalarToAnyValue).filter(Boolean);
|
|
131
|
+
return { arrayValue: { values } };
|
|
132
|
+
}
|
|
133
|
+
return scalarToAnyValue(value);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function attributesToOtlp(attributes) {
|
|
137
|
+
const out = [];
|
|
138
|
+
for (const [key, value] of Object.entries(attributes || {})) {
|
|
139
|
+
const otlpValue = valueToAnyValue(value);
|
|
140
|
+
if (otlpValue) {
|
|
141
|
+
out.push({ key, value: otlpValue });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function spanToOtlp(span) {
|
|
148
|
+
const spanContext = span.spanContext();
|
|
149
|
+
const otlpSpan = {
|
|
150
|
+
traceId: spanContext.traceId,
|
|
151
|
+
spanId: spanContext.spanId,
|
|
152
|
+
name: span.name,
|
|
153
|
+
kind: spanKindToOtlp(span.kind),
|
|
154
|
+
startTimeUnixNano: hrTimeToUnixNano(span.startTime),
|
|
155
|
+
endTimeUnixNano: hrTimeToUnixNano(span.endTime),
|
|
156
|
+
attributes: attributesToOtlp(span.attributes),
|
|
157
|
+
status: { code: span.status?.code ?? 0 },
|
|
158
|
+
};
|
|
159
|
+
if (span.parentSpanId) {
|
|
160
|
+
otlpSpan.parentSpanId = span.parentSpanId;
|
|
161
|
+
}
|
|
162
|
+
if (span.status?.message) {
|
|
163
|
+
otlpSpan.status.message = span.status.message;
|
|
164
|
+
}
|
|
165
|
+
return otlpSpan;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function buildOtlpTraceRequest(spans) {
|
|
169
|
+
const resourceGroups = new Map();
|
|
170
|
+
for (const span of spans) {
|
|
171
|
+
const resourceAttrs = attributesToOtlp(span.resource?.attributes || {});
|
|
172
|
+
const scope = span.instrumentationLibrary || {};
|
|
173
|
+
const scopeData = {
|
|
174
|
+
name: scope.name || "openclaw-cozeloop-trace",
|
|
175
|
+
version: scope.version || PLUGIN_VERSION,
|
|
176
|
+
};
|
|
177
|
+
if (scope.schemaUrl) {
|
|
178
|
+
scopeData.schemaUrl = scope.schemaUrl;
|
|
179
|
+
}
|
|
180
|
+
const resourceKey = JSON.stringify(resourceAttrs);
|
|
181
|
+
let resourceGroup = resourceGroups.get(resourceKey);
|
|
182
|
+
if (!resourceGroup) {
|
|
183
|
+
resourceGroup = {
|
|
184
|
+
resource: { attributes: resourceAttrs },
|
|
185
|
+
scopeGroups: new Map(),
|
|
186
|
+
};
|
|
187
|
+
resourceGroups.set(resourceKey, resourceGroup);
|
|
188
|
+
}
|
|
189
|
+
const scopeKey = JSON.stringify(scopeData);
|
|
190
|
+
let scopeGroup = resourceGroup.scopeGroups.get(scopeKey);
|
|
191
|
+
if (!scopeGroup) {
|
|
192
|
+
scopeGroup = { scope: scopeData, spans: [] };
|
|
193
|
+
resourceGroup.scopeGroups.set(scopeKey, scopeGroup);
|
|
194
|
+
}
|
|
195
|
+
scopeGroup.spans.push(spanToOtlp(span));
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
resourceSpans: Array.from(resourceGroups.values()).map((resourceGroup) => ({
|
|
199
|
+
resource: resourceGroup.resource,
|
|
200
|
+
scopeSpans: Array.from(resourceGroup.scopeGroups.values()),
|
|
201
|
+
})),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function postJson(url, body, headers) {
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
const data = JSON.stringify(body);
|
|
208
|
+
const u = new URL(url);
|
|
209
|
+
const client = u.protocol === "http:" ? http : https;
|
|
210
|
+
const req = client.request({
|
|
211
|
+
protocol: u.protocol,
|
|
212
|
+
hostname: u.hostname,
|
|
213
|
+
port: u.port || undefined,
|
|
214
|
+
path: u.pathname + u.search,
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers: {
|
|
217
|
+
"Content-Type": "application/json",
|
|
218
|
+
"Content-Length": Buffer.byteLength(data),
|
|
219
|
+
...(headers || {}),
|
|
220
|
+
},
|
|
221
|
+
}, (res) => {
|
|
222
|
+
let buf = "";
|
|
223
|
+
res.on("data", c => buf += c);
|
|
224
|
+
res.on("end", () => resolve({ status: res.statusCode || 0, body: buf }));
|
|
225
|
+
});
|
|
226
|
+
req.on("error", reject);
|
|
227
|
+
req.setTimeout(15000, () => req.destroy(new Error("OTLP JSON export timed out")));
|
|
228
|
+
req.write(data);
|
|
229
|
+
req.end();
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
class JsonOtlpTraceExporter {
|
|
234
|
+
constructor(config) {
|
|
235
|
+
this.url = normalizeTraceUrl(config.url);
|
|
236
|
+
this.headers = config.headers || {};
|
|
237
|
+
this.logger = config.logger;
|
|
238
|
+
this.shutdownRequested = false;
|
|
239
|
+
}
|
|
240
|
+
export(spans, resultCallback) {
|
|
241
|
+
if (!spans || spans.length === 0 || this.shutdownRequested) {
|
|
242
|
+
resultCallback({ code: EXPORT_SUCCESS });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
this.postSpans(spans)
|
|
246
|
+
.then(() => resultCallback({ code: EXPORT_SUCCESS }))
|
|
247
|
+
.catch((err) => {
|
|
248
|
+
this.logger?.error?.(`[CozeloopTrace] OTLP JSON export failed: ${err?.message || err}`);
|
|
249
|
+
resultCallback({ code: EXPORT_FAILED, error: err });
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
async postSpans(spans) {
|
|
253
|
+
const body = buildOtlpTraceRequest(spans);
|
|
254
|
+
const res = await postJson(this.url, body, this.headers);
|
|
255
|
+
if (res.status < 200 || res.status >= 300) {
|
|
256
|
+
const snippet = String(res.body || "").slice(0, 300);
|
|
257
|
+
throw new Error(`HTTP ${res.status}${snippet ? `: ${snippet}` : ""}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async forceFlush() {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
async shutdown() {
|
|
264
|
+
this.shutdownRequested = true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
81
268
|
export class CozeloopExporter {
|
|
82
269
|
config;
|
|
83
270
|
api;
|
|
@@ -173,8 +360,9 @@ export class CozeloopExporter {
|
|
|
173
360
|
const authorization = this.config.authorization;
|
|
174
361
|
const workspaceId = this.config.workspaceId;
|
|
175
362
|
this.api.logger.info(`[CozeloopTrace] Using authorization, workspaceId=${workspaceId}, tokenLength=${authorization?.length}`);
|
|
176
|
-
const exporter = new
|
|
363
|
+
const exporter = new JsonOtlpTraceExporter({
|
|
177
364
|
url: `${this.config.endpoint}/v1/traces`,
|
|
365
|
+
logger: this.api.logger,
|
|
178
366
|
headers: {
|
|
179
367
|
"Authorization": authorization,
|
|
180
368
|
"cozeloop-workspace-id": workspaceId,
|
|
@@ -441,12 +629,22 @@ export class CozeloopExporter {
|
|
|
441
629
|
}
|
|
442
630
|
async flush() {
|
|
443
631
|
if (this.provider) {
|
|
444
|
-
|
|
632
|
+
try {
|
|
633
|
+
await this.provider.forceFlush();
|
|
634
|
+
}
|
|
635
|
+
catch (err) {
|
|
636
|
+
this.api.logger.error(`[CozeloopTrace] Flush failed: ${err?.message || err}`);
|
|
637
|
+
}
|
|
445
638
|
}
|
|
446
639
|
}
|
|
447
640
|
async dispose() {
|
|
448
641
|
if (this.provider) {
|
|
449
|
-
|
|
642
|
+
try {
|
|
643
|
+
await this.provider.shutdown();
|
|
644
|
+
}
|
|
645
|
+
catch (err) {
|
|
646
|
+
this.api.logger.error(`[CozeloopTrace] Shutdown failed: ${err?.message || err}`);
|
|
647
|
+
}
|
|
450
648
|
}
|
|
451
649
|
}
|
|
452
650
|
}
|
|
@@ -481,15 +481,21 @@ function resolveCozeContext(input, sessionId) {
|
|
|
481
481
|
// use this to ensure every span lands in the same Trace.
|
|
482
482
|
let activeAgentCtx;
|
|
483
483
|
let activeAgentChannelId;
|
|
484
|
+
const activeAgentContextByKey = new Map();
|
|
484
485
|
// Latest user input captured from message_received, independent of any ctx.
|
|
485
486
|
// Used by ensureRootSpan as a reliable fallback for the root span's input.
|
|
486
487
|
let lastUserInput;
|
|
487
|
-
|
|
488
|
+
const lastUserContextByKey = new Map();
|
|
489
|
+
const pendingToolCallsByKey = new Map();
|
|
488
490
|
function resolveOpenclawSessionId(hookCtx, eventSessionId) {
|
|
489
491
|
const raw = hookCtx.sessionId?.trim() || eventSessionId?.trim();
|
|
490
|
-
if (
|
|
491
|
-
return
|
|
492
|
-
|
|
492
|
+
if (raw)
|
|
493
|
+
return raw;
|
|
494
|
+
const sessionKey = hookCtx.sessionKey?.trim();
|
|
495
|
+
const match = sessionKey?.match(/^agent:[^:]+:(.+)$/);
|
|
496
|
+
if (match?.[1])
|
|
497
|
+
return match[1];
|
|
498
|
+
return undefined;
|
|
493
499
|
}
|
|
494
500
|
const cozeloopTracePlugin = {
|
|
495
501
|
id: "openclaw-cozeloop-trace",
|
|
@@ -558,6 +564,15 @@ const cozeloopTracePlugin = {
|
|
|
558
564
|
const ctx = contextByRunId.get(runId);
|
|
559
565
|
return ctx?.originalChannelId || ctx?.channelId;
|
|
560
566
|
};
|
|
567
|
+
const sessionIdFromChannelId = (channelId) => {
|
|
568
|
+
const match = String(channelId || "").match(/^agent\/[^:]+:(.+)$/);
|
|
569
|
+
return match?.[1];
|
|
570
|
+
};
|
|
571
|
+
const canFallbackToLastUserContext = (rawChannelId) => {
|
|
572
|
+
const rawSessionId = sessionIdFromChannelId(rawChannelId);
|
|
573
|
+
const lastSessionId = lastUserTraceContext?.openclawSessionId || sessionIdFromChannelId(lastUserChannelId);
|
|
574
|
+
return !rawSessionId || !lastSessionId || rawSessionId === lastSessionId;
|
|
575
|
+
};
|
|
561
576
|
const startTurn = (runId, channelId, originalChannelId, openclawSessionId) => {
|
|
562
577
|
const traceId = generateId(32);
|
|
563
578
|
const ctx = {
|
|
@@ -594,7 +609,7 @@ const cozeloopTracePlugin = {
|
|
|
594
609
|
if (!activeCtx) {
|
|
595
610
|
activeCtx = getContextByRun(effectiveRunId);
|
|
596
611
|
}
|
|
597
|
-
if (!activeCtx && rawChannelId.startsWith("agent/") && lastUserTraceContext) {
|
|
612
|
+
if (!activeCtx && rawChannelId.startsWith("agent/") && lastUserTraceContext && canFallbackToLastUserContext(rawChannelId)) {
|
|
598
613
|
activeCtx = lastUserTraceContext;
|
|
599
614
|
channelId = lastUserChannelId || channelId;
|
|
600
615
|
contextByChannelId.set(rawChannelId, activeCtx);
|
|
@@ -616,19 +631,136 @@ const cozeloopTracePlugin = {
|
|
|
616
631
|
}
|
|
617
632
|
return { ctx: activeCtx, channelId, isNew };
|
|
618
633
|
};
|
|
634
|
+
const contextKeys = (hookCtx, channelId, eventSessionId) => {
|
|
635
|
+
const keys = [];
|
|
636
|
+
const add = (value) => {
|
|
637
|
+
const s = String(value || "").trim();
|
|
638
|
+
if (s && !keys.includes(s))
|
|
639
|
+
keys.push(s);
|
|
640
|
+
};
|
|
641
|
+
add(resolveOpenclawSessionId(hookCtx || {}, eventSessionId));
|
|
642
|
+
add(hookCtx?.sessionKey);
|
|
643
|
+
add(hookCtx?.conversationId);
|
|
644
|
+
add(channelId);
|
|
645
|
+
return keys;
|
|
646
|
+
};
|
|
647
|
+
const rememberLastUserContext = (hookCtx, channelId, ctx) => {
|
|
648
|
+
for (const key of contextKeys(hookCtx, channelId)) {
|
|
649
|
+
lastUserContextByKey.set(key, { ctx, channelId });
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
const resolveLastUserContext = (hookCtx, channelId) => {
|
|
653
|
+
for (const key of contextKeys(hookCtx, channelId)) {
|
|
654
|
+
const found = lastUserContextByKey.get(key);
|
|
655
|
+
if (found)
|
|
656
|
+
return found;
|
|
657
|
+
}
|
|
658
|
+
if (lastUserTraceContext) {
|
|
659
|
+
return { ctx: lastUserTraceContext, channelId: lastUserChannelId || channelId };
|
|
660
|
+
}
|
|
661
|
+
return undefined;
|
|
662
|
+
};
|
|
663
|
+
const rememberActiveAgentContext = (hookCtx, channelId, ctx) => {
|
|
664
|
+
for (const key of contextKeys(hookCtx, channelId, ctx.openclawSessionId)) {
|
|
665
|
+
activeAgentContextByKey.set(key, { ctx, channelId });
|
|
666
|
+
}
|
|
667
|
+
activeAgentCtx = ctx;
|
|
668
|
+
activeAgentChannelId = channelId;
|
|
669
|
+
};
|
|
670
|
+
const resolveActiveAgentContext = (hookCtx, channelId) => {
|
|
671
|
+
for (const key of contextKeys(hookCtx, channelId)) {
|
|
672
|
+
const found = activeAgentContextByKey.get(key);
|
|
673
|
+
if (found)
|
|
674
|
+
return found;
|
|
675
|
+
}
|
|
676
|
+
if (activeAgentCtx) {
|
|
677
|
+
return { ctx: activeAgentCtx, channelId: activeAgentChannelId || channelId };
|
|
678
|
+
}
|
|
679
|
+
return undefined;
|
|
680
|
+
};
|
|
681
|
+
const clearActiveAgentContext = (ctx) => {
|
|
682
|
+
for (const [key, found] of activeAgentContextByKey.entries()) {
|
|
683
|
+
if (found.ctx === ctx)
|
|
684
|
+
activeAgentContextByKey.delete(key);
|
|
685
|
+
}
|
|
686
|
+
if (activeAgentCtx === ctx) {
|
|
687
|
+
activeAgentCtx = undefined;
|
|
688
|
+
activeAgentChannelId = undefined;
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
const clearLastUserContext = (ctx) => {
|
|
692
|
+
for (const [key, found] of lastUserContextByKey.entries()) {
|
|
693
|
+
if (found.ctx === ctx)
|
|
694
|
+
lastUserContextByKey.delete(key);
|
|
695
|
+
}
|
|
696
|
+
if (lastUserTraceContext === ctx) {
|
|
697
|
+
lastUserTraceContext = undefined;
|
|
698
|
+
lastUserChannelId = undefined;
|
|
699
|
+
}
|
|
700
|
+
};
|
|
619
701
|
// Resolve context for hooks that fire between before_agent_start and
|
|
620
702
|
// agent_end. When an agent is active, always return that agent's context
|
|
621
703
|
// so every span ends up in the same Trace regardless of channelId drift.
|
|
622
|
-
const resolveActiveContext = (rawChannelId, runId, hookName) => {
|
|
623
|
-
|
|
704
|
+
const resolveActiveContext = (rawChannelId, runId, hookName, hookCtx) => {
|
|
705
|
+
const active = resolveActiveAgentContext(hookCtx || {}, rawChannelId);
|
|
706
|
+
if (active) {
|
|
624
707
|
if (config.debug) {
|
|
625
|
-
api.logger.info(`[CozeloopTrace] Using activeAgentCtx for ${hookName}: traceId=${
|
|
708
|
+
api.logger.info(`[CozeloopTrace] Using activeAgentCtx for ${hookName}: traceId=${active.ctx.traceId}, rootSpanId=${active.ctx.rootSpanId}`);
|
|
626
709
|
}
|
|
627
|
-
return
|
|
710
|
+
return active;
|
|
628
711
|
}
|
|
629
712
|
const { ctx, channelId } = getOrCreateContext(rawChannelId, runId, hookName);
|
|
630
713
|
return { ctx, channelId };
|
|
631
714
|
};
|
|
715
|
+
const getToolCallId = (event) => {
|
|
716
|
+
const candidates = [
|
|
717
|
+
event?.toolCallId,
|
|
718
|
+
event?.tool_call_id,
|
|
719
|
+
event?.callId,
|
|
720
|
+
event?.id,
|
|
721
|
+
event?.requestId,
|
|
722
|
+
event?.invocationId,
|
|
723
|
+
event?.params?.toolCallId,
|
|
724
|
+
event?.params?.tool_call_id,
|
|
725
|
+
];
|
|
726
|
+
const found = candidates.find((value) => value !== undefined && value !== null && String(value).trim());
|
|
727
|
+
return found === undefined ? undefined : String(found);
|
|
728
|
+
};
|
|
729
|
+
const pendingToolKey = (ctx, channelId, toolName, toolCallId) => {
|
|
730
|
+
const traceKey = ctx.rootSpanId || ctx.traceId || channelId;
|
|
731
|
+
if (toolCallId)
|
|
732
|
+
return `${traceKey}:id:${toolCallId}`;
|
|
733
|
+
return `${traceKey}:name:${toolName || "unknown_tool"}`;
|
|
734
|
+
};
|
|
735
|
+
const pushPendingToolCall = (ctx, channelId, pending) => {
|
|
736
|
+
const key = pendingToolKey(ctx, channelId, pending.toolName, pending.toolCallId);
|
|
737
|
+
const queue = pendingToolCallsByKey.get(key) || [];
|
|
738
|
+
queue.push(pending);
|
|
739
|
+
pendingToolCallsByKey.set(key, queue);
|
|
740
|
+
return queue.length;
|
|
741
|
+
};
|
|
742
|
+
const shiftPendingToolCall = (ctx, channelId, toolName, toolCallId) => {
|
|
743
|
+
const key = pendingToolKey(ctx, channelId, toolName, toolCallId);
|
|
744
|
+
const queue = pendingToolCallsByKey.get(key);
|
|
745
|
+
if (!queue || queue.length === 0)
|
|
746
|
+
return undefined;
|
|
747
|
+
const pending = queue.shift();
|
|
748
|
+
if (queue.length === 0) {
|
|
749
|
+
pendingToolCallsByKey.delete(key);
|
|
750
|
+
}
|
|
751
|
+
return pending;
|
|
752
|
+
};
|
|
753
|
+
const clearPendingToolCallsForContext = (ctx) => {
|
|
754
|
+
for (const [key, queue] of pendingToolCallsByKey.entries()) {
|
|
755
|
+
const kept = queue.filter((pending) => pending.traceContext !== ctx);
|
|
756
|
+
if (kept.length === 0) {
|
|
757
|
+
pendingToolCallsByKey.delete(key);
|
|
758
|
+
}
|
|
759
|
+
else if (kept.length !== queue.length) {
|
|
760
|
+
pendingToolCallsByKey.set(key, kept);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
};
|
|
632
764
|
const createSpan = (ctx, channelId, name, type, startTime, endTime, attributes = {}, input, output, parentSpanId) => {
|
|
633
765
|
return {
|
|
634
766
|
name,
|
|
@@ -878,6 +1010,7 @@ const cozeloopTracePlugin = {
|
|
|
878
1010
|
if (role === "user" || !role) {
|
|
879
1011
|
lastUserChannelId = channelId;
|
|
880
1012
|
lastUserTraceContext = ctx;
|
|
1013
|
+
rememberLastUserContext(hookCtx, channelId, ctx);
|
|
881
1014
|
ctx.userInput = event.content;
|
|
882
1015
|
lastUserInput = event.content;
|
|
883
1016
|
rememberCozeContext(event.content, ctx.openclawSessionId || lastOpenclawSessionId);
|
|
@@ -891,20 +1024,22 @@ const cozeloopTracePlugin = {
|
|
|
891
1024
|
if (!lastUserTraceContext) {
|
|
892
1025
|
lastUserTraceContext = ctx;
|
|
893
1026
|
lastUserChannelId = channelId;
|
|
1027
|
+
rememberLastUserContext(hookCtx, channelId, ctx);
|
|
894
1028
|
}
|
|
895
1029
|
});
|
|
896
1030
|
}
|
|
897
1031
|
if (shouldHookEnabled("message_sending")) {
|
|
898
1032
|
on("message_sending", async (event, hookCtx) => {
|
|
899
|
-
|
|
900
|
-
|
|
1033
|
+
const rawChannelId = resolveChannelId(hookCtx, event.to);
|
|
1034
|
+
const lastUser = resolveLastUserContext(hookCtx, rawChannelId);
|
|
1035
|
+
if (lastUser) {
|
|
1036
|
+
lastUser.ctx.lastOutput = event.content;
|
|
901
1037
|
if (config.debug) {
|
|
902
|
-
api.logger.info(`[CozeloopTrace] Captured output for root span: traceId=${
|
|
1038
|
+
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
1039
|
}
|
|
904
1040
|
}
|
|
905
1041
|
else {
|
|
906
|
-
const
|
|
907
|
-
const { ctx } = resolveActiveContext(rawChannelId, undefined, "message_sending");
|
|
1042
|
+
const { ctx } = resolveActiveContext(rawChannelId, undefined, "message_sending", hookCtx);
|
|
908
1043
|
ctx.lastOutput = event.content;
|
|
909
1044
|
if (config.debug) {
|
|
910
1045
|
api.logger.info(`[CozeloopTrace] Captured output (fallback) for root span: traceId=${ctx.traceId}`);
|
|
@@ -915,15 +1050,16 @@ const cozeloopTracePlugin = {
|
|
|
915
1050
|
if (shouldHookEnabled("message_sent")) {
|
|
916
1051
|
on("message_sent", async (event, hookCtx) => {
|
|
917
1052
|
if (event.content && event.success) {
|
|
918
|
-
|
|
919
|
-
|
|
1053
|
+
const rawChannelId = resolveChannelId(hookCtx, event.to);
|
|
1054
|
+
const lastUser = resolveLastUserContext(hookCtx, rawChannelId);
|
|
1055
|
+
if (lastUser) {
|
|
1056
|
+
lastUser.ctx.lastOutput = event.content;
|
|
920
1057
|
if (config.debug) {
|
|
921
|
-
api.logger.info(`[CozeloopTrace] Captured output from message_sent: traceId=${
|
|
1058
|
+
api.logger.info(`[CozeloopTrace] Captured output from message_sent: traceId=${lastUser.ctx.traceId}`);
|
|
922
1059
|
}
|
|
923
1060
|
}
|
|
924
1061
|
else {
|
|
925
|
-
const
|
|
926
|
-
const { ctx } = resolveActiveContext(rawChannelId, undefined, "message_sent");
|
|
1062
|
+
const { ctx } = resolveActiveContext(rawChannelId, undefined, "message_sent", hookCtx);
|
|
927
1063
|
ctx.lastOutput = event.content;
|
|
928
1064
|
if (config.debug) {
|
|
929
1065
|
api.logger.info(`[CozeloopTrace] Captured output from message_sent (fallback): traceId=${ctx.traceId}`);
|
|
@@ -941,7 +1077,7 @@ const cozeloopTracePlugin = {
|
|
|
941
1077
|
if (config.debug) {
|
|
942
1078
|
api.logger.info(`[CozeloopTrace] llm_input hookCtx: ${JSON.stringify({ channelId: hookCtx.channelId, sessionKey: hookCtx.sessionKey, conversationId: hookCtx.conversationId })}, event.runId=${event.runId}`);
|
|
943
1079
|
}
|
|
944
|
-
const { ctx } = resolveActiveContext(rawChannelId, event.runId, "llm_input");
|
|
1080
|
+
const { ctx, channelId } = resolveActiveContext(rawChannelId, event.runId, "llm_input", hookCtx);
|
|
945
1081
|
const ocSessionId = resolveOpenclawSessionId(hookCtx);
|
|
946
1082
|
if (ocSessionId) {
|
|
947
1083
|
ctx.openclawSessionId = ocSessionId;
|
|
@@ -966,9 +1102,10 @@ const cozeloopTracePlugin = {
|
|
|
966
1102
|
rememberCozeContext(event.prompt, ctx.openclawSessionId || lastOpenclawSessionId);
|
|
967
1103
|
// Fallback: ensure root + agent spans exist in case before_agent_start
|
|
968
1104
|
// was not fired (older OpenClaw versions or resumed sessions).
|
|
969
|
-
const
|
|
1105
|
+
const active = resolveActiveAgentContext(hookCtx, channelId);
|
|
1106
|
+
const channelIdForSpans = active?.channelId || channelId || rawChannelId;
|
|
970
1107
|
await ensureRootSpan(ctx, channelIdForSpans);
|
|
971
|
-
await ensureAgentSpan(ctx, channelIdForSpans);
|
|
1108
|
+
await ensureAgentSpan(ctx, channelIdForSpans, undefined, hookCtx);
|
|
972
1109
|
const messages = [];
|
|
973
1110
|
if (event.systemPrompt) {
|
|
974
1111
|
messages.push({ role: "system", content: safeClone(event.systemPrompt) });
|
|
@@ -1035,14 +1172,15 @@ const cozeloopTracePlugin = {
|
|
|
1035
1172
|
api.logger.info(`[CozeloopTrace][DEBUG] llm_output event keys=${JSON.stringify(Object.keys(event))}`);
|
|
1036
1173
|
api.logger.info(`[CozeloopTrace] llm_output hookCtx: ${JSON.stringify({ channelId: hookCtx.channelId, sessionKey: hookCtx.sessionKey, conversationId: hookCtx.conversationId })}, event.runId=${event.runId}`);
|
|
1037
1174
|
}
|
|
1038
|
-
const { ctx, channelId } = resolveActiveContext(rawChannelId, event.runId, "llm_output");
|
|
1175
|
+
const { ctx, channelId } = resolveActiveContext(rawChannelId, event.runId, "llm_output", hookCtx);
|
|
1039
1176
|
const now = Date.now();
|
|
1040
1177
|
const startTime = ctx.llmStartTime || lastLlmStartTime || now;
|
|
1041
1178
|
if (event.assistantTexts && event.assistantTexts.length > 0) {
|
|
1042
1179
|
const outputText = event.assistantTexts.join("\n");
|
|
1043
1180
|
ctx.lastOutput = outputText;
|
|
1044
|
-
|
|
1045
|
-
|
|
1181
|
+
const lastUser = resolveLastUserContext(hookCtx, channelId);
|
|
1182
|
+
if (lastUser) {
|
|
1183
|
+
lastUser.ctx.lastOutput = outputText;
|
|
1046
1184
|
}
|
|
1047
1185
|
if (config.debug) {
|
|
1048
1186
|
api.logger.info(`[CozeloopTrace] Captured output from llm_output (will use last): traceId=${ctx.traceId}, length=${outputText.length}`);
|
|
@@ -1128,34 +1266,39 @@ const cozeloopTracePlugin = {
|
|
|
1128
1266
|
if (config.debug) {
|
|
1129
1267
|
api.logger.info(`[CozeloopTrace] before_tool_call hookCtx: ${JSON.stringify({ channelId: hookCtx.channelId, sessionKey: hookCtx.sessionKey, conversationId: hookCtx.conversationId })}, toolName=${event.toolName}`);
|
|
1130
1268
|
}
|
|
1131
|
-
const { ctx, channelId } = resolveActiveContext(rawChannelId, undefined, "before_tool_call");
|
|
1132
|
-
pendingToolCall = {
|
|
1269
|
+
const { ctx, channelId } = resolveActiveContext(rawChannelId, undefined, "before_tool_call", hookCtx);
|
|
1270
|
+
const pendingToolCall = {
|
|
1133
1271
|
toolName: event.toolName,
|
|
1272
|
+
toolCallId: getToolCallId(event),
|
|
1134
1273
|
toolSpanId: generateId(16),
|
|
1135
1274
|
toolStartTime: Date.now(),
|
|
1136
1275
|
toolInput: event.params,
|
|
1137
1276
|
traceContext: ctx,
|
|
1138
1277
|
channelId: channelId,
|
|
1139
1278
|
};
|
|
1279
|
+
const pendingCount = pushPendingToolCall(ctx, channelId, pendingToolCall);
|
|
1140
1280
|
ctx.reactCount = (ctx.reactCount || 0) + 1;
|
|
1141
1281
|
if (config.debug) {
|
|
1142
|
-
api.logger.info(`[CozeloopTrace] Tool call started: ${event.toolName}, spanId=${pendingToolCall.toolSpanId}, traceId=${ctx.traceId}`);
|
|
1282
|
+
api.logger.info(`[CozeloopTrace] Tool call started: ${event.toolName}, toolCallId=${pendingToolCall.toolCallId || "none"}, spanId=${pendingToolCall.toolSpanId}, traceId=${ctx.traceId}, pendingCount=${pendingCount}`);
|
|
1143
1283
|
}
|
|
1144
1284
|
});
|
|
1145
1285
|
}
|
|
1146
1286
|
if (shouldHookEnabled("after_tool_call")) {
|
|
1147
1287
|
on("after_tool_call", async (event, hookCtx) => {
|
|
1288
|
+
const rawChannelId = resolveChannelId(hookCtx);
|
|
1289
|
+
const { ctx, channelId } = resolveActiveContext(rawChannelId, undefined, "after_tool_call", hookCtx);
|
|
1148
1290
|
if (config.debug) {
|
|
1149
1291
|
api.logger.info(`[CozeloopTrace] after_tool_call hookCtx: ${JSON.stringify({ channelId: hookCtx.channelId, sessionKey: hookCtx.sessionKey, conversationId: hookCtx.conversationId })}, toolName=${event.toolName}`);
|
|
1150
1292
|
}
|
|
1151
|
-
|
|
1293
|
+
const toolCallId = getToolCallId(event);
|
|
1294
|
+
const pendingToolCall = shiftPendingToolCall(ctx, channelId, event.toolName, toolCallId);
|
|
1295
|
+
if (!pendingToolCall) {
|
|
1152
1296
|
if (config.debug) {
|
|
1153
|
-
api.logger.info(`[CozeloopTrace] Skipping after_tool_call: no pending tool
|
|
1297
|
+
api.logger.info(`[CozeloopTrace] Skipping after_tool_call: no pending tool, toolName=${event.toolName}, toolCallId=${toolCallId || "none"}, pendingKeys=${pendingToolCallsByKey.size}`);
|
|
1154
1298
|
}
|
|
1155
1299
|
return;
|
|
1156
1300
|
}
|
|
1157
1301
|
const { toolName, toolSpanId, toolStartTime, toolInput, traceContext } = pendingToolCall;
|
|
1158
|
-
pendingToolCall = undefined;
|
|
1159
1302
|
const now = Date.now();
|
|
1160
1303
|
if (!traceContext.pendingToolSpans) {
|
|
1161
1304
|
traceContext.pendingToolSpans = [];
|
|
@@ -1177,11 +1320,12 @@ const cozeloopTracePlugin = {
|
|
|
1177
1320
|
// Helper: finalize a trace — end agent span (if open), end root span, flush,
|
|
1178
1321
|
// and clean up all state. Called from agent_end (normal path) and
|
|
1179
1322
|
// session_end (fallback for old OpenClaw versions that don't emit agent_end).
|
|
1180
|
-
|
|
1323
|
+
const finalizingTraces = new Set();
|
|
1181
1324
|
const finalizeTrace = (ctx, channelId, agentEndAttrs, agentOutput) => {
|
|
1182
|
-
|
|
1325
|
+
const finalizeKey = ctx.rootSpanId || ctx.traceId;
|
|
1326
|
+
if (finalizingTraces.has(finalizeKey))
|
|
1183
1327
|
return;
|
|
1184
|
-
|
|
1328
|
+
finalizingTraces.add(finalizeKey);
|
|
1185
1329
|
const now = Date.now();
|
|
1186
1330
|
// End agent span if still open.
|
|
1187
1331
|
if (ctx.agentSpanId) {
|
|
@@ -1215,20 +1359,17 @@ const cozeloopTracePlugin = {
|
|
|
1215
1359
|
}
|
|
1216
1360
|
await exporter.flush();
|
|
1217
1361
|
exporter.endTrace(rootSpanId);
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
activeAgentChannelId = undefined;
|
|
1221
|
-
}
|
|
1362
|
+
clearPendingToolCallsForContext(ctx);
|
|
1363
|
+
clearActiveAgentContext(ctx);
|
|
1222
1364
|
if (savedLastUserChannelId) {
|
|
1223
1365
|
endTurn(savedLastUserChannelId);
|
|
1224
1366
|
}
|
|
1225
1367
|
if (originalChannelId && originalChannelId !== savedLastUserChannelId) {
|
|
1226
1368
|
endTurn(originalChannelId);
|
|
1227
1369
|
}
|
|
1228
|
-
|
|
1229
|
-
lastUserTraceContext = undefined;
|
|
1370
|
+
clearLastUserContext(ctx);
|
|
1230
1371
|
lastUserInput = undefined;
|
|
1231
|
-
|
|
1372
|
+
finalizingTraces.delete(finalizeKey);
|
|
1232
1373
|
}, 200);
|
|
1233
1374
|
};
|
|
1234
1375
|
// OpenClaw runtime 周期性发送的心跳轮询消息,不是真实对话,整条 trace 丢弃。
|
|
@@ -1322,9 +1463,11 @@ const cozeloopTracePlugin = {
|
|
|
1322
1463
|
};
|
|
1323
1464
|
// Helper: ensure the agent span exists for a given context.
|
|
1324
1465
|
// Safe to call multiple times — only creates the span once.
|
|
1325
|
-
const ensureAgentSpan = async (ctx, channelId, agentId) => {
|
|
1326
|
-
if (ctx.agentSpanId)
|
|
1466
|
+
const ensureAgentSpan = async (ctx, channelId, agentId, hookCtx) => {
|
|
1467
|
+
if (ctx.agentSpanId) {
|
|
1468
|
+
rememberActiveAgentContext(hookCtx || {}, channelId, ctx);
|
|
1327
1469
|
return;
|
|
1470
|
+
}
|
|
1328
1471
|
const effectiveAgentId = agentId || "main";
|
|
1329
1472
|
const now = Date.now();
|
|
1330
1473
|
ctx.agentStartTime = now;
|
|
@@ -1347,8 +1490,7 @@ const cozeloopTracePlugin = {
|
|
|
1347
1490
|
};
|
|
1348
1491
|
await exporter.startSpan(spanData, ctx.agentSpanId);
|
|
1349
1492
|
// Set active agent context so all subsequent hooks use the same Trace.
|
|
1350
|
-
|
|
1351
|
-
activeAgentChannelId = channelId;
|
|
1493
|
+
rememberActiveAgentContext(hookCtx || {}, channelId, ctx);
|
|
1352
1494
|
if (config.debug) {
|
|
1353
1495
|
api.logger.info(`[CozeloopTrace] ensureAgentSpan: created agent span, agentId=${effectiveAgentId}, spanId=${ctx.agentSpanId}, traceId=${ctx.traceId}`);
|
|
1354
1496
|
}
|
|
@@ -1368,7 +1510,7 @@ const cozeloopTracePlugin = {
|
|
|
1368
1510
|
}
|
|
1369
1511
|
ctx.openclawSessionId = ctx.openclawSessionId || lastOpenclawSessionId;
|
|
1370
1512
|
await ensureRootSpan(ctx, channelId);
|
|
1371
|
-
await ensureAgentSpan(ctx, channelId, agentId);
|
|
1513
|
+
await ensureAgentSpan(ctx, channelId, agentId, hookCtx);
|
|
1372
1514
|
});
|
|
1373
1515
|
}
|
|
1374
1516
|
if (shouldHookEnabled("agent_end")) {
|
|
@@ -1378,8 +1520,9 @@ const cozeloopTracePlugin = {
|
|
|
1378
1520
|
api.logger.info(`[CozeloopTrace] agent_end hookCtx: ${JSON.stringify({ channelId: hookCtx.channelId, sessionKey: hookCtx.sessionKey, conversationId: hookCtx.conversationId })}`);
|
|
1379
1521
|
}
|
|
1380
1522
|
// Use activeAgentCtx if available, otherwise fall back to resolution.
|
|
1381
|
-
const
|
|
1382
|
-
const
|
|
1523
|
+
const active = resolveActiveAgentContext(hookCtx, rawChannelId);
|
|
1524
|
+
const ctx = active?.ctx || getOrCreateContext(rawChannelId, undefined, "agent_end").ctx;
|
|
1525
|
+
const channelId = active?.channelId || rawChannelId;
|
|
1383
1526
|
finalizeTrace(ctx, channelId, {
|
|
1384
1527
|
"agent.duration_ms": event.durationMs || 0,
|
|
1385
1528
|
"agent.message_count": event.messageCount || 0,
|
|
@@ -1397,9 +1540,11 @@ const cozeloopTracePlugin = {
|
|
|
1397
1540
|
if (config.debug) {
|
|
1398
1541
|
api.logger.info(`[CozeloopTrace] session_end: ${rawChannelId}`);
|
|
1399
1542
|
}
|
|
1400
|
-
const
|
|
1543
|
+
const active = resolveActiveAgentContext(hookCtx, rawChannelId);
|
|
1544
|
+
const lastUser = resolveLastUserContext(hookCtx, rawChannelId);
|
|
1545
|
+
const ctx = active?.ctx || lastUser?.ctx;
|
|
1401
1546
|
if (ctx && ctx.rootSpanStartTime) {
|
|
1402
|
-
const channelId =
|
|
1547
|
+
const channelId = active?.channelId || lastUser?.channelId || rawChannelId;
|
|
1403
1548
|
if (config.debug) {
|
|
1404
1549
|
api.logger.info(`[CozeloopTrace] session_end: finalizing trace as fallback, traceId=${ctx.traceId}`);
|
|
1405
1550
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cozeloop/openclaw-cozeloop-trace",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
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"
|