@tangle-network/traces 0.6.0 → 0.7.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 an **OTLP-JSONL artifact**, and you can run external engines like [HALO](https://github.com/context-labs/halo) over it with `--analyzer halo` (traces converts our spans to the canonical OpenInference shape HALO needs) — 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
+ HALO needs canonical OpenInference, so `--analyzer halo` converts our spans automatically (`toCanonicalOpenInference`). 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
@@ -31,11 +31,11 @@ function serializeSpans(spans) {
31
31
  `;
32
32
  }
33
33
  async function writeOtlpFile(spans, outPath) {
34
- const { mkdtemp, writeFile: writeFile2 } = await import("fs/promises");
34
+ const { mkdtemp, writeFile: writeFile3 } = await import("fs/promises");
35
35
  const { tmpdir } = await import("os");
36
36
  const { join: join13 } = await import("path");
37
37
  const path = outPath ?? join13(await mkdtemp(join13(tmpdir(), "traces-")), "spans.otlp.jsonl");
38
- await writeFile2(path, serializeSpans(spans), "utf8");
38
+ await writeFile3(path, serializeSpans(spans), "utf8");
39
39
  return path;
40
40
  }
41
41
 
@@ -72,6 +72,7 @@ async function analyzeSpans(spans, opts = {}) {
72
72
 
73
73
  // src/external.ts
74
74
  import { spawn } from "child_process";
75
+ import { readFile, writeFile } from "fs/promises";
75
76
  function runCommand(command, args = [], opts = {}) {
76
77
  const timeoutMs = opts.timeoutMs ?? 12e4;
77
78
  const maxBuffer = opts.maxBuffer ?? 32 * 1024 * 1024;
@@ -141,14 +142,50 @@ function commandAnalyzer(spec) {
141
142
  }
142
143
  };
143
144
  }
145
+ function toCanonicalOpenInference(otlpJsonl, opts = {}) {
146
+ return otlpJsonl.split("\n").filter((l) => l.trim()).map((line) => {
147
+ const s = JSON.parse(line);
148
+ const a = s.attributes ?? {};
149
+ const resourceAttrs = { "service.name": a["service.name"] ?? opts.service ?? "traces" };
150
+ if (a["agent.name"]) resourceAttrs["agent.name"] = a["agent.name"];
151
+ return JSON.stringify({
152
+ trace_id: s.trace_id,
153
+ span_id: s.span_id,
154
+ parent_span_id: s.parent_span_id ?? "",
155
+ trace_state: "",
156
+ name: s.name,
157
+ kind: a["openinference.span.kind"] ?? "CHAIN",
158
+ start_time: s.start_time,
159
+ end_time: s.end_time,
160
+ status: { code: s.status?.code ?? "OK", message: s.status?.message ?? "" },
161
+ resource: { attributes: resourceAttrs },
162
+ scope: { name: opts.scopeName ?? "tangle-traces", version: "" },
163
+ attributes: a
164
+ });
165
+ }).join("\n");
166
+ }
144
167
  function haloAnalyzer(opts = {}) {
168
+ const command = opts.command ?? "halo";
145
169
  const defaultPrompt = opts.defaultPrompt ?? "diagnose";
146
- return commandAnalyzer({
170
+ return {
147
171
  name: "halo",
148
- command: opts.command ?? "halo",
149
- args: (otlpPath, prompt) => [otlpPath, "-p", prompt ?? defaultPrompt],
150
- timeoutMs: opts.timeoutMs
151
- });
172
+ async analyze(otlpPath, o = {}) {
173
+ try {
174
+ const canonical = toCanonicalOpenInference(await readFile(otlpPath, "utf8"));
175
+ const canonPath = `${otlpPath}.halo.jsonl`;
176
+ await writeFile(canonPath, canonical);
177
+ const args = [canonPath, "-p", o.prompt ?? defaultPrompt];
178
+ if (opts.model) args.push("-m", opts.model);
179
+ const res = await runCommand(command, args, { signal: o.signal, timeoutMs: opts.timeoutMs });
180
+ if (res.code !== 0) {
181
+ return { analyzer: "halo", ok: false, output: res.stdout, error: res.stderr.trim() || `exit ${res.code}` };
182
+ }
183
+ return { analyzer: "halo", ok: true, output: res.stdout.trim() };
184
+ } catch (e) {
185
+ return { analyzer: "halo", ok: false, output: "", error: e instanceof Error ? e.message : String(e) };
186
+ }
187
+ }
188
+ };
152
189
  }
153
190
  function runExternalAnalyzers(otlpPath, analyzers, opts = {}) {
154
191
  return Promise.all(analyzers.map((a) => a.analyze(otlpPath, opts)));
@@ -270,7 +307,7 @@ function userPromptSpan(o) {
270
307
  }
271
308
 
272
309
  // src/adapters/amp.ts
273
- import { readdir, readFile, stat } from "fs/promises";
310
+ import { readdir, readFile as readFile2, stat } from "fs/promises";
274
311
  import { homedir } from "os";
275
312
  import { basename, join } from "path";
276
313
  var SERVICE = "amp";
@@ -312,7 +349,7 @@ var AmpAdapter = class {
312
349
  async parse(ref) {
313
350
  let thread;
314
351
  try {
315
- thread = JSON.parse(await readFile(ref.path, "utf8"));
352
+ thread = JSON.parse(await readFile2(ref.path, "utf8"));
316
353
  } catch {
317
354
  return [];
318
355
  }
@@ -396,7 +433,7 @@ var AmpAdapter = class {
396
433
  };
397
434
 
398
435
  // src/adapters/claude.ts
399
- import { readdir as readdir2, readFile as readFile2, stat as stat2 } from "fs/promises";
436
+ import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "fs/promises";
400
437
  import { homedir as homedir2 } from "os";
401
438
  import { basename as basename2, join as join2 } from "path";
402
439
  var SERVICE2 = "claude-code";
@@ -557,7 +594,7 @@ var ClaudeAdapter = class {
557
594
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
558
595
  }
559
596
  async parse(ref) {
560
- const raw = await readFile2(ref.path, "utf8");
597
+ const raw = await readFile3(ref.path, "utf8");
561
598
  const events = parseLines(raw);
562
599
  const traceId = events.find((e) => e.sessionId)?.sessionId ?? ref.sessionId;
563
600
  const rootId = `root:${traceId}`;
@@ -600,13 +637,13 @@ var ClaudeAdapter = class {
600
637
  const hash = basename2(f, ".jsonl");
601
638
  let meta = {};
602
639
  try {
603
- meta = JSON.parse(await readFile2(join2(subDir, `${hash}.meta.json`), "utf8"));
640
+ meta = JSON.parse(await readFile3(join2(subDir, `${hash}.meta.json`), "utf8"));
604
641
  } catch {
605
642
  }
606
643
  const parent = meta.toolUseId && main.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
607
644
  let raw;
608
645
  try {
609
- raw = await readFile2(join2(subDir, f), "utf8");
646
+ raw = await readFile3(join2(subDir, f), "utf8");
610
647
  } catch {
611
648
  continue;
612
649
  }
@@ -624,7 +661,7 @@ var ClaudeAdapter = class {
624
661
  };
625
662
 
626
663
  // src/adapters/codex.ts
627
- import { readdir as readdir3, readFile as readFile3, stat as stat3 } from "fs/promises";
664
+ import { readdir as readdir3, readFile as readFile4, stat as stat3 } from "fs/promises";
628
665
  import { homedir as homedir3 } from "os";
629
666
  import { basename as basename3, join as join3 } from "path";
630
667
  var SERVICE3 = "codex";
@@ -708,7 +745,7 @@ var CodexAdapter = class {
708
745
  continue;
709
746
  }
710
747
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
711
- const head = (await readFile3(path, "utf8").catch(() => "")).split("\n", 1)[0] ?? "";
748
+ const head = (await readFile4(path, "utf8").catch(() => "")).split("\n", 1)[0] ?? "";
712
749
  let cwd = null;
713
750
  let id = basename3(path).replace(/^rollout-[\dT-]+-/, "").replace(/\.jsonl$/, "");
714
751
  try {
@@ -723,7 +760,7 @@ var CodexAdapter = class {
723
760
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
724
761
  }
725
762
  async parse(ref) {
726
- const lines = parseLines2(await readFile3(ref.path, "utf8"));
763
+ const lines = parseLines2(await readFile4(ref.path, "utf8"));
727
764
  const meta = lines.find((l) => l.type === "session_meta");
728
765
  const traceId = meta?.payload?.id ?? ref.sessionId;
729
766
  const model = lines.find((l) => l.type === "turn_context")?.payload?.model ?? null;
@@ -839,7 +876,7 @@ var CodexAdapter = class {
839
876
  };
840
877
 
841
878
  // src/adapters/copilot.ts
842
- import { readdir as readdir4, readFile as readFile4, stat as stat4 } from "fs/promises";
879
+ import { readdir as readdir4, readFile as readFile5, stat as stat4 } from "fs/promises";
843
880
  import { homedir as homedir4 } from "os";
844
881
  import { join as join4 } from "path";
845
882
  var SERVICE4 = "github-copilot";
@@ -884,7 +921,7 @@ var CopilotAdapter = class {
884
921
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
885
922
  }
886
923
  async parse(ref) {
887
- const events = parseLines3(await readFile4(ref.path, "utf8"));
924
+ const events = parseLines3(await readFile5(ref.path, "utf8"));
888
925
  const traceId = ref.sessionId;
889
926
  const rootId = `root:${traceId}`;
890
927
  const spans = [
@@ -965,7 +1002,7 @@ var CopilotAdapter = class {
965
1002
  };
966
1003
 
967
1004
  // src/adapters/factory.ts
968
- import { readdir as readdir5, readFile as readFile5, stat as stat5 } from "fs/promises";
1005
+ import { readdir as readdir5, readFile as readFile6, stat as stat5 } from "fs/promises";
969
1006
  import { homedir as homedir5 } from "os";
970
1007
  import { basename as basename4, join as join5 } from "path";
971
1008
  var SERVICE5 = "factory";
@@ -1026,13 +1063,13 @@ var FactoryAdapter = class {
1026
1063
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1027
1064
  }
1028
1065
  async parse(ref) {
1029
- const lines = parseLines4(await readFile5(ref.path, "utf8"));
1066
+ const lines = parseLines4(await readFile6(ref.path, "utf8"));
1030
1067
  const start = lines.find((l) => l.type === "session_start");
1031
1068
  const traceId = start?.id ?? ref.sessionId;
1032
1069
  const rootId = `root:${traceId}`;
1033
1070
  let settings = {};
1034
1071
  try {
1035
- settings = JSON.parse(await readFile5(ref.path.replace(/\.jsonl$/, ".settings.json"), "utf8"));
1072
+ settings = JSON.parse(await readFile6(ref.path.replace(/\.jsonl$/, ".settings.json"), "utf8"));
1036
1073
  } catch {
1037
1074
  }
1038
1075
  const spans = [
@@ -1126,7 +1163,7 @@ var FactoryAdapter = class {
1126
1163
  };
1127
1164
 
1128
1165
  // src/adapters/forge.ts
1129
- import { readdir as readdir6, readFile as readFile6, stat as stat6 } from "fs/promises";
1166
+ import { readdir as readdir6, readFile as readFile7, stat as stat6 } from "fs/promises";
1130
1167
  import { homedir as homedir6 } from "os";
1131
1168
  import { basename as basename5, join as join6 } from "path";
1132
1169
  var SERVICE6 = "forge";
@@ -1172,7 +1209,7 @@ var ForgeAdapter = class {
1172
1209
  async parse(ref) {
1173
1210
  let dump;
1174
1211
  try {
1175
- dump = JSON.parse(await readFile6(ref.path, "utf8"));
1212
+ dump = JSON.parse(await readFile7(ref.path, "utf8"));
1176
1213
  } catch {
1177
1214
  return [];
1178
1215
  }
@@ -1256,7 +1293,7 @@ var ForgeAdapter = class {
1256
1293
  };
1257
1294
 
1258
1295
  // src/adapters/gemini.ts
1259
- import { readdir as readdir7, readFile as readFile7, stat as stat7 } from "fs/promises";
1296
+ import { readdir as readdir7, readFile as readFile8, stat as stat7 } from "fs/promises";
1260
1297
  import { homedir as homedir7 } from "os";
1261
1298
  import { join as join7 } from "path";
1262
1299
  function iso(ts, fallback) {
@@ -1321,7 +1358,7 @@ var GeminiFamilyAdapter = class {
1321
1358
  async parse(ref) {
1322
1359
  let session;
1323
1360
  try {
1324
- session = JSON.parse(await readFile7(ref.path, "utf8"));
1361
+ session = JSON.parse(await readFile8(ref.path, "utf8"));
1325
1362
  } catch {
1326
1363
  return [];
1327
1364
  }
@@ -1415,7 +1452,7 @@ var GeminiAdapter = class extends GeminiFamilyAdapter {
1415
1452
  };
1416
1453
 
1417
1454
  // src/adapters/opencode.ts
1418
- import { readdir as readdir8, readFile as readFile8, stat as stat8 } from "fs/promises";
1455
+ import { readdir as readdir8, readFile as readFile9, stat as stat8 } from "fs/promises";
1419
1456
  import { homedir as homedir8 } from "os";
1420
1457
  import { join as join8 } from "path";
1421
1458
  var SERVICE7 = "opencode";
@@ -1470,7 +1507,7 @@ var OpencodeAdapter = class {
1470
1507
  const messages = [];
1471
1508
  for (const f of msgFiles) {
1472
1509
  try {
1473
- messages.push({ msg: JSON.parse(await readFile8(join8(ref.path, f), "utf8")), file: f });
1510
+ messages.push({ msg: JSON.parse(await readFile9(join8(ref.path, f), "utf8")), file: f });
1474
1511
  } catch {
1475
1512
  }
1476
1513
  }
@@ -1498,7 +1535,7 @@ var OpencodeAdapter = class {
1498
1535
  const partFiles = (await readdir8(pdir)).filter((f) => f.endsWith(".json"));
1499
1536
  for (const pf of partFiles) {
1500
1537
  try {
1501
- parts.push(JSON.parse(await readFile8(join8(pdir, pf), "utf8")));
1538
+ parts.push(JSON.parse(await readFile9(join8(pdir, pf), "utf8")));
1502
1539
  } catch {
1503
1540
  }
1504
1541
  }
@@ -1568,7 +1605,7 @@ var OpencodeAdapter = class {
1568
1605
  };
1569
1606
 
1570
1607
  // src/adapters/pi.ts
1571
- import { readdir as readdir9, readFile as readFile9, stat as stat9 } from "fs/promises";
1608
+ import { readdir as readdir9, readFile as readFile10, stat as stat9 } from "fs/promises";
1572
1609
  import { homedir as homedir9 } from "os";
1573
1610
  import { basename as basename6, join as join9 } from "path";
1574
1611
  var SERVICE8 = "pi";
@@ -1636,7 +1673,7 @@ var PiAdapter = class {
1636
1673
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1637
1674
  }
1638
1675
  async parse(ref) {
1639
- const lines = parseLines5(await readFile9(ref.path, "utf8"));
1676
+ const lines = parseLines5(await readFile10(ref.path, "utf8"));
1640
1677
  const sessionLine = lines.find((l) => l.type === "session");
1641
1678
  const traceId = sessionLine?.id ?? ref.sessionId;
1642
1679
  const rootId = `root:${traceId}`;
@@ -1737,7 +1774,7 @@ var PiAdapter = class {
1737
1774
  };
1738
1775
 
1739
1776
  // src/adapters/qwen.ts
1740
- import { readdir as readdir10, readFile as readFile10, stat as stat10 } from "fs/promises";
1777
+ import { readdir as readdir10, readFile as readFile11, stat as stat10 } from "fs/promises";
1741
1778
  import { homedir as homedir10 } from "os";
1742
1779
  import { basename as basename7, join as join10 } from "path";
1743
1780
  var SERVICE9 = "qwen";
@@ -1796,7 +1833,7 @@ var QwenAdapter = class {
1796
1833
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1797
1834
  }
1798
1835
  async parse(ref) {
1799
- const records = parseLines6(await readFile10(ref.path, "utf8"));
1836
+ const records = parseLines6(await readFile11(ref.path, "utf8"));
1800
1837
  const traceId = records.find((r) => r.sessionId)?.sessionId ?? ref.sessionId;
1801
1838
  const rootId = `root:${traceId}`;
1802
1839
  const spans = [
@@ -2121,7 +2158,7 @@ async function applyRedactor(spans, redactor) {
2121
2158
 
2122
2159
  // src/upload-state.ts
2123
2160
  import { createHash } from "crypto";
2124
- import { mkdir, readFile as readFile11, writeFile } from "fs/promises";
2161
+ import { mkdir, readFile as readFile12, writeFile as writeFile2 } from "fs/promises";
2125
2162
  import { homedir as homedir11 } from "os";
2126
2163
  import { dirname, join as join11 } from "path";
2127
2164
  function stateFile() {
@@ -2141,7 +2178,7 @@ function sessionHash(spans) {
2141
2178
  }
2142
2179
  async function loadState() {
2143
2180
  try {
2144
- return JSON.parse(await readFile11(stateFile(), "utf8"));
2181
+ return JSON.parse(await readFile12(stateFile(), "utf8"));
2145
2182
  } catch {
2146
2183
  return {};
2147
2184
  }
@@ -2149,7 +2186,7 @@ async function loadState() {
2149
2186
  async function saveState(state) {
2150
2187
  const f = stateFile();
2151
2188
  await mkdir(dirname(f), { recursive: true });
2152
- await writeFile(f, `${JSON.stringify(state, null, 2)}
2189
+ await writeFile2(f, `${JSON.stringify(state, null, 2)}
2153
2190
  `, "utf8");
2154
2191
  }
2155
2192
  function alreadyUploaded(state, harness, sessionId, hash) {
@@ -2157,7 +2194,7 @@ function alreadyUploaded(state, harness, sessionId, hash) {
2157
2194
  }
2158
2195
 
2159
2196
  // src/upload.ts
2160
- import { readFile as readFile12 } from "fs/promises";
2197
+ import { readFile as readFile13 } from "fs/promises";
2161
2198
  import { createRequire } from "module";
2162
2199
  import { hostname } from "os";
2163
2200
  import { basename as basename8, join as join12 } from "path";
@@ -2184,7 +2221,7 @@ async function planUpload(opts = {}) {
2184
2221
  async function gitBranch(cwd) {
2185
2222
  if (!cwd) return void 0;
2186
2223
  try {
2187
- const head = await readFile12(join12(cwd, ".git", "HEAD"), "utf8");
2224
+ const head = await readFile13(join12(cwd, ".git", "HEAD"), "utf8");
2188
2225
  const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
2189
2226
  return m?.[1]?.trim();
2190
2227
  } catch {
@@ -2298,6 +2335,7 @@ export {
2298
2335
  analyzeSpans,
2299
2336
  runCommand,
2300
2337
  commandAnalyzer,
2338
+ toCanonicalOpenInference,
2301
2339
  haloAnalyzer,
2302
2340
  runExternalAnalyzers,
2303
2341
  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-Q56AP2EY.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)
@@ -524,8 +526,10 @@ declare function toRuntimeStore(spans: readonly OtlpSpan[]): Promise<RuntimeTrac
524
526
  * LLM, model-agnostic). Supply `ai` to add the agentic RLM kinds
525
527
  * (failure-mode / knowledge-gap / knowledge-poisoning / improvement).
526
528
  *
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.
529
+ * The written OTLP file is the artifact our pipeline reads. External engines can
530
+ * analyze it too: HALO needs canonical OpenInference, so `--analyzer halo`
531
+ * converts it first (see external.ts `toCanonicalOpenInference`) — analysis is
532
+ * never locked to one engine.
529
533
  */
530
534
 
531
535
  interface AnalyzeOptions {
@@ -547,7 +551,7 @@ interface AnalyzeOptions {
547
551
  log?: (msg: string, fields?: Record<string, unknown>) => void;
548
552
  }
549
553
  interface AnalyzeResult {
550
- /** Path to the OTLP-JSONL artifact (feeds HALO too). */
554
+ /** Path to the OTLP-JSONL artifact (convert to canonical for HALO). */
551
555
  otlpPath: string;
552
556
  result: Awaited<ReturnType<ReturnType<typeof buildDefaultAnalystRegistry>['run']>>;
553
557
  }
@@ -607,11 +611,24 @@ declare function commandAnalyzer(spec: {
607
611
  parse?: (stdout: string) => AnalystFinding[] | undefined;
608
612
  timeoutMs?: number;
609
613
  }): ExternalAnalyzer;
610
- /** HALO (github.com/context-labs/halo) over the OTLP artifact: `halo <file> -p <prompt>`.
611
- * Install HALO yourself; this just drives it. */
614
+ /** Convert our OTLP-JSONL (the flat shape our pipeline reads) into the canonical
615
+ * OpenInference span schema HALO's `SpanRecord` requires: top-level `kind`,
616
+ * `resource`, `scope`, and a string (never null) `parent_span_id`. Our own
617
+ * writer keeps the simpler shape for the agent-eval reader; this is the bridge
618
+ * to OpenInference consumers. Input + output are JSONL strings (one span/line). */
619
+ declare function toCanonicalOpenInference(otlpJsonl: string, opts?: {
620
+ service?: string;
621
+ scopeName?: string;
622
+ }): string;
623
+ /** HALO (github.com/context-labs/halo) over the OTLP artifact. HALO requires
624
+ * canonical OpenInference, so this converts our OTLP to a `<path>.halo.jsonl`
625
+ * first, then runs `halo <file> -p <prompt> [-m <model>]`. Install HALO yourself
626
+ * and configure its LLM provider (it uses the OpenAI client — `OPENAI_BASE_URL`
627
+ * / `OPENAI_API_KEY`); this just drives it. */
612
628
  declare function haloAnalyzer(opts?: {
613
629
  command?: string;
614
630
  defaultPrompt?: string;
631
+ model?: string;
615
632
  timeoutMs?: number;
616
633
  }): ExternalAnalyzer;
617
634
  /** Run external analyzers over one OTLP artifact, concurrently. Never throws —
@@ -850,4 +867,4 @@ interface ExecuteOptions {
850
867
  /** Send the NEW items (or, on dryRun, write the redacted OTLP that would send). */
851
868
  declare function executeUpload(plan: UploadPlan, opts?: ExecuteOptions): Promise<UploadResult>;
852
869
 
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 };
870
+ 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, toRuntimeStore, toTraceSpanEvents, uploadKey, userPromptSpan, watchSessions, writeOtlpFile };
package/dist/index.js CHANGED
@@ -40,13 +40,14 @@ import {
40
40
  serializeSpans,
41
41
  sessionHash,
42
42
  span,
43
+ toCanonicalOpenInference,
43
44
  toRuntimeStore,
44
45
  toTraceSpanEvents,
45
46
  uploadKey,
46
47
  userPromptSpan,
47
48
  watchSessions,
48
49
  writeOtlpFile
49
- } from "./chunk-6TVW7PL6.js";
50
+ } from "./chunk-Q56AP2EY.js";
50
51
 
51
52
  // src/collect.ts
52
53
  async function collectSessions(opts = {}) {
@@ -121,6 +122,7 @@ export {
121
122
  serializeSpans,
122
123
  sessionHash,
123
124
  span,
125
+ toCanonicalOpenInference,
124
126
  toRuntimeStore,
125
127
  toTraceSpanEvents,
126
128
  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.7.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",