@tangle-network/traces 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/{chunk-6TVW7PL6.js → chunk-BUD6I3ZJ.js} +96 -38
- package/dist/cli.js +9 -4
- package/dist/index.d.ts +34 -9
- package/dist/index.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ That's the command in the demo above. The **deterministic pass** — stuck loops
|
|
|
43
43
|
|
|
44
44
|
Add `--llm` for the **agentic analysts** (failure-mode / knowledge-gap / knowledge-poisoning / improvement); they call OpenAI and respect `--budget <usd>`.
|
|
45
45
|
|
|
46
|
-
Every run also writes
|
|
46
|
+
Every run also writes a **canonical OpenInference JSONL artifact**, so you can run external engines like [HALO](https://github.com/context-labs/halo) over it directly with `--analyzer halo` — see [External engines](#external-engines-bring-your-own). Analysis is never locked to one engine.
|
|
47
47
|
|
|
48
48
|
## What it finds
|
|
49
49
|
|
|
@@ -142,6 +142,8 @@ traces analyze --last 1 --analyzer halo --analyzer-prompt "find token waste"
|
|
|
142
142
|
traces analyze --last 1 --analyzer halo --analyzer my-engine # repeatable
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
+
Our OTLP artifact is **canonical OpenInference** (top-level `kind`, `resource`, `scope`), so HALO reads it directly — no conversion. HALO runs its *own* LLM (OpenAI client — set `OPENAI_BASE_URL` / `OPENAI_API_KEY`, or use HALO's provider); `--model` is forwarded to it. traces supplies the trace and drives the CLI; it doesn't pay for or configure HALO's model.
|
|
146
|
+
|
|
145
147
|
**Redactors** scrub prompt/response prose with an external PII model (catching names/addresses the regex pass can't), running *after* the built-in redaction:
|
|
146
148
|
|
|
147
149
|
```bash
|
|
@@ -25,17 +25,36 @@ function span(input) {
|
|
|
25
25
|
attributes
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
function toOpenInferenceSpan(s) {
|
|
29
|
+
const a = s.attributes;
|
|
30
|
+
const resourceAttrs = {};
|
|
31
|
+
if (a["service.name"] != null) resourceAttrs["service.name"] = a["service.name"];
|
|
32
|
+
if (a["agent.name"] != null) resourceAttrs["agent.name"] = a["agent.name"];
|
|
33
|
+
return {
|
|
34
|
+
trace_id: s.trace_id,
|
|
35
|
+
span_id: s.span_id,
|
|
36
|
+
parent_span_id: s.parent_span_id ?? "",
|
|
37
|
+
name: s.name,
|
|
38
|
+
kind: typeof a["openinference.span.kind"] === "string" ? a["openinference.span.kind"] : "CHAIN",
|
|
39
|
+
start_time: s.start_time,
|
|
40
|
+
end_time: s.end_time,
|
|
41
|
+
status: { code: s.status.code, message: s.status.message ?? "" },
|
|
42
|
+
resource: { attributes: resourceAttrs },
|
|
43
|
+
scope: { name: "tangle-traces", version: "" },
|
|
44
|
+
attributes: a
|
|
45
|
+
};
|
|
46
|
+
}
|
|
28
47
|
function serializeSpans(spans) {
|
|
29
48
|
if (spans.length === 0) return "";
|
|
30
|
-
return `${spans.map((s) => JSON.stringify(s)).join("\n")}
|
|
49
|
+
return `${spans.map((s) => JSON.stringify(toOpenInferenceSpan(s))).join("\n")}
|
|
31
50
|
`;
|
|
32
51
|
}
|
|
33
52
|
async function writeOtlpFile(spans, outPath) {
|
|
34
|
-
const { mkdtemp, writeFile:
|
|
53
|
+
const { mkdtemp, writeFile: writeFile3 } = await import("fs/promises");
|
|
35
54
|
const { tmpdir } = await import("os");
|
|
36
55
|
const { join: join13 } = await import("path");
|
|
37
56
|
const path = outPath ?? join13(await mkdtemp(join13(tmpdir(), "traces-")), "spans.otlp.jsonl");
|
|
38
|
-
await
|
|
57
|
+
await writeFile3(path, serializeSpans(spans), "utf8");
|
|
39
58
|
return path;
|
|
40
59
|
}
|
|
41
60
|
|
|
@@ -72,6 +91,7 @@ async function analyzeSpans(spans, opts = {}) {
|
|
|
72
91
|
|
|
73
92
|
// src/external.ts
|
|
74
93
|
import { spawn } from "child_process";
|
|
94
|
+
import { readFile, writeFile } from "fs/promises";
|
|
75
95
|
function runCommand(command, args = [], opts = {}) {
|
|
76
96
|
const timeoutMs = opts.timeoutMs ?? 12e4;
|
|
77
97
|
const maxBuffer = opts.maxBuffer ?? 32 * 1024 * 1024;
|
|
@@ -141,14 +161,50 @@ function commandAnalyzer(spec) {
|
|
|
141
161
|
}
|
|
142
162
|
};
|
|
143
163
|
}
|
|
164
|
+
function toCanonicalOpenInference(otlpJsonl, opts = {}) {
|
|
165
|
+
return otlpJsonl.split("\n").filter((l) => l.trim()).map((line) => {
|
|
166
|
+
const s = JSON.parse(line);
|
|
167
|
+
const a = s.attributes ?? {};
|
|
168
|
+
const resourceAttrs = { "service.name": a["service.name"] ?? opts.service ?? "traces" };
|
|
169
|
+
if (a["agent.name"]) resourceAttrs["agent.name"] = a["agent.name"];
|
|
170
|
+
return JSON.stringify({
|
|
171
|
+
trace_id: s.trace_id,
|
|
172
|
+
span_id: s.span_id,
|
|
173
|
+
parent_span_id: s.parent_span_id ?? "",
|
|
174
|
+
trace_state: "",
|
|
175
|
+
name: s.name,
|
|
176
|
+
kind: a["openinference.span.kind"] ?? "CHAIN",
|
|
177
|
+
start_time: s.start_time,
|
|
178
|
+
end_time: s.end_time,
|
|
179
|
+
status: { code: s.status?.code ?? "OK", message: s.status?.message ?? "" },
|
|
180
|
+
resource: { attributes: resourceAttrs },
|
|
181
|
+
scope: { name: opts.scopeName ?? "tangle-traces", version: "" },
|
|
182
|
+
attributes: a
|
|
183
|
+
});
|
|
184
|
+
}).join("\n");
|
|
185
|
+
}
|
|
144
186
|
function haloAnalyzer(opts = {}) {
|
|
187
|
+
const command = opts.command ?? "halo";
|
|
145
188
|
const defaultPrompt = opts.defaultPrompt ?? "diagnose";
|
|
146
|
-
return
|
|
189
|
+
return {
|
|
147
190
|
name: "halo",
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
191
|
+
async analyze(otlpPath, o = {}) {
|
|
192
|
+
try {
|
|
193
|
+
const canonical = toCanonicalOpenInference(await readFile(otlpPath, "utf8"));
|
|
194
|
+
const canonPath = `${otlpPath}.halo.jsonl`;
|
|
195
|
+
await writeFile(canonPath, canonical);
|
|
196
|
+
const args = [canonPath, "-p", o.prompt ?? defaultPrompt];
|
|
197
|
+
if (opts.model) args.push("-m", opts.model);
|
|
198
|
+
const res = await runCommand(command, args, { signal: o.signal, timeoutMs: opts.timeoutMs });
|
|
199
|
+
if (res.code !== 0) {
|
|
200
|
+
return { analyzer: "halo", ok: false, output: res.stdout, error: res.stderr.trim() || `exit ${res.code}` };
|
|
201
|
+
}
|
|
202
|
+
return { analyzer: "halo", ok: true, output: res.stdout.trim() };
|
|
203
|
+
} catch (e) {
|
|
204
|
+
return { analyzer: "halo", ok: false, output: "", error: e instanceof Error ? e.message : String(e) };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|
|
152
208
|
}
|
|
153
209
|
function runExternalAnalyzers(otlpPath, analyzers, opts = {}) {
|
|
154
210
|
return Promise.all(analyzers.map((a) => a.analyze(otlpPath, opts)));
|
|
@@ -270,7 +326,7 @@ function userPromptSpan(o) {
|
|
|
270
326
|
}
|
|
271
327
|
|
|
272
328
|
// src/adapters/amp.ts
|
|
273
|
-
import { readdir, readFile, stat } from "fs/promises";
|
|
329
|
+
import { readdir, readFile as readFile2, stat } from "fs/promises";
|
|
274
330
|
import { homedir } from "os";
|
|
275
331
|
import { basename, join } from "path";
|
|
276
332
|
var SERVICE = "amp";
|
|
@@ -312,7 +368,7 @@ var AmpAdapter = class {
|
|
|
312
368
|
async parse(ref) {
|
|
313
369
|
let thread;
|
|
314
370
|
try {
|
|
315
|
-
thread = JSON.parse(await
|
|
371
|
+
thread = JSON.parse(await readFile2(ref.path, "utf8"));
|
|
316
372
|
} catch {
|
|
317
373
|
return [];
|
|
318
374
|
}
|
|
@@ -396,7 +452,7 @@ var AmpAdapter = class {
|
|
|
396
452
|
};
|
|
397
453
|
|
|
398
454
|
// src/adapters/claude.ts
|
|
399
|
-
import { readdir as readdir2, readFile as
|
|
455
|
+
import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
400
456
|
import { homedir as homedir2 } from "os";
|
|
401
457
|
import { basename as basename2, join as join2 } from "path";
|
|
402
458
|
var SERVICE2 = "claude-code";
|
|
@@ -557,7 +613,7 @@ var ClaudeAdapter = class {
|
|
|
557
613
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
558
614
|
}
|
|
559
615
|
async parse(ref) {
|
|
560
|
-
const raw = await
|
|
616
|
+
const raw = await readFile3(ref.path, "utf8");
|
|
561
617
|
const events = parseLines(raw);
|
|
562
618
|
const traceId = events.find((e) => e.sessionId)?.sessionId ?? ref.sessionId;
|
|
563
619
|
const rootId = `root:${traceId}`;
|
|
@@ -600,13 +656,13 @@ var ClaudeAdapter = class {
|
|
|
600
656
|
const hash = basename2(f, ".jsonl");
|
|
601
657
|
let meta = {};
|
|
602
658
|
try {
|
|
603
|
-
meta = JSON.parse(await
|
|
659
|
+
meta = JSON.parse(await readFile3(join2(subDir, `${hash}.meta.json`), "utf8"));
|
|
604
660
|
} catch {
|
|
605
661
|
}
|
|
606
662
|
const parent = meta.toolUseId && main.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
|
|
607
663
|
let raw;
|
|
608
664
|
try {
|
|
609
|
-
raw = await
|
|
665
|
+
raw = await readFile3(join2(subDir, f), "utf8");
|
|
610
666
|
} catch {
|
|
611
667
|
continue;
|
|
612
668
|
}
|
|
@@ -624,7 +680,7 @@ var ClaudeAdapter = class {
|
|
|
624
680
|
};
|
|
625
681
|
|
|
626
682
|
// src/adapters/codex.ts
|
|
627
|
-
import { readdir as readdir3, readFile as
|
|
683
|
+
import { readdir as readdir3, readFile as readFile4, stat as stat3 } from "fs/promises";
|
|
628
684
|
import { homedir as homedir3 } from "os";
|
|
629
685
|
import { basename as basename3, join as join3 } from "path";
|
|
630
686
|
var SERVICE3 = "codex";
|
|
@@ -708,7 +764,7 @@ var CodexAdapter = class {
|
|
|
708
764
|
continue;
|
|
709
765
|
}
|
|
710
766
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
711
|
-
const head = (await
|
|
767
|
+
const head = (await readFile4(path, "utf8").catch(() => "")).split("\n", 1)[0] ?? "";
|
|
712
768
|
let cwd = null;
|
|
713
769
|
let id = basename3(path).replace(/^rollout-[\dT-]+-/, "").replace(/\.jsonl$/, "");
|
|
714
770
|
try {
|
|
@@ -723,7 +779,7 @@ var CodexAdapter = class {
|
|
|
723
779
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
724
780
|
}
|
|
725
781
|
async parse(ref) {
|
|
726
|
-
const lines = parseLines2(await
|
|
782
|
+
const lines = parseLines2(await readFile4(ref.path, "utf8"));
|
|
727
783
|
const meta = lines.find((l) => l.type === "session_meta");
|
|
728
784
|
const traceId = meta?.payload?.id ?? ref.sessionId;
|
|
729
785
|
const model = lines.find((l) => l.type === "turn_context")?.payload?.model ?? null;
|
|
@@ -839,7 +895,7 @@ var CodexAdapter = class {
|
|
|
839
895
|
};
|
|
840
896
|
|
|
841
897
|
// src/adapters/copilot.ts
|
|
842
|
-
import { readdir as readdir4, readFile as
|
|
898
|
+
import { readdir as readdir4, readFile as readFile5, stat as stat4 } from "fs/promises";
|
|
843
899
|
import { homedir as homedir4 } from "os";
|
|
844
900
|
import { join as join4 } from "path";
|
|
845
901
|
var SERVICE4 = "github-copilot";
|
|
@@ -884,7 +940,7 @@ var CopilotAdapter = class {
|
|
|
884
940
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
885
941
|
}
|
|
886
942
|
async parse(ref) {
|
|
887
|
-
const events = parseLines3(await
|
|
943
|
+
const events = parseLines3(await readFile5(ref.path, "utf8"));
|
|
888
944
|
const traceId = ref.sessionId;
|
|
889
945
|
const rootId = `root:${traceId}`;
|
|
890
946
|
const spans = [
|
|
@@ -965,7 +1021,7 @@ var CopilotAdapter = class {
|
|
|
965
1021
|
};
|
|
966
1022
|
|
|
967
1023
|
// src/adapters/factory.ts
|
|
968
|
-
import { readdir as readdir5, readFile as
|
|
1024
|
+
import { readdir as readdir5, readFile as readFile6, stat as stat5 } from "fs/promises";
|
|
969
1025
|
import { homedir as homedir5 } from "os";
|
|
970
1026
|
import { basename as basename4, join as join5 } from "path";
|
|
971
1027
|
var SERVICE5 = "factory";
|
|
@@ -1026,13 +1082,13 @@ var FactoryAdapter = class {
|
|
|
1026
1082
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1027
1083
|
}
|
|
1028
1084
|
async parse(ref) {
|
|
1029
|
-
const lines = parseLines4(await
|
|
1085
|
+
const lines = parseLines4(await readFile6(ref.path, "utf8"));
|
|
1030
1086
|
const start = lines.find((l) => l.type === "session_start");
|
|
1031
1087
|
const traceId = start?.id ?? ref.sessionId;
|
|
1032
1088
|
const rootId = `root:${traceId}`;
|
|
1033
1089
|
let settings = {};
|
|
1034
1090
|
try {
|
|
1035
|
-
settings = JSON.parse(await
|
|
1091
|
+
settings = JSON.parse(await readFile6(ref.path.replace(/\.jsonl$/, ".settings.json"), "utf8"));
|
|
1036
1092
|
} catch {
|
|
1037
1093
|
}
|
|
1038
1094
|
const spans = [
|
|
@@ -1126,7 +1182,7 @@ var FactoryAdapter = class {
|
|
|
1126
1182
|
};
|
|
1127
1183
|
|
|
1128
1184
|
// src/adapters/forge.ts
|
|
1129
|
-
import { readdir as readdir6, readFile as
|
|
1185
|
+
import { readdir as readdir6, readFile as readFile7, stat as stat6 } from "fs/promises";
|
|
1130
1186
|
import { homedir as homedir6 } from "os";
|
|
1131
1187
|
import { basename as basename5, join as join6 } from "path";
|
|
1132
1188
|
var SERVICE6 = "forge";
|
|
@@ -1172,7 +1228,7 @@ var ForgeAdapter = class {
|
|
|
1172
1228
|
async parse(ref) {
|
|
1173
1229
|
let dump;
|
|
1174
1230
|
try {
|
|
1175
|
-
dump = JSON.parse(await
|
|
1231
|
+
dump = JSON.parse(await readFile7(ref.path, "utf8"));
|
|
1176
1232
|
} catch {
|
|
1177
1233
|
return [];
|
|
1178
1234
|
}
|
|
@@ -1256,7 +1312,7 @@ var ForgeAdapter = class {
|
|
|
1256
1312
|
};
|
|
1257
1313
|
|
|
1258
1314
|
// src/adapters/gemini.ts
|
|
1259
|
-
import { readdir as readdir7, readFile as
|
|
1315
|
+
import { readdir as readdir7, readFile as readFile8, stat as stat7 } from "fs/promises";
|
|
1260
1316
|
import { homedir as homedir7 } from "os";
|
|
1261
1317
|
import { join as join7 } from "path";
|
|
1262
1318
|
function iso(ts, fallback) {
|
|
@@ -1321,7 +1377,7 @@ var GeminiFamilyAdapter = class {
|
|
|
1321
1377
|
async parse(ref) {
|
|
1322
1378
|
let session;
|
|
1323
1379
|
try {
|
|
1324
|
-
session = JSON.parse(await
|
|
1380
|
+
session = JSON.parse(await readFile8(ref.path, "utf8"));
|
|
1325
1381
|
} catch {
|
|
1326
1382
|
return [];
|
|
1327
1383
|
}
|
|
@@ -1415,7 +1471,7 @@ var GeminiAdapter = class extends GeminiFamilyAdapter {
|
|
|
1415
1471
|
};
|
|
1416
1472
|
|
|
1417
1473
|
// src/adapters/opencode.ts
|
|
1418
|
-
import { readdir as readdir8, readFile as
|
|
1474
|
+
import { readdir as readdir8, readFile as readFile9, stat as stat8 } from "fs/promises";
|
|
1419
1475
|
import { homedir as homedir8 } from "os";
|
|
1420
1476
|
import { join as join8 } from "path";
|
|
1421
1477
|
var SERVICE7 = "opencode";
|
|
@@ -1470,7 +1526,7 @@ var OpencodeAdapter = class {
|
|
|
1470
1526
|
const messages = [];
|
|
1471
1527
|
for (const f of msgFiles) {
|
|
1472
1528
|
try {
|
|
1473
|
-
messages.push({ msg: JSON.parse(await
|
|
1529
|
+
messages.push({ msg: JSON.parse(await readFile9(join8(ref.path, f), "utf8")), file: f });
|
|
1474
1530
|
} catch {
|
|
1475
1531
|
}
|
|
1476
1532
|
}
|
|
@@ -1498,7 +1554,7 @@ var OpencodeAdapter = class {
|
|
|
1498
1554
|
const partFiles = (await readdir8(pdir)).filter((f) => f.endsWith(".json"));
|
|
1499
1555
|
for (const pf of partFiles) {
|
|
1500
1556
|
try {
|
|
1501
|
-
parts.push(JSON.parse(await
|
|
1557
|
+
parts.push(JSON.parse(await readFile9(join8(pdir, pf), "utf8")));
|
|
1502
1558
|
} catch {
|
|
1503
1559
|
}
|
|
1504
1560
|
}
|
|
@@ -1568,7 +1624,7 @@ var OpencodeAdapter = class {
|
|
|
1568
1624
|
};
|
|
1569
1625
|
|
|
1570
1626
|
// src/adapters/pi.ts
|
|
1571
|
-
import { readdir as readdir9, readFile as
|
|
1627
|
+
import { readdir as readdir9, readFile as readFile10, stat as stat9 } from "fs/promises";
|
|
1572
1628
|
import { homedir as homedir9 } from "os";
|
|
1573
1629
|
import { basename as basename6, join as join9 } from "path";
|
|
1574
1630
|
var SERVICE8 = "pi";
|
|
@@ -1636,7 +1692,7 @@ var PiAdapter = class {
|
|
|
1636
1692
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1637
1693
|
}
|
|
1638
1694
|
async parse(ref) {
|
|
1639
|
-
const lines = parseLines5(await
|
|
1695
|
+
const lines = parseLines5(await readFile10(ref.path, "utf8"));
|
|
1640
1696
|
const sessionLine = lines.find((l) => l.type === "session");
|
|
1641
1697
|
const traceId = sessionLine?.id ?? ref.sessionId;
|
|
1642
1698
|
const rootId = `root:${traceId}`;
|
|
@@ -1737,7 +1793,7 @@ var PiAdapter = class {
|
|
|
1737
1793
|
};
|
|
1738
1794
|
|
|
1739
1795
|
// src/adapters/qwen.ts
|
|
1740
|
-
import { readdir as readdir10, readFile as
|
|
1796
|
+
import { readdir as readdir10, readFile as readFile11, stat as stat10 } from "fs/promises";
|
|
1741
1797
|
import { homedir as homedir10 } from "os";
|
|
1742
1798
|
import { basename as basename7, join as join10 } from "path";
|
|
1743
1799
|
var SERVICE9 = "qwen";
|
|
@@ -1796,7 +1852,7 @@ var QwenAdapter = class {
|
|
|
1796
1852
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1797
1853
|
}
|
|
1798
1854
|
async parse(ref) {
|
|
1799
|
-
const records = parseLines6(await
|
|
1855
|
+
const records = parseLines6(await readFile11(ref.path, "utf8"));
|
|
1800
1856
|
const traceId = records.find((r) => r.sessionId)?.sessionId ?? ref.sessionId;
|
|
1801
1857
|
const rootId = `root:${traceId}`;
|
|
1802
1858
|
const spans = [
|
|
@@ -2121,7 +2177,7 @@ async function applyRedactor(spans, redactor) {
|
|
|
2121
2177
|
|
|
2122
2178
|
// src/upload-state.ts
|
|
2123
2179
|
import { createHash } from "crypto";
|
|
2124
|
-
import { mkdir, readFile as
|
|
2180
|
+
import { mkdir, readFile as readFile12, writeFile as writeFile2 } from "fs/promises";
|
|
2125
2181
|
import { homedir as homedir11 } from "os";
|
|
2126
2182
|
import { dirname, join as join11 } from "path";
|
|
2127
2183
|
function stateFile() {
|
|
@@ -2141,7 +2197,7 @@ function sessionHash(spans) {
|
|
|
2141
2197
|
}
|
|
2142
2198
|
async function loadState() {
|
|
2143
2199
|
try {
|
|
2144
|
-
return JSON.parse(await
|
|
2200
|
+
return JSON.parse(await readFile12(stateFile(), "utf8"));
|
|
2145
2201
|
} catch {
|
|
2146
2202
|
return {};
|
|
2147
2203
|
}
|
|
@@ -2149,7 +2205,7 @@ async function loadState() {
|
|
|
2149
2205
|
async function saveState(state) {
|
|
2150
2206
|
const f = stateFile();
|
|
2151
2207
|
await mkdir(dirname(f), { recursive: true });
|
|
2152
|
-
await
|
|
2208
|
+
await writeFile2(f, `${JSON.stringify(state, null, 2)}
|
|
2153
2209
|
`, "utf8");
|
|
2154
2210
|
}
|
|
2155
2211
|
function alreadyUploaded(state, harness, sessionId, hash) {
|
|
@@ -2157,7 +2213,7 @@ function alreadyUploaded(state, harness, sessionId, hash) {
|
|
|
2157
2213
|
}
|
|
2158
2214
|
|
|
2159
2215
|
// src/upload.ts
|
|
2160
|
-
import { readFile as
|
|
2216
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
2161
2217
|
import { createRequire } from "module";
|
|
2162
2218
|
import { hostname } from "os";
|
|
2163
2219
|
import { basename as basename8, join as join12 } from "path";
|
|
@@ -2184,7 +2240,7 @@ async function planUpload(opts = {}) {
|
|
|
2184
2240
|
async function gitBranch(cwd) {
|
|
2185
2241
|
if (!cwd) return void 0;
|
|
2186
2242
|
try {
|
|
2187
|
-
const head = await
|
|
2243
|
+
const head = await readFile13(join12(cwd, ".git", "HEAD"), "utf8");
|
|
2188
2244
|
const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
|
|
2189
2245
|
return m?.[1]?.trim();
|
|
2190
2246
|
} catch {
|
|
@@ -2293,11 +2349,13 @@ async function executeUpload(plan, opts = {}) {
|
|
|
2293
2349
|
|
|
2294
2350
|
export {
|
|
2295
2351
|
span,
|
|
2352
|
+
toOpenInferenceSpan,
|
|
2296
2353
|
serializeSpans,
|
|
2297
2354
|
writeOtlpFile,
|
|
2298
2355
|
analyzeSpans,
|
|
2299
2356
|
runCommand,
|
|
2300
2357
|
commandAnalyzer,
|
|
2358
|
+
toCanonicalOpenInference,
|
|
2301
2359
|
haloAnalyzer,
|
|
2302
2360
|
runExternalAnalyzers,
|
|
2303
2361
|
commandRedactor,
|
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
selectAdapters,
|
|
15
15
|
watchSessions,
|
|
16
16
|
writeOtlpFile
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-BUD6I3ZJ.js";
|
|
18
18
|
|
|
19
19
|
// src/cli.ts
|
|
20
20
|
import { stat, writeFile } from "fs/promises";
|
|
@@ -72,7 +72,7 @@ function renderReport(result, meta) {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
lines.push("---");
|
|
75
|
-
lines.push(`OTLP artifact: \`${meta.otlpPath}\` \u2014
|
|
75
|
+
lines.push(`OTLP artifact: \`${meta.otlpPath}\` \u2014 run external engines with \`traces analyze --analyzer halo\`.`);
|
|
76
76
|
lines.push("");
|
|
77
77
|
return lines.join("\n");
|
|
78
78
|
}
|
|
@@ -148,6 +148,9 @@ function parseArgs(argv) {
|
|
|
148
148
|
case "--budget":
|
|
149
149
|
a.budget = Number(next());
|
|
150
150
|
break;
|
|
151
|
+
case "--model":
|
|
152
|
+
a.model = next();
|
|
153
|
+
break;
|
|
151
154
|
case "--interval":
|
|
152
155
|
a.interval = Number(next());
|
|
153
156
|
break;
|
|
@@ -270,6 +273,7 @@ async function cmdAnalyze(args) {
|
|
|
270
273
|
const ai = args.llm ? await buildAxService() : void 0;
|
|
271
274
|
const { otlpPath, result } = await analyzeSpans(spans, {
|
|
272
275
|
ai,
|
|
276
|
+
model: args.model,
|
|
273
277
|
budgetUsd: args.budget,
|
|
274
278
|
otlpOutPath: args.otlp,
|
|
275
279
|
log: (msg) => process.stderr.write(`${msg}
|
|
@@ -280,7 +284,7 @@ async function cmdAnalyze(args) {
|
|
|
280
284
|
${renderPipelines(pipelines)}`;
|
|
281
285
|
if (args.analyzers.length > 0 && otlpPath) {
|
|
282
286
|
const engines = args.analyzers.map(
|
|
283
|
-
(spec) => spec === "halo" ? haloAnalyzer({ defaultPrompt: args.analyzerPrompt }) : commandAnalyzer({ name: spec, command: spec, args: (p, prompt) => prompt ? [p, prompt] : [p] })
|
|
287
|
+
(spec) => spec === "halo" ? haloAnalyzer({ defaultPrompt: args.analyzerPrompt, model: args.model }) : commandAnalyzer({ name: spec, command: spec, args: (p, prompt) => prompt ? [p, prompt] : [p] })
|
|
284
288
|
);
|
|
285
289
|
const results = await runExternalAnalyzers(otlpPath, engines, { prompt: args.analyzerPrompt });
|
|
286
290
|
for (const r of results) {
|
|
@@ -384,7 +388,7 @@ function usage() {
|
|
|
384
388
|
Commands:
|
|
385
389
|
list List discovered sessions
|
|
386
390
|
analyze Run analyst suite + loop/waste pipelines, write a markdown report
|
|
387
|
-
convert Emit OTLP-JSONL only (
|
|
391
|
+
convert Emit OTLP-JSONL only (HALO: use analyze --analyzer halo)
|
|
388
392
|
watch Online observer: tail active sessions, notify on stuck loops (read-only)
|
|
389
393
|
upload Redact + upload sessions in a time window to the Tangle Intelligence Platform
|
|
390
394
|
|
|
@@ -398,6 +402,7 @@ Options:
|
|
|
398
402
|
--out <path> Write report to a file
|
|
399
403
|
--otlp <path> OTLP artifact path (also the dry-run upload preview path)
|
|
400
404
|
--llm Enable agentic RLM analysts (needs OPENAI_API_KEY / OPENAI_BASE_URL)
|
|
405
|
+
--model <id> --llm model id (e.g. a router model like glm-5.2); default is agent-eval's
|
|
401
406
|
--budget <usd> USD cap for agentic analysts
|
|
402
407
|
--analyzer <cmd> analyze: also run an external engine over the OTLP (repeatable; "halo" or any command)
|
|
403
408
|
--analyzer-prompt <p> analyze: prompt passed to external analyzers (default: diagnose)
|
package/dist/index.d.ts
CHANGED
|
@@ -14,8 +14,10 @@ export { HostedClient, createHostedClient, hostedClientFromEnv } from '@tangle-n
|
|
|
14
14
|
* Every harness adapter projects its native session log onto `OtlpSpan[]`
|
|
15
15
|
* and we serialize one span per JSONL line. This is the exact wire shape
|
|
16
16
|
* `@tangle-network/agent-eval`'s `OtlpFileTraceStore` indexes
|
|
17
|
-
* (`projectOtlpFlatLine` + `extractOtlpAttributes`)
|
|
18
|
-
*
|
|
17
|
+
* (`projectOtlpFlatLine` + `extractOtlpAttributes`). It is a flat subset, NOT
|
|
18
|
+
* canonical OpenInference (kind lives in attributes; no resource/scope; root
|
|
19
|
+
* parent is null) — external OpenInference engines like HALO need the canonical
|
|
20
|
+
* form, which `external.ts` `toCanonicalOpenInference` produces.
|
|
19
21
|
*
|
|
20
22
|
* Attribute vocabulary the downstream analysts key off:
|
|
21
23
|
* - `openinference.span.kind` → span kind (AGENT | LLM | TOOL | CHAIN)
|
|
@@ -71,7 +73,15 @@ interface SpanInput {
|
|
|
71
73
|
* so the behavioral analyst's "no LLM data" case stays honest.
|
|
72
74
|
*/
|
|
73
75
|
declare function span(input: SpanInput): OtlpSpan;
|
|
74
|
-
/**
|
|
76
|
+
/** Project an in-memory span to a COMPLETE OpenInference span: the standard shape
|
|
77
|
+
* OTel/OpenInference consumers (and HALO) expect — top-level `kind`, `resource`,
|
|
78
|
+
* `scope`, and a string `parent_span_id` ("" at the root) — while keeping the
|
|
79
|
+
* attribute vocabulary (incl. `openinference.span.kind`) our own analysts read.
|
|
80
|
+
* One artifact feeds our pipeline, HALO, and any OpenInference tool. */
|
|
81
|
+
declare function toOpenInferenceSpan(s: OtlpSpan): Record<string, unknown>;
|
|
82
|
+
/** Serialize spans to OpenInference JSONL (one complete span per line). This is a
|
|
83
|
+
* standard OpenInference representation: it feeds our analysts, HALO, and other
|
|
84
|
+
* OpenInference tools directly — no per-tool conversion. */
|
|
75
85
|
declare function serializeSpans(spans: readonly OtlpSpan[]): string;
|
|
76
86
|
/** Write spans to an OTLP-JSONL file (a temp file when no path is given). */
|
|
77
87
|
declare function writeOtlpFile(spans: readonly OtlpSpan[], outPath?: string): Promise<string>;
|
|
@@ -524,8 +534,10 @@ declare function toRuntimeStore(spans: readonly OtlpSpan[]): Promise<RuntimeTrac
|
|
|
524
534
|
* LLM, model-agnostic). Supply `ai` to add the agentic RLM kinds
|
|
525
535
|
* (failure-mode / knowledge-gap / knowledge-poisoning / improvement).
|
|
526
536
|
*
|
|
527
|
-
* The written OTLP file is the
|
|
528
|
-
*
|
|
537
|
+
* The written OTLP file is the artifact our pipeline reads. External engines can
|
|
538
|
+
* analyze it too: HALO needs canonical OpenInference, so `--analyzer halo`
|
|
539
|
+
* converts it first (see external.ts `toCanonicalOpenInference`) — analysis is
|
|
540
|
+
* never locked to one engine.
|
|
529
541
|
*/
|
|
530
542
|
|
|
531
543
|
interface AnalyzeOptions {
|
|
@@ -547,7 +559,7 @@ interface AnalyzeOptions {
|
|
|
547
559
|
log?: (msg: string, fields?: Record<string, unknown>) => void;
|
|
548
560
|
}
|
|
549
561
|
interface AnalyzeResult {
|
|
550
|
-
/** Path to the OTLP-JSONL artifact (
|
|
562
|
+
/** Path to the OTLP-JSONL artifact (convert to canonical for HALO). */
|
|
551
563
|
otlpPath: string;
|
|
552
564
|
result: Awaited<ReturnType<ReturnType<typeof buildDefaultAnalystRegistry>['run']>>;
|
|
553
565
|
}
|
|
@@ -607,11 +619,24 @@ declare function commandAnalyzer(spec: {
|
|
|
607
619
|
parse?: (stdout: string) => AnalystFinding[] | undefined;
|
|
608
620
|
timeoutMs?: number;
|
|
609
621
|
}): ExternalAnalyzer;
|
|
610
|
-
/**
|
|
611
|
-
*
|
|
622
|
+
/** Convert our OTLP-JSONL (the flat shape our pipeline reads) into the canonical
|
|
623
|
+
* OpenInference span schema HALO's `SpanRecord` requires: top-level `kind`,
|
|
624
|
+
* `resource`, `scope`, and a string (never null) `parent_span_id`. Our own
|
|
625
|
+
* writer keeps the simpler shape for the agent-eval reader; this is the bridge
|
|
626
|
+
* to OpenInference consumers. Input + output are JSONL strings (one span/line). */
|
|
627
|
+
declare function toCanonicalOpenInference(otlpJsonl: string, opts?: {
|
|
628
|
+
service?: string;
|
|
629
|
+
scopeName?: string;
|
|
630
|
+
}): string;
|
|
631
|
+
/** HALO (github.com/context-labs/halo) over the OTLP artifact. HALO requires
|
|
632
|
+
* canonical OpenInference, so this converts our OTLP to a `<path>.halo.jsonl`
|
|
633
|
+
* first, then runs `halo <file> -p <prompt> [-m <model>]`. Install HALO yourself
|
|
634
|
+
* and configure its LLM provider (it uses the OpenAI client — `OPENAI_BASE_URL`
|
|
635
|
+
* / `OPENAI_API_KEY`); this just drives it. */
|
|
612
636
|
declare function haloAnalyzer(opts?: {
|
|
613
637
|
command?: string;
|
|
614
638
|
defaultPrompt?: string;
|
|
639
|
+
model?: string;
|
|
615
640
|
timeoutMs?: number;
|
|
616
641
|
}): ExternalAnalyzer;
|
|
617
642
|
/** Run external analyzers over one OTLP artifact, concurrently. Never throws —
|
|
@@ -850,4 +875,4 @@ interface ExecuteOptions {
|
|
|
850
875
|
/** Send the NEW items (or, on dryRun, write the redacted OTLP that would send). */
|
|
851
876
|
declare function executeUpload(plan: UploadPlan, opts?: ExecuteOptions): Promise<UploadResult>;
|
|
852
877
|
|
|
853
|
-
export { ATTR, AmpAdapter, type AnalyzeOptions, type AnalyzeResult, CODING_REDACTION_RULES, CONTENT_CAP, ClaudeAdapter, CodexAdapter, type CollectOptions, CopilotAdapter, DEFAULT_HARNESS, type ExecuteOptions, type ExternalAnalysisResult, type ExternalAnalyzer, FactoryAdapter, ForgeAdapter, GeminiAdapter, type HarnessTraceAdapter, INGEST_SOURCE_CLI, type LocateOptions, type ObservedLoop, type ObserverOptions, OpencodeAdapter, type OtlpSpan, type OtlpSpanKind, type OtlpStatusCode, PiAdapter, type PipelineOptions, type PipelineReport, type PlanOptions, QwenAdapter, type Redactor, type RunResult, type RuntimeTrace, type ScanOptions, type ScannedSession, type SessionBatch, type SessionRef, type SpanInput, type SpanRedaction, TRACES_REDACTION_RULES, type UploadBackend, type UploadItem, type UploadPlan, type UploadRecord, type UploadResult, type UploadState, alreadyUploaded, analyzeSpans, applyRedactor, capText, collectSessions, commandAnalyzer, commandRedactor, executeUpload, haloAnalyzer, knownHarnesses, listAdapters, loadState, parseIsoToEpochMs, parseSince, planUpload, redactSpans, resolveAdapter, runCommand, runExternalAnalyzers, runPipelines, saveState, scanSessions, selectAdapters, serializeSpans, sessionHash, span, toRuntimeStore, toTraceSpanEvents, uploadKey, userPromptSpan, watchSessions, writeOtlpFile };
|
|
878
|
+
export { ATTR, AmpAdapter, type AnalyzeOptions, type AnalyzeResult, CODING_REDACTION_RULES, CONTENT_CAP, ClaudeAdapter, CodexAdapter, type CollectOptions, CopilotAdapter, DEFAULT_HARNESS, type ExecuteOptions, type ExternalAnalysisResult, type ExternalAnalyzer, FactoryAdapter, ForgeAdapter, GeminiAdapter, type HarnessTraceAdapter, INGEST_SOURCE_CLI, type LocateOptions, type ObservedLoop, type ObserverOptions, OpencodeAdapter, type OtlpSpan, type OtlpSpanKind, type OtlpStatusCode, PiAdapter, type PipelineOptions, type PipelineReport, type PlanOptions, QwenAdapter, type Redactor, type RunResult, type RuntimeTrace, type ScanOptions, type ScannedSession, type SessionBatch, type SessionRef, type SpanInput, type SpanRedaction, TRACES_REDACTION_RULES, type UploadBackend, type UploadItem, type UploadPlan, type UploadRecord, type UploadResult, type UploadState, alreadyUploaded, analyzeSpans, applyRedactor, capText, collectSessions, commandAnalyzer, commandRedactor, executeUpload, haloAnalyzer, knownHarnesses, listAdapters, loadState, parseIsoToEpochMs, parseSince, planUpload, redactSpans, resolveAdapter, runCommand, runExternalAnalyzers, runPipelines, saveState, scanSessions, selectAdapters, serializeSpans, sessionHash, span, toCanonicalOpenInference, toOpenInferenceSpan, toRuntimeStore, toTraceSpanEvents, uploadKey, userPromptSpan, watchSessions, writeOtlpFile };
|
package/dist/index.js
CHANGED
|
@@ -40,13 +40,15 @@ import {
|
|
|
40
40
|
serializeSpans,
|
|
41
41
|
sessionHash,
|
|
42
42
|
span,
|
|
43
|
+
toCanonicalOpenInference,
|
|
44
|
+
toOpenInferenceSpan,
|
|
43
45
|
toRuntimeStore,
|
|
44
46
|
toTraceSpanEvents,
|
|
45
47
|
uploadKey,
|
|
46
48
|
userPromptSpan,
|
|
47
49
|
watchSessions,
|
|
48
50
|
writeOtlpFile
|
|
49
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-BUD6I3ZJ.js";
|
|
50
52
|
|
|
51
53
|
// src/collect.ts
|
|
52
54
|
async function collectSessions(opts = {}) {
|
|
@@ -121,6 +123,8 @@ export {
|
|
|
121
123
|
serializeSpans,
|
|
122
124
|
sessionHash,
|
|
123
125
|
span,
|
|
126
|
+
toCanonicalOpenInference,
|
|
127
|
+
toOpenInferenceSpan,
|
|
124
128
|
toRuntimeStore,
|
|
125
129
|
toTraceSpanEvents,
|
|
126
130
|
uploadKey,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/traces",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Point it at your coding-agent session traces (Claude Code, Codex, OpenCode, Gemini, Pi, …) and get failure-mode + efficiency findings. CLI + SDK over the @tangle-network/agent-eval analyst suite — observe live sessions, run your own analysts, redact, and upload to the Tangle Intelligence Platform.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|