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