@tangle-network/traces 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -1
- package/dist/{chunk-HF2KGEBW.js → chunk-Q56AP2EY.js} +212 -33
- package/dist/cli.js +52 -10
- package/dist/index.d.ts +119 -7
- package/dist/index.js +15 -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)
|
|
@@ -42,7 +43,7 @@ That's the command in the demo above. The **deterministic pass** — stuck loops
|
|
|
42
43
|
|
|
43
44
|
Add `--llm` for the **agentic analysts** (failure-mode / knowledge-gap / knowledge-poisoning / improvement); they call OpenAI and respect `--budget <usd>`.
|
|
44
45
|
|
|
45
|
-
Every run also writes an **OTLP-JSONL artifact
|
|
46
|
+
Every run also writes an **OTLP-JSONL artifact**, and you can run external engines like [HALO](https://github.com/context-labs/halo) over it with `--analyzer halo` (traces converts our spans to the canonical OpenInference shape HALO needs) — see [External engines](#external-engines-bring-your-own). Analysis is never locked to one engine.
|
|
46
47
|
|
|
47
48
|
## What it finds
|
|
48
49
|
|
|
@@ -129,6 +130,31 @@ It does **not** catch free-form PII — names, postal addresses, phone numbers i
|
|
|
129
130
|
|
|
130
131
|
Always `--dry-run` first to see exactly what would be sent.
|
|
131
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
|
+
HALO needs canonical OpenInference, so `--analyzer halo` converts our spans automatically (`toCanonicalOpenInference`). HALO runs its *own* LLM (OpenAI client — set `OPENAI_BASE_URL` / `OPENAI_API_KEY`, or use HALO's provider); `--model` is forwarded to it. traces supplies the trace and drives the CLI; it doesn't pay for or configure HALO's model.
|
|
146
|
+
|
|
147
|
+
**Redactors** scrub prompt/response prose with an external PII model (catching names/addresses the regex pass can't), running *after* the built-in redaction:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# the command reads a JSON array of strings on stdin, writes the scrubbed array on stdout
|
|
151
|
+
traces upload --since 24h --dry-run --redactor "my-pii-scrubber"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
In the SDK these are the `ExternalAnalyzer` and `Redactor` interfaces (`haloAnalyzer`, `commandAnalyzer`, `commandRedactor`, `applyRedactor`, `runExternalAnalyzers`). See [`examples/external-engines.ts`](./examples/external-engines.ts).
|
|
155
|
+
|
|
156
|
+
> 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.
|
|
157
|
+
|
|
132
158
|
## Library (SDK)
|
|
133
159
|
|
|
134
160
|
The CLI is a thin consumer of these exports.
|
|
@@ -143,6 +169,7 @@ The CLI is a thin consumer of these exports.
|
|
|
143
169
|
| `planUpload` / `executeUpload` | `(…, { backend? }) → …` | redact + dedup + send to any sink |
|
|
144
170
|
| `selectAdapters` / `listAdapters` / `resolveAdapter` | adapter selection + the harness registry |
|
|
145
171
|
| `HarnessTraceAdapter` | interface (`locate` + `parse`) | implement to add a harness |
|
|
172
|
+
| `ExternalAnalyzer` / `Redactor` | `haloAnalyzer` / `commandAnalyzer` / `commandRedactor` | drive engines/models you install (not bundled) |
|
|
146
173
|
|
|
147
174
|
```ts
|
|
148
175
|
import { watchSessions, analyzeSpans, AnalystRegistry, makeFinding } from '@tangle-network/traces'
|
|
@@ -172,6 +199,7 @@ Runnable, in [`examples/`](./examples):
|
|
|
172
199
|
| [`custom-analyst.ts`](./examples/custom-analyst.ts) | register and run your own analyst |
|
|
173
200
|
| [`custom-backend.ts`](./examples/custom-backend.ts) | redact + dedup + upload to your own sink |
|
|
174
201
|
| [`register-harness.ts`](./examples/register-harness.ts) | add a new harness by implementing `HarnessTraceAdapter` |
|
|
202
|
+
| [`external-engines.ts`](./examples/external-engines.ts) | drive HALO + an external PII scrubber you install yourself |
|
|
175
203
|
|
|
176
204
|
## Develop
|
|
177
205
|
|
|
@@ -31,11 +31,11 @@ function serializeSpans(spans) {
|
|
|
31
31
|
`;
|
|
32
32
|
}
|
|
33
33
|
async function writeOtlpFile(spans, outPath) {
|
|
34
|
-
const { mkdtemp, writeFile:
|
|
34
|
+
const { mkdtemp, writeFile: writeFile3 } = await import("fs/promises");
|
|
35
35
|
const { tmpdir } = await import("os");
|
|
36
36
|
const { join: join13 } = await import("path");
|
|
37
37
|
const path = outPath ?? join13(await mkdtemp(join13(tmpdir(), "traces-")), "spans.otlp.jsonl");
|
|
38
|
-
await
|
|
38
|
+
await writeFile3(path, serializeSpans(spans), "utf8");
|
|
39
39
|
return path;
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -70,6 +70,151 @@ 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
|
+
import { readFile, writeFile } from "fs/promises";
|
|
76
|
+
function runCommand(command, args = [], opts = {}) {
|
|
77
|
+
const timeoutMs = opts.timeoutMs ?? 12e4;
|
|
78
|
+
const maxBuffer = opts.maxBuffer ?? 32 * 1024 * 1024;
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
let child;
|
|
81
|
+
try {
|
|
82
|
+
child = spawn(command, [...args], { signal: opts.signal });
|
|
83
|
+
} catch (err2) {
|
|
84
|
+
reject(err2 instanceof Error ? err2 : new Error(String(err2)));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
let out = "";
|
|
88
|
+
let err = "";
|
|
89
|
+
let killed = false;
|
|
90
|
+
const timer = setTimeout(() => {
|
|
91
|
+
killed = true;
|
|
92
|
+
child.kill("SIGKILL");
|
|
93
|
+
reject(new Error(`${command}: timed out after ${timeoutMs}ms`));
|
|
94
|
+
}, timeoutMs);
|
|
95
|
+
const cap = (add, buf) => {
|
|
96
|
+
const next = buf + add;
|
|
97
|
+
if (next.length > maxBuffer) {
|
|
98
|
+
killed = true;
|
|
99
|
+
child.kill("SIGKILL");
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
reject(new Error(`${command}: output exceeded ${maxBuffer} bytes`));
|
|
102
|
+
}
|
|
103
|
+
return next;
|
|
104
|
+
};
|
|
105
|
+
child.stdout?.on("data", (d) => {
|
|
106
|
+
out = cap(String(d), out);
|
|
107
|
+
});
|
|
108
|
+
child.stderr?.on("data", (d) => {
|
|
109
|
+
err = cap(String(d), err);
|
|
110
|
+
});
|
|
111
|
+
child.on("error", (e) => {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
if (!killed) reject(e);
|
|
114
|
+
});
|
|
115
|
+
child.on("close", (code) => {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
if (!killed) resolve({ stdout: out, stderr: err, code });
|
|
118
|
+
});
|
|
119
|
+
if (opts.input !== void 0) {
|
|
120
|
+
child.stdin?.on("error", () => {
|
|
121
|
+
});
|
|
122
|
+
child.stdin?.end(opts.input);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function commandAnalyzer(spec) {
|
|
127
|
+
return {
|
|
128
|
+
name: spec.name,
|
|
129
|
+
async analyze(otlpPath, opts = {}) {
|
|
130
|
+
try {
|
|
131
|
+
const res = await runCommand(spec.command, spec.args(otlpPath, opts.prompt), {
|
|
132
|
+
signal: opts.signal,
|
|
133
|
+
timeoutMs: spec.timeoutMs
|
|
134
|
+
});
|
|
135
|
+
if (res.code !== 0) {
|
|
136
|
+
return { analyzer: spec.name, ok: false, output: res.stdout, error: res.stderr.trim() || `exit ${res.code}` };
|
|
137
|
+
}
|
|
138
|
+
return { analyzer: spec.name, ok: true, output: res.stdout.trim(), findings: spec.parse?.(res.stdout) };
|
|
139
|
+
} catch (e) {
|
|
140
|
+
return { analyzer: spec.name, ok: false, output: "", error: e instanceof Error ? e.message : String(e) };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function toCanonicalOpenInference(otlpJsonl, opts = {}) {
|
|
146
|
+
return otlpJsonl.split("\n").filter((l) => l.trim()).map((line) => {
|
|
147
|
+
const s = JSON.parse(line);
|
|
148
|
+
const a = s.attributes ?? {};
|
|
149
|
+
const resourceAttrs = { "service.name": a["service.name"] ?? opts.service ?? "traces" };
|
|
150
|
+
if (a["agent.name"]) resourceAttrs["agent.name"] = a["agent.name"];
|
|
151
|
+
return JSON.stringify({
|
|
152
|
+
trace_id: s.trace_id,
|
|
153
|
+
span_id: s.span_id,
|
|
154
|
+
parent_span_id: s.parent_span_id ?? "",
|
|
155
|
+
trace_state: "",
|
|
156
|
+
name: s.name,
|
|
157
|
+
kind: a["openinference.span.kind"] ?? "CHAIN",
|
|
158
|
+
start_time: s.start_time,
|
|
159
|
+
end_time: s.end_time,
|
|
160
|
+
status: { code: s.status?.code ?? "OK", message: s.status?.message ?? "" },
|
|
161
|
+
resource: { attributes: resourceAttrs },
|
|
162
|
+
scope: { name: opts.scopeName ?? "tangle-traces", version: "" },
|
|
163
|
+
attributes: a
|
|
164
|
+
});
|
|
165
|
+
}).join("\n");
|
|
166
|
+
}
|
|
167
|
+
function haloAnalyzer(opts = {}) {
|
|
168
|
+
const command = opts.command ?? "halo";
|
|
169
|
+
const defaultPrompt = opts.defaultPrompt ?? "diagnose";
|
|
170
|
+
return {
|
|
171
|
+
name: "halo",
|
|
172
|
+
async analyze(otlpPath, o = {}) {
|
|
173
|
+
try {
|
|
174
|
+
const canonical = toCanonicalOpenInference(await readFile(otlpPath, "utf8"));
|
|
175
|
+
const canonPath = `${otlpPath}.halo.jsonl`;
|
|
176
|
+
await writeFile(canonPath, canonical);
|
|
177
|
+
const args = [canonPath, "-p", o.prompt ?? defaultPrompt];
|
|
178
|
+
if (opts.model) args.push("-m", opts.model);
|
|
179
|
+
const res = await runCommand(command, args, { signal: o.signal, timeoutMs: opts.timeoutMs });
|
|
180
|
+
if (res.code !== 0) {
|
|
181
|
+
return { analyzer: "halo", ok: false, output: res.stdout, error: res.stderr.trim() || `exit ${res.code}` };
|
|
182
|
+
}
|
|
183
|
+
return { analyzer: "halo", ok: true, output: res.stdout.trim() };
|
|
184
|
+
} catch (e) {
|
|
185
|
+
return { analyzer: "halo", ok: false, output: "", error: e instanceof Error ? e.message : String(e) };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function runExternalAnalyzers(otlpPath, analyzers, opts = {}) {
|
|
191
|
+
return Promise.all(analyzers.map((a) => a.analyze(otlpPath, opts)));
|
|
192
|
+
}
|
|
193
|
+
function commandRedactor(spec) {
|
|
194
|
+
const name = spec.name ?? spec.command;
|
|
195
|
+
return {
|
|
196
|
+
name,
|
|
197
|
+
async redactText(texts) {
|
|
198
|
+
if (texts.length === 0) return [];
|
|
199
|
+
const res = await runCommand(spec.command, spec.args ?? [], {
|
|
200
|
+
input: JSON.stringify(texts),
|
|
201
|
+
timeoutMs: spec.timeoutMs
|
|
202
|
+
});
|
|
203
|
+
if (res.code !== 0) throw new Error(`redactor ${name}: exit ${res.code}: ${res.stderr.trim()}`);
|
|
204
|
+
let parsed;
|
|
205
|
+
try {
|
|
206
|
+
parsed = JSON.parse(res.stdout);
|
|
207
|
+
} catch {
|
|
208
|
+
throw new Error(`redactor ${name}: stdout was not JSON`);
|
|
209
|
+
}
|
|
210
|
+
if (!Array.isArray(parsed) || parsed.length !== texts.length) {
|
|
211
|
+
throw new Error(`redactor ${name}: expected a JSON array of ${texts.length} strings`);
|
|
212
|
+
}
|
|
213
|
+
return parsed.map(String);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
73
218
|
// src/time.ts
|
|
74
219
|
function parseIsoToEpochMs(ts) {
|
|
75
220
|
if (!ts) return 0;
|
|
@@ -162,7 +307,7 @@ function userPromptSpan(o) {
|
|
|
162
307
|
}
|
|
163
308
|
|
|
164
309
|
// src/adapters/amp.ts
|
|
165
|
-
import { readdir, readFile, stat } from "fs/promises";
|
|
310
|
+
import { readdir, readFile as readFile2, stat } from "fs/promises";
|
|
166
311
|
import { homedir } from "os";
|
|
167
312
|
import { basename, join } from "path";
|
|
168
313
|
var SERVICE = "amp";
|
|
@@ -204,7 +349,7 @@ var AmpAdapter = class {
|
|
|
204
349
|
async parse(ref) {
|
|
205
350
|
let thread;
|
|
206
351
|
try {
|
|
207
|
-
thread = JSON.parse(await
|
|
352
|
+
thread = JSON.parse(await readFile2(ref.path, "utf8"));
|
|
208
353
|
} catch {
|
|
209
354
|
return [];
|
|
210
355
|
}
|
|
@@ -288,7 +433,7 @@ var AmpAdapter = class {
|
|
|
288
433
|
};
|
|
289
434
|
|
|
290
435
|
// src/adapters/claude.ts
|
|
291
|
-
import { readdir as readdir2, readFile as
|
|
436
|
+
import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
292
437
|
import { homedir as homedir2 } from "os";
|
|
293
438
|
import { basename as basename2, join as join2 } from "path";
|
|
294
439
|
var SERVICE2 = "claude-code";
|
|
@@ -449,7 +594,7 @@ var ClaudeAdapter = class {
|
|
|
449
594
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
450
595
|
}
|
|
451
596
|
async parse(ref) {
|
|
452
|
-
const raw = await
|
|
597
|
+
const raw = await readFile3(ref.path, "utf8");
|
|
453
598
|
const events = parseLines(raw);
|
|
454
599
|
const traceId = events.find((e) => e.sessionId)?.sessionId ?? ref.sessionId;
|
|
455
600
|
const rootId = `root:${traceId}`;
|
|
@@ -492,13 +637,13 @@ var ClaudeAdapter = class {
|
|
|
492
637
|
const hash = basename2(f, ".jsonl");
|
|
493
638
|
let meta = {};
|
|
494
639
|
try {
|
|
495
|
-
meta = JSON.parse(await
|
|
640
|
+
meta = JSON.parse(await readFile3(join2(subDir, `${hash}.meta.json`), "utf8"));
|
|
496
641
|
} catch {
|
|
497
642
|
}
|
|
498
643
|
const parent = meta.toolUseId && main.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
|
|
499
644
|
let raw;
|
|
500
645
|
try {
|
|
501
|
-
raw = await
|
|
646
|
+
raw = await readFile3(join2(subDir, f), "utf8");
|
|
502
647
|
} catch {
|
|
503
648
|
continue;
|
|
504
649
|
}
|
|
@@ -516,7 +661,7 @@ var ClaudeAdapter = class {
|
|
|
516
661
|
};
|
|
517
662
|
|
|
518
663
|
// src/adapters/codex.ts
|
|
519
|
-
import { readdir as readdir3, readFile as
|
|
664
|
+
import { readdir as readdir3, readFile as readFile4, stat as stat3 } from "fs/promises";
|
|
520
665
|
import { homedir as homedir3 } from "os";
|
|
521
666
|
import { basename as basename3, join as join3 } from "path";
|
|
522
667
|
var SERVICE3 = "codex";
|
|
@@ -600,7 +745,7 @@ var CodexAdapter = class {
|
|
|
600
745
|
continue;
|
|
601
746
|
}
|
|
602
747
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
603
|
-
const head = (await
|
|
748
|
+
const head = (await readFile4(path, "utf8").catch(() => "")).split("\n", 1)[0] ?? "";
|
|
604
749
|
let cwd = null;
|
|
605
750
|
let id = basename3(path).replace(/^rollout-[\dT-]+-/, "").replace(/\.jsonl$/, "");
|
|
606
751
|
try {
|
|
@@ -615,7 +760,7 @@ var CodexAdapter = class {
|
|
|
615
760
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
616
761
|
}
|
|
617
762
|
async parse(ref) {
|
|
618
|
-
const lines = parseLines2(await
|
|
763
|
+
const lines = parseLines2(await readFile4(ref.path, "utf8"));
|
|
619
764
|
const meta = lines.find((l) => l.type === "session_meta");
|
|
620
765
|
const traceId = meta?.payload?.id ?? ref.sessionId;
|
|
621
766
|
const model = lines.find((l) => l.type === "turn_context")?.payload?.model ?? null;
|
|
@@ -731,7 +876,7 @@ var CodexAdapter = class {
|
|
|
731
876
|
};
|
|
732
877
|
|
|
733
878
|
// src/adapters/copilot.ts
|
|
734
|
-
import { readdir as readdir4, readFile as
|
|
879
|
+
import { readdir as readdir4, readFile as readFile5, stat as stat4 } from "fs/promises";
|
|
735
880
|
import { homedir as homedir4 } from "os";
|
|
736
881
|
import { join as join4 } from "path";
|
|
737
882
|
var SERVICE4 = "github-copilot";
|
|
@@ -776,7 +921,7 @@ var CopilotAdapter = class {
|
|
|
776
921
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
777
922
|
}
|
|
778
923
|
async parse(ref) {
|
|
779
|
-
const events = parseLines3(await
|
|
924
|
+
const events = parseLines3(await readFile5(ref.path, "utf8"));
|
|
780
925
|
const traceId = ref.sessionId;
|
|
781
926
|
const rootId = `root:${traceId}`;
|
|
782
927
|
const spans = [
|
|
@@ -857,7 +1002,7 @@ var CopilotAdapter = class {
|
|
|
857
1002
|
};
|
|
858
1003
|
|
|
859
1004
|
// src/adapters/factory.ts
|
|
860
|
-
import { readdir as readdir5, readFile as
|
|
1005
|
+
import { readdir as readdir5, readFile as readFile6, stat as stat5 } from "fs/promises";
|
|
861
1006
|
import { homedir as homedir5 } from "os";
|
|
862
1007
|
import { basename as basename4, join as join5 } from "path";
|
|
863
1008
|
var SERVICE5 = "factory";
|
|
@@ -918,13 +1063,13 @@ var FactoryAdapter = class {
|
|
|
918
1063
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
919
1064
|
}
|
|
920
1065
|
async parse(ref) {
|
|
921
|
-
const lines = parseLines4(await
|
|
1066
|
+
const lines = parseLines4(await readFile6(ref.path, "utf8"));
|
|
922
1067
|
const start = lines.find((l) => l.type === "session_start");
|
|
923
1068
|
const traceId = start?.id ?? ref.sessionId;
|
|
924
1069
|
const rootId = `root:${traceId}`;
|
|
925
1070
|
let settings = {};
|
|
926
1071
|
try {
|
|
927
|
-
settings = JSON.parse(await
|
|
1072
|
+
settings = JSON.parse(await readFile6(ref.path.replace(/\.jsonl$/, ".settings.json"), "utf8"));
|
|
928
1073
|
} catch {
|
|
929
1074
|
}
|
|
930
1075
|
const spans = [
|
|
@@ -1018,7 +1163,7 @@ var FactoryAdapter = class {
|
|
|
1018
1163
|
};
|
|
1019
1164
|
|
|
1020
1165
|
// src/adapters/forge.ts
|
|
1021
|
-
import { readdir as readdir6, readFile as
|
|
1166
|
+
import { readdir as readdir6, readFile as readFile7, stat as stat6 } from "fs/promises";
|
|
1022
1167
|
import { homedir as homedir6 } from "os";
|
|
1023
1168
|
import { basename as basename5, join as join6 } from "path";
|
|
1024
1169
|
var SERVICE6 = "forge";
|
|
@@ -1064,7 +1209,7 @@ var ForgeAdapter = class {
|
|
|
1064
1209
|
async parse(ref) {
|
|
1065
1210
|
let dump;
|
|
1066
1211
|
try {
|
|
1067
|
-
dump = JSON.parse(await
|
|
1212
|
+
dump = JSON.parse(await readFile7(ref.path, "utf8"));
|
|
1068
1213
|
} catch {
|
|
1069
1214
|
return [];
|
|
1070
1215
|
}
|
|
@@ -1148,7 +1293,7 @@ var ForgeAdapter = class {
|
|
|
1148
1293
|
};
|
|
1149
1294
|
|
|
1150
1295
|
// src/adapters/gemini.ts
|
|
1151
|
-
import { readdir as readdir7, readFile as
|
|
1296
|
+
import { readdir as readdir7, readFile as readFile8, stat as stat7 } from "fs/promises";
|
|
1152
1297
|
import { homedir as homedir7 } from "os";
|
|
1153
1298
|
import { join as join7 } from "path";
|
|
1154
1299
|
function iso(ts, fallback) {
|
|
@@ -1213,7 +1358,7 @@ var GeminiFamilyAdapter = class {
|
|
|
1213
1358
|
async parse(ref) {
|
|
1214
1359
|
let session;
|
|
1215
1360
|
try {
|
|
1216
|
-
session = JSON.parse(await
|
|
1361
|
+
session = JSON.parse(await readFile8(ref.path, "utf8"));
|
|
1217
1362
|
} catch {
|
|
1218
1363
|
return [];
|
|
1219
1364
|
}
|
|
@@ -1307,7 +1452,7 @@ var GeminiAdapter = class extends GeminiFamilyAdapter {
|
|
|
1307
1452
|
};
|
|
1308
1453
|
|
|
1309
1454
|
// src/adapters/opencode.ts
|
|
1310
|
-
import { readdir as readdir8, readFile as
|
|
1455
|
+
import { readdir as readdir8, readFile as readFile9, stat as stat8 } from "fs/promises";
|
|
1311
1456
|
import { homedir as homedir8 } from "os";
|
|
1312
1457
|
import { join as join8 } from "path";
|
|
1313
1458
|
var SERVICE7 = "opencode";
|
|
@@ -1362,7 +1507,7 @@ var OpencodeAdapter = class {
|
|
|
1362
1507
|
const messages = [];
|
|
1363
1508
|
for (const f of msgFiles) {
|
|
1364
1509
|
try {
|
|
1365
|
-
messages.push({ msg: JSON.parse(await
|
|
1510
|
+
messages.push({ msg: JSON.parse(await readFile9(join8(ref.path, f), "utf8")), file: f });
|
|
1366
1511
|
} catch {
|
|
1367
1512
|
}
|
|
1368
1513
|
}
|
|
@@ -1390,7 +1535,7 @@ var OpencodeAdapter = class {
|
|
|
1390
1535
|
const partFiles = (await readdir8(pdir)).filter((f) => f.endsWith(".json"));
|
|
1391
1536
|
for (const pf of partFiles) {
|
|
1392
1537
|
try {
|
|
1393
|
-
parts.push(JSON.parse(await
|
|
1538
|
+
parts.push(JSON.parse(await readFile9(join8(pdir, pf), "utf8")));
|
|
1394
1539
|
} catch {
|
|
1395
1540
|
}
|
|
1396
1541
|
}
|
|
@@ -1460,7 +1605,7 @@ var OpencodeAdapter = class {
|
|
|
1460
1605
|
};
|
|
1461
1606
|
|
|
1462
1607
|
// src/adapters/pi.ts
|
|
1463
|
-
import { readdir as readdir9, readFile as
|
|
1608
|
+
import { readdir as readdir9, readFile as readFile10, stat as stat9 } from "fs/promises";
|
|
1464
1609
|
import { homedir as homedir9 } from "os";
|
|
1465
1610
|
import { basename as basename6, join as join9 } from "path";
|
|
1466
1611
|
var SERVICE8 = "pi";
|
|
@@ -1528,7 +1673,7 @@ var PiAdapter = class {
|
|
|
1528
1673
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1529
1674
|
}
|
|
1530
1675
|
async parse(ref) {
|
|
1531
|
-
const lines = parseLines5(await
|
|
1676
|
+
const lines = parseLines5(await readFile10(ref.path, "utf8"));
|
|
1532
1677
|
const sessionLine = lines.find((l) => l.type === "session");
|
|
1533
1678
|
const traceId = sessionLine?.id ?? ref.sessionId;
|
|
1534
1679
|
const rootId = `root:${traceId}`;
|
|
@@ -1629,7 +1774,7 @@ var PiAdapter = class {
|
|
|
1629
1774
|
};
|
|
1630
1775
|
|
|
1631
1776
|
// src/adapters/qwen.ts
|
|
1632
|
-
import { readdir as readdir10, readFile as
|
|
1777
|
+
import { readdir as readdir10, readFile as readFile11, stat as stat10 } from "fs/promises";
|
|
1633
1778
|
import { homedir as homedir10 } from "os";
|
|
1634
1779
|
import { basename as basename7, join as join10 } from "path";
|
|
1635
1780
|
var SERVICE9 = "qwen";
|
|
@@ -1688,7 +1833,7 @@ var QwenAdapter = class {
|
|
|
1688
1833
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1689
1834
|
}
|
|
1690
1835
|
async parse(ref) {
|
|
1691
|
-
const records = parseLines6(await
|
|
1836
|
+
const records = parseLines6(await readFile11(ref.path, "utf8"));
|
|
1692
1837
|
const traceId = records.find((r) => r.sessionId)?.sessionId ?? ref.sessionId;
|
|
1693
1838
|
const rootId = `root:${traceId}`;
|
|
1694
1839
|
const spans = [
|
|
@@ -1988,10 +2133,32 @@ function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
|
|
|
1988
2133
|
});
|
|
1989
2134
|
return { spans: out, report };
|
|
1990
2135
|
}
|
|
2136
|
+
async function applyRedactor(spans, redactor) {
|
|
2137
|
+
const idx = [];
|
|
2138
|
+
const texts = [];
|
|
2139
|
+
spans.forEach((s, i) => {
|
|
2140
|
+
const c = s.attributes["content"];
|
|
2141
|
+
if (typeof c === "string" && c.length > 0) {
|
|
2142
|
+
idx.push(i);
|
|
2143
|
+
texts.push(c);
|
|
2144
|
+
}
|
|
2145
|
+
});
|
|
2146
|
+
const out = spans.map((s) => ({ ...s, attributes: { ...s.attributes } }));
|
|
2147
|
+
if (texts.length === 0) return { spans: out, changed: 0 };
|
|
2148
|
+
const scrubbed = await redactor.redactText(texts);
|
|
2149
|
+
let changed = 0;
|
|
2150
|
+
idx.forEach((spanI, k) => {
|
|
2151
|
+
if (scrubbed[k] !== texts[k]) {
|
|
2152
|
+
out[spanI].attributes["content"] = scrubbed[k];
|
|
2153
|
+
changed += 1;
|
|
2154
|
+
}
|
|
2155
|
+
});
|
|
2156
|
+
return { spans: out, changed };
|
|
2157
|
+
}
|
|
1991
2158
|
|
|
1992
2159
|
// src/upload-state.ts
|
|
1993
2160
|
import { createHash } from "crypto";
|
|
1994
|
-
import { mkdir, readFile as
|
|
2161
|
+
import { mkdir, readFile as readFile12, writeFile as writeFile2 } from "fs/promises";
|
|
1995
2162
|
import { homedir as homedir11 } from "os";
|
|
1996
2163
|
import { dirname, join as join11 } from "path";
|
|
1997
2164
|
function stateFile() {
|
|
@@ -2011,7 +2178,7 @@ function sessionHash(spans) {
|
|
|
2011
2178
|
}
|
|
2012
2179
|
async function loadState() {
|
|
2013
2180
|
try {
|
|
2014
|
-
return JSON.parse(await
|
|
2181
|
+
return JSON.parse(await readFile12(stateFile(), "utf8"));
|
|
2015
2182
|
} catch {
|
|
2016
2183
|
return {};
|
|
2017
2184
|
}
|
|
@@ -2019,7 +2186,7 @@ async function loadState() {
|
|
|
2019
2186
|
async function saveState(state) {
|
|
2020
2187
|
const f = stateFile();
|
|
2021
2188
|
await mkdir(dirname(f), { recursive: true });
|
|
2022
|
-
await
|
|
2189
|
+
await writeFile2(f, `${JSON.stringify(state, null, 2)}
|
|
2023
2190
|
`, "utf8");
|
|
2024
2191
|
}
|
|
2025
2192
|
function alreadyUploaded(state, harness, sessionId, hash) {
|
|
@@ -2027,7 +2194,7 @@ function alreadyUploaded(state, harness, sessionId, hash) {
|
|
|
2027
2194
|
}
|
|
2028
2195
|
|
|
2029
2196
|
// src/upload.ts
|
|
2030
|
-
import { readFile as
|
|
2197
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
2031
2198
|
import { createRequire } from "module";
|
|
2032
2199
|
import { hostname } from "os";
|
|
2033
2200
|
import { basename as basename8, join as join12 } from "path";
|
|
@@ -2054,7 +2221,7 @@ async function planUpload(opts = {}) {
|
|
|
2054
2221
|
async function gitBranch(cwd) {
|
|
2055
2222
|
if (!cwd) return void 0;
|
|
2056
2223
|
try {
|
|
2057
|
-
const head = await
|
|
2224
|
+
const head = await readFile13(join12(cwd, ".git", "HEAD"), "utf8");
|
|
2058
2225
|
const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
|
|
2059
2226
|
return m?.[1]?.trim();
|
|
2060
2227
|
} catch {
|
|
@@ -2108,7 +2275,12 @@ function stripSpanContent(spans) {
|
|
|
2108
2275
|
});
|
|
2109
2276
|
}
|
|
2110
2277
|
async function executeUpload(plan, opts = {}) {
|
|
2111
|
-
|
|
2278
|
+
let newItems = plan.items.filter((i) => i.isNew);
|
|
2279
|
+
if (opts.redactor && !opts.stripContent) {
|
|
2280
|
+
const r = opts.redactor;
|
|
2281
|
+
newItems = await Promise.all(newItems.map(async (i) => ({ ...i, spans: (await applyRedactor(i.spans, r)).spans })));
|
|
2282
|
+
}
|
|
2283
|
+
if (opts.stripContent) newItems = newItems.map((i) => ({ ...i, spans: stripSpanContent(i.spans) }));
|
|
2112
2284
|
const skipped = plan.items.length - newItems.length;
|
|
2113
2285
|
const redactionCount = newItems.reduce((n, i) => n + i.redaction.redactionCount, 0);
|
|
2114
2286
|
const uploadedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2161,6 +2333,12 @@ export {
|
|
|
2161
2333
|
serializeSpans,
|
|
2162
2334
|
writeOtlpFile,
|
|
2163
2335
|
analyzeSpans,
|
|
2336
|
+
runCommand,
|
|
2337
|
+
commandAnalyzer,
|
|
2338
|
+
toCanonicalOpenInference,
|
|
2339
|
+
haloAnalyzer,
|
|
2340
|
+
runExternalAnalyzers,
|
|
2341
|
+
commandRedactor,
|
|
2164
2342
|
parseIsoToEpochMs,
|
|
2165
2343
|
parseSince,
|
|
2166
2344
|
toRuntimeStore,
|
|
@@ -2190,6 +2368,7 @@ export {
|
|
|
2190
2368
|
CODING_REDACTION_RULES,
|
|
2191
2369
|
TRACES_REDACTION_RULES,
|
|
2192
2370
|
redactSpans,
|
|
2371
|
+
applyRedactor,
|
|
2193
2372
|
uploadKey,
|
|
2194
2373
|
sessionHash,
|
|
2195
2374
|
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-Q56AP2EY.js";
|
|
14
18
|
|
|
15
19
|
// src/cli.ts
|
|
16
20
|
import { stat, writeFile } from "fs/promises";
|
|
@@ -68,7 +72,7 @@ function renderReport(result, meta) {
|
|
|
68
72
|
}
|
|
69
73
|
}
|
|
70
74
|
lines.push("---");
|
|
71
|
-
lines.push(`OTLP artifact: \`${meta.otlpPath}\` \u2014
|
|
75
|
+
lines.push(`OTLP artifact: \`${meta.otlpPath}\` \u2014 run external engines with \`traces analyze --analyzer halo\`.`);
|
|
72
76
|
lines.push("");
|
|
73
77
|
return lines.join("\n");
|
|
74
78
|
}
|
|
@@ -106,7 +110,8 @@ function parseArgs(argv) {
|
|
|
106
110
|
minLoop: 3,
|
|
107
111
|
dryRun: false,
|
|
108
112
|
yes: false,
|
|
109
|
-
noContent: false
|
|
113
|
+
noContent: false,
|
|
114
|
+
analyzers: []
|
|
110
115
|
};
|
|
111
116
|
for (let i = 1; i < argv.length; i++) {
|
|
112
117
|
const arg = argv[i];
|
|
@@ -143,6 +148,9 @@ function parseArgs(argv) {
|
|
|
143
148
|
case "--budget":
|
|
144
149
|
a.budget = Number(next());
|
|
145
150
|
break;
|
|
151
|
+
case "--model":
|
|
152
|
+
a.model = next();
|
|
153
|
+
break;
|
|
146
154
|
case "--interval":
|
|
147
155
|
a.interval = Number(next());
|
|
148
156
|
break;
|
|
@@ -158,6 +166,17 @@ function parseArgs(argv) {
|
|
|
158
166
|
case "--no-content":
|
|
159
167
|
a.noContent = true;
|
|
160
168
|
break;
|
|
169
|
+
case "--analyzer": {
|
|
170
|
+
const v = next();
|
|
171
|
+
if (v) a.analyzers.push(v);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case "--analyzer-prompt":
|
|
175
|
+
a.analyzerPrompt = next();
|
|
176
|
+
break;
|
|
177
|
+
case "--redactor":
|
|
178
|
+
a.redactorCmd = next();
|
|
179
|
+
break;
|
|
161
180
|
case "--yes":
|
|
162
181
|
case "-y":
|
|
163
182
|
a.yes = true;
|
|
@@ -197,10 +216,13 @@ async function discover(args) {
|
|
|
197
216
|
async function buildAxService() {
|
|
198
217
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
199
218
|
if (!apiKey) {
|
|
200
|
-
throw new Error(
|
|
219
|
+
throw new Error(
|
|
220
|
+
"--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."
|
|
221
|
+
);
|
|
201
222
|
}
|
|
223
|
+
const apiURL = process.env.OPENAI_BASE_URL || void 0;
|
|
202
224
|
const { AxAI } = await import("@ax-llm/ax");
|
|
203
|
-
return new AxAI({ name: "openai", apiKey });
|
|
225
|
+
return new AxAI({ name: "openai", apiKey, ...apiURL ? { apiURL } : {} });
|
|
204
226
|
}
|
|
205
227
|
async function cmdList(args) {
|
|
206
228
|
const groups = await discover({ ...args, last: args.last || 20 });
|
|
@@ -251,14 +273,28 @@ async function cmdAnalyze(args) {
|
|
|
251
273
|
const ai = args.llm ? await buildAxService() : void 0;
|
|
252
274
|
const { otlpPath, result } = await analyzeSpans(spans, {
|
|
253
275
|
ai,
|
|
276
|
+
model: args.model,
|
|
254
277
|
budgetUsd: args.budget,
|
|
255
278
|
otlpOutPath: args.otlp,
|
|
256
279
|
log: (msg) => process.stderr.write(`${msg}
|
|
257
280
|
`)
|
|
258
281
|
});
|
|
259
282
|
const pipelines = await runPipelines(spans, { minLoopOccurrences: args.minLoop });
|
|
260
|
-
|
|
283
|
+
let report = `${renderReport(result, { harness, sessionCount, spanCount: spans.length, otlpPath })}
|
|
261
284
|
${renderPipelines(pipelines)}`;
|
|
285
|
+
if (args.analyzers.length > 0 && otlpPath) {
|
|
286
|
+
const engines = args.analyzers.map(
|
|
287
|
+
(spec) => spec === "halo" ? haloAnalyzer({ defaultPrompt: args.analyzerPrompt, model: args.model }) : commandAnalyzer({ name: spec, command: spec, args: (p, prompt) => prompt ? [p, prompt] : [p] })
|
|
288
|
+
);
|
|
289
|
+
const results = await runExternalAnalyzers(otlpPath, engines, { prompt: args.analyzerPrompt });
|
|
290
|
+
for (const r of results) {
|
|
291
|
+
report += `
|
|
292
|
+
|
|
293
|
+
## ${r.analyzer} (external analyzer)
|
|
294
|
+
|
|
295
|
+
${r.ok ? r.output || "(no output)" : `failed: ${r.error}`}`;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
262
298
|
if (args.out) {
|
|
263
299
|
await writeFile(args.out, report, "utf8");
|
|
264
300
|
console.log(`report \u2192 ${args.out} (${result.findings.length} findings, ${pipelines.stuckLoops.findings.length} loops, OTLP: ${otlpPath})`);
|
|
@@ -300,6 +336,8 @@ async function cmdUpload(args) {
|
|
|
300
336
|
cwd: args.cwd,
|
|
301
337
|
sinceMs
|
|
302
338
|
});
|
|
339
|
+
const redactorParts = args.redactorCmd?.split(/\s+/).filter(Boolean) ?? [];
|
|
340
|
+
const redactor = redactorParts[0] ? commandRedactor({ command: redactorParts[0], args: redactorParts.slice(1) }) : void 0;
|
|
303
341
|
const newItems = plan.items.filter((i) => i.isNew);
|
|
304
342
|
const byRule = {};
|
|
305
343
|
let totalRedactions = 0;
|
|
@@ -326,7 +364,7 @@ Window: since ${new Date(sinceMs).toISOString()}
|
|
|
326
364
|
return;
|
|
327
365
|
}
|
|
328
366
|
if (args.dryRun) {
|
|
329
|
-
const res2 = await executeUpload(plan, { dryRun: true, otlpOut: args.otlp, stripContent: args.noContent });
|
|
367
|
+
const res2 = await executeUpload(plan, { dryRun: true, otlpOut: args.otlp, stripContent: args.noContent, redactor });
|
|
330
368
|
console.log(`dry run \u2014 ${newItems.length} session(s), ${totalRedactions} redaction(s). Redacted OTLP \u2192 ${res2.otlpPath}`);
|
|
331
369
|
console.log("No upload performed. Set TANGLE_INGEST_URL / TANGLE_INGEST_API_KEY / TANGLE_TENANT_ID and drop --dry-run to send.");
|
|
332
370
|
return;
|
|
@@ -339,7 +377,7 @@ Window: since ${new Date(sinceMs).toISOString()}
|
|
|
339
377
|
}
|
|
340
378
|
}
|
|
341
379
|
const res = await executeUpload(plan, { log: (m) => w(`${m}
|
|
342
|
-
`), stripContent: args.noContent });
|
|
380
|
+
`), stripContent: args.noContent, redactor });
|
|
343
381
|
console.log(
|
|
344
382
|
`Uploaded ${res.uploadedSessions} session(s), ${res.acceptedSpans} spans accepted, ${res.redactionCount} redaction(s); ${res.skippedSessions} already-uploaded skipped.`
|
|
345
383
|
);
|
|
@@ -350,7 +388,7 @@ function usage() {
|
|
|
350
388
|
Commands:
|
|
351
389
|
list List discovered sessions
|
|
352
390
|
analyze Run analyst suite + loop/waste pipelines, write a markdown report
|
|
353
|
-
convert Emit OTLP-JSONL only (
|
|
391
|
+
convert Emit OTLP-JSONL only (HALO: use analyze --analyzer halo)
|
|
354
392
|
watch Online observer: tail active sessions, notify on stuck loops (read-only)
|
|
355
393
|
upload Redact + upload sessions in a time window to the Tangle Intelligence Platform
|
|
356
394
|
|
|
@@ -363,13 +401,17 @@ Options:
|
|
|
363
401
|
--since <t> upload: window \u2014 30m / 2h / 7d or an ISO date (default 24h); analyze: ISO cutoff
|
|
364
402
|
--out <path> Write report to a file
|
|
365
403
|
--otlp <path> OTLP artifact path (also the dry-run upload preview path)
|
|
366
|
-
--llm Enable agentic RLM analysts (needs OPENAI_API_KEY)
|
|
404
|
+
--llm Enable agentic RLM analysts (needs OPENAI_API_KEY / OPENAI_BASE_URL)
|
|
405
|
+
--model <id> --llm model id (e.g. a router model like glm-5.2); default is agent-eval's
|
|
367
406
|
--budget <usd> USD cap for agentic analysts
|
|
407
|
+
--analyzer <cmd> analyze: also run an external engine over the OTLP (repeatable; "halo" or any command)
|
|
408
|
+
--analyzer-prompt <p> analyze: prompt passed to external analyzers (default: diagnose)
|
|
368
409
|
--interval <s> watch: poll interval seconds (default 5)
|
|
369
410
|
--window <m> watch: only sessions active in the last N minutes (default 30)
|
|
370
411
|
--min-loop <n> Min identical repeated calls to flag a loop (default 3)
|
|
371
412
|
--dry-run upload: redact + dedup + preview, write OTLP, but do NOT send
|
|
372
413
|
--no-content upload: strip prompt/response text \u2014 send metadata only
|
|
414
|
+
--redactor <cmd> upload: external PII scrubber (JSON array stdin\u2192stdout) after the regex pass
|
|
373
415
|
--yes, -y upload: skip the confirmation prompt
|
|
374
416
|
|
|
375
417
|
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';
|
|
@@ -14,8 +14,10 @@ export { HostedClient, createHostedClient, hostedClientFromEnv } from '@tangle-n
|
|
|
14
14
|
* Every harness adapter projects its native session log onto `OtlpSpan[]`
|
|
15
15
|
* and we serialize one span per JSONL line. This is the exact wire shape
|
|
16
16
|
* `@tangle-network/agent-eval`'s `OtlpFileTraceStore` indexes
|
|
17
|
-
* (`projectOtlpFlatLine` + `extractOtlpAttributes`)
|
|
18
|
-
*
|
|
17
|
+
* (`projectOtlpFlatLine` + `extractOtlpAttributes`). It is a flat subset, NOT
|
|
18
|
+
* canonical OpenInference (kind lives in attributes; no resource/scope; root
|
|
19
|
+
* parent is null) — external OpenInference engines like HALO need the canonical
|
|
20
|
+
* form, which `external.ts` `toCanonicalOpenInference` produces.
|
|
19
21
|
*
|
|
20
22
|
* Attribute vocabulary the downstream analysts key off:
|
|
21
23
|
* - `openinference.span.kind` → span kind (AGENT | LLM | TOOL | CHAIN)
|
|
@@ -524,8 +526,10 @@ declare function toRuntimeStore(spans: readonly OtlpSpan[]): Promise<RuntimeTrac
|
|
|
524
526
|
* LLM, model-agnostic). Supply `ai` to add the agentic RLM kinds
|
|
525
527
|
* (failure-mode / knowledge-gap / knowledge-poisoning / improvement).
|
|
526
528
|
*
|
|
527
|
-
* The written OTLP file is the
|
|
528
|
-
*
|
|
529
|
+
* The written OTLP file is the artifact our pipeline reads. External engines can
|
|
530
|
+
* analyze it too: HALO needs canonical OpenInference, so `--analyzer halo`
|
|
531
|
+
* converts it first (see external.ts `toCanonicalOpenInference`) — analysis is
|
|
532
|
+
* never locked to one engine.
|
|
529
533
|
*/
|
|
530
534
|
|
|
531
535
|
interface AnalyzeOptions {
|
|
@@ -547,12 +551,109 @@ interface AnalyzeOptions {
|
|
|
547
551
|
log?: (msg: string, fields?: Record<string, unknown>) => void;
|
|
548
552
|
}
|
|
549
553
|
interface AnalyzeResult {
|
|
550
|
-
/** Path to the OTLP-JSONL artifact (
|
|
554
|
+
/** Path to the OTLP-JSONL artifact (convert to canonical for HALO). */
|
|
551
555
|
otlpPath: string;
|
|
552
556
|
result: Awaited<ReturnType<ReturnType<typeof buildDefaultAnalystRegistry>['run']>>;
|
|
553
557
|
}
|
|
554
558
|
declare function analyzeSpans(spans: readonly OtlpSpan[], opts?: AnalyzeOptions): Promise<AnalyzeResult>;
|
|
555
559
|
|
|
560
|
+
/**
|
|
561
|
+
* External-tool adapters: run analysis engines and PII scrubbers that `traces`
|
|
562
|
+
* does NOT bundle. Each is a thin command adapter over a shared subprocess
|
|
563
|
+
* runner — install the tool yourself (HALO, openai/privacy-filter, anything),
|
|
564
|
+
* point an adapter at it, and it composes with the built-in pipeline.
|
|
565
|
+
*
|
|
566
|
+
* - ExternalAnalyzer — run an engine over the emitted OTLP-JSONL artifact and
|
|
567
|
+
* fold its output into the report (HALO and friends).
|
|
568
|
+
* - Redactor — scrub free-form prompt/response text via an external model that
|
|
569
|
+
* catches what the regex pass can't (names, addresses).
|
|
570
|
+
*/
|
|
571
|
+
|
|
572
|
+
interface RunResult {
|
|
573
|
+
stdout: string;
|
|
574
|
+
stderr: string;
|
|
575
|
+
code: number | null;
|
|
576
|
+
}
|
|
577
|
+
/** Spawn `command args`, optionally write `input` to stdin, and collect output.
|
|
578
|
+
* Rejects on spawn error, timeout, or output exceeding `maxBuffer`. */
|
|
579
|
+
declare function runCommand(command: string, args?: readonly string[], opts?: {
|
|
580
|
+
input?: string;
|
|
581
|
+
signal?: AbortSignal;
|
|
582
|
+
timeoutMs?: number;
|
|
583
|
+
maxBuffer?: number;
|
|
584
|
+
}): Promise<RunResult>;
|
|
585
|
+
interface ExternalAnalysisResult {
|
|
586
|
+
analyzer: string;
|
|
587
|
+
ok: boolean;
|
|
588
|
+
/** Raw text the engine produced (markdown/JSON/plain — engine-specific). */
|
|
589
|
+
output: string;
|
|
590
|
+
/** Structured findings, when the engine emits parseable JSON. */
|
|
591
|
+
findings?: AnalystFinding[];
|
|
592
|
+
error?: string;
|
|
593
|
+
}
|
|
594
|
+
/** An analysis engine that runs over the emitted OTLP-JSONL artifact — a peer to
|
|
595
|
+
* the built-in analysts, so you get many analyzers beyond our own. */
|
|
596
|
+
interface ExternalAnalyzer {
|
|
597
|
+
name: string;
|
|
598
|
+
analyze(otlpPath: string, opts?: {
|
|
599
|
+
prompt?: string;
|
|
600
|
+
signal?: AbortSignal;
|
|
601
|
+
}): Promise<ExternalAnalysisResult>;
|
|
602
|
+
}
|
|
603
|
+
/** Wrap any CLI that reads the OTLP file and prints analysis to stdout. `args`
|
|
604
|
+
* builds the argv from the artifact path + optional prompt. Non-zero exit →
|
|
605
|
+
* `ok:false` with stderr in `error` (fail-soft: one engine never breaks a run). */
|
|
606
|
+
declare function commandAnalyzer(spec: {
|
|
607
|
+
name: string;
|
|
608
|
+
command: string;
|
|
609
|
+
args: (otlpPath: string, prompt?: string) => string[];
|
|
610
|
+
/** Parse stdout into structured findings; omit to keep raw output only. */
|
|
611
|
+
parse?: (stdout: string) => AnalystFinding[] | undefined;
|
|
612
|
+
timeoutMs?: number;
|
|
613
|
+
}): ExternalAnalyzer;
|
|
614
|
+
/** Convert our OTLP-JSONL (the flat shape our pipeline reads) into the canonical
|
|
615
|
+
* OpenInference span schema HALO's `SpanRecord` requires: top-level `kind`,
|
|
616
|
+
* `resource`, `scope`, and a string (never null) `parent_span_id`. Our own
|
|
617
|
+
* writer keeps the simpler shape for the agent-eval reader; this is the bridge
|
|
618
|
+
* to OpenInference consumers. Input + output are JSONL strings (one span/line). */
|
|
619
|
+
declare function toCanonicalOpenInference(otlpJsonl: string, opts?: {
|
|
620
|
+
service?: string;
|
|
621
|
+
scopeName?: string;
|
|
622
|
+
}): string;
|
|
623
|
+
/** HALO (github.com/context-labs/halo) over the OTLP artifact. HALO requires
|
|
624
|
+
* canonical OpenInference, so this converts our OTLP to a `<path>.halo.jsonl`
|
|
625
|
+
* first, then runs `halo <file> -p <prompt> [-m <model>]`. Install HALO yourself
|
|
626
|
+
* and configure its LLM provider (it uses the OpenAI client — `OPENAI_BASE_URL`
|
|
627
|
+
* / `OPENAI_API_KEY`); this just drives it. */
|
|
628
|
+
declare function haloAnalyzer(opts?: {
|
|
629
|
+
command?: string;
|
|
630
|
+
defaultPrompt?: string;
|
|
631
|
+
model?: string;
|
|
632
|
+
timeoutMs?: number;
|
|
633
|
+
}): ExternalAnalyzer;
|
|
634
|
+
/** Run external analyzers over one OTLP artifact, concurrently. Never throws —
|
|
635
|
+
* a failing engine returns `ok:false`. */
|
|
636
|
+
declare function runExternalAnalyzers(otlpPath: string, analyzers: readonly ExternalAnalyzer[], opts?: {
|
|
637
|
+
prompt?: string;
|
|
638
|
+
signal?: AbortSignal;
|
|
639
|
+
}): Promise<ExternalAnalysisResult[]>;
|
|
640
|
+
/** An external PII/secret scrubber for free-form text — catches what regex
|
|
641
|
+
* can't (names, addresses). Use to harden upload beyond the built-in rules. */
|
|
642
|
+
interface Redactor {
|
|
643
|
+
name: string;
|
|
644
|
+
/** Scrub a batch of texts; returns one scrubbed string per input, in order. */
|
|
645
|
+
redactText(texts: readonly string[]): Promise<string[]>;
|
|
646
|
+
}
|
|
647
|
+
/** Wrap any CLI that speaks the redaction protocol: read a JSON array of strings
|
|
648
|
+
* on stdin, write a same-length JSON array of scrubbed strings on stdout.
|
|
649
|
+
* (A 3-line wrapper adapts tools like `opf`/openai-privacy-filter.) */
|
|
650
|
+
declare function commandRedactor(spec: {
|
|
651
|
+
name?: string;
|
|
652
|
+
command: string;
|
|
653
|
+
args?: string[];
|
|
654
|
+
timeoutMs?: number;
|
|
655
|
+
}): Redactor;
|
|
656
|
+
|
|
556
657
|
/**
|
|
557
658
|
* Live trace observer — the reusable, event-driven core behind `traces watch`.
|
|
558
659
|
*
|
|
@@ -642,6 +743,14 @@ interface SpanRedaction {
|
|
|
642
743
|
/** Redact every span's attributes + status message. Returns scrubbed spans and
|
|
643
744
|
* an aggregate report (count + per-rule breakdown) over the whole set. */
|
|
644
745
|
declare function redactSpans(spans: readonly OtlpSpan[], rules?: RedactionRule[]): SpanRedaction;
|
|
746
|
+
/** Defense-in-depth: run an external {@link Redactor} (e.g. an ML PII model) over
|
|
747
|
+
* every span's captured `content` (prompt/response prose), catching free-form
|
|
748
|
+
* PII the regex pass misses. Compose AFTER `redactSpans`. Returns scrubbed spans
|
|
749
|
+
* and the number of `content` fields the model changed. */
|
|
750
|
+
declare function applyRedactor(spans: readonly OtlpSpan[], redactor: Redactor): Promise<{
|
|
751
|
+
spans: OtlpSpan[];
|
|
752
|
+
changed: number;
|
|
753
|
+
}>;
|
|
645
754
|
|
|
646
755
|
/**
|
|
647
756
|
* Batch collection seam — `collectSessions` returns redacted spans per session
|
|
@@ -750,9 +859,12 @@ interface ExecuteOptions {
|
|
|
750
859
|
* span before it leaves the machine (still keeps tool calls, tokens, timing,
|
|
751
860
|
* loop signal). The strongest privacy posture when prose can't leave. */
|
|
752
861
|
stripContent?: boolean;
|
|
862
|
+
/** External PII scrubber applied to `content` AFTER the regex pass — catches
|
|
863
|
+
* free-form PII (names, addresses) the rules miss. Ignored if stripContent. */
|
|
864
|
+
redactor?: Redactor;
|
|
753
865
|
log?: (msg: string) => void;
|
|
754
866
|
}
|
|
755
867
|
/** Send the NEW items (or, on dryRun, write the redacted OTLP that would send). */
|
|
756
868
|
declare function executeUpload(plan: UploadPlan, opts?: ExecuteOptions): Promise<UploadResult>;
|
|
757
869
|
|
|
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 };
|
|
870
|
+
export { ATTR, AmpAdapter, type AnalyzeOptions, type AnalyzeResult, CODING_REDACTION_RULES, CONTENT_CAP, ClaudeAdapter, CodexAdapter, type CollectOptions, CopilotAdapter, DEFAULT_HARNESS, type ExecuteOptions, type ExternalAnalysisResult, type ExternalAnalyzer, FactoryAdapter, ForgeAdapter, GeminiAdapter, type HarnessTraceAdapter, INGEST_SOURCE_CLI, type LocateOptions, type ObservedLoop, type ObserverOptions, OpencodeAdapter, type OtlpSpan, type OtlpSpanKind, type OtlpStatusCode, PiAdapter, type PipelineOptions, type PipelineReport, type PlanOptions, QwenAdapter, type Redactor, type RunResult, type RuntimeTrace, type ScanOptions, type ScannedSession, type SessionBatch, type SessionRef, type SpanInput, type SpanRedaction, TRACES_REDACTION_RULES, type UploadBackend, type UploadItem, type UploadPlan, type UploadRecord, type UploadResult, type UploadState, alreadyUploaded, analyzeSpans, applyRedactor, capText, collectSessions, commandAnalyzer, commandRedactor, executeUpload, haloAnalyzer, knownHarnesses, listAdapters, loadState, parseIsoToEpochMs, parseSince, planUpload, redactSpans, resolveAdapter, runCommand, runExternalAnalyzers, runPipelines, saveState, scanSessions, selectAdapters, serializeSpans, sessionHash, span, toCanonicalOpenInference, toRuntimeStore, toTraceSpanEvents, uploadKey, userPromptSpan, watchSessions, writeOtlpFile };
|
package/dist/index.js
CHANGED
|
@@ -17,8 +17,12 @@ import {
|
|
|
17
17
|
TRACES_REDACTION_RULES,
|
|
18
18
|
alreadyUploaded,
|
|
19
19
|
analyzeSpans,
|
|
20
|
+
applyRedactor,
|
|
20
21
|
capText,
|
|
22
|
+
commandAnalyzer,
|
|
23
|
+
commandRedactor,
|
|
21
24
|
executeUpload,
|
|
25
|
+
haloAnalyzer,
|
|
22
26
|
knownHarnesses,
|
|
23
27
|
listAdapters,
|
|
24
28
|
loadState,
|
|
@@ -27,6 +31,8 @@ import {
|
|
|
27
31
|
planUpload,
|
|
28
32
|
redactSpans,
|
|
29
33
|
resolveAdapter,
|
|
34
|
+
runCommand,
|
|
35
|
+
runExternalAnalyzers,
|
|
30
36
|
runPipelines,
|
|
31
37
|
saveState,
|
|
32
38
|
scanSessions,
|
|
@@ -34,13 +40,14 @@ import {
|
|
|
34
40
|
serializeSpans,
|
|
35
41
|
sessionHash,
|
|
36
42
|
span,
|
|
43
|
+
toCanonicalOpenInference,
|
|
37
44
|
toRuntimeStore,
|
|
38
45
|
toTraceSpanEvents,
|
|
39
46
|
uploadKey,
|
|
40
47
|
userPromptSpan,
|
|
41
48
|
watchSessions,
|
|
42
49
|
writeOtlpFile
|
|
43
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-Q56AP2EY.js";
|
|
44
51
|
|
|
45
52
|
// src/collect.ts
|
|
46
53
|
async function collectSessions(opts = {}) {
|
|
@@ -85,11 +92,15 @@ export {
|
|
|
85
92
|
TRACES_REDACTION_RULES,
|
|
86
93
|
alreadyUploaded,
|
|
87
94
|
analyzeSpans,
|
|
95
|
+
applyRedactor,
|
|
88
96
|
buildDefaultAnalystRegistry,
|
|
89
97
|
capText,
|
|
90
98
|
collectSessions,
|
|
99
|
+
commandAnalyzer,
|
|
100
|
+
commandRedactor,
|
|
91
101
|
createHostedClient,
|
|
92
102
|
executeUpload,
|
|
103
|
+
haloAnalyzer,
|
|
93
104
|
hostedClientFromEnv,
|
|
94
105
|
knownHarnesses,
|
|
95
106
|
listAdapters,
|
|
@@ -102,6 +113,8 @@ export {
|
|
|
102
113
|
redactString,
|
|
103
114
|
redactValue,
|
|
104
115
|
resolveAdapter,
|
|
116
|
+
runCommand,
|
|
117
|
+
runExternalAnalyzers,
|
|
105
118
|
runPipelines,
|
|
106
119
|
saveState,
|
|
107
120
|
scanSessions,
|
|
@@ -109,6 +122,7 @@ export {
|
|
|
109
122
|
serializeSpans,
|
|
110
123
|
sessionHash,
|
|
111
124
|
span,
|
|
125
|
+
toCanonicalOpenInference,
|
|
112
126
|
toRuntimeStore,
|
|
113
127
|
toTraceSpanEvents,
|
|
114
128
|
uploadKey,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/traces",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Point it at your coding-agent session traces (Claude Code, Codex, OpenCode, Gemini, Pi, …) and get failure-mode + efficiency findings. CLI + SDK over the @tangle-network/agent-eval analyst suite — observe live sessions, run your own analysts, redact, and upload to the Tangle Intelligence Platform.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|