@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 +43 -3
- package/dist/{chunk-33NBMIH4.js → chunk-6TVW7PL6.js} +218 -55
- package/dist/cli.js +50 -8
- package/dist/index.d.ts +141 -5
- package/dist/index.js +19 -1
- package/package.json +1 -1
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
|
|
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
|
|
113
|
-
traces upload --since 24h
|
|
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 (
|
|
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
|
-
|
|
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
|
|
296
|
-
return
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 ?? "")
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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)
|
|
1865
|
+
const prompt = capText(textOf9(r.message?.parts));
|
|
1750
1866
|
if (prompt) {
|
|
1751
1867
|
spans.push(
|
|
1752
|
-
|
|
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
|
-
|
|
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-
|
|
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(
|
|
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
|
-
|
|
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,
|
|
593
|
-
* runs over every span attribute + status message
|
|
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-
|
|
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.
|
|
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",
|