@tangle-network/sdk-telemetry 0.7.5 → 0.7.6
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 +4 -0
- package/dist/index.d.mts +72 -207
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +406 -353
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -10
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,285 @@
|
|
|
1
1
|
import { PROVISION_TRACE_ID_HEADER, SpanRecorder, isAdoptablePtid, mintPtid } from "./stage-span.mjs";
|
|
2
|
-
import { GEN_AI_CONVERSATION_ID, GEN_AI_CONVERSATION_ID as GEN_AI_CONVERSATION_ID$1, GEN_AI_INPUT_TOKEN_KEYS, GEN_AI_MODEL_KEYS, GEN_AI_OPERATION_NAME, GEN_AI_OUTPUT_TOKEN_KEYS, GEN_AI_REQUEST_MODEL, GEN_AI_REQUEST_MODEL as GEN_AI_REQUEST_MODEL$1, GEN_AI_RESPONSE_MODEL, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_INPUT_TOKENS as GEN_AI_USAGE_INPUT_TOKENS$1, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS as GEN_AI_USAGE_OUTPUT_TOKENS$1, SSEChunkParser, TOKEN_USAGE_INPUT_KEYS, TOKEN_USAGE_OUTPUT_KEYS, createStreamUsageExtractor, firstTokenCount, genAiUsageAttributes, isFilePart, isReasoningPart, isTextPart, isToolPart, isToolPart as isToolPart$1, tokenCount } from "@tangle-network/agent-core";
|
|
2
|
+
import { GEN_AI_CONVERSATION_ID, GEN_AI_CONVERSATION_ID as GEN_AI_CONVERSATION_ID$1, GEN_AI_INPUT_TOKEN_KEYS, GEN_AI_MODEL_KEYS, GEN_AI_OPERATION_NAME, GEN_AI_OUTPUT_TOKEN_KEYS, GEN_AI_REQUEST_MODEL, GEN_AI_REQUEST_MODEL as GEN_AI_REQUEST_MODEL$1, GEN_AI_RESPONSE_MODEL, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_INPUT_TOKENS as GEN_AI_USAGE_INPUT_TOKENS$1, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS as GEN_AI_USAGE_OUTPUT_TOKENS$1, SSEChunkParser, TOKEN_USAGE_INPUT_KEYS, TOKEN_USAGE_OUTPUT_KEYS, createStreamUsageExtractor, firstTokenCount, firstTokenCount as firstTokenCount$1, genAiUsageAttributes, isFilePart, isReasoningPart, isTextPart, isToolPart, isToolPart as isToolPart$1, readTokenUsage, tokenCount } from "@tangle-network/agent-core";
|
|
3
|
+
import { CACHE_READ_TOKEN_ATTR_KEYS, CACHE_WRITE_TOKEN_ATTR_KEYS, REASONING_TOKEN_ATTR_KEYS, contractSpan, llmSpan, toW3CSpanId, toolSpan } from "@tangle-network/agent-trace-contract";
|
|
4
|
+
//#region src/contract-spans.ts
|
|
5
|
+
/**
|
|
6
|
+
* One execution's `TraceEvent`s as `@tangle-network/agent-trace-contract`
|
|
7
|
+
* spans, which any OTLP-JSONL reader takes without an adapter.
|
|
8
|
+
*
|
|
9
|
+
* The execution is an `invoke_agent` AGENT span. Each model turn the harness
|
|
10
|
+
* reported is an `llm.turn` span with that turn's usage, and each message,
|
|
11
|
+
* reasoning part and tool call is a span inside the turn it happened in. Names
|
|
12
|
+
* and attributes follow `@tangle-network/harness-sessions` (`tool.<name>`,
|
|
13
|
+
* `input.value`, `output.value`), so a live execution and a session file of
|
|
14
|
+
* the same harness read alike. Unreported usage carries no token attribute and
|
|
15
|
+
* an unreported tool end keeps an unset status: unknown is never zero. A patch
|
|
16
|
+
* ends when a file part reports the last file it lists written, as a Sandbox
|
|
17
|
+
* reports a Codex patch landing. Span ids derive from the execution id, so a
|
|
18
|
+
* writer can rewrite its file instead of appending.
|
|
19
|
+
*/
|
|
20
|
+
const OK = { code: "STATUS_CODE_OK" };
|
|
21
|
+
const UNSET = { code: "STATUS_CODE_UNSET" };
|
|
22
|
+
const ENDED = new Set([
|
|
23
|
+
"completed",
|
|
24
|
+
"error",
|
|
25
|
+
"failed"
|
|
26
|
+
]);
|
|
27
|
+
/** Project one execution's events: the execution span, then each span in the order its first event arrived. */
|
|
28
|
+
function executionTraceToContractSpans(trace, context) {
|
|
29
|
+
const chars = context.contentChars ?? 8e3;
|
|
30
|
+
const content = (key, value) => {
|
|
31
|
+
const text = typeof value === "string" ? value : JSON.stringify(value) ?? "";
|
|
32
|
+
return chars > 0 && value != null && text.length > 0 ? { [key]: cap(text, chars) } : {};
|
|
33
|
+
};
|
|
34
|
+
const id = (key) => toW3CSpanId(`${trace.id}:${key}`);
|
|
35
|
+
const executionId = id("execution");
|
|
36
|
+
const first = trace.events[0]?.timestamp ?? trace.startedAt;
|
|
37
|
+
const last = trace.events.at(-1)?.timestamp ?? trace.completedAt ?? first;
|
|
38
|
+
const spans = [];
|
|
39
|
+
const turns = [];
|
|
40
|
+
const messages = /* @__PURE__ */ new Map();
|
|
41
|
+
const tools = /* @__PURE__ */ new Map();
|
|
42
|
+
const unwritten = /* @__PURE__ */ new Map();
|
|
43
|
+
let openTurn;
|
|
44
|
+
let model;
|
|
45
|
+
let profile;
|
|
46
|
+
let sandboxId;
|
|
47
|
+
let harness;
|
|
48
|
+
let finalText;
|
|
49
|
+
let resultUsage;
|
|
50
|
+
let outcome;
|
|
51
|
+
let error;
|
|
52
|
+
let base = { traceId: context.traceId };
|
|
53
|
+
let shared = {};
|
|
54
|
+
const startTurn = (start) => {
|
|
55
|
+
const turn = {
|
|
56
|
+
spanId: id(`turn:${turns.length}`),
|
|
57
|
+
end: start,
|
|
58
|
+
status: UNSET
|
|
59
|
+
};
|
|
60
|
+
turns.push(turn);
|
|
61
|
+
spans.push(() => llmSpan({
|
|
62
|
+
...base,
|
|
63
|
+
spanId: turn.spanId,
|
|
64
|
+
parentSpanId: executionId,
|
|
65
|
+
name: "llm.turn",
|
|
66
|
+
startTime: start.toISOString(),
|
|
67
|
+
endTime: turn.end.toISOString(),
|
|
68
|
+
status: turn.status,
|
|
69
|
+
...model ? { model } : {},
|
|
70
|
+
...turn.usage ? {
|
|
71
|
+
inputTokens: turn.usage.inputTokens,
|
|
72
|
+
outputTokens: turn.usage.outputTokens
|
|
73
|
+
} : {},
|
|
74
|
+
attributes: {
|
|
75
|
+
...shared,
|
|
76
|
+
...usageDetail(turn.usage)
|
|
77
|
+
}
|
|
78
|
+
}));
|
|
79
|
+
return turn;
|
|
80
|
+
};
|
|
81
|
+
for (const event of trace.events) {
|
|
82
|
+
const at = event.timestamp;
|
|
83
|
+
if (openTurn !== void 0) openTurn.end = at;
|
|
84
|
+
const parentSpanId = openTurn?.spanId ?? executionId;
|
|
85
|
+
if (event.type === "message.part.updated") {
|
|
86
|
+
const part = event.part;
|
|
87
|
+
if (isTextPart(part) || isReasoningPart(part)) {
|
|
88
|
+
const held = messages.get(part.id);
|
|
89
|
+
if (held !== void 0 && part.text.startsWith(held.text)) Object.assign(held, {
|
|
90
|
+
text: part.text,
|
|
91
|
+
end: at
|
|
92
|
+
});
|
|
93
|
+
else if (part.text.length > 0) {
|
|
94
|
+
const kind = isTextPart(part) ? "assistant" : "reasoning";
|
|
95
|
+
const message = {
|
|
96
|
+
text: part.text,
|
|
97
|
+
end: at
|
|
98
|
+
};
|
|
99
|
+
const spanId = id(`${kind}:${spans.length}`);
|
|
100
|
+
messages.set(part.id, message);
|
|
101
|
+
spans.push(() => contractSpan({
|
|
102
|
+
...base,
|
|
103
|
+
spanId,
|
|
104
|
+
parentSpanId,
|
|
105
|
+
name: `message.${kind}`,
|
|
106
|
+
kind: "CHAIN",
|
|
107
|
+
startTime: at.toISOString(),
|
|
108
|
+
endTime: message.end.toISOString(),
|
|
109
|
+
status: OK,
|
|
110
|
+
attributes: {
|
|
111
|
+
...shared,
|
|
112
|
+
"message.role": "assistant",
|
|
113
|
+
"message.part.type": isTextPart(part) ? "text" : "reasoning",
|
|
114
|
+
"message.part.text_length": message.text.length,
|
|
115
|
+
...content("output.value", message.text)
|
|
116
|
+
}
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
} else if (isToolPart$1(part)) {
|
|
120
|
+
let tool = tools.get(part.id);
|
|
121
|
+
if (tool === void 0) {
|
|
122
|
+
const created = {
|
|
123
|
+
status: part.state.status,
|
|
124
|
+
lastSeen: at
|
|
125
|
+
};
|
|
126
|
+
tools.set(part.id, created);
|
|
127
|
+
tool = created;
|
|
128
|
+
spans.push(() => {
|
|
129
|
+
const start = created.start ?? at.getTime();
|
|
130
|
+
const end = created.end ?? (ENDED.has(created.status) ? created.lastSeen.getTime() : start);
|
|
131
|
+
const failed = created.status !== "completed" && ENDED.has(created.status);
|
|
132
|
+
return toolSpan({
|
|
133
|
+
...base,
|
|
134
|
+
spanId: id(`tool:${part.id}`),
|
|
135
|
+
parentSpanId,
|
|
136
|
+
name: `tool.${part.tool}`,
|
|
137
|
+
toolName: part.tool,
|
|
138
|
+
startTime: new Date(start).toISOString(),
|
|
139
|
+
endTime: new Date(Math.max(start, end)).toISOString(),
|
|
140
|
+
status: failed ? {
|
|
141
|
+
code: "STATUS_CODE_ERROR",
|
|
142
|
+
message: cap(created.error ?? "tool reported an error", 500)
|
|
143
|
+
} : created.status === "completed" ? OK : UNSET,
|
|
144
|
+
attributes: {
|
|
145
|
+
...shared,
|
|
146
|
+
"gen_ai.tool.call.id": part.id,
|
|
147
|
+
"tangle.tool.status": created.status,
|
|
148
|
+
...content("input.value", created.input),
|
|
149
|
+
...content("output.value", created.output ?? created.error)
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
foldTool(tool, part.state, at);
|
|
155
|
+
if (tool.status === "running") for (const path of changedPaths(tool.input)) unwritten.set(path, tool);
|
|
156
|
+
} else if (isFilePart(part) && part.url?.startsWith("file://")) {
|
|
157
|
+
const patch = unwritten.get(part.url.slice(7));
|
|
158
|
+
if (patch !== void 0) {
|
|
159
|
+
unwritten.delete(part.url.slice(7));
|
|
160
|
+
if (![...unwritten.values()].includes(patch)) Object.assign(patch, {
|
|
161
|
+
status: "completed",
|
|
162
|
+
lastSeen: at,
|
|
163
|
+
end: at.getTime()
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} else if (event.type === "message.updated") {
|
|
168
|
+
finalText = event.finalText ?? event.text ?? finalText;
|
|
169
|
+
resultUsage = usageOf(event.tokenUsage) ?? resultUsage;
|
|
170
|
+
outcome = "completed";
|
|
171
|
+
} else if (event.type === "error") {
|
|
172
|
+
error = event.message;
|
|
173
|
+
outcome = event.code ?? "error";
|
|
174
|
+
} else if (event.type === "custom" && event.name === "execution.started") {
|
|
175
|
+
const backend = record(event.data.effectiveBackend);
|
|
176
|
+
model = text(backend?.model) ?? model;
|
|
177
|
+
profile = text(record(backend?.profile)?.name) ?? profile;
|
|
178
|
+
sandboxId = text(event.data.sandboxId) ?? sandboxId;
|
|
179
|
+
} else if (event.type === "custom" && event.name === "raw") {
|
|
180
|
+
harness ??= text(event.data.backend);
|
|
181
|
+
const raw = record(event.data.event);
|
|
182
|
+
const type = text(raw?.type);
|
|
183
|
+
if (type === "turn.started") openTurn = startTurn(at);
|
|
184
|
+
else if (type === "turn.completed" || type === "turn.failed") {
|
|
185
|
+
const turn = openTurn ?? startTurn(at);
|
|
186
|
+
turn.end = at;
|
|
187
|
+
turn.usage = usageOf(raw?.usage);
|
|
188
|
+
turn.status = type === "turn.completed" ? OK : {
|
|
189
|
+
code: "STATUS_CODE_ERROR",
|
|
190
|
+
message: text(record(raw?.error)?.message) ?? "turn failed"
|
|
191
|
+
};
|
|
192
|
+
openTurn = void 0;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (turns.length === 0 && resultUsage !== void 0) startTurn(first).end = last;
|
|
197
|
+
const lastTurn = turns.at(-1);
|
|
198
|
+
if (lastTurn !== void 0 && !turns.some((turn) => turn.usage)) lastTurn.usage = resultUsage;
|
|
199
|
+
const agent = context.agent ?? (profile ? { profileName: profile } : void 0);
|
|
200
|
+
base = {
|
|
201
|
+
traceId: context.traceId,
|
|
202
|
+
...agent ? { agent } : {}
|
|
203
|
+
};
|
|
204
|
+
shared = {
|
|
205
|
+
...harness ? { "service.name": harness } : {},
|
|
206
|
+
...context.attributes,
|
|
207
|
+
"tangle.execution.id": trace.id,
|
|
208
|
+
...sandboxId ? { "tangle.sandbox.id": sandboxId } : {}
|
|
209
|
+
};
|
|
210
|
+
return [contractSpan({
|
|
211
|
+
...base,
|
|
212
|
+
spanId: executionId,
|
|
213
|
+
parentSpanId: context.parentSpanId ?? null,
|
|
214
|
+
name: "invoke_agent",
|
|
215
|
+
kind: "AGENT",
|
|
216
|
+
startTime: first.toISOString(),
|
|
217
|
+
endTime: last.toISOString(),
|
|
218
|
+
status: error !== void 0 ? {
|
|
219
|
+
code: "STATUS_CODE_ERROR",
|
|
220
|
+
message: cap(error, 500)
|
|
221
|
+
} : outcome === "completed" ? OK : UNSET,
|
|
222
|
+
attributes: {
|
|
223
|
+
...shared,
|
|
224
|
+
"gen_ai.operation.name": "invoke_agent",
|
|
225
|
+
...profile ? { "gen_ai.agent.name": profile } : {},
|
|
226
|
+
...model ? { "gen_ai.request.model": model } : {},
|
|
227
|
+
...outcome ? { "tangle.execution.outcome": outcome } : {},
|
|
228
|
+
...content("output.value", finalText)
|
|
229
|
+
}
|
|
230
|
+
}), ...spans.map((build) => build())];
|
|
231
|
+
}
|
|
232
|
+
/** Fold one event's view of a tool call into what its earlier events reported. */
|
|
233
|
+
function foldTool(tool, state, at) {
|
|
234
|
+
tool.lastSeen = at;
|
|
235
|
+
tool.status = String(state.status);
|
|
236
|
+
const input = record(state.input);
|
|
237
|
+
if (state.input !== void 0 && (!input || Object.keys(input).length > 0)) tool.input = state.input;
|
|
238
|
+
if (state.output != null) tool.output = state.output;
|
|
239
|
+
tool.error = text(state.error) ?? tool.error;
|
|
240
|
+
const time = record(state.time);
|
|
241
|
+
const start = number(time?.start);
|
|
242
|
+
if (start !== void 0 && (tool.start === void 0 || start < tool.start)) tool.start = start;
|
|
243
|
+
tool.end = number(time?.end) ?? tool.end;
|
|
244
|
+
}
|
|
245
|
+
/** The paths a patch's input lists as `changes: [{path}]`. */
|
|
246
|
+
function changedPaths(input) {
|
|
247
|
+
const changes = record(input)?.changes;
|
|
248
|
+
return Array.isArray(changes) ? changes.flatMap((change) => text(record(change)?.path) ?? []) : [];
|
|
249
|
+
}
|
|
250
|
+
/** A usage report in any producer's spelling, read by agent-core, plus its reasoning share. */
|
|
251
|
+
function usageOf(value) {
|
|
252
|
+
const bag = record(value);
|
|
253
|
+
const counts = bag && readTokenUsage(bag);
|
|
254
|
+
if (!bag || !counts) return void 0;
|
|
255
|
+
const reasoningTokens = firstTokenCount$1(bag, ["reasoning_output_tokens", "reasoningTokens"]);
|
|
256
|
+
return reasoningTokens === void 0 ? counts : {
|
|
257
|
+
...counts,
|
|
258
|
+
reasoningTokens
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/** The usage `llmSpan` does not carry, under the contract's first-read keys. */
|
|
262
|
+
function usageDetail(usage) {
|
|
263
|
+
const detail = [
|
|
264
|
+
[CACHE_READ_TOKEN_ATTR_KEYS[0] ?? "", usage?.cacheReadTokens],
|
|
265
|
+
[CACHE_WRITE_TOKEN_ATTR_KEYS[0] ?? "", usage?.cacheWriteTokens],
|
|
266
|
+
[REASONING_TOKEN_ATTR_KEYS[0] ?? "", usage?.reasoningTokens]
|
|
267
|
+
];
|
|
268
|
+
return Object.fromEntries(detail.filter(([, count]) => count !== void 0));
|
|
269
|
+
}
|
|
270
|
+
function cap(value, chars) {
|
|
271
|
+
return value.length <= chars ? value : `${value.slice(0, chars)}…`;
|
|
272
|
+
}
|
|
273
|
+
function record(value) {
|
|
274
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
275
|
+
}
|
|
276
|
+
function text(value) {
|
|
277
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
278
|
+
}
|
|
279
|
+
function number(value) {
|
|
280
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
281
|
+
}
|
|
282
|
+
//#endregion
|
|
3
283
|
//#region src/enrichment/parse.ts
|
|
4
284
|
const VALID_PRODUCTS = [
|
|
5
285
|
"vibe_coding",
|
|
@@ -75,6 +355,67 @@ function parseJsonBlob(blob) {
|
|
|
75
355
|
return context;
|
|
76
356
|
}
|
|
77
357
|
//#endregion
|
|
358
|
+
//#region src/otel-provider.ts
|
|
359
|
+
let provider = null;
|
|
360
|
+
/** Parse `OTEL_EXPORTER_OTLP_HEADERS` ("k=v,k2=v2") into a header map. */
|
|
361
|
+
function parseHeaders(raw) {
|
|
362
|
+
const out = {};
|
|
363
|
+
if (!raw) return out;
|
|
364
|
+
for (const pair of raw.split(",")) {
|
|
365
|
+
const eq = pair.indexOf("=");
|
|
366
|
+
if (eq <= 0) continue;
|
|
367
|
+
const key = pair.slice(0, eq).trim();
|
|
368
|
+
const value = pair.slice(eq + 1).trim();
|
|
369
|
+
if (key) out[key] = value;
|
|
370
|
+
}
|
|
371
|
+
return out;
|
|
372
|
+
}
|
|
373
|
+
async function initOtelProvider(serviceName) {
|
|
374
|
+
if (provider) return { active: true };
|
|
375
|
+
if (process.env.TELEMETRY_ENABLED !== "true") return {
|
|
376
|
+
active: false,
|
|
377
|
+
reason: "TELEMETRY_ENABLED!=true"
|
|
378
|
+
};
|
|
379
|
+
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
380
|
+
if (!endpoint) return {
|
|
381
|
+
active: false,
|
|
382
|
+
reason: "OTEL_EXPORTER_OTLP_ENDPOINT unset"
|
|
383
|
+
};
|
|
384
|
+
try {
|
|
385
|
+
const [{ NodeTracerProvider, BatchSpanProcessor }, { OTLPTraceExporter }, { resourceFromAttributes }, { ATTR_SERVICE_NAME }] = await Promise.all([
|
|
386
|
+
import("@opentelemetry/sdk-trace-node"),
|
|
387
|
+
import("@opentelemetry/exporter-trace-otlp-http"),
|
|
388
|
+
import("@opentelemetry/resources"),
|
|
389
|
+
import("@opentelemetry/semantic-conventions")
|
|
390
|
+
]);
|
|
391
|
+
const exporter = new OTLPTraceExporter({
|
|
392
|
+
url: endpoint,
|
|
393
|
+
headers: parseHeaders(process.env.OTEL_EXPORTER_OTLP_HEADERS)
|
|
394
|
+
});
|
|
395
|
+
const tracerProvider = new NodeTracerProvider({
|
|
396
|
+
resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: serviceName }),
|
|
397
|
+
spanProcessors: [new BatchSpanProcessor(exporter)]
|
|
398
|
+
});
|
|
399
|
+
tracerProvider.register();
|
|
400
|
+
provider = tracerProvider;
|
|
401
|
+
return { active: true };
|
|
402
|
+
} catch (error) {
|
|
403
|
+
return {
|
|
404
|
+
active: false,
|
|
405
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async function shutdownOtelProvider() {
|
|
410
|
+
if (!provider) return;
|
|
411
|
+
const current = provider;
|
|
412
|
+
provider = null;
|
|
413
|
+
try {
|
|
414
|
+
await current.forceFlush();
|
|
415
|
+
} catch {}
|
|
416
|
+
await current.shutdown();
|
|
417
|
+
}
|
|
418
|
+
//#endregion
|
|
78
419
|
//#region src/trace-span-projection.ts
|
|
79
420
|
const GEN_AI_PROMOTED_KEYS = Object.freeze([
|
|
80
421
|
GEN_AI_REQUEST_MODEL$1,
|
|
@@ -148,7 +489,6 @@ function projectTraceEvent(event, format, includePayloads = false) {
|
|
|
148
489
|
if (format === "intelligence") {
|
|
149
490
|
const { timestamp, type, ...rest } = event;
|
|
150
491
|
return {
|
|
151
|
-
kind: "span",
|
|
152
492
|
name: type,
|
|
153
493
|
startTime: timestamp,
|
|
154
494
|
attributes: {
|
|
@@ -157,89 +497,34 @@ function projectTraceEvent(event, format, includePayloads = false) {
|
|
|
157
497
|
}
|
|
158
498
|
};
|
|
159
499
|
}
|
|
160
|
-
const otel = format === "otel";
|
|
161
500
|
const attrs = { "event.type": event.type };
|
|
162
501
|
const projection = {
|
|
163
|
-
kind: otel ? "span" : "event",
|
|
164
502
|
name: "unknown",
|
|
165
503
|
startTime: event.timestamp,
|
|
166
|
-
|
|
504
|
+
endTime: "durationMs" in event && typeof event.durationMs === "number" ? new Date(event.timestamp.getTime() + event.durationMs) : event.timestamp,
|
|
505
|
+
attributes: attrs
|
|
167
506
|
};
|
|
168
507
|
switch (event.type) {
|
|
169
508
|
case "message.part.updated": {
|
|
170
509
|
const part = event.part;
|
|
171
|
-
projection.metadata = {
|
|
172
|
-
partId: part.id,
|
|
173
|
-
sessionID: part.sessionID,
|
|
174
|
-
messageID: part.messageID
|
|
175
|
-
};
|
|
176
510
|
if (isToolPart$1(part)) {
|
|
177
511
|
const state = part.state;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
projection.error = typeof state.error === "string" ? state.error : "Tool execution failed";
|
|
183
|
-
projection.exception = true;
|
|
184
|
-
}
|
|
185
|
-
} else if (state.status === "running") {
|
|
186
|
-
projection.kind = "span";
|
|
187
|
-
projection.name = `tool.${part.tool}`;
|
|
188
|
-
projection.input = state.input ?? { callID: part.callID };
|
|
189
|
-
projection.metadata.status = status;
|
|
190
|
-
} else if (state.status !== "pending") {
|
|
191
|
-
projection.kind = "span";
|
|
192
|
-
projection.startTime = state.time?.start ? new Date(state.time.start) : event.timestamp;
|
|
193
|
-
projection.endTime = state.time?.end ? new Date(state.time.end) : event.timestamp;
|
|
194
|
-
projection.input = state.input;
|
|
195
|
-
projection.output = state.status === "completed" ? state.output ?? "success" : state.output ?? state.error;
|
|
196
|
-
projection.metadata.callID = part.callID;
|
|
197
|
-
projection.metadata.success = state.status === "completed";
|
|
198
|
-
if (state.status !== "completed") {
|
|
199
|
-
projection.level = "ERROR";
|
|
200
|
-
projection.statusMessage = state.error;
|
|
201
|
-
}
|
|
202
|
-
} else return void 0;
|
|
203
|
-
} else if (isTextPart(part) || isReasoningPart(part)) {
|
|
204
|
-
projection.name = `${otel ? "llm." : ""}${part.type}`;
|
|
205
|
-
if (!otel) {
|
|
206
|
-
projection.kind = "generation";
|
|
207
|
-
projection.output = part.text;
|
|
208
|
-
projection.metadata.delta = event.delta;
|
|
512
|
+
projection.name = `tool.${part.tool}.${state.status}`;
|
|
513
|
+
if (state.status === "error" || state.status === "failed") {
|
|
514
|
+
projection.error = typeof state.error === "string" ? state.error : "Tool execution failed";
|
|
515
|
+
projection.exception = true;
|
|
209
516
|
}
|
|
210
|
-
} else if (
|
|
211
|
-
projection.name = otel ? "message.file" : `file.${part.mediaType || "unknown"}`;
|
|
212
|
-
projection.metadata = {
|
|
213
|
-
partId: part.id,
|
|
214
|
-
filename: part.filename,
|
|
215
|
-
mediaType: part.mediaType,
|
|
216
|
-
url: part.url
|
|
217
|
-
};
|
|
218
|
-
} else if (!otel) return void 0;
|
|
517
|
+
} else if (isTextPart(part) || isReasoningPart(part)) projection.name = `llm.${part.type}`;
|
|
219
518
|
else projection.name = "message.file";
|
|
220
|
-
|
|
519
|
+
addPartUpdateAttributes(attrs, event, includePayloads);
|
|
221
520
|
break;
|
|
222
521
|
}
|
|
223
522
|
case "message.updated":
|
|
224
523
|
projection.name = "message.updated";
|
|
225
|
-
|
|
226
|
-
text: event.text ?? event.finalText,
|
|
227
|
-
finalText: event.finalText,
|
|
228
|
-
tokenUsage: event.tokenUsage,
|
|
229
|
-
timing: event.timing,
|
|
230
|
-
toolInvocations: event.toolInvocations,
|
|
231
|
-
metadata: event.metadata
|
|
232
|
-
};
|
|
233
|
-
if (otel) addMessageUpdateAttributes(attrs, event, includePayloads);
|
|
524
|
+
addMessageUpdateAttributes(attrs, event, includePayloads);
|
|
234
525
|
break;
|
|
235
526
|
case "error":
|
|
236
527
|
projection.name = `error.${event.category}`;
|
|
237
|
-
projection.metadata = {
|
|
238
|
-
message: event.message,
|
|
239
|
-
code: event.code,
|
|
240
|
-
source: event.source
|
|
241
|
-
};
|
|
242
|
-
projection.level = "ERROR";
|
|
243
528
|
projection.error = event.message;
|
|
244
529
|
projection.exception = true;
|
|
245
530
|
attrs["error.category"] = event.category;
|
|
@@ -247,276 +532,20 @@ function projectTraceEvent(event, format, includePayloads = false) {
|
|
|
247
532
|
if (event.code) attrs["error.code"] = event.code;
|
|
248
533
|
if (event.source) attrs["error.source"] = event.source;
|
|
249
534
|
break;
|
|
250
|
-
case "diagnostic":
|
|
251
|
-
projection.name = `diagnostic.${event.language || "unknown"}`;
|
|
252
|
-
projection.metadata = {
|
|
253
|
-
file: event.file,
|
|
254
|
-
errorDelta: event.errorDelta,
|
|
255
|
-
errorsAfter: event.afterSummary.errors,
|
|
256
|
-
warningsAfter: event.afterSummary.warnings
|
|
257
|
-
};
|
|
258
|
-
attrs["diagnostic.file"] = event.file;
|
|
259
|
-
if (event.language) attrs["diagnostic.language"] = event.language;
|
|
260
|
-
attrs["diagnostic.error_delta"] = event.errorDelta;
|
|
261
|
-
attrs["diagnostic.errors_after"] = event.afterSummary.errors;
|
|
262
|
-
attrs["diagnostic.warnings_after"] = event.afterSummary.warnings;
|
|
263
|
-
break;
|
|
264
|
-
case "file_change":
|
|
265
|
-
projection.name = `file.${event.changeType}`;
|
|
266
|
-
projection.metadata = {
|
|
267
|
-
file: event.file,
|
|
268
|
-
linesAdded: event.linesAdded,
|
|
269
|
-
linesRemoved: event.linesRemoved
|
|
270
|
-
};
|
|
271
|
-
attrs["file.path"] = event.file;
|
|
272
|
-
attrs["file.change_type"] = event.changeType;
|
|
273
|
-
if (event.linesAdded) attrs["file.lines_added"] = event.linesAdded;
|
|
274
|
-
if (event.linesRemoved) attrs["file.lines_removed"] = event.linesRemoved;
|
|
275
|
-
break;
|
|
276
|
-
case "build_result":
|
|
277
|
-
case "test_result": {
|
|
278
|
-
const build = event.type === "build_result";
|
|
279
|
-
const prefix = build ? "build" : "test";
|
|
280
|
-
projection.name = `${prefix}.${event.success ? "success" : "failure"}`;
|
|
281
|
-
projection.kind = "span";
|
|
282
|
-
projection.metadata = build ? {
|
|
283
|
-
command: event.command,
|
|
284
|
-
errorCount: event.errorCount,
|
|
285
|
-
warningCount: event.warningCount
|
|
286
|
-
} : {
|
|
287
|
-
command: event.command,
|
|
288
|
-
passed: event.passed,
|
|
289
|
-
failed: event.failed,
|
|
290
|
-
skipped: event.skipped
|
|
291
|
-
};
|
|
292
|
-
if (!otel) {
|
|
293
|
-
projection.endTime = event.durationMs ? new Date(event.timestamp.getTime() + event.durationMs) : void 0;
|
|
294
|
-
projection.output = event.success ? "success" : "failed";
|
|
295
|
-
projection.level = event.success ? void 0 : "ERROR";
|
|
296
|
-
}
|
|
297
|
-
if (!event.success) projection.error = build ? "Build failed" : "Tests failed";
|
|
298
|
-
attrs[`${prefix}.success`] = event.success;
|
|
299
|
-
if (event.command) attrs[`${prefix}.command`] = event.command;
|
|
300
|
-
if (event.durationMs) attrs[`${prefix}.duration_ms`] = event.durationMs;
|
|
301
|
-
const counts = build ? {
|
|
302
|
-
error_count: event.errorCount,
|
|
303
|
-
warning_count: event.warningCount
|
|
304
|
-
} : {
|
|
305
|
-
passed: event.passed,
|
|
306
|
-
failed: event.failed,
|
|
307
|
-
skipped: event.skipped
|
|
308
|
-
};
|
|
309
|
-
for (const [key, value] of Object.entries(counts)) if (value) attrs[`${prefix}.${key}`] = value;
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
312
|
-
case "environment":
|
|
313
|
-
projection.name = "environment.snapshot";
|
|
314
|
-
projection.metadata = {
|
|
315
|
-
workingDir: event.workingDir,
|
|
316
|
-
platform: event.platform
|
|
317
|
-
};
|
|
318
|
-
if (event.workingDir) attrs["env.working_dir"] = event.workingDir;
|
|
319
|
-
if (event.platform) attrs["env.platform"] = event.platform;
|
|
320
|
-
break;
|
|
321
535
|
case "custom":
|
|
322
536
|
projection.name = `custom.${event.name}`;
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
projection.kind = "span";
|
|
326
|
-
const phase = typeof event.data.phase === "string" ? event.data.phase : "unknown";
|
|
327
|
-
projection.name = `${event.name === "http.phase" ? "http" : "startup"}.${phase}`;
|
|
537
|
+
if (event.name === "startup.phase") {
|
|
538
|
+
projection.name = `startup.${typeof event.data.phase === "string" ? event.data.phase : "unknown"}`;
|
|
328
539
|
if (typeof event.data.durationMs === "number") projection.startTime = new Date(event.timestamp.getTime() - event.data.durationMs);
|
|
329
540
|
projection.endTime = event.timestamp;
|
|
330
|
-
if (event.data.status === "error")
|
|
331
|
-
projection.level = "ERROR";
|
|
332
|
-
projection.error = typeof event.data.error === "string" ? event.data.error : "Startup phase failed";
|
|
333
|
-
}
|
|
334
|
-
if (!otel && typeof event.data.error === "string") projection.statusMessage = event.data.error;
|
|
541
|
+
if (event.data.status === "error") projection.error = typeof event.data.error === "string" ? event.data.error : "Startup phase failed";
|
|
335
542
|
}
|
|
336
|
-
|
|
543
|
+
addCustomEventAttributes(attrs, event);
|
|
337
544
|
break;
|
|
338
|
-
default: if (!otel) return void 0;
|
|
339
545
|
}
|
|
340
|
-
if (otel) projection.attributes = attrs;
|
|
341
546
|
return projection;
|
|
342
547
|
}
|
|
343
548
|
//#endregion
|
|
344
|
-
//#region src/langfuse-sink.ts
|
|
345
|
-
/**
|
|
346
|
-
* Langfuse Sink
|
|
347
|
-
*
|
|
348
|
-
* Native Langfuse SDK integration for trace export.
|
|
349
|
-
* Uses the langfuse-node SDK directly instead of going through OTEL,
|
|
350
|
-
* which provides better compatibility and richer trace visualization.
|
|
351
|
-
*/
|
|
352
|
-
let langfuseClient = null;
|
|
353
|
-
let langfuseConfig = null;
|
|
354
|
-
/**
|
|
355
|
-
* Get or create the Langfuse client
|
|
356
|
-
*/
|
|
357
|
-
async function getLangfuseClient(config) {
|
|
358
|
-
if (langfuseClient && langfuseConfig === config) return langfuseClient;
|
|
359
|
-
try {
|
|
360
|
-
const { Langfuse } = await import("langfuse-node");
|
|
361
|
-
langfuseClient = new Langfuse({
|
|
362
|
-
publicKey: config.publicKey,
|
|
363
|
-
secretKey: config.secretKey,
|
|
364
|
-
baseUrl: config.baseUrl || "https://cloud.langfuse.com"
|
|
365
|
-
});
|
|
366
|
-
langfuseConfig = config;
|
|
367
|
-
return langfuseClient;
|
|
368
|
-
} catch (err) {
|
|
369
|
-
console.warn("[langfuse-sink] Failed to initialize Langfuse:", err);
|
|
370
|
-
return null;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
/**
|
|
374
|
-
* Langfuse trace exporter using the native SDK.
|
|
375
|
-
*
|
|
376
|
-
* Creates proper Langfuse traces with spans and generations
|
|
377
|
-
* for rich observability in the Langfuse dashboard.
|
|
378
|
-
*
|
|
379
|
-
* Note: This is a trace-level exporter (ExecutionTrace), not a
|
|
380
|
-
* TelemetrySink (TelemetryEvent). Use for exporting complete traces.
|
|
381
|
-
*/
|
|
382
|
-
var LangfuseSink = class {
|
|
383
|
-
name = "langfuse";
|
|
384
|
-
config;
|
|
385
|
-
constructor(config) {
|
|
386
|
-
this.config = config;
|
|
387
|
-
}
|
|
388
|
-
/**
|
|
389
|
-
* Export an execution trace to Langfuse.
|
|
390
|
-
*/
|
|
391
|
-
async exportTrace(trace) {
|
|
392
|
-
const client = await getLangfuseClient(this.config);
|
|
393
|
-
if (!client) return;
|
|
394
|
-
const effectiveTrace = this.config.redactTrace ? this.config.redactTrace(trace) : trace;
|
|
395
|
-
try {
|
|
396
|
-
const langfuseTrace = client.trace({
|
|
397
|
-
id: effectiveTrace.id,
|
|
398
|
-
name: effectiveTrace.task,
|
|
399
|
-
sessionId: effectiveTrace.scope.sessionId,
|
|
400
|
-
userId: effectiveTrace.scope.userId,
|
|
401
|
-
metadata: {
|
|
402
|
-
outcome: effectiveTrace.outcome,
|
|
403
|
-
projectRef: effectiveTrace.scope.projectRef,
|
|
404
|
-
...effectiveTrace.metadata
|
|
405
|
-
},
|
|
406
|
-
tags: effectiveTrace.outcome === "failure" ? ["failed"] : void 0
|
|
407
|
-
});
|
|
408
|
-
for (const event of effectiveTrace.events) {
|
|
409
|
-
if (!event) continue;
|
|
410
|
-
this.exportTraceEvent(langfuseTrace, event);
|
|
411
|
-
}
|
|
412
|
-
for (const signal of effectiveTrace.signals) langfuseTrace.event({
|
|
413
|
-
name: `signal.${signal.signal}`,
|
|
414
|
-
startTime: signal.timestamp,
|
|
415
|
-
metadata: signal.metadata
|
|
416
|
-
});
|
|
417
|
-
langfuseTrace.update({ output: effectiveTrace.outcome });
|
|
418
|
-
await client.flushAsync();
|
|
419
|
-
} catch (err) {
|
|
420
|
-
console.warn("[langfuse-sink] Failed to export trace:", err);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
exportTraceEvent(trace, event) {
|
|
424
|
-
const projected = projectTraceEvent(event, "langfuse");
|
|
425
|
-
if (!projected) return;
|
|
426
|
-
const { kind, attributes: _attributes, error: _error, exception: _exception, ...data } = projected;
|
|
427
|
-
if (kind === "generation") trace.generation(data);
|
|
428
|
-
else if (kind === "span") trace.span(data);
|
|
429
|
-
else trace.event(data);
|
|
430
|
-
}
|
|
431
|
-
async flush() {
|
|
432
|
-
const client = await getLangfuseClient(this.config);
|
|
433
|
-
if (client) await client.flushAsync();
|
|
434
|
-
}
|
|
435
|
-
async close() {
|
|
436
|
-
const client = await getLangfuseClient(this.config);
|
|
437
|
-
if (client) {
|
|
438
|
-
await client.shutdownAsync();
|
|
439
|
-
langfuseClient = null;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
/**
|
|
444
|
-
* Create a Langfuse sink from environment variables.
|
|
445
|
-
*/
|
|
446
|
-
function createLangfuseSinkFromEnv(options) {
|
|
447
|
-
const publicKey = process.env.LANGFUSE_PUBLIC_KEY;
|
|
448
|
-
const secretKey = process.env.LANGFUSE_SECRET_KEY;
|
|
449
|
-
if (!publicKey || !secretKey) return null;
|
|
450
|
-
return new LangfuseSink({
|
|
451
|
-
publicKey,
|
|
452
|
-
secretKey,
|
|
453
|
-
baseUrl: process.env.LANGFUSE_HOST || "https://cloud.langfuse.com",
|
|
454
|
-
debug: process.env.LANGFUSE_DEBUG === "true",
|
|
455
|
-
...options?.redactTrace ? { redactTrace: options.redactTrace } : {}
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
//#endregion
|
|
459
|
-
//#region src/otel-provider.ts
|
|
460
|
-
let provider = null;
|
|
461
|
-
/** Parse `OTEL_EXPORTER_OTLP_HEADERS` ("k=v,k2=v2") into a header map. */
|
|
462
|
-
function parseHeaders(raw) {
|
|
463
|
-
const out = {};
|
|
464
|
-
if (!raw) return out;
|
|
465
|
-
for (const pair of raw.split(",")) {
|
|
466
|
-
const eq = pair.indexOf("=");
|
|
467
|
-
if (eq <= 0) continue;
|
|
468
|
-
const key = pair.slice(0, eq).trim();
|
|
469
|
-
const value = pair.slice(eq + 1).trim();
|
|
470
|
-
if (key) out[key] = value;
|
|
471
|
-
}
|
|
472
|
-
return out;
|
|
473
|
-
}
|
|
474
|
-
async function initOtelProvider(serviceName) {
|
|
475
|
-
if (provider) return { active: true };
|
|
476
|
-
if (process.env.TELEMETRY_ENABLED !== "true") return {
|
|
477
|
-
active: false,
|
|
478
|
-
reason: "TELEMETRY_ENABLED!=true"
|
|
479
|
-
};
|
|
480
|
-
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
481
|
-
if (!endpoint) return {
|
|
482
|
-
active: false,
|
|
483
|
-
reason: "OTEL_EXPORTER_OTLP_ENDPOINT unset"
|
|
484
|
-
};
|
|
485
|
-
try {
|
|
486
|
-
const [{ NodeTracerProvider, BatchSpanProcessor }, { OTLPTraceExporter }, { resourceFromAttributes }, { ATTR_SERVICE_NAME }] = await Promise.all([
|
|
487
|
-
import("@opentelemetry/sdk-trace-node"),
|
|
488
|
-
import("@opentelemetry/exporter-trace-otlp-http"),
|
|
489
|
-
import("@opentelemetry/resources"),
|
|
490
|
-
import("@opentelemetry/semantic-conventions")
|
|
491
|
-
]);
|
|
492
|
-
const exporter = new OTLPTraceExporter({
|
|
493
|
-
url: endpoint,
|
|
494
|
-
headers: parseHeaders(process.env.OTEL_EXPORTER_OTLP_HEADERS)
|
|
495
|
-
});
|
|
496
|
-
const tracerProvider = new NodeTracerProvider({
|
|
497
|
-
resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: serviceName }),
|
|
498
|
-
spanProcessors: [new BatchSpanProcessor(exporter)]
|
|
499
|
-
});
|
|
500
|
-
tracerProvider.register();
|
|
501
|
-
provider = tracerProvider;
|
|
502
|
-
return { active: true };
|
|
503
|
-
} catch (error) {
|
|
504
|
-
return {
|
|
505
|
-
active: false,
|
|
506
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
async function shutdownOtelProvider() {
|
|
511
|
-
if (!provider) return;
|
|
512
|
-
const current = provider;
|
|
513
|
-
provider = null;
|
|
514
|
-
try {
|
|
515
|
-
await current.forceFlush();
|
|
516
|
-
} catch {}
|
|
517
|
-
await current.shutdown();
|
|
518
|
-
}
|
|
519
|
-
//#endregion
|
|
520
549
|
//#region src/otel-sink.ts
|
|
521
550
|
/**
|
|
522
551
|
* OpenTelemetry Sink
|
|
@@ -525,8 +554,8 @@ async function shutdownOtelProvider() {
|
|
|
525
554
|
* Langfuse, Jaeger, Honeycomb, and other OTLP collectors.
|
|
526
555
|
*
|
|
527
556
|
* IMPORTANT: This sink uses the GLOBAL tracer from @opentelemetry/api.
|
|
528
|
-
* The OTEL SDK must be initialized separately (e.g., via
|
|
529
|
-
*
|
|
557
|
+
* The OTEL SDK must be initialized separately (e.g., via initOtelProvider)
|
|
558
|
+
* before using this sink.
|
|
530
559
|
*
|
|
531
560
|
* This sink converts ExecutionTrace data into OTEL spans using the
|
|
532
561
|
* global tracer, which ensures spans flow through whatever processor
|
|
@@ -545,17 +574,8 @@ function buildTraceSpanAttributes(trace, flattenedMetadata) {
|
|
|
545
574
|
"agent.trace.id": trace.id,
|
|
546
575
|
"agent.task": trace.task,
|
|
547
576
|
"agent.outcome": trace.outcome,
|
|
548
|
-
"
|
|
549
|
-
|
|
550
|
-
...trace.scope.projectRef && { "langfuse.project": trace.scope.projectRef },
|
|
551
|
-
...trace.scope.sessionId && {
|
|
552
|
-
"langfuse.session.id": trace.scope.sessionId,
|
|
553
|
-
"session.id": trace.scope.sessionId
|
|
554
|
-
},
|
|
555
|
-
...trace.scope.userId && {
|
|
556
|
-
"langfuse.user.id": trace.scope.userId,
|
|
557
|
-
"user.id": trace.scope.userId
|
|
558
|
-
},
|
|
577
|
+
...trace.scope.sessionId && { "session.id": trace.scope.sessionId },
|
|
578
|
+
...trace.scope.userId && { "user.id": trace.scope.userId },
|
|
559
579
|
...flattenedMetadata ?? {}
|
|
560
580
|
};
|
|
561
581
|
}
|
|
@@ -626,7 +646,6 @@ var OtelTelemetrySink = class {
|
|
|
626
646
|
if (!event?.type) continue;
|
|
627
647
|
api.context.with(parentContext, () => {
|
|
628
648
|
const projected = projectTraceEvent(event, "otel", this.config.includePayloads);
|
|
629
|
-
if (!projected) return;
|
|
630
649
|
const span = tracer.startSpan(projected.name, {
|
|
631
650
|
attributes: projected.attributes,
|
|
632
651
|
startTime: projected.startTime
|
|
@@ -673,14 +692,11 @@ function createOtelSink(config) {
|
|
|
673
692
|
/**
|
|
674
693
|
* Create an OTEL sink from environment variables.
|
|
675
694
|
*
|
|
676
|
-
* Returns a sink that uses the global tracer (
|
|
677
|
-
* separately via NodeSDK with LangfuseSpanProcessor or OTLPTraceExporter).
|
|
695
|
+
* Returns a sink that uses the global tracer (initialized separately).
|
|
678
696
|
*/
|
|
679
697
|
function createOtelSinkFromEnv() {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
const hasOtlp = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
683
|
-
if (!enabled || !hasLangfuse && !hasOtlp) return null;
|
|
698
|
+
if (process.env.TELEMETRY_ENABLED !== "true") return null;
|
|
699
|
+
if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) return null;
|
|
684
700
|
return new OtelTelemetrySink({
|
|
685
701
|
serviceName: process.env.OTEL_SERVICE_NAME || "orchestrator",
|
|
686
702
|
includePayloads: process.env.OTEL_INCLUDE_PAYLOADS === "true"
|
|
@@ -889,6 +905,41 @@ function normalizeRawToolInvocation(event, timestamp) {
|
|
|
889
905
|
timestamp
|
|
890
906
|
};
|
|
891
907
|
}
|
|
908
|
+
/** Codex exec item statuses as the ToolState they report. */
|
|
909
|
+
const CODEX_ITEM_STATUS = new Map([
|
|
910
|
+
["in_progress", "running"],
|
|
911
|
+
["completed", "completed"],
|
|
912
|
+
["failed", "failed"]
|
|
913
|
+
]);
|
|
914
|
+
/**
|
|
915
|
+
* A Codex exec `file_change` item, which a Sandbox forwards in a `raw`
|
|
916
|
+
* envelope, as the tool part it is: the files the agent's patch changed.
|
|
917
|
+
* Other Codex items already arrive as parts.
|
|
918
|
+
*/
|
|
919
|
+
function codexFileChange(event, timestamp) {
|
|
920
|
+
const raw = event.event;
|
|
921
|
+
const item = raw?.item;
|
|
922
|
+
if (!String(raw?.type).startsWith("item.") || item?.type !== "file_change" || typeof item.id !== "string" || !Array.isArray(item.changes)) return null;
|
|
923
|
+
const status = CODEX_ITEM_STATUS.get(item.status) ?? "running";
|
|
924
|
+
const state = {
|
|
925
|
+
status,
|
|
926
|
+
input: { changes: item.changes },
|
|
927
|
+
...status === "completed" ? { output: null } : {}
|
|
928
|
+
};
|
|
929
|
+
return {
|
|
930
|
+
type: "message.part.updated",
|
|
931
|
+
part: {
|
|
932
|
+
id: item.id,
|
|
933
|
+
sessionID: "",
|
|
934
|
+
messageID: "",
|
|
935
|
+
type: "tool",
|
|
936
|
+
callID: item.id,
|
|
937
|
+
tool: "file_change",
|
|
938
|
+
state
|
|
939
|
+
},
|
|
940
|
+
timestamp
|
|
941
|
+
};
|
|
942
|
+
}
|
|
892
943
|
/**
|
|
893
944
|
* Convert SSE event data to a typed TraceEvent.
|
|
894
945
|
* Returns null for events that should be skipped (heartbeats, status-only, etc.)
|
|
@@ -901,11 +952,13 @@ function normalizeRawToolInvocation(event, timestamp) {
|
|
|
901
952
|
* `ToolPart`) so consumers don't need to reach into provider-specific
|
|
902
953
|
* raw shapes to render tool calls. Unrecognized raw events fall
|
|
903
954
|
* through to a `custom` trace event.
|
|
955
|
+
*
|
|
956
|
+
* `timestamp` is when the event happened: now for a live stream, the recorded
|
|
957
|
+
* time for a recorded one, so the trace keeps the run's own clock.
|
|
904
958
|
*/
|
|
905
|
-
function sseToTraceEvent(event) {
|
|
906
|
-
const timestamp = /* @__PURE__ */ new Date();
|
|
959
|
+
function sseToTraceEvent(event, timestamp = /* @__PURE__ */ new Date()) {
|
|
907
960
|
if (event.type === "raw" || typeof event.type === "string" && TOOL_INVOCATION_TYPES.has(event.type)) {
|
|
908
|
-
const normalized = normalizeRawToolInvocation(event, timestamp);
|
|
961
|
+
const normalized = (event.type === "raw" ? codexFileChange(event, timestamp) : null) ?? normalizeRawToolInvocation(event, timestamp);
|
|
909
962
|
if (normalized) {
|
|
910
963
|
if (normalized.part.state?.status === "pending") return null;
|
|
911
964
|
return normalized;
|
|
@@ -973,13 +1026,13 @@ function sseToTraceEvent(event) {
|
|
|
973
1026
|
}
|
|
974
1027
|
}
|
|
975
1028
|
/**
|
|
976
|
-
* Extract a signal from an SSE event if present
|
|
1029
|
+
* Extract a signal from an SSE event if present; `at` as in {@link sseToTraceEvent}'s `timestamp`.
|
|
977
1030
|
*/
|
|
978
|
-
function sseToSignal(event) {
|
|
1031
|
+
function sseToSignal(event, at = /* @__PURE__ */ new Date()) {
|
|
979
1032
|
if (event.type === "status" && typeof event.status === "string") {
|
|
980
1033
|
if (VALID_SIGNALS.includes(event.status)) return {
|
|
981
1034
|
signal: event.status,
|
|
982
|
-
timestamp:
|
|
1035
|
+
timestamp: at
|
|
983
1036
|
};
|
|
984
1037
|
}
|
|
985
1038
|
return null;
|
|
@@ -1168,6 +1221,6 @@ function createTraceCollector(config) {
|
|
|
1168
1221
|
return new TraceCollector(config);
|
|
1169
1222
|
}
|
|
1170
1223
|
//#endregion
|
|
1171
|
-
export { GEN_AI_CONVERSATION_ID, GEN_AI_INPUT_TOKEN_KEYS, GEN_AI_MODEL_KEYS, GEN_AI_OUTPUT_TOKEN_KEYS, GEN_AI_REQUEST_MODEL, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, HttpTelemetrySink,
|
|
1224
|
+
export { GEN_AI_CONVERSATION_ID, GEN_AI_INPUT_TOKEN_KEYS, GEN_AI_MODEL_KEYS, GEN_AI_OUTPUT_TOKEN_KEYS, GEN_AI_REQUEST_MODEL, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, HttpTelemetrySink, MultiTelemetrySink, OtelTelemetrySink, PROVISION_TRACE_ID_HEADER, SSEChunkParser, SpanRecorder, TOKEN_USAGE_INPUT_KEYS, TOKEN_USAGE_OUTPUT_KEYS, TraceCollector, createHttpSink, createOtelSink, createOtelSinkFromEnv, createStreamUsageExtractor, createTraceCollector, executionTraceToContractSpans, firstTokenCount, flattenTraceMetadata, genAiUsageAttributes, initOtelProvider, isAdoptablePtid, isToolPart, mintPtid, parseMetadataBlob, projectTraceEvent, shutdownOtelProvider, sseToSignal, sseToTraceEvent, tokenCount };
|
|
1172
1225
|
|
|
1173
1226
|
//# sourceMappingURL=index.mjs.map
|