@tangle-network/traces 0.1.0 → 0.2.1
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 +77 -29
- package/dist/chunk-YYVPAOOD.js +1935 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +129 -1640
- package/dist/index.d.ts +659 -0
- package/dist/index.js +126 -0
- package/package.json +13 -3
|
@@ -0,0 +1,1935 @@
|
|
|
1
|
+
// src/otlp.ts
|
|
2
|
+
function span(input) {
|
|
3
|
+
const attributes = {
|
|
4
|
+
"openinference.span.kind": input.kind
|
|
5
|
+
};
|
|
6
|
+
if (input.service != null) attributes["service.name"] = input.service;
|
|
7
|
+
if (input.agent != null) attributes["agent.name"] = input.agent;
|
|
8
|
+
if (input.model != null) attributes["llm.model_name"] = input.model;
|
|
9
|
+
if (input.tool != null) attributes["tool.name"] = input.tool;
|
|
10
|
+
if (input.inputTokens != null) attributes["llm.input_tokens"] = input.inputTokens;
|
|
11
|
+
if (input.outputTokens != null) attributes["llm.output_tokens"] = input.outputTokens;
|
|
12
|
+
if (input.step != null) attributes.step = input.step;
|
|
13
|
+
if (input.content != null && input.content.length > 0) attributes["content"] = input.content;
|
|
14
|
+
if (input.extra) Object.assign(attributes, input.extra);
|
|
15
|
+
const status = { code: input.status ?? "OK" };
|
|
16
|
+
if (input.statusMessage && input.statusMessage.length > 0) status.message = input.statusMessage;
|
|
17
|
+
return {
|
|
18
|
+
trace_id: input.traceId,
|
|
19
|
+
span_id: input.spanId,
|
|
20
|
+
parent_span_id: input.parentSpanId ?? null,
|
|
21
|
+
name: input.name,
|
|
22
|
+
start_time: input.startTime,
|
|
23
|
+
end_time: input.endTime ?? input.startTime,
|
|
24
|
+
status,
|
|
25
|
+
attributes
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function serializeSpans(spans) {
|
|
29
|
+
if (spans.length === 0) return "";
|
|
30
|
+
return `${spans.map((s) => JSON.stringify(s)).join("\n")}
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
async function writeOtlpFile(spans, outPath) {
|
|
34
|
+
const { mkdtemp, writeFile: writeFile2 } = await import("fs/promises");
|
|
35
|
+
const { tmpdir } = await import("os");
|
|
36
|
+
const { join: join13 } = await import("path");
|
|
37
|
+
const path = outPath ?? join13(await mkdtemp(join13(tmpdir(), "traces-")), "spans.otlp.jsonl");
|
|
38
|
+
await writeFile2(path, serializeSpans(spans), "utf8");
|
|
39
|
+
return path;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/analyze.ts
|
|
43
|
+
import { buildDefaultAnalystRegistry } from "@tangle-network/agent-eval/analyst";
|
|
44
|
+
import { OtlpFileTraceStore } from "@tangle-network/agent-eval/traces";
|
|
45
|
+
var DETERMINISTIC_VIEW_CEILING = 256 * 1024 * 1024;
|
|
46
|
+
async function analyzeSpans(spans, opts = {}) {
|
|
47
|
+
if (spans.length === 0) throw new Error("analyzeSpans: no spans to analyze");
|
|
48
|
+
const otlpPath = await writeOtlpFile(spans, opts.otlpOutPath);
|
|
49
|
+
const runId = opts.runId ?? `traces-${Date.now()}`;
|
|
50
|
+
const detStore = new OtlpFileTraceStore({ path: otlpPath, perCallByteCeiling: DETERMINISTIC_VIEW_CEILING });
|
|
51
|
+
await detStore.ensureIndexed();
|
|
52
|
+
const detRegistry = opts.registry ?? buildDefaultAnalystRegistry({ registry: { log: opts.log } });
|
|
53
|
+
const result = await detRegistry.run(runId, { traceStore: detStore });
|
|
54
|
+
if (opts.ai) {
|
|
55
|
+
const agStore = new OtlpFileTraceStore({ path: otlpPath });
|
|
56
|
+
await agStore.ensureIndexed();
|
|
57
|
+
const agRegistry = buildDefaultAnalystRegistry({
|
|
58
|
+
ai: opts.ai,
|
|
59
|
+
model: opts.model,
|
|
60
|
+
includeBehavioral: false,
|
|
61
|
+
registry: { log: opts.log }
|
|
62
|
+
});
|
|
63
|
+
const agResult = await agRegistry.run(runId, { traceStore: agStore }, {
|
|
64
|
+
budget: opts.budgetUsd != null ? { totalUsd: opts.budgetUsd } : void 0
|
|
65
|
+
});
|
|
66
|
+
result.findings.push(...agResult.findings);
|
|
67
|
+
result.per_analyst.push(...agResult.per_analyst);
|
|
68
|
+
result.total_cost_usd += agResult.total_cost_usd;
|
|
69
|
+
}
|
|
70
|
+
return { otlpPath, result };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/runtime-store.ts
|
|
74
|
+
import { InMemoryTraceStore } from "@tangle-network/agent-eval";
|
|
75
|
+
var KIND = {
|
|
76
|
+
AGENT: "agent",
|
|
77
|
+
LLM: "llm",
|
|
78
|
+
TOOL: "tool",
|
|
79
|
+
CHAIN: "custom",
|
|
80
|
+
SPAN: "custom"
|
|
81
|
+
};
|
|
82
|
+
function ms(iso2) {
|
|
83
|
+
const n = Date.parse(iso2);
|
|
84
|
+
return Number.isNaN(n) ? 0 : n;
|
|
85
|
+
}
|
|
86
|
+
async function toRuntimeStore(spans) {
|
|
87
|
+
const store = new InMemoryTraceStore();
|
|
88
|
+
const runIds = /* @__PURE__ */ new Set();
|
|
89
|
+
for (const s of spans) {
|
|
90
|
+
if (!runIds.has(s.trace_id)) {
|
|
91
|
+
runIds.add(s.trace_id);
|
|
92
|
+
await store.appendRun({ runId: s.trace_id, scenarioId: "session" });
|
|
93
|
+
}
|
|
94
|
+
const kind = KIND[s.attributes["openinference.span.kind"]] ?? "custom";
|
|
95
|
+
const base = {
|
|
96
|
+
spanId: s.span_id,
|
|
97
|
+
parentSpanId: s.parent_span_id ?? void 0,
|
|
98
|
+
runId: s.trace_id,
|
|
99
|
+
name: s.name,
|
|
100
|
+
startedAt: ms(s.start_time),
|
|
101
|
+
endedAt: ms(s.end_time),
|
|
102
|
+
status: s.status.code === "ERROR" ? "error" : "ok",
|
|
103
|
+
error: s.status.message,
|
|
104
|
+
attributes: s.attributes
|
|
105
|
+
};
|
|
106
|
+
const span2 = kind === "tool" ? {
|
|
107
|
+
...base,
|
|
108
|
+
kind: "tool",
|
|
109
|
+
toolName: String(s.attributes["tool.name"] ?? "tool"),
|
|
110
|
+
args: s.attributes.content ?? ""
|
|
111
|
+
} : { ...base, kind };
|
|
112
|
+
await store.appendSpan(span2);
|
|
113
|
+
}
|
|
114
|
+
return { store, runIds: [...runIds] };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/pipelines.ts
|
|
118
|
+
import { computeToolUseMetrics } from "@tangle-network/agent-eval";
|
|
119
|
+
import { stuckLoopView } from "@tangle-network/agent-eval/pipelines";
|
|
120
|
+
async function runPipelines(spans, opts = {}) {
|
|
121
|
+
const { store, runIds } = await toRuntimeStore(spans);
|
|
122
|
+
const stuckLoops = await stuckLoopView(store, { minOccurrences: opts.minLoopOccurrences ?? 3 });
|
|
123
|
+
const toolUse = await Promise.all(runIds.map((runId) => computeToolUseMetrics(store, runId)));
|
|
124
|
+
return { stuckLoops, toolUse };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/adapters/amp.ts
|
|
128
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
129
|
+
import { homedir } from "os";
|
|
130
|
+
import { basename, join } from "path";
|
|
131
|
+
var SERVICE = "amp";
|
|
132
|
+
var AmpAdapter = class {
|
|
133
|
+
harness = "amp";
|
|
134
|
+
threadsRoot() {
|
|
135
|
+
const base = process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
|
|
136
|
+
return join(base, "amp", "threads");
|
|
137
|
+
}
|
|
138
|
+
async locate(opts = {}) {
|
|
139
|
+
let files;
|
|
140
|
+
try {
|
|
141
|
+
files = await readdir(this.threadsRoot());
|
|
142
|
+
} catch {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
const refs = [];
|
|
146
|
+
for (const f of files) {
|
|
147
|
+
if (!f.startsWith("T-") || !f.endsWith(".json")) continue;
|
|
148
|
+
const path = join(this.threadsRoot(), f);
|
|
149
|
+
let st;
|
|
150
|
+
try {
|
|
151
|
+
st = await stat(path);
|
|
152
|
+
} catch {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
156
|
+
if (opts.cwd) continue;
|
|
157
|
+
refs.push({ harness: this.harness, sessionId: basename(f, ".json"), path, cwd: null, mtimeMs: st.mtimeMs });
|
|
158
|
+
}
|
|
159
|
+
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
160
|
+
}
|
|
161
|
+
async parse(ref) {
|
|
162
|
+
let thread;
|
|
163
|
+
try {
|
|
164
|
+
thread = JSON.parse(await readFile(ref.path, "utf8"));
|
|
165
|
+
} catch {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
const traceId = thread.id ?? ref.sessionId;
|
|
169
|
+
const rootId = `root:${traceId}`;
|
|
170
|
+
const start = new Date(thread.created ?? 0).toISOString();
|
|
171
|
+
const messages = thread.messages ?? [];
|
|
172
|
+
const spans = [
|
|
173
|
+
span({ traceId, spanId: rootId, parentSpanId: null, name: "session", kind: "AGENT", startTime: start, service: SERVICE, agent: SERVICE })
|
|
174
|
+
];
|
|
175
|
+
const toolByUseId = /* @__PURE__ */ new Map();
|
|
176
|
+
let step = 0;
|
|
177
|
+
for (const m of messages) {
|
|
178
|
+
const mid = m.messageId ?? step;
|
|
179
|
+
const llmId = `llm:${mid}`;
|
|
180
|
+
const u = m.usage;
|
|
181
|
+
const inputTokens2 = u ? (u.inputTokens ?? 0) + (u.cacheReadInputTokens ?? 0) + (u.cacheCreationInputTokens ?? 0) || null : null;
|
|
182
|
+
spans.push(
|
|
183
|
+
span({
|
|
184
|
+
traceId,
|
|
185
|
+
spanId: llmId,
|
|
186
|
+
parentSpanId: rootId,
|
|
187
|
+
name: `message.${m.role ?? "unknown"}`,
|
|
188
|
+
kind: "LLM",
|
|
189
|
+
startTime: start,
|
|
190
|
+
service: SERVICE,
|
|
191
|
+
agent: SERVICE,
|
|
192
|
+
model: u?.model ?? null,
|
|
193
|
+
inputTokens: inputTokens2,
|
|
194
|
+
outputTokens: u?.outputTokens ?? null,
|
|
195
|
+
step
|
|
196
|
+
})
|
|
197
|
+
);
|
|
198
|
+
step += 1;
|
|
199
|
+
for (const b of m.content ?? []) {
|
|
200
|
+
if (b.type === "tool_use" && b.name) {
|
|
201
|
+
const t = span({
|
|
202
|
+
traceId,
|
|
203
|
+
spanId: `tool:${b.id ?? `${mid}:${step}`}`,
|
|
204
|
+
parentSpanId: llmId,
|
|
205
|
+
name: `tool.${b.name}`,
|
|
206
|
+
kind: "TOOL",
|
|
207
|
+
startTime: start,
|
|
208
|
+
service: SERVICE,
|
|
209
|
+
agent: SERVICE,
|
|
210
|
+
tool: b.name,
|
|
211
|
+
step,
|
|
212
|
+
content: b.input != null ? JSON.stringify(b.input) : null
|
|
213
|
+
});
|
|
214
|
+
spans.push(t);
|
|
215
|
+
if (b.id) toolByUseId.set(b.id, t);
|
|
216
|
+
step += 1;
|
|
217
|
+
} else if (b.type === "tool_result" && b.tool_use_id) {
|
|
218
|
+
const t = toolByUseId.get(b.tool_use_id);
|
|
219
|
+
if (t) t.status = b.is_error === true ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return spans;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// src/adapters/claude.ts
|
|
228
|
+
import { readdir as readdir2, readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
229
|
+
import { homedir as homedir2 } from "os";
|
|
230
|
+
import { basename as basename2, join as join2 } from "path";
|
|
231
|
+
var SERVICE2 = "claude-code";
|
|
232
|
+
function parseLines(raw) {
|
|
233
|
+
const out = [];
|
|
234
|
+
for (const line of raw.split("\n")) {
|
|
235
|
+
if (!line) continue;
|
|
236
|
+
try {
|
|
237
|
+
out.push(JSON.parse(line));
|
|
238
|
+
} catch {
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
function inputTokens(u) {
|
|
244
|
+
if (!u) return null;
|
|
245
|
+
const v = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
|
|
246
|
+
return v > 0 ? v : null;
|
|
247
|
+
}
|
|
248
|
+
function asBlocks(content) {
|
|
249
|
+
return Array.isArray(content) ? content : [];
|
|
250
|
+
}
|
|
251
|
+
function stringifyToolResult(content) {
|
|
252
|
+
if (typeof content === "string") return content;
|
|
253
|
+
if (Array.isArray(content)) {
|
|
254
|
+
return content.map((c) => c && typeof c === "object" && "text" in c ? String(c.text ?? "") : "").join("");
|
|
255
|
+
}
|
|
256
|
+
return "";
|
|
257
|
+
}
|
|
258
|
+
function parseClaudeStream(events, ctx) {
|
|
259
|
+
const spans = [];
|
|
260
|
+
const toolSpanByUseId = /* @__PURE__ */ new Map();
|
|
261
|
+
let step = ctx.startStep;
|
|
262
|
+
for (const ev of events) {
|
|
263
|
+
const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
264
|
+
const uid = ev.uuid ?? `${ctx.idPrefix}step${step}`;
|
|
265
|
+
if (ev.type === "assistant" && ev.message) {
|
|
266
|
+
const llmId = `${ctx.idPrefix}${uid}`;
|
|
267
|
+
spans.push(
|
|
268
|
+
span({
|
|
269
|
+
traceId: ctx.traceId,
|
|
270
|
+
spanId: llmId,
|
|
271
|
+
parentSpanId: ctx.rootParent,
|
|
272
|
+
name: "llm.turn",
|
|
273
|
+
kind: "LLM",
|
|
274
|
+
startTime: ts,
|
|
275
|
+
service: SERVICE2,
|
|
276
|
+
agent: ctx.agent,
|
|
277
|
+
model: ev.message.model ?? null,
|
|
278
|
+
inputTokens: inputTokens(ev.message.usage),
|
|
279
|
+
outputTokens: ev.message.usage?.output_tokens ?? null,
|
|
280
|
+
step
|
|
281
|
+
})
|
|
282
|
+
);
|
|
283
|
+
step += 1;
|
|
284
|
+
let toolIdx = 0;
|
|
285
|
+
for (const block of asBlocks(ev.message.content)) {
|
|
286
|
+
if (block.type !== "tool_use" || !block.name) continue;
|
|
287
|
+
const toolSpan = span({
|
|
288
|
+
traceId: ctx.traceId,
|
|
289
|
+
spanId: `${ctx.idPrefix}${uid}:tool:${toolIdx}`,
|
|
290
|
+
parentSpanId: llmId,
|
|
291
|
+
name: `tool.${block.name}`,
|
|
292
|
+
kind: "TOOL",
|
|
293
|
+
startTime: ts,
|
|
294
|
+
service: SERVICE2,
|
|
295
|
+
agent: ctx.agent,
|
|
296
|
+
tool: block.name,
|
|
297
|
+
step,
|
|
298
|
+
content: block.input != null ? JSON.stringify(block.input) : null
|
|
299
|
+
});
|
|
300
|
+
spans.push(toolSpan);
|
|
301
|
+
if (block.id) toolSpanByUseId.set(block.id, toolSpan);
|
|
302
|
+
toolIdx += 1;
|
|
303
|
+
step += 1;
|
|
304
|
+
}
|
|
305
|
+
} else if (ev.type === "user" && ev.message) {
|
|
306
|
+
for (const block of asBlocks(ev.message.content)) {
|
|
307
|
+
if (block.type !== "tool_result" || !block.tool_use_id) continue;
|
|
308
|
+
backfillResult(toolSpanByUseId.get(block.tool_use_id), ts, block.is_error === true, stringifyToolResult(block.content));
|
|
309
|
+
}
|
|
310
|
+
} else if (ev.type === "attachment" && ev.attachment?.type === "tool_result" && ev.attachment.toolUseID) {
|
|
311
|
+
const err = typeof ev.attachment.exitCode === "number" && ev.attachment.exitCode !== 0;
|
|
312
|
+
backfillResult(toolSpanByUseId.get(ev.attachment.toolUseID), ts, err, ev.attachment.stderr ?? "");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return { spans, toolSpanByUseId, nextStep: step };
|
|
316
|
+
}
|
|
317
|
+
function backfillResult(s, endTime, isError, message) {
|
|
318
|
+
if (!s) return;
|
|
319
|
+
s.end_time = endTime;
|
|
320
|
+
const code = isError ? "ERROR" : "OK";
|
|
321
|
+
s.status = { code };
|
|
322
|
+
if (isError && message) s.status.message = message.slice(0, 500);
|
|
323
|
+
}
|
|
324
|
+
var ClaudeAdapter = class {
|
|
325
|
+
harness = "claude-code";
|
|
326
|
+
aliases = ["claude", "claudish", "openclaw", "nanoclaw"];
|
|
327
|
+
root() {
|
|
328
|
+
return join2(homedir2(), ".claude", "projects");
|
|
329
|
+
}
|
|
330
|
+
async locate(opts = {}) {
|
|
331
|
+
const root = this.root();
|
|
332
|
+
let projectDirs;
|
|
333
|
+
try {
|
|
334
|
+
projectDirs = await readdir2(root);
|
|
335
|
+
} catch {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
const refs = [];
|
|
339
|
+
for (const dir of projectDirs) {
|
|
340
|
+
const dirPath = join2(root, dir);
|
|
341
|
+
let files;
|
|
342
|
+
try {
|
|
343
|
+
files = await readdir2(dirPath);
|
|
344
|
+
} catch {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
for (const f of files) {
|
|
348
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
349
|
+
const path = join2(dirPath, f);
|
|
350
|
+
let st;
|
|
351
|
+
try {
|
|
352
|
+
st = await stat2(path);
|
|
353
|
+
} catch {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (!st.isFile()) continue;
|
|
357
|
+
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
358
|
+
const cwd = `/${dir.replace(/^-/, "").replace(/-/g, "/")}`;
|
|
359
|
+
if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
|
|
360
|
+
refs.push({ harness: this.harness, sessionId: basename2(f, ".jsonl"), path, cwd, mtimeMs: st.mtimeMs });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
364
|
+
}
|
|
365
|
+
async parse(ref) {
|
|
366
|
+
const raw = await readFile2(ref.path, "utf8");
|
|
367
|
+
const events = parseLines(raw);
|
|
368
|
+
const traceId = events.find((e) => e.sessionId)?.sessionId ?? ref.sessionId;
|
|
369
|
+
const rootId = `root:${traceId}`;
|
|
370
|
+
const spans = [
|
|
371
|
+
span({
|
|
372
|
+
traceId,
|
|
373
|
+
spanId: rootId,
|
|
374
|
+
parentSpanId: null,
|
|
375
|
+
name: "session",
|
|
376
|
+
kind: "AGENT",
|
|
377
|
+
startTime: events[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
378
|
+
endTime: events.at(-1)?.timestamp,
|
|
379
|
+
service: SERVICE2,
|
|
380
|
+
agent: SERVICE2
|
|
381
|
+
})
|
|
382
|
+
];
|
|
383
|
+
const main = parseClaudeStream(events, {
|
|
384
|
+
traceId,
|
|
385
|
+
agent: SERVICE2,
|
|
386
|
+
startStep: 0,
|
|
387
|
+
idPrefix: "",
|
|
388
|
+
rootParent: rootId
|
|
389
|
+
});
|
|
390
|
+
spans.push(...main.spans);
|
|
391
|
+
await this.foldSubagents(ref, traceId, main, spans);
|
|
392
|
+
return spans;
|
|
393
|
+
}
|
|
394
|
+
/** Parse `<session>/subagents/agent-*.jsonl`, parenting each under its Agent call. */
|
|
395
|
+
async foldSubagents(ref, traceId, main, out) {
|
|
396
|
+
const subDir = join2(ref.path.replace(/\.jsonl$/, ""), "subagents");
|
|
397
|
+
let files;
|
|
398
|
+
try {
|
|
399
|
+
files = await readdir2(subDir);
|
|
400
|
+
} catch {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
let step = main.nextStep;
|
|
404
|
+
for (const f of files) {
|
|
405
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
406
|
+
const hash = basename2(f, ".jsonl");
|
|
407
|
+
let meta = {};
|
|
408
|
+
try {
|
|
409
|
+
meta = JSON.parse(await readFile2(join2(subDir, `${hash}.meta.json`), "utf8"));
|
|
410
|
+
} catch {
|
|
411
|
+
}
|
|
412
|
+
const parent = meta.toolUseId && main.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
|
|
413
|
+
let raw;
|
|
414
|
+
try {
|
|
415
|
+
raw = await readFile2(join2(subDir, f), "utf8");
|
|
416
|
+
} catch {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const parsed = parseClaudeStream(parseLines(raw), {
|
|
420
|
+
traceId,
|
|
421
|
+
agent: meta.agentType ? `subagent:${meta.agentType}` : "subagent",
|
|
422
|
+
startStep: step,
|
|
423
|
+
idPrefix: `${hash}:`,
|
|
424
|
+
rootParent: parent
|
|
425
|
+
});
|
|
426
|
+
out.push(...parsed.spans);
|
|
427
|
+
step = parsed.nextStep;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// src/adapters/codex.ts
|
|
433
|
+
import { readdir as readdir3, readFile as readFile3, stat as stat3 } from "fs/promises";
|
|
434
|
+
import { homedir as homedir3 } from "os";
|
|
435
|
+
import { basename as basename3, join as join3 } from "path";
|
|
436
|
+
var SERVICE3 = "codex";
|
|
437
|
+
function parseLines2(raw) {
|
|
438
|
+
const out = [];
|
|
439
|
+
for (const line of raw.split("\n")) {
|
|
440
|
+
if (!line) continue;
|
|
441
|
+
try {
|
|
442
|
+
out.push(JSON.parse(line));
|
|
443
|
+
} catch {
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
function contentToString(content) {
|
|
449
|
+
if (typeof content === "string") return content;
|
|
450
|
+
if (Array.isArray(content)) {
|
|
451
|
+
return content.map((c) => c && typeof c === "object" && "text" in c ? String(c.text ?? "") : "").join("");
|
|
452
|
+
}
|
|
453
|
+
return "";
|
|
454
|
+
}
|
|
455
|
+
function outputIsError(output) {
|
|
456
|
+
const s = typeof output === "string" ? output : JSON.stringify(output ?? "");
|
|
457
|
+
const error = /"success"\s*:\s*false|exit(?:_| )code["\s:]+[1-9]|\bcommand failed\b|\bENOENT\b|\berror:/i.test(s);
|
|
458
|
+
return { error, message: error ? s.slice(0, 500) : "" };
|
|
459
|
+
}
|
|
460
|
+
async function* walkRollouts(root) {
|
|
461
|
+
let years;
|
|
462
|
+
try {
|
|
463
|
+
years = await readdir3(root);
|
|
464
|
+
} catch {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
for (const y of years) {
|
|
468
|
+
const yp = join3(root, y);
|
|
469
|
+
let months;
|
|
470
|
+
try {
|
|
471
|
+
months = await readdir3(yp);
|
|
472
|
+
} catch {
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
for (const m of months) {
|
|
476
|
+
const mp = join3(yp, m);
|
|
477
|
+
let days;
|
|
478
|
+
try {
|
|
479
|
+
days = await readdir3(mp);
|
|
480
|
+
} catch {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
for (const d of days) {
|
|
484
|
+
const dp = join3(mp, d);
|
|
485
|
+
let files;
|
|
486
|
+
try {
|
|
487
|
+
files = await readdir3(dp);
|
|
488
|
+
} catch {
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
for (const f of files) {
|
|
492
|
+
if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join3(dp, f);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
var CodexAdapter = class {
|
|
499
|
+
harness = "codex";
|
|
500
|
+
aliases = ["codex-acp"];
|
|
501
|
+
root() {
|
|
502
|
+
return process.env.CODEX_HOME ? join3(process.env.CODEX_HOME, "sessions") : join3(homedir3(), ".codex", "sessions");
|
|
503
|
+
}
|
|
504
|
+
async locate(opts = {}) {
|
|
505
|
+
const refs = [];
|
|
506
|
+
for await (const path of walkRollouts(this.root())) {
|
|
507
|
+
let st;
|
|
508
|
+
try {
|
|
509
|
+
st = await stat3(path);
|
|
510
|
+
} catch {
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
514
|
+
const head = (await readFile3(path, "utf8").catch(() => "")).split("\n", 1)[0] ?? "";
|
|
515
|
+
let cwd = null;
|
|
516
|
+
let id = basename3(path).replace(/^rollout-[\dT-]+-/, "").replace(/\.jsonl$/, "");
|
|
517
|
+
try {
|
|
518
|
+
const meta = JSON.parse(head);
|
|
519
|
+
if (meta.payload?.cwd) cwd = meta.payload.cwd;
|
|
520
|
+
if (meta.payload?.id) id = meta.payload.id;
|
|
521
|
+
} catch {
|
|
522
|
+
}
|
|
523
|
+
if (opts.cwd && (!cwd || !cwd.startsWith(opts.cwd))) continue;
|
|
524
|
+
refs.push({ harness: this.harness, sessionId: id, path, cwd, mtimeMs: st.mtimeMs });
|
|
525
|
+
}
|
|
526
|
+
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
527
|
+
}
|
|
528
|
+
async parse(ref) {
|
|
529
|
+
const lines = parseLines2(await readFile3(ref.path, "utf8"));
|
|
530
|
+
const meta = lines.find((l) => l.type === "session_meta");
|
|
531
|
+
const traceId = meta?.payload?.id ?? ref.sessionId;
|
|
532
|
+
const model = lines.find((l) => l.type === "turn_context")?.payload?.model ?? null;
|
|
533
|
+
const rootId = `root:${traceId}`;
|
|
534
|
+
const spans = [
|
|
535
|
+
span({
|
|
536
|
+
traceId,
|
|
537
|
+
spanId: rootId,
|
|
538
|
+
parentSpanId: null,
|
|
539
|
+
name: "session",
|
|
540
|
+
kind: "AGENT",
|
|
541
|
+
startTime: meta?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
542
|
+
endTime: lines.at(-1)?.timestamp,
|
|
543
|
+
service: SERVICE3,
|
|
544
|
+
agent: SERVICE3,
|
|
545
|
+
model
|
|
546
|
+
})
|
|
547
|
+
];
|
|
548
|
+
const toolByCallId = /* @__PURE__ */ new Map();
|
|
549
|
+
let step = 0;
|
|
550
|
+
let lastLlm = rootId;
|
|
551
|
+
for (const l of lines) {
|
|
552
|
+
const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
553
|
+
if (l.type === "event_msg" && l.payload?.type === "token_count") {
|
|
554
|
+
const u = l.payload.info?.last_token_usage;
|
|
555
|
+
if (u && (u.input_tokens || u.output_tokens)) {
|
|
556
|
+
const id = `llm:${step}`;
|
|
557
|
+
spans.push(
|
|
558
|
+
span({
|
|
559
|
+
traceId,
|
|
560
|
+
spanId: id,
|
|
561
|
+
parentSpanId: rootId,
|
|
562
|
+
name: "llm.turn",
|
|
563
|
+
kind: "LLM",
|
|
564
|
+
startTime: ts,
|
|
565
|
+
service: SERVICE3,
|
|
566
|
+
agent: SERVICE3,
|
|
567
|
+
model,
|
|
568
|
+
inputTokens: u.input_tokens ?? null,
|
|
569
|
+
outputTokens: u.output_tokens ?? null,
|
|
570
|
+
step
|
|
571
|
+
})
|
|
572
|
+
);
|
|
573
|
+
lastLlm = id;
|
|
574
|
+
step += 1;
|
|
575
|
+
}
|
|
576
|
+
} else if (l.type === "response_item" && l.payload?.type === "function_call") {
|
|
577
|
+
const name = l.payload.name ?? "tool";
|
|
578
|
+
const callId = l.payload.call_id ?? `${step}`;
|
|
579
|
+
const toolSpan = span({
|
|
580
|
+
traceId,
|
|
581
|
+
spanId: `tool:${callId}`,
|
|
582
|
+
parentSpanId: lastLlm,
|
|
583
|
+
name: `tool.${name}`,
|
|
584
|
+
kind: "TOOL",
|
|
585
|
+
startTime: ts,
|
|
586
|
+
service: SERVICE3,
|
|
587
|
+
agent: SERVICE3,
|
|
588
|
+
tool: name,
|
|
589
|
+
step,
|
|
590
|
+
content: l.payload.arguments ?? null
|
|
591
|
+
});
|
|
592
|
+
spans.push(toolSpan);
|
|
593
|
+
toolByCallId.set(callId, toolSpan);
|
|
594
|
+
step += 1;
|
|
595
|
+
} else if (l.type === "response_item" && l.payload?.type === "function_call_output") {
|
|
596
|
+
const t = toolByCallId.get(l.payload.call_id ?? "");
|
|
597
|
+
if (t) {
|
|
598
|
+
const { error, message } = outputIsError(l.payload.output);
|
|
599
|
+
t.end_time = ts;
|
|
600
|
+
t.status = error ? { code: "ERROR", message } : { code: "OK" };
|
|
601
|
+
}
|
|
602
|
+
} else if (l.type === "response_item" && l.payload?.type === "message") {
|
|
603
|
+
const text = contentToString(l.payload.content);
|
|
604
|
+
if (text) {
|
|
605
|
+
spans.push(
|
|
606
|
+
span({
|
|
607
|
+
traceId,
|
|
608
|
+
spanId: `msg:${step}`,
|
|
609
|
+
parentSpanId: rootId,
|
|
610
|
+
name: `message.${l.payload.role ?? "unknown"}`,
|
|
611
|
+
kind: "CHAIN",
|
|
612
|
+
startTime: ts,
|
|
613
|
+
service: SERVICE3,
|
|
614
|
+
agent: SERVICE3,
|
|
615
|
+
step,
|
|
616
|
+
content: text.slice(0, 8e3)
|
|
617
|
+
})
|
|
618
|
+
);
|
|
619
|
+
step += 1;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return spans;
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
// src/adapters/copilot.ts
|
|
628
|
+
import { readdir as readdir4, readFile as readFile4, stat as stat4 } from "fs/promises";
|
|
629
|
+
import { homedir as homedir4 } from "os";
|
|
630
|
+
import { join as join4 } from "path";
|
|
631
|
+
var SERVICE4 = "github-copilot";
|
|
632
|
+
function parseLines3(raw) {
|
|
633
|
+
const out = [];
|
|
634
|
+
for (const line of raw.split("\n")) {
|
|
635
|
+
if (!line.trim()) continue;
|
|
636
|
+
try {
|
|
637
|
+
out.push(JSON.parse(line));
|
|
638
|
+
} catch {
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return out;
|
|
642
|
+
}
|
|
643
|
+
var CopilotAdapter = class {
|
|
644
|
+
harness = "github-copilot";
|
|
645
|
+
aliases = ["copilot"];
|
|
646
|
+
root() {
|
|
647
|
+
return join4(process.env.COPILOT_HOME ?? join4(homedir4(), ".copilot"), "session-state");
|
|
648
|
+
}
|
|
649
|
+
async locate(opts = {}) {
|
|
650
|
+
let dirs;
|
|
651
|
+
try {
|
|
652
|
+
dirs = await readdir4(this.root());
|
|
653
|
+
} catch {
|
|
654
|
+
return [];
|
|
655
|
+
}
|
|
656
|
+
const refs = [];
|
|
657
|
+
for (const id of dirs) {
|
|
658
|
+
const path = join4(this.root(), id, "events.jsonl");
|
|
659
|
+
let st;
|
|
660
|
+
try {
|
|
661
|
+
st = await stat4(path);
|
|
662
|
+
} catch {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
if (!st.isFile()) continue;
|
|
666
|
+
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
667
|
+
if (opts.cwd) continue;
|
|
668
|
+
refs.push({ harness: this.harness, sessionId: id, path, cwd: null, mtimeMs: st.mtimeMs });
|
|
669
|
+
}
|
|
670
|
+
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
671
|
+
}
|
|
672
|
+
async parse(ref) {
|
|
673
|
+
const events = parseLines3(await readFile4(ref.path, "utf8"));
|
|
674
|
+
const traceId = ref.sessionId;
|
|
675
|
+
const rootId = `root:${traceId}`;
|
|
676
|
+
const spans = [
|
|
677
|
+
span({
|
|
678
|
+
traceId,
|
|
679
|
+
spanId: rootId,
|
|
680
|
+
parentSpanId: null,
|
|
681
|
+
name: "session",
|
|
682
|
+
kind: "AGENT",
|
|
683
|
+
startTime: events[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
684
|
+
endTime: events.at(-1)?.timestamp,
|
|
685
|
+
service: SERVICE4,
|
|
686
|
+
agent: SERVICE4
|
|
687
|
+
})
|
|
688
|
+
];
|
|
689
|
+
const toolByCallId = /* @__PURE__ */ new Map();
|
|
690
|
+
let step = 0;
|
|
691
|
+
let lastLlm = rootId;
|
|
692
|
+
let pendingInputTokens = null;
|
|
693
|
+
for (const ev of events) {
|
|
694
|
+
const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
695
|
+
const d = ev.data ?? {};
|
|
696
|
+
if (ev.type === "assistant.usage") {
|
|
697
|
+
pendingInputTokens = d.inputTokens ?? null;
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
if (ev.type === "assistant.message") {
|
|
701
|
+
const llmId = `llm:${d.messageId ?? ev.id ?? step}`;
|
|
702
|
+
spans.push(
|
|
703
|
+
span({
|
|
704
|
+
traceId,
|
|
705
|
+
spanId: llmId,
|
|
706
|
+
parentSpanId: rootId,
|
|
707
|
+
name: "message.assistant",
|
|
708
|
+
kind: "LLM",
|
|
709
|
+
startTime: ts,
|
|
710
|
+
service: SERVICE4,
|
|
711
|
+
agent: SERVICE4,
|
|
712
|
+
model: d.model ?? null,
|
|
713
|
+
inputTokens: pendingInputTokens,
|
|
714
|
+
outputTokens: d.outputTokens ?? null,
|
|
715
|
+
step,
|
|
716
|
+
content: typeof d.content === "string" ? d.content.slice(0, 8e3) : null
|
|
717
|
+
})
|
|
718
|
+
);
|
|
719
|
+
lastLlm = llmId;
|
|
720
|
+
pendingInputTokens = null;
|
|
721
|
+
step += 1;
|
|
722
|
+
} else if (ev.type === "tool.execution_start" && d.toolCallId) {
|
|
723
|
+
const name = d.toolName ?? "tool";
|
|
724
|
+
const t = span({
|
|
725
|
+
traceId,
|
|
726
|
+
spanId: `tool:${d.toolCallId}`,
|
|
727
|
+
parentSpanId: lastLlm,
|
|
728
|
+
name: `tool.${name}`,
|
|
729
|
+
kind: "TOOL",
|
|
730
|
+
startTime: ts,
|
|
731
|
+
service: SERVICE4,
|
|
732
|
+
agent: SERVICE4,
|
|
733
|
+
tool: name,
|
|
734
|
+
step,
|
|
735
|
+
content: d.arguments != null ? JSON.stringify(d.arguments) : null
|
|
736
|
+
});
|
|
737
|
+
spans.push(t);
|
|
738
|
+
toolByCallId.set(d.toolCallId, t);
|
|
739
|
+
step += 1;
|
|
740
|
+
} else if (ev.type === "tool.execution_complete" && d.toolCallId) {
|
|
741
|
+
const t = toolByCallId.get(d.toolCallId);
|
|
742
|
+
if (t) {
|
|
743
|
+
t.end_time = ts;
|
|
744
|
+
const err = d.success === false;
|
|
745
|
+
t.status = err ? { code: "ERROR", message: (d.error?.message ?? "").slice(0, 500) } : { code: "OK" };
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return spans;
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
// src/adapters/factory.ts
|
|
754
|
+
import { readdir as readdir5, readFile as readFile5, stat as stat5 } from "fs/promises";
|
|
755
|
+
import { homedir as homedir5 } from "os";
|
|
756
|
+
import { basename as basename4, join as join5 } from "path";
|
|
757
|
+
var SERVICE5 = "factory";
|
|
758
|
+
function parseLines4(raw) {
|
|
759
|
+
const out = [];
|
|
760
|
+
for (const line of raw.split("\n")) {
|
|
761
|
+
if (!line) continue;
|
|
762
|
+
try {
|
|
763
|
+
out.push(JSON.parse(line));
|
|
764
|
+
} catch {
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return out;
|
|
768
|
+
}
|
|
769
|
+
var FactoryAdapter = class {
|
|
770
|
+
harness = "factory";
|
|
771
|
+
aliases = ["factory-droids", "droid"];
|
|
772
|
+
root() {
|
|
773
|
+
return join5(homedir5(), ".factory", "sessions");
|
|
774
|
+
}
|
|
775
|
+
async locate(opts = {}) {
|
|
776
|
+
const root = this.root();
|
|
777
|
+
let dirs;
|
|
778
|
+
try {
|
|
779
|
+
dirs = await readdir5(root);
|
|
780
|
+
} catch {
|
|
781
|
+
return [];
|
|
782
|
+
}
|
|
783
|
+
const refs = [];
|
|
784
|
+
for (const dir of dirs) {
|
|
785
|
+
const dp = join5(root, dir);
|
|
786
|
+
let files;
|
|
787
|
+
try {
|
|
788
|
+
files = await readdir5(dp);
|
|
789
|
+
} catch {
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
const cwd = dir.replace(/-/g, "/");
|
|
793
|
+
if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
|
|
794
|
+
for (const f of files) {
|
|
795
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
796
|
+
const path = join5(dp, f);
|
|
797
|
+
let st;
|
|
798
|
+
try {
|
|
799
|
+
st = await stat5(path);
|
|
800
|
+
} catch {
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
804
|
+
refs.push({ harness: this.harness, sessionId: basename4(f, ".jsonl"), path, cwd, mtimeMs: st.mtimeMs });
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
808
|
+
}
|
|
809
|
+
async parse(ref) {
|
|
810
|
+
const lines = parseLines4(await readFile5(ref.path, "utf8"));
|
|
811
|
+
const start = lines.find((l) => l.type === "session_start");
|
|
812
|
+
const traceId = start?.id ?? ref.sessionId;
|
|
813
|
+
const rootId = `root:${traceId}`;
|
|
814
|
+
let settings = {};
|
|
815
|
+
try {
|
|
816
|
+
settings = JSON.parse(await readFile5(ref.path.replace(/\.jsonl$/, ".settings.json"), "utf8"));
|
|
817
|
+
} catch {
|
|
818
|
+
}
|
|
819
|
+
const spans = [
|
|
820
|
+
span({
|
|
821
|
+
traceId,
|
|
822
|
+
spanId: rootId,
|
|
823
|
+
parentSpanId: null,
|
|
824
|
+
name: "session",
|
|
825
|
+
kind: "AGENT",
|
|
826
|
+
startTime: start?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
827
|
+
endTime: lines.at(-1)?.timestamp,
|
|
828
|
+
service: SERVICE5,
|
|
829
|
+
agent: SERVICE5,
|
|
830
|
+
model: settings.model ?? null,
|
|
831
|
+
extra: settings.tokenUsage ? {
|
|
832
|
+
"session.input_tokens": settings.tokenUsage.inputTokens ?? 0,
|
|
833
|
+
"session.output_tokens": settings.tokenUsage.outputTokens ?? 0
|
|
834
|
+
} : void 0
|
|
835
|
+
})
|
|
836
|
+
];
|
|
837
|
+
const toolByUseId = /* @__PURE__ */ new Map();
|
|
838
|
+
let step = 0;
|
|
839
|
+
let lastLlm = rootId;
|
|
840
|
+
for (const l of lines) {
|
|
841
|
+
if (l.type !== "message" || !l.message) continue;
|
|
842
|
+
const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
843
|
+
const role = l.message.role;
|
|
844
|
+
const llmId = `llm:${l.id ?? step}`;
|
|
845
|
+
const text = (l.message.content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
|
|
846
|
+
spans.push(
|
|
847
|
+
span({
|
|
848
|
+
traceId,
|
|
849
|
+
spanId: llmId,
|
|
850
|
+
parentSpanId: rootId,
|
|
851
|
+
name: `message.${role ?? "unknown"}`,
|
|
852
|
+
kind: "LLM",
|
|
853
|
+
startTime: ts,
|
|
854
|
+
service: SERVICE5,
|
|
855
|
+
agent: SERVICE5,
|
|
856
|
+
model: settings.model ?? null,
|
|
857
|
+
step,
|
|
858
|
+
content: text ? text.slice(0, 8e3) : null
|
|
859
|
+
})
|
|
860
|
+
);
|
|
861
|
+
lastLlm = llmId;
|
|
862
|
+
step += 1;
|
|
863
|
+
for (const b of l.message.content ?? []) {
|
|
864
|
+
if (b.type === "tool_use" && b.name) {
|
|
865
|
+
const t = span({
|
|
866
|
+
traceId,
|
|
867
|
+
spanId: `tool:${b.id ?? `${l.id}:${step}`}`,
|
|
868
|
+
parentSpanId: lastLlm,
|
|
869
|
+
name: `tool.${b.name}`,
|
|
870
|
+
kind: "TOOL",
|
|
871
|
+
startTime: ts,
|
|
872
|
+
service: SERVICE5,
|
|
873
|
+
agent: SERVICE5,
|
|
874
|
+
tool: b.name,
|
|
875
|
+
step,
|
|
876
|
+
content: b.input != null ? JSON.stringify(b.input) : null
|
|
877
|
+
});
|
|
878
|
+
spans.push(t);
|
|
879
|
+
if (b.id) toolByUseId.set(b.id, t);
|
|
880
|
+
step += 1;
|
|
881
|
+
} else if (b.type === "tool_result" && b.tool_use_id) {
|
|
882
|
+
const t = toolByUseId.get(b.tool_use_id);
|
|
883
|
+
if (t) t.status = b.is_error === true ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return spans;
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
// src/adapters/forge.ts
|
|
892
|
+
import { readdir as readdir6, readFile as readFile6, stat as stat6 } from "fs/promises";
|
|
893
|
+
import { homedir as homedir6 } from "os";
|
|
894
|
+
import { basename as basename5, join as join6 } from "path";
|
|
895
|
+
var SERVICE6 = "forge";
|
|
896
|
+
function tokens(t) {
|
|
897
|
+
if (!t) return null;
|
|
898
|
+
return t.actual ?? t.approx ?? null;
|
|
899
|
+
}
|
|
900
|
+
var ForgeAdapter = class {
|
|
901
|
+
harness = "forge";
|
|
902
|
+
aliases = ["forgecode"];
|
|
903
|
+
/** Directories scanned for `*-dump.json` exports. */
|
|
904
|
+
dumpDirs() {
|
|
905
|
+
return [process.cwd(), join6(homedir6(), ".forge")];
|
|
906
|
+
}
|
|
907
|
+
async locate(opts = {}) {
|
|
908
|
+
const refs = [];
|
|
909
|
+
const dirs = opts.cwd ? [opts.cwd] : this.dumpDirs();
|
|
910
|
+
for (const dir of dirs) {
|
|
911
|
+
let files;
|
|
912
|
+
try {
|
|
913
|
+
files = await readdir6(dir);
|
|
914
|
+
} catch {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
for (const f of files) {
|
|
918
|
+
if (!f.endsWith("-dump.json")) continue;
|
|
919
|
+
const path = join6(dir, f);
|
|
920
|
+
let st;
|
|
921
|
+
try {
|
|
922
|
+
st = await stat6(path);
|
|
923
|
+
} catch {
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
927
|
+
refs.push({ harness: this.harness, sessionId: basename5(f, ".json"), path, cwd: dir, mtimeMs: st.mtimeMs });
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
931
|
+
}
|
|
932
|
+
async parse(ref) {
|
|
933
|
+
let dump;
|
|
934
|
+
try {
|
|
935
|
+
dump = JSON.parse(await readFile6(ref.path, "utf8"));
|
|
936
|
+
} catch {
|
|
937
|
+
return [];
|
|
938
|
+
}
|
|
939
|
+
const ctx = dump.context ?? dump;
|
|
940
|
+
const traceId = ctx.conversation_id ?? ref.sessionId;
|
|
941
|
+
const rootId = `root:${traceId}`;
|
|
942
|
+
const messages = ctx.messages ?? [];
|
|
943
|
+
const spans = [
|
|
944
|
+
span({ traceId, spanId: rootId, parentSpanId: null, name: "session", kind: "AGENT", startTime: (/* @__PURE__ */ new Date(0)).toISOString(), service: SERVICE6, agent: SERVICE6 })
|
|
945
|
+
];
|
|
946
|
+
const toolByCallId = /* @__PURE__ */ new Map();
|
|
947
|
+
let step = 0;
|
|
948
|
+
let lastLlm = rootId;
|
|
949
|
+
for (const raw of messages) {
|
|
950
|
+
const entry = raw.message ?? raw;
|
|
951
|
+
const ts = (/* @__PURE__ */ new Date(0)).toISOString();
|
|
952
|
+
if (entry.text) {
|
|
953
|
+
const llmId = `llm:${step}`;
|
|
954
|
+
spans.push(
|
|
955
|
+
span({
|
|
956
|
+
traceId,
|
|
957
|
+
spanId: llmId,
|
|
958
|
+
parentSpanId: rootId,
|
|
959
|
+
name: `message.${entry.text.role ?? "unknown"}`,
|
|
960
|
+
kind: "LLM",
|
|
961
|
+
startTime: ts,
|
|
962
|
+
service: SERVICE6,
|
|
963
|
+
agent: SERVICE6,
|
|
964
|
+
model: entry.text.model ?? null,
|
|
965
|
+
inputTokens: tokens(raw.usage?.prompt_tokens),
|
|
966
|
+
outputTokens: tokens(raw.usage?.completion_tokens),
|
|
967
|
+
step,
|
|
968
|
+
content: entry.text.content ? entry.text.content.slice(0, 8e3) : null
|
|
969
|
+
})
|
|
970
|
+
);
|
|
971
|
+
lastLlm = llmId;
|
|
972
|
+
step += 1;
|
|
973
|
+
for (const tc of entry.text.tool_calls ?? []) {
|
|
974
|
+
const id = tc.call_id ?? `${step}`;
|
|
975
|
+
const t = span({
|
|
976
|
+
traceId,
|
|
977
|
+
spanId: `tool:${id}`,
|
|
978
|
+
parentSpanId: llmId,
|
|
979
|
+
name: `tool.${tc.name ?? "tool"}`,
|
|
980
|
+
kind: "TOOL",
|
|
981
|
+
startTime: ts,
|
|
982
|
+
service: SERVICE6,
|
|
983
|
+
agent: SERVICE6,
|
|
984
|
+
tool: tc.name ?? "tool",
|
|
985
|
+
step,
|
|
986
|
+
content: tc.arguments != null ? JSON.stringify(tc.arguments) : null
|
|
987
|
+
});
|
|
988
|
+
spans.push(t);
|
|
989
|
+
toolByCallId.set(id, t);
|
|
990
|
+
step += 1;
|
|
991
|
+
}
|
|
992
|
+
} else if (entry.tool) {
|
|
993
|
+
const t = toolByCallId.get(entry.tool.call_id ?? "");
|
|
994
|
+
if (t) t.status = entry.tool.output?.is_error ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return spans;
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
// src/adapters/gemini.ts
|
|
1002
|
+
import { readdir as readdir7, readFile as readFile7, stat as stat7 } from "fs/promises";
|
|
1003
|
+
import { homedir as homedir7 } from "os";
|
|
1004
|
+
import { join as join7 } from "path";
|
|
1005
|
+
function iso(ts, fallback) {
|
|
1006
|
+
if (typeof ts === "number") return new Date(ts).toISOString();
|
|
1007
|
+
if (typeof ts === "string" && ts.length > 0) return ts;
|
|
1008
|
+
return fallback;
|
|
1009
|
+
}
|
|
1010
|
+
function toolStatusError(status) {
|
|
1011
|
+
return status != null && /error|fail|cancel/i.test(status);
|
|
1012
|
+
}
|
|
1013
|
+
var GeminiFamilyAdapter = class {
|
|
1014
|
+
harness;
|
|
1015
|
+
aliases;
|
|
1016
|
+
service;
|
|
1017
|
+
homeDirName;
|
|
1018
|
+
constructor(cfg) {
|
|
1019
|
+
this.harness = cfg.harness;
|
|
1020
|
+
this.aliases = cfg.aliases;
|
|
1021
|
+
this.service = cfg.service;
|
|
1022
|
+
this.homeDirName = cfg.homeDirName;
|
|
1023
|
+
}
|
|
1024
|
+
chatsRoot() {
|
|
1025
|
+
return join7(homedir7(), this.homeDirName, "tmp");
|
|
1026
|
+
}
|
|
1027
|
+
async locate(opts = {}) {
|
|
1028
|
+
const root = this.chatsRoot();
|
|
1029
|
+
let projectDirs;
|
|
1030
|
+
try {
|
|
1031
|
+
projectDirs = await readdir7(root);
|
|
1032
|
+
} catch {
|
|
1033
|
+
return [];
|
|
1034
|
+
}
|
|
1035
|
+
const refs = [];
|
|
1036
|
+
for (const pd of projectDirs) {
|
|
1037
|
+
const chatsDir = join7(root, pd, "chats");
|
|
1038
|
+
let files;
|
|
1039
|
+
try {
|
|
1040
|
+
files = await readdir7(chatsDir);
|
|
1041
|
+
} catch {
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
for (const f of files) {
|
|
1045
|
+
if (!f.startsWith("session-") || !f.endsWith(".json")) continue;
|
|
1046
|
+
const path = join7(chatsDir, f);
|
|
1047
|
+
let st;
|
|
1048
|
+
try {
|
|
1049
|
+
st = await stat7(path);
|
|
1050
|
+
} catch {
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
1054
|
+
if (opts.cwd) continue;
|
|
1055
|
+
refs.push({ harness: this.harness, sessionId: f.replace(/\.json$/, ""), path, cwd: null, mtimeMs: st.mtimeMs });
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1059
|
+
}
|
|
1060
|
+
async parse(ref) {
|
|
1061
|
+
let session;
|
|
1062
|
+
try {
|
|
1063
|
+
session = JSON.parse(await readFile7(ref.path, "utf8"));
|
|
1064
|
+
} catch {
|
|
1065
|
+
return [];
|
|
1066
|
+
}
|
|
1067
|
+
const traceId = session.sessionId ?? ref.sessionId;
|
|
1068
|
+
const messages = session.messages ?? [];
|
|
1069
|
+
const rootId = `root:${traceId}`;
|
|
1070
|
+
const start = iso(session.startTime ?? messages[0]?.timestamp, (/* @__PURE__ */ new Date(0)).toISOString());
|
|
1071
|
+
const spans = [
|
|
1072
|
+
span({
|
|
1073
|
+
traceId,
|
|
1074
|
+
spanId: rootId,
|
|
1075
|
+
parentSpanId: null,
|
|
1076
|
+
name: "session",
|
|
1077
|
+
kind: "AGENT",
|
|
1078
|
+
startTime: start,
|
|
1079
|
+
endTime: iso(messages.at(-1)?.timestamp, start),
|
|
1080
|
+
service: this.service,
|
|
1081
|
+
agent: this.service
|
|
1082
|
+
})
|
|
1083
|
+
];
|
|
1084
|
+
let step = 0;
|
|
1085
|
+
for (const m of messages) {
|
|
1086
|
+
const mid = m.id ?? `m${step}`;
|
|
1087
|
+
const ts = iso(m.timestamp, start);
|
|
1088
|
+
const llmId = `llm:${mid}`;
|
|
1089
|
+
const content = typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? "");
|
|
1090
|
+
spans.push(
|
|
1091
|
+
span({
|
|
1092
|
+
traceId,
|
|
1093
|
+
spanId: llmId,
|
|
1094
|
+
parentSpanId: rootId,
|
|
1095
|
+
name: `message.${m.type ?? "unknown"}`,
|
|
1096
|
+
kind: "LLM",
|
|
1097
|
+
startTime: ts,
|
|
1098
|
+
service: this.service,
|
|
1099
|
+
agent: this.service,
|
|
1100
|
+
model: m.model ?? null,
|
|
1101
|
+
inputTokens: m.tokens?.input ?? null,
|
|
1102
|
+
outputTokens: m.tokens?.output ?? null,
|
|
1103
|
+
step,
|
|
1104
|
+
content: content ? content.slice(0, 8e3) : null
|
|
1105
|
+
})
|
|
1106
|
+
);
|
|
1107
|
+
step += 1;
|
|
1108
|
+
for (const tc of m.toolCalls ?? []) {
|
|
1109
|
+
const err = toolStatusError(tc.status);
|
|
1110
|
+
spans.push(
|
|
1111
|
+
span({
|
|
1112
|
+
traceId,
|
|
1113
|
+
spanId: `tool:${tc.id ?? `${mid}:${step}`}`,
|
|
1114
|
+
parentSpanId: llmId,
|
|
1115
|
+
name: `tool.${tc.name ?? "tool"}`,
|
|
1116
|
+
kind: "TOOL",
|
|
1117
|
+
startTime: ts,
|
|
1118
|
+
status: err ? "ERROR" : "OK",
|
|
1119
|
+
statusMessage: err ? `status=${tc.status}` : void 0,
|
|
1120
|
+
service: this.service,
|
|
1121
|
+
agent: this.service,
|
|
1122
|
+
tool: tc.name ?? "tool",
|
|
1123
|
+
step,
|
|
1124
|
+
content: tc.args != null ? JSON.stringify(tc.args) : null
|
|
1125
|
+
})
|
|
1126
|
+
);
|
|
1127
|
+
step += 1;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return spans;
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
var GeminiAdapter = class extends GeminiFamilyAdapter {
|
|
1134
|
+
constructor() {
|
|
1135
|
+
super({ harness: "gemini", service: "gemini", homeDirName: ".gemini", aliases: ["gemini-cli"] });
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
// src/adapters/opencode.ts
|
|
1140
|
+
import { readdir as readdir8, readFile as readFile8, stat as stat8 } from "fs/promises";
|
|
1141
|
+
import { homedir as homedir8 } from "os";
|
|
1142
|
+
import { join as join8 } from "path";
|
|
1143
|
+
var SERVICE7 = "opencode";
|
|
1144
|
+
function isoOf(ms2) {
|
|
1145
|
+
return new Date(ms2 ?? 0).toISOString();
|
|
1146
|
+
}
|
|
1147
|
+
var OpencodeAdapter = class {
|
|
1148
|
+
harness = "opencode";
|
|
1149
|
+
storage() {
|
|
1150
|
+
const base = process.env.XDG_DATA_HOME ?? join8(homedir8(), ".local", "share");
|
|
1151
|
+
return join8(base, "opencode", "storage");
|
|
1152
|
+
}
|
|
1153
|
+
async locate(opts = {}) {
|
|
1154
|
+
const msgRoot = join8(this.storage(), "message");
|
|
1155
|
+
let sessionDirs;
|
|
1156
|
+
try {
|
|
1157
|
+
sessionDirs = await readdir8(msgRoot);
|
|
1158
|
+
} catch {
|
|
1159
|
+
return [];
|
|
1160
|
+
}
|
|
1161
|
+
const refs = [];
|
|
1162
|
+
for (const sid of sessionDirs) {
|
|
1163
|
+
const dir = join8(msgRoot, sid);
|
|
1164
|
+
let st;
|
|
1165
|
+
try {
|
|
1166
|
+
st = await stat8(dir);
|
|
1167
|
+
} catch {
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
if (!st.isDirectory()) continue;
|
|
1171
|
+
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
1172
|
+
if (opts.cwd) continue;
|
|
1173
|
+
refs.push({ harness: this.harness, sessionId: sid, path: dir, cwd: null, mtimeMs: st.mtimeMs });
|
|
1174
|
+
}
|
|
1175
|
+
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1176
|
+
}
|
|
1177
|
+
async parse(ref) {
|
|
1178
|
+
const traceId = ref.sessionId;
|
|
1179
|
+
const rootId = `root:${traceId}`;
|
|
1180
|
+
const partRoot = join8(this.storage(), "part");
|
|
1181
|
+
let msgFiles;
|
|
1182
|
+
try {
|
|
1183
|
+
msgFiles = (await readdir8(ref.path)).filter((f) => f.endsWith(".json"));
|
|
1184
|
+
} catch {
|
|
1185
|
+
return [];
|
|
1186
|
+
}
|
|
1187
|
+
const messages = [];
|
|
1188
|
+
for (const f of msgFiles) {
|
|
1189
|
+
try {
|
|
1190
|
+
messages.push({ msg: JSON.parse(await readFile8(join8(ref.path, f), "utf8")), file: f });
|
|
1191
|
+
} catch {
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
messages.sort((a, b) => (a.msg.time?.created ?? 0) - (b.msg.time?.created ?? 0));
|
|
1195
|
+
const spans = [
|
|
1196
|
+
span({
|
|
1197
|
+
traceId,
|
|
1198
|
+
spanId: rootId,
|
|
1199
|
+
parentSpanId: null,
|
|
1200
|
+
name: "session",
|
|
1201
|
+
kind: "AGENT",
|
|
1202
|
+
startTime: isoOf(messages[0]?.msg.time?.created),
|
|
1203
|
+
endTime: isoOf(messages.at(-1)?.msg.time?.completed ?? messages.at(-1)?.msg.time?.created),
|
|
1204
|
+
service: SERVICE7,
|
|
1205
|
+
agent: SERVICE7
|
|
1206
|
+
})
|
|
1207
|
+
];
|
|
1208
|
+
let step = 0;
|
|
1209
|
+
for (const { msg } of messages) {
|
|
1210
|
+
const mid = msg.id ?? `m${step}`;
|
|
1211
|
+
const llmId = `llm:${mid}`;
|
|
1212
|
+
spans.push(
|
|
1213
|
+
span({
|
|
1214
|
+
traceId,
|
|
1215
|
+
spanId: llmId,
|
|
1216
|
+
parentSpanId: rootId,
|
|
1217
|
+
name: `message.${msg.role ?? "unknown"}`,
|
|
1218
|
+
kind: "LLM",
|
|
1219
|
+
startTime: isoOf(msg.time?.created),
|
|
1220
|
+
endTime: isoOf(msg.time?.completed ?? msg.time?.created),
|
|
1221
|
+
service: SERVICE7,
|
|
1222
|
+
agent: SERVICE7,
|
|
1223
|
+
model: msg.modelID ?? null,
|
|
1224
|
+
inputTokens: msg.tokens?.input ?? null,
|
|
1225
|
+
outputTokens: msg.tokens?.output ?? null,
|
|
1226
|
+
step
|
|
1227
|
+
})
|
|
1228
|
+
);
|
|
1229
|
+
step += 1;
|
|
1230
|
+
const pdir = join8(partRoot, mid);
|
|
1231
|
+
let partFiles;
|
|
1232
|
+
try {
|
|
1233
|
+
partFiles = (await readdir8(pdir)).filter((f) => f.endsWith(".json"));
|
|
1234
|
+
} catch {
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
for (const pf of partFiles) {
|
|
1238
|
+
let part;
|
|
1239
|
+
try {
|
|
1240
|
+
part = JSON.parse(await readFile8(join8(pdir, pf), "utf8"));
|
|
1241
|
+
} catch {
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
if (part.type !== "tool" || !part.tool) continue;
|
|
1245
|
+
const err = part.state?.status === "error";
|
|
1246
|
+
spans.push(
|
|
1247
|
+
span({
|
|
1248
|
+
traceId,
|
|
1249
|
+
spanId: `tool:${part.callID ?? `${mid}:${step}`}`,
|
|
1250
|
+
parentSpanId: llmId,
|
|
1251
|
+
name: `tool.${part.tool}`,
|
|
1252
|
+
kind: "TOOL",
|
|
1253
|
+
startTime: isoOf(msg.time?.created),
|
|
1254
|
+
endTime: isoOf(msg.time?.completed ?? msg.time?.created),
|
|
1255
|
+
status: err ? "ERROR" : "OK",
|
|
1256
|
+
statusMessage: err ? (part.state?.error ?? "").slice(0, 500) : void 0,
|
|
1257
|
+
service: SERVICE7,
|
|
1258
|
+
agent: SERVICE7,
|
|
1259
|
+
tool: part.tool,
|
|
1260
|
+
step,
|
|
1261
|
+
content: part.state?.input != null ? JSON.stringify(part.state.input) : null
|
|
1262
|
+
})
|
|
1263
|
+
);
|
|
1264
|
+
step += 1;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return spans;
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
|
|
1271
|
+
// src/adapters/pi.ts
|
|
1272
|
+
import { readdir as readdir9, readFile as readFile9, stat as stat9 } from "fs/promises";
|
|
1273
|
+
import { homedir as homedir9 } from "os";
|
|
1274
|
+
import { basename as basename6, join as join9 } from "path";
|
|
1275
|
+
var SERVICE8 = "pi";
|
|
1276
|
+
function parseLines5(raw) {
|
|
1277
|
+
const out = [];
|
|
1278
|
+
for (const line of raw.split("\n")) {
|
|
1279
|
+
if (!line) continue;
|
|
1280
|
+
try {
|
|
1281
|
+
out.push(JSON.parse(line));
|
|
1282
|
+
} catch {
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
return out;
|
|
1286
|
+
}
|
|
1287
|
+
function isToolBlock(b) {
|
|
1288
|
+
return typeof b.type === "string" && /tool[_-]?call|tool[_-]?use|^tool$/i.test(b.type);
|
|
1289
|
+
}
|
|
1290
|
+
function isToolResultBlock(b) {
|
|
1291
|
+
return typeof b.type === "string" && /tool[_-]?result|tool[_-]?output/i.test(b.type);
|
|
1292
|
+
}
|
|
1293
|
+
function textOf(content) {
|
|
1294
|
+
if (!content) return "";
|
|
1295
|
+
return content.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
|
|
1296
|
+
}
|
|
1297
|
+
var PiAdapter = class {
|
|
1298
|
+
harness = "pi";
|
|
1299
|
+
root() {
|
|
1300
|
+
return join9(homedir9(), ".pi", "agent", "sessions");
|
|
1301
|
+
}
|
|
1302
|
+
async locate(opts = {}) {
|
|
1303
|
+
const root = this.root();
|
|
1304
|
+
let dirs;
|
|
1305
|
+
try {
|
|
1306
|
+
dirs = await readdir9(root);
|
|
1307
|
+
} catch {
|
|
1308
|
+
return [];
|
|
1309
|
+
}
|
|
1310
|
+
const refs = [];
|
|
1311
|
+
for (const dir of dirs) {
|
|
1312
|
+
const dp = join9(root, dir);
|
|
1313
|
+
let files;
|
|
1314
|
+
try {
|
|
1315
|
+
files = await readdir9(dp);
|
|
1316
|
+
} catch {
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
const cwd = `/${dir.replace(/^-+/, "").replace(/-+$/, "").replace(/-/g, "/")}`;
|
|
1320
|
+
if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
|
|
1321
|
+
for (const f of files) {
|
|
1322
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
1323
|
+
const path = join9(dp, f);
|
|
1324
|
+
let st;
|
|
1325
|
+
try {
|
|
1326
|
+
st = await stat9(path);
|
|
1327
|
+
} catch {
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
1331
|
+
const id = basename6(f, ".jsonl").replace(/^[\dTZ.-]+_/, "");
|
|
1332
|
+
refs.push({ harness: this.harness, sessionId: id, path, cwd, mtimeMs: st.mtimeMs });
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1336
|
+
}
|
|
1337
|
+
async parse(ref) {
|
|
1338
|
+
const lines = parseLines5(await readFile9(ref.path, "utf8"));
|
|
1339
|
+
const sessionLine = lines.find((l) => l.type === "session");
|
|
1340
|
+
const traceId = sessionLine?.id ?? ref.sessionId;
|
|
1341
|
+
const rootId = `root:${traceId}`;
|
|
1342
|
+
const spans = [
|
|
1343
|
+
span({
|
|
1344
|
+
traceId,
|
|
1345
|
+
spanId: rootId,
|
|
1346
|
+
parentSpanId: null,
|
|
1347
|
+
name: "session",
|
|
1348
|
+
kind: "AGENT",
|
|
1349
|
+
startTime: sessionLine?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1350
|
+
endTime: lines.at(-1)?.timestamp,
|
|
1351
|
+
service: SERVICE8,
|
|
1352
|
+
agent: SERVICE8
|
|
1353
|
+
})
|
|
1354
|
+
];
|
|
1355
|
+
const toolByCallId = /* @__PURE__ */ new Map();
|
|
1356
|
+
let step = 0;
|
|
1357
|
+
for (const l of lines) {
|
|
1358
|
+
if (l.type !== "message" || !l.message) continue;
|
|
1359
|
+
const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1360
|
+
const mid = l.id ?? `m${step}`;
|
|
1361
|
+
const msg = l.message;
|
|
1362
|
+
const llmId = `llm:${mid}`;
|
|
1363
|
+
const errored = !!msg.errorMessage;
|
|
1364
|
+
spans.push(
|
|
1365
|
+
span({
|
|
1366
|
+
traceId,
|
|
1367
|
+
spanId: llmId,
|
|
1368
|
+
parentSpanId: rootId,
|
|
1369
|
+
name: `message.${msg.role ?? "unknown"}`,
|
|
1370
|
+
kind: "LLM",
|
|
1371
|
+
startTime: ts,
|
|
1372
|
+
status: errored ? "ERROR" : "OK",
|
|
1373
|
+
statusMessage: errored ? msg.errorMessage.slice(0, 500) : void 0,
|
|
1374
|
+
service: SERVICE8,
|
|
1375
|
+
agent: SERVICE8,
|
|
1376
|
+
model: msg.model ?? null,
|
|
1377
|
+
inputTokens: msg.usage?.input ?? null,
|
|
1378
|
+
outputTokens: msg.usage?.output ?? null,
|
|
1379
|
+
step,
|
|
1380
|
+
content: textOf(msg.content).slice(0, 8e3) || null
|
|
1381
|
+
})
|
|
1382
|
+
);
|
|
1383
|
+
step += 1;
|
|
1384
|
+
for (const b of msg.content ?? []) {
|
|
1385
|
+
if (isToolBlock(b)) {
|
|
1386
|
+
const name = b.toolName ?? b.name ?? "tool";
|
|
1387
|
+
const callId = b.id ?? b.callId ?? b.toolCallId ?? `${mid}:${step}`;
|
|
1388
|
+
const toolSpan = span({
|
|
1389
|
+
traceId,
|
|
1390
|
+
spanId: `tool:${callId}`,
|
|
1391
|
+
parentSpanId: llmId,
|
|
1392
|
+
name: `tool.${name}`,
|
|
1393
|
+
kind: "TOOL",
|
|
1394
|
+
startTime: ts,
|
|
1395
|
+
service: SERVICE8,
|
|
1396
|
+
agent: SERVICE8,
|
|
1397
|
+
tool: name,
|
|
1398
|
+
step,
|
|
1399
|
+
content: (b.input ?? b.args) != null ? JSON.stringify(b.input ?? b.args) : null
|
|
1400
|
+
});
|
|
1401
|
+
spans.push(toolSpan);
|
|
1402
|
+
toolByCallId.set(callId, toolSpan);
|
|
1403
|
+
step += 1;
|
|
1404
|
+
} else if (isToolResultBlock(b)) {
|
|
1405
|
+
const callId = b.toolCallId ?? b.callId ?? b.id ?? "";
|
|
1406
|
+
const t = toolByCallId.get(callId);
|
|
1407
|
+
if (t) {
|
|
1408
|
+
const err = b.isError === true || b.is_error === true;
|
|
1409
|
+
t.end_time = ts;
|
|
1410
|
+
t.status = err ? { code: "ERROR", message: "tool result reported error" } : { code: "OK" };
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
return spans;
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
|
|
1419
|
+
// src/adapters/qwen.ts
|
|
1420
|
+
import { readdir as readdir10, readFile as readFile10, stat as stat10 } from "fs/promises";
|
|
1421
|
+
import { homedir as homedir10 } from "os";
|
|
1422
|
+
import { basename as basename7, join as join10 } from "path";
|
|
1423
|
+
var SERVICE9 = "qwen";
|
|
1424
|
+
function parseLines6(raw) {
|
|
1425
|
+
const out = [];
|
|
1426
|
+
for (const line of raw.split("\n")) {
|
|
1427
|
+
if (!line) continue;
|
|
1428
|
+
try {
|
|
1429
|
+
out.push(JSON.parse(line));
|
|
1430
|
+
} catch {
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
return out;
|
|
1434
|
+
}
|
|
1435
|
+
function textOf2(parts) {
|
|
1436
|
+
if (!parts) return "";
|
|
1437
|
+
return parts.filter((p) => typeof p.text === "string").map((p) => p.text).join("");
|
|
1438
|
+
}
|
|
1439
|
+
var QwenAdapter = class {
|
|
1440
|
+
harness = "qwen";
|
|
1441
|
+
aliases = ["qwen-code"];
|
|
1442
|
+
root() {
|
|
1443
|
+
return join10(homedir10(), ".qwen", "projects");
|
|
1444
|
+
}
|
|
1445
|
+
async locate(opts = {}) {
|
|
1446
|
+
const root = this.root();
|
|
1447
|
+
let projectDirs;
|
|
1448
|
+
try {
|
|
1449
|
+
projectDirs = await readdir10(root);
|
|
1450
|
+
} catch {
|
|
1451
|
+
return [];
|
|
1452
|
+
}
|
|
1453
|
+
const refs = [];
|
|
1454
|
+
for (const pd of projectDirs) {
|
|
1455
|
+
const chatsDir = join10(root, pd, "chats");
|
|
1456
|
+
let files;
|
|
1457
|
+
try {
|
|
1458
|
+
files = await readdir10(chatsDir);
|
|
1459
|
+
} catch {
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
if (opts.cwd) continue;
|
|
1463
|
+
for (const f of files) {
|
|
1464
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
1465
|
+
const path = join10(chatsDir, f);
|
|
1466
|
+
let st;
|
|
1467
|
+
try {
|
|
1468
|
+
st = await stat10(path);
|
|
1469
|
+
} catch {
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
1473
|
+
refs.push({ harness: this.harness, sessionId: basename7(f, ".jsonl"), path, cwd: null, mtimeMs: st.mtimeMs });
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1477
|
+
}
|
|
1478
|
+
async parse(ref) {
|
|
1479
|
+
const records = parseLines6(await readFile10(ref.path, "utf8"));
|
|
1480
|
+
const traceId = records.find((r) => r.sessionId)?.sessionId ?? ref.sessionId;
|
|
1481
|
+
const rootId = `root:${traceId}`;
|
|
1482
|
+
const spans = [
|
|
1483
|
+
span({
|
|
1484
|
+
traceId,
|
|
1485
|
+
spanId: rootId,
|
|
1486
|
+
parentSpanId: null,
|
|
1487
|
+
name: "session",
|
|
1488
|
+
kind: "AGENT",
|
|
1489
|
+
startTime: records[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1490
|
+
endTime: records.at(-1)?.timestamp,
|
|
1491
|
+
service: SERVICE9,
|
|
1492
|
+
agent: SERVICE9
|
|
1493
|
+
})
|
|
1494
|
+
];
|
|
1495
|
+
const openToolsByName = /* @__PURE__ */ new Map();
|
|
1496
|
+
let step = 0;
|
|
1497
|
+
let lastLlm = rootId;
|
|
1498
|
+
for (const r of records) {
|
|
1499
|
+
const ts = r.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1500
|
+
const role = r.message?.role;
|
|
1501
|
+
if (r.type === "assistant" || role === "model") {
|
|
1502
|
+
const llmId = `llm:${r.uuid ?? step}`;
|
|
1503
|
+
spans.push(
|
|
1504
|
+
span({
|
|
1505
|
+
traceId,
|
|
1506
|
+
spanId: llmId,
|
|
1507
|
+
parentSpanId: rootId,
|
|
1508
|
+
name: "llm.turn",
|
|
1509
|
+
kind: "LLM",
|
|
1510
|
+
startTime: ts,
|
|
1511
|
+
service: SERVICE9,
|
|
1512
|
+
agent: SERVICE9,
|
|
1513
|
+
model: r.model ?? null,
|
|
1514
|
+
inputTokens: r.usageMetadata?.promptTokenCount ?? null,
|
|
1515
|
+
outputTokens: r.usageMetadata?.candidatesTokenCount ?? null,
|
|
1516
|
+
step,
|
|
1517
|
+
content: textOf2(r.message?.parts).slice(0, 8e3) || null
|
|
1518
|
+
})
|
|
1519
|
+
);
|
|
1520
|
+
lastLlm = llmId;
|
|
1521
|
+
step += 1;
|
|
1522
|
+
for (const p of r.message?.parts ?? []) {
|
|
1523
|
+
if (!p.functionCall?.name) continue;
|
|
1524
|
+
const name = p.functionCall.name;
|
|
1525
|
+
const t = span({
|
|
1526
|
+
traceId,
|
|
1527
|
+
spanId: `tool:${name}:${step}`,
|
|
1528
|
+
parentSpanId: llmId,
|
|
1529
|
+
name: `tool.${name}`,
|
|
1530
|
+
kind: "TOOL",
|
|
1531
|
+
startTime: ts,
|
|
1532
|
+
service: SERVICE9,
|
|
1533
|
+
agent: SERVICE9,
|
|
1534
|
+
tool: name,
|
|
1535
|
+
step,
|
|
1536
|
+
content: p.functionCall.args != null ? JSON.stringify(p.functionCall.args) : null
|
|
1537
|
+
});
|
|
1538
|
+
spans.push(t);
|
|
1539
|
+
const q = openToolsByName.get(name) ?? [];
|
|
1540
|
+
q.push(t);
|
|
1541
|
+
openToolsByName.set(name, q);
|
|
1542
|
+
step += 1;
|
|
1543
|
+
}
|
|
1544
|
+
} else if (r.type === "tool_result") {
|
|
1545
|
+
const err = r.toolCallResult?.status === "error" || r.toolCallResult?.error != null;
|
|
1546
|
+
for (const p of r.message?.parts ?? []) {
|
|
1547
|
+
const name = p.functionResponse?.name;
|
|
1548
|
+
if (!name) continue;
|
|
1549
|
+
const t = openToolsByName.get(name)?.shift();
|
|
1550
|
+
if (t) {
|
|
1551
|
+
t.end_time = ts;
|
|
1552
|
+
t.status = err ? { code: "ERROR", message: "tool result reported error" } : { code: "OK" };
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return spans;
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
// src/registry.ts
|
|
1562
|
+
var ADAPTERS = [
|
|
1563
|
+
new ClaudeAdapter(),
|
|
1564
|
+
new CodexAdapter(),
|
|
1565
|
+
new OpencodeAdapter(),
|
|
1566
|
+
new GeminiAdapter(),
|
|
1567
|
+
new QwenAdapter(),
|
|
1568
|
+
new FactoryAdapter(),
|
|
1569
|
+
new PiAdapter(),
|
|
1570
|
+
new AmpAdapter(),
|
|
1571
|
+
new CopilotAdapter(),
|
|
1572
|
+
new ForgeAdapter()
|
|
1573
|
+
];
|
|
1574
|
+
var BY_NAME = /* @__PURE__ */ new Map();
|
|
1575
|
+
for (const a of ADAPTERS) {
|
|
1576
|
+
BY_NAME.set(a.harness, a);
|
|
1577
|
+
for (const alias of a.aliases ?? []) BY_NAME.set(alias, a);
|
|
1578
|
+
}
|
|
1579
|
+
function listAdapters() {
|
|
1580
|
+
return ADAPTERS;
|
|
1581
|
+
}
|
|
1582
|
+
function resolveAdapter(harness) {
|
|
1583
|
+
return BY_NAME.get(harness) ?? BY_NAME.get(harness.toLowerCase());
|
|
1584
|
+
}
|
|
1585
|
+
function knownHarnesses() {
|
|
1586
|
+
return [...BY_NAME.keys()].sort();
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// src/observer.ts
|
|
1590
|
+
var sleep = (ms2, signal) => new Promise((resolve) => {
|
|
1591
|
+
const onAbort = () => {
|
|
1592
|
+
clearTimeout(t);
|
|
1593
|
+
resolve();
|
|
1594
|
+
};
|
|
1595
|
+
const t = setTimeout(() => {
|
|
1596
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1597
|
+
resolve();
|
|
1598
|
+
}, ms2);
|
|
1599
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1600
|
+
});
|
|
1601
|
+
function adaptersFor(opts) {
|
|
1602
|
+
if (opts.adapters && opts.adapters.length > 0) return opts.adapters;
|
|
1603
|
+
if (opts.all || !opts.harnesses || opts.harnesses.length === 0) return [...listAdapters()];
|
|
1604
|
+
const out = [];
|
|
1605
|
+
for (const h of opts.harnesses) {
|
|
1606
|
+
const a = resolveAdapter(h);
|
|
1607
|
+
if (a && !out.includes(a)) out.push(a);
|
|
1608
|
+
}
|
|
1609
|
+
return out;
|
|
1610
|
+
}
|
|
1611
|
+
function loopKey(sessionId, toolName, argHash) {
|
|
1612
|
+
return `${sessionId}\0${toolName}\0${argHash}`;
|
|
1613
|
+
}
|
|
1614
|
+
async function watchSessions(opts) {
|
|
1615
|
+
const windowMs = opts.windowMs ?? 30 * 6e4;
|
|
1616
|
+
const intervalMs = Math.max(250, opts.intervalMs ?? 5e3);
|
|
1617
|
+
const minLoop = opts.minLoopOccurrences ?? 3;
|
|
1618
|
+
const adapters = adaptersFor(opts);
|
|
1619
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1620
|
+
while (!opts.signal?.aborted) {
|
|
1621
|
+
const sinceMs = Date.now() - windowMs;
|
|
1622
|
+
let sessionCount = 0;
|
|
1623
|
+
for (const adapter of adapters) {
|
|
1624
|
+
if (opts.signal?.aborted) break;
|
|
1625
|
+
let refs;
|
|
1626
|
+
try {
|
|
1627
|
+
refs = await adapter.locate({ cwd: opts.cwd, sinceMs });
|
|
1628
|
+
} catch (err) {
|
|
1629
|
+
opts.onError?.(err);
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
for (const ref of refs) {
|
|
1633
|
+
if (opts.signal?.aborted) break;
|
|
1634
|
+
let report;
|
|
1635
|
+
try {
|
|
1636
|
+
const spans = await adapter.parse(ref);
|
|
1637
|
+
if (spans.length === 0) continue;
|
|
1638
|
+
report = await runPipelines(spans, { minLoopOccurrences: minLoop });
|
|
1639
|
+
} catch (err) {
|
|
1640
|
+
opts.onError?.(err, ref);
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1643
|
+
sessionCount += 1;
|
|
1644
|
+
if (opts.onReport) {
|
|
1645
|
+
try {
|
|
1646
|
+
await opts.onReport(ref, report);
|
|
1647
|
+
} catch (err) {
|
|
1648
|
+
opts.onError?.(err, ref);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
for (const f of report.stuckLoops.findings) {
|
|
1652
|
+
const key = loopKey(ref.sessionId, f.toolName, f.argHash);
|
|
1653
|
+
if (f.occurrences > (seen.get(key) ?? 0)) {
|
|
1654
|
+
seen.set(key, f.occurrences);
|
|
1655
|
+
if (opts.onLoop) {
|
|
1656
|
+
try {
|
|
1657
|
+
await opts.onLoop({
|
|
1658
|
+
harness: adapter.harness,
|
|
1659
|
+
sessionId: ref.sessionId,
|
|
1660
|
+
cwd: ref.cwd,
|
|
1661
|
+
toolName: f.toolName,
|
|
1662
|
+
argHash: f.argHash,
|
|
1663
|
+
occurrences: f.occurrences,
|
|
1664
|
+
windowMs: f.windowMs
|
|
1665
|
+
});
|
|
1666
|
+
} catch (err) {
|
|
1667
|
+
opts.onError?.(err, ref);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
try {
|
|
1675
|
+
opts.onTick?.({ sessions: sessionCount });
|
|
1676
|
+
} catch (err) {
|
|
1677
|
+
opts.onError?.(err);
|
|
1678
|
+
}
|
|
1679
|
+
if (opts.signal?.aborted) break;
|
|
1680
|
+
await sleep(intervalMs, opts.signal);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// src/redact.ts
|
|
1685
|
+
import { DEFAULT_REDACTION_RULES, redactValue } from "@tangle-network/agent-eval/traces";
|
|
1686
|
+
var CODING_REDACTION_RULES = [
|
|
1687
|
+
{ id: "github-token", pattern: /\b(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{22,})\b/g },
|
|
1688
|
+
{ id: "aws-akid", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
1689
|
+
{ id: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
|
|
1690
|
+
{ id: "jwt", pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
|
|
1691
|
+
{ id: "bearer", pattern: /\bBearer\s+[A-Za-z0-9._-]{16,}/gi },
|
|
1692
|
+
{
|
|
1693
|
+
id: "private-key",
|
|
1694
|
+
pattern: /-----BEGIN(?:[A-Z ]+)?PRIVATE KEY-----[\s\S]*?-----END(?:[A-Z ]+)?PRIVATE KEY-----/g
|
|
1695
|
+
},
|
|
1696
|
+
{
|
|
1697
|
+
id: "assigned-secret",
|
|
1698
|
+
pattern: /\b(?:api[_-]?key|secret|token|password|passwd|access[_-]?token|client[_-]?secret)\b\s*[:=]\s*["']?[A-Za-z0-9._\-]{12,}["']?/gi
|
|
1699
|
+
}
|
|
1700
|
+
];
|
|
1701
|
+
var TRACES_REDACTION_RULES = [...DEFAULT_REDACTION_RULES, ...CODING_REDACTION_RULES];
|
|
1702
|
+
function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
|
|
1703
|
+
const report = { redactionCount: 0, byRule: {} };
|
|
1704
|
+
const out = spans.map((s) => {
|
|
1705
|
+
const attributes = redactValue(s.attributes, rules, report).value;
|
|
1706
|
+
let status = s.status;
|
|
1707
|
+
if (status.message) {
|
|
1708
|
+
const m = redactValue(status.message, rules, report).value;
|
|
1709
|
+
if (typeof m === "string" && m !== status.message) status = { ...status, message: m };
|
|
1710
|
+
}
|
|
1711
|
+
return { ...s, attributes, status };
|
|
1712
|
+
});
|
|
1713
|
+
return { spans: out, report };
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// src/upload-state.ts
|
|
1717
|
+
import { createHash } from "crypto";
|
|
1718
|
+
import { mkdir, readFile as readFile11, writeFile } from "fs/promises";
|
|
1719
|
+
import { homedir as homedir11 } from "os";
|
|
1720
|
+
import { dirname, join as join11 } from "path";
|
|
1721
|
+
function stateFile() {
|
|
1722
|
+
const base = process.env.XDG_STATE_HOME ?? join11(homedir11(), ".local", "state");
|
|
1723
|
+
return join11(base, "tangle-traces", "uploaded.json");
|
|
1724
|
+
}
|
|
1725
|
+
function uploadKey(harness, sessionId) {
|
|
1726
|
+
return `${harness}:${sessionId}`;
|
|
1727
|
+
}
|
|
1728
|
+
function sessionHash(spans) {
|
|
1729
|
+
const h = createHash("sha256");
|
|
1730
|
+
for (const s of spans) {
|
|
1731
|
+
h.update(`${s.span_id}\0${s.name}\0${s.start_time}\0${s.status.code}
|
|
1732
|
+
`);
|
|
1733
|
+
}
|
|
1734
|
+
return h.digest("hex").slice(0, 32);
|
|
1735
|
+
}
|
|
1736
|
+
async function loadState() {
|
|
1737
|
+
try {
|
|
1738
|
+
return JSON.parse(await readFile11(stateFile(), "utf8"));
|
|
1739
|
+
} catch {
|
|
1740
|
+
return {};
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
async function saveState(state) {
|
|
1744
|
+
const f = stateFile();
|
|
1745
|
+
await mkdir(dirname(f), { recursive: true });
|
|
1746
|
+
await writeFile(f, `${JSON.stringify(state, null, 2)}
|
|
1747
|
+
`, "utf8");
|
|
1748
|
+
}
|
|
1749
|
+
function alreadyUploaded(state, harness, sessionId, hash) {
|
|
1750
|
+
return state[uploadKey(harness, sessionId)]?.hash === hash;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// src/upload.ts
|
|
1754
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
1755
|
+
import { createRequire } from "module";
|
|
1756
|
+
import { hostname } from "os";
|
|
1757
|
+
import { basename as basename8, join as join12 } from "path";
|
|
1758
|
+
import { hostedClientFromEnv } from "@tangle-network/agent-eval/hosted";
|
|
1759
|
+
import { REDACTION_VERSION } from "@tangle-network/agent-eval/traces";
|
|
1760
|
+
var require2 = createRequire(import.meta.url);
|
|
1761
|
+
var TRACES_VERSION = require2("../package.json").version;
|
|
1762
|
+
function adaptersFor2(opts) {
|
|
1763
|
+
if (opts.all) return [...listAdapters()];
|
|
1764
|
+
const a = resolveAdapter(opts.harness ?? "claude-code");
|
|
1765
|
+
if (!a) throw new Error(`unknown harness "${opts.harness}"`);
|
|
1766
|
+
return [a];
|
|
1767
|
+
}
|
|
1768
|
+
async function planUpload(opts) {
|
|
1769
|
+
const state = await loadState();
|
|
1770
|
+
const items = [];
|
|
1771
|
+
for (const adapter of adaptersFor2(opts)) {
|
|
1772
|
+
let refs;
|
|
1773
|
+
try {
|
|
1774
|
+
refs = await adapter.locate({ cwd: opts.cwd, sinceMs: opts.sinceMs });
|
|
1775
|
+
} catch (err) {
|
|
1776
|
+
opts.onError?.(err);
|
|
1777
|
+
continue;
|
|
1778
|
+
}
|
|
1779
|
+
for (const ref of refs) {
|
|
1780
|
+
let raw;
|
|
1781
|
+
try {
|
|
1782
|
+
raw = await adapter.parse(ref);
|
|
1783
|
+
} catch (err) {
|
|
1784
|
+
opts.onError?.(err, ref);
|
|
1785
|
+
continue;
|
|
1786
|
+
}
|
|
1787
|
+
if (raw.length === 0) continue;
|
|
1788
|
+
const { spans, report } = redactSpans(raw);
|
|
1789
|
+
const hash = sessionHash(spans);
|
|
1790
|
+
items.push({
|
|
1791
|
+
ref,
|
|
1792
|
+
spans,
|
|
1793
|
+
redaction: report,
|
|
1794
|
+
hash,
|
|
1795
|
+
isNew: !alreadyUploaded(state, ref.harness, ref.sessionId, hash)
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
return { items, state, sinceMs: opts.sinceMs };
|
|
1800
|
+
}
|
|
1801
|
+
async function gitBranch(cwd) {
|
|
1802
|
+
if (!cwd) return void 0;
|
|
1803
|
+
try {
|
|
1804
|
+
const head = await readFile12(join12(cwd, ".git", "HEAD"), "utf8");
|
|
1805
|
+
const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
|
|
1806
|
+
return m?.[1]?.trim();
|
|
1807
|
+
} catch {
|
|
1808
|
+
return void 0;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
async function sessionMeta(item, uploadedAt) {
|
|
1812
|
+
const meta = {
|
|
1813
|
+
"tangle.harness": item.ref.harness,
|
|
1814
|
+
"tangle.source": basename8(item.ref.path),
|
|
1815
|
+
"tangle.host": hostname(),
|
|
1816
|
+
"tangle.uploaded_at": uploadedAt,
|
|
1817
|
+
"tangle.uploader": `tangle-traces@${TRACES_VERSION}`,
|
|
1818
|
+
"redaction.version": REDACTION_VERSION,
|
|
1819
|
+
"redaction.count": item.redaction.redactionCount
|
|
1820
|
+
};
|
|
1821
|
+
if (item.ref.cwd) meta["tangle.cwd"] = item.ref.cwd;
|
|
1822
|
+
const branch = await gitBranch(item.ref.cwd);
|
|
1823
|
+
if (branch) meta["tangle.git_branch"] = branch;
|
|
1824
|
+
return meta;
|
|
1825
|
+
}
|
|
1826
|
+
function epochMs(ts) {
|
|
1827
|
+
if (!ts) return 0;
|
|
1828
|
+
if (/^\d+$/.test(ts)) return Number(ts);
|
|
1829
|
+
const n = Date.parse(ts);
|
|
1830
|
+
return Number.isNaN(n) ? 0 : n;
|
|
1831
|
+
}
|
|
1832
|
+
var msToNano = (iso2) => epochMs(iso2) * 1e6;
|
|
1833
|
+
function toTraceSpanEvents(spans, rootMeta) {
|
|
1834
|
+
return spans.map((s) => {
|
|
1835
|
+
const attributes = {};
|
|
1836
|
+
for (const [k, v] of Object.entries(s.attributes)) {
|
|
1837
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") attributes[k] = v;
|
|
1838
|
+
else if (v != null) attributes[k] = JSON.stringify(v);
|
|
1839
|
+
}
|
|
1840
|
+
if (s.parent_span_id === null) Object.assign(attributes, rootMeta);
|
|
1841
|
+
const startNano = msToNano(s.start_time);
|
|
1842
|
+
return {
|
|
1843
|
+
traceId: s.trace_id,
|
|
1844
|
+
spanId: s.span_id,
|
|
1845
|
+
...s.parent_span_id ? { parentSpanId: s.parent_span_id } : {},
|
|
1846
|
+
name: s.name,
|
|
1847
|
+
startTimeUnixNano: startNano,
|
|
1848
|
+
endTimeUnixNano: Math.max(startNano, msToNano(s.end_time)),
|
|
1849
|
+
attributes,
|
|
1850
|
+
...s.status.code === "UNSET" ? {} : { status: { code: s.status.code, ...s.status.message ? { message: s.status.message } : {} } }
|
|
1851
|
+
};
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
async function executeUpload(plan, opts = {}) {
|
|
1855
|
+
const newItems = plan.items.filter((i) => i.isNew);
|
|
1856
|
+
const skipped = plan.items.length - newItems.length;
|
|
1857
|
+
const redactionCount = newItems.reduce((n, i) => n + i.redaction.redactionCount, 0);
|
|
1858
|
+
const uploadedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1859
|
+
if (opts.dryRun) {
|
|
1860
|
+
const allSpans = newItems.flatMap((i) => i.spans);
|
|
1861
|
+
const path = await writeOtlpFile(allSpans, opts.otlpOut);
|
|
1862
|
+
return {
|
|
1863
|
+
uploadedSessions: 0,
|
|
1864
|
+
skippedSessions: skipped,
|
|
1865
|
+
acceptedSpans: 0,
|
|
1866
|
+
redactionCount,
|
|
1867
|
+
dryRun: true,
|
|
1868
|
+
otlpPath: path
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
const client = opts.backend ?? hostedClientFromEnv();
|
|
1872
|
+
if (!client) {
|
|
1873
|
+
throw new Error(
|
|
1874
|
+
"upload: no backend. Pass opts.backend, or configure the hosted Tangle Intelligence client via TANGLE_INGEST_URL (or TANGLE_ORCHESTRATOR_URL), TANGLE_INGEST_API_KEY (or TANGLE_API_KEY), and TANGLE_TENANT_ID. Use --dry-run to preview without uploading."
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
const state = plan.state;
|
|
1878
|
+
let acceptedSpans = 0;
|
|
1879
|
+
for (const item of newItems) {
|
|
1880
|
+
const meta = await sessionMeta(item, uploadedAt);
|
|
1881
|
+
const events = toTraceSpanEvents(item.spans, meta);
|
|
1882
|
+
const idempotencyKey = `${uploadKey(item.ref.harness, item.ref.sessionId)}:${item.hash}`;
|
|
1883
|
+
const res = await client.ingestTraces(events, idempotencyKey);
|
|
1884
|
+
acceptedSpans += res.accepted;
|
|
1885
|
+
state[uploadKey(item.ref.harness, item.ref.sessionId)] = {
|
|
1886
|
+
hash: item.hash,
|
|
1887
|
+
uploadedAt,
|
|
1888
|
+
harness: item.ref.harness,
|
|
1889
|
+
spanCount: item.spans.length
|
|
1890
|
+
};
|
|
1891
|
+
opts.log?.(`uploaded ${item.ref.harness} ${item.ref.sessionId.slice(0, 8)} \u2014 ${res.accepted} spans`);
|
|
1892
|
+
}
|
|
1893
|
+
await saveState(state);
|
|
1894
|
+
return {
|
|
1895
|
+
uploadedSessions: newItems.length,
|
|
1896
|
+
skippedSessions: skipped,
|
|
1897
|
+
acceptedSpans,
|
|
1898
|
+
redactionCount,
|
|
1899
|
+
dryRun: false
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
export {
|
|
1904
|
+
span,
|
|
1905
|
+
serializeSpans,
|
|
1906
|
+
writeOtlpFile,
|
|
1907
|
+
analyzeSpans,
|
|
1908
|
+
toRuntimeStore,
|
|
1909
|
+
runPipelines,
|
|
1910
|
+
AmpAdapter,
|
|
1911
|
+
ClaudeAdapter,
|
|
1912
|
+
CodexAdapter,
|
|
1913
|
+
CopilotAdapter,
|
|
1914
|
+
FactoryAdapter,
|
|
1915
|
+
ForgeAdapter,
|
|
1916
|
+
GeminiAdapter,
|
|
1917
|
+
OpencodeAdapter,
|
|
1918
|
+
PiAdapter,
|
|
1919
|
+
QwenAdapter,
|
|
1920
|
+
listAdapters,
|
|
1921
|
+
resolveAdapter,
|
|
1922
|
+
knownHarnesses,
|
|
1923
|
+
watchSessions,
|
|
1924
|
+
CODING_REDACTION_RULES,
|
|
1925
|
+
TRACES_REDACTION_RULES,
|
|
1926
|
+
redactSpans,
|
|
1927
|
+
uploadKey,
|
|
1928
|
+
sessionHash,
|
|
1929
|
+
loadState,
|
|
1930
|
+
saveState,
|
|
1931
|
+
alreadyUploaded,
|
|
1932
|
+
planUpload,
|
|
1933
|
+
toTraceSpanEvents,
|
|
1934
|
+
executeUpload
|
|
1935
|
+
};
|