@tangle-network/traces 0.8.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 +55 -1
- package/dist/{chunk-BUD6I3ZJ.js → chunk-D5OMTM4S.js} +1203 -314
- package/dist/cli.js +260 -12
- package/dist/index.d.ts +321 -27
- package/dist/index.js +49 -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 = {
|
|
@@ -30,6 +137,9 @@ function toOpenInferenceSpan(s) {
|
|
|
30
137
|
const resourceAttrs = {};
|
|
31
138
|
if (a["service.name"] != null) resourceAttrs["service.name"] = a["service.name"];
|
|
32
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
|
+
}
|
|
33
143
|
return {
|
|
34
144
|
trace_id: s.trace_id,
|
|
35
145
|
span_id: s.span_id,
|
|
@@ -50,11 +160,11 @@ function serializeSpans(spans) {
|
|
|
50
160
|
`;
|
|
51
161
|
}
|
|
52
162
|
async function writeOtlpFile(spans, outPath) {
|
|
53
|
-
const { mkdtemp, writeFile:
|
|
54
|
-
const { tmpdir } = await import("os");
|
|
55
|
-
const { join:
|
|
56
|
-
const path = outPath ??
|
|
57
|
-
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");
|
|
58
168
|
return path;
|
|
59
169
|
}
|
|
60
170
|
|
|
@@ -89,150 +199,37 @@ async function analyzeSpans(spans, opts = {}) {
|
|
|
89
199
|
return { otlpPath, result };
|
|
90
200
|
}
|
|
91
201
|
|
|
92
|
-
// src/
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
};
|
|
124
|
-
child.stdout?.on("data", (d) => {
|
|
125
|
-
out = cap(String(d), out);
|
|
126
|
-
});
|
|
127
|
-
child.stderr?.on("data", (d) => {
|
|
128
|
-
err = cap(String(d), err);
|
|
129
|
-
});
|
|
130
|
-
child.on("error", (e) => {
|
|
131
|
-
clearTimeout(timer);
|
|
132
|
-
if (!killed) reject(e);
|
|
133
|
-
});
|
|
134
|
-
child.on("close", (code) => {
|
|
135
|
-
clearTimeout(timer);
|
|
136
|
-
if (!killed) resolve({ stdout: out, stderr: err, code });
|
|
137
|
-
});
|
|
138
|
-
if (opts.input !== void 0) {
|
|
139
|
-
child.stdin?.on("error", () => {
|
|
140
|
-
});
|
|
141
|
-
child.stdin?.end(opts.input);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
function commandAnalyzer(spec) {
|
|
146
|
-
return {
|
|
147
|
-
name: spec.name,
|
|
148
|
-
async analyze(otlpPath, opts = {}) {
|
|
149
|
-
try {
|
|
150
|
-
const res = await runCommand(spec.command, spec.args(otlpPath, opts.prompt), {
|
|
151
|
-
signal: opts.signal,
|
|
152
|
-
timeoutMs: spec.timeoutMs
|
|
153
|
-
});
|
|
154
|
-
if (res.code !== 0) {
|
|
155
|
-
return { analyzer: spec.name, ok: false, output: res.stdout, error: res.stderr.trim() || `exit ${res.code}` };
|
|
156
|
-
}
|
|
157
|
-
return { analyzer: spec.name, ok: true, output: res.stdout.trim(), findings: spec.parse?.(res.stdout) };
|
|
158
|
-
} catch (e) {
|
|
159
|
-
return { analyzer: spec.name, ok: false, output: "", error: e instanceof Error ? e.message : String(e) };
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
function toCanonicalOpenInference(otlpJsonl, opts = {}) {
|
|
165
|
-
return otlpJsonl.split("\n").filter((l) => l.trim()).map((line) => {
|
|
166
|
-
const s = JSON.parse(line);
|
|
167
|
-
const a = s.attributes ?? {};
|
|
168
|
-
const resourceAttrs = { "service.name": a["service.name"] ?? opts.service ?? "traces" };
|
|
169
|
-
if (a["agent.name"]) resourceAttrs["agent.name"] = a["agent.name"];
|
|
170
|
-
return JSON.stringify({
|
|
171
|
-
trace_id: s.trace_id,
|
|
172
|
-
span_id: s.span_id,
|
|
173
|
-
parent_span_id: s.parent_span_id ?? "",
|
|
174
|
-
trace_state: "",
|
|
175
|
-
name: s.name,
|
|
176
|
-
kind: a["openinference.span.kind"] ?? "CHAIN",
|
|
177
|
-
start_time: s.start_time,
|
|
178
|
-
end_time: s.end_time,
|
|
179
|
-
status: { code: s.status?.code ?? "OK", message: s.status?.message ?? "" },
|
|
180
|
-
resource: { attributes: resourceAttrs },
|
|
181
|
-
scope: { name: opts.scopeName ?? "tangle-traces", version: "" },
|
|
182
|
-
attributes: a
|
|
183
|
-
});
|
|
184
|
-
}).join("\n");
|
|
185
|
-
}
|
|
186
|
-
function haloAnalyzer(opts = {}) {
|
|
187
|
-
const command = opts.command ?? "halo";
|
|
188
|
-
const defaultPrompt = opts.defaultPrompt ?? "diagnose";
|
|
189
|
-
return {
|
|
190
|
-
name: "halo",
|
|
191
|
-
async analyze(otlpPath, o = {}) {
|
|
192
|
-
try {
|
|
193
|
-
const canonical = toCanonicalOpenInference(await readFile(otlpPath, "utf8"));
|
|
194
|
-
const canonPath = `${otlpPath}.halo.jsonl`;
|
|
195
|
-
await writeFile(canonPath, canonical);
|
|
196
|
-
const args = [canonPath, "-p", o.prompt ?? defaultPrompt];
|
|
197
|
-
if (opts.model) args.push("-m", opts.model);
|
|
198
|
-
const res = await runCommand(command, args, { signal: o.signal, timeoutMs: opts.timeoutMs });
|
|
199
|
-
if (res.code !== 0) {
|
|
200
|
-
return { analyzer: "halo", ok: false, output: res.stdout, error: res.stderr.trim() || `exit ${res.code}` };
|
|
201
|
-
}
|
|
202
|
-
return { analyzer: "halo", ok: true, output: res.stdout.trim() };
|
|
203
|
-
} catch (e) {
|
|
204
|
-
return { analyzer: "halo", ok: false, output: "", error: e instanceof Error ? e.message : String(e) };
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
function runExternalAnalyzers(otlpPath, analyzers, opts = {}) {
|
|
210
|
-
return Promise.all(analyzers.map((a) => a.analyze(otlpPath, opts)));
|
|
211
|
-
}
|
|
212
|
-
function commandRedactor(spec) {
|
|
213
|
-
const name = spec.name ?? spec.command;
|
|
214
|
-
return {
|
|
215
|
-
name,
|
|
216
|
-
async redactText(texts) {
|
|
217
|
-
if (texts.length === 0) return [];
|
|
218
|
-
const res = await runCommand(spec.command, spec.args ?? [], {
|
|
219
|
-
input: JSON.stringify(texts),
|
|
220
|
-
timeoutMs: spec.timeoutMs
|
|
221
|
-
});
|
|
222
|
-
if (res.code !== 0) throw new Error(`redactor ${name}: exit ${res.code}: ${res.stderr.trim()}`);
|
|
223
|
-
let parsed;
|
|
224
|
-
try {
|
|
225
|
-
parsed = JSON.parse(res.stdout);
|
|
226
|
-
} catch {
|
|
227
|
-
throw new Error(`redactor ${name}: stdout was not JSON`);
|
|
228
|
-
}
|
|
229
|
-
if (!Array.isArray(parsed) || parsed.length !== texts.length) {
|
|
230
|
-
throw new Error(`redactor ${name}: expected a JSON array of ${texts.length} strings`);
|
|
231
|
-
}
|
|
232
|
-
return parsed.map(String);
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
}
|
|
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";
|
|
236
233
|
|
|
237
234
|
// src/time.ts
|
|
238
235
|
function parseIsoToEpochMs(ts) {
|
|
@@ -310,6 +307,7 @@ function capText(raw) {
|
|
|
310
307
|
if (t.length <= CONTENT_CAP) return t;
|
|
311
308
|
return `${t.slice(0, CONTENT_CAP)}\u2026 [+${t.length - CONTENT_CAP} chars]`;
|
|
312
309
|
}
|
|
310
|
+
var ACTOR_ATTR = "tangle.actor";
|
|
313
311
|
function userPromptSpan(o) {
|
|
314
312
|
return span({
|
|
315
313
|
traceId: o.traceId,
|
|
@@ -321,14 +319,15 @@ function userPromptSpan(o) {
|
|
|
321
319
|
service: o.service ?? null,
|
|
322
320
|
agent: o.agent ?? null,
|
|
323
321
|
step: o.step,
|
|
324
|
-
content: o.content
|
|
322
|
+
content: o.content,
|
|
323
|
+
extra: { [ACTOR_ATTR]: o.actor ?? "human" }
|
|
325
324
|
});
|
|
326
325
|
}
|
|
327
326
|
|
|
328
327
|
// src/adapters/amp.ts
|
|
329
328
|
import { readdir, readFile as readFile2, stat } from "fs/promises";
|
|
330
329
|
import { homedir } from "os";
|
|
331
|
-
import { basename, join } from "path";
|
|
330
|
+
import { basename, join as join2 } from "path";
|
|
332
331
|
var SERVICE = "amp";
|
|
333
332
|
function textOf(content) {
|
|
334
333
|
if (typeof content === "string") return content.slice(0, CONTENT_CAP);
|
|
@@ -339,8 +338,8 @@ function textOf(content) {
|
|
|
339
338
|
var AmpAdapter = class {
|
|
340
339
|
harness = "amp";
|
|
341
340
|
threadsRoot() {
|
|
342
|
-
const base = process.env.XDG_DATA_HOME ??
|
|
343
|
-
return
|
|
341
|
+
const base = process.env.XDG_DATA_HOME ?? join2(homedir(), ".local", "share");
|
|
342
|
+
return join2(base, "amp", "threads");
|
|
344
343
|
}
|
|
345
344
|
async locate(opts = {}) {
|
|
346
345
|
let files;
|
|
@@ -352,7 +351,7 @@ var AmpAdapter = class {
|
|
|
352
351
|
const refs = [];
|
|
353
352
|
for (const f of files) {
|
|
354
353
|
if (!f.startsWith("T-") || !f.endsWith(".json")) continue;
|
|
355
|
-
const path =
|
|
354
|
+
const path = join2(this.threadsRoot(), f);
|
|
356
355
|
let st;
|
|
357
356
|
try {
|
|
358
357
|
st = await stat(path);
|
|
@@ -451,10 +450,98 @@ var AmpAdapter = class {
|
|
|
451
450
|
}
|
|
452
451
|
};
|
|
453
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
|
+
|
|
454
541
|
// src/adapters/claude.ts
|
|
455
542
|
import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
456
543
|
import { homedir as homedir2 } from "os";
|
|
457
|
-
import { basename as basename2, join as
|
|
544
|
+
import { basename as basename2, join as join3 } from "path";
|
|
458
545
|
var SERVICE2 = "claude-code";
|
|
459
546
|
function parseLines(raw) {
|
|
460
547
|
const out = [];
|
|
@@ -492,6 +579,7 @@ function parseClaudeStream(events, ctx) {
|
|
|
492
579
|
const spans = [];
|
|
493
580
|
const toolSpanByUseId = /* @__PURE__ */ new Map();
|
|
494
581
|
let step = ctx.startStep;
|
|
582
|
+
let sawUserTurn = false;
|
|
495
583
|
for (const ev of events) {
|
|
496
584
|
const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
497
585
|
const uid = ev.uuid ?? `${ctx.idPrefix}step${step}`;
|
|
@@ -539,6 +627,13 @@ function parseClaudeStream(events, ctx) {
|
|
|
539
627
|
} else if (ev.type === "user" && ev.message) {
|
|
540
628
|
const prompt = textOf2(ev.message.content);
|
|
541
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;
|
|
542
637
|
spans.push(
|
|
543
638
|
userPromptSpan({
|
|
544
639
|
traceId: ctx.traceId,
|
|
@@ -548,7 +643,8 @@ function parseClaudeStream(events, ctx) {
|
|
|
548
643
|
service: SERVICE2,
|
|
549
644
|
agent: ctx.agent,
|
|
550
645
|
step,
|
|
551
|
-
content: prompt
|
|
646
|
+
content: prompt,
|
|
647
|
+
actor
|
|
552
648
|
})
|
|
553
649
|
);
|
|
554
650
|
step += 1;
|
|
@@ -575,7 +671,7 @@ var ClaudeAdapter = class {
|
|
|
575
671
|
harness = "claude-code";
|
|
576
672
|
aliases = ["claude", "claudish", "openclaw", "nanoclaw"];
|
|
577
673
|
root() {
|
|
578
|
-
return
|
|
674
|
+
return join3(homedir2(), ".claude", "projects");
|
|
579
675
|
}
|
|
580
676
|
async locate(opts = {}) {
|
|
581
677
|
const root = this.root();
|
|
@@ -587,7 +683,7 @@ var ClaudeAdapter = class {
|
|
|
587
683
|
}
|
|
588
684
|
const refs = [];
|
|
589
685
|
for (const dir of projectDirs) {
|
|
590
|
-
const dirPath =
|
|
686
|
+
const dirPath = join3(root, dir);
|
|
591
687
|
let files;
|
|
592
688
|
try {
|
|
593
689
|
files = await readdir2(dirPath);
|
|
@@ -596,7 +692,7 @@ var ClaudeAdapter = class {
|
|
|
596
692
|
}
|
|
597
693
|
for (const f of files) {
|
|
598
694
|
if (!f.endsWith(".jsonl")) continue;
|
|
599
|
-
const path =
|
|
695
|
+
const path = join3(dirPath, f);
|
|
600
696
|
let st;
|
|
601
697
|
try {
|
|
602
698
|
st = await stat2(path);
|
|
@@ -643,7 +739,7 @@ var ClaudeAdapter = class {
|
|
|
643
739
|
}
|
|
644
740
|
/** Parse `<session>/subagents/agent-*.jsonl`, parenting each under its Agent call. */
|
|
645
741
|
async foldSubagents(ref, traceId, main, out) {
|
|
646
|
-
const subDir =
|
|
742
|
+
const subDir = join3(ref.path.replace(/\.jsonl$/, ""), "subagents");
|
|
647
743
|
let files;
|
|
648
744
|
try {
|
|
649
745
|
files = await readdir2(subDir);
|
|
@@ -656,13 +752,13 @@ var ClaudeAdapter = class {
|
|
|
656
752
|
const hash = basename2(f, ".jsonl");
|
|
657
753
|
let meta = {};
|
|
658
754
|
try {
|
|
659
|
-
meta = JSON.parse(await readFile3(
|
|
755
|
+
meta = JSON.parse(await readFile3(join3(subDir, `${hash}.meta.json`), "utf8"));
|
|
660
756
|
} catch {
|
|
661
757
|
}
|
|
662
758
|
const parent = meta.toolUseId && main.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
|
|
663
759
|
let raw;
|
|
664
760
|
try {
|
|
665
|
-
raw = await readFile3(
|
|
761
|
+
raw = await readFile3(join3(subDir, f), "utf8");
|
|
666
762
|
} catch {
|
|
667
763
|
continue;
|
|
668
764
|
}
|
|
@@ -682,7 +778,7 @@ var ClaudeAdapter = class {
|
|
|
682
778
|
// src/adapters/codex.ts
|
|
683
779
|
import { readdir as readdir3, readFile as readFile4, stat as stat3 } from "fs/promises";
|
|
684
780
|
import { homedir as homedir3 } from "os";
|
|
685
|
-
import { basename as basename3, join as
|
|
781
|
+
import { basename as basename3, join as join4 } from "path";
|
|
686
782
|
var SERVICE3 = "codex";
|
|
687
783
|
function parseLines2(raw) {
|
|
688
784
|
const out = [];
|
|
@@ -718,7 +814,7 @@ async function* walkRollouts(root) {
|
|
|
718
814
|
return;
|
|
719
815
|
}
|
|
720
816
|
for (const y of years) {
|
|
721
|
-
const yp =
|
|
817
|
+
const yp = join4(root, y);
|
|
722
818
|
let months;
|
|
723
819
|
try {
|
|
724
820
|
months = await readdir3(yp);
|
|
@@ -726,7 +822,7 @@ async function* walkRollouts(root) {
|
|
|
726
822
|
continue;
|
|
727
823
|
}
|
|
728
824
|
for (const m of months) {
|
|
729
|
-
const mp =
|
|
825
|
+
const mp = join4(yp, m);
|
|
730
826
|
let days;
|
|
731
827
|
try {
|
|
732
828
|
days = await readdir3(mp);
|
|
@@ -734,7 +830,7 @@ async function* walkRollouts(root) {
|
|
|
734
830
|
continue;
|
|
735
831
|
}
|
|
736
832
|
for (const d of days) {
|
|
737
|
-
const dp =
|
|
833
|
+
const dp = join4(mp, d);
|
|
738
834
|
let files;
|
|
739
835
|
try {
|
|
740
836
|
files = await readdir3(dp);
|
|
@@ -742,7 +838,7 @@ async function* walkRollouts(root) {
|
|
|
742
838
|
continue;
|
|
743
839
|
}
|
|
744
840
|
for (const f of files) {
|
|
745
|
-
if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield
|
|
841
|
+
if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join4(dp, f);
|
|
746
842
|
}
|
|
747
843
|
}
|
|
748
844
|
}
|
|
@@ -752,7 +848,7 @@ var CodexAdapter = class {
|
|
|
752
848
|
harness = "codex";
|
|
753
849
|
aliases = ["codex-acp"];
|
|
754
850
|
root() {
|
|
755
|
-
return process.env.CODEX_HOME ?
|
|
851
|
+
return process.env.CODEX_HOME ? join4(process.env.CODEX_HOME, "sessions") : join4(homedir3(), ".codex", "sessions");
|
|
756
852
|
}
|
|
757
853
|
async locate(opts = {}) {
|
|
758
854
|
const refs = [];
|
|
@@ -801,6 +897,7 @@ var CodexAdapter = class {
|
|
|
801
897
|
const toolByCallId = /* @__PURE__ */ new Map();
|
|
802
898
|
let step = 0;
|
|
803
899
|
let lastLlm = rootId;
|
|
900
|
+
let sawUserTurn = false;
|
|
804
901
|
for (const l of lines) {
|
|
805
902
|
const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
806
903
|
if (l.type === "event_msg" && l.payload?.type === "token_count") {
|
|
@@ -855,6 +952,8 @@ var CodexAdapter = class {
|
|
|
855
952
|
} else if (l.type === "response_item" && l.payload?.type === "message" && l.payload.role === "user") {
|
|
856
953
|
const prompt = textOf3(l.payload.content);
|
|
857
954
|
if (prompt) {
|
|
955
|
+
const actor = codexActor({ text: prompt, isFirstUserTurn: !sawUserTurn });
|
|
956
|
+
sawUserTurn = true;
|
|
858
957
|
spans.push(
|
|
859
958
|
userPromptSpan({
|
|
860
959
|
traceId,
|
|
@@ -864,7 +963,8 @@ var CodexAdapter = class {
|
|
|
864
963
|
content: prompt,
|
|
865
964
|
service: SERVICE3,
|
|
866
965
|
agent: SERVICE3,
|
|
867
|
-
step
|
|
966
|
+
step,
|
|
967
|
+
actor
|
|
868
968
|
})
|
|
869
969
|
);
|
|
870
970
|
step += 1;
|
|
@@ -897,7 +997,7 @@ var CodexAdapter = class {
|
|
|
897
997
|
// src/adapters/copilot.ts
|
|
898
998
|
import { readdir as readdir4, readFile as readFile5, stat as stat4 } from "fs/promises";
|
|
899
999
|
import { homedir as homedir4 } from "os";
|
|
900
|
-
import { join as
|
|
1000
|
+
import { join as join5 } from "path";
|
|
901
1001
|
var SERVICE4 = "github-copilot";
|
|
902
1002
|
function parseLines3(raw) {
|
|
903
1003
|
const out = [];
|
|
@@ -914,7 +1014,7 @@ var CopilotAdapter = class {
|
|
|
914
1014
|
harness = "github-copilot";
|
|
915
1015
|
aliases = ["copilot"];
|
|
916
1016
|
root() {
|
|
917
|
-
return
|
|
1017
|
+
return join5(process.env.COPILOT_HOME ?? join5(homedir4(), ".copilot"), "session-state");
|
|
918
1018
|
}
|
|
919
1019
|
async locate(opts = {}) {
|
|
920
1020
|
let dirs;
|
|
@@ -925,7 +1025,7 @@ var CopilotAdapter = class {
|
|
|
925
1025
|
}
|
|
926
1026
|
const refs = [];
|
|
927
1027
|
for (const id of dirs) {
|
|
928
|
-
const path =
|
|
1028
|
+
const path = join5(this.root(), id, "events.jsonl");
|
|
929
1029
|
let st;
|
|
930
1030
|
try {
|
|
931
1031
|
st = await stat4(path);
|
|
@@ -1023,7 +1123,7 @@ var CopilotAdapter = class {
|
|
|
1023
1123
|
// src/adapters/factory.ts
|
|
1024
1124
|
import { readdir as readdir5, readFile as readFile6, stat as stat5 } from "fs/promises";
|
|
1025
1125
|
import { homedir as homedir5 } from "os";
|
|
1026
|
-
import { basename as basename4, join as
|
|
1126
|
+
import { basename as basename4, join as join6 } from "path";
|
|
1027
1127
|
var SERVICE5 = "factory";
|
|
1028
1128
|
function textOf4(content) {
|
|
1029
1129
|
return capText(
|
|
@@ -1045,7 +1145,7 @@ var FactoryAdapter = class {
|
|
|
1045
1145
|
harness = "factory";
|
|
1046
1146
|
aliases = ["factory-droids", "droid"];
|
|
1047
1147
|
root() {
|
|
1048
|
-
return
|
|
1148
|
+
return join6(homedir5(), ".factory", "sessions");
|
|
1049
1149
|
}
|
|
1050
1150
|
async locate(opts = {}) {
|
|
1051
1151
|
const root = this.root();
|
|
@@ -1057,7 +1157,7 @@ var FactoryAdapter = class {
|
|
|
1057
1157
|
}
|
|
1058
1158
|
const refs = [];
|
|
1059
1159
|
for (const dir of dirs) {
|
|
1060
|
-
const dp =
|
|
1160
|
+
const dp = join6(root, dir);
|
|
1061
1161
|
let files;
|
|
1062
1162
|
try {
|
|
1063
1163
|
files = await readdir5(dp);
|
|
@@ -1068,7 +1168,7 @@ var FactoryAdapter = class {
|
|
|
1068
1168
|
if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
|
|
1069
1169
|
for (const f of files) {
|
|
1070
1170
|
if (!f.endsWith(".jsonl")) continue;
|
|
1071
|
-
const path =
|
|
1171
|
+
const path = join6(dp, f);
|
|
1072
1172
|
let st;
|
|
1073
1173
|
try {
|
|
1074
1174
|
st = await stat5(path);
|
|
@@ -1184,7 +1284,7 @@ var FactoryAdapter = class {
|
|
|
1184
1284
|
// src/adapters/forge.ts
|
|
1185
1285
|
import { readdir as readdir6, readFile as readFile7, stat as stat6 } from "fs/promises";
|
|
1186
1286
|
import { homedir as homedir6 } from "os";
|
|
1187
|
-
import { basename as basename5, join as
|
|
1287
|
+
import { basename as basename5, join as join7 } from "path";
|
|
1188
1288
|
var SERVICE6 = "forge";
|
|
1189
1289
|
function tokens(t) {
|
|
1190
1290
|
if (!t) return null;
|
|
@@ -1198,7 +1298,7 @@ var ForgeAdapter = class {
|
|
|
1198
1298
|
aliases = ["forgecode"];
|
|
1199
1299
|
/** Directories scanned for `*-dump.json` exports. */
|
|
1200
1300
|
dumpDirs() {
|
|
1201
|
-
return [process.cwd(),
|
|
1301
|
+
return [process.cwd(), join7(homedir6(), ".forge")];
|
|
1202
1302
|
}
|
|
1203
1303
|
async locate(opts = {}) {
|
|
1204
1304
|
const refs = [];
|
|
@@ -1212,7 +1312,7 @@ var ForgeAdapter = class {
|
|
|
1212
1312
|
}
|
|
1213
1313
|
for (const f of files) {
|
|
1214
1314
|
if (!f.endsWith("-dump.json")) continue;
|
|
1215
|
-
const path =
|
|
1315
|
+
const path = join7(dir, f);
|
|
1216
1316
|
let st;
|
|
1217
1317
|
try {
|
|
1218
1318
|
st = await stat6(path);
|
|
@@ -1314,7 +1414,7 @@ var ForgeAdapter = class {
|
|
|
1314
1414
|
// src/adapters/gemini.ts
|
|
1315
1415
|
import { readdir as readdir7, readFile as readFile8, stat as stat7 } from "fs/promises";
|
|
1316
1416
|
import { homedir as homedir7 } from "os";
|
|
1317
|
-
import { join as
|
|
1417
|
+
import { join as join8 } from "path";
|
|
1318
1418
|
function iso(ts, fallback) {
|
|
1319
1419
|
if (typeof ts === "number") return new Date(ts).toISOString();
|
|
1320
1420
|
if (typeof ts === "string" && ts.length > 0) return ts;
|
|
@@ -1339,7 +1439,7 @@ var GeminiFamilyAdapter = class {
|
|
|
1339
1439
|
this.homeDirName = cfg.homeDirName;
|
|
1340
1440
|
}
|
|
1341
1441
|
chatsRoot() {
|
|
1342
|
-
return
|
|
1442
|
+
return join8(homedir7(), this.homeDirName, "tmp");
|
|
1343
1443
|
}
|
|
1344
1444
|
async locate(opts = {}) {
|
|
1345
1445
|
const root = this.chatsRoot();
|
|
@@ -1351,7 +1451,7 @@ var GeminiFamilyAdapter = class {
|
|
|
1351
1451
|
}
|
|
1352
1452
|
const refs = [];
|
|
1353
1453
|
for (const pd of projectDirs) {
|
|
1354
|
-
const chatsDir =
|
|
1454
|
+
const chatsDir = join8(root, pd, "chats");
|
|
1355
1455
|
let files;
|
|
1356
1456
|
try {
|
|
1357
1457
|
files = await readdir7(chatsDir);
|
|
@@ -1360,7 +1460,7 @@ var GeminiFamilyAdapter = class {
|
|
|
1360
1460
|
}
|
|
1361
1461
|
for (const f of files) {
|
|
1362
1462
|
if (!f.startsWith("session-") || !f.endsWith(".json")) continue;
|
|
1363
|
-
const path =
|
|
1463
|
+
const path = join8(chatsDir, f);
|
|
1364
1464
|
let st;
|
|
1365
1465
|
try {
|
|
1366
1466
|
st = await stat7(path);
|
|
@@ -1473,7 +1573,7 @@ var GeminiAdapter = class extends GeminiFamilyAdapter {
|
|
|
1473
1573
|
// src/adapters/opencode.ts
|
|
1474
1574
|
import { readdir as readdir8, readFile as readFile9, stat as stat8 } from "fs/promises";
|
|
1475
1575
|
import { homedir as homedir8 } from "os";
|
|
1476
|
-
import { join as
|
|
1576
|
+
import { join as join9 } from "path";
|
|
1477
1577
|
var SERVICE7 = "opencode";
|
|
1478
1578
|
function isoOf(ms) {
|
|
1479
1579
|
return new Date(ms ?? 0).toISOString();
|
|
@@ -1486,11 +1586,11 @@ function textOf7(parts) {
|
|
|
1486
1586
|
var OpencodeAdapter = class {
|
|
1487
1587
|
harness = "opencode";
|
|
1488
1588
|
storage() {
|
|
1489
|
-
const base = process.env.XDG_DATA_HOME ??
|
|
1490
|
-
return
|
|
1589
|
+
const base = process.env.XDG_DATA_HOME ?? join9(homedir8(), ".local", "share");
|
|
1590
|
+
return join9(base, "opencode", "storage");
|
|
1491
1591
|
}
|
|
1492
1592
|
async locate(opts = {}) {
|
|
1493
|
-
const msgRoot =
|
|
1593
|
+
const msgRoot = join9(this.storage(), "message");
|
|
1494
1594
|
let sessionDirs;
|
|
1495
1595
|
try {
|
|
1496
1596
|
sessionDirs = await readdir8(msgRoot);
|
|
@@ -1499,7 +1599,7 @@ var OpencodeAdapter = class {
|
|
|
1499
1599
|
}
|
|
1500
1600
|
const refs = [];
|
|
1501
1601
|
for (const sid of sessionDirs) {
|
|
1502
|
-
const dir =
|
|
1602
|
+
const dir = join9(msgRoot, sid);
|
|
1503
1603
|
let st;
|
|
1504
1604
|
try {
|
|
1505
1605
|
st = await stat8(dir);
|
|
@@ -1516,7 +1616,7 @@ var OpencodeAdapter = class {
|
|
|
1516
1616
|
async parse(ref) {
|
|
1517
1617
|
const traceId = ref.sessionId;
|
|
1518
1618
|
const rootId = `root:${traceId}`;
|
|
1519
|
-
const partRoot =
|
|
1619
|
+
const partRoot = join9(this.storage(), "part");
|
|
1520
1620
|
let msgFiles;
|
|
1521
1621
|
try {
|
|
1522
1622
|
msgFiles = (await readdir8(ref.path)).filter((f) => f.endsWith(".json"));
|
|
@@ -1526,7 +1626,7 @@ var OpencodeAdapter = class {
|
|
|
1526
1626
|
const messages = [];
|
|
1527
1627
|
for (const f of msgFiles) {
|
|
1528
1628
|
try {
|
|
1529
|
-
messages.push({ msg: JSON.parse(await readFile9(
|
|
1629
|
+
messages.push({ msg: JSON.parse(await readFile9(join9(ref.path, f), "utf8")), file: f });
|
|
1530
1630
|
} catch {
|
|
1531
1631
|
}
|
|
1532
1632
|
}
|
|
@@ -1548,13 +1648,13 @@ var OpencodeAdapter = class {
|
|
|
1548
1648
|
for (const { msg } of messages) {
|
|
1549
1649
|
const mid = msg.id ?? `m${step}`;
|
|
1550
1650
|
const llmId = `llm:${mid}`;
|
|
1551
|
-
const pdir =
|
|
1651
|
+
const pdir = join9(partRoot, mid);
|
|
1552
1652
|
const parts = [];
|
|
1553
1653
|
try {
|
|
1554
1654
|
const partFiles = (await readdir8(pdir)).filter((f) => f.endsWith(".json"));
|
|
1555
1655
|
for (const pf of partFiles) {
|
|
1556
1656
|
try {
|
|
1557
|
-
parts.push(JSON.parse(await readFile9(
|
|
1657
|
+
parts.push(JSON.parse(await readFile9(join9(pdir, pf), "utf8")));
|
|
1558
1658
|
} catch {
|
|
1559
1659
|
}
|
|
1560
1660
|
}
|
|
@@ -1626,7 +1726,7 @@ var OpencodeAdapter = class {
|
|
|
1626
1726
|
// src/adapters/pi.ts
|
|
1627
1727
|
import { readdir as readdir9, readFile as readFile10, stat as stat9 } from "fs/promises";
|
|
1628
1728
|
import { homedir as homedir9 } from "os";
|
|
1629
|
-
import { basename as basename6, join as
|
|
1729
|
+
import { basename as basename6, join as join10 } from "path";
|
|
1630
1730
|
var SERVICE8 = "pi";
|
|
1631
1731
|
function parseLines5(raw) {
|
|
1632
1732
|
const out = [];
|
|
@@ -1654,7 +1754,7 @@ function textOf8(content) {
|
|
|
1654
1754
|
var PiAdapter = class {
|
|
1655
1755
|
harness = "pi";
|
|
1656
1756
|
root() {
|
|
1657
|
-
return
|
|
1757
|
+
return join10(homedir9(), ".pi", "agent", "sessions");
|
|
1658
1758
|
}
|
|
1659
1759
|
async locate(opts = {}) {
|
|
1660
1760
|
const root = this.root();
|
|
@@ -1666,7 +1766,7 @@ var PiAdapter = class {
|
|
|
1666
1766
|
}
|
|
1667
1767
|
const refs = [];
|
|
1668
1768
|
for (const dir of dirs) {
|
|
1669
|
-
const dp =
|
|
1769
|
+
const dp = join10(root, dir);
|
|
1670
1770
|
let files;
|
|
1671
1771
|
try {
|
|
1672
1772
|
files = await readdir9(dp);
|
|
@@ -1677,7 +1777,7 @@ var PiAdapter = class {
|
|
|
1677
1777
|
if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
|
|
1678
1778
|
for (const f of files) {
|
|
1679
1779
|
if (!f.endsWith(".jsonl")) continue;
|
|
1680
|
-
const path =
|
|
1780
|
+
const path = join10(dp, f);
|
|
1681
1781
|
let st;
|
|
1682
1782
|
try {
|
|
1683
1783
|
st = await stat9(path);
|
|
@@ -1795,7 +1895,7 @@ var PiAdapter = class {
|
|
|
1795
1895
|
// src/adapters/qwen.ts
|
|
1796
1896
|
import { readdir as readdir10, readFile as readFile11, stat as stat10 } from "fs/promises";
|
|
1797
1897
|
import { homedir as homedir10 } from "os";
|
|
1798
|
-
import { basename as basename7, join as
|
|
1898
|
+
import { basename as basename7, join as join11 } from "path";
|
|
1799
1899
|
var SERVICE9 = "qwen";
|
|
1800
1900
|
function parseLines6(raw) {
|
|
1801
1901
|
const out = [];
|
|
@@ -1816,7 +1916,7 @@ var QwenAdapter = class {
|
|
|
1816
1916
|
harness = "qwen";
|
|
1817
1917
|
aliases = ["qwen-code"];
|
|
1818
1918
|
root() {
|
|
1819
|
-
return
|
|
1919
|
+
return join11(homedir10(), ".qwen", "projects");
|
|
1820
1920
|
}
|
|
1821
1921
|
async locate(opts = {}) {
|
|
1822
1922
|
const root = this.root();
|
|
@@ -1828,7 +1928,7 @@ var QwenAdapter = class {
|
|
|
1828
1928
|
}
|
|
1829
1929
|
const refs = [];
|
|
1830
1930
|
for (const pd of projectDirs) {
|
|
1831
|
-
const chatsDir =
|
|
1931
|
+
const chatsDir = join11(root, pd, "chats");
|
|
1832
1932
|
let files;
|
|
1833
1933
|
try {
|
|
1834
1934
|
files = await readdir10(chatsDir);
|
|
@@ -1838,7 +1938,7 @@ var QwenAdapter = class {
|
|
|
1838
1938
|
if (opts.cwd) continue;
|
|
1839
1939
|
for (const f of files) {
|
|
1840
1940
|
if (!f.endsWith(".jsonl")) continue;
|
|
1841
|
-
const path =
|
|
1941
|
+
const path = join11(chatsDir, f);
|
|
1842
1942
|
let st;
|
|
1843
1943
|
try {
|
|
1844
1944
|
st = await stat10(path);
|
|
@@ -1988,7 +2088,103 @@ function selectAdapters(opts) {
|
|
|
1988
2088
|
});
|
|
1989
2089
|
}
|
|
1990
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
|
+
|
|
1991
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
|
+
}
|
|
1992
2188
|
async function* scanSessions(opts) {
|
|
1993
2189
|
for (const adapter of selectAdapters(opts)) {
|
|
1994
2190
|
if (opts.signal?.aborted) return;
|
|
@@ -2004,7 +2200,7 @@ async function* scanSessions(opts) {
|
|
|
2004
2200
|
if (opts.signal?.aborted) return;
|
|
2005
2201
|
let spans;
|
|
2006
2202
|
try {
|
|
2007
|
-
spans = await adapter
|
|
2203
|
+
spans = await parseSession(adapter, ref);
|
|
2008
2204
|
} catch (err) {
|
|
2009
2205
|
opts.onError?.(err, ref);
|
|
2010
2206
|
continue;
|
|
@@ -2015,106 +2211,233 @@ async function* scanSessions(opts) {
|
|
|
2015
2211
|
}
|
|
2016
2212
|
}
|
|
2017
2213
|
|
|
2018
|
-
// src/
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
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
|
|
2023
2246
|
};
|
|
2024
|
-
const t = setTimeout(() => {
|
|
2025
|
-
signal?.removeEventListener("abort", onAbort);
|
|
2026
|
-
resolve();
|
|
2027
|
-
}, ms);
|
|
2028
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2029
|
-
});
|
|
2030
|
-
function loopKey(sessionId, toolName, argHash) {
|
|
2031
|
-
return `${sessionId}\0${toolName}\0${argHash}`;
|
|
2032
2247
|
}
|
|
2033
|
-
|
|
2034
|
-
const
|
|
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
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
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}` };
|
|
2083
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) };
|
|
2084
2396
|
}
|
|
2085
2397
|
}
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
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);
|
|
2090
2437
|
}
|
|
2091
|
-
|
|
2092
|
-
await sleep(intervalMs, opts.signal);
|
|
2093
|
-
}
|
|
2438
|
+
};
|
|
2094
2439
|
}
|
|
2095
2440
|
|
|
2096
|
-
// src/attributes.ts
|
|
2097
|
-
var ATTR = {
|
|
2098
|
-
/** = the trace/session id. Wire contract (dedup linkage). */
|
|
2099
|
-
SESSION_ID: "tangle.sessionId",
|
|
2100
|
-
/** "cli" marks a CLI upload so the platform can drop a re-upload of a session
|
|
2101
|
-
* that also streamed live. Wire contract. */
|
|
2102
|
-
INGEST_SOURCE: "tangle.ingest_source",
|
|
2103
|
-
HARNESS: "tangle.harness",
|
|
2104
|
-
CWD: "tangle.cwd",
|
|
2105
|
-
GIT_BRANCH: "tangle.git_branch",
|
|
2106
|
-
HOST: "tangle.host",
|
|
2107
|
-
/** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
|
|
2108
|
-
* which collided with `tangle.ingest_source`.) */
|
|
2109
|
-
SESSION_FILE: "tangle.session_file",
|
|
2110
|
-
UPLOADED_AT: "tangle.uploaded_at",
|
|
2111
|
-
UPLOADER: "tangle.uploader",
|
|
2112
|
-
REDACTION_VERSION: "redaction.version",
|
|
2113
|
-
REDACTION_COUNT: "redaction.count"
|
|
2114
|
-
};
|
|
2115
|
-
var INGEST_SOURCE_CLI = "cli";
|
|
2116
|
-
var DEFAULT_HARNESS = "claude-code";
|
|
2117
|
-
|
|
2118
2441
|
// src/redact.ts
|
|
2119
2442
|
import { DEFAULT_REDACTION_RULES, redactValue } from "@tangle-network/agent-eval/traces";
|
|
2120
2443
|
var CODING_REDACTION_RULES = [
|
|
@@ -2175,20 +2498,564 @@ async function applyRedactor(spans, redactor) {
|
|
|
2175
2498
|
return { spans: out, changed };
|
|
2176
2499
|
}
|
|
2177
2500
|
|
|
2178
|
-
// src/
|
|
2501
|
+
// src/file-export.ts
|
|
2179
2502
|
import { createHash } from "crypto";
|
|
2180
|
-
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";
|
|
2181
3048
|
import { homedir as homedir11 } from "os";
|
|
2182
|
-
import { dirname, join as
|
|
3049
|
+
import { dirname, join as join14 } from "path";
|
|
2183
3050
|
function stateFile() {
|
|
2184
|
-
const base = process.env.XDG_STATE_HOME ??
|
|
2185
|
-
return
|
|
3051
|
+
const base = process.env.XDG_STATE_HOME ?? join14(homedir11(), ".local", "state");
|
|
3052
|
+
return join14(base, "tangle-traces", "uploaded.json");
|
|
2186
3053
|
}
|
|
2187
3054
|
function uploadKey(harness, sessionId) {
|
|
2188
3055
|
return `${harness}:${sessionId}`;
|
|
2189
3056
|
}
|
|
2190
3057
|
function sessionHash(spans) {
|
|
2191
|
-
const h =
|
|
3058
|
+
const h = createHash2("sha256");
|
|
2192
3059
|
for (const s of spans) {
|
|
2193
3060
|
h.update(`${s.span_id}\0${s.name}\0${s.start_time}\0${s.status.code}
|
|
2194
3061
|
`);
|
|
@@ -2197,7 +3064,7 @@ function sessionHash(spans) {
|
|
|
2197
3064
|
}
|
|
2198
3065
|
async function loadState() {
|
|
2199
3066
|
try {
|
|
2200
|
-
return JSON.parse(await
|
|
3067
|
+
return JSON.parse(await readFile13(stateFile(), "utf8"));
|
|
2201
3068
|
} catch {
|
|
2202
3069
|
return {};
|
|
2203
3070
|
}
|
|
@@ -2205,7 +3072,7 @@ async function loadState() {
|
|
|
2205
3072
|
async function saveState(state) {
|
|
2206
3073
|
const f = stateFile();
|
|
2207
3074
|
await mkdir(dirname(f), { recursive: true });
|
|
2208
|
-
await
|
|
3075
|
+
await writeFile3(f, `${JSON.stringify(state, null, 2)}
|
|
2209
3076
|
`, "utf8");
|
|
2210
3077
|
}
|
|
2211
3078
|
function alreadyUploaded(state, harness, sessionId, hash) {
|
|
@@ -2213,10 +3080,10 @@ function alreadyUploaded(state, harness, sessionId, hash) {
|
|
|
2213
3080
|
}
|
|
2214
3081
|
|
|
2215
3082
|
// src/upload.ts
|
|
2216
|
-
import { readFile as
|
|
3083
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
2217
3084
|
import { createRequire } from "module";
|
|
2218
3085
|
import { hostname } from "os";
|
|
2219
|
-
import { basename as basename8, join as
|
|
3086
|
+
import { basename as basename8, join as join15 } from "path";
|
|
2220
3087
|
import { hostedClientFromEnv } from "@tangle-network/agent-eval/hosted";
|
|
2221
3088
|
import { REDACTION_VERSION } from "@tangle-network/agent-eval/traces";
|
|
2222
3089
|
var require2 = createRequire(import.meta.url);
|
|
@@ -2240,7 +3107,7 @@ async function planUpload(opts = {}) {
|
|
|
2240
3107
|
async function gitBranch(cwd) {
|
|
2241
3108
|
if (!cwd) return void 0;
|
|
2242
3109
|
try {
|
|
2243
|
-
const head = await
|
|
3110
|
+
const head = await readFile14(join15(cwd, ".git", "HEAD"), "utf8");
|
|
2244
3111
|
const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
|
|
2245
3112
|
return m?.[1]?.trim();
|
|
2246
3113
|
} catch {
|
|
@@ -2348,25 +3215,31 @@ async function executeUpload(plan, opts = {}) {
|
|
|
2348
3215
|
}
|
|
2349
3216
|
|
|
2350
3217
|
export {
|
|
3218
|
+
countSkillRunsJsonl,
|
|
3219
|
+
analyzeAdoption,
|
|
2351
3220
|
span,
|
|
2352
3221
|
toOpenInferenceSpan,
|
|
2353
3222
|
serializeSpans,
|
|
2354
3223
|
writeOtlpFile,
|
|
2355
3224
|
analyzeSpans,
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
haloAnalyzer,
|
|
2360
|
-
runExternalAnalyzers,
|
|
2361
|
-
commandRedactor,
|
|
3225
|
+
ATTR,
|
|
3226
|
+
INGEST_SOURCE_CLI,
|
|
3227
|
+
DEFAULT_HARNESS,
|
|
2362
3228
|
parseIsoToEpochMs,
|
|
2363
3229
|
parseSince,
|
|
2364
3230
|
toRuntimeStore,
|
|
2365
3231
|
runPipelines,
|
|
2366
3232
|
CONTENT_CAP,
|
|
2367
3233
|
capText,
|
|
3234
|
+
ACTOR_ATTR,
|
|
2368
3235
|
userPromptSpan,
|
|
2369
3236
|
AmpAdapter,
|
|
3237
|
+
textIsSynthetic,
|
|
3238
|
+
looksLikeAgentPrompt,
|
|
3239
|
+
claudeActor,
|
|
3240
|
+
codexActor,
|
|
3241
|
+
classifyReaction,
|
|
3242
|
+
CORRECTIVE_REACTIONS,
|
|
2370
3243
|
ClaudeAdapter,
|
|
2371
3244
|
CodexAdapter,
|
|
2372
3245
|
CopilotAdapter,
|
|
@@ -2380,15 +3253,31 @@ export {
|
|
|
2380
3253
|
resolveAdapter,
|
|
2381
3254
|
knownHarnesses,
|
|
2382
3255
|
selectAdapters,
|
|
3256
|
+
normalizeRemote,
|
|
3257
|
+
resolveRepoAttrs,
|
|
3258
|
+
stampRepoAttrs,
|
|
3259
|
+
parseSession,
|
|
2383
3260
|
scanSessions,
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
3261
|
+
buildPolicyEvidenceRecord,
|
|
3262
|
+
collectPolicyEvidence,
|
|
3263
|
+
serializePolicyEvidence,
|
|
3264
|
+
writePolicyEvidenceFile,
|
|
3265
|
+
runCommand,
|
|
3266
|
+
commandAnalyzer,
|
|
3267
|
+
haloAnalyzer,
|
|
3268
|
+
runExternalAnalyzers,
|
|
3269
|
+
commandRedactor,
|
|
2388
3270
|
CODING_REDACTION_RULES,
|
|
2389
3271
|
TRACES_REDACTION_RULES,
|
|
2390
3272
|
redactSpans,
|
|
2391
3273
|
applyRedactor,
|
|
3274
|
+
exportTraceEvidenceRows,
|
|
3275
|
+
exportTraceEvidenceText,
|
|
3276
|
+
exportTraceEvidenceFile,
|
|
3277
|
+
writeTraceEvidenceExportFile,
|
|
3278
|
+
watchSessions,
|
|
3279
|
+
REACTIONS,
|
|
3280
|
+
analyzeReactions,
|
|
2392
3281
|
uploadKey,
|
|
2393
3282
|
sessionHash,
|
|
2394
3283
|
loadState,
|