@tangle-network/sdk-telemetry 0.7.5 → 0.7.7

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/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
- ...otel && { endTime: "durationMs" in event && typeof event.durationMs === "number" ? new Date(event.timestamp.getTime() + event.durationMs) : event.timestamp }
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
- const status = state.status;
179
- projection.name = `tool.${part.tool}.${otel ? status : "result"}`;
180
- if (otel) {
181
- if (state.status === "error" || state.status === "failed") {
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 (isFilePart(part)) {
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
- if (otel) addPartUpdateAttributes(attrs, event, includePayloads);
519
+ addPartUpdateAttributes(attrs, event, includePayloads);
221
520
  break;
222
521
  }
223
522
  case "message.updated":
224
523
  projection.name = "message.updated";
225
- projection.metadata = {
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
- projection.metadata = event.data;
324
- if (event.name === "startup.phase" || !otel && event.name === "http.phase") {
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
- if (otel) addCustomEventAttributes(attrs, event);
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 NodeSDK with
529
- * LangfuseSpanProcessor or OTLPTraceExporter) before using this sink.
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
- "langfuse.trace.id": trace.id,
549
- "langfuse.trace.name": trace.task,
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 (which should be initialized
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
- const enabled = process.env.TELEMETRY_ENABLED === "true";
681
- const hasLangfuse = process.env.LANGFUSE_PUBLIC_KEY && process.env.LANGFUSE_SECRET_KEY;
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: /* @__PURE__ */ new Date()
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, LangfuseSink, MultiTelemetrySink, OtelTelemetrySink, PROVISION_TRACE_ID_HEADER, SSEChunkParser, SpanRecorder, TOKEN_USAGE_INPUT_KEYS, TOKEN_USAGE_OUTPUT_KEYS, TraceCollector, createHttpSink, createLangfuseSinkFromEnv, createOtelSink, createOtelSinkFromEnv, createStreamUsageExtractor, createTraceCollector, firstTokenCount, flattenTraceMetadata, genAiUsageAttributes, initOtelProvider, isAdoptablePtid, isToolPart, mintPtid, parseMetadataBlob, projectTraceEvent, shutdownOtelProvider, sseToSignal, sseToTraceEvent, tokenCount };
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