@tracebird/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -0
- package/dist/cli.js +1484 -0
- package/dist/cli.js.map +1 -0
- package/dist/ui/assets/index-BEvedEM9.css +1 -0
- package/dist/ui/assets/index-BU67d56w.js +44 -0
- package/dist/ui/index.html +13 -0
- package/package.json +51 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1484 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { parseArgs } from "util";
|
|
5
|
+
import { resolve as resolve2 } from "path";
|
|
6
|
+
|
|
7
|
+
// src/commands/live.ts
|
|
8
|
+
import { join as join5 } from "path";
|
|
9
|
+
import { buildRun } from "@tracebird/core";
|
|
10
|
+
|
|
11
|
+
// src/server.ts
|
|
12
|
+
import {
|
|
13
|
+
createServer as createHttpServer
|
|
14
|
+
} from "http";
|
|
15
|
+
import { parseOtlp } from "@tracebird/core";
|
|
16
|
+
|
|
17
|
+
// src/otlp/protobuf.ts
|
|
18
|
+
import protobuf from "protobufjs";
|
|
19
|
+
var common = {
|
|
20
|
+
AnyValue: {
|
|
21
|
+
oneofs: {
|
|
22
|
+
value: {
|
|
23
|
+
oneof: [
|
|
24
|
+
"stringValue",
|
|
25
|
+
"boolValue",
|
|
26
|
+
"intValue",
|
|
27
|
+
"doubleValue",
|
|
28
|
+
"arrayValue",
|
|
29
|
+
"kvlistValue",
|
|
30
|
+
"bytesValue"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
fields: {
|
|
35
|
+
stringValue: { type: "string", id: 1 },
|
|
36
|
+
boolValue: { type: "bool", id: 2 },
|
|
37
|
+
intValue: { type: "int64", id: 3 },
|
|
38
|
+
doubleValue: { type: "double", id: 4 },
|
|
39
|
+
arrayValue: { type: "ArrayValue", id: 5 },
|
|
40
|
+
kvlistValue: { type: "KeyValueList", id: 6 },
|
|
41
|
+
bytesValue: { type: "bytes", id: 7 }
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
ArrayValue: { fields: { values: { rule: "repeated", type: "AnyValue", id: 1 } } },
|
|
45
|
+
KeyValueList: { fields: { values: { rule: "repeated", type: "KeyValue", id: 1 } } },
|
|
46
|
+
KeyValue: {
|
|
47
|
+
fields: { key: { type: "string", id: 1 }, value: { type: "AnyValue", id: 2 } }
|
|
48
|
+
},
|
|
49
|
+
InstrumentationScope: {
|
|
50
|
+
fields: {
|
|
51
|
+
name: { type: "string", id: 1 },
|
|
52
|
+
version: { type: "string", id: 2 },
|
|
53
|
+
attributes: { rule: "repeated", type: "KeyValue", id: 3 }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var KV = "opentelemetry.proto.common.v1.KeyValue";
|
|
58
|
+
var descriptor = {
|
|
59
|
+
nested: {
|
|
60
|
+
opentelemetry: {
|
|
61
|
+
nested: {
|
|
62
|
+
proto: {
|
|
63
|
+
nested: {
|
|
64
|
+
common: { nested: { v1: { nested: common } } },
|
|
65
|
+
resource: {
|
|
66
|
+
nested: {
|
|
67
|
+
v1: {
|
|
68
|
+
nested: {
|
|
69
|
+
Resource: {
|
|
70
|
+
fields: { attributes: { rule: "repeated", type: KV, id: 1 } }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
trace: {
|
|
77
|
+
nested: {
|
|
78
|
+
v1: {
|
|
79
|
+
nested: {
|
|
80
|
+
ResourceSpans: {
|
|
81
|
+
fields: {
|
|
82
|
+
resource: { type: "opentelemetry.proto.resource.v1.Resource", id: 1 },
|
|
83
|
+
scopeSpans: { rule: "repeated", type: "ScopeSpans", id: 2 },
|
|
84
|
+
schemaUrl: { type: "string", id: 3 }
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
ScopeSpans: {
|
|
88
|
+
fields: {
|
|
89
|
+
scope: {
|
|
90
|
+
type: "opentelemetry.proto.common.v1.InstrumentationScope",
|
|
91
|
+
id: 1
|
|
92
|
+
},
|
|
93
|
+
spans: { rule: "repeated", type: "Span", id: 2 },
|
|
94
|
+
schemaUrl: { type: "string", id: 3 }
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
Span: {
|
|
98
|
+
fields: {
|
|
99
|
+
traceId: { type: "bytes", id: 1 },
|
|
100
|
+
spanId: { type: "bytes", id: 2 },
|
|
101
|
+
traceState: { type: "string", id: 3 },
|
|
102
|
+
parentSpanId: { type: "bytes", id: 4 },
|
|
103
|
+
name: { type: "string", id: 5 },
|
|
104
|
+
kind: { type: "int32", id: 6 },
|
|
105
|
+
startTimeUnixNano: { type: "fixed64", id: 7 },
|
|
106
|
+
endTimeUnixNano: { type: "fixed64", id: 8 },
|
|
107
|
+
attributes: { rule: "repeated", type: KV, id: 9 },
|
|
108
|
+
events: { rule: "repeated", type: "Event", id: 11 },
|
|
109
|
+
status: { type: "Status", id: 15 }
|
|
110
|
+
},
|
|
111
|
+
nested: {
|
|
112
|
+
Event: {
|
|
113
|
+
fields: {
|
|
114
|
+
timeUnixNano: { type: "fixed64", id: 1 },
|
|
115
|
+
name: { type: "string", id: 2 },
|
|
116
|
+
attributes: { rule: "repeated", type: KV, id: 3 }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
Status: {
|
|
122
|
+
fields: {
|
|
123
|
+
message: { type: "string", id: 2 },
|
|
124
|
+
code: { type: "int32", id: 3 }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
collector: {
|
|
132
|
+
nested: {
|
|
133
|
+
trace: {
|
|
134
|
+
nested: {
|
|
135
|
+
v1: {
|
|
136
|
+
nested: {
|
|
137
|
+
ExportTraceServiceRequest: {
|
|
138
|
+
fields: {
|
|
139
|
+
resourceSpans: {
|
|
140
|
+
rule: "repeated",
|
|
141
|
+
type: "opentelemetry.proto.trace.v1.ResourceSpans",
|
|
142
|
+
id: 1
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
var root = protobuf.Root.fromJSON(descriptor);
|
|
159
|
+
var RequestType = root.lookupType(
|
|
160
|
+
"opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest"
|
|
161
|
+
);
|
|
162
|
+
function bytesToHex(bytes) {
|
|
163
|
+
if (!bytes) return void 0;
|
|
164
|
+
const arr = Array.isArray(bytes) ? bytes : bytes instanceof Uint8Array ? Array.from(bytes) : [];
|
|
165
|
+
if (arr.length === 0) return void 0;
|
|
166
|
+
return arr.map((b) => (b & 255).toString(16).padStart(2, "0")).join("");
|
|
167
|
+
}
|
|
168
|
+
function decodeProtobufTraces(body) {
|
|
169
|
+
const message = RequestType.decode(body);
|
|
170
|
+
const obj = RequestType.toObject(message, {
|
|
171
|
+
longs: String,
|
|
172
|
+
enums: Number,
|
|
173
|
+
bytes: Array,
|
|
174
|
+
defaults: false,
|
|
175
|
+
arrays: true,
|
|
176
|
+
objects: true
|
|
177
|
+
});
|
|
178
|
+
for (const rs of obj.resourceSpans ?? []) {
|
|
179
|
+
for (const ss of rs.scopeSpans ?? []) {
|
|
180
|
+
for (const span2 of ss.spans ?? []) {
|
|
181
|
+
span2.traceId = bytesToHex(span2.traceId) ?? "";
|
|
182
|
+
span2.spanId = bytesToHex(span2.spanId) ?? "";
|
|
183
|
+
const parent = bytesToHex(span2.parentSpanId);
|
|
184
|
+
if (parent) span2.parentSpanId = parent;
|
|
185
|
+
else delete span2.parentSpanId;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return obj;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/server.ts
|
|
193
|
+
var MAX_BODY_BYTES = 50 * 1024 * 1024;
|
|
194
|
+
function readBody(req) {
|
|
195
|
+
return new Promise((resolve3, reject) => {
|
|
196
|
+
const chunks = [];
|
|
197
|
+
let size = 0;
|
|
198
|
+
req.on("data", (chunk) => {
|
|
199
|
+
size += chunk.length;
|
|
200
|
+
if (size > MAX_BODY_BYTES) {
|
|
201
|
+
reject(new Error("payload too large"));
|
|
202
|
+
req.destroy();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
chunks.push(chunk);
|
|
206
|
+
});
|
|
207
|
+
req.on("end", () => resolve3(Buffer.concat(chunks)));
|
|
208
|
+
req.on("error", reject);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
function decodeExport(contentType, body) {
|
|
212
|
+
if (contentType.includes("application/x-protobuf") || contentType.includes("application/protobuf")) {
|
|
213
|
+
return decodeProtobufTraces(body);
|
|
214
|
+
}
|
|
215
|
+
const text = body.toString("utf8").trim();
|
|
216
|
+
return text ? JSON.parse(text) : {};
|
|
217
|
+
}
|
|
218
|
+
function sendProtobuf(res, status) {
|
|
219
|
+
res.writeHead(status, { "content-type": "application/x-protobuf" });
|
|
220
|
+
res.end();
|
|
221
|
+
}
|
|
222
|
+
function sendJson(res, status, payload) {
|
|
223
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
224
|
+
res.end(JSON.stringify(payload));
|
|
225
|
+
}
|
|
226
|
+
async function handleTraces(req, res, onExport) {
|
|
227
|
+
const contentType = req.headers["content-type"] ?? "application/json";
|
|
228
|
+
let body;
|
|
229
|
+
try {
|
|
230
|
+
body = await readBody(req);
|
|
231
|
+
} catch {
|
|
232
|
+
sendJson(res, 413, { error: "payload too large" });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
let request;
|
|
236
|
+
try {
|
|
237
|
+
request = decodeExport(contentType, body);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
sendJson(res, 400, { error: `failed to decode OTLP payload: ${err.message}` });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const spans = parseOtlp(request);
|
|
243
|
+
try {
|
|
244
|
+
await onExport?.(spans, request);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
sendJson(res, 500, { error: `export handler failed: ${err.message}` });
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const isProtobuf = contentType.includes("application/x-protobuf") || contentType.includes("application/protobuf");
|
|
250
|
+
if (isProtobuf) sendProtobuf(res, 200);
|
|
251
|
+
else sendJson(res, 200, {});
|
|
252
|
+
}
|
|
253
|
+
function createServer(options = {}) {
|
|
254
|
+
return createHttpServer((req, res) => {
|
|
255
|
+
void (async () => {
|
|
256
|
+
const url = (req.url ?? "/").split("?")[0];
|
|
257
|
+
if (req.method === "POST" && url === "/v1/traces") {
|
|
258
|
+
await handleTraces(req, res, options.onExport);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (req.method === "GET" && (url === "/health" || url === "/healthz")) {
|
|
262
|
+
sendJson(res, 200, { status: "ok" });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (options.extraHandler) {
|
|
266
|
+
const handled = await options.extraHandler(req, res);
|
|
267
|
+
if (handled) return;
|
|
268
|
+
}
|
|
269
|
+
sendJson(res, 404, { error: "not found" });
|
|
270
|
+
})().catch((err) => {
|
|
271
|
+
if (!res.headersSent) sendJson(res, 500, { error: err.message });
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
function listen(server, port, host) {
|
|
276
|
+
return new Promise((resolve3, reject) => {
|
|
277
|
+
server.once("error", reject);
|
|
278
|
+
server.listen(port, host, () => {
|
|
279
|
+
const addr = server.address();
|
|
280
|
+
const boundPort = typeof addr === "object" && addr ? addr.port : port;
|
|
281
|
+
resolve3(boundPort);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/storage/session-store.ts
|
|
287
|
+
import { createWriteStream, mkdirSync, readFileSync } from "fs";
|
|
288
|
+
import { dirname } from "path";
|
|
289
|
+
import {
|
|
290
|
+
parseSession,
|
|
291
|
+
runMatches,
|
|
292
|
+
serializeRun,
|
|
293
|
+
StatusCode
|
|
294
|
+
} from "@tracebird/core";
|
|
295
|
+
function statusLabel(code) {
|
|
296
|
+
if (code === StatusCode.Error) return "error";
|
|
297
|
+
if (code === StatusCode.Ok) return "ok";
|
|
298
|
+
return "unset";
|
|
299
|
+
}
|
|
300
|
+
function countNodes(node) {
|
|
301
|
+
return 1 + node.children.reduce((sum, child) => sum + countNodes(child), 0);
|
|
302
|
+
}
|
|
303
|
+
var SessionStore = class _SessionStore {
|
|
304
|
+
runs = [];
|
|
305
|
+
byId = /* @__PURE__ */ new Map();
|
|
306
|
+
stream;
|
|
307
|
+
filePath;
|
|
308
|
+
constructor(filePath) {
|
|
309
|
+
if (filePath) {
|
|
310
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
311
|
+
this.filePath = filePath;
|
|
312
|
+
this.stream = createWriteStream(filePath, { flags: "a" });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/** Load an existing session file read-only (for `tracebird open`). */
|
|
316
|
+
static load(filePath) {
|
|
317
|
+
const store = new _SessionStore();
|
|
318
|
+
const text = readFileSync(filePath, "utf8");
|
|
319
|
+
for (const run of parseSession(text)) store.addRun(run, { persist: false });
|
|
320
|
+
return store;
|
|
321
|
+
}
|
|
322
|
+
addRun(run, options = {}) {
|
|
323
|
+
this.runs.push(run);
|
|
324
|
+
this.byId.set(run.id, run);
|
|
325
|
+
if (options.persist !== false) this.stream?.write(serializeRun(run) + "\n");
|
|
326
|
+
}
|
|
327
|
+
get(id) {
|
|
328
|
+
return this.byId.get(id);
|
|
329
|
+
}
|
|
330
|
+
all() {
|
|
331
|
+
return this.runs;
|
|
332
|
+
}
|
|
333
|
+
/** Run summaries, newest first, optionally filtered by text and status. */
|
|
334
|
+
list(filter = {}) {
|
|
335
|
+
const status = filter.status ?? "all";
|
|
336
|
+
const query = filter.query ?? "";
|
|
337
|
+
return this.runs.filter((run) => status === "all" || statusLabel(run.status.code) === status).filter((run) => runMatches(run, query)).map((run) => ({
|
|
338
|
+
id: run.id,
|
|
339
|
+
traceId: run.traceId,
|
|
340
|
+
summary: run.summary,
|
|
341
|
+
startTimeUnixNano: run.startTimeUnixNano,
|
|
342
|
+
durationMs: run.durationMs,
|
|
343
|
+
status: statusLabel(run.status.code),
|
|
344
|
+
tokens: run.tokens,
|
|
345
|
+
costUsd: run.costUsd,
|
|
346
|
+
...run.service ? { service: run.service } : {},
|
|
347
|
+
nodeCount: countNodes(run.root)
|
|
348
|
+
})).sort((a, b) => a.startTimeUnixNano < b.startTimeUnixNano ? 1 : -1);
|
|
349
|
+
}
|
|
350
|
+
get size() {
|
|
351
|
+
return this.runs.length;
|
|
352
|
+
}
|
|
353
|
+
async close() {
|
|
354
|
+
const stream = this.stream;
|
|
355
|
+
if (!stream) return;
|
|
356
|
+
await new Promise((resolve3, reject) => {
|
|
357
|
+
stream.end((err) => err ? reject(err) : resolve3());
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// src/trace-buffer.ts
|
|
363
|
+
var TraceBuffer = class {
|
|
364
|
+
idleMs;
|
|
365
|
+
onComplete;
|
|
366
|
+
traces = /* @__PURE__ */ new Map();
|
|
367
|
+
constructor(options) {
|
|
368
|
+
this.idleMs = options.idleMs ?? 1500;
|
|
369
|
+
this.onComplete = options.onComplete;
|
|
370
|
+
}
|
|
371
|
+
add(spans) {
|
|
372
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
373
|
+
for (const span2 of spans) {
|
|
374
|
+
const list = grouped.get(span2.traceId) ?? [];
|
|
375
|
+
list.push(span2);
|
|
376
|
+
grouped.set(span2.traceId, list);
|
|
377
|
+
}
|
|
378
|
+
for (const [traceId, group] of grouped) {
|
|
379
|
+
const entry = this.traces.get(traceId);
|
|
380
|
+
if (entry) {
|
|
381
|
+
entry.spans.push(...group);
|
|
382
|
+
clearTimeout(entry.timer);
|
|
383
|
+
entry.timer = this.schedule(traceId);
|
|
384
|
+
} else {
|
|
385
|
+
this.traces.set(traceId, { spans: group, timer: this.schedule(traceId) });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
schedule(traceId) {
|
|
390
|
+
const timer = setTimeout(() => this.flush(traceId), this.idleMs);
|
|
391
|
+
timer.unref?.();
|
|
392
|
+
return timer;
|
|
393
|
+
}
|
|
394
|
+
/** Flush a single trace now, if buffered. */
|
|
395
|
+
flush(traceId) {
|
|
396
|
+
const entry = this.traces.get(traceId);
|
|
397
|
+
if (!entry) return;
|
|
398
|
+
clearTimeout(entry.timer);
|
|
399
|
+
this.traces.delete(traceId);
|
|
400
|
+
this.onComplete(traceId, entry.spans);
|
|
401
|
+
}
|
|
402
|
+
/** Flush every buffered trace (e.g. on shutdown). */
|
|
403
|
+
flushAll() {
|
|
404
|
+
for (const traceId of [...this.traces.keys()]) this.flush(traceId);
|
|
405
|
+
}
|
|
406
|
+
get pending() {
|
|
407
|
+
return this.traces.size;
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// src/render/tree.ts
|
|
412
|
+
import { StatusCode as StatusCode2 } from "@tracebird/core";
|
|
413
|
+
|
|
414
|
+
// src/render/format.ts
|
|
415
|
+
function formatDuration(ms) {
|
|
416
|
+
if (!Number.isFinite(ms) || ms < 0) return "\u2014";
|
|
417
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
418
|
+
const seconds = ms / 1e3;
|
|
419
|
+
if (seconds < 60) return `${seconds.toFixed(2).replace(/\.?0+$/, "")}s`;
|
|
420
|
+
const mins = Math.floor(seconds / 60);
|
|
421
|
+
const rem = Math.round(seconds % 60);
|
|
422
|
+
return `${mins}m ${rem}s`;
|
|
423
|
+
}
|
|
424
|
+
function formatTokens(tokens) {
|
|
425
|
+
return tokens == null ? void 0 : `${tokens.toLocaleString("en-US")} tok`;
|
|
426
|
+
}
|
|
427
|
+
function formatCost(usd) {
|
|
428
|
+
if (usd == null) return void 0;
|
|
429
|
+
if (usd === 0) return "$0";
|
|
430
|
+
if (usd < 0.01) return `$${usd.toFixed(4)}`;
|
|
431
|
+
return `$${usd.toFixed(2)}`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/render/tree.ts
|
|
435
|
+
var ICON = {
|
|
436
|
+
run: "\u23FA",
|
|
437
|
+
agent: "\u25C6",
|
|
438
|
+
llm: "\u2726",
|
|
439
|
+
tool: "\u2699",
|
|
440
|
+
step: "\u25AB"
|
|
441
|
+
};
|
|
442
|
+
function metrics(node) {
|
|
443
|
+
const parts = [formatDuration(node.durationMs)];
|
|
444
|
+
if (node.kind === "llm") {
|
|
445
|
+
const llm = node;
|
|
446
|
+
const tok = formatTokens(llm.usage.total ?? llm.usage.input);
|
|
447
|
+
if (tok) parts.push(tok);
|
|
448
|
+
const cost = formatCost(llm.costUsd);
|
|
449
|
+
if (cost) parts.push(cost);
|
|
450
|
+
if (llm.model) parts.push(llm.model);
|
|
451
|
+
}
|
|
452
|
+
if (node.kind === "tool" && node.isError) parts.push("ERROR");
|
|
453
|
+
return parts.join(" ");
|
|
454
|
+
}
|
|
455
|
+
function label(node) {
|
|
456
|
+
return `${ICON[node.kind]} ${node.name} ${metrics(node)}`;
|
|
457
|
+
}
|
|
458
|
+
function renderChildren(nodes, prefix, lines) {
|
|
459
|
+
nodes.forEach((node, i) => {
|
|
460
|
+
const last = i === nodes.length - 1;
|
|
461
|
+
lines.push(`${prefix}${last ? "\u2514\u2500 " : "\u251C\u2500 "}${label(node)}`);
|
|
462
|
+
renderChildren(node.children, prefix + (last ? " " : "\u2502 "), lines);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
function renderRunTree(run) {
|
|
466
|
+
const header = [run.summary, formatDuration(run.durationMs)];
|
|
467
|
+
const tok = formatTokens(run.tokens.total);
|
|
468
|
+
if (tok) header.push(tok);
|
|
469
|
+
const cost = formatCost(run.costUsd);
|
|
470
|
+
if (cost) header.push(cost);
|
|
471
|
+
if (run.status.code === StatusCode2.Error) header.push("ERROR");
|
|
472
|
+
const lines = [header.join(" \xB7 ")];
|
|
473
|
+
renderChildren(run.root.children, "", lines);
|
|
474
|
+
return lines.join("\n");
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/api.ts
|
|
478
|
+
import { existsSync } from "fs";
|
|
479
|
+
import { join as join2 } from "path";
|
|
480
|
+
import { diffRuns } from "@tracebird/core";
|
|
481
|
+
|
|
482
|
+
// src/export.ts
|
|
483
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
484
|
+
import { join } from "path";
|
|
485
|
+
import { serializeSession } from "@tracebird/core";
|
|
486
|
+
function exportJsonl(runs) {
|
|
487
|
+
return serializeSession(runs);
|
|
488
|
+
}
|
|
489
|
+
function inlineAssets(uiDir, html) {
|
|
490
|
+
const withJs = html.replace(
|
|
491
|
+
/<script\b[^>]*\bsrc="(\.?\/?assets\/[^"]+\.js)"[^>]*><\/script>/g,
|
|
492
|
+
(_match, src) => {
|
|
493
|
+
const code = readFileSync2(join(uiDir, src.replace(/^\.?\//, "")), "utf8").replace(
|
|
494
|
+
/<\/(script)/gi,
|
|
495
|
+
"<\\/$1"
|
|
496
|
+
);
|
|
497
|
+
return `<script type="module">${code}</script>`;
|
|
498
|
+
}
|
|
499
|
+
);
|
|
500
|
+
return withJs.replace(
|
|
501
|
+
/<link\b[^>]*\bhref="(\.?\/?assets\/[^"]+\.css)"[^>]*>/g,
|
|
502
|
+
(_match, href) => {
|
|
503
|
+
const css = readFileSync2(join(uiDir, href.replace(/^\.?\//, "")), "utf8");
|
|
504
|
+
return `<style>${css}</style>`;
|
|
505
|
+
}
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
function buildHtmlSnapshot(uiDir, runs) {
|
|
509
|
+
const store = new SessionStore();
|
|
510
|
+
for (const run of runs) store.addRun(run, { persist: false });
|
|
511
|
+
const snapshot = {
|
|
512
|
+
session: { live: false, filePath: null, count: runs.length },
|
|
513
|
+
runs: store.list(),
|
|
514
|
+
runsById: Object.fromEntries(runs.map((r) => [r.id, r]))
|
|
515
|
+
};
|
|
516
|
+
const json2 = JSON.stringify(snapshot).replace(/<\//g, "<\\/");
|
|
517
|
+
const inject = `<script>window.__TRACEBIRD_SNAPSHOT__=${json2}</script>`;
|
|
518
|
+
const html = inlineAssets(uiDir, readFileSync2(join(uiDir, "index.html"), "utf8"));
|
|
519
|
+
if (html.includes('<script type="module">')) {
|
|
520
|
+
return html.replace('<script type="module">', `${inject}<script type="module">`);
|
|
521
|
+
}
|
|
522
|
+
return html.replace("</head>", `${inject}</head>`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/api.ts
|
|
526
|
+
function json(res, status, payload) {
|
|
527
|
+
res.writeHead(status, { "content-type": "application/json", "cache-control": "no-store" });
|
|
528
|
+
res.end(JSON.stringify(payload));
|
|
529
|
+
}
|
|
530
|
+
function handleApi(ctx, req, res) {
|
|
531
|
+
const url = (req.url ?? "/").split("?")[0];
|
|
532
|
+
if (!url.startsWith("/api/")) return false;
|
|
533
|
+
if (req.method !== "GET") {
|
|
534
|
+
json(res, 405, { error: "method not allowed" });
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
if (url === "/api/session") {
|
|
538
|
+
json(res, 200, {
|
|
539
|
+
live: ctx.live,
|
|
540
|
+
filePath: ctx.store.filePath ?? null,
|
|
541
|
+
count: ctx.store.size
|
|
542
|
+
});
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
if (url === "/api/runs") {
|
|
546
|
+
const params = new URL(req.url ?? "", "http://localhost").searchParams;
|
|
547
|
+
const query = params.get("q") ?? void 0;
|
|
548
|
+
const statusParam = params.get("status");
|
|
549
|
+
const status = statusParam === "ok" || statusParam === "error" || statusParam === "unset" ? statusParam : "all";
|
|
550
|
+
json(res, 200, ctx.store.list({ query, status }));
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
if (url === "/api/diff") {
|
|
554
|
+
const params = new URL(req.url ?? "", "http://localhost").searchParams;
|
|
555
|
+
const aId = params.get("a");
|
|
556
|
+
const bId = params.get("b");
|
|
557
|
+
if (!aId || !bId) {
|
|
558
|
+
json(res, 400, { error: "diff requires ?a=<runId>&b=<runId>" });
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
const a = ctx.store.get(aId);
|
|
562
|
+
const b = ctx.store.get(bId);
|
|
563
|
+
if (!a || !b) {
|
|
564
|
+
json(res, 404, { error: `run not found: ${!a ? aId : bId}` });
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
json(res, 200, diffRuns(a, b));
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
if (url === "/api/export") {
|
|
571
|
+
const params = new URL(req.url ?? "", "http://localhost").searchParams;
|
|
572
|
+
const id = params.get("id");
|
|
573
|
+
const format = params.get("format") === "jsonl" ? "jsonl" : "html";
|
|
574
|
+
let runs;
|
|
575
|
+
let name;
|
|
576
|
+
if (id) {
|
|
577
|
+
const run = ctx.store.get(id);
|
|
578
|
+
if (!run) {
|
|
579
|
+
json(res, 404, { error: `run not found: ${id}` });
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
runs = [run];
|
|
583
|
+
name = `tracebird-run-${run.traceId.slice(0, 8) || "export"}`;
|
|
584
|
+
} else {
|
|
585
|
+
runs = ctx.store.all();
|
|
586
|
+
name = "tracebird-session";
|
|
587
|
+
}
|
|
588
|
+
if (format === "jsonl") {
|
|
589
|
+
res.writeHead(200, {
|
|
590
|
+
"content-type": "application/x-ndjson; charset=utf-8",
|
|
591
|
+
"content-disposition": `attachment; filename="${name}.jsonl"`
|
|
592
|
+
});
|
|
593
|
+
res.end(exportJsonl(runs));
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
if (!ctx.uiDir || !existsSync(join2(ctx.uiDir, "index.html"))) {
|
|
597
|
+
json(res, 503, { error: "UI assets not available for HTML export" });
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
res.writeHead(200, {
|
|
601
|
+
"content-type": "text/html; charset=utf-8",
|
|
602
|
+
"content-disposition": `attachment; filename="${name}.html"`
|
|
603
|
+
});
|
|
604
|
+
res.end(buildHtmlSnapshot(ctx.uiDir, runs));
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
const match = /^\/api\/runs\/([^/]+)$/.exec(url);
|
|
608
|
+
if (match) {
|
|
609
|
+
const id = decodeURIComponent(match[1]);
|
|
610
|
+
const run = ctx.store.get(id);
|
|
611
|
+
if (!run) json(res, 404, { error: `run not found: ${id}` });
|
|
612
|
+
else json(res, 200, run);
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
json(res, 404, { error: "unknown api route" });
|
|
616
|
+
return true;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/static-files.ts
|
|
620
|
+
import { createReadStream, existsSync as existsSync2, statSync } from "fs";
|
|
621
|
+
import { join as join3, normalize, extname } from "path";
|
|
622
|
+
var MIME = {
|
|
623
|
+
".html": "text/html; charset=utf-8",
|
|
624
|
+
".js": "text/javascript; charset=utf-8",
|
|
625
|
+
".css": "text/css; charset=utf-8",
|
|
626
|
+
".json": "application/json; charset=utf-8",
|
|
627
|
+
".svg": "image/svg+xml",
|
|
628
|
+
".png": "image/png",
|
|
629
|
+
".jpg": "image/jpeg",
|
|
630
|
+
".ico": "image/x-icon",
|
|
631
|
+
".woff2": "font/woff2",
|
|
632
|
+
".map": "application/json; charset=utf-8"
|
|
633
|
+
};
|
|
634
|
+
function serveStatic(rootDir, req, res) {
|
|
635
|
+
if (!existsSync2(rootDir)) return false;
|
|
636
|
+
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
|
637
|
+
const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0]);
|
|
638
|
+
const rel = normalize(urlPath).replace(/^(\.\.[/\\])+/, "");
|
|
639
|
+
let filePath = join3(rootDir, rel);
|
|
640
|
+
if (!filePath.startsWith(rootDir)) filePath = join3(rootDir, "index.html");
|
|
641
|
+
if (!existsSync2(filePath) || statSync(filePath).isDirectory()) {
|
|
642
|
+
filePath = join3(rootDir, "index.html");
|
|
643
|
+
}
|
|
644
|
+
if (!existsSync2(filePath)) return false;
|
|
645
|
+
const type = MIME[extname(filePath)] ?? "application/octet-stream";
|
|
646
|
+
res.writeHead(200, { "content-type": type });
|
|
647
|
+
if (req.method === "HEAD") {
|
|
648
|
+
res.end();
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
createReadStream(filePath).pipe(res);
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/app.ts
|
|
656
|
+
function createAppHandler(ctx, uiDir, sse) {
|
|
657
|
+
const apiCtx = { ...ctx, uiDir };
|
|
658
|
+
return (req, res) => {
|
|
659
|
+
const url = (req.url ?? "/").split("?")[0];
|
|
660
|
+
if (sse && req.method === "GET" && url === "/api/stream") {
|
|
661
|
+
sse.handle(req, res);
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
if (handleApi(apiCtx, req, res)) return true;
|
|
665
|
+
if (serveStatic(uiDir, req, res)) return true;
|
|
666
|
+
return false;
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/ui-dir.ts
|
|
671
|
+
import { existsSync as existsSync3 } from "fs";
|
|
672
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
673
|
+
import { fileURLToPath } from "url";
|
|
674
|
+
function resolveUiDir() {
|
|
675
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
676
|
+
const candidates = [
|
|
677
|
+
join4(here, "ui"),
|
|
678
|
+
// dist/ui (published / built)
|
|
679
|
+
join4(here, "..", "..", "ui", "dist")
|
|
680
|
+
// packages/cli/dist → packages/ui/dist
|
|
681
|
+
];
|
|
682
|
+
return candidates.find((dir) => existsSync3(join4(dir, "index.html"))) ?? candidates[0];
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/open-browser.ts
|
|
686
|
+
import { spawn } from "child_process";
|
|
687
|
+
function openBrowser(url) {
|
|
688
|
+
const platform = process.platform;
|
|
689
|
+
const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
690
|
+
const args = platform === "win32" ? ["/c", "start", '""', url] : [url];
|
|
691
|
+
try {
|
|
692
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
693
|
+
child.on("error", () => void 0);
|
|
694
|
+
child.unref();
|
|
695
|
+
} catch {
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/sse.ts
|
|
700
|
+
var SseHub = class {
|
|
701
|
+
clients = /* @__PURE__ */ new Set();
|
|
702
|
+
heartbeat;
|
|
703
|
+
/** Register a long-lived SSE connection. */
|
|
704
|
+
handle(req, res) {
|
|
705
|
+
res.writeHead(200, {
|
|
706
|
+
"content-type": "text/event-stream",
|
|
707
|
+
"cache-control": "no-cache, no-transform",
|
|
708
|
+
connection: "keep-alive",
|
|
709
|
+
"x-accel-buffering": "no"
|
|
710
|
+
});
|
|
711
|
+
res.write("retry: 2000\n\n");
|
|
712
|
+
this.clients.add(res);
|
|
713
|
+
req.on("close", () => this.clients.delete(res));
|
|
714
|
+
if (!this.heartbeat) {
|
|
715
|
+
this.heartbeat = setInterval(() => this.write(":ping\n\n"), 15e3);
|
|
716
|
+
this.heartbeat.unref?.();
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
/** Push a named event with a JSON payload to every connected client. */
|
|
720
|
+
broadcast(event, data = {}) {
|
|
721
|
+
this.write(`event: ${event}
|
|
722
|
+
data: ${JSON.stringify(data)}
|
|
723
|
+
|
|
724
|
+
`);
|
|
725
|
+
}
|
|
726
|
+
write(text) {
|
|
727
|
+
for (const res of this.clients) {
|
|
728
|
+
try {
|
|
729
|
+
res.write(text);
|
|
730
|
+
} catch {
|
|
731
|
+
this.clients.delete(res);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
get size() {
|
|
736
|
+
return this.clients.size;
|
|
737
|
+
}
|
|
738
|
+
close() {
|
|
739
|
+
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
740
|
+
for (const res of this.clients) {
|
|
741
|
+
try {
|
|
742
|
+
res.end();
|
|
743
|
+
} catch {
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
this.clients.clear();
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
// src/commands/live.ts
|
|
751
|
+
async function runLive(options) {
|
|
752
|
+
const store = new SessionStore(join5(options.outDir, `session-${Date.now()}.jsonl`));
|
|
753
|
+
const sse = new SseHub();
|
|
754
|
+
const buffer = new TraceBuffer({
|
|
755
|
+
onComplete: (_traceId, spans) => {
|
|
756
|
+
const run = buildRun(spans);
|
|
757
|
+
store.addRun(run);
|
|
758
|
+
process.stdout.write("\n" + renderRunTree(run) + "\n");
|
|
759
|
+
sse.broadcast("run", { id: run.id });
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
const server = createServer({
|
|
763
|
+
onExport: (spans) => {
|
|
764
|
+
buffer.add(spans);
|
|
765
|
+
sse.broadcast("activity", { spans: spans.length });
|
|
766
|
+
},
|
|
767
|
+
extraHandler: createAppHandler({ store, live: true }, resolveUiDir(), sse)
|
|
768
|
+
});
|
|
769
|
+
const boundPort = await listen(server, options.port, options.host);
|
|
770
|
+
const endpoint = `http://${options.host}:${boundPort}`;
|
|
771
|
+
process.stdout.write(
|
|
772
|
+
[
|
|
773
|
+
"",
|
|
774
|
+
" tracebird \u2014 listening for OpenTelemetry traces",
|
|
775
|
+
"",
|
|
776
|
+
` UI ${endpoint}`,
|
|
777
|
+
` OTLP endpoint ${endpoint}/v1/traces`,
|
|
778
|
+
` Session file ${store.filePath}`,
|
|
779
|
+
"",
|
|
780
|
+
" Point your agent at this receiver:",
|
|
781
|
+
` export OTEL_EXPORTER_OTLP_ENDPOINT=${endpoint}`,
|
|
782
|
+
"",
|
|
783
|
+
" Waiting for your first agent run\u2026 (Ctrl-C to stop)",
|
|
784
|
+
""
|
|
785
|
+
].join("\n") + "\n"
|
|
786
|
+
);
|
|
787
|
+
if (options.open) openBrowser(endpoint);
|
|
788
|
+
await new Promise((resolveShutdown) => {
|
|
789
|
+
const shutdown = () => {
|
|
790
|
+
process.stdout.write("\n Stopping\u2026\n");
|
|
791
|
+
buffer.flushAll();
|
|
792
|
+
sse.close();
|
|
793
|
+
server.close(() => {
|
|
794
|
+
void store.close().then(() => resolveShutdown());
|
|
795
|
+
});
|
|
796
|
+
};
|
|
797
|
+
process.once("SIGINT", shutdown);
|
|
798
|
+
process.once("SIGTERM", shutdown);
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// src/commands/open.ts
|
|
803
|
+
import { existsSync as existsSync4 } from "fs";
|
|
804
|
+
import { resolve } from "path";
|
|
805
|
+
async function runOpen(options) {
|
|
806
|
+
const file = resolve(options.file);
|
|
807
|
+
if (!existsSync4(file)) {
|
|
808
|
+
process.stderr.write(`tracebird: session file not found: ${file}
|
|
809
|
+
`);
|
|
810
|
+
process.exitCode = 1;
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
let store;
|
|
814
|
+
try {
|
|
815
|
+
store = SessionStore.load(file);
|
|
816
|
+
} catch (err) {
|
|
817
|
+
process.stderr.write(`tracebird: failed to load session: ${err.message}
|
|
818
|
+
`);
|
|
819
|
+
process.exitCode = 1;
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const server = createServer({
|
|
823
|
+
extraHandler: createAppHandler({ store, live: false }, resolveUiDir())
|
|
824
|
+
});
|
|
825
|
+
const boundPort = await listen(server, options.port, options.host);
|
|
826
|
+
const endpoint = `http://${options.host}:${boundPort}`;
|
|
827
|
+
process.stdout.write(
|
|
828
|
+
[
|
|
829
|
+
"",
|
|
830
|
+
` tracebird \u2014 serving ${store.size} run(s) from`,
|
|
831
|
+
` ${file}`,
|
|
832
|
+
"",
|
|
833
|
+
` UI ${endpoint}`,
|
|
834
|
+
"",
|
|
835
|
+
" (Ctrl-C to stop)",
|
|
836
|
+
""
|
|
837
|
+
].join("\n") + "\n"
|
|
838
|
+
);
|
|
839
|
+
if (options.open) openBrowser(endpoint);
|
|
840
|
+
await new Promise((resolveShutdown) => {
|
|
841
|
+
const shutdown = () => {
|
|
842
|
+
process.stdout.write("\n Stopping\u2026\n");
|
|
843
|
+
server.close(() => resolveShutdown());
|
|
844
|
+
};
|
|
845
|
+
process.once("SIGINT", shutdown);
|
|
846
|
+
process.once("SIGTERM", shutdown);
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// src/commands/demo.ts
|
|
851
|
+
import { buildRuns, parseOtlp as parseOtlp2 } from "@tracebird/core";
|
|
852
|
+
|
|
853
|
+
// ../../libs/fixtures/src/otlp/builders.ts
|
|
854
|
+
function toAnyValue(v) {
|
|
855
|
+
if (typeof v === "string") return { stringValue: v };
|
|
856
|
+
if (typeof v === "boolean") return { boolValue: v };
|
|
857
|
+
return Number.isInteger(v) ? { intValue: String(v) } : { doubleValue: v };
|
|
858
|
+
}
|
|
859
|
+
function attrs(record) {
|
|
860
|
+
return Object.entries(record).map(([key, v]) => ({ key, value: toAnyValue(v) }));
|
|
861
|
+
}
|
|
862
|
+
function span(input) {
|
|
863
|
+
return {
|
|
864
|
+
traceId: input.traceId,
|
|
865
|
+
spanId: input.spanId,
|
|
866
|
+
...input.parentSpanId ? { parentSpanId: input.parentSpanId } : {},
|
|
867
|
+
name: input.name,
|
|
868
|
+
kind: input.kind ?? 1,
|
|
869
|
+
startTimeUnixNano: input.startTimeUnixNano,
|
|
870
|
+
endTimeUnixNano: input.endTimeUnixNano,
|
|
871
|
+
attributes: attrs(input.attributes ?? {}),
|
|
872
|
+
status: {
|
|
873
|
+
code: input.statusCode ?? 0,
|
|
874
|
+
...input.statusMessage ? { message: input.statusMessage } : {}
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
function traceRequest(input) {
|
|
879
|
+
return {
|
|
880
|
+
resourceSpans: [
|
|
881
|
+
{
|
|
882
|
+
resource: {
|
|
883
|
+
attributes: attrs({ "service.name": input.serviceName })
|
|
884
|
+
},
|
|
885
|
+
scopeSpans: [
|
|
886
|
+
{
|
|
887
|
+
scope: {
|
|
888
|
+
name: input.scopeName,
|
|
889
|
+
...input.scopeVersion ? { version: input.scopeVersion } : {}
|
|
890
|
+
},
|
|
891
|
+
spans: input.spans
|
|
892
|
+
}
|
|
893
|
+
]
|
|
894
|
+
}
|
|
895
|
+
]
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
function at(baseNano, offsetMs) {
|
|
899
|
+
return (baseNano + BigInt(offsetMs) * 1000000n).toString();
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// ../../libs/fixtures/src/otlp/weather-happy-path.ts
|
|
903
|
+
var BASE = 1733000000000000000n;
|
|
904
|
+
var TRACE = "0af7651916cd43dd8448eb211c80319c";
|
|
905
|
+
var ROOT = "1111111111111111";
|
|
906
|
+
var LLM1 = "2222222222222222";
|
|
907
|
+
var TOOL1 = "3333333333333333";
|
|
908
|
+
var TOOL2 = "4444444444444444";
|
|
909
|
+
var LLM2 = "5555555555555555";
|
|
910
|
+
var weatherHappyPath = traceRequest({
|
|
911
|
+
serviceName: "weather-assistant",
|
|
912
|
+
scopeName: "opentelemetry.instrumentation.openai",
|
|
913
|
+
scopeVersion: "0.30.0",
|
|
914
|
+
spans: [
|
|
915
|
+
span({
|
|
916
|
+
traceId: TRACE,
|
|
917
|
+
spanId: ROOT,
|
|
918
|
+
name: "invoke_agent weather-assistant",
|
|
919
|
+
startTimeUnixNano: at(BASE, 0),
|
|
920
|
+
endTimeUnixNano: at(BASE, 1300),
|
|
921
|
+
statusCode: 1,
|
|
922
|
+
attributes: {
|
|
923
|
+
"gen_ai.operation.name": "invoke_agent",
|
|
924
|
+
"gen_ai.agent.name": "weather-assistant",
|
|
925
|
+
"gen_ai.system": "openai"
|
|
926
|
+
}
|
|
927
|
+
}),
|
|
928
|
+
span({
|
|
929
|
+
traceId: TRACE,
|
|
930
|
+
spanId: LLM1,
|
|
931
|
+
parentSpanId: ROOT,
|
|
932
|
+
name: "chat gpt-4o",
|
|
933
|
+
startTimeUnixNano: at(BASE, 10),
|
|
934
|
+
endTimeUnixNano: at(BASE, 520),
|
|
935
|
+
statusCode: 1,
|
|
936
|
+
attributes: {
|
|
937
|
+
"gen_ai.operation.name": "chat",
|
|
938
|
+
"gen_ai.system": "openai",
|
|
939
|
+
"gen_ai.request.model": "gpt-4o",
|
|
940
|
+
"gen_ai.response.model": "gpt-4o-2024-08-06",
|
|
941
|
+
"gen_ai.request.temperature": 0.7,
|
|
942
|
+
"gen_ai.usage.input_tokens": 58,
|
|
943
|
+
"gen_ai.usage.output_tokens": 34,
|
|
944
|
+
"gen_ai.prompt.0.role": "system",
|
|
945
|
+
"gen_ai.prompt.0.content": "You are a helpful weather assistant. Use tools to look up live data before answering.",
|
|
946
|
+
"gen_ai.prompt.1.role": "user",
|
|
947
|
+
"gen_ai.prompt.1.content": "What should I wear in Paris today?",
|
|
948
|
+
"gen_ai.completion.0.role": "assistant",
|
|
949
|
+
"gen_ai.completion.0.content": "",
|
|
950
|
+
"gen_ai.completion.0.tool_calls.0.id": "call_weather",
|
|
951
|
+
"gen_ai.completion.0.tool_calls.0.name": "get_weather",
|
|
952
|
+
"gen_ai.completion.0.tool_calls.0.arguments": '{"location":"Paris"}',
|
|
953
|
+
"gen_ai.completion.0.tool_calls.1.id": "call_forecast",
|
|
954
|
+
"gen_ai.completion.0.tool_calls.1.name": "get_forecast",
|
|
955
|
+
"gen_ai.completion.0.tool_calls.1.arguments": '{"location":"Paris","hours":12}'
|
|
956
|
+
}
|
|
957
|
+
}),
|
|
958
|
+
span({
|
|
959
|
+
traceId: TRACE,
|
|
960
|
+
spanId: TOOL1,
|
|
961
|
+
parentSpanId: ROOT,
|
|
962
|
+
name: "execute_tool get_weather",
|
|
963
|
+
startTimeUnixNano: at(BASE, 530),
|
|
964
|
+
endTimeUnixNano: at(BASE, 548),
|
|
965
|
+
statusCode: 1,
|
|
966
|
+
attributes: {
|
|
967
|
+
"gen_ai.operation.name": "execute_tool",
|
|
968
|
+
"gen_ai.tool.name": "get_weather",
|
|
969
|
+
"gen_ai.tool.call.id": "call_weather",
|
|
970
|
+
"gen_ai.tool.call.arguments": '{"location":"Paris"}',
|
|
971
|
+
"gen_ai.tool.call.result": '{"tempC":18,"condition":"sunny","humidity":0.41}'
|
|
972
|
+
}
|
|
973
|
+
}),
|
|
974
|
+
span({
|
|
975
|
+
traceId: TRACE,
|
|
976
|
+
spanId: TOOL2,
|
|
977
|
+
parentSpanId: ROOT,
|
|
978
|
+
name: "execute_tool get_forecast",
|
|
979
|
+
startTimeUnixNano: at(BASE, 532),
|
|
980
|
+
endTimeUnixNano: at(BASE, 560),
|
|
981
|
+
statusCode: 1,
|
|
982
|
+
attributes: {
|
|
983
|
+
"gen_ai.operation.name": "execute_tool",
|
|
984
|
+
"gen_ai.tool.name": "get_forecast",
|
|
985
|
+
"gen_ai.tool.call.id": "call_forecast",
|
|
986
|
+
"gen_ai.tool.call.arguments": '{"location":"Paris","hours":12}',
|
|
987
|
+
"gen_ai.tool.call.result": '{"highC":21,"lowC":12,"rain":false}'
|
|
988
|
+
}
|
|
989
|
+
}),
|
|
990
|
+
span({
|
|
991
|
+
traceId: TRACE,
|
|
992
|
+
spanId: LLM2,
|
|
993
|
+
parentSpanId: ROOT,
|
|
994
|
+
name: "chat gpt-4o",
|
|
995
|
+
startTimeUnixNano: at(BASE, 580),
|
|
996
|
+
endTimeUnixNano: at(BASE, 1290),
|
|
997
|
+
statusCode: 1,
|
|
998
|
+
attributes: {
|
|
999
|
+
"gen_ai.operation.name": "chat",
|
|
1000
|
+
"gen_ai.system": "openai",
|
|
1001
|
+
"gen_ai.request.model": "gpt-4o",
|
|
1002
|
+
"gen_ai.response.model": "gpt-4o-2024-08-06",
|
|
1003
|
+
"gen_ai.request.temperature": 0.7,
|
|
1004
|
+
"gen_ai.usage.input_tokens": 140,
|
|
1005
|
+
"gen_ai.usage.output_tokens": 42,
|
|
1006
|
+
"gen_ai.prompt.0.role": "system",
|
|
1007
|
+
"gen_ai.prompt.0.content": "You are a helpful weather assistant. Use tools to look up live data before answering.",
|
|
1008
|
+
"gen_ai.prompt.1.role": "user",
|
|
1009
|
+
"gen_ai.prompt.1.content": "What should I wear in Paris today?",
|
|
1010
|
+
"gen_ai.prompt.2.role": "tool",
|
|
1011
|
+
"gen_ai.prompt.2.content": '{"tempC":18,"condition":"sunny","humidity":0.41}',
|
|
1012
|
+
"gen_ai.prompt.3.role": "tool",
|
|
1013
|
+
"gen_ai.prompt.3.content": '{"highC":21,"lowC":12,"rain":false}',
|
|
1014
|
+
"gen_ai.completion.0.role": "assistant",
|
|
1015
|
+
"gen_ai.completion.0.content": "It's 18\xB0C and sunny in Paris, with a high of 21\xB0C and no rain expected. A light jacket or long sleeves will be perfect \u2014 you won't need an umbrella."
|
|
1016
|
+
}
|
|
1017
|
+
})
|
|
1018
|
+
]
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
// ../../libs/fixtures/src/otlp/tool-error.ts
|
|
1022
|
+
var BASE2 = 1733000100000000000n;
|
|
1023
|
+
var TRACE2 = "b2c3d4e5f60718293a4b5c6d7e8f9012";
|
|
1024
|
+
var ROOT2 = "aa00000000000001";
|
|
1025
|
+
var LLM12 = "aa00000000000002";
|
|
1026
|
+
var TOOL12 = "aa00000000000003";
|
|
1027
|
+
var LLM22 = "aa00000000000004";
|
|
1028
|
+
var toolError = traceRequest({
|
|
1029
|
+
serviceName: "weather-assistant",
|
|
1030
|
+
scopeName: "opentelemetry.instrumentation.openai",
|
|
1031
|
+
scopeVersion: "0.30.0",
|
|
1032
|
+
spans: [
|
|
1033
|
+
span({
|
|
1034
|
+
traceId: TRACE2,
|
|
1035
|
+
spanId: ROOT2,
|
|
1036
|
+
name: "invoke_agent weather-assistant",
|
|
1037
|
+
startTimeUnixNano: at(BASE2, 0),
|
|
1038
|
+
endTimeUnixNano: at(BASE2, 900),
|
|
1039
|
+
statusCode: 1,
|
|
1040
|
+
attributes: {
|
|
1041
|
+
"gen_ai.operation.name": "invoke_agent",
|
|
1042
|
+
"gen_ai.agent.name": "weather-assistant",
|
|
1043
|
+
"gen_ai.system": "openai"
|
|
1044
|
+
}
|
|
1045
|
+
}),
|
|
1046
|
+
span({
|
|
1047
|
+
traceId: TRACE2,
|
|
1048
|
+
spanId: LLM12,
|
|
1049
|
+
parentSpanId: ROOT2,
|
|
1050
|
+
name: "chat gpt-4o",
|
|
1051
|
+
startTimeUnixNano: at(BASE2, 8),
|
|
1052
|
+
endTimeUnixNano: at(BASE2, 410),
|
|
1053
|
+
statusCode: 1,
|
|
1054
|
+
attributes: {
|
|
1055
|
+
"gen_ai.operation.name": "chat",
|
|
1056
|
+
"gen_ai.system": "openai",
|
|
1057
|
+
"gen_ai.request.model": "gpt-4o",
|
|
1058
|
+
"gen_ai.response.model": "gpt-4o-2024-08-06",
|
|
1059
|
+
"gen_ai.usage.input_tokens": 52,
|
|
1060
|
+
"gen_ai.usage.output_tokens": 16,
|
|
1061
|
+
"gen_ai.prompt.0.role": "user",
|
|
1062
|
+
"gen_ai.prompt.0.content": "What's the weather in Atlantis?",
|
|
1063
|
+
"gen_ai.completion.0.role": "assistant",
|
|
1064
|
+
"gen_ai.completion.0.content": "",
|
|
1065
|
+
"gen_ai.completion.0.tool_calls.0.id": "call_weather",
|
|
1066
|
+
"gen_ai.completion.0.tool_calls.0.name": "get_weather",
|
|
1067
|
+
"gen_ai.completion.0.tool_calls.0.arguments": '{"location":"Atlantis"}'
|
|
1068
|
+
}
|
|
1069
|
+
}),
|
|
1070
|
+
span({
|
|
1071
|
+
traceId: TRACE2,
|
|
1072
|
+
spanId: TOOL12,
|
|
1073
|
+
parentSpanId: ROOT2,
|
|
1074
|
+
name: "execute_tool get_weather",
|
|
1075
|
+
startTimeUnixNano: at(BASE2, 420),
|
|
1076
|
+
endTimeUnixNano: at(BASE2, 438),
|
|
1077
|
+
statusCode: 2,
|
|
1078
|
+
statusMessage: 'UnknownLocationError: no such location "Atlantis"',
|
|
1079
|
+
attributes: {
|
|
1080
|
+
"gen_ai.operation.name": "execute_tool",
|
|
1081
|
+
"gen_ai.tool.name": "get_weather",
|
|
1082
|
+
"gen_ai.tool.call.id": "call_weather",
|
|
1083
|
+
"gen_ai.tool.call.arguments": '{"location":"Atlantis"}',
|
|
1084
|
+
"error.type": "UnknownLocationError",
|
|
1085
|
+
"gen_ai.tool.call.result": '{"error":"no such location \\"Atlantis\\""}'
|
|
1086
|
+
}
|
|
1087
|
+
}),
|
|
1088
|
+
span({
|
|
1089
|
+
traceId: TRACE2,
|
|
1090
|
+
spanId: LLM22,
|
|
1091
|
+
parentSpanId: ROOT2,
|
|
1092
|
+
name: "chat gpt-4o",
|
|
1093
|
+
startTimeUnixNano: at(BASE2, 450),
|
|
1094
|
+
endTimeUnixNano: at(BASE2, 890),
|
|
1095
|
+
statusCode: 1,
|
|
1096
|
+
attributes: {
|
|
1097
|
+
"gen_ai.operation.name": "chat",
|
|
1098
|
+
"gen_ai.system": "openai",
|
|
1099
|
+
"gen_ai.request.model": "gpt-4o",
|
|
1100
|
+
"gen_ai.response.model": "gpt-4o-2024-08-06",
|
|
1101
|
+
"gen_ai.usage.input_tokens": 88,
|
|
1102
|
+
"gen_ai.usage.output_tokens": 28,
|
|
1103
|
+
"gen_ai.prompt.0.role": "user",
|
|
1104
|
+
"gen_ai.prompt.0.content": "What's the weather in Atlantis?",
|
|
1105
|
+
"gen_ai.prompt.1.role": "tool",
|
|
1106
|
+
"gen_ai.prompt.1.content": '{"error":"no such location \\"Atlantis\\""}',
|
|
1107
|
+
"gen_ai.completion.0.role": "assistant",
|
|
1108
|
+
"gen_ai.completion.0.content": `I couldn't find a place called "Atlantis" to look up the weather. Could you double-check the spelling or give me a nearby city?`
|
|
1109
|
+
}
|
|
1110
|
+
})
|
|
1111
|
+
]
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
// ../../libs/fixtures/src/otlp/diff-pair.ts
|
|
1115
|
+
var TICKET = "Customer reports the checkout page returns a 500 error intermittently on mobile Safari. They have tried twice. No charge was made.";
|
|
1116
|
+
var SYSTEM = "You are a support triage agent. Read the ticket and assign a priority from P1 (urgent) to P4 (low).";
|
|
1117
|
+
function triageRun(opts) {
|
|
1118
|
+
return traceRequest({
|
|
1119
|
+
serviceName: "support-triage",
|
|
1120
|
+
scopeName: "opentelemetry.instrumentation.openai",
|
|
1121
|
+
scopeVersion: "0.30.0",
|
|
1122
|
+
spans: [
|
|
1123
|
+
span({
|
|
1124
|
+
traceId: opts.trace,
|
|
1125
|
+
spanId: opts.rootId,
|
|
1126
|
+
name: "invoke_agent support-triage",
|
|
1127
|
+
startTimeUnixNano: at(opts.base, 0),
|
|
1128
|
+
endTimeUnixNano: at(opts.base, opts.endMs),
|
|
1129
|
+
statusCode: 1,
|
|
1130
|
+
attributes: {
|
|
1131
|
+
"gen_ai.operation.name": "invoke_agent",
|
|
1132
|
+
"gen_ai.agent.name": "support-triage",
|
|
1133
|
+
"gen_ai.system": "openai"
|
|
1134
|
+
}
|
|
1135
|
+
}),
|
|
1136
|
+
span({
|
|
1137
|
+
traceId: opts.trace,
|
|
1138
|
+
spanId: opts.llmId,
|
|
1139
|
+
parentSpanId: opts.rootId,
|
|
1140
|
+
name: `chat ${opts.model}`,
|
|
1141
|
+
startTimeUnixNano: at(opts.base, 6),
|
|
1142
|
+
endTimeUnixNano: at(opts.base, opts.endMs - 4),
|
|
1143
|
+
statusCode: 1,
|
|
1144
|
+
attributes: {
|
|
1145
|
+
"gen_ai.operation.name": "chat",
|
|
1146
|
+
"gen_ai.system": "openai",
|
|
1147
|
+
"gen_ai.request.model": opts.model,
|
|
1148
|
+
"gen_ai.response.model": opts.responseModel,
|
|
1149
|
+
"gen_ai.request.temperature": 0.2,
|
|
1150
|
+
"gen_ai.usage.input_tokens": 96,
|
|
1151
|
+
"gen_ai.usage.output_tokens": opts.outputTokens,
|
|
1152
|
+
"gen_ai.prompt.0.role": "system",
|
|
1153
|
+
"gen_ai.prompt.0.content": SYSTEM,
|
|
1154
|
+
"gen_ai.prompt.1.role": "user",
|
|
1155
|
+
"gen_ai.prompt.1.content": TICKET,
|
|
1156
|
+
"gen_ai.completion.0.role": "assistant",
|
|
1157
|
+
"gen_ai.completion.0.content": opts.completion
|
|
1158
|
+
}
|
|
1159
|
+
})
|
|
1160
|
+
]
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
var diffPairA = triageRun({
|
|
1164
|
+
base: 1733000200000000000n,
|
|
1165
|
+
trace: "c1000000000000000000000000000a01",
|
|
1166
|
+
rootId: "c100000000000a01",
|
|
1167
|
+
llmId: "c100000000000a02",
|
|
1168
|
+
model: "gpt-4o",
|
|
1169
|
+
responseModel: "gpt-4o-2024-08-06",
|
|
1170
|
+
outputTokens: 54,
|
|
1171
|
+
endMs: 640,
|
|
1172
|
+
completion: "Priority: P1. A 500 error at checkout blocks revenue and affects all mobile Safari users intermittently. Escalate to the payments team immediately."
|
|
1173
|
+
});
|
|
1174
|
+
var diffPairB = triageRun({
|
|
1175
|
+
base: 1733000300000000000n,
|
|
1176
|
+
trace: "c2000000000000000000000000000b01",
|
|
1177
|
+
rootId: "c200000000000b01",
|
|
1178
|
+
llmId: "c200000000000b02",
|
|
1179
|
+
model: "gpt-4o-mini",
|
|
1180
|
+
responseModel: "gpt-4o-mini-2024-07-18",
|
|
1181
|
+
outputTokens: 48,
|
|
1182
|
+
endMs: 410,
|
|
1183
|
+
completion: "Priority: P2. Checkout returns a 500 on mobile Safari intermittently, but no charge was made and a retry path exists. Route to the web team for investigation."
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
// ../../libs/fixtures/src/otlp/openinference.ts
|
|
1187
|
+
var BASE3 = 1733000400000000000n;
|
|
1188
|
+
var TRACE3 = "d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1";
|
|
1189
|
+
var ROOT3 = "d1000000000000a1";
|
|
1190
|
+
var LLM = "d1000000000000a2";
|
|
1191
|
+
var TOOL = "d1000000000000a3";
|
|
1192
|
+
var openinferenceAgent = traceRequest({
|
|
1193
|
+
serviceName: "docs-assistant",
|
|
1194
|
+
scopeName: "openinference.instrumentation.openai",
|
|
1195
|
+
scopeVersion: "0.1.0",
|
|
1196
|
+
spans: [
|
|
1197
|
+
span({
|
|
1198
|
+
traceId: TRACE3,
|
|
1199
|
+
spanId: ROOT3,
|
|
1200
|
+
name: "agent",
|
|
1201
|
+
startTimeUnixNano: at(BASE3, 0),
|
|
1202
|
+
endTimeUnixNano: at(BASE3, 900),
|
|
1203
|
+
statusCode: 1,
|
|
1204
|
+
attributes: { "openinference.span.kind": "AGENT" }
|
|
1205
|
+
}),
|
|
1206
|
+
span({
|
|
1207
|
+
traceId: TRACE3,
|
|
1208
|
+
spanId: LLM,
|
|
1209
|
+
parentSpanId: ROOT3,
|
|
1210
|
+
name: "ChatCompletion",
|
|
1211
|
+
startTimeUnixNano: at(BASE3, 10),
|
|
1212
|
+
endTimeUnixNano: at(BASE3, 460),
|
|
1213
|
+
statusCode: 1,
|
|
1214
|
+
attributes: {
|
|
1215
|
+
"openinference.span.kind": "LLM",
|
|
1216
|
+
"llm.model_name": "gpt-4o",
|
|
1217
|
+
"llm.provider": "openai",
|
|
1218
|
+
"llm.token_count.prompt": 42,
|
|
1219
|
+
"llm.token_count.completion": 18,
|
|
1220
|
+
"llm.input_messages.0.message.role": "system",
|
|
1221
|
+
"llm.input_messages.0.message.content": "You answer questions about the docs.",
|
|
1222
|
+
"llm.input_messages.1.message.role": "user",
|
|
1223
|
+
"llm.input_messages.1.message.content": "How do I enable telemetry?",
|
|
1224
|
+
"llm.output_messages.0.message.role": "assistant",
|
|
1225
|
+
"llm.output_messages.0.message.content": "",
|
|
1226
|
+
"llm.output_messages.0.message.tool_calls.0.tool_call.function.name": "search_docs",
|
|
1227
|
+
"llm.output_messages.0.message.tool_calls.0.tool_call.function.arguments": '{"query":"enable telemetry"}'
|
|
1228
|
+
}
|
|
1229
|
+
}),
|
|
1230
|
+
span({
|
|
1231
|
+
traceId: TRACE3,
|
|
1232
|
+
spanId: TOOL,
|
|
1233
|
+
parentSpanId: ROOT3,
|
|
1234
|
+
name: "search_docs",
|
|
1235
|
+
startTimeUnixNano: at(BASE3, 470),
|
|
1236
|
+
endTimeUnixNano: at(BASE3, 500),
|
|
1237
|
+
statusCode: 1,
|
|
1238
|
+
attributes: {
|
|
1239
|
+
"openinference.span.kind": "TOOL",
|
|
1240
|
+
"tool.name": "search_docs",
|
|
1241
|
+
"input.value": '{"query":"enable telemetry"}',
|
|
1242
|
+
"output.value": '{"hits":["Set OTEL_EXPORTER_OTLP_ENDPOINT=\u2026"]}'
|
|
1243
|
+
}
|
|
1244
|
+
})
|
|
1245
|
+
]
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
// ../../libs/fixtures/src/otlp/vercel-ai-sdk.ts
|
|
1249
|
+
var BASE4 = 1733000500000000000n;
|
|
1250
|
+
var TRACE4 = "e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1";
|
|
1251
|
+
var ROOT4 = "e1000000000000b1";
|
|
1252
|
+
var GEN = "e1000000000000b2";
|
|
1253
|
+
var TOOL3 = "e1000000000000b3";
|
|
1254
|
+
var vercelAiSdk = traceRequest({
|
|
1255
|
+
serviceName: "ai-sdk-app",
|
|
1256
|
+
scopeName: "ai",
|
|
1257
|
+
scopeVersion: "4.0.0",
|
|
1258
|
+
spans: [
|
|
1259
|
+
span({
|
|
1260
|
+
traceId: TRACE4,
|
|
1261
|
+
spanId: ROOT4,
|
|
1262
|
+
name: "ai.generateText",
|
|
1263
|
+
startTimeUnixNano: at(BASE4, 0),
|
|
1264
|
+
endTimeUnixNano: at(BASE4, 1100),
|
|
1265
|
+
statusCode: 1,
|
|
1266
|
+
attributes: {
|
|
1267
|
+
"ai.model.id": "gpt-4o",
|
|
1268
|
+
"ai.model.provider": "openai.chat",
|
|
1269
|
+
"ai.usage.promptTokens": 64,
|
|
1270
|
+
"ai.usage.completionTokens": 30
|
|
1271
|
+
}
|
|
1272
|
+
}),
|
|
1273
|
+
span({
|
|
1274
|
+
traceId: TRACE4,
|
|
1275
|
+
spanId: GEN,
|
|
1276
|
+
parentSpanId: ROOT4,
|
|
1277
|
+
name: "ai.generateText.doGenerate",
|
|
1278
|
+
startTimeUnixNano: at(BASE4, 20),
|
|
1279
|
+
endTimeUnixNano: at(BASE4, 540),
|
|
1280
|
+
statusCode: 1,
|
|
1281
|
+
attributes: {
|
|
1282
|
+
"ai.model.id": "gpt-4o",
|
|
1283
|
+
"ai.model.provider": "openai.chat",
|
|
1284
|
+
"ai.usage.promptTokens": 64,
|
|
1285
|
+
"ai.usage.completionTokens": 30,
|
|
1286
|
+
"ai.prompt.messages": '[{"role":"user","content":"What is the weather in San Francisco?"}]',
|
|
1287
|
+
"ai.response.text": "Let me check the current conditions in San Francisco."
|
|
1288
|
+
}
|
|
1289
|
+
}),
|
|
1290
|
+
span({
|
|
1291
|
+
traceId: TRACE4,
|
|
1292
|
+
spanId: TOOL3,
|
|
1293
|
+
parentSpanId: ROOT4,
|
|
1294
|
+
name: "ai.toolCall",
|
|
1295
|
+
startTimeUnixNano: at(BASE4, 560),
|
|
1296
|
+
endTimeUnixNano: at(BASE4, 600),
|
|
1297
|
+
statusCode: 1,
|
|
1298
|
+
attributes: {
|
|
1299
|
+
"ai.toolCall.name": "getWeather",
|
|
1300
|
+
"ai.toolCall.args": '{"city":"San Francisco"}',
|
|
1301
|
+
"ai.toolCall.result": '{"tempC":17,"condition":"foggy"}'
|
|
1302
|
+
}
|
|
1303
|
+
})
|
|
1304
|
+
]
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
// ../../libs/fixtures/src/otlp/claude-code.ts
|
|
1308
|
+
var BASE5 = 1733000600000000000n;
|
|
1309
|
+
var TRACE5 = "f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1";
|
|
1310
|
+
var ROOT5 = "f1000000000000c1";
|
|
1311
|
+
var LLM3 = "f1000000000000c2";
|
|
1312
|
+
var TOOL4 = "f1000000000000c3";
|
|
1313
|
+
var claudeCodeSession = traceRequest({
|
|
1314
|
+
serviceName: "claude-code",
|
|
1315
|
+
scopeName: "com.anthropic.claude_code",
|
|
1316
|
+
scopeVersion: "2.0.0",
|
|
1317
|
+
spans: [
|
|
1318
|
+
span({
|
|
1319
|
+
traceId: TRACE5,
|
|
1320
|
+
spanId: ROOT5,
|
|
1321
|
+
name: "claude_code.interaction",
|
|
1322
|
+
startTimeUnixNano: at(BASE5, 0),
|
|
1323
|
+
endTimeUnixNano: at(BASE5, 4200),
|
|
1324
|
+
statusCode: 1,
|
|
1325
|
+
attributes: {
|
|
1326
|
+
"user_prompt": "Add a --json flag to the parser CLI",
|
|
1327
|
+
"interaction.sequence": 1
|
|
1328
|
+
}
|
|
1329
|
+
}),
|
|
1330
|
+
span({
|
|
1331
|
+
traceId: TRACE5,
|
|
1332
|
+
spanId: LLM3,
|
|
1333
|
+
parentSpanId: ROOT5,
|
|
1334
|
+
name: "claude_code.llm_request",
|
|
1335
|
+
startTimeUnixNano: at(BASE5, 30),
|
|
1336
|
+
endTimeUnixNano: at(BASE5, 2600),
|
|
1337
|
+
statusCode: 1,
|
|
1338
|
+
attributes: {
|
|
1339
|
+
"gen_ai.system": "anthropic",
|
|
1340
|
+
"gen_ai.request.model": "claude-sonnet-4",
|
|
1341
|
+
"input_tokens": 1840,
|
|
1342
|
+
"output_tokens": 420,
|
|
1343
|
+
"cache_read_tokens": 12e3,
|
|
1344
|
+
"gen_ai.response.finish_reasons": "tool_use",
|
|
1345
|
+
"user_prompt": "Add a --json flag to the parser CLI"
|
|
1346
|
+
}
|
|
1347
|
+
}),
|
|
1348
|
+
span({
|
|
1349
|
+
traceId: TRACE5,
|
|
1350
|
+
spanId: TOOL4,
|
|
1351
|
+
parentSpanId: ROOT5,
|
|
1352
|
+
name: "claude_code.tool",
|
|
1353
|
+
startTimeUnixNano: at(BASE5, 2650),
|
|
1354
|
+
endTimeUnixNano: at(BASE5, 2710),
|
|
1355
|
+
statusCode: 1,
|
|
1356
|
+
attributes: {
|
|
1357
|
+
"tool_name": "Edit",
|
|
1358
|
+
"file_path": "packages/cli/src/parser.ts",
|
|
1359
|
+
"tool_output": "Applied 1 edit to packages/cli/src/parser.ts"
|
|
1360
|
+
}
|
|
1361
|
+
})
|
|
1362
|
+
]
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
// ../../libs/fixtures/src/index.ts
|
|
1366
|
+
var otlpFixtures = {
|
|
1367
|
+
weatherHappyPath,
|
|
1368
|
+
toolError,
|
|
1369
|
+
diffPairA,
|
|
1370
|
+
diffPairB,
|
|
1371
|
+
openinferenceAgent,
|
|
1372
|
+
vercelAiSdk,
|
|
1373
|
+
claudeCodeSession
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
// src/commands/demo.ts
|
|
1377
|
+
async function runDemo(options) {
|
|
1378
|
+
const store = new SessionStore();
|
|
1379
|
+
for (const payload of Object.values(otlpFixtures)) {
|
|
1380
|
+
for (const run of buildRuns(parseOtlp2(payload))) store.addRun(run, { persist: false });
|
|
1381
|
+
}
|
|
1382
|
+
const server = createServer({
|
|
1383
|
+
extraHandler: createAppHandler({ store, live: false }, resolveUiDir())
|
|
1384
|
+
});
|
|
1385
|
+
const boundPort = await listen(server, options.port, options.host);
|
|
1386
|
+
const endpoint = `http://${options.host}:${boundPort}`;
|
|
1387
|
+
process.stdout.write(
|
|
1388
|
+
[
|
|
1389
|
+
"",
|
|
1390
|
+
` tracebird \u2014 demo mode, ${store.size} sample run(s) loaded`,
|
|
1391
|
+
"",
|
|
1392
|
+
` UI ${endpoint}`,
|
|
1393
|
+
"",
|
|
1394
|
+
" Try the Diff tab (the two support-triage runs differ on one decision),",
|
|
1395
|
+
" and drag the scrubber to time-travel through a run.",
|
|
1396
|
+
"",
|
|
1397
|
+
" (Ctrl-C to stop)",
|
|
1398
|
+
""
|
|
1399
|
+
].join("\n") + "\n"
|
|
1400
|
+
);
|
|
1401
|
+
if (options.open) openBrowser(endpoint);
|
|
1402
|
+
await new Promise((resolveShutdown) => {
|
|
1403
|
+
const shutdown = () => {
|
|
1404
|
+
process.stdout.write("\n Stopping\u2026\n");
|
|
1405
|
+
server.close(() => resolveShutdown());
|
|
1406
|
+
};
|
|
1407
|
+
process.once("SIGINT", shutdown);
|
|
1408
|
+
process.once("SIGTERM", shutdown);
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// src/cli.ts
|
|
1413
|
+
var HELP = `tracebird \u2014 a local-first, time-travel debugger for AI agent runs.
|
|
1414
|
+
|
|
1415
|
+
Usage:
|
|
1416
|
+
tracebird [live] Start the OTLP receiver + UI (default).
|
|
1417
|
+
tracebird demo Serve the UI with bundled sample runs (no agent needed).
|
|
1418
|
+
tracebird open <file.jsonl> Load a saved session and serve the UI.
|
|
1419
|
+
tracebird --help Show this help.
|
|
1420
|
+
|
|
1421
|
+
Options:
|
|
1422
|
+
--port <n> Port for the OTLP receiver / UI server (default 4318).
|
|
1423
|
+
--host <addr> Address to bind (default 127.0.0.1).
|
|
1424
|
+
--out <dir> Directory for captured sessions (default ./.tracebird).
|
|
1425
|
+
--no-open Do not open the browser on start.
|
|
1426
|
+
|
|
1427
|
+
Point your agent's OpenTelemetry exporter at the receiver:
|
|
1428
|
+
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
|
1429
|
+
`;
|
|
1430
|
+
async function main(argv) {
|
|
1431
|
+
const noOpen = argv.includes("--no-open");
|
|
1432
|
+
const cleaned = argv.filter((a) => a !== "--no-open");
|
|
1433
|
+
const { values, positionals } = parseArgs({
|
|
1434
|
+
args: cleaned,
|
|
1435
|
+
allowPositionals: true,
|
|
1436
|
+
options: {
|
|
1437
|
+
port: { type: "string" },
|
|
1438
|
+
host: { type: "string" },
|
|
1439
|
+
out: { type: "string" },
|
|
1440
|
+
help: { type: "boolean", short: "h" }
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
if (values.help) {
|
|
1444
|
+
process.stdout.write(HELP);
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
const command = positionals[0] ?? "live";
|
|
1448
|
+
const port = values.port ? Number(values.port) : 4318;
|
|
1449
|
+
const host = values.host ?? "127.0.0.1";
|
|
1450
|
+
const outDir = resolve2(values.out ?? ".tracebird");
|
|
1451
|
+
const open = !noOpen;
|
|
1452
|
+
switch (command) {
|
|
1453
|
+
case "live":
|
|
1454
|
+
await runLive({ port, host, outDir, open });
|
|
1455
|
+
break;
|
|
1456
|
+
case "demo":
|
|
1457
|
+
await runDemo({ port, host, open });
|
|
1458
|
+
break;
|
|
1459
|
+
case "open": {
|
|
1460
|
+
const file = positionals[1];
|
|
1461
|
+
if (!file) {
|
|
1462
|
+
process.stderr.write("Usage: tracebird open <file.jsonl>\n");
|
|
1463
|
+
process.exitCode = 1;
|
|
1464
|
+
break;
|
|
1465
|
+
}
|
|
1466
|
+
await runOpen({ file, port, host, open });
|
|
1467
|
+
break;
|
|
1468
|
+
}
|
|
1469
|
+
case "help":
|
|
1470
|
+
process.stdout.write(HELP);
|
|
1471
|
+
break;
|
|
1472
|
+
default:
|
|
1473
|
+
process.stderr.write(`Unknown command: ${command}
|
|
1474
|
+
|
|
1475
|
+
${HELP}`);
|
|
1476
|
+
process.exitCode = 1;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
1480
|
+
process.stderr.write(`tracebird: ${err.message}
|
|
1481
|
+
`);
|
|
1482
|
+
process.exitCode = 1;
|
|
1483
|
+
});
|
|
1484
|
+
//# sourceMappingURL=cli.js.map
|