@tangle-network/traces 0.4.1 → 0.5.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 +17 -3
- package/dist/{chunk-33NBMIH4.js → chunk-HF2KGEBW.js} +77 -55
- package/dist/cli.js +9 -4
- package/dist/index.d.ts +45 -4
- package/dist/index.js +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,19 +102,33 @@ traces upload --since 24h # upload last day to the Inte
|
|
|
102
102
|
| `--llm` / `--budget <usd>` | Enable agentic analysts (needs `OPENAI_API_KEY`) / cap their spend |
|
|
103
103
|
| `--interval <s>` / `--window <m>` | `watch`: poll seconds (default 5) / active-session window minutes (default 30) |
|
|
104
104
|
| `--min-loop <n>` | Identical repeated calls before flagging a loop (default 3) |
|
|
105
|
+
| `--no-content` | `upload`: send metadata only — strip all prompt/response text |
|
|
105
106
|
| `--dry-run` / `--yes` | `upload`: preview without sending / skip the confirm prompt |
|
|
106
107
|
|
|
107
108
|
## Upload to the Intelligence Platform
|
|
108
109
|
|
|
109
|
-
`upload` **redacts
|
|
110
|
+
`upload` **redacts locally before anything leaves the machine**, dedups against already-uploaded sessions, and tags each with metadata (harness, cwd, git branch, host).
|
|
110
111
|
|
|
111
112
|
```bash
|
|
112
|
-
traces upload --since 24h --dry-run
|
|
113
|
-
traces upload --since 24h
|
|
113
|
+
traces upload --since 24h --dry-run # see exactly what would be sent — no network
|
|
114
|
+
traces upload --since 24h --no-content # send metadata only — drop all prompt/response text
|
|
115
|
+
traces upload --since 24h # send it
|
|
114
116
|
```
|
|
115
117
|
|
|
116
118
|
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
119
|
|
|
120
|
+
### Redaction scope — read this before uploading prose
|
|
121
|
+
|
|
122
|
+
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.
|
|
123
|
+
|
|
124
|
+
It does **not** catch free-form PII — names, postal addresses, phone numbers in prose — which needs a context-aware model. Three postures, strongest first:
|
|
125
|
+
|
|
126
|
+
1. **`--no-content`** — upload metadata only (tool calls, tokens, timing, loop signal); no prose leaves the machine.
|
|
127
|
+
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.
|
|
128
|
+
3. Default — regex redaction of structured secrets.
|
|
129
|
+
|
|
130
|
+
Always `--dry-run` first to see exactly what would be sent.
|
|
131
|
+
|
|
118
132
|
## Library (SDK)
|
|
119
133
|
|
|
120
134
|
The CLI is a thin consumer of these exports.
|
|
@@ -139,15 +139,38 @@ async function runPipelines(spans, opts = {}) {
|
|
|
139
139
|
return { stuckLoops, toolUse };
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// src/adapters/conversation.ts
|
|
143
|
+
var CONTENT_CAP = 8e3;
|
|
144
|
+
function capText(raw) {
|
|
145
|
+
const t = raw.trim();
|
|
146
|
+
if (t.length <= CONTENT_CAP) return t;
|
|
147
|
+
return `${t.slice(0, CONTENT_CAP)}\u2026 [+${t.length - CONTENT_CAP} chars]`;
|
|
148
|
+
}
|
|
149
|
+
function userPromptSpan(o) {
|
|
150
|
+
return span({
|
|
151
|
+
traceId: o.traceId,
|
|
152
|
+
spanId: o.spanId,
|
|
153
|
+
parentSpanId: o.parentSpanId,
|
|
154
|
+
name: "user.prompt",
|
|
155
|
+
kind: "CHAIN",
|
|
156
|
+
startTime: o.startTime,
|
|
157
|
+
service: o.service ?? null,
|
|
158
|
+
agent: o.agent ?? null,
|
|
159
|
+
step: o.step,
|
|
160
|
+
content: o.content
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
142
164
|
// src/adapters/amp.ts
|
|
143
165
|
import { readdir, readFile, stat } from "fs/promises";
|
|
144
166
|
import { homedir } from "os";
|
|
145
167
|
import { basename, join } from "path";
|
|
146
168
|
var SERVICE = "amp";
|
|
147
|
-
var CONTENT_CAP = 8e3;
|
|
148
169
|
function textOf(content) {
|
|
149
170
|
if (typeof content === "string") return content.slice(0, CONTENT_CAP);
|
|
150
|
-
return (
|
|
171
|
+
return capText(
|
|
172
|
+
(Array.isArray(content) ? content : []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n")
|
|
173
|
+
);
|
|
151
174
|
}
|
|
152
175
|
var AmpAdapter = class {
|
|
153
176
|
harness = "amp";
|
|
@@ -201,12 +224,10 @@ var AmpAdapter = class {
|
|
|
201
224
|
const prompt = textOf(m.content);
|
|
202
225
|
if (prompt) {
|
|
203
226
|
spans.push(
|
|
204
|
-
|
|
227
|
+
userPromptSpan({
|
|
205
228
|
traceId,
|
|
206
229
|
spanId: `${llmId}:user`,
|
|
207
230
|
parentSpanId: rootId,
|
|
208
|
-
name: "user.prompt",
|
|
209
|
-
kind: "CHAIN",
|
|
210
231
|
startTime: start,
|
|
211
232
|
service: SERVICE,
|
|
212
233
|
agent: SERVICE,
|
|
@@ -290,10 +311,11 @@ function inputTokens(u) {
|
|
|
290
311
|
function asBlocks(content) {
|
|
291
312
|
return Array.isArray(content) ? content : [];
|
|
292
313
|
}
|
|
293
|
-
var CONTENT_CAP2 = 8e3;
|
|
294
314
|
function textOf2(content) {
|
|
295
|
-
if (typeof content === "string") return content
|
|
296
|
-
return
|
|
315
|
+
if (typeof content === "string") return capText(content);
|
|
316
|
+
return capText(
|
|
317
|
+
asBlocks(content).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n")
|
|
318
|
+
);
|
|
297
319
|
}
|
|
298
320
|
function stringifyToolResult(content) {
|
|
299
321
|
if (typeof content === "string") return content;
|
|
@@ -354,12 +376,10 @@ function parseClaudeStream(events, ctx) {
|
|
|
354
376
|
const prompt = textOf2(ev.message.content);
|
|
355
377
|
if (prompt) {
|
|
356
378
|
spans.push(
|
|
357
|
-
|
|
379
|
+
userPromptSpan({
|
|
358
380
|
traceId: ctx.traceId,
|
|
359
381
|
spanId: `${ctx.idPrefix}${uid}:user`,
|
|
360
382
|
parentSpanId: ctx.rootParent,
|
|
361
|
-
name: "user.prompt",
|
|
362
|
-
kind: "CHAIN",
|
|
363
383
|
startTime: ts,
|
|
364
384
|
service: SERVICE2,
|
|
365
385
|
agent: ctx.agent,
|
|
@@ -518,9 +538,8 @@ function contentToString(content) {
|
|
|
518
538
|
}
|
|
519
539
|
return "";
|
|
520
540
|
}
|
|
521
|
-
var CONTENT_CAP3 = 8e3;
|
|
522
541
|
function textOf3(content) {
|
|
523
|
-
return contentToString(content)
|
|
542
|
+
return capText(contentToString(content));
|
|
524
543
|
}
|
|
525
544
|
function outputIsError(output) {
|
|
526
545
|
const s = typeof output === "string" ? output : JSON.stringify(output ?? "");
|
|
@@ -673,17 +692,15 @@ var CodexAdapter = class {
|
|
|
673
692
|
const prompt = textOf3(l.payload.content);
|
|
674
693
|
if (prompt) {
|
|
675
694
|
spans.push(
|
|
676
|
-
|
|
695
|
+
userPromptSpan({
|
|
677
696
|
traceId,
|
|
678
697
|
spanId: `msg:${step}:user`,
|
|
679
698
|
parentSpanId: rootId,
|
|
680
|
-
name: "user.prompt",
|
|
681
|
-
kind: "CHAIN",
|
|
682
699
|
startTime: ts,
|
|
700
|
+
content: prompt,
|
|
683
701
|
service: SERVICE3,
|
|
684
702
|
agent: SERVICE3,
|
|
685
|
-
step
|
|
686
|
-
content: prompt
|
|
703
|
+
step
|
|
687
704
|
})
|
|
688
705
|
);
|
|
689
706
|
step += 1;
|
|
@@ -844,9 +861,10 @@ import { readdir as readdir5, readFile as readFile5, stat as stat5 } from "fs/pr
|
|
|
844
861
|
import { homedir as homedir5 } from "os";
|
|
845
862
|
import { basename as basename4, join as join5 } from "path";
|
|
846
863
|
var SERVICE5 = "factory";
|
|
847
|
-
var CONTENT_CAP4 = 8e3;
|
|
848
864
|
function textOf4(content) {
|
|
849
|
-
return (
|
|
865
|
+
return capText(
|
|
866
|
+
(content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("")
|
|
867
|
+
);
|
|
850
868
|
}
|
|
851
869
|
function parseLines4(raw) {
|
|
852
870
|
const out = [];
|
|
@@ -938,17 +956,15 @@ var FactoryAdapter = class {
|
|
|
938
956
|
if (role === "user") {
|
|
939
957
|
if (text) {
|
|
940
958
|
spans.push(
|
|
941
|
-
|
|
959
|
+
userPromptSpan({
|
|
942
960
|
traceId,
|
|
943
961
|
spanId: `user:${l.id ?? step}`,
|
|
944
962
|
parentSpanId: rootId,
|
|
945
|
-
name: "user.prompt",
|
|
946
|
-
kind: "CHAIN",
|
|
947
963
|
startTime: ts,
|
|
964
|
+
content: text,
|
|
948
965
|
service: SERVICE5,
|
|
949
966
|
agent: SERVICE5,
|
|
950
|
-
step
|
|
951
|
-
content: text
|
|
967
|
+
step
|
|
952
968
|
})
|
|
953
969
|
);
|
|
954
970
|
step += 1;
|
|
@@ -1010,9 +1026,8 @@ function tokens(t) {
|
|
|
1010
1026
|
if (!t) return null;
|
|
1011
1027
|
return t.actual ?? t.approx ?? null;
|
|
1012
1028
|
}
|
|
1013
|
-
var CONTENT_CAP5 = 8e3;
|
|
1014
1029
|
function textOf5(content) {
|
|
1015
|
-
return (content ?? "")
|
|
1030
|
+
return capText(content ?? "");
|
|
1016
1031
|
}
|
|
1017
1032
|
var ForgeAdapter = class {
|
|
1018
1033
|
harness = "forge";
|
|
@@ -1070,17 +1085,15 @@ var ForgeAdapter = class {
|
|
|
1070
1085
|
const prompt = textOf5(entry.text.content);
|
|
1071
1086
|
if (prompt) {
|
|
1072
1087
|
spans.push(
|
|
1073
|
-
|
|
1088
|
+
userPromptSpan({
|
|
1074
1089
|
traceId,
|
|
1075
1090
|
spanId: `user:${step}`,
|
|
1076
1091
|
parentSpanId: rootId,
|
|
1077
|
-
name: "user.prompt",
|
|
1078
|
-
kind: "CHAIN",
|
|
1079
1092
|
startTime: ts,
|
|
1093
|
+
content: prompt,
|
|
1080
1094
|
service: SERVICE6,
|
|
1081
1095
|
agent: SERVICE6,
|
|
1082
|
-
step
|
|
1083
|
-
content: prompt
|
|
1096
|
+
step
|
|
1084
1097
|
})
|
|
1085
1098
|
);
|
|
1086
1099
|
step += 1;
|
|
@@ -1146,10 +1159,9 @@ function iso(ts, fallback) {
|
|
|
1146
1159
|
function toolStatusError(status) {
|
|
1147
1160
|
return status != null && /error|fail|cancel/i.test(status);
|
|
1148
1161
|
}
|
|
1149
|
-
var CONTENT_CAP6 = 8e3;
|
|
1150
1162
|
function textOf6(content) {
|
|
1151
1163
|
const s = typeof content === "string" ? content : JSON.stringify(content ?? "");
|
|
1152
|
-
return s
|
|
1164
|
+
return capText(s);
|
|
1153
1165
|
}
|
|
1154
1166
|
var GeminiFamilyAdapter = class {
|
|
1155
1167
|
harness;
|
|
@@ -1231,17 +1243,15 @@ var GeminiFamilyAdapter = class {
|
|
|
1231
1243
|
const prompt = textOf6(m.content);
|
|
1232
1244
|
if (prompt) {
|
|
1233
1245
|
spans.push(
|
|
1234
|
-
|
|
1246
|
+
userPromptSpan({
|
|
1235
1247
|
traceId,
|
|
1236
1248
|
spanId: `${llmId}:user`,
|
|
1237
1249
|
parentSpanId: rootId,
|
|
1238
|
-
name: "user.prompt",
|
|
1239
|
-
kind: "CHAIN",
|
|
1240
1250
|
startTime: ts,
|
|
1251
|
+
content: prompt,
|
|
1241
1252
|
service: this.service,
|
|
1242
1253
|
agent: this.service,
|
|
1243
|
-
step
|
|
1244
|
-
content: prompt
|
|
1254
|
+
step
|
|
1245
1255
|
})
|
|
1246
1256
|
);
|
|
1247
1257
|
step += 1;
|
|
@@ -1304,9 +1314,10 @@ var SERVICE7 = "opencode";
|
|
|
1304
1314
|
function isoOf(ms) {
|
|
1305
1315
|
return new Date(ms ?? 0).toISOString();
|
|
1306
1316
|
}
|
|
1307
|
-
var CONTENT_CAP7 = 8e3;
|
|
1308
1317
|
function textOf7(parts) {
|
|
1309
|
-
return
|
|
1318
|
+
return capText(
|
|
1319
|
+
parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("\n")
|
|
1320
|
+
);
|
|
1310
1321
|
}
|
|
1311
1322
|
var OpencodeAdapter = class {
|
|
1312
1323
|
harness = "opencode";
|
|
@@ -1407,12 +1418,10 @@ var OpencodeAdapter = class {
|
|
|
1407
1418
|
step += 1;
|
|
1408
1419
|
if (msg.role === "user" && turnText) {
|
|
1409
1420
|
spans.push(
|
|
1410
|
-
|
|
1421
|
+
userPromptSpan({
|
|
1411
1422
|
traceId,
|
|
1412
1423
|
spanId: `${llmId}:user`,
|
|
1413
1424
|
parentSpanId: rootId,
|
|
1414
|
-
name: "user.prompt",
|
|
1415
|
-
kind: "CHAIN",
|
|
1416
1425
|
startTime: isoOf(msg.time?.created),
|
|
1417
1426
|
service: SERVICE7,
|
|
1418
1427
|
agent: SERVICE7,
|
|
@@ -1472,10 +1481,11 @@ function isToolBlock(b) {
|
|
|
1472
1481
|
function isToolResultBlock(b) {
|
|
1473
1482
|
return typeof b.type === "string" && /tool[_-]?result|tool[_-]?output/i.test(b.type);
|
|
1474
1483
|
}
|
|
1475
|
-
var CONTENT_CAP8 = 8e3;
|
|
1476
1484
|
function textOf8(content) {
|
|
1477
1485
|
if (!content) return "";
|
|
1478
|
-
return
|
|
1486
|
+
return capText(
|
|
1487
|
+
content.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("")
|
|
1488
|
+
);
|
|
1479
1489
|
}
|
|
1480
1490
|
var PiAdapter = class {
|
|
1481
1491
|
harness = "pi";
|
|
@@ -1547,12 +1557,10 @@ var PiAdapter = class {
|
|
|
1547
1557
|
const prompt = textOf8(msg.content);
|
|
1548
1558
|
if (prompt) {
|
|
1549
1559
|
spans.push(
|
|
1550
|
-
|
|
1560
|
+
userPromptSpan({
|
|
1551
1561
|
traceId,
|
|
1552
1562
|
spanId: `${mid}:user`,
|
|
1553
1563
|
parentSpanId: rootId,
|
|
1554
|
-
name: "user.prompt",
|
|
1555
|
-
kind: "CHAIN",
|
|
1556
1564
|
startTime: ts,
|
|
1557
1565
|
service: SERVICE8,
|
|
1558
1566
|
agent: SERVICE8,
|
|
@@ -1718,7 +1726,7 @@ var QwenAdapter = class {
|
|
|
1718
1726
|
inputTokens: r.usageMetadata?.promptTokenCount ?? null,
|
|
1719
1727
|
outputTokens: r.usageMetadata?.candidatesTokenCount ?? null,
|
|
1720
1728
|
step,
|
|
1721
|
-
content: textOf9(r.message?.parts)
|
|
1729
|
+
content: capText(textOf9(r.message?.parts)) || null
|
|
1722
1730
|
})
|
|
1723
1731
|
);
|
|
1724
1732
|
lastLlm = llmId;
|
|
@@ -1746,15 +1754,13 @@ var QwenAdapter = class {
|
|
|
1746
1754
|
step += 1;
|
|
1747
1755
|
}
|
|
1748
1756
|
} else if (r.type === "user") {
|
|
1749
|
-
const prompt = textOf9(r.message?.parts)
|
|
1757
|
+
const prompt = capText(textOf9(r.message?.parts));
|
|
1750
1758
|
if (prompt) {
|
|
1751
1759
|
spans.push(
|
|
1752
|
-
|
|
1760
|
+
userPromptSpan({
|
|
1753
1761
|
traceId,
|
|
1754
1762
|
spanId: `user:${r.uuid ?? step}`,
|
|
1755
1763
|
parentSpanId: rootId,
|
|
1756
|
-
name: "user.prompt",
|
|
1757
|
-
kind: "CHAIN",
|
|
1758
1764
|
startTime: ts,
|
|
1759
1765
|
service: SERVICE9,
|
|
1760
1766
|
agent: SERVICE9,
|
|
@@ -1960,6 +1966,12 @@ var CODING_REDACTION_RULES = [
|
|
|
1960
1966
|
{
|
|
1961
1967
|
id: "assigned-secret",
|
|
1962
1968
|
pattern: /\b(?:api[_-]?key|secret|token|password|passwd|access[_-]?token|client[_-]?secret)\b\s*[:=]\s*["']?[A-Za-z0-9._\-]{12,}["']?/gi
|
|
1969
|
+
},
|
|
1970
|
+
// Credentials embedded in URLs — common when a prompt pastes a curl/clone line.
|
|
1971
|
+
{ id: "url-userinfo", pattern: /\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:[^\s/@]+@/gi },
|
|
1972
|
+
{
|
|
1973
|
+
id: "url-secret-param",
|
|
1974
|
+
pattern: /[?&](?:access[_-]?token|api[_-]?key|apikey|auth|token|secret|password|sig|signature)=[^&\s"'#)]+/gi
|
|
1963
1975
|
}
|
|
1964
1976
|
];
|
|
1965
1977
|
var TRACES_REDACTION_RULES = [...DEFAULT_REDACTION_RULES, ...CODING_REDACTION_RULES];
|
|
@@ -2088,8 +2100,15 @@ function toTraceSpanEvents(spans, rootMeta) {
|
|
|
2088
2100
|
};
|
|
2089
2101
|
});
|
|
2090
2102
|
}
|
|
2103
|
+
function stripSpanContent(spans) {
|
|
2104
|
+
return spans.map((s) => {
|
|
2105
|
+
if (s.attributes["content"] == null) return s;
|
|
2106
|
+
const { content: _drop, ...attributes } = s.attributes;
|
|
2107
|
+
return { ...s, attributes };
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2091
2110
|
async function executeUpload(plan, opts = {}) {
|
|
2092
|
-
const newItems = plan.items.filter((i) => i.isNew);
|
|
2111
|
+
const newItems = plan.items.filter((i) => i.isNew).map((i) => opts.stripContent ? { ...i, spans: stripSpanContent(i.spans) } : i);
|
|
2093
2112
|
const skipped = plan.items.length - newItems.length;
|
|
2094
2113
|
const redactionCount = newItems.reduce((n, i) => n + i.redaction.redactionCount, 0);
|
|
2095
2114
|
const uploadedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2146,6 +2165,9 @@ export {
|
|
|
2146
2165
|
parseSince,
|
|
2147
2166
|
toRuntimeStore,
|
|
2148
2167
|
runPipelines,
|
|
2168
|
+
CONTENT_CAP,
|
|
2169
|
+
capText,
|
|
2170
|
+
userPromptSpan,
|
|
2149
2171
|
AmpAdapter,
|
|
2150
2172
|
ClaudeAdapter,
|
|
2151
2173
|
CodexAdapter,
|
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
selectAdapters,
|
|
11
11
|
watchSessions,
|
|
12
12
|
writeOtlpFile
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-HF2KGEBW.js";
|
|
14
14
|
|
|
15
15
|
// src/cli.ts
|
|
16
16
|
import { stat, writeFile } from "fs/promises";
|
|
@@ -105,7 +105,8 @@ function parseArgs(argv) {
|
|
|
105
105
|
window: 30,
|
|
106
106
|
minLoop: 3,
|
|
107
107
|
dryRun: false,
|
|
108
|
-
yes: false
|
|
108
|
+
yes: false,
|
|
109
|
+
noContent: false
|
|
109
110
|
};
|
|
110
111
|
for (let i = 1; i < argv.length; i++) {
|
|
111
112
|
const arg = argv[i];
|
|
@@ -154,6 +155,9 @@ function parseArgs(argv) {
|
|
|
154
155
|
case "--dry-run":
|
|
155
156
|
a.dryRun = true;
|
|
156
157
|
break;
|
|
158
|
+
case "--no-content":
|
|
159
|
+
a.noContent = true;
|
|
160
|
+
break;
|
|
157
161
|
case "--yes":
|
|
158
162
|
case "-y":
|
|
159
163
|
a.yes = true;
|
|
@@ -322,7 +326,7 @@ Window: since ${new Date(sinceMs).toISOString()}
|
|
|
322
326
|
return;
|
|
323
327
|
}
|
|
324
328
|
if (args.dryRun) {
|
|
325
|
-
const res2 = await executeUpload(plan, { dryRun: true, otlpOut: args.otlp });
|
|
329
|
+
const res2 = await executeUpload(plan, { dryRun: true, otlpOut: args.otlp, stripContent: args.noContent });
|
|
326
330
|
console.log(`dry run \u2014 ${newItems.length} session(s), ${totalRedactions} redaction(s). Redacted OTLP \u2192 ${res2.otlpPath}`);
|
|
327
331
|
console.log("No upload performed. Set TANGLE_INGEST_URL / TANGLE_INGEST_API_KEY / TANGLE_TENANT_ID and drop --dry-run to send.");
|
|
328
332
|
return;
|
|
@@ -335,7 +339,7 @@ Window: since ${new Date(sinceMs).toISOString()}
|
|
|
335
339
|
}
|
|
336
340
|
}
|
|
337
341
|
const res = await executeUpload(plan, { log: (m) => w(`${m}
|
|
338
|
-
`) });
|
|
342
|
+
`), stripContent: args.noContent });
|
|
339
343
|
console.log(
|
|
340
344
|
`Uploaded ${res.uploadedSessions} session(s), ${res.acceptedSpans} spans accepted, ${res.redactionCount} redaction(s); ${res.skippedSessions} already-uploaded skipped.`
|
|
341
345
|
);
|
|
@@ -365,6 +369,7 @@ Options:
|
|
|
365
369
|
--window <m> watch: only sessions active in the last N minutes (default 30)
|
|
366
370
|
--min-loop <n> Min identical repeated calls to flag a loop (default 3)
|
|
367
371
|
--dry-run upload: redact + dedup + preview, write OTLP, but do NOT send
|
|
372
|
+
--no-content upload: strip prompt/response text \u2014 send metadata only
|
|
368
373
|
--yes, -y upload: skip the confirmation prompt
|
|
369
374
|
|
|
370
375
|
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
|
@@ -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
|
*
|
|
@@ -589,9 +620,15 @@ declare function watchSessions(opts: ObserverOptions): Promise<void>;
|
|
|
589
620
|
* Reuses agent-eval's `redactValue` + `DEFAULT_REDACTION_RULES` (email, IPs,
|
|
590
621
|
* generic secret keys, …) and layers on coding-session-specific rules that the
|
|
591
622
|
* defaults miss — GitHub tokens, cloud keys, JWTs, bearer headers, private-key
|
|
592
|
-
* blocks,
|
|
593
|
-
* runs over every span attribute + status message
|
|
594
|
-
* is already scrubbed.
|
|
623
|
+
* blocks, `key=secret` assignments, and credentials embedded in URLs. Redaction
|
|
624
|
+
* runs over every span attribute + status message (including captured prompt /
|
|
625
|
+
* response `content`), so what leaves the machine is already scrubbed.
|
|
626
|
+
*
|
|
627
|
+
* Scope, stated honestly: this is **best-effort regex** for *structured* secrets
|
|
628
|
+
* and credentials. It does NOT catch free-form PII — names, postal addresses,
|
|
629
|
+
* phone numbers, account numbers in prose — which need a context-aware model.
|
|
630
|
+
* For that assurance, run an ML PII scrubber (e.g. openai/privacy-filter) on the
|
|
631
|
+
* ingest side of the platform, or upload metadata-only with `--no-content`.
|
|
595
632
|
*/
|
|
596
633
|
|
|
597
634
|
/** Secrets common in coding-agent traces that the substrate defaults don't cover. */
|
|
@@ -709,9 +746,13 @@ interface ExecuteOptions {
|
|
|
709
746
|
otlpOut?: string;
|
|
710
747
|
/** Custom sink. Defaults to the hosted Tangle Intelligence client from env. */
|
|
711
748
|
backend?: UploadBackend;
|
|
749
|
+
/** Metadata-only upload: drop captured prompt/response `content` from every
|
|
750
|
+
* span before it leaves the machine (still keeps tool calls, tokens, timing,
|
|
751
|
+
* loop signal). The strongest privacy posture when prose can't leave. */
|
|
752
|
+
stripContent?: boolean;
|
|
712
753
|
log?: (msg: string) => void;
|
|
713
754
|
}
|
|
714
755
|
/** Send the NEW items (or, on dryRun, write the redacted OTLP that would send). */
|
|
715
756
|
declare function executeUpload(plan: UploadPlan, opts?: ExecuteOptions): Promise<UploadResult>;
|
|
716
757
|
|
|
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 };
|
|
758
|
+
export { ATTR, AmpAdapter, type AnalyzeOptions, type AnalyzeResult, CODING_REDACTION_RULES, CONTENT_CAP, 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, capText, collectSessions, executeUpload, knownHarnesses, listAdapters, loadState, parseIsoToEpochMs, parseSince, planUpload, redactSpans, resolveAdapter, 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,6 +17,7 @@ import {
|
|
|
16
17
|
TRACES_REDACTION_RULES,
|
|
17
18
|
alreadyUploaded,
|
|
18
19
|
analyzeSpans,
|
|
20
|
+
capText,
|
|
19
21
|
executeUpload,
|
|
20
22
|
knownHarnesses,
|
|
21
23
|
listAdapters,
|
|
@@ -35,9 +37,10 @@ import {
|
|
|
35
37
|
toRuntimeStore,
|
|
36
38
|
toTraceSpanEvents,
|
|
37
39
|
uploadKey,
|
|
40
|
+
userPromptSpan,
|
|
38
41
|
watchSessions,
|
|
39
42
|
writeOtlpFile
|
|
40
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-HF2KGEBW.js";
|
|
41
44
|
|
|
42
45
|
// src/collect.ts
|
|
43
46
|
async function collectSessions(opts = {}) {
|
|
@@ -66,6 +69,7 @@ export {
|
|
|
66
69
|
AmpAdapter,
|
|
67
70
|
AnalystRegistry,
|
|
68
71
|
CODING_REDACTION_RULES,
|
|
72
|
+
CONTENT_CAP,
|
|
69
73
|
ClaudeAdapter,
|
|
70
74
|
CodexAdapter,
|
|
71
75
|
CopilotAdapter,
|
|
@@ -82,6 +86,7 @@ export {
|
|
|
82
86
|
alreadyUploaded,
|
|
83
87
|
analyzeSpans,
|
|
84
88
|
buildDefaultAnalystRegistry,
|
|
89
|
+
capText,
|
|
85
90
|
collectSessions,
|
|
86
91
|
createHostedClient,
|
|
87
92
|
executeUpload,
|
|
@@ -107,6 +112,7 @@ export {
|
|
|
107
112
|
toRuntimeStore,
|
|
108
113
|
toTraceSpanEvents,
|
|
109
114
|
uploadKey,
|
|
115
|
+
userPromptSpan,
|
|
110
116
|
watchSessions,
|
|
111
117
|
writeOtlpFile
|
|
112
118
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/traces",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|