@tangle-network/traces 0.7.0 → 0.8.5
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 +57 -3
- package/dist/{chunk-Q56AP2EY.js → chunk-D5OMTM4S.js} +1224 -315
- package/dist/cli.js +260 -12
- package/dist/index.d.ts +330 -28
- package/dist/index.js +51 -5
- package/package.json +12 -13
|
@@ -1,3 +1,110 @@
|
|
|
1
|
+
// src/adoption.ts
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
function toolName(s) {
|
|
5
|
+
const n = s.attributes["tool.name"];
|
|
6
|
+
if (typeof n === "string") return n;
|
|
7
|
+
if (s.name.startsWith("tool.")) return s.name.slice("tool.".length);
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
function parseInput(s) {
|
|
11
|
+
const c = s.attributes.content;
|
|
12
|
+
if (typeof c !== "string") return {};
|
|
13
|
+
try {
|
|
14
|
+
const v = JSON.parse(c);
|
|
15
|
+
return v && typeof v === "object" ? v : {};
|
|
16
|
+
} catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function skillNameOf(input) {
|
|
21
|
+
const v = input.skill ?? input.command;
|
|
22
|
+
return typeof v === "string" && v.length > 0 ? v : "?";
|
|
23
|
+
}
|
|
24
|
+
function subagentTypeOf(input) {
|
|
25
|
+
const v = input.subagent_type;
|
|
26
|
+
return typeof v === "string" && v.length > 0 ? v : "?";
|
|
27
|
+
}
|
|
28
|
+
function countSkillRunsJsonl(raw) {
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const line of raw.split("\n")) {
|
|
31
|
+
const t = line.trim();
|
|
32
|
+
if (!t) continue;
|
|
33
|
+
let o;
|
|
34
|
+
try {
|
|
35
|
+
o = JSON.parse(t);
|
|
36
|
+
} catch {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!o || typeof o !== "object") continue;
|
|
40
|
+
const rec = o;
|
|
41
|
+
const names = [];
|
|
42
|
+
if (Array.isArray(rec.skills)) {
|
|
43
|
+
for (const s of rec.skills) if (typeof s === "string" && s) names.push(s);
|
|
44
|
+
}
|
|
45
|
+
if (typeof rec.skill === "string" && rec.skill) names.push(rec.skill);
|
|
46
|
+
if (names.length === 0) names.push("?");
|
|
47
|
+
for (const n of names) out[n] = (out[n] ?? 0) + 1;
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
async function readLoopRuns(cwds) {
|
|
52
|
+
const counts = {};
|
|
53
|
+
let filesRead = 0;
|
|
54
|
+
const seen = /* @__PURE__ */ new Set();
|
|
55
|
+
for (const cwd of cwds) {
|
|
56
|
+
if (!cwd || seen.has(cwd)) continue;
|
|
57
|
+
seen.add(cwd);
|
|
58
|
+
const path = join(cwd, ".evolve", "skill-runs.jsonl");
|
|
59
|
+
let raw;
|
|
60
|
+
try {
|
|
61
|
+
raw = await readFile(path, "utf8");
|
|
62
|
+
} catch {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
filesRead += 1;
|
|
66
|
+
for (const [k, v] of Object.entries(countSkillRunsJsonl(raw))) counts[k] = (counts[k] ?? 0) + v;
|
|
67
|
+
}
|
|
68
|
+
return { counts, filesRead };
|
|
69
|
+
}
|
|
70
|
+
async function analyzeAdoption(spans, opts = {}) {
|
|
71
|
+
const sessions = /* @__PURE__ */ new Set();
|
|
72
|
+
const sessionsWithSkill = /* @__PURE__ */ new Set();
|
|
73
|
+
const sessionsWithSubagent = /* @__PURE__ */ new Set();
|
|
74
|
+
const skillInvocations = {};
|
|
75
|
+
const subagentSpawns = {};
|
|
76
|
+
for (const s of spans) {
|
|
77
|
+
sessions.add(s.trace_id);
|
|
78
|
+
const tn = toolName(s);
|
|
79
|
+
if (tn === "Skill") {
|
|
80
|
+
const name = skillNameOf(parseInput(s));
|
|
81
|
+
skillInvocations[name] = (skillInvocations[name] ?? 0) + 1;
|
|
82
|
+
sessionsWithSkill.add(s.trace_id);
|
|
83
|
+
} else if (tn === "Task" || tn === "Agent") {
|
|
84
|
+
const type = subagentTypeOf(parseInput(s));
|
|
85
|
+
subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
|
|
86
|
+
sessionsWithSubagent.add(s.trace_id);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const { counts: loopDispatchedRuns, filesRead } = await readLoopRuns(opts.cwds ?? []);
|
|
90
|
+
const totalSkillInvocations = Object.values(skillInvocations).reduce((a, b) => a + b, 0);
|
|
91
|
+
const totalSubagentSpawns = Object.values(subagentSpawns).reduce((a, b) => a + b, 0);
|
|
92
|
+
const totalLoopDispatchedRuns = Object.values(loopDispatchedRuns).reduce((a, b) => a + b, 0);
|
|
93
|
+
return {
|
|
94
|
+
sessionCount: sessions.size,
|
|
95
|
+
sessionsWithSkill: sessionsWithSkill.size,
|
|
96
|
+
skillPenetration: sessions.size === 0 ? 0 : sessionsWithSkill.size / sessions.size,
|
|
97
|
+
skillInvocations,
|
|
98
|
+
totalSkillInvocations,
|
|
99
|
+
subagentSpawns,
|
|
100
|
+
totalSubagentSpawns,
|
|
101
|
+
sessionsWithSubagent: sessionsWithSubagent.size,
|
|
102
|
+
loopDispatchedRuns,
|
|
103
|
+
totalLoopDispatchedRuns,
|
|
104
|
+
skillRunFilesRead: filesRead
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
1
108
|
// src/otlp.ts
|
|
2
109
|
function span(input) {
|
|
3
110
|
const attributes = {
|
|
@@ -25,17 +132,39 @@ function span(input) {
|
|
|
25
132
|
attributes
|
|
26
133
|
};
|
|
27
134
|
}
|
|
135
|
+
function toOpenInferenceSpan(s) {
|
|
136
|
+
const a = s.attributes;
|
|
137
|
+
const resourceAttrs = {};
|
|
138
|
+
if (a["service.name"] != null) resourceAttrs["service.name"] = a["service.name"];
|
|
139
|
+
if (a["agent.name"] != null) resourceAttrs["agent.name"] = a["agent.name"];
|
|
140
|
+
for (const k of ["tangle.subject.key", "git.repository", "git.branch", "git.commit", "tangle.cwd"]) {
|
|
141
|
+
if (a[k] != null) resourceAttrs[k] = a[k];
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
trace_id: s.trace_id,
|
|
145
|
+
span_id: s.span_id,
|
|
146
|
+
parent_span_id: s.parent_span_id ?? "",
|
|
147
|
+
name: s.name,
|
|
148
|
+
kind: typeof a["openinference.span.kind"] === "string" ? a["openinference.span.kind"] : "CHAIN",
|
|
149
|
+
start_time: s.start_time,
|
|
150
|
+
end_time: s.end_time,
|
|
151
|
+
status: { code: s.status.code, message: s.status.message ?? "" },
|
|
152
|
+
resource: { attributes: resourceAttrs },
|
|
153
|
+
scope: { name: "tangle-traces", version: "" },
|
|
154
|
+
attributes: a
|
|
155
|
+
};
|
|
156
|
+
}
|
|
28
157
|
function serializeSpans(spans) {
|
|
29
158
|
if (spans.length === 0) return "";
|
|
30
|
-
return `${spans.map((s) => JSON.stringify(s)).join("\n")}
|
|
159
|
+
return `${spans.map((s) => JSON.stringify(toOpenInferenceSpan(s))).join("\n")}
|
|
31
160
|
`;
|
|
32
161
|
}
|
|
33
162
|
async function writeOtlpFile(spans, outPath) {
|
|
34
|
-
const { mkdtemp, writeFile:
|
|
35
|
-
const { tmpdir } = await import("os");
|
|
36
|
-
const { join:
|
|
37
|
-
const path = outPath ??
|
|
38
|
-
await
|
|
163
|
+
const { mkdtemp: mkdtemp3, writeFile: writeFile4 } = await import("fs/promises");
|
|
164
|
+
const { tmpdir: tmpdir3 } = await import("os");
|
|
165
|
+
const { join: join16 } = await import("path");
|
|
166
|
+
const path = outPath ?? join16(await mkdtemp3(join16(tmpdir3(), "traces-")), "spans.otlp.jsonl");
|
|
167
|
+
await writeFile4(path, serializeSpans(spans), "utf8");
|
|
39
168
|
return path;
|
|
40
169
|
}
|
|
41
170
|
|
|
@@ -70,150 +199,37 @@ async function analyzeSpans(spans, opts = {}) {
|
|
|
70
199
|
return { otlpPath, result };
|
|
71
200
|
}
|
|
72
201
|
|
|
73
|
-
// src/
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
}
|
|
202
|
+
// src/attributes.ts
|
|
203
|
+
var ATTR = {
|
|
204
|
+
/** = the trace/session id. Wire contract (dedup linkage). */
|
|
205
|
+
SESSION_ID: "tangle.sessionId",
|
|
206
|
+
/** "cli" marks a CLI upload so the platform can drop a re-upload of a session
|
|
207
|
+
* that also streamed live. Wire contract. */
|
|
208
|
+
INGEST_SOURCE: "tangle.ingest_source",
|
|
209
|
+
HARNESS: "tangle.harness",
|
|
210
|
+
CWD: "tangle.cwd",
|
|
211
|
+
/** THE per-session grouping key the spine derives subjects from
|
|
212
|
+
* (`deriveSubjectKey` reads `tangle.subject.key` first). Resolves to the git
|
|
213
|
+
* remote (host/owner/repo) when readable, else the cwd path basename. */
|
|
214
|
+
SUBJECT_KEY: "tangle.subject.key",
|
|
215
|
+
/** Normalized git remote (e.g. `github.com/tangle-network/agent-dev-container`). */
|
|
216
|
+
GIT_REPOSITORY: "git.repository",
|
|
217
|
+
/** Current branch at conversion time. */
|
|
218
|
+
GIT_BRANCH_NAME: "git.branch",
|
|
219
|
+
/** HEAD short sha at conversion time. */
|
|
220
|
+
GIT_COMMIT: "git.commit",
|
|
221
|
+
GIT_BRANCH: "tangle.git_branch",
|
|
222
|
+
HOST: "tangle.host",
|
|
223
|
+
/** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
|
|
224
|
+
* which collided with `tangle.ingest_source`.) */
|
|
225
|
+
SESSION_FILE: "tangle.session_file",
|
|
226
|
+
UPLOADED_AT: "tangle.uploaded_at",
|
|
227
|
+
UPLOADER: "tangle.uploader",
|
|
228
|
+
REDACTION_VERSION: "redaction.version",
|
|
229
|
+
REDACTION_COUNT: "redaction.count"
|
|
230
|
+
};
|
|
231
|
+
var INGEST_SOURCE_CLI = "cli";
|
|
232
|
+
var DEFAULT_HARNESS = "claude-code";
|
|
217
233
|
|
|
218
234
|
// src/time.ts
|
|
219
235
|
function parseIsoToEpochMs(ts) {
|
|
@@ -291,6 +307,7 @@ function capText(raw) {
|
|
|
291
307
|
if (t.length <= CONTENT_CAP) return t;
|
|
292
308
|
return `${t.slice(0, CONTENT_CAP)}\u2026 [+${t.length - CONTENT_CAP} chars]`;
|
|
293
309
|
}
|
|
310
|
+
var ACTOR_ATTR = "tangle.actor";
|
|
294
311
|
function userPromptSpan(o) {
|
|
295
312
|
return span({
|
|
296
313
|
traceId: o.traceId,
|
|
@@ -302,14 +319,15 @@ function userPromptSpan(o) {
|
|
|
302
319
|
service: o.service ?? null,
|
|
303
320
|
agent: o.agent ?? null,
|
|
304
321
|
step: o.step,
|
|
305
|
-
content: o.content
|
|
322
|
+
content: o.content,
|
|
323
|
+
extra: { [ACTOR_ATTR]: o.actor ?? "human" }
|
|
306
324
|
});
|
|
307
325
|
}
|
|
308
326
|
|
|
309
327
|
// src/adapters/amp.ts
|
|
310
328
|
import { readdir, readFile as readFile2, stat } from "fs/promises";
|
|
311
329
|
import { homedir } from "os";
|
|
312
|
-
import { basename, join } from "path";
|
|
330
|
+
import { basename, join as join2 } from "path";
|
|
313
331
|
var SERVICE = "amp";
|
|
314
332
|
function textOf(content) {
|
|
315
333
|
if (typeof content === "string") return content.slice(0, CONTENT_CAP);
|
|
@@ -320,8 +338,8 @@ function textOf(content) {
|
|
|
320
338
|
var AmpAdapter = class {
|
|
321
339
|
harness = "amp";
|
|
322
340
|
threadsRoot() {
|
|
323
|
-
const base = process.env.XDG_DATA_HOME ??
|
|
324
|
-
return
|
|
341
|
+
const base = process.env.XDG_DATA_HOME ?? join2(homedir(), ".local", "share");
|
|
342
|
+
return join2(base, "amp", "threads");
|
|
325
343
|
}
|
|
326
344
|
async locate(opts = {}) {
|
|
327
345
|
let files;
|
|
@@ -333,7 +351,7 @@ var AmpAdapter = class {
|
|
|
333
351
|
const refs = [];
|
|
334
352
|
for (const f of files) {
|
|
335
353
|
if (!f.startsWith("T-") || !f.endsWith(".json")) continue;
|
|
336
|
-
const path =
|
|
354
|
+
const path = join2(this.threadsRoot(), f);
|
|
337
355
|
let st;
|
|
338
356
|
try {
|
|
339
357
|
st = await stat(path);
|
|
@@ -432,10 +450,98 @@ var AmpAdapter = class {
|
|
|
432
450
|
}
|
|
433
451
|
};
|
|
434
452
|
|
|
453
|
+
// src/adapters/actor.ts
|
|
454
|
+
var CMD_MARKERS = [
|
|
455
|
+
"<command-name>",
|
|
456
|
+
"<local-command-caveat>",
|
|
457
|
+
"<command-message>",
|
|
458
|
+
"<command-args>",
|
|
459
|
+
"<command-stdout>"
|
|
460
|
+
];
|
|
461
|
+
var INJECT_MARKERS = [
|
|
462
|
+
"<system-reminder>",
|
|
463
|
+
"Caveat: The messages below",
|
|
464
|
+
"<user-memory-input>",
|
|
465
|
+
// Harness-injected wrappers that arrive as `userType: external` user turns —
|
|
466
|
+
// they pass the structural human test but are the harness feeding the model,
|
|
467
|
+
// not a person. (Observed in Claude Code task/loop sessions.)
|
|
468
|
+
"<task-notification>",
|
|
469
|
+
"<task-prompt>",
|
|
470
|
+
"<local-command-stdout>",
|
|
471
|
+
"This session is being continued from a previous conversation",
|
|
472
|
+
// Slash-command skill bodies are expanded into a user turn (no <command-name>
|
|
473
|
+
// wrapper) — the skill prompt, not the human.
|
|
474
|
+
"Base directory for this skill:"
|
|
475
|
+
];
|
|
476
|
+
var SYNTHETIC_MARKERS = [
|
|
477
|
+
"You are in a development environment with full file system access",
|
|
478
|
+
"## What the user asked for",
|
|
479
|
+
"Build the project described below",
|
|
480
|
+
"Filesystem sandboxing defines",
|
|
481
|
+
"<permissions instructions>",
|
|
482
|
+
"You are an autonomous",
|
|
483
|
+
"You are a coding agent",
|
|
484
|
+
"Your task is to build",
|
|
485
|
+
"## Deliverable",
|
|
486
|
+
"## Acceptance criteria",
|
|
487
|
+
"Return ONLY",
|
|
488
|
+
"respond with JSON",
|
|
489
|
+
"<environment_details>",
|
|
490
|
+
"<task>",
|
|
491
|
+
"Review this change before it is pushed",
|
|
492
|
+
"Complete a security review",
|
|
493
|
+
"Focus on correctness, regressions, security issues",
|
|
494
|
+
"Read-only critical audit",
|
|
495
|
+
"Staff-engineer code review"
|
|
496
|
+
];
|
|
497
|
+
var AGENT_PROMPT = /^(you are\b|you're\b|read[- ]only\b|stop now\b|your task\b|your job\b|review the\b|audit\b|analyze the\b|return (only|a |the )|here is the\b|context:|task:|do not (revert|edit|touch)|work only in\b|you have been\b|act as\b|operate the\b|goal:|objective:)/i;
|
|
498
|
+
function textIsCmdOrInject(s) {
|
|
499
|
+
return CMD_MARKERS.some((m) => s.includes(m)) || INJECT_MARKERS.some((m) => s.includes(m));
|
|
500
|
+
}
|
|
501
|
+
function textIsSynthetic(s) {
|
|
502
|
+
const head = s.slice(0, 600);
|
|
503
|
+
return SYNTHETIC_MARKERS.some((m) => head.includes(m));
|
|
504
|
+
}
|
|
505
|
+
function looksLikeAgentPrompt(s) {
|
|
506
|
+
const t = s.trim();
|
|
507
|
+
if (t.length > 1500) return true;
|
|
508
|
+
return AGENT_PROMPT.test(t);
|
|
509
|
+
}
|
|
510
|
+
function claudeActor(args) {
|
|
511
|
+
if (args.isSidechain === true) return "subagent-spawn";
|
|
512
|
+
if (args.userType != null && args.userType !== "external") return "injected";
|
|
513
|
+
if (textIsCmdOrInject(args.text)) return "injected";
|
|
514
|
+
if (textIsSynthetic(args.text)) return "injected";
|
|
515
|
+
if (args.isFirstUserTurn && looksLikeAgentPrompt(args.text)) return "subagent-spawn";
|
|
516
|
+
return "human";
|
|
517
|
+
}
|
|
518
|
+
function codexActor(args) {
|
|
519
|
+
if (textIsCmdOrInject(args.text)) return "injected";
|
|
520
|
+
if (textIsSynthetic(args.text)) return "injected";
|
|
521
|
+
if (args.isFirstUserTurn && looksLikeAgentPrompt(args.text)) return "injected";
|
|
522
|
+
return "human";
|
|
523
|
+
}
|
|
524
|
+
var JARGON_COMPLAINT = /\b(jargon|plain english|plain language|eli5|explain like|in plain|what does that mean|what does .* mean|i don'?t (understand|get)|gibberish|english please|too technical|speak (plainly|english)|dumb(ed)? it down|simpler|simplify your|no buzzwords?)\b/;
|
|
525
|
+
var STRUCTURE_COMPLAINT = /\b(too long|tl;?dr|get to the point|bluf|lead with|bullet|bulletpoint|wall of text|too verbose|shorter|be concise|stop (yapping|rambling)|less words?|fewer words?|just (the|tell me)|cut the)\b/;
|
|
526
|
+
var CORRECTION = /(\bno[,. ]|^no\b|\bstop\b|\bdon'?t\b|you keep|i (said|told you|asked)|that'?s not (what|right|it)|not what i|didn'?t ask|incorrect|that'?s wrong|you'?re wrong|why are you|why did you|that'?s not the|missed the|not the point|ur wrong|thats not)/;
|
|
527
|
+
var FRUSTRATION = /(ugh|wtf|wth|come on|seriously|again\?|for the (last|nth) time|are you kidding|jesus|christ|frustrat|annoying|wasting|waste of)/;
|
|
528
|
+
var PRAISE = /\b(perfect|exactly|nailed it|love (it|this)|beautiful|great work|ship it|clean|nice work|excellent|that'?s it|yes!+|amazing|chef'?s kiss|brilliant)\b/;
|
|
529
|
+
function classifyReaction(text) {
|
|
530
|
+
const t = text.toLowerCase();
|
|
531
|
+
const hits = [];
|
|
532
|
+
if (JARGON_COMPLAINT.test(t)) hits.push("jargon");
|
|
533
|
+
if (STRUCTURE_COMPLAINT.test(t)) hits.push("structure");
|
|
534
|
+
if (CORRECTION.test(t)) hits.push("correction");
|
|
535
|
+
if (FRUSTRATION.test(t)) hits.push("frustration");
|
|
536
|
+
if (PRAISE.test(t)) hits.push("praise");
|
|
537
|
+
return hits;
|
|
538
|
+
}
|
|
539
|
+
var CORRECTIVE_REACTIONS = ["correction", "frustration", "jargon", "structure"];
|
|
540
|
+
|
|
435
541
|
// src/adapters/claude.ts
|
|
436
542
|
import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
437
543
|
import { homedir as homedir2 } from "os";
|
|
438
|
-
import { basename as basename2, join as
|
|
544
|
+
import { basename as basename2, join as join3 } from "path";
|
|
439
545
|
var SERVICE2 = "claude-code";
|
|
440
546
|
function parseLines(raw) {
|
|
441
547
|
const out = [];
|
|
@@ -473,6 +579,7 @@ function parseClaudeStream(events, ctx) {
|
|
|
473
579
|
const spans = [];
|
|
474
580
|
const toolSpanByUseId = /* @__PURE__ */ new Map();
|
|
475
581
|
let step = ctx.startStep;
|
|
582
|
+
let sawUserTurn = false;
|
|
476
583
|
for (const ev of events) {
|
|
477
584
|
const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
478
585
|
const uid = ev.uuid ?? `${ctx.idPrefix}step${step}`;
|
|
@@ -520,6 +627,13 @@ function parseClaudeStream(events, ctx) {
|
|
|
520
627
|
} else if (ev.type === "user" && ev.message) {
|
|
521
628
|
const prompt = textOf2(ev.message.content);
|
|
522
629
|
if (prompt) {
|
|
630
|
+
const actor = claudeActor({
|
|
631
|
+
text: prompt,
|
|
632
|
+
isSidechain: ev.isSidechain,
|
|
633
|
+
userType: ev.userType ?? null,
|
|
634
|
+
isFirstUserTurn: !sawUserTurn
|
|
635
|
+
});
|
|
636
|
+
sawUserTurn = true;
|
|
523
637
|
spans.push(
|
|
524
638
|
userPromptSpan({
|
|
525
639
|
traceId: ctx.traceId,
|
|
@@ -529,7 +643,8 @@ function parseClaudeStream(events, ctx) {
|
|
|
529
643
|
service: SERVICE2,
|
|
530
644
|
agent: ctx.agent,
|
|
531
645
|
step,
|
|
532
|
-
content: prompt
|
|
646
|
+
content: prompt,
|
|
647
|
+
actor
|
|
533
648
|
})
|
|
534
649
|
);
|
|
535
650
|
step += 1;
|
|
@@ -556,7 +671,7 @@ var ClaudeAdapter = class {
|
|
|
556
671
|
harness = "claude-code";
|
|
557
672
|
aliases = ["claude", "claudish", "openclaw", "nanoclaw"];
|
|
558
673
|
root() {
|
|
559
|
-
return
|
|
674
|
+
return join3(homedir2(), ".claude", "projects");
|
|
560
675
|
}
|
|
561
676
|
async locate(opts = {}) {
|
|
562
677
|
const root = this.root();
|
|
@@ -568,7 +683,7 @@ var ClaudeAdapter = class {
|
|
|
568
683
|
}
|
|
569
684
|
const refs = [];
|
|
570
685
|
for (const dir of projectDirs) {
|
|
571
|
-
const dirPath =
|
|
686
|
+
const dirPath = join3(root, dir);
|
|
572
687
|
let files;
|
|
573
688
|
try {
|
|
574
689
|
files = await readdir2(dirPath);
|
|
@@ -577,7 +692,7 @@ var ClaudeAdapter = class {
|
|
|
577
692
|
}
|
|
578
693
|
for (const f of files) {
|
|
579
694
|
if (!f.endsWith(".jsonl")) continue;
|
|
580
|
-
const path =
|
|
695
|
+
const path = join3(dirPath, f);
|
|
581
696
|
let st;
|
|
582
697
|
try {
|
|
583
698
|
st = await stat2(path);
|
|
@@ -624,7 +739,7 @@ var ClaudeAdapter = class {
|
|
|
624
739
|
}
|
|
625
740
|
/** Parse `<session>/subagents/agent-*.jsonl`, parenting each under its Agent call. */
|
|
626
741
|
async foldSubagents(ref, traceId, main, out) {
|
|
627
|
-
const subDir =
|
|
742
|
+
const subDir = join3(ref.path.replace(/\.jsonl$/, ""), "subagents");
|
|
628
743
|
let files;
|
|
629
744
|
try {
|
|
630
745
|
files = await readdir2(subDir);
|
|
@@ -637,13 +752,13 @@ var ClaudeAdapter = class {
|
|
|
637
752
|
const hash = basename2(f, ".jsonl");
|
|
638
753
|
let meta = {};
|
|
639
754
|
try {
|
|
640
|
-
meta = JSON.parse(await readFile3(
|
|
755
|
+
meta = JSON.parse(await readFile3(join3(subDir, `${hash}.meta.json`), "utf8"));
|
|
641
756
|
} catch {
|
|
642
757
|
}
|
|
643
758
|
const parent = meta.toolUseId && main.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
|
|
644
759
|
let raw;
|
|
645
760
|
try {
|
|
646
|
-
raw = await readFile3(
|
|
761
|
+
raw = await readFile3(join3(subDir, f), "utf8");
|
|
647
762
|
} catch {
|
|
648
763
|
continue;
|
|
649
764
|
}
|
|
@@ -663,7 +778,7 @@ var ClaudeAdapter = class {
|
|
|
663
778
|
// src/adapters/codex.ts
|
|
664
779
|
import { readdir as readdir3, readFile as readFile4, stat as stat3 } from "fs/promises";
|
|
665
780
|
import { homedir as homedir3 } from "os";
|
|
666
|
-
import { basename as basename3, join as
|
|
781
|
+
import { basename as basename3, join as join4 } from "path";
|
|
667
782
|
var SERVICE3 = "codex";
|
|
668
783
|
function parseLines2(raw) {
|
|
669
784
|
const out = [];
|
|
@@ -699,7 +814,7 @@ async function* walkRollouts(root) {
|
|
|
699
814
|
return;
|
|
700
815
|
}
|
|
701
816
|
for (const y of years) {
|
|
702
|
-
const yp =
|
|
817
|
+
const yp = join4(root, y);
|
|
703
818
|
let months;
|
|
704
819
|
try {
|
|
705
820
|
months = await readdir3(yp);
|
|
@@ -707,7 +822,7 @@ async function* walkRollouts(root) {
|
|
|
707
822
|
continue;
|
|
708
823
|
}
|
|
709
824
|
for (const m of months) {
|
|
710
|
-
const mp =
|
|
825
|
+
const mp = join4(yp, m);
|
|
711
826
|
let days;
|
|
712
827
|
try {
|
|
713
828
|
days = await readdir3(mp);
|
|
@@ -715,7 +830,7 @@ async function* walkRollouts(root) {
|
|
|
715
830
|
continue;
|
|
716
831
|
}
|
|
717
832
|
for (const d of days) {
|
|
718
|
-
const dp =
|
|
833
|
+
const dp = join4(mp, d);
|
|
719
834
|
let files;
|
|
720
835
|
try {
|
|
721
836
|
files = await readdir3(dp);
|
|
@@ -723,7 +838,7 @@ async function* walkRollouts(root) {
|
|
|
723
838
|
continue;
|
|
724
839
|
}
|
|
725
840
|
for (const f of files) {
|
|
726
|
-
if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield
|
|
841
|
+
if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join4(dp, f);
|
|
727
842
|
}
|
|
728
843
|
}
|
|
729
844
|
}
|
|
@@ -733,7 +848,7 @@ var CodexAdapter = class {
|
|
|
733
848
|
harness = "codex";
|
|
734
849
|
aliases = ["codex-acp"];
|
|
735
850
|
root() {
|
|
736
|
-
return process.env.CODEX_HOME ?
|
|
851
|
+
return process.env.CODEX_HOME ? join4(process.env.CODEX_HOME, "sessions") : join4(homedir3(), ".codex", "sessions");
|
|
737
852
|
}
|
|
738
853
|
async locate(opts = {}) {
|
|
739
854
|
const refs = [];
|
|
@@ -782,6 +897,7 @@ var CodexAdapter = class {
|
|
|
782
897
|
const toolByCallId = /* @__PURE__ */ new Map();
|
|
783
898
|
let step = 0;
|
|
784
899
|
let lastLlm = rootId;
|
|
900
|
+
let sawUserTurn = false;
|
|
785
901
|
for (const l of lines) {
|
|
786
902
|
const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
787
903
|
if (l.type === "event_msg" && l.payload?.type === "token_count") {
|
|
@@ -836,6 +952,8 @@ var CodexAdapter = class {
|
|
|
836
952
|
} else if (l.type === "response_item" && l.payload?.type === "message" && l.payload.role === "user") {
|
|
837
953
|
const prompt = textOf3(l.payload.content);
|
|
838
954
|
if (prompt) {
|
|
955
|
+
const actor = codexActor({ text: prompt, isFirstUserTurn: !sawUserTurn });
|
|
956
|
+
sawUserTurn = true;
|
|
839
957
|
spans.push(
|
|
840
958
|
userPromptSpan({
|
|
841
959
|
traceId,
|
|
@@ -845,7 +963,8 @@ var CodexAdapter = class {
|
|
|
845
963
|
content: prompt,
|
|
846
964
|
service: SERVICE3,
|
|
847
965
|
agent: SERVICE3,
|
|
848
|
-
step
|
|
966
|
+
step,
|
|
967
|
+
actor
|
|
849
968
|
})
|
|
850
969
|
);
|
|
851
970
|
step += 1;
|
|
@@ -878,7 +997,7 @@ var CodexAdapter = class {
|
|
|
878
997
|
// src/adapters/copilot.ts
|
|
879
998
|
import { readdir as readdir4, readFile as readFile5, stat as stat4 } from "fs/promises";
|
|
880
999
|
import { homedir as homedir4 } from "os";
|
|
881
|
-
import { join as
|
|
1000
|
+
import { join as join5 } from "path";
|
|
882
1001
|
var SERVICE4 = "github-copilot";
|
|
883
1002
|
function parseLines3(raw) {
|
|
884
1003
|
const out = [];
|
|
@@ -895,7 +1014,7 @@ var CopilotAdapter = class {
|
|
|
895
1014
|
harness = "github-copilot";
|
|
896
1015
|
aliases = ["copilot"];
|
|
897
1016
|
root() {
|
|
898
|
-
return
|
|
1017
|
+
return join5(process.env.COPILOT_HOME ?? join5(homedir4(), ".copilot"), "session-state");
|
|
899
1018
|
}
|
|
900
1019
|
async locate(opts = {}) {
|
|
901
1020
|
let dirs;
|
|
@@ -906,7 +1025,7 @@ var CopilotAdapter = class {
|
|
|
906
1025
|
}
|
|
907
1026
|
const refs = [];
|
|
908
1027
|
for (const id of dirs) {
|
|
909
|
-
const path =
|
|
1028
|
+
const path = join5(this.root(), id, "events.jsonl");
|
|
910
1029
|
let st;
|
|
911
1030
|
try {
|
|
912
1031
|
st = await stat4(path);
|
|
@@ -1004,7 +1123,7 @@ var CopilotAdapter = class {
|
|
|
1004
1123
|
// src/adapters/factory.ts
|
|
1005
1124
|
import { readdir as readdir5, readFile as readFile6, stat as stat5 } from "fs/promises";
|
|
1006
1125
|
import { homedir as homedir5 } from "os";
|
|
1007
|
-
import { basename as basename4, join as
|
|
1126
|
+
import { basename as basename4, join as join6 } from "path";
|
|
1008
1127
|
var SERVICE5 = "factory";
|
|
1009
1128
|
function textOf4(content) {
|
|
1010
1129
|
return capText(
|
|
@@ -1026,7 +1145,7 @@ var FactoryAdapter = class {
|
|
|
1026
1145
|
harness = "factory";
|
|
1027
1146
|
aliases = ["factory-droids", "droid"];
|
|
1028
1147
|
root() {
|
|
1029
|
-
return
|
|
1148
|
+
return join6(homedir5(), ".factory", "sessions");
|
|
1030
1149
|
}
|
|
1031
1150
|
async locate(opts = {}) {
|
|
1032
1151
|
const root = this.root();
|
|
@@ -1038,7 +1157,7 @@ var FactoryAdapter = class {
|
|
|
1038
1157
|
}
|
|
1039
1158
|
const refs = [];
|
|
1040
1159
|
for (const dir of dirs) {
|
|
1041
|
-
const dp =
|
|
1160
|
+
const dp = join6(root, dir);
|
|
1042
1161
|
let files;
|
|
1043
1162
|
try {
|
|
1044
1163
|
files = await readdir5(dp);
|
|
@@ -1049,7 +1168,7 @@ var FactoryAdapter = class {
|
|
|
1049
1168
|
if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
|
|
1050
1169
|
for (const f of files) {
|
|
1051
1170
|
if (!f.endsWith(".jsonl")) continue;
|
|
1052
|
-
const path =
|
|
1171
|
+
const path = join6(dp, f);
|
|
1053
1172
|
let st;
|
|
1054
1173
|
try {
|
|
1055
1174
|
st = await stat5(path);
|
|
@@ -1165,7 +1284,7 @@ var FactoryAdapter = class {
|
|
|
1165
1284
|
// src/adapters/forge.ts
|
|
1166
1285
|
import { readdir as readdir6, readFile as readFile7, stat as stat6 } from "fs/promises";
|
|
1167
1286
|
import { homedir as homedir6 } from "os";
|
|
1168
|
-
import { basename as basename5, join as
|
|
1287
|
+
import { basename as basename5, join as join7 } from "path";
|
|
1169
1288
|
var SERVICE6 = "forge";
|
|
1170
1289
|
function tokens(t) {
|
|
1171
1290
|
if (!t) return null;
|
|
@@ -1179,7 +1298,7 @@ var ForgeAdapter = class {
|
|
|
1179
1298
|
aliases = ["forgecode"];
|
|
1180
1299
|
/** Directories scanned for `*-dump.json` exports. */
|
|
1181
1300
|
dumpDirs() {
|
|
1182
|
-
return [process.cwd(),
|
|
1301
|
+
return [process.cwd(), join7(homedir6(), ".forge")];
|
|
1183
1302
|
}
|
|
1184
1303
|
async locate(opts = {}) {
|
|
1185
1304
|
const refs = [];
|
|
@@ -1193,7 +1312,7 @@ var ForgeAdapter = class {
|
|
|
1193
1312
|
}
|
|
1194
1313
|
for (const f of files) {
|
|
1195
1314
|
if (!f.endsWith("-dump.json")) continue;
|
|
1196
|
-
const path =
|
|
1315
|
+
const path = join7(dir, f);
|
|
1197
1316
|
let st;
|
|
1198
1317
|
try {
|
|
1199
1318
|
st = await stat6(path);
|
|
@@ -1295,7 +1414,7 @@ var ForgeAdapter = class {
|
|
|
1295
1414
|
// src/adapters/gemini.ts
|
|
1296
1415
|
import { readdir as readdir7, readFile as readFile8, stat as stat7 } from "fs/promises";
|
|
1297
1416
|
import { homedir as homedir7 } from "os";
|
|
1298
|
-
import { join as
|
|
1417
|
+
import { join as join8 } from "path";
|
|
1299
1418
|
function iso(ts, fallback) {
|
|
1300
1419
|
if (typeof ts === "number") return new Date(ts).toISOString();
|
|
1301
1420
|
if (typeof ts === "string" && ts.length > 0) return ts;
|
|
@@ -1320,7 +1439,7 @@ var GeminiFamilyAdapter = class {
|
|
|
1320
1439
|
this.homeDirName = cfg.homeDirName;
|
|
1321
1440
|
}
|
|
1322
1441
|
chatsRoot() {
|
|
1323
|
-
return
|
|
1442
|
+
return join8(homedir7(), this.homeDirName, "tmp");
|
|
1324
1443
|
}
|
|
1325
1444
|
async locate(opts = {}) {
|
|
1326
1445
|
const root = this.chatsRoot();
|
|
@@ -1332,7 +1451,7 @@ var GeminiFamilyAdapter = class {
|
|
|
1332
1451
|
}
|
|
1333
1452
|
const refs = [];
|
|
1334
1453
|
for (const pd of projectDirs) {
|
|
1335
|
-
const chatsDir =
|
|
1454
|
+
const chatsDir = join8(root, pd, "chats");
|
|
1336
1455
|
let files;
|
|
1337
1456
|
try {
|
|
1338
1457
|
files = await readdir7(chatsDir);
|
|
@@ -1341,7 +1460,7 @@ var GeminiFamilyAdapter = class {
|
|
|
1341
1460
|
}
|
|
1342
1461
|
for (const f of files) {
|
|
1343
1462
|
if (!f.startsWith("session-") || !f.endsWith(".json")) continue;
|
|
1344
|
-
const path =
|
|
1463
|
+
const path = join8(chatsDir, f);
|
|
1345
1464
|
let st;
|
|
1346
1465
|
try {
|
|
1347
1466
|
st = await stat7(path);
|
|
@@ -1454,7 +1573,7 @@ var GeminiAdapter = class extends GeminiFamilyAdapter {
|
|
|
1454
1573
|
// src/adapters/opencode.ts
|
|
1455
1574
|
import { readdir as readdir8, readFile as readFile9, stat as stat8 } from "fs/promises";
|
|
1456
1575
|
import { homedir as homedir8 } from "os";
|
|
1457
|
-
import { join as
|
|
1576
|
+
import { join as join9 } from "path";
|
|
1458
1577
|
var SERVICE7 = "opencode";
|
|
1459
1578
|
function isoOf(ms) {
|
|
1460
1579
|
return new Date(ms ?? 0).toISOString();
|
|
@@ -1467,11 +1586,11 @@ function textOf7(parts) {
|
|
|
1467
1586
|
var OpencodeAdapter = class {
|
|
1468
1587
|
harness = "opencode";
|
|
1469
1588
|
storage() {
|
|
1470
|
-
const base = process.env.XDG_DATA_HOME ??
|
|
1471
|
-
return
|
|
1589
|
+
const base = process.env.XDG_DATA_HOME ?? join9(homedir8(), ".local", "share");
|
|
1590
|
+
return join9(base, "opencode", "storage");
|
|
1472
1591
|
}
|
|
1473
1592
|
async locate(opts = {}) {
|
|
1474
|
-
const msgRoot =
|
|
1593
|
+
const msgRoot = join9(this.storage(), "message");
|
|
1475
1594
|
let sessionDirs;
|
|
1476
1595
|
try {
|
|
1477
1596
|
sessionDirs = await readdir8(msgRoot);
|
|
@@ -1480,7 +1599,7 @@ var OpencodeAdapter = class {
|
|
|
1480
1599
|
}
|
|
1481
1600
|
const refs = [];
|
|
1482
1601
|
for (const sid of sessionDirs) {
|
|
1483
|
-
const dir =
|
|
1602
|
+
const dir = join9(msgRoot, sid);
|
|
1484
1603
|
let st;
|
|
1485
1604
|
try {
|
|
1486
1605
|
st = await stat8(dir);
|
|
@@ -1497,7 +1616,7 @@ var OpencodeAdapter = class {
|
|
|
1497
1616
|
async parse(ref) {
|
|
1498
1617
|
const traceId = ref.sessionId;
|
|
1499
1618
|
const rootId = `root:${traceId}`;
|
|
1500
|
-
const partRoot =
|
|
1619
|
+
const partRoot = join9(this.storage(), "part");
|
|
1501
1620
|
let msgFiles;
|
|
1502
1621
|
try {
|
|
1503
1622
|
msgFiles = (await readdir8(ref.path)).filter((f) => f.endsWith(".json"));
|
|
@@ -1507,7 +1626,7 @@ var OpencodeAdapter = class {
|
|
|
1507
1626
|
const messages = [];
|
|
1508
1627
|
for (const f of msgFiles) {
|
|
1509
1628
|
try {
|
|
1510
|
-
messages.push({ msg: JSON.parse(await readFile9(
|
|
1629
|
+
messages.push({ msg: JSON.parse(await readFile9(join9(ref.path, f), "utf8")), file: f });
|
|
1511
1630
|
} catch {
|
|
1512
1631
|
}
|
|
1513
1632
|
}
|
|
@@ -1529,13 +1648,13 @@ var OpencodeAdapter = class {
|
|
|
1529
1648
|
for (const { msg } of messages) {
|
|
1530
1649
|
const mid = msg.id ?? `m${step}`;
|
|
1531
1650
|
const llmId = `llm:${mid}`;
|
|
1532
|
-
const pdir =
|
|
1651
|
+
const pdir = join9(partRoot, mid);
|
|
1533
1652
|
const parts = [];
|
|
1534
1653
|
try {
|
|
1535
1654
|
const partFiles = (await readdir8(pdir)).filter((f) => f.endsWith(".json"));
|
|
1536
1655
|
for (const pf of partFiles) {
|
|
1537
1656
|
try {
|
|
1538
|
-
parts.push(JSON.parse(await readFile9(
|
|
1657
|
+
parts.push(JSON.parse(await readFile9(join9(pdir, pf), "utf8")));
|
|
1539
1658
|
} catch {
|
|
1540
1659
|
}
|
|
1541
1660
|
}
|
|
@@ -1607,7 +1726,7 @@ var OpencodeAdapter = class {
|
|
|
1607
1726
|
// src/adapters/pi.ts
|
|
1608
1727
|
import { readdir as readdir9, readFile as readFile10, stat as stat9 } from "fs/promises";
|
|
1609
1728
|
import { homedir as homedir9 } from "os";
|
|
1610
|
-
import { basename as basename6, join as
|
|
1729
|
+
import { basename as basename6, join as join10 } from "path";
|
|
1611
1730
|
var SERVICE8 = "pi";
|
|
1612
1731
|
function parseLines5(raw) {
|
|
1613
1732
|
const out = [];
|
|
@@ -1635,7 +1754,7 @@ function textOf8(content) {
|
|
|
1635
1754
|
var PiAdapter = class {
|
|
1636
1755
|
harness = "pi";
|
|
1637
1756
|
root() {
|
|
1638
|
-
return
|
|
1757
|
+
return join10(homedir9(), ".pi", "agent", "sessions");
|
|
1639
1758
|
}
|
|
1640
1759
|
async locate(opts = {}) {
|
|
1641
1760
|
const root = this.root();
|
|
@@ -1647,7 +1766,7 @@ var PiAdapter = class {
|
|
|
1647
1766
|
}
|
|
1648
1767
|
const refs = [];
|
|
1649
1768
|
for (const dir of dirs) {
|
|
1650
|
-
const dp =
|
|
1769
|
+
const dp = join10(root, dir);
|
|
1651
1770
|
let files;
|
|
1652
1771
|
try {
|
|
1653
1772
|
files = await readdir9(dp);
|
|
@@ -1658,7 +1777,7 @@ var PiAdapter = class {
|
|
|
1658
1777
|
if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
|
|
1659
1778
|
for (const f of files) {
|
|
1660
1779
|
if (!f.endsWith(".jsonl")) continue;
|
|
1661
|
-
const path =
|
|
1780
|
+
const path = join10(dp, f);
|
|
1662
1781
|
let st;
|
|
1663
1782
|
try {
|
|
1664
1783
|
st = await stat9(path);
|
|
@@ -1776,7 +1895,7 @@ var PiAdapter = class {
|
|
|
1776
1895
|
// src/adapters/qwen.ts
|
|
1777
1896
|
import { readdir as readdir10, readFile as readFile11, stat as stat10 } from "fs/promises";
|
|
1778
1897
|
import { homedir as homedir10 } from "os";
|
|
1779
|
-
import { basename as basename7, join as
|
|
1898
|
+
import { basename as basename7, join as join11 } from "path";
|
|
1780
1899
|
var SERVICE9 = "qwen";
|
|
1781
1900
|
function parseLines6(raw) {
|
|
1782
1901
|
const out = [];
|
|
@@ -1797,7 +1916,7 @@ var QwenAdapter = class {
|
|
|
1797
1916
|
harness = "qwen";
|
|
1798
1917
|
aliases = ["qwen-code"];
|
|
1799
1918
|
root() {
|
|
1800
|
-
return
|
|
1919
|
+
return join11(homedir10(), ".qwen", "projects");
|
|
1801
1920
|
}
|
|
1802
1921
|
async locate(opts = {}) {
|
|
1803
1922
|
const root = this.root();
|
|
@@ -1809,7 +1928,7 @@ var QwenAdapter = class {
|
|
|
1809
1928
|
}
|
|
1810
1929
|
const refs = [];
|
|
1811
1930
|
for (const pd of projectDirs) {
|
|
1812
|
-
const chatsDir =
|
|
1931
|
+
const chatsDir = join11(root, pd, "chats");
|
|
1813
1932
|
let files;
|
|
1814
1933
|
try {
|
|
1815
1934
|
files = await readdir10(chatsDir);
|
|
@@ -1819,7 +1938,7 @@ var QwenAdapter = class {
|
|
|
1819
1938
|
if (opts.cwd) continue;
|
|
1820
1939
|
for (const f of files) {
|
|
1821
1940
|
if (!f.endsWith(".jsonl")) continue;
|
|
1822
|
-
const path =
|
|
1941
|
+
const path = join11(chatsDir, f);
|
|
1823
1942
|
let st;
|
|
1824
1943
|
try {
|
|
1825
1944
|
st = await stat10(path);
|
|
@@ -1969,7 +2088,103 @@ function selectAdapters(opts) {
|
|
|
1969
2088
|
});
|
|
1970
2089
|
}
|
|
1971
2090
|
|
|
2091
|
+
// src/repo.ts
|
|
2092
|
+
function normalizeRemote(url) {
|
|
2093
|
+
const raw = url.trim();
|
|
2094
|
+
if (!raw) return null;
|
|
2095
|
+
const url2 = raw.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/(.+)$/);
|
|
2096
|
+
if (url2) {
|
|
2097
|
+
let rest = url2[1];
|
|
2098
|
+
const at = rest.lastIndexOf("@");
|
|
2099
|
+
if (at !== -1) rest = rest.slice(at + 1);
|
|
2100
|
+
rest = rest.replace(/\.git$/, "").replace(/\/+$/g, "");
|
|
2101
|
+
const slash = rest.indexOf("/");
|
|
2102
|
+
if (slash === -1) return null;
|
|
2103
|
+
const host = rest.slice(0, slash).replace(/:\d+$/, "");
|
|
2104
|
+
const path = rest.slice(slash + 1).replace(/^\/+/, "");
|
|
2105
|
+
return host && path ? `${host}/${path}` : null;
|
|
2106
|
+
}
|
|
2107
|
+
const scp = raw.match(/^[^@/]+@([^:/]+):(.+)$/);
|
|
2108
|
+
if (scp) {
|
|
2109
|
+
const path = scp[2].replace(/\.git$/, "").replace(/^\/+|\/+$/g, "");
|
|
2110
|
+
return path ? `${scp[1]}/${path}` : null;
|
|
2111
|
+
}
|
|
2112
|
+
return null;
|
|
2113
|
+
}
|
|
2114
|
+
async function readGit(cwd) {
|
|
2115
|
+
const { stat: stat11 } = await import("fs/promises");
|
|
2116
|
+
await stat11(`${cwd}/.git`);
|
|
2117
|
+
const { execFile } = await import("child_process");
|
|
2118
|
+
const { promisify } = await import("util");
|
|
2119
|
+
const run = promisify(execFile);
|
|
2120
|
+
const git = async (args) => {
|
|
2121
|
+
try {
|
|
2122
|
+
const { stdout } = await run("git", ["-C", cwd, ...args], { timeout: 5e3 });
|
|
2123
|
+
const v = stdout.trim();
|
|
2124
|
+
return v.length > 0 ? v : null;
|
|
2125
|
+
} catch {
|
|
2126
|
+
return null;
|
|
2127
|
+
}
|
|
2128
|
+
};
|
|
2129
|
+
let remote = await git(["remote", "get-url", "origin"]);
|
|
2130
|
+
if (!remote) {
|
|
2131
|
+
const first = await git(["remote"]);
|
|
2132
|
+
const name = first?.split("\n")[0]?.trim();
|
|
2133
|
+
if (name) remote = await git(["remote", "get-url", name]);
|
|
2134
|
+
}
|
|
2135
|
+
const branch = await git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
2136
|
+
const commit = await git(["rev-parse", "--short", "HEAD"]);
|
|
2137
|
+
return {
|
|
2138
|
+
remote,
|
|
2139
|
+
branch: branch === "HEAD" ? null : branch,
|
|
2140
|
+
// detached HEAD → no branch name
|
|
2141
|
+
commit
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
async function resolveRepoAttrs(cwd) {
|
|
2145
|
+
if (!cwd) return {};
|
|
2146
|
+
const attrs = { [ATTR.CWD]: cwd };
|
|
2147
|
+
const basename9 = (() => {
|
|
2148
|
+
const trimmed = cwd.replace(/\/+$/g, "");
|
|
2149
|
+
const idx = trimmed.lastIndexOf("/");
|
|
2150
|
+
const name = idx === -1 ? trimmed : trimmed.slice(idx + 1);
|
|
2151
|
+
return name || trimmed;
|
|
2152
|
+
})();
|
|
2153
|
+
try {
|
|
2154
|
+
const { remote, branch, commit } = await readGit(cwd);
|
|
2155
|
+
const normalized = remote ? normalizeRemote(remote) : null;
|
|
2156
|
+
if (normalized) {
|
|
2157
|
+
attrs[ATTR.SUBJECT_KEY] = normalized;
|
|
2158
|
+
attrs[ATTR.GIT_REPOSITORY] = normalized;
|
|
2159
|
+
if (branch) attrs[ATTR.GIT_BRANCH_NAME] = branch;
|
|
2160
|
+
if (commit) attrs[ATTR.GIT_COMMIT] = commit;
|
|
2161
|
+
return attrs;
|
|
2162
|
+
}
|
|
2163
|
+
if (branch) attrs[ATTR.GIT_BRANCH_NAME] = branch;
|
|
2164
|
+
if (commit) attrs[ATTR.GIT_COMMIT] = commit;
|
|
2165
|
+
} catch {
|
|
2166
|
+
}
|
|
2167
|
+
attrs[ATTR.SUBJECT_KEY] = basename9;
|
|
2168
|
+
return attrs;
|
|
2169
|
+
}
|
|
2170
|
+
function stampRepoAttrs(spans, attrs) {
|
|
2171
|
+
const keys = Object.keys(attrs);
|
|
2172
|
+
if (keys.length === 0) return spans;
|
|
2173
|
+
for (const s of spans) {
|
|
2174
|
+
for (const k of keys) {
|
|
2175
|
+
if (s.attributes[k] === void 0) s.attributes[k] = attrs[k];
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
return spans;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
1972
2181
|
// src/session-source.ts
|
|
2182
|
+
async function parseSession(adapter, ref) {
|
|
2183
|
+
const spans = await adapter.parse(ref);
|
|
2184
|
+
const repoAttrs = await resolveRepoAttrs(ref.cwd);
|
|
2185
|
+
stampRepoAttrs(spans, repoAttrs);
|
|
2186
|
+
return spans;
|
|
2187
|
+
}
|
|
1973
2188
|
async function* scanSessions(opts) {
|
|
1974
2189
|
for (const adapter of selectAdapters(opts)) {
|
|
1975
2190
|
if (opts.signal?.aborted) return;
|
|
@@ -1985,7 +2200,7 @@ async function* scanSessions(opts) {
|
|
|
1985
2200
|
if (opts.signal?.aborted) return;
|
|
1986
2201
|
let spans;
|
|
1987
2202
|
try {
|
|
1988
|
-
spans = await adapter
|
|
2203
|
+
spans = await parseSession(adapter, ref);
|
|
1989
2204
|
} catch (err) {
|
|
1990
2205
|
opts.onError?.(err, ref);
|
|
1991
2206
|
continue;
|
|
@@ -1996,106 +2211,233 @@ async function* scanSessions(opts) {
|
|
|
1996
2211
|
}
|
|
1997
2212
|
}
|
|
1998
2213
|
|
|
1999
|
-
// src/
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2214
|
+
// src/evidence.ts
|
|
2215
|
+
import { mkdtemp, writeFile } from "fs/promises";
|
|
2216
|
+
import { tmpdir } from "os";
|
|
2217
|
+
import { join as join12 } from "path";
|
|
2218
|
+
function stringAttr(span2, key) {
|
|
2219
|
+
const value = span2.attributes[key];
|
|
2220
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2221
|
+
}
|
|
2222
|
+
function numberAttr(span2, key) {
|
|
2223
|
+
const value = span2.attributes[key];
|
|
2224
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
2225
|
+
}
|
|
2226
|
+
function spanKind(span2) {
|
|
2227
|
+
return stringAttr(span2, "openinference.span.kind");
|
|
2228
|
+
}
|
|
2229
|
+
function repoFromSpans(spans) {
|
|
2230
|
+
const attrs = {};
|
|
2231
|
+
for (const span2 of spans) {
|
|
2232
|
+
attrs.subjectKey ??= stringAttr(span2, ATTR.SUBJECT_KEY);
|
|
2233
|
+
attrs.repository ??= stringAttr(span2, ATTR.GIT_REPOSITORY);
|
|
2234
|
+
attrs.branch ??= stringAttr(span2, ATTR.GIT_BRANCH_NAME);
|
|
2235
|
+
attrs.commit ??= stringAttr(span2, ATTR.GIT_COMMIT);
|
|
2236
|
+
attrs.cwd ??= stringAttr(span2, ATTR.CWD);
|
|
2237
|
+
if (attrs.subjectKey && attrs.repository && attrs.branch && attrs.commit && attrs.cwd) break;
|
|
2238
|
+
}
|
|
2239
|
+
return attrs;
|
|
2240
|
+
}
|
|
2241
|
+
function timeBounds(spans) {
|
|
2242
|
+
const times = spans.flatMap((span2) => [span2.start_time, span2.end_time]).filter((value) => value && value !== "now").sort();
|
|
2243
|
+
return {
|
|
2244
|
+
firstSpanAt: times[0] ?? null,
|
|
2245
|
+
lastSpanAt: times[times.length - 1] ?? null
|
|
2004
2246
|
};
|
|
2005
|
-
const t = setTimeout(() => {
|
|
2006
|
-
signal?.removeEventListener("abort", onAbort);
|
|
2007
|
-
resolve();
|
|
2008
|
-
}, ms);
|
|
2009
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2010
|
-
});
|
|
2011
|
-
function loopKey(sessionId, toolName, argHash) {
|
|
2012
|
-
return `${sessionId}\0${toolName}\0${argHash}`;
|
|
2013
2247
|
}
|
|
2014
|
-
|
|
2015
|
-
const
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2248
|
+
function summarizeTools(spans) {
|
|
2249
|
+
const byTool = /* @__PURE__ */ new Map();
|
|
2250
|
+
for (const span2 of spans) {
|
|
2251
|
+
if (spanKind(span2) !== "TOOL") continue;
|
|
2252
|
+
const name = stringAttr(span2, "tool.name") ?? span2.name.replace(/^tool\./, "");
|
|
2253
|
+
const current = byTool.get(name) ?? { calls: 0, errors: 0 };
|
|
2254
|
+
current.calls += 1;
|
|
2255
|
+
if (span2.status.code === "ERROR") current.errors += 1;
|
|
2256
|
+
byTool.set(name, current);
|
|
2257
|
+
}
|
|
2258
|
+
return [...byTool.entries()].map(([name, row]) => ({ name, calls: row.calls, errors: row.errors })).sort((a, b) => b.calls - a.calls || a.name.localeCompare(b.name));
|
|
2259
|
+
}
|
|
2260
|
+
async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
|
|
2261
|
+
const llmSpans = spans.filter((span2) => spanKind(span2) === "LLM");
|
|
2262
|
+
const toolSpans = spans.filter((span2) => spanKind(span2) === "TOOL");
|
|
2263
|
+
const erroredToolCallCount = toolSpans.filter((span2) => span2.status.code === "ERROR").length;
|
|
2264
|
+
const pipelines = await runPipelines(spans, { minLoopOccurrences: opts.minLoopOccurrences });
|
|
2265
|
+
const loopLimit = opts.maxLoopExamples ?? 25;
|
|
2266
|
+
const loopFindings = pipelines.stuckLoops.findings;
|
|
2267
|
+
const { firstSpanAt, lastSpanAt } = timeBounds(spans);
|
|
2268
|
+
return {
|
|
2269
|
+
schemaVersion: 1,
|
|
2270
|
+
kind: "traces.policy_evidence.session",
|
|
2271
|
+
generatedAt: opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2272
|
+
session: {
|
|
2273
|
+
harness: ref.harness,
|
|
2274
|
+
sessionId: ref.sessionId,
|
|
2275
|
+
path: ref.path,
|
|
2276
|
+
cwd: ref.cwd,
|
|
2277
|
+
mtimeMs: ref.mtimeMs
|
|
2278
|
+
},
|
|
2279
|
+
repo: repoFromSpans(spans),
|
|
2280
|
+
metrics: {
|
|
2281
|
+
spanCount: spans.length,
|
|
2282
|
+
llmTurnCount: llmSpans.length,
|
|
2283
|
+
toolCallCount: toolSpans.length,
|
|
2284
|
+
erroredToolCallCount,
|
|
2285
|
+
inputTokens: llmSpans.reduce((sum, span2) => sum + numberAttr(span2, "llm.input_tokens"), 0),
|
|
2286
|
+
outputTokens: llmSpans.reduce((sum, span2) => sum + numberAttr(span2, "llm.output_tokens"), 0),
|
|
2287
|
+
models: [...new Set(llmSpans.map((span2) => stringAttr(span2, "llm.model_name")).filter((value) => Boolean(value)))].sort(),
|
|
2288
|
+
tools: summarizeTools(spans),
|
|
2289
|
+
firstSpanAt,
|
|
2290
|
+
lastSpanAt
|
|
2291
|
+
},
|
|
2292
|
+
signals: {
|
|
2293
|
+
stuckLoopCount: loopFindings.length,
|
|
2294
|
+
affectedRunRatio: pipelines.stuckLoops.affectedRunRatio,
|
|
2295
|
+
stuckLoops: loopFindings.slice(0, loopLimit).map((finding) => ({
|
|
2296
|
+
toolName: finding.toolName,
|
|
2297
|
+
occurrences: finding.occurrences
|
|
2298
|
+
})),
|
|
2299
|
+
stuckLoopsOmitted: Math.max(0, loopFindings.length - loopLimit),
|
|
2300
|
+
toolErrorRate: toolSpans.length === 0 ? 0 : erroredToolCallCount / toolSpans.length
|
|
2301
|
+
},
|
|
2302
|
+
provenance: {
|
|
2303
|
+
source: "traces",
|
|
2304
|
+
evidenceKind: "session-summary",
|
|
2305
|
+
...opts.otlpPath ? { otlpPath: opts.otlpPath } : {},
|
|
2306
|
+
notCampaignCell: true,
|
|
2307
|
+
note: "This is normalized coding-agent session evidence for downstream policy mining; it is not an eval campaign cell."
|
|
2308
|
+
}
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
async function collectPolicyEvidence(opts) {
|
|
2312
|
+
const records = [];
|
|
2313
|
+
for await (const session of scanSessions(opts)) {
|
|
2314
|
+
records.push(await buildPolicyEvidenceRecord(session.ref, session.spans, opts));
|
|
2315
|
+
}
|
|
2316
|
+
return records;
|
|
2317
|
+
}
|
|
2318
|
+
function serializePolicyEvidence(records) {
|
|
2319
|
+
if (records.length === 0) return "";
|
|
2320
|
+
return `${records.map((record) => JSON.stringify(record)).join("\n")}
|
|
2321
|
+
`;
|
|
2322
|
+
}
|
|
2323
|
+
async function writePolicyEvidenceFile(records, outPath) {
|
|
2324
|
+
const path = outPath ?? join12(await mkdtemp(join12(tmpdir(), "traces-evidence-")), "policy-evidence.jsonl");
|
|
2325
|
+
await writeFile(path, serializePolicyEvidence(records), "utf8");
|
|
2326
|
+
return path;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
// src/external.ts
|
|
2330
|
+
import { spawn } from "child_process";
|
|
2331
|
+
function runCommand(command, args = [], opts = {}) {
|
|
2332
|
+
const timeoutMs = opts.timeoutMs ?? 12e4;
|
|
2333
|
+
const maxBuffer = opts.maxBuffer ?? 32 * 1024 * 1024;
|
|
2334
|
+
return new Promise((resolve, reject) => {
|
|
2335
|
+
let child;
|
|
2336
|
+
try {
|
|
2337
|
+
child = spawn(command, [...args], { signal: opts.signal });
|
|
2338
|
+
} catch (err2) {
|
|
2339
|
+
reject(err2 instanceof Error ? err2 : new Error(String(err2)));
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
let out = "";
|
|
2343
|
+
let err = "";
|
|
2344
|
+
let killed = false;
|
|
2345
|
+
const timer = setTimeout(() => {
|
|
2346
|
+
killed = true;
|
|
2347
|
+
child.kill("SIGKILL");
|
|
2348
|
+
reject(new Error(`${command}: timed out after ${timeoutMs}ms`));
|
|
2349
|
+
}, timeoutMs);
|
|
2350
|
+
const cap = (add, buf) => {
|
|
2351
|
+
const next = buf + add;
|
|
2352
|
+
if (next.length > maxBuffer) {
|
|
2353
|
+
killed = true;
|
|
2354
|
+
child.kill("SIGKILL");
|
|
2355
|
+
clearTimeout(timer);
|
|
2356
|
+
reject(new Error(`${command}: output exceeded ${maxBuffer} bytes`));
|
|
2357
|
+
}
|
|
2358
|
+
return next;
|
|
2359
|
+
};
|
|
2360
|
+
child.stdout?.on("data", (d) => {
|
|
2361
|
+
out = cap(String(d), out);
|
|
2362
|
+
});
|
|
2363
|
+
child.stderr?.on("data", (d) => {
|
|
2364
|
+
err = cap(String(d), err);
|
|
2365
|
+
});
|
|
2366
|
+
child.on("error", (e) => {
|
|
2367
|
+
clearTimeout(timer);
|
|
2368
|
+
if (!killed) reject(e);
|
|
2369
|
+
});
|
|
2370
|
+
child.on("close", (code) => {
|
|
2371
|
+
clearTimeout(timer);
|
|
2372
|
+
if (!killed) resolve({ stdout: out, stderr: err, code });
|
|
2373
|
+
});
|
|
2374
|
+
if (opts.input !== void 0) {
|
|
2375
|
+
child.stdin?.on("error", () => {
|
|
2376
|
+
});
|
|
2377
|
+
child.stdin?.end(opts.input);
|
|
2378
|
+
}
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
2381
|
+
function commandAnalyzer(spec) {
|
|
2382
|
+
return {
|
|
2383
|
+
name: spec.name,
|
|
2384
|
+
async analyze(otlpPath, opts = {}) {
|
|
2385
|
+
try {
|
|
2386
|
+
const res = await runCommand(spec.command, spec.args(otlpPath, opts.prompt), {
|
|
2387
|
+
signal: opts.signal,
|
|
2388
|
+
timeoutMs: spec.timeoutMs
|
|
2389
|
+
});
|
|
2390
|
+
if (res.code !== 0) {
|
|
2391
|
+
return { analyzer: spec.name, ok: false, output: res.stdout, error: res.stderr.trim() || `exit ${res.code}` };
|
|
2064
2392
|
}
|
|
2393
|
+
return { analyzer: spec.name, ok: true, output: res.stdout.trim(), findings: spec.parse?.(res.stdout) };
|
|
2394
|
+
} catch (e) {
|
|
2395
|
+
return { analyzer: spec.name, ok: false, output: "", error: e instanceof Error ? e.message : String(e) };
|
|
2065
2396
|
}
|
|
2066
2397
|
}
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
function haloAnalyzer(opts = {}) {
|
|
2401
|
+
const defaultPrompt = opts.defaultPrompt ?? "diagnose";
|
|
2402
|
+
return commandAnalyzer({
|
|
2403
|
+
name: "halo",
|
|
2404
|
+
command: opts.command ?? "halo",
|
|
2405
|
+
args: (otlpPath, prompt) => {
|
|
2406
|
+
const args = [otlpPath, "-p", prompt ?? defaultPrompt];
|
|
2407
|
+
if (opts.model) args.push("-m", opts.model);
|
|
2408
|
+
return args;
|
|
2409
|
+
},
|
|
2410
|
+
timeoutMs: opts.timeoutMs
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
function runExternalAnalyzers(otlpPath, analyzers, opts = {}) {
|
|
2414
|
+
return Promise.all(analyzers.map((a) => a.analyze(otlpPath, opts)));
|
|
2415
|
+
}
|
|
2416
|
+
function commandRedactor(spec) {
|
|
2417
|
+
const name = spec.name ?? spec.command;
|
|
2418
|
+
return {
|
|
2419
|
+
name,
|
|
2420
|
+
async redactText(texts) {
|
|
2421
|
+
if (texts.length === 0) return [];
|
|
2422
|
+
const res = await runCommand(spec.command, spec.args ?? [], {
|
|
2423
|
+
input: JSON.stringify(texts),
|
|
2424
|
+
timeoutMs: spec.timeoutMs
|
|
2425
|
+
});
|
|
2426
|
+
if (res.code !== 0) throw new Error(`redactor ${name}: exit ${res.code}: ${res.stderr.trim()}`);
|
|
2427
|
+
let parsed;
|
|
2428
|
+
try {
|
|
2429
|
+
parsed = JSON.parse(res.stdout);
|
|
2430
|
+
} catch {
|
|
2431
|
+
throw new Error(`redactor ${name}: stdout was not JSON`);
|
|
2432
|
+
}
|
|
2433
|
+
if (!Array.isArray(parsed) || parsed.length !== texts.length) {
|
|
2434
|
+
throw new Error(`redactor ${name}: expected a JSON array of ${texts.length} strings`);
|
|
2435
|
+
}
|
|
2436
|
+
return parsed.map(String);
|
|
2071
2437
|
}
|
|
2072
|
-
|
|
2073
|
-
await sleep(intervalMs, opts.signal);
|
|
2074
|
-
}
|
|
2438
|
+
};
|
|
2075
2439
|
}
|
|
2076
2440
|
|
|
2077
|
-
// src/attributes.ts
|
|
2078
|
-
var ATTR = {
|
|
2079
|
-
/** = the trace/session id. Wire contract (dedup linkage). */
|
|
2080
|
-
SESSION_ID: "tangle.sessionId",
|
|
2081
|
-
/** "cli" marks a CLI upload so the platform can drop a re-upload of a session
|
|
2082
|
-
* that also streamed live. Wire contract. */
|
|
2083
|
-
INGEST_SOURCE: "tangle.ingest_source",
|
|
2084
|
-
HARNESS: "tangle.harness",
|
|
2085
|
-
CWD: "tangle.cwd",
|
|
2086
|
-
GIT_BRANCH: "tangle.git_branch",
|
|
2087
|
-
HOST: "tangle.host",
|
|
2088
|
-
/** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
|
|
2089
|
-
* which collided with `tangle.ingest_source`.) */
|
|
2090
|
-
SESSION_FILE: "tangle.session_file",
|
|
2091
|
-
UPLOADED_AT: "tangle.uploaded_at",
|
|
2092
|
-
UPLOADER: "tangle.uploader",
|
|
2093
|
-
REDACTION_VERSION: "redaction.version",
|
|
2094
|
-
REDACTION_COUNT: "redaction.count"
|
|
2095
|
-
};
|
|
2096
|
-
var INGEST_SOURCE_CLI = "cli";
|
|
2097
|
-
var DEFAULT_HARNESS = "claude-code";
|
|
2098
|
-
|
|
2099
2441
|
// src/redact.ts
|
|
2100
2442
|
import { DEFAULT_REDACTION_RULES, redactValue } from "@tangle-network/agent-eval/traces";
|
|
2101
2443
|
var CODING_REDACTION_RULES = [
|
|
@@ -2156,20 +2498,564 @@ async function applyRedactor(spans, redactor) {
|
|
|
2156
2498
|
return { spans: out, changed };
|
|
2157
2499
|
}
|
|
2158
2500
|
|
|
2159
|
-
// src/
|
|
2501
|
+
// src/file-export.ts
|
|
2160
2502
|
import { createHash } from "crypto";
|
|
2161
|
-
import {
|
|
2503
|
+
import { mkdtemp as mkdtemp2, readFile as readFile12, writeFile as writeFile2 } from "fs/promises";
|
|
2504
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
2505
|
+
import { join as join13 } from "path";
|
|
2506
|
+
function isObject(value) {
|
|
2507
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
2508
|
+
}
|
|
2509
|
+
function stringValue(value) {
|
|
2510
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2511
|
+
}
|
|
2512
|
+
function objectValue(value) {
|
|
2513
|
+
return isObject(value) ? value : void 0;
|
|
2514
|
+
}
|
|
2515
|
+
function stableJson(value) {
|
|
2516
|
+
return JSON.stringify(value, (_key, v) => {
|
|
2517
|
+
if (!isObject(v)) return v;
|
|
2518
|
+
return Object.keys(v).sort().reduce((acc, key) => {
|
|
2519
|
+
acc[key] = v[key];
|
|
2520
|
+
return acc;
|
|
2521
|
+
}, {});
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
function hashId(value, chars = 16) {
|
|
2525
|
+
return createHash("sha256").update(stableJson(value)).digest("hex").slice(0, chars);
|
|
2526
|
+
}
|
|
2527
|
+
function isoTime(value) {
|
|
2528
|
+
if (typeof value === "string") {
|
|
2529
|
+
const ms = Date.parse(value);
|
|
2530
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString() : void 0;
|
|
2531
|
+
}
|
|
2532
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2533
|
+
const ms = value < 1e10 ? value * 1e3 : value;
|
|
2534
|
+
return new Date(ms).toISOString();
|
|
2535
|
+
}
|
|
2536
|
+
return void 0;
|
|
2537
|
+
}
|
|
2538
|
+
function firstTime(obj, keys) {
|
|
2539
|
+
for (const key of keys) {
|
|
2540
|
+
const value = isoTime(obj[key]);
|
|
2541
|
+
if (value) return value;
|
|
2542
|
+
}
|
|
2543
|
+
return void 0;
|
|
2544
|
+
}
|
|
2545
|
+
function firstTimeDeep(value, keys, depth = 5) {
|
|
2546
|
+
if (depth < 0) return void 0;
|
|
2547
|
+
if (isObject(value)) {
|
|
2548
|
+
const direct = firstTime(value, keys);
|
|
2549
|
+
if (direct) return direct;
|
|
2550
|
+
for (const child of Object.values(value)) {
|
|
2551
|
+
const found = firstTimeDeep(child, keys, depth - 1);
|
|
2552
|
+
if (found) return found;
|
|
2553
|
+
}
|
|
2554
|
+
} else if (Array.isArray(value)) {
|
|
2555
|
+
for (const child of value) {
|
|
2556
|
+
const found = firstTimeDeep(child, keys, depth - 1);
|
|
2557
|
+
if (found) return found;
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
return void 0;
|
|
2561
|
+
}
|
|
2562
|
+
function copyDefined(target, entries) {
|
|
2563
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
2564
|
+
if (value != null && value !== "") target[key] = value;
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
function parseJsonRows(text) {
|
|
2568
|
+
const trimmed = text.trim();
|
|
2569
|
+
if (!trimmed) throw new Error("input file is empty");
|
|
2570
|
+
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
2571
|
+
const parsed = JSON.parse(trimmed);
|
|
2572
|
+
if (Array.isArray(parsed)) return { rows: parsed };
|
|
2573
|
+
if (isObject(parsed)) {
|
|
2574
|
+
const events = parsed.events;
|
|
2575
|
+
if (Array.isArray(events)) return { rows: events, wrapper: parsed };
|
|
2576
|
+
return { rows: [parsed] };
|
|
2577
|
+
}
|
|
2578
|
+
throw new Error("input JSON must be an object, array, or JSONL rows");
|
|
2579
|
+
}
|
|
2580
|
+
return {
|
|
2581
|
+
rows: trimmed.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => JSON.parse(line))
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
function isPolicyEvidenceRow(row) {
|
|
2585
|
+
return isObject(row) && row.kind === "traces.policy_evidence.session";
|
|
2586
|
+
}
|
|
2587
|
+
function isOpenInferenceRow(row) {
|
|
2588
|
+
if (!isObject(row)) return false;
|
|
2589
|
+
return typeof row.trace_id === "string" && typeof row.span_id === "string" && typeof row.name === "string" && typeof row.start_time === "string" && typeof row.end_time === "string" && isObject(row.status) && isObject(row.attributes) && isObject(row.resource) && isObject(row.scope);
|
|
2590
|
+
}
|
|
2591
|
+
function eventType(row) {
|
|
2592
|
+
return stringValue(row.type) ?? stringValue(row.event) ?? stringValue(row.name) ?? stringValue(row.kind);
|
|
2593
|
+
}
|
|
2594
|
+
function looksLikeSandboxEvent(row) {
|
|
2595
|
+
if (!isObject(row)) return false;
|
|
2596
|
+
const type = eventType(row)?.toLowerCase();
|
|
2597
|
+
return type === "start" || type === "raw" || type === "result" || type === "done" || type === "error";
|
|
2598
|
+
}
|
|
2599
|
+
function detectFormat(rows, requested) {
|
|
2600
|
+
if (requested !== "auto") return requested;
|
|
2601
|
+
if (rows.length === 0) throw new Error("input contains no rows");
|
|
2602
|
+
if (rows.every(isPolicyEvidenceRow)) return "policy-evidence";
|
|
2603
|
+
if (rows.every(isOpenInferenceRow)) return "openinference";
|
|
2604
|
+
if (rows.some(looksLikeSandboxEvent)) return "sandbox-events";
|
|
2605
|
+
throw new Error("could not detect input format; use --format policy-evidence, sandbox-events, or openinference");
|
|
2606
|
+
}
|
|
2607
|
+
function requirePolicyEvidenceRows(rows) {
|
|
2608
|
+
if (!rows.every(isPolicyEvidenceRow)) throw new Error("policy-evidence input must contain only traces.policy_evidence.session rows");
|
|
2609
|
+
return rows;
|
|
2610
|
+
}
|
|
2611
|
+
function requireOpenInferenceRows(rows) {
|
|
2612
|
+
if (!rows.every(isOpenInferenceRow)) throw new Error("openinference input must contain only complete OpenInference span rows");
|
|
2613
|
+
return rows;
|
|
2614
|
+
}
|
|
2615
|
+
function requireObjectRows(rows) {
|
|
2616
|
+
if (!rows.every(isObject)) throw new Error("sandbox-events input must contain only JSON object events");
|
|
2617
|
+
return rows;
|
|
2618
|
+
}
|
|
2619
|
+
function evidenceTimeBounds(record) {
|
|
2620
|
+
const sessionTime = new Date(record.session.mtimeMs).toISOString();
|
|
2621
|
+
const start = record.metrics.firstSpanAt ?? record.generatedAt ?? sessionTime;
|
|
2622
|
+
const end = record.metrics.lastSpanAt ?? start;
|
|
2623
|
+
return { start, end };
|
|
2624
|
+
}
|
|
2625
|
+
function policyEvidenceToSpans(records) {
|
|
2626
|
+
return records.map((record) => {
|
|
2627
|
+
const { start, end } = evidenceTimeBounds(record);
|
|
2628
|
+
const traceId = record.session.sessionId || hashId(record, 32);
|
|
2629
|
+
const extra = {
|
|
2630
|
+
"traces.source_format": "policy-evidence",
|
|
2631
|
+
"traces.policy_evidence.schema_version": record.schemaVersion,
|
|
2632
|
+
"traces.policy_evidence.kind": record.kind,
|
|
2633
|
+
"traces.session.id": record.session.sessionId,
|
|
2634
|
+
"traces.session.path": record.session.path,
|
|
2635
|
+
"traces.session.cwd": record.session.cwd,
|
|
2636
|
+
"traces.session.mtime_ms": record.session.mtimeMs,
|
|
2637
|
+
"traces.metrics.span_count": record.metrics.spanCount,
|
|
2638
|
+
"traces.metrics.llm_turn_count": record.metrics.llmTurnCount,
|
|
2639
|
+
"traces.metrics.tool_call_count": record.metrics.toolCallCount,
|
|
2640
|
+
"traces.metrics.errored_tool_call_count": record.metrics.erroredToolCallCount,
|
|
2641
|
+
"traces.metrics.input_tokens": record.metrics.inputTokens,
|
|
2642
|
+
"traces.metrics.output_tokens": record.metrics.outputTokens,
|
|
2643
|
+
"traces.metrics.models": record.metrics.models,
|
|
2644
|
+
"traces.metrics.tools": stableJson(record.metrics.tools),
|
|
2645
|
+
"traces.signals.stuck_loop_count": record.signals.stuckLoopCount,
|
|
2646
|
+
"traces.signals.affected_run_ratio": record.signals.affectedRunRatio,
|
|
2647
|
+
"traces.signals.stuck_loops": stableJson(record.signals.stuckLoops),
|
|
2648
|
+
"traces.signals.stuck_loops_omitted": record.signals.stuckLoopsOmitted,
|
|
2649
|
+
"traces.signals.tool_error_rate": record.signals.toolErrorRate,
|
|
2650
|
+
"traces.provenance.source": record.provenance.source,
|
|
2651
|
+
"traces.provenance.evidence_kind": record.provenance.evidenceKind,
|
|
2652
|
+
"traces.provenance.otlp_path": record.provenance.otlpPath,
|
|
2653
|
+
[ATTR.SUBJECT_KEY]: record.repo.subjectKey,
|
|
2654
|
+
[ATTR.GIT_REPOSITORY]: record.repo.repository,
|
|
2655
|
+
[ATTR.GIT_BRANCH_NAME]: record.repo.branch,
|
|
2656
|
+
[ATTR.GIT_COMMIT]: record.repo.commit,
|
|
2657
|
+
[ATTR.CWD]: record.repo.cwd ?? record.session.cwd
|
|
2658
|
+
};
|
|
2659
|
+
return span({
|
|
2660
|
+
traceId,
|
|
2661
|
+
spanId: `policy:${hashId(record)}`,
|
|
2662
|
+
parentSpanId: null,
|
|
2663
|
+
name: "policy_evidence.session",
|
|
2664
|
+
kind: "AGENT",
|
|
2665
|
+
startTime: start,
|
|
2666
|
+
endTime: end,
|
|
2667
|
+
service: record.session.harness,
|
|
2668
|
+
agent: "traces",
|
|
2669
|
+
content: capText(stableJson({
|
|
2670
|
+
session: record.session,
|
|
2671
|
+
repo: record.repo,
|
|
2672
|
+
metrics: record.metrics,
|
|
2673
|
+
signals: record.signals,
|
|
2674
|
+
provenance: record.provenance
|
|
2675
|
+
})),
|
|
2676
|
+
extra
|
|
2677
|
+
});
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
2680
|
+
function findStringKey(value, keys, depth = 4) {
|
|
2681
|
+
if (depth < 0) return void 0;
|
|
2682
|
+
if (isObject(value)) {
|
|
2683
|
+
for (const key of keys) {
|
|
2684
|
+
const found = stringValue(value[key]);
|
|
2685
|
+
if (found) return found;
|
|
2686
|
+
}
|
|
2687
|
+
for (const child of Object.values(value)) {
|
|
2688
|
+
const found = findStringKey(child, keys, depth - 1);
|
|
2689
|
+
if (found) return found;
|
|
2690
|
+
}
|
|
2691
|
+
} else if (Array.isArray(value)) {
|
|
2692
|
+
for (const child of value) {
|
|
2693
|
+
const found = findStringKey(child, keys, depth - 1);
|
|
2694
|
+
if (found) return found;
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
return void 0;
|
|
2698
|
+
}
|
|
2699
|
+
function eventTimestamp(row) {
|
|
2700
|
+
return firstTimeDeep(row, [
|
|
2701
|
+
"timestamp",
|
|
2702
|
+
"start_time",
|
|
2703
|
+
"startTime",
|
|
2704
|
+
"startedAt",
|
|
2705
|
+
"createdAt",
|
|
2706
|
+
"created_at",
|
|
2707
|
+
"created",
|
|
2708
|
+
"time"
|
|
2709
|
+
]);
|
|
2710
|
+
}
|
|
2711
|
+
function eventEndTimestamp(row, fallback) {
|
|
2712
|
+
return firstTimeDeep(row, [
|
|
2713
|
+
"end_time",
|
|
2714
|
+
"endTime",
|
|
2715
|
+
"completedAt",
|
|
2716
|
+
"completed_at",
|
|
2717
|
+
"finishedAt",
|
|
2718
|
+
"updatedAt",
|
|
2719
|
+
"updated_at",
|
|
2720
|
+
"updated"
|
|
2721
|
+
]) ?? fallback;
|
|
2722
|
+
}
|
|
2723
|
+
function eventStatus(row, type) {
|
|
2724
|
+
const lowered = type.toLowerCase();
|
|
2725
|
+
const errorMessage = stringValue(row.error) ?? findStringKey(row, ["error", "errorMessage", "error_message"], 3);
|
|
2726
|
+
if (lowered.includes("error") || errorMessage) {
|
|
2727
|
+
const message = errorMessage ?? stringValue(row.message);
|
|
2728
|
+
return { status: "ERROR", ...message ? { message: capText(message) } : {} };
|
|
2729
|
+
}
|
|
2730
|
+
return { status: "OK" };
|
|
2731
|
+
}
|
|
2732
|
+
function eventKind(row, type) {
|
|
2733
|
+
const lowered = type.toLowerCase();
|
|
2734
|
+
const tool = findStringKey(row, ["tool", "toolName", "tool_name"], 4);
|
|
2735
|
+
if (tool || lowered.includes("tool") || stableJson(row).includes("tool-invocation")) return { kind: "TOOL", tool };
|
|
2736
|
+
if (lowered.includes("llm") || lowered === "token") return { kind: "LLM" };
|
|
2737
|
+
return { kind: "CHAIN" };
|
|
2738
|
+
}
|
|
2739
|
+
function sandboxEventsToSpans(rows, wrapper) {
|
|
2740
|
+
const context = wrapper ?? {};
|
|
2741
|
+
const traceId = findStringKey(context, ["trace_id", "traceId", "session_id", "sessionId", "run_id", "runId"], 3) ?? findStringKey(rows, ["trace_id", "traceId", "session_id", "sessionId", "run_id", "runId"], 4) ?? `sandbox:${hashId(rows, 32)}`;
|
|
2742
|
+
const service = findStringKey(context, ["service", "harness", "source"], 2) ?? "sandbox-opencode";
|
|
2743
|
+
const times = rows.flatMap((row) => {
|
|
2744
|
+
const time = eventTimestamp(row);
|
|
2745
|
+
return time ? [time] : [];
|
|
2746
|
+
}).sort();
|
|
2747
|
+
const rootId = `events:${hashId({ traceId, rows: rows.length })}`;
|
|
2748
|
+
const hasError = rows.some((row) => eventStatus(row, eventType(row) ?? "event").status === "ERROR");
|
|
2749
|
+
const rootStart = times[0] ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
2750
|
+
const rootEnd = times[times.length - 1] ?? rootStart;
|
|
2751
|
+
const spans = [
|
|
2752
|
+
span({
|
|
2753
|
+
traceId,
|
|
2754
|
+
spanId: rootId,
|
|
2755
|
+
parentSpanId: null,
|
|
2756
|
+
name: "sandbox.events",
|
|
2757
|
+
kind: "AGENT",
|
|
2758
|
+
startTime: rootStart,
|
|
2759
|
+
endTime: rootEnd,
|
|
2760
|
+
status: hasError ? "ERROR" : "OK",
|
|
2761
|
+
service,
|
|
2762
|
+
agent: service,
|
|
2763
|
+
extra: {
|
|
2764
|
+
"traces.source_format": "sandbox-events",
|
|
2765
|
+
"traces.event_count": rows.length
|
|
2766
|
+
}
|
|
2767
|
+
})
|
|
2768
|
+
];
|
|
2769
|
+
let previousTime = rootStart;
|
|
2770
|
+
rows.forEach((row, index) => {
|
|
2771
|
+
const type = eventType(row) ?? "event";
|
|
2772
|
+
const time = eventTimestamp(row) ?? previousTime;
|
|
2773
|
+
previousTime = time;
|
|
2774
|
+
const { status, message } = eventStatus(row, type);
|
|
2775
|
+
const { kind, tool } = eventKind(row, type);
|
|
2776
|
+
const name = kind === "TOOL" && tool ? `tool.${tool}` : `event.${type}`;
|
|
2777
|
+
const extra = {
|
|
2778
|
+
"traces.source_format": "sandbox-events",
|
|
2779
|
+
"traces.event.type": type,
|
|
2780
|
+
"traces.event.index": index
|
|
2781
|
+
};
|
|
2782
|
+
copyDefined(extra, {
|
|
2783
|
+
"traces.event.id": stringValue(row.id),
|
|
2784
|
+
"traces.event.raw_type": stringValue(row.type),
|
|
2785
|
+
"traces.event.raw_event": stringValue(row.event)
|
|
2786
|
+
});
|
|
2787
|
+
spans.push(span({
|
|
2788
|
+
traceId,
|
|
2789
|
+
spanId: `event:${index}:${hashId(row)}`,
|
|
2790
|
+
parentSpanId: rootId,
|
|
2791
|
+
name,
|
|
2792
|
+
kind,
|
|
2793
|
+
startTime: time,
|
|
2794
|
+
endTime: eventEndTimestamp(row, time),
|
|
2795
|
+
status,
|
|
2796
|
+
statusMessage: message,
|
|
2797
|
+
service,
|
|
2798
|
+
agent: service,
|
|
2799
|
+
tool: kind === "TOOL" ? tool ?? type : null,
|
|
2800
|
+
step: index,
|
|
2801
|
+
content: capText(stableJson(row)),
|
|
2802
|
+
extra
|
|
2803
|
+
}));
|
|
2804
|
+
});
|
|
2805
|
+
return spans;
|
|
2806
|
+
}
|
|
2807
|
+
function statusCode(value) {
|
|
2808
|
+
return value === "OK" || value === "ERROR" || value === "UNSET" ? value : "UNSET";
|
|
2809
|
+
}
|
|
2810
|
+
function openInferenceToSpans(rows) {
|
|
2811
|
+
return rows.map((row) => {
|
|
2812
|
+
const attrs = isObject(row.attributes) ? { ...row.attributes } : {};
|
|
2813
|
+
const resource = objectValue(row.resource);
|
|
2814
|
+
const resourceAttrs = objectValue(resource?.attributes);
|
|
2815
|
+
if (resourceAttrs) copyDefined(attrs, resourceAttrs);
|
|
2816
|
+
const kind = stringValue(row.kind);
|
|
2817
|
+
if (kind && attrs["openinference.span.kind"] == null) attrs["openinference.span.kind"] = kind;
|
|
2818
|
+
const status = objectValue(row.status);
|
|
2819
|
+
const message = stringValue(status?.message);
|
|
2820
|
+
return {
|
|
2821
|
+
trace_id: stringValue(row.trace_id) ?? hashId(row, 32),
|
|
2822
|
+
span_id: stringValue(row.span_id) ?? hashId({ row, field: "span" }),
|
|
2823
|
+
parent_span_id: stringValue(row.parent_span_id) || null,
|
|
2824
|
+
name: stringValue(row.name) ?? "span",
|
|
2825
|
+
start_time: stringValue(row.start_time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2826
|
+
end_time: stringValue(row.end_time) ?? stringValue(row.start_time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2827
|
+
status: { code: statusCode(status?.code), ...message ? { message } : {} },
|
|
2828
|
+
attributes: attrs
|
|
2829
|
+
};
|
|
2830
|
+
});
|
|
2831
|
+
}
|
|
2832
|
+
function normalizeAttributes(attributes) {
|
|
2833
|
+
const normalized = {};
|
|
2834
|
+
for (const [key, value] of Object.entries(attributes ?? {})) {
|
|
2835
|
+
if (!key || value == null) continue;
|
|
2836
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
2837
|
+
normalized[key] = value;
|
|
2838
|
+
} else if (Array.isArray(value) && value.every(
|
|
2839
|
+
(item) => typeof item === "string" || typeof item === "number" || typeof item === "boolean"
|
|
2840
|
+
)) {
|
|
2841
|
+
normalized[key] = value;
|
|
2842
|
+
} else {
|
|
2843
|
+
normalized[key] = stableJson(value);
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
return normalized;
|
|
2847
|
+
}
|
|
2848
|
+
function withExportAttributes(spans, attributes) {
|
|
2849
|
+
const normalized = normalizeAttributes(attributes);
|
|
2850
|
+
if (Object.keys(normalized).length === 0) return [...spans];
|
|
2851
|
+
return spans.map((s) => ({ ...s, attributes: { ...s.attributes, ...normalized } }));
|
|
2852
|
+
}
|
|
2853
|
+
function exportTraceEvidenceRows(rows, opts = {}, wrapper) {
|
|
2854
|
+
const format = detectFormat(rows, opts.format ?? "auto");
|
|
2855
|
+
const converted = format === "policy-evidence" ? policyEvidenceToSpans(requirePolicyEvidenceRows(rows)) : format === "openinference" ? openInferenceToSpans(requireOpenInferenceRows(rows)) : sandboxEventsToSpans(requireObjectRows(rows), wrapper);
|
|
2856
|
+
const spans = withExportAttributes(converted, opts.attributes);
|
|
2857
|
+
if (spans.length === 0) throw new Error(`no spans exported from ${format} input`);
|
|
2858
|
+
const redacted = redactSpans(spans);
|
|
2859
|
+
return {
|
|
2860
|
+
format,
|
|
2861
|
+
spans: redacted.spans,
|
|
2862
|
+
redactionCount: redacted.report.redactionCount,
|
|
2863
|
+
redactionsByRule: redacted.report.byRule
|
|
2864
|
+
};
|
|
2865
|
+
}
|
|
2866
|
+
function exportTraceEvidenceText(text, opts = {}) {
|
|
2867
|
+
const parsed = parseJsonRows(text);
|
|
2868
|
+
return exportTraceEvidenceRows(parsed.rows, opts, parsed.wrapper);
|
|
2869
|
+
}
|
|
2870
|
+
async function exportTraceEvidenceFile(inputPath, opts = {}) {
|
|
2871
|
+
const text = await readFile12(inputPath, "utf8");
|
|
2872
|
+
return exportTraceEvidenceText(text, { ...opts, sourcePath: inputPath });
|
|
2873
|
+
}
|
|
2874
|
+
async function writeTraceEvidenceExportFile(inputPath, outPath, opts = {}) {
|
|
2875
|
+
const result = await exportTraceEvidenceFile(inputPath, opts);
|
|
2876
|
+
const path = outPath ?? join13(await mkdtemp2(join13(tmpdir2(), "traces-export-")), "spans.openinference.jsonl");
|
|
2877
|
+
await writeFile2(path, serializeSpans(result.spans), "utf8");
|
|
2878
|
+
return { ...result, path };
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
// src/observer.ts
|
|
2882
|
+
var sleep = (ms, signal) => new Promise((resolve) => {
|
|
2883
|
+
const onAbort = () => {
|
|
2884
|
+
clearTimeout(t);
|
|
2885
|
+
resolve();
|
|
2886
|
+
};
|
|
2887
|
+
const t = setTimeout(() => {
|
|
2888
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2889
|
+
resolve();
|
|
2890
|
+
}, ms);
|
|
2891
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2892
|
+
});
|
|
2893
|
+
function loopKey(sessionId, toolName2, argHash) {
|
|
2894
|
+
return `${sessionId}\0${toolName2}\0${argHash}`;
|
|
2895
|
+
}
|
|
2896
|
+
async function watchSessions(opts) {
|
|
2897
|
+
const windowMs = opts.windowMs ?? 30 * 6e4;
|
|
2898
|
+
const intervalMs = Math.max(250, opts.intervalMs ?? 5e3);
|
|
2899
|
+
const minLoop = opts.minLoopOccurrences ?? 3;
|
|
2900
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2901
|
+
while (!opts.signal?.aborted) {
|
|
2902
|
+
let sessionCount = 0;
|
|
2903
|
+
for await (const { adapter, ref, spans } of scanSessions({
|
|
2904
|
+
adapters: opts.adapters,
|
|
2905
|
+
all: opts.all,
|
|
2906
|
+
harnesses: opts.harnesses,
|
|
2907
|
+
cwd: opts.cwd,
|
|
2908
|
+
sinceMs: Date.now() - windowMs,
|
|
2909
|
+
signal: opts.signal,
|
|
2910
|
+
onError: opts.onError
|
|
2911
|
+
})) {
|
|
2912
|
+
let report;
|
|
2913
|
+
try {
|
|
2914
|
+
report = await runPipelines(spans, { minLoopOccurrences: minLoop });
|
|
2915
|
+
} catch (err) {
|
|
2916
|
+
opts.onError?.(err, ref);
|
|
2917
|
+
continue;
|
|
2918
|
+
}
|
|
2919
|
+
sessionCount += 1;
|
|
2920
|
+
if (opts.onReport) {
|
|
2921
|
+
try {
|
|
2922
|
+
await opts.onReport(ref, report);
|
|
2923
|
+
} catch (err) {
|
|
2924
|
+
opts.onError?.(err, ref);
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
for (const f of report.stuckLoops.findings) {
|
|
2928
|
+
const key = loopKey(ref.sessionId, f.toolName, f.argHash);
|
|
2929
|
+
if (f.occurrences > (seen.get(key) ?? 0)) {
|
|
2930
|
+
seen.set(key, f.occurrences);
|
|
2931
|
+
if (opts.onLoop) {
|
|
2932
|
+
try {
|
|
2933
|
+
await opts.onLoop({
|
|
2934
|
+
harness: adapter.harness,
|
|
2935
|
+
sessionId: ref.sessionId,
|
|
2936
|
+
cwd: ref.cwd,
|
|
2937
|
+
toolName: f.toolName,
|
|
2938
|
+
argHash: f.argHash,
|
|
2939
|
+
occurrences: f.occurrences,
|
|
2940
|
+
windowMs: f.windowMs
|
|
2941
|
+
});
|
|
2942
|
+
} catch (err) {
|
|
2943
|
+
opts.onError?.(err, ref);
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
try {
|
|
2950
|
+
opts.onTick?.({ sessions: sessionCount });
|
|
2951
|
+
} catch (err) {
|
|
2952
|
+
opts.onError?.(err);
|
|
2953
|
+
}
|
|
2954
|
+
if (opts.signal?.aborted) break;
|
|
2955
|
+
await sleep(intervalMs, opts.signal);
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
// src/reactions.ts
|
|
2960
|
+
var REACTIONS = ["correction", "frustration", "jargon", "structure", "praise"];
|
|
2961
|
+
function emptySignals() {
|
|
2962
|
+
return { correction: 0, frustration: 0, jargon: 0, structure: 0, praise: 0 };
|
|
2963
|
+
}
|
|
2964
|
+
function spanStep(s) {
|
|
2965
|
+
const v = s.attributes.step;
|
|
2966
|
+
return typeof v === "number" ? v : Number.MAX_SAFE_INTEGER;
|
|
2967
|
+
}
|
|
2968
|
+
function ordered(spans) {
|
|
2969
|
+
return [...spans].sort((a, b) => {
|
|
2970
|
+
const ds = spanStep(a) - spanStep(b);
|
|
2971
|
+
if (ds !== 0) return ds;
|
|
2972
|
+
return a.start_time.localeCompare(b.start_time);
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
function isAssistant(s) {
|
|
2976
|
+
const kind = s.attributes["openinference.span.kind"];
|
|
2977
|
+
if (s.name === "user.prompt") return false;
|
|
2978
|
+
return kind === "LLM" || s.name.startsWith("message.assistant");
|
|
2979
|
+
}
|
|
2980
|
+
function isHumanPrompt(s) {
|
|
2981
|
+
return s.name === "user.prompt" && s.attributes[ACTOR_ATTR] === "human";
|
|
2982
|
+
}
|
|
2983
|
+
function contentOf(s) {
|
|
2984
|
+
const c = s.attributes.content;
|
|
2985
|
+
return typeof c === "string" ? c : "";
|
|
2986
|
+
}
|
|
2987
|
+
var MAX_PAIRS = 10;
|
|
2988
|
+
function pairStrength(reactions) {
|
|
2989
|
+
let n = 0;
|
|
2990
|
+
for (const r of reactions) n += CORRECTIVE_REACTIONS.includes(r) ? 2 : 1;
|
|
2991
|
+
return n;
|
|
2992
|
+
}
|
|
2993
|
+
function analyzeReactions(spans) {
|
|
2994
|
+
const byTrace = /* @__PURE__ */ new Map();
|
|
2995
|
+
for (const s of spans) {
|
|
2996
|
+
const arr = byTrace.get(s.trace_id) ?? [];
|
|
2997
|
+
arr.push(s);
|
|
2998
|
+
byTrace.set(s.trace_id, arr);
|
|
2999
|
+
}
|
|
3000
|
+
const sessions = [];
|
|
3001
|
+
const corpus = emptySignals();
|
|
3002
|
+
let corpusTurns = 0;
|
|
3003
|
+
const pairs = [];
|
|
3004
|
+
for (const [sessionId, traceSpans] of byTrace) {
|
|
3005
|
+
const seq = ordered(traceSpans);
|
|
3006
|
+
const sessionSignals = emptySignals();
|
|
3007
|
+
let humanReactionTurns = 0;
|
|
3008
|
+
let lastAssistant = null;
|
|
3009
|
+
for (const s of seq) {
|
|
3010
|
+
if (isAssistant(s)) {
|
|
3011
|
+
const text = contentOf(s);
|
|
3012
|
+
if (text) lastAssistant = text;
|
|
3013
|
+
continue;
|
|
3014
|
+
}
|
|
3015
|
+
if (!isHumanPrompt(s)) continue;
|
|
3016
|
+
if (lastAssistant === null) continue;
|
|
3017
|
+
humanReactionTurns += 1;
|
|
3018
|
+
const reactions = classifyReaction(contentOf(s));
|
|
3019
|
+
if (reactions.length > 0) {
|
|
3020
|
+
for (const r of reactions) {
|
|
3021
|
+
sessionSignals[r] += 1;
|
|
3022
|
+
corpus[r] += 1;
|
|
3023
|
+
}
|
|
3024
|
+
pairs.push({ assistant: lastAssistant, human: contentOf(s), reactions, sessionId });
|
|
3025
|
+
}
|
|
3026
|
+
lastAssistant = null;
|
|
3027
|
+
}
|
|
3028
|
+
corpusTurns += humanReactionTurns;
|
|
3029
|
+
sessions.push({ sessionId, humanReactionTurns, signals: sessionSignals });
|
|
3030
|
+
}
|
|
3031
|
+
const corrective = CORRECTIVE_REACTIONS.reduce((n, r) => n + corpus[r], 0);
|
|
3032
|
+
const positive = corpus.praise;
|
|
3033
|
+
const totalSignals = corrective + positive;
|
|
3034
|
+
const ratio = totalSignals === 0 ? null : positive === 0 ? Number.POSITIVE_INFINITY : corrective / positive;
|
|
3035
|
+
pairs.sort((a, b) => pairStrength(b.reactions) - pairStrength(a.reactions));
|
|
3036
|
+
return {
|
|
3037
|
+
sessions,
|
|
3038
|
+
signals: corpus,
|
|
3039
|
+
humanReactionTurns: corpusTurns,
|
|
3040
|
+
correctiveToPositiveRatio: ratio,
|
|
3041
|
+
triggerPairs: pairs.slice(0, MAX_PAIRS)
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
// src/upload-state.ts
|
|
3046
|
+
import { createHash as createHash2 } from "crypto";
|
|
3047
|
+
import { mkdir, readFile as readFile13, writeFile as writeFile3 } from "fs/promises";
|
|
2162
3048
|
import { homedir as homedir11 } from "os";
|
|
2163
|
-
import { dirname, join as
|
|
3049
|
+
import { dirname, join as join14 } from "path";
|
|
2164
3050
|
function stateFile() {
|
|
2165
|
-
const base = process.env.XDG_STATE_HOME ??
|
|
2166
|
-
return
|
|
3051
|
+
const base = process.env.XDG_STATE_HOME ?? join14(homedir11(), ".local", "state");
|
|
3052
|
+
return join14(base, "tangle-traces", "uploaded.json");
|
|
2167
3053
|
}
|
|
2168
3054
|
function uploadKey(harness, sessionId) {
|
|
2169
3055
|
return `${harness}:${sessionId}`;
|
|
2170
3056
|
}
|
|
2171
3057
|
function sessionHash(spans) {
|
|
2172
|
-
const h =
|
|
3058
|
+
const h = createHash2("sha256");
|
|
2173
3059
|
for (const s of spans) {
|
|
2174
3060
|
h.update(`${s.span_id}\0${s.name}\0${s.start_time}\0${s.status.code}
|
|
2175
3061
|
`);
|
|
@@ -2178,7 +3064,7 @@ function sessionHash(spans) {
|
|
|
2178
3064
|
}
|
|
2179
3065
|
async function loadState() {
|
|
2180
3066
|
try {
|
|
2181
|
-
return JSON.parse(await
|
|
3067
|
+
return JSON.parse(await readFile13(stateFile(), "utf8"));
|
|
2182
3068
|
} catch {
|
|
2183
3069
|
return {};
|
|
2184
3070
|
}
|
|
@@ -2186,7 +3072,7 @@ async function loadState() {
|
|
|
2186
3072
|
async function saveState(state) {
|
|
2187
3073
|
const f = stateFile();
|
|
2188
3074
|
await mkdir(dirname(f), { recursive: true });
|
|
2189
|
-
await
|
|
3075
|
+
await writeFile3(f, `${JSON.stringify(state, null, 2)}
|
|
2190
3076
|
`, "utf8");
|
|
2191
3077
|
}
|
|
2192
3078
|
function alreadyUploaded(state, harness, sessionId, hash) {
|
|
@@ -2194,10 +3080,10 @@ function alreadyUploaded(state, harness, sessionId, hash) {
|
|
|
2194
3080
|
}
|
|
2195
3081
|
|
|
2196
3082
|
// src/upload.ts
|
|
2197
|
-
import { readFile as
|
|
3083
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
2198
3084
|
import { createRequire } from "module";
|
|
2199
3085
|
import { hostname } from "os";
|
|
2200
|
-
import { basename as basename8, join as
|
|
3086
|
+
import { basename as basename8, join as join15 } from "path";
|
|
2201
3087
|
import { hostedClientFromEnv } from "@tangle-network/agent-eval/hosted";
|
|
2202
3088
|
import { REDACTION_VERSION } from "@tangle-network/agent-eval/traces";
|
|
2203
3089
|
var require2 = createRequire(import.meta.url);
|
|
@@ -2221,7 +3107,7 @@ async function planUpload(opts = {}) {
|
|
|
2221
3107
|
async function gitBranch(cwd) {
|
|
2222
3108
|
if (!cwd) return void 0;
|
|
2223
3109
|
try {
|
|
2224
|
-
const head = await
|
|
3110
|
+
const head = await readFile14(join15(cwd, ".git", "HEAD"), "utf8");
|
|
2225
3111
|
const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
|
|
2226
3112
|
return m?.[1]?.trim();
|
|
2227
3113
|
} catch {
|
|
@@ -2329,24 +3215,31 @@ async function executeUpload(plan, opts = {}) {
|
|
|
2329
3215
|
}
|
|
2330
3216
|
|
|
2331
3217
|
export {
|
|
3218
|
+
countSkillRunsJsonl,
|
|
3219
|
+
analyzeAdoption,
|
|
2332
3220
|
span,
|
|
3221
|
+
toOpenInferenceSpan,
|
|
2333
3222
|
serializeSpans,
|
|
2334
3223
|
writeOtlpFile,
|
|
2335
3224
|
analyzeSpans,
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
haloAnalyzer,
|
|
2340
|
-
runExternalAnalyzers,
|
|
2341
|
-
commandRedactor,
|
|
3225
|
+
ATTR,
|
|
3226
|
+
INGEST_SOURCE_CLI,
|
|
3227
|
+
DEFAULT_HARNESS,
|
|
2342
3228
|
parseIsoToEpochMs,
|
|
2343
3229
|
parseSince,
|
|
2344
3230
|
toRuntimeStore,
|
|
2345
3231
|
runPipelines,
|
|
2346
3232
|
CONTENT_CAP,
|
|
2347
3233
|
capText,
|
|
3234
|
+
ACTOR_ATTR,
|
|
2348
3235
|
userPromptSpan,
|
|
2349
3236
|
AmpAdapter,
|
|
3237
|
+
textIsSynthetic,
|
|
3238
|
+
looksLikeAgentPrompt,
|
|
3239
|
+
claudeActor,
|
|
3240
|
+
codexActor,
|
|
3241
|
+
classifyReaction,
|
|
3242
|
+
CORRECTIVE_REACTIONS,
|
|
2350
3243
|
ClaudeAdapter,
|
|
2351
3244
|
CodexAdapter,
|
|
2352
3245
|
CopilotAdapter,
|
|
@@ -2360,15 +3253,31 @@ export {
|
|
|
2360
3253
|
resolveAdapter,
|
|
2361
3254
|
knownHarnesses,
|
|
2362
3255
|
selectAdapters,
|
|
3256
|
+
normalizeRemote,
|
|
3257
|
+
resolveRepoAttrs,
|
|
3258
|
+
stampRepoAttrs,
|
|
3259
|
+
parseSession,
|
|
2363
3260
|
scanSessions,
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
3261
|
+
buildPolicyEvidenceRecord,
|
|
3262
|
+
collectPolicyEvidence,
|
|
3263
|
+
serializePolicyEvidence,
|
|
3264
|
+
writePolicyEvidenceFile,
|
|
3265
|
+
runCommand,
|
|
3266
|
+
commandAnalyzer,
|
|
3267
|
+
haloAnalyzer,
|
|
3268
|
+
runExternalAnalyzers,
|
|
3269
|
+
commandRedactor,
|
|
2368
3270
|
CODING_REDACTION_RULES,
|
|
2369
3271
|
TRACES_REDACTION_RULES,
|
|
2370
3272
|
redactSpans,
|
|
2371
3273
|
applyRedactor,
|
|
3274
|
+
exportTraceEvidenceRows,
|
|
3275
|
+
exportTraceEvidenceText,
|
|
3276
|
+
exportTraceEvidenceFile,
|
|
3277
|
+
writeTraceEvidenceExportFile,
|
|
3278
|
+
watchSessions,
|
|
3279
|
+
REACTIONS,
|
|
3280
|
+
analyzeReactions,
|
|
2372
3281
|
uploadKey,
|
|
2373
3282
|
sessionHash,
|
|
2374
3283
|
loadState,
|