@tangle-network/traces 0.4.1 → 0.6.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
@@ -18,6 +18,7 @@ It reads the transcripts your harness leaves on disk, reconstructs the run as sp
18
18
  - [Supported harnesses](#supported-harnesses)
19
19
  - [CLI reference](#cli-reference)
20
20
  - [Upload to the Intelligence Platform](#upload-to-the-intelligence-platform)
21
+ - [External engines (bring your own)](#external-engines-bring-your-own)
21
22
  - [Library (SDK)](#library-sdk)
22
23
  - [Examples](#examples)
23
24
  - [Develop](#develop)
@@ -102,19 +103,56 @@ traces upload --since 24h # upload last day to the Inte
102
103
  | `--llm` / `--budget <usd>` | Enable agentic analysts (needs `OPENAI_API_KEY`) / cap their spend |
103
104
  | `--interval <s>` / `--window <m>` | `watch`: poll seconds (default 5) / active-session window minutes (default 30) |
104
105
  | `--min-loop <n>` | Identical repeated calls before flagging a loop (default 3) |
106
+ | `--no-content` | `upload`: send metadata only — strip all prompt/response text |
105
107
  | `--dry-run` / `--yes` | `upload`: preview without sending / skip the confirm prompt |
106
108
 
107
109
  ## Upload to the Intelligence Platform
108
110
 
109
- `upload` **redacts PII/secrets locally before anything leaves the machine**, dedups against already-uploaded sessions, and tags each with metadata (harness, cwd, git branch, host).
111
+ `upload` **redacts locally before anything leaves the machine**, dedups against already-uploaded sessions, and tags each with metadata (harness, cwd, git branch, host).
110
112
 
111
113
  ```bash
112
- traces upload --since 24h --dry-run # see exactly what would be sent — no network
113
- traces upload --since 24h # send it
114
+ traces upload --since 24h --dry-run # see exactly what would be sent — no network
115
+ traces upload --since 24h --no-content # send metadata only — drop all prompt/response text
116
+ traces upload --since 24h # send it
114
117
  ```
115
118
 
116
119
  It needs `TANGLE_INGEST_URL` (or `TANGLE_ORCHESTRATOR_URL`), `TANGLE_INGEST_API_KEY` (or `TANGLE_API_KEY`), and `TANGLE_TENANT_ID`. Without them, `--dry-run` still works fully.
117
120
 
121
+ ### Redaction scope — read this before uploading prose
122
+
123
+ Redaction is **best-effort regex** for *structured* secrets and credentials: API keys, GitHub/cloud tokens, JWTs, bearer headers, private-key blocks, `KEY=secret` assignments, and credentials embedded in URLs. It runs over every span attribute, including the captured prompt/response text.
124
+
125
+ It does **not** catch free-form PII — names, postal addresses, phone numbers in prose — which needs a context-aware model. Three postures, strongest first:
126
+
127
+ 1. **`--no-content`** — upload metadata only (tool calls, tokens, timing, loop signal); no prose leaves the machine.
128
+ 2. Run an ML PII scrubber (e.g. [`openai/privacy-filter`](https://github.com/openai/privacy-filter)) on the platform ingest side as defense-in-depth.
129
+ 3. Default — regex redaction of structured secrets.
130
+
131
+ Always `--dry-run` first to see exactly what would be sent.
132
+
133
+ ## External engines (bring your own)
134
+
135
+ `traces` hosts analysis engines and PII scrubbers it does **not** bundle — you install the tool, `traces` drives it over a thin command adapter. Same pattern for any future engine or model.
136
+
137
+ **Analyzers** run over the emitted OTLP artifact as peers to the built-in analysts:
138
+
139
+ ```bash
140
+ traces analyze --last 1 --analyzer halo # run HALO too
141
+ traces analyze --last 1 --analyzer halo --analyzer-prompt "find token waste"
142
+ traces analyze --last 1 --analyzer halo --analyzer my-engine # repeatable
143
+ ```
144
+
145
+ **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
+
147
+ ```bash
148
+ # the command reads a JSON array of strings on stdin, writes the scrubbed array on stdout
149
+ traces upload --since 24h --dry-run --redactor "my-pii-scrubber"
150
+ ```
151
+
152
+ In the SDK these are the `ExternalAnalyzer` and `Redactor` interfaces (`haloAnalyzer`, `commandAnalyzer`, `commandRedactor`, `applyRedactor`, `runExternalAnalyzers`). See [`examples/external-engines.ts`](./examples/external-engines.ts).
153
+
154
+ > For the built-in agentic analysts (`--llm`), set `OPENAI_API_KEY` — or point at any OpenAI-compatible gateway with `OPENAI_BASE_URL` (e.g. an internal router) to use a non-OpenAI key.
155
+
118
156
  ## Library (SDK)
119
157
 
120
158
  The CLI is a thin consumer of these exports.
@@ -129,6 +167,7 @@ The CLI is a thin consumer of these exports.
129
167
  | `planUpload` / `executeUpload` | `(…, { backend? }) → …` | redact + dedup + send to any sink |
130
168
  | `selectAdapters` / `listAdapters` / `resolveAdapter` | adapter selection + the harness registry |
131
169
  | `HarnessTraceAdapter` | interface (`locate` + `parse`) | implement to add a harness |
170
+ | `ExternalAnalyzer` / `Redactor` | `haloAnalyzer` / `commandAnalyzer` / `commandRedactor` | drive engines/models you install (not bundled) |
132
171
 
133
172
  ```ts
134
173
  import { watchSessions, analyzeSpans, AnalystRegistry, makeFinding } from '@tangle-network/traces'
@@ -158,6 +197,7 @@ Runnable, in [`examples/`](./examples):
158
197
  | [`custom-analyst.ts`](./examples/custom-analyst.ts) | register and run your own analyst |
159
198
  | [`custom-backend.ts`](./examples/custom-backend.ts) | redact + dedup + upload to your own sink |
160
199
  | [`register-harness.ts`](./examples/register-harness.ts) | add a new harness by implementing `HarnessTraceAdapter` |
200
+ | [`external-engines.ts`](./examples/external-engines.ts) | drive HALO + an external PII scrubber you install yourself |
161
201
 
162
202
  ## Develop
163
203
 
@@ -70,6 +70,114 @@ async function analyzeSpans(spans, opts = {}) {
70
70
  return { otlpPath, result };
71
71
  }
72
72
 
73
+ // src/external.ts
74
+ import { spawn } from "child_process";
75
+ function runCommand(command, args = [], opts = {}) {
76
+ const timeoutMs = opts.timeoutMs ?? 12e4;
77
+ const maxBuffer = opts.maxBuffer ?? 32 * 1024 * 1024;
78
+ return new Promise((resolve, reject) => {
79
+ let child;
80
+ try {
81
+ child = spawn(command, [...args], { signal: opts.signal });
82
+ } catch (err2) {
83
+ reject(err2 instanceof Error ? err2 : new Error(String(err2)));
84
+ return;
85
+ }
86
+ let out = "";
87
+ let err = "";
88
+ let killed = false;
89
+ const timer = setTimeout(() => {
90
+ killed = true;
91
+ child.kill("SIGKILL");
92
+ reject(new Error(`${command}: timed out after ${timeoutMs}ms`));
93
+ }, timeoutMs);
94
+ const cap = (add, buf) => {
95
+ const next = buf + add;
96
+ if (next.length > maxBuffer) {
97
+ killed = true;
98
+ child.kill("SIGKILL");
99
+ clearTimeout(timer);
100
+ reject(new Error(`${command}: output exceeded ${maxBuffer} bytes`));
101
+ }
102
+ return next;
103
+ };
104
+ child.stdout?.on("data", (d) => {
105
+ out = cap(String(d), out);
106
+ });
107
+ child.stderr?.on("data", (d) => {
108
+ err = cap(String(d), err);
109
+ });
110
+ child.on("error", (e) => {
111
+ clearTimeout(timer);
112
+ if (!killed) reject(e);
113
+ });
114
+ child.on("close", (code) => {
115
+ clearTimeout(timer);
116
+ if (!killed) resolve({ stdout: out, stderr: err, code });
117
+ });
118
+ if (opts.input !== void 0) {
119
+ child.stdin?.on("error", () => {
120
+ });
121
+ child.stdin?.end(opts.input);
122
+ }
123
+ });
124
+ }
125
+ function commandAnalyzer(spec) {
126
+ return {
127
+ name: spec.name,
128
+ async analyze(otlpPath, opts = {}) {
129
+ try {
130
+ const res = await runCommand(spec.command, spec.args(otlpPath, opts.prompt), {
131
+ signal: opts.signal,
132
+ timeoutMs: spec.timeoutMs
133
+ });
134
+ if (res.code !== 0) {
135
+ return { analyzer: spec.name, ok: false, output: res.stdout, error: res.stderr.trim() || `exit ${res.code}` };
136
+ }
137
+ return { analyzer: spec.name, ok: true, output: res.stdout.trim(), findings: spec.parse?.(res.stdout) };
138
+ } catch (e) {
139
+ return { analyzer: spec.name, ok: false, output: "", error: e instanceof Error ? e.message : String(e) };
140
+ }
141
+ }
142
+ };
143
+ }
144
+ function haloAnalyzer(opts = {}) {
145
+ const defaultPrompt = opts.defaultPrompt ?? "diagnose";
146
+ return commandAnalyzer({
147
+ name: "halo",
148
+ command: opts.command ?? "halo",
149
+ args: (otlpPath, prompt) => [otlpPath, "-p", prompt ?? defaultPrompt],
150
+ timeoutMs: opts.timeoutMs
151
+ });
152
+ }
153
+ function runExternalAnalyzers(otlpPath, analyzers, opts = {}) {
154
+ return Promise.all(analyzers.map((a) => a.analyze(otlpPath, opts)));
155
+ }
156
+ function commandRedactor(spec) {
157
+ const name = spec.name ?? spec.command;
158
+ return {
159
+ name,
160
+ async redactText(texts) {
161
+ if (texts.length === 0) return [];
162
+ const res = await runCommand(spec.command, spec.args ?? [], {
163
+ input: JSON.stringify(texts),
164
+ timeoutMs: spec.timeoutMs
165
+ });
166
+ if (res.code !== 0) throw new Error(`redactor ${name}: exit ${res.code}: ${res.stderr.trim()}`);
167
+ let parsed;
168
+ try {
169
+ parsed = JSON.parse(res.stdout);
170
+ } catch {
171
+ throw new Error(`redactor ${name}: stdout was not JSON`);
172
+ }
173
+ if (!Array.isArray(parsed) || parsed.length !== texts.length) {
174
+ throw new Error(`redactor ${name}: expected a JSON array of ${texts.length} strings`);
175
+ }
176
+ return parsed.map(String);
177
+ }
178
+ };
179
+ }
180
+
73
181
  // src/time.ts
74
182
  function parseIsoToEpochMs(ts) {
75
183
  if (!ts) return 0;
@@ -139,15 +247,38 @@ async function runPipelines(spans, opts = {}) {
139
247
  return { stuckLoops, toolUse };
140
248
  }
141
249
 
250
+ // src/adapters/conversation.ts
251
+ var CONTENT_CAP = 8e3;
252
+ function capText(raw) {
253
+ const t = raw.trim();
254
+ if (t.length <= CONTENT_CAP) return t;
255
+ return `${t.slice(0, CONTENT_CAP)}\u2026 [+${t.length - CONTENT_CAP} chars]`;
256
+ }
257
+ function userPromptSpan(o) {
258
+ return span({
259
+ traceId: o.traceId,
260
+ spanId: o.spanId,
261
+ parentSpanId: o.parentSpanId,
262
+ name: "user.prompt",
263
+ kind: "CHAIN",
264
+ startTime: o.startTime,
265
+ service: o.service ?? null,
266
+ agent: o.agent ?? null,
267
+ step: o.step,
268
+ content: o.content
269
+ });
270
+ }
271
+
142
272
  // src/adapters/amp.ts
143
273
  import { readdir, readFile, stat } from "fs/promises";
144
274
  import { homedir } from "os";
145
275
  import { basename, join } from "path";
146
276
  var SERVICE = "amp";
147
- var CONTENT_CAP = 8e3;
148
277
  function textOf(content) {
149
278
  if (typeof content === "string") return content.slice(0, CONTENT_CAP);
150
- return (Array.isArray(content) ? content : []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n").trim().slice(0, CONTENT_CAP);
279
+ return capText(
280
+ (Array.isArray(content) ? content : []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n")
281
+ );
151
282
  }
152
283
  var AmpAdapter = class {
153
284
  harness = "amp";
@@ -201,12 +332,10 @@ var AmpAdapter = class {
201
332
  const prompt = textOf(m.content);
202
333
  if (prompt) {
203
334
  spans.push(
204
- span({
335
+ userPromptSpan({
205
336
  traceId,
206
337
  spanId: `${llmId}:user`,
207
338
  parentSpanId: rootId,
208
- name: "user.prompt",
209
- kind: "CHAIN",
210
339
  startTime: start,
211
340
  service: SERVICE,
212
341
  agent: SERVICE,
@@ -290,10 +419,11 @@ function inputTokens(u) {
290
419
  function asBlocks(content) {
291
420
  return Array.isArray(content) ? content : [];
292
421
  }
293
- var CONTENT_CAP2 = 8e3;
294
422
  function textOf2(content) {
295
- if (typeof content === "string") return content.slice(0, CONTENT_CAP2);
296
- return asBlocks(content).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n").trim().slice(0, CONTENT_CAP2);
423
+ if (typeof content === "string") return capText(content);
424
+ return capText(
425
+ asBlocks(content).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n")
426
+ );
297
427
  }
298
428
  function stringifyToolResult(content) {
299
429
  if (typeof content === "string") return content;
@@ -354,12 +484,10 @@ function parseClaudeStream(events, ctx) {
354
484
  const prompt = textOf2(ev.message.content);
355
485
  if (prompt) {
356
486
  spans.push(
357
- span({
487
+ userPromptSpan({
358
488
  traceId: ctx.traceId,
359
489
  spanId: `${ctx.idPrefix}${uid}:user`,
360
490
  parentSpanId: ctx.rootParent,
361
- name: "user.prompt",
362
- kind: "CHAIN",
363
491
  startTime: ts,
364
492
  service: SERVICE2,
365
493
  agent: ctx.agent,
@@ -518,9 +646,8 @@ function contentToString(content) {
518
646
  }
519
647
  return "";
520
648
  }
521
- var CONTENT_CAP3 = 8e3;
522
649
  function textOf3(content) {
523
- return contentToString(content).trim().slice(0, CONTENT_CAP3);
650
+ return capText(contentToString(content));
524
651
  }
525
652
  function outputIsError(output) {
526
653
  const s = typeof output === "string" ? output : JSON.stringify(output ?? "");
@@ -673,17 +800,15 @@ var CodexAdapter = class {
673
800
  const prompt = textOf3(l.payload.content);
674
801
  if (prompt) {
675
802
  spans.push(
676
- span({
803
+ userPromptSpan({
677
804
  traceId,
678
805
  spanId: `msg:${step}:user`,
679
806
  parentSpanId: rootId,
680
- name: "user.prompt",
681
- kind: "CHAIN",
682
807
  startTime: ts,
808
+ content: prompt,
683
809
  service: SERVICE3,
684
810
  agent: SERVICE3,
685
- step,
686
- content: prompt
811
+ step
687
812
  })
688
813
  );
689
814
  step += 1;
@@ -844,9 +969,10 @@ import { readdir as readdir5, readFile as readFile5, stat as stat5 } from "fs/pr
844
969
  import { homedir as homedir5 } from "os";
845
970
  import { basename as basename4, join as join5 } from "path";
846
971
  var SERVICE5 = "factory";
847
- var CONTENT_CAP4 = 8e3;
848
972
  function textOf4(content) {
849
- return (content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("").trim().slice(0, CONTENT_CAP4);
973
+ return capText(
974
+ (content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("")
975
+ );
850
976
  }
851
977
  function parseLines4(raw) {
852
978
  const out = [];
@@ -938,17 +1064,15 @@ var FactoryAdapter = class {
938
1064
  if (role === "user") {
939
1065
  if (text) {
940
1066
  spans.push(
941
- span({
1067
+ userPromptSpan({
942
1068
  traceId,
943
1069
  spanId: `user:${l.id ?? step}`,
944
1070
  parentSpanId: rootId,
945
- name: "user.prompt",
946
- kind: "CHAIN",
947
1071
  startTime: ts,
1072
+ content: text,
948
1073
  service: SERVICE5,
949
1074
  agent: SERVICE5,
950
- step,
951
- content: text
1075
+ step
952
1076
  })
953
1077
  );
954
1078
  step += 1;
@@ -1010,9 +1134,8 @@ function tokens(t) {
1010
1134
  if (!t) return null;
1011
1135
  return t.actual ?? t.approx ?? null;
1012
1136
  }
1013
- var CONTENT_CAP5 = 8e3;
1014
1137
  function textOf5(content) {
1015
- return (content ?? "").trim().slice(0, CONTENT_CAP5);
1138
+ return capText(content ?? "");
1016
1139
  }
1017
1140
  var ForgeAdapter = class {
1018
1141
  harness = "forge";
@@ -1070,17 +1193,15 @@ var ForgeAdapter = class {
1070
1193
  const prompt = textOf5(entry.text.content);
1071
1194
  if (prompt) {
1072
1195
  spans.push(
1073
- span({
1196
+ userPromptSpan({
1074
1197
  traceId,
1075
1198
  spanId: `user:${step}`,
1076
1199
  parentSpanId: rootId,
1077
- name: "user.prompt",
1078
- kind: "CHAIN",
1079
1200
  startTime: ts,
1201
+ content: prompt,
1080
1202
  service: SERVICE6,
1081
1203
  agent: SERVICE6,
1082
- step,
1083
- content: prompt
1204
+ step
1084
1205
  })
1085
1206
  );
1086
1207
  step += 1;
@@ -1146,10 +1267,9 @@ function iso(ts, fallback) {
1146
1267
  function toolStatusError(status) {
1147
1268
  return status != null && /error|fail|cancel/i.test(status);
1148
1269
  }
1149
- var CONTENT_CAP6 = 8e3;
1150
1270
  function textOf6(content) {
1151
1271
  const s = typeof content === "string" ? content : JSON.stringify(content ?? "");
1152
- return s.trim().slice(0, CONTENT_CAP6);
1272
+ return capText(s);
1153
1273
  }
1154
1274
  var GeminiFamilyAdapter = class {
1155
1275
  harness;
@@ -1231,17 +1351,15 @@ var GeminiFamilyAdapter = class {
1231
1351
  const prompt = textOf6(m.content);
1232
1352
  if (prompt) {
1233
1353
  spans.push(
1234
- span({
1354
+ userPromptSpan({
1235
1355
  traceId,
1236
1356
  spanId: `${llmId}:user`,
1237
1357
  parentSpanId: rootId,
1238
- name: "user.prompt",
1239
- kind: "CHAIN",
1240
1358
  startTime: ts,
1359
+ content: prompt,
1241
1360
  service: this.service,
1242
1361
  agent: this.service,
1243
- step,
1244
- content: prompt
1362
+ step
1245
1363
  })
1246
1364
  );
1247
1365
  step += 1;
@@ -1304,9 +1422,10 @@ var SERVICE7 = "opencode";
1304
1422
  function isoOf(ms) {
1305
1423
  return new Date(ms ?? 0).toISOString();
1306
1424
  }
1307
- var CONTENT_CAP7 = 8e3;
1308
1425
  function textOf7(parts) {
1309
- return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("\n").trim().slice(0, CONTENT_CAP7);
1426
+ return capText(
1427
+ parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("\n")
1428
+ );
1310
1429
  }
1311
1430
  var OpencodeAdapter = class {
1312
1431
  harness = "opencode";
@@ -1407,12 +1526,10 @@ var OpencodeAdapter = class {
1407
1526
  step += 1;
1408
1527
  if (msg.role === "user" && turnText) {
1409
1528
  spans.push(
1410
- span({
1529
+ userPromptSpan({
1411
1530
  traceId,
1412
1531
  spanId: `${llmId}:user`,
1413
1532
  parentSpanId: rootId,
1414
- name: "user.prompt",
1415
- kind: "CHAIN",
1416
1533
  startTime: isoOf(msg.time?.created),
1417
1534
  service: SERVICE7,
1418
1535
  agent: SERVICE7,
@@ -1472,10 +1589,11 @@ function isToolBlock(b) {
1472
1589
  function isToolResultBlock(b) {
1473
1590
  return typeof b.type === "string" && /tool[_-]?result|tool[_-]?output/i.test(b.type);
1474
1591
  }
1475
- var CONTENT_CAP8 = 8e3;
1476
1592
  function textOf8(content) {
1477
1593
  if (!content) return "";
1478
- return content.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("").trim().slice(0, CONTENT_CAP8);
1594
+ return capText(
1595
+ content.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("")
1596
+ );
1479
1597
  }
1480
1598
  var PiAdapter = class {
1481
1599
  harness = "pi";
@@ -1547,12 +1665,10 @@ var PiAdapter = class {
1547
1665
  const prompt = textOf8(msg.content);
1548
1666
  if (prompt) {
1549
1667
  spans.push(
1550
- span({
1668
+ userPromptSpan({
1551
1669
  traceId,
1552
1670
  spanId: `${mid}:user`,
1553
1671
  parentSpanId: rootId,
1554
- name: "user.prompt",
1555
- kind: "CHAIN",
1556
1672
  startTime: ts,
1557
1673
  service: SERVICE8,
1558
1674
  agent: SERVICE8,
@@ -1718,7 +1834,7 @@ var QwenAdapter = class {
1718
1834
  inputTokens: r.usageMetadata?.promptTokenCount ?? null,
1719
1835
  outputTokens: r.usageMetadata?.candidatesTokenCount ?? null,
1720
1836
  step,
1721
- content: textOf9(r.message?.parts).slice(0, 8e3) || null
1837
+ content: capText(textOf9(r.message?.parts)) || null
1722
1838
  })
1723
1839
  );
1724
1840
  lastLlm = llmId;
@@ -1746,15 +1862,13 @@ var QwenAdapter = class {
1746
1862
  step += 1;
1747
1863
  }
1748
1864
  } else if (r.type === "user") {
1749
- const prompt = textOf9(r.message?.parts).slice(0, 8e3);
1865
+ const prompt = capText(textOf9(r.message?.parts));
1750
1866
  if (prompt) {
1751
1867
  spans.push(
1752
- span({
1868
+ userPromptSpan({
1753
1869
  traceId,
1754
1870
  spanId: `user:${r.uuid ?? step}`,
1755
1871
  parentSpanId: rootId,
1756
- name: "user.prompt",
1757
- kind: "CHAIN",
1758
1872
  startTime: ts,
1759
1873
  service: SERVICE9,
1760
1874
  agent: SERVICE9,
@@ -1960,6 +2074,12 @@ var CODING_REDACTION_RULES = [
1960
2074
  {
1961
2075
  id: "assigned-secret",
1962
2076
  pattern: /\b(?:api[_-]?key|secret|token|password|passwd|access[_-]?token|client[_-]?secret)\b\s*[:=]\s*["']?[A-Za-z0-9._\-]{12,}["']?/gi
2077
+ },
2078
+ // Credentials embedded in URLs — common when a prompt pastes a curl/clone line.
2079
+ { id: "url-userinfo", pattern: /\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:[^\s/@]+@/gi },
2080
+ {
2081
+ id: "url-secret-param",
2082
+ pattern: /[?&](?:access[_-]?token|api[_-]?key|apikey|auth|token|secret|password|sig|signature)=[^&\s"'#)]+/gi
1963
2083
  }
1964
2084
  ];
1965
2085
  var TRACES_REDACTION_RULES = [...DEFAULT_REDACTION_RULES, ...CODING_REDACTION_RULES];
@@ -1976,6 +2096,28 @@ function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
1976
2096
  });
1977
2097
  return { spans: out, report };
1978
2098
  }
2099
+ async function applyRedactor(spans, redactor) {
2100
+ const idx = [];
2101
+ const texts = [];
2102
+ spans.forEach((s, i) => {
2103
+ const c = s.attributes["content"];
2104
+ if (typeof c === "string" && c.length > 0) {
2105
+ idx.push(i);
2106
+ texts.push(c);
2107
+ }
2108
+ });
2109
+ const out = spans.map((s) => ({ ...s, attributes: { ...s.attributes } }));
2110
+ if (texts.length === 0) return { spans: out, changed: 0 };
2111
+ const scrubbed = await redactor.redactText(texts);
2112
+ let changed = 0;
2113
+ idx.forEach((spanI, k) => {
2114
+ if (scrubbed[k] !== texts[k]) {
2115
+ out[spanI].attributes["content"] = scrubbed[k];
2116
+ changed += 1;
2117
+ }
2118
+ });
2119
+ return { spans: out, changed };
2120
+ }
1979
2121
 
1980
2122
  // src/upload-state.ts
1981
2123
  import { createHash } from "crypto";
@@ -2088,8 +2230,20 @@ function toTraceSpanEvents(spans, rootMeta) {
2088
2230
  };
2089
2231
  });
2090
2232
  }
2233
+ function stripSpanContent(spans) {
2234
+ return spans.map((s) => {
2235
+ if (s.attributes["content"] == null) return s;
2236
+ const { content: _drop, ...attributes } = s.attributes;
2237
+ return { ...s, attributes };
2238
+ });
2239
+ }
2091
2240
  async function executeUpload(plan, opts = {}) {
2092
- const newItems = plan.items.filter((i) => i.isNew);
2241
+ let newItems = plan.items.filter((i) => i.isNew);
2242
+ if (opts.redactor && !opts.stripContent) {
2243
+ const r = opts.redactor;
2244
+ newItems = await Promise.all(newItems.map(async (i) => ({ ...i, spans: (await applyRedactor(i.spans, r)).spans })));
2245
+ }
2246
+ if (opts.stripContent) newItems = newItems.map((i) => ({ ...i, spans: stripSpanContent(i.spans) }));
2093
2247
  const skipped = plan.items.length - newItems.length;
2094
2248
  const redactionCount = newItems.reduce((n, i) => n + i.redaction.redactionCount, 0);
2095
2249
  const uploadedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -2142,10 +2296,18 @@ export {
2142
2296
  serializeSpans,
2143
2297
  writeOtlpFile,
2144
2298
  analyzeSpans,
2299
+ runCommand,
2300
+ commandAnalyzer,
2301
+ haloAnalyzer,
2302
+ runExternalAnalyzers,
2303
+ commandRedactor,
2145
2304
  parseIsoToEpochMs,
2146
2305
  parseSince,
2147
2306
  toRuntimeStore,
2148
2307
  runPipelines,
2308
+ CONTENT_CAP,
2309
+ capText,
2310
+ userPromptSpan,
2149
2311
  AmpAdapter,
2150
2312
  ClaudeAdapter,
2151
2313
  CodexAdapter,
@@ -2168,6 +2330,7 @@ export {
2168
2330
  CODING_REDACTION_RULES,
2169
2331
  TRACES_REDACTION_RULES,
2170
2332
  redactSpans,
2333
+ applyRedactor,
2171
2334
  uploadKey,
2172
2335
  sessionHash,
2173
2336
  loadState,
package/dist/cli.js CHANGED
@@ -1,16 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  analyzeSpans,
4
+ commandAnalyzer,
5
+ commandRedactor,
4
6
  executeUpload,
7
+ haloAnalyzer,
5
8
  knownHarnesses,
6
9
  parseSince,
7
10
  planUpload,
8
11
  resolveAdapter,
12
+ runExternalAnalyzers,
9
13
  runPipelines,
10
14
  selectAdapters,
11
15
  watchSessions,
12
16
  writeOtlpFile
13
- } from "./chunk-33NBMIH4.js";
17
+ } from "./chunk-6TVW7PL6.js";
14
18
 
15
19
  // src/cli.ts
16
20
  import { stat, writeFile } from "fs/promises";
@@ -105,7 +109,9 @@ function parseArgs(argv) {
105
109
  window: 30,
106
110
  minLoop: 3,
107
111
  dryRun: false,
108
- yes: false
112
+ yes: false,
113
+ noContent: false,
114
+ analyzers: []
109
115
  };
110
116
  for (let i = 1; i < argv.length; i++) {
111
117
  const arg = argv[i];
@@ -154,6 +160,20 @@ function parseArgs(argv) {
154
160
  case "--dry-run":
155
161
  a.dryRun = true;
156
162
  break;
163
+ case "--no-content":
164
+ a.noContent = true;
165
+ break;
166
+ case "--analyzer": {
167
+ const v = next();
168
+ if (v) a.analyzers.push(v);
169
+ break;
170
+ }
171
+ case "--analyzer-prompt":
172
+ a.analyzerPrompt = next();
173
+ break;
174
+ case "--redactor":
175
+ a.redactorCmd = next();
176
+ break;
157
177
  case "--yes":
158
178
  case "-y":
159
179
  a.yes = true;
@@ -193,10 +213,13 @@ async function discover(args) {
193
213
  async function buildAxService() {
194
214
  const apiKey = process.env.OPENAI_API_KEY;
195
215
  if (!apiKey) {
196
- throw new Error("--llm needs OPENAI_API_KEY for the agentic analyst kinds (deterministic analysis needs no key)");
216
+ throw new Error(
217
+ "--llm needs OPENAI_API_KEY \u2014 an OpenAI key, or a router/gateway key with OPENAI_BASE_URL set to an OpenAI-compatible endpoint (e.g. the Tangle router). Deterministic analysis needs no key."
218
+ );
197
219
  }
220
+ const apiURL = process.env.OPENAI_BASE_URL || void 0;
198
221
  const { AxAI } = await import("@ax-llm/ax");
199
- return new AxAI({ name: "openai", apiKey });
222
+ return new AxAI({ name: "openai", apiKey, ...apiURL ? { apiURL } : {} });
200
223
  }
201
224
  async function cmdList(args) {
202
225
  const groups = await discover({ ...args, last: args.last || 20 });
@@ -253,8 +276,21 @@ async function cmdAnalyze(args) {
253
276
  `)
254
277
  });
255
278
  const pipelines = await runPipelines(spans, { minLoopOccurrences: args.minLoop });
256
- const report = `${renderReport(result, { harness, sessionCount, spanCount: spans.length, otlpPath })}
279
+ let report = `${renderReport(result, { harness, sessionCount, spanCount: spans.length, otlpPath })}
257
280
  ${renderPipelines(pipelines)}`;
281
+ if (args.analyzers.length > 0 && otlpPath) {
282
+ 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] })
284
+ );
285
+ const results = await runExternalAnalyzers(otlpPath, engines, { prompt: args.analyzerPrompt });
286
+ for (const r of results) {
287
+ report += `
288
+
289
+ ## ${r.analyzer} (external analyzer)
290
+
291
+ ${r.ok ? r.output || "(no output)" : `failed: ${r.error}`}`;
292
+ }
293
+ }
258
294
  if (args.out) {
259
295
  await writeFile(args.out, report, "utf8");
260
296
  console.log(`report \u2192 ${args.out} (${result.findings.length} findings, ${pipelines.stuckLoops.findings.length} loops, OTLP: ${otlpPath})`);
@@ -296,6 +332,8 @@ async function cmdUpload(args) {
296
332
  cwd: args.cwd,
297
333
  sinceMs
298
334
  });
335
+ const redactorParts = args.redactorCmd?.split(/\s+/).filter(Boolean) ?? [];
336
+ const redactor = redactorParts[0] ? commandRedactor({ command: redactorParts[0], args: redactorParts.slice(1) }) : void 0;
299
337
  const newItems = plan.items.filter((i) => i.isNew);
300
338
  const byRule = {};
301
339
  let totalRedactions = 0;
@@ -322,7 +360,7 @@ Window: since ${new Date(sinceMs).toISOString()}
322
360
  return;
323
361
  }
324
362
  if (args.dryRun) {
325
- const res2 = await executeUpload(plan, { dryRun: true, otlpOut: args.otlp });
363
+ const res2 = await executeUpload(plan, { dryRun: true, otlpOut: args.otlp, stripContent: args.noContent, redactor });
326
364
  console.log(`dry run \u2014 ${newItems.length} session(s), ${totalRedactions} redaction(s). Redacted OTLP \u2192 ${res2.otlpPath}`);
327
365
  console.log("No upload performed. Set TANGLE_INGEST_URL / TANGLE_INGEST_API_KEY / TANGLE_TENANT_ID and drop --dry-run to send.");
328
366
  return;
@@ -335,7 +373,7 @@ Window: since ${new Date(sinceMs).toISOString()}
335
373
  }
336
374
  }
337
375
  const res = await executeUpload(plan, { log: (m) => w(`${m}
338
- `) });
376
+ `), stripContent: args.noContent, redactor });
339
377
  console.log(
340
378
  `Uploaded ${res.uploadedSessions} session(s), ${res.acceptedSpans} spans accepted, ${res.redactionCount} redaction(s); ${res.skippedSessions} already-uploaded skipped.`
341
379
  );
@@ -359,12 +397,16 @@ Options:
359
397
  --since <t> upload: window \u2014 30m / 2h / 7d or an ISO date (default 24h); analyze: ISO cutoff
360
398
  --out <path> Write report to a file
361
399
  --otlp <path> OTLP artifact path (also the dry-run upload preview path)
362
- --llm Enable agentic RLM analysts (needs OPENAI_API_KEY)
400
+ --llm Enable agentic RLM analysts (needs OPENAI_API_KEY / OPENAI_BASE_URL)
363
401
  --budget <usd> USD cap for agentic analysts
402
+ --analyzer <cmd> analyze: also run an external engine over the OTLP (repeatable; "halo" or any command)
403
+ --analyzer-prompt <p> analyze: prompt passed to external analyzers (default: diagnose)
364
404
  --interval <s> watch: poll interval seconds (default 5)
365
405
  --window <m> watch: only sessions active in the last N minutes (default 30)
366
406
  --min-loop <n> Min identical repeated calls to flag a loop (default 3)
367
407
  --dry-run upload: redact + dedup + preview, write OTLP, but do NOT send
408
+ --no-content upload: strip prompt/response text \u2014 send metadata only
409
+ --redactor <cmd> upload: external PII scrubber (JSON array stdin\u2192stdout) after the regex pass
368
410
  --yes, -y upload: skip the confirmation prompt
369
411
 
370
412
  Upload env: TANGLE_INGEST_URL (or TANGLE_ORCHESTRATOR_URL), TANGLE_INGEST_API_KEY (or TANGLE_API_KEY), TANGLE_TENANT_ID`);
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ToolUseMetrics, InMemoryTraceStore } from '@tangle-network/agent-eval';
2
2
  import { StuckLoopReport } from '@tangle-network/agent-eval/pipelines';
3
3
  import { AxAIService } from '@ax-llm/ax';
4
- import { AnalystRegistry, buildDefaultAnalystRegistry } from '@tangle-network/agent-eval/analyst';
4
+ import { AnalystRegistry, buildDefaultAnalystRegistry, AnalystFinding } from '@tangle-network/agent-eval/analyst';
5
5
  export { Analyst, AnalystContext, AnalystFinding, AnalystRegistry, buildDefaultAnalystRegistry, makeFinding } from '@tangle-network/agent-eval/analyst';
6
6
  import { RedactionRule, RedactionReport } from '@tangle-network/agent-eval/traces';
7
7
  export { DEFAULT_REDACTION_RULES, RedactionReport, RedactionRule, redactString, redactValue } from '@tangle-network/agent-eval/traces';
@@ -436,6 +436,37 @@ declare class ForgeAdapter implements HarnessTraceAdapter {
436
436
  parse(ref: SessionRef): Promise<OtlpSpan[]>;
437
437
  }
438
438
 
439
+ /**
440
+ * Shared conversation-capture primitives for the harness adapters.
441
+ *
442
+ * Every adapter records the human's prompt and the assistant's prose, not just
443
+ * the execution skeleton (LLM token spans + tool calls). These helpers keep that
444
+ * capture identical across harnesses: one text cap, one `user.prompt` span shape.
445
+ */
446
+
447
+ /** Max chars of conversation text kept per span — enough for prompt/response
448
+ * analysis, bounded for storage + redaction cost. */
449
+ declare const CONTENT_CAP = 8000;
450
+ /** Trim and cap a message's text at {@link CONTENT_CAP}. When the text is cut, a
451
+ * visible `… [+N chars]` marker is appended so analysts (and readers) know the
452
+ * content was truncated rather than silently lost. */
453
+ declare function capText(raw: string): string;
454
+ interface UserPromptInput {
455
+ traceId: string;
456
+ spanId: string;
457
+ parentSpanId: string | null;
458
+ startTime: string;
459
+ /** The human's prompt text (extracted by the adapter; cap with capText). */
460
+ content: string;
461
+ service?: string | null;
462
+ agent?: string | null;
463
+ step?: number;
464
+ }
465
+ /** The canonical `user.prompt` span. Every adapter emits the human's turn the
466
+ * same way — a CHAIN span, because a user message is not an LLM call — so
467
+ * analysts see one consistent shape regardless of harness. */
468
+ declare function userPromptSpan(o: UserPromptInput): OtlpSpan;
469
+
439
470
  /**
440
471
  * Run agent-eval's shipped trace pipelines over normalized harness spans.
441
472
  *
@@ -522,6 +553,90 @@ interface AnalyzeResult {
522
553
  }
523
554
  declare function analyzeSpans(spans: readonly OtlpSpan[], opts?: AnalyzeOptions): Promise<AnalyzeResult>;
524
555
 
556
+ /**
557
+ * External-tool adapters: run analysis engines and PII scrubbers that `traces`
558
+ * does NOT bundle. Each is a thin command adapter over a shared subprocess
559
+ * runner — install the tool yourself (HALO, openai/privacy-filter, anything),
560
+ * point an adapter at it, and it composes with the built-in pipeline.
561
+ *
562
+ * - ExternalAnalyzer — run an engine over the emitted OTLP-JSONL artifact and
563
+ * fold its output into the report (HALO and friends).
564
+ * - Redactor — scrub free-form prompt/response text via an external model that
565
+ * catches what the regex pass can't (names, addresses).
566
+ */
567
+
568
+ interface RunResult {
569
+ stdout: string;
570
+ stderr: string;
571
+ code: number | null;
572
+ }
573
+ /** Spawn `command args`, optionally write `input` to stdin, and collect output.
574
+ * Rejects on spawn error, timeout, or output exceeding `maxBuffer`. */
575
+ declare function runCommand(command: string, args?: readonly string[], opts?: {
576
+ input?: string;
577
+ signal?: AbortSignal;
578
+ timeoutMs?: number;
579
+ maxBuffer?: number;
580
+ }): Promise<RunResult>;
581
+ interface ExternalAnalysisResult {
582
+ analyzer: string;
583
+ ok: boolean;
584
+ /** Raw text the engine produced (markdown/JSON/plain — engine-specific). */
585
+ output: string;
586
+ /** Structured findings, when the engine emits parseable JSON. */
587
+ findings?: AnalystFinding[];
588
+ error?: string;
589
+ }
590
+ /** An analysis engine that runs over the emitted OTLP-JSONL artifact — a peer to
591
+ * the built-in analysts, so you get many analyzers beyond our own. */
592
+ interface ExternalAnalyzer {
593
+ name: string;
594
+ analyze(otlpPath: string, opts?: {
595
+ prompt?: string;
596
+ signal?: AbortSignal;
597
+ }): Promise<ExternalAnalysisResult>;
598
+ }
599
+ /** Wrap any CLI that reads the OTLP file and prints analysis to stdout. `args`
600
+ * builds the argv from the artifact path + optional prompt. Non-zero exit →
601
+ * `ok:false` with stderr in `error` (fail-soft: one engine never breaks a run). */
602
+ declare function commandAnalyzer(spec: {
603
+ name: string;
604
+ command: string;
605
+ args: (otlpPath: string, prompt?: string) => string[];
606
+ /** Parse stdout into structured findings; omit to keep raw output only. */
607
+ parse?: (stdout: string) => AnalystFinding[] | undefined;
608
+ timeoutMs?: number;
609
+ }): ExternalAnalyzer;
610
+ /** HALO (github.com/context-labs/halo) over the OTLP artifact: `halo <file> -p <prompt>`.
611
+ * Install HALO yourself; this just drives it. */
612
+ declare function haloAnalyzer(opts?: {
613
+ command?: string;
614
+ defaultPrompt?: string;
615
+ timeoutMs?: number;
616
+ }): ExternalAnalyzer;
617
+ /** Run external analyzers over one OTLP artifact, concurrently. Never throws —
618
+ * a failing engine returns `ok:false`. */
619
+ declare function runExternalAnalyzers(otlpPath: string, analyzers: readonly ExternalAnalyzer[], opts?: {
620
+ prompt?: string;
621
+ signal?: AbortSignal;
622
+ }): Promise<ExternalAnalysisResult[]>;
623
+ /** An external PII/secret scrubber for free-form text — catches what regex
624
+ * can't (names, addresses). Use to harden upload beyond the built-in rules. */
625
+ interface Redactor {
626
+ name: string;
627
+ /** Scrub a batch of texts; returns one scrubbed string per input, in order. */
628
+ redactText(texts: readonly string[]): Promise<string[]>;
629
+ }
630
+ /** Wrap any CLI that speaks the redaction protocol: read a JSON array of strings
631
+ * on stdin, write a same-length JSON array of scrubbed strings on stdout.
632
+ * (A 3-line wrapper adapts tools like `opf`/openai-privacy-filter.) */
633
+ declare function commandRedactor(spec: {
634
+ name?: string;
635
+ command: string;
636
+ args?: string[];
637
+ timeoutMs?: number;
638
+ }): Redactor;
639
+
525
640
  /**
526
641
  * Live trace observer — the reusable, event-driven core behind `traces watch`.
527
642
  *
@@ -589,9 +704,15 @@ declare function watchSessions(opts: ObserverOptions): Promise<void>;
589
704
  * Reuses agent-eval's `redactValue` + `DEFAULT_REDACTION_RULES` (email, IPs,
590
705
  * generic secret keys, …) and layers on coding-session-specific rules that the
591
706
  * defaults miss — GitHub tokens, cloud keys, JWTs, bearer headers, private-key
592
- * blocks, and `key=secret` assignments common in shell/tool output. Redaction
593
- * runs over every span attribute + status message, so what leaves the machine
594
- * is already scrubbed.
707
+ * blocks, `key=secret` assignments, and credentials embedded in URLs. Redaction
708
+ * runs over every span attribute + status message (including captured prompt /
709
+ * response `content`), so what leaves the machine is already scrubbed.
710
+ *
711
+ * Scope, stated honestly: this is **best-effort regex** for *structured* secrets
712
+ * and credentials. It does NOT catch free-form PII — names, postal addresses,
713
+ * phone numbers, account numbers in prose — which need a context-aware model.
714
+ * For that assurance, run an ML PII scrubber (e.g. openai/privacy-filter) on the
715
+ * ingest side of the platform, or upload metadata-only with `--no-content`.
595
716
  */
596
717
 
597
718
  /** Secrets common in coding-agent traces that the substrate defaults don't cover. */
@@ -605,6 +726,14 @@ interface SpanRedaction {
605
726
  /** Redact every span's attributes + status message. Returns scrubbed spans and
606
727
  * an aggregate report (count + per-rule breakdown) over the whole set. */
607
728
  declare function redactSpans(spans: readonly OtlpSpan[], rules?: RedactionRule[]): SpanRedaction;
729
+ /** Defense-in-depth: run an external {@link Redactor} (e.g. an ML PII model) over
730
+ * every span's captured `content` (prompt/response prose), catching free-form
731
+ * PII the regex pass misses. Compose AFTER `redactSpans`. Returns scrubbed spans
732
+ * and the number of `content` fields the model changed. */
733
+ declare function applyRedactor(spans: readonly OtlpSpan[], redactor: Redactor): Promise<{
734
+ spans: OtlpSpan[];
735
+ changed: number;
736
+ }>;
608
737
 
609
738
  /**
610
739
  * Batch collection seam — `collectSessions` returns redacted spans per session
@@ -709,9 +838,16 @@ interface ExecuteOptions {
709
838
  otlpOut?: string;
710
839
  /** Custom sink. Defaults to the hosted Tangle Intelligence client from env. */
711
840
  backend?: UploadBackend;
841
+ /** Metadata-only upload: drop captured prompt/response `content` from every
842
+ * span before it leaves the machine (still keeps tool calls, tokens, timing,
843
+ * loop signal). The strongest privacy posture when prose can't leave. */
844
+ stripContent?: boolean;
845
+ /** External PII scrubber applied to `content` AFTER the regex pass — catches
846
+ * free-form PII (names, addresses) the rules miss. Ignored if stripContent. */
847
+ redactor?: Redactor;
712
848
  log?: (msg: string) => void;
713
849
  }
714
850
  /** Send the NEW items (or, on dryRun, write the redacted OTLP that would send). */
715
851
  declare function executeUpload(plan: UploadPlan, opts?: ExecuteOptions): Promise<UploadResult>;
716
852
 
717
- export { ATTR, AmpAdapter, type AnalyzeOptions, type AnalyzeResult, CODING_REDACTION_RULES, ClaudeAdapter, CodexAdapter, type CollectOptions, CopilotAdapter, DEFAULT_HARNESS, type ExecuteOptions, 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 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, collectSessions, executeUpload, knownHarnesses, listAdapters, loadState, parseIsoToEpochMs, parseSince, planUpload, redactSpans, resolveAdapter, runPipelines, saveState, scanSessions, selectAdapters, serializeSpans, sessionHash, span, toRuntimeStore, toTraceSpanEvents, uploadKey, watchSessions, writeOtlpFile };
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 };
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  ATTR,
3
3
  AmpAdapter,
4
4
  CODING_REDACTION_RULES,
5
+ CONTENT_CAP,
5
6
  ClaudeAdapter,
6
7
  CodexAdapter,
7
8
  CopilotAdapter,
@@ -16,7 +17,12 @@ import {
16
17
  TRACES_REDACTION_RULES,
17
18
  alreadyUploaded,
18
19
  analyzeSpans,
20
+ applyRedactor,
21
+ capText,
22
+ commandAnalyzer,
23
+ commandRedactor,
19
24
  executeUpload,
25
+ haloAnalyzer,
20
26
  knownHarnesses,
21
27
  listAdapters,
22
28
  loadState,
@@ -25,6 +31,8 @@ import {
25
31
  planUpload,
26
32
  redactSpans,
27
33
  resolveAdapter,
34
+ runCommand,
35
+ runExternalAnalyzers,
28
36
  runPipelines,
29
37
  saveState,
30
38
  scanSessions,
@@ -35,9 +43,10 @@ import {
35
43
  toRuntimeStore,
36
44
  toTraceSpanEvents,
37
45
  uploadKey,
46
+ userPromptSpan,
38
47
  watchSessions,
39
48
  writeOtlpFile
40
- } from "./chunk-33NBMIH4.js";
49
+ } from "./chunk-6TVW7PL6.js";
41
50
 
42
51
  // src/collect.ts
43
52
  async function collectSessions(opts = {}) {
@@ -66,6 +75,7 @@ export {
66
75
  AmpAdapter,
67
76
  AnalystRegistry,
68
77
  CODING_REDACTION_RULES,
78
+ CONTENT_CAP,
69
79
  ClaudeAdapter,
70
80
  CodexAdapter,
71
81
  CopilotAdapter,
@@ -81,10 +91,15 @@ export {
81
91
  TRACES_REDACTION_RULES,
82
92
  alreadyUploaded,
83
93
  analyzeSpans,
94
+ applyRedactor,
84
95
  buildDefaultAnalystRegistry,
96
+ capText,
85
97
  collectSessions,
98
+ commandAnalyzer,
99
+ commandRedactor,
86
100
  createHostedClient,
87
101
  executeUpload,
102
+ haloAnalyzer,
88
103
  hostedClientFromEnv,
89
104
  knownHarnesses,
90
105
  listAdapters,
@@ -97,6 +112,8 @@ export {
97
112
  redactString,
98
113
  redactValue,
99
114
  resolveAdapter,
115
+ runCommand,
116
+ runExternalAnalyzers,
100
117
  runPipelines,
101
118
  saveState,
102
119
  scanSessions,
@@ -107,6 +124,7 @@ export {
107
124
  toRuntimeStore,
108
125
  toTraceSpanEvents,
109
126
  uploadKey,
127
+ userPromptSpan,
110
128
  watchSessions,
111
129
  writeOtlpFile
112
130
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/traces",
3
- "version": "0.4.1",
3
+ "version": "0.6.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",