@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 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 an **OTLP-JSONL artifact** that feeds [HALO](https://github.com/context-labs/halo) (`halo spans.jsonl -p "diagnose"`) — analysis is never locked to one engine.
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: writeFile2 } = await import("fs/promises");
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 writeFile2(path, serializeSpans(spans), "utf8");
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 commandAnalyzer({
189
+ return {
147
190
  name: "halo",
148
- command: opts.command ?? "halo",
149
- args: (otlpPath, prompt) => [otlpPath, "-p", prompt ?? defaultPrompt],
150
- timeoutMs: opts.timeoutMs
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 readFile(ref.path, "utf8"));
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 readFile2, stat as stat2 } from "fs/promises";
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 readFile2(ref.path, "utf8");
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 readFile2(join2(subDir, `${hash}.meta.json`), "utf8"));
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 readFile2(join2(subDir, f), "utf8");
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 readFile3, stat as stat3 } from "fs/promises";
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 readFile3(path, "utf8").catch(() => "")).split("\n", 1)[0] ?? "";
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 readFile3(ref.path, "utf8"));
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 readFile4, stat as stat4 } from "fs/promises";
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 readFile4(ref.path, "utf8"));
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 readFile5, stat as stat5 } from "fs/promises";
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 readFile5(ref.path, "utf8"));
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 readFile5(ref.path.replace(/\.jsonl$/, ".settings.json"), "utf8"));
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 readFile6, stat as stat6 } from "fs/promises";
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 readFile6(ref.path, "utf8"));
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 readFile7, stat as stat7 } from "fs/promises";
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 readFile7(ref.path, "utf8"));
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 readFile8, stat as stat8 } from "fs/promises";
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 readFile8(join8(ref.path, f), "utf8")), file: f });
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 readFile8(join8(pdir, pf), "utf8")));
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 readFile9, stat as stat9 } from "fs/promises";
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 readFile9(ref.path, "utf8"));
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 readFile10, stat as stat10 } from "fs/promises";
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 readFile10(ref.path, "utf8"));
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 readFile11, writeFile } from "fs/promises";
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 readFile11(stateFile(), "utf8"));
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 writeFile(f, `${JSON.stringify(state, null, 2)}
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 readFile12 } from "fs/promises";
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 readFile12(join12(cwd, ".git", "HEAD"), "utf8");
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-6TVW7PL6.js";
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 also runnable with \`halo ${meta.otlpPath} -p "diagnose"\`.`);
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 (also feeds HALO)
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`) AND the OpenInference
18
- * OTLP form HALO consumes — so one emitted file feeds both engines.
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
- /** Serialize spans to OTLP-JSONL (one span per line, trailing newline). */
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 canonical artifact — the same file feeds HALO
528
- * (`halo <file> -p "diagnose"`), so analysis is never locked to one engine.
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 (feeds HALO too). */
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
- /** HALO (github.com/context-labs/halo) over the OTLP artifact: `halo <file> -p <prompt>`.
611
- * Install HALO yourself; this just drives it. */
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-6TVW7PL6.js";
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.6.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",