@tangle-network/traces 0.8.27 → 0.9.1
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 +16 -18
- package/dist/{chunk-EZDMNNW4.js → chunk-RUG26PIS.js} +2312 -1054
- package/dist/cli.js +108 -83
- package/dist/index.d.ts +236 -146
- package/dist/index.js +35 -3
- package/package.json +3 -3
|
@@ -1,138 +1,25 @@
|
|
|
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 ?? input.agent_type ?? input.type ?? input.name;
|
|
26
|
-
return typeof v === "string" && v.length > 0 ? v : "?";
|
|
27
|
-
}
|
|
28
|
-
function isSpawnAgentTool(name) {
|
|
29
|
-
return name === "spawn_agent" || name.endsWith("__spawn_agent");
|
|
30
|
-
}
|
|
31
|
-
function countSkillRunsJsonl(raw) {
|
|
32
|
-
const out = {};
|
|
33
|
-
for (const line of raw.split("\n")) {
|
|
34
|
-
const t = line.trim();
|
|
35
|
-
if (!t) continue;
|
|
36
|
-
let o;
|
|
37
|
-
try {
|
|
38
|
-
o = JSON.parse(t);
|
|
39
|
-
} catch {
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
if (!o || typeof o !== "object") continue;
|
|
43
|
-
const rec = o;
|
|
44
|
-
const names = [];
|
|
45
|
-
if (Array.isArray(rec.skills)) {
|
|
46
|
-
for (const s of rec.skills) if (typeof s === "string" && s) names.push(s);
|
|
47
|
-
}
|
|
48
|
-
if (typeof rec.skill === "string" && rec.skill) names.push(rec.skill);
|
|
49
|
-
if (names.length === 0) names.push("?");
|
|
50
|
-
for (const n of names) out[n] = (out[n] ?? 0) + 1;
|
|
51
|
-
}
|
|
52
|
-
return out;
|
|
53
|
-
}
|
|
54
|
-
async function readLoopRuns(cwds) {
|
|
55
|
-
const counts = {};
|
|
56
|
-
let filesRead = 0;
|
|
57
|
-
const seen = /* @__PURE__ */ new Set();
|
|
58
|
-
for (const cwd of cwds) {
|
|
59
|
-
if (!cwd || seen.has(cwd)) continue;
|
|
60
|
-
seen.add(cwd);
|
|
61
|
-
const path = join(cwd, ".evolve", "skill-runs.jsonl");
|
|
62
|
-
let raw;
|
|
63
|
-
try {
|
|
64
|
-
raw = await readFile(path, "utf8");
|
|
65
|
-
} catch {
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
filesRead += 1;
|
|
69
|
-
for (const [k, v] of Object.entries(countSkillRunsJsonl(raw))) counts[k] = (counts[k] ?? 0) + v;
|
|
70
|
-
}
|
|
71
|
-
return { counts, filesRead };
|
|
72
|
-
}
|
|
73
|
-
async function analyzeAdoption(spans, opts = {}) {
|
|
74
|
-
const sessions = /* @__PURE__ */ new Set();
|
|
75
|
-
const sessionsWithSkill = /* @__PURE__ */ new Set();
|
|
76
|
-
const sessionsWithSubagent = /* @__PURE__ */ new Set();
|
|
77
|
-
const skillInvocations = {};
|
|
78
|
-
const subagentSpawns = {};
|
|
79
|
-
const canonicalSubagentSessions = /* @__PURE__ */ new Set();
|
|
80
|
-
const fallbackSubagents = /* @__PURE__ */ new Map();
|
|
81
|
-
for (const s of spans) {
|
|
82
|
-
sessions.add(s.trace_id);
|
|
83
|
-
const tn = toolName(s);
|
|
84
|
-
if (tn === "Skill") {
|
|
85
|
-
const name = skillNameOf(parseInput(s));
|
|
86
|
-
skillInvocations[name] = (skillInvocations[name] ?? 0) + 1;
|
|
87
|
-
sessionsWithSkill.add(s.trace_id);
|
|
88
|
-
} else if (tn === "Task" || tn === "Agent") {
|
|
89
|
-
const type = subagentTypeOf(parseInput(s));
|
|
90
|
-
subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
|
|
91
|
-
sessionsWithSubagent.add(s.trace_id);
|
|
92
|
-
canonicalSubagentSessions.add(s.trace_id);
|
|
93
|
-
} else if (tn && isSpawnAgentTool(tn)) {
|
|
94
|
-
const types = fallbackSubagents.get(s.trace_id) ?? [];
|
|
95
|
-
types.push(subagentTypeOf(parseInput(s)));
|
|
96
|
-
fallbackSubagents.set(s.trace_id, types);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
for (const [traceId, types] of fallbackSubagents) {
|
|
100
|
-
if (canonicalSubagentSessions.has(traceId)) continue;
|
|
101
|
-
sessionsWithSubagent.add(traceId);
|
|
102
|
-
for (const type of types) {
|
|
103
|
-
subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
const { counts: loopDispatchedRuns, filesRead } = await readLoopRuns(opts.cwds ?? []);
|
|
107
|
-
const totalSkillInvocations = Object.values(skillInvocations).reduce((a, b) => a + b, 0);
|
|
108
|
-
const totalSubagentSpawns = Object.values(subagentSpawns).reduce((a, b) => a + b, 0);
|
|
109
|
-
const totalLoopDispatchedRuns = Object.values(loopDispatchedRuns).reduce((a, b) => a + b, 0);
|
|
110
|
-
return {
|
|
111
|
-
sessionCount: sessions.size,
|
|
112
|
-
sessionsWithSkill: sessionsWithSkill.size,
|
|
113
|
-
skillPenetration: sessions.size === 0 ? 0 : sessionsWithSkill.size / sessions.size,
|
|
114
|
-
skillInvocations,
|
|
115
|
-
totalSkillInvocations,
|
|
116
|
-
subagentSpawns,
|
|
117
|
-
totalSubagentSpawns,
|
|
118
|
-
sessionsWithSubagent: sessionsWithSubagent.size,
|
|
119
|
-
loopDispatchedRuns,
|
|
120
|
-
totalLoopDispatchedRuns,
|
|
121
|
-
skillRunFilesRead: filesRead
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
1
|
// src/otlp.ts
|
|
2
|
+
import {
|
|
3
|
+
applyLlmSpanOtlpAttributes,
|
|
4
|
+
OPENINFERENCE_SPAN_KIND,
|
|
5
|
+
TOOL_NAME
|
|
6
|
+
} from "@tangle-network/agent-eval/trace-attributes";
|
|
126
7
|
function span(input) {
|
|
127
8
|
const attributes = {
|
|
128
|
-
|
|
9
|
+
[OPENINFERENCE_SPAN_KIND]: input.kind
|
|
129
10
|
};
|
|
130
11
|
if (input.service != null) attributes["service.name"] = input.service;
|
|
131
12
|
if (input.agent != null) attributes["agent.name"] = input.agent;
|
|
132
|
-
if (input.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
13
|
+
if (input.tool != null) attributes[TOOL_NAME] = input.tool;
|
|
14
|
+
applyLlmSpanOtlpAttributes(attributes, {
|
|
15
|
+
model: input.model ?? void 0,
|
|
16
|
+
inputTokens: input.inputTokens ?? void 0,
|
|
17
|
+
outputTokens: input.outputTokens ?? void 0,
|
|
18
|
+
reasoningTokens: input.reasoningTokens ?? void 0,
|
|
19
|
+
cachedTokens: input.cachedInputTokens ?? void 0,
|
|
20
|
+
cacheWriteTokens: input.cacheWriteInputTokens ?? void 0,
|
|
21
|
+
costUsd: input.costUsd ?? void 0
|
|
22
|
+
});
|
|
136
23
|
if (input.step != null) attributes.step = input.step;
|
|
137
24
|
if (input.content != null && input.content.length > 0) attributes["content"] = input.content;
|
|
138
25
|
if (input.extra) Object.assign(attributes, input.extra);
|
|
@@ -162,7 +49,7 @@ function toOpenInferenceSpan(s) {
|
|
|
162
49
|
span_id: s.span_id,
|
|
163
50
|
parent_span_id: s.parent_span_id ?? "",
|
|
164
51
|
name: s.name,
|
|
165
|
-
kind: typeof a[
|
|
52
|
+
kind: typeof a[OPENINFERENCE_SPAN_KIND] === "string" ? a[OPENINFERENCE_SPAN_KIND] : "CHAIN",
|
|
166
53
|
start_time: s.start_time,
|
|
167
54
|
end_time: s.end_time,
|
|
168
55
|
status: { code: s.status.code, message: s.status.message ?? "" },
|
|
@@ -185,35 +72,33 @@ async function writeOtlpFile(spans, outPath) {
|
|
|
185
72
|
return path;
|
|
186
73
|
}
|
|
187
74
|
|
|
188
|
-
// src/
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
return { otlpPath, result };
|
|
75
|
+
// src/adapters/conversation.ts
|
|
76
|
+
var CONTENT_CAP = 8e3;
|
|
77
|
+
function capText(raw) {
|
|
78
|
+
const t = raw.trim();
|
|
79
|
+
if (t.length <= CONTENT_CAP) return t;
|
|
80
|
+
return `${t.slice(0, CONTENT_CAP)}\u2026 [+${t.length - CONTENT_CAP} chars]`;
|
|
81
|
+
}
|
|
82
|
+
var ACTOR_ATTR = "tangle.actor";
|
|
83
|
+
function userPromptSpan(o) {
|
|
84
|
+
return span({
|
|
85
|
+
traceId: o.traceId,
|
|
86
|
+
spanId: o.spanId,
|
|
87
|
+
parentSpanId: o.parentSpanId,
|
|
88
|
+
name: "user.prompt",
|
|
89
|
+
kind: "CHAIN",
|
|
90
|
+
startTime: o.startTime,
|
|
91
|
+
service: o.service ?? null,
|
|
92
|
+
agent: o.agent ?? null,
|
|
93
|
+
step: o.step,
|
|
94
|
+
content: o.content,
|
|
95
|
+
extra: { [ACTOR_ATTR]: o.actor ?? "human" }
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/arrays.ts
|
|
100
|
+
function appendAll(target, values) {
|
|
101
|
+
for (const value of values) target.push(value);
|
|
217
102
|
}
|
|
218
103
|
|
|
219
104
|
// src/attributes.ts
|
|
@@ -245,10 +130,94 @@ var ATTR = {
|
|
|
245
130
|
UPLOADED_AT: "tangle.uploaded_at",
|
|
246
131
|
UPLOADER: "tangle.uploader",
|
|
247
132
|
REDACTION_VERSION: "redaction.version",
|
|
248
|
-
REDACTION_COUNT: "redaction.count"
|
|
133
|
+
REDACTION_COUNT: "redaction.count",
|
|
134
|
+
/** Upload contract: bounded root summary plus location-only receipt child spans.
|
|
135
|
+
* Receipt hashes are deliberate integrity evidence; raw malformed bytes never upload. */
|
|
136
|
+
SESSION_INTEGRITY: "traces.session.integrity",
|
|
137
|
+
CORRUPTION_COUNT: "traces.session.corruption_count",
|
|
138
|
+
CORRUPTION_DIGEST: "traces.session.corruption_digest",
|
|
139
|
+
CORRUPTION_RECEIPT_VERSION: "traces.session.corruption.receipt_version",
|
|
140
|
+
CORRUPTION_RECEIPT_KIND: "traces.session.corruption.kind",
|
|
141
|
+
CORRUPTION_SOURCE_PATH: "traces.session.corruption.source_path",
|
|
142
|
+
CORRUPTION_LINE_NUMBER: "traces.session.corruption.line_number",
|
|
143
|
+
CORRUPTION_BYTE_OFFSET: "traces.session.corruption.byte_offset",
|
|
144
|
+
CORRUPTION_BYTE_LENGTH: "traces.session.corruption.byte_length",
|
|
145
|
+
CORRUPTION_SHA256: "traces.session.corruption.sha256",
|
|
146
|
+
RAW_SOURCE_RETENTION: "traces.session.raw_source_retention"
|
|
249
147
|
};
|
|
250
148
|
var INGEST_SOURCE_CLI = "cli";
|
|
251
149
|
var DEFAULT_HARNESS = "claude-code";
|
|
150
|
+
var SESSION_ID_ATTRIBUTE_KEYS = [ATTR.SESSION_ID, "session.id", "traces.session.id"];
|
|
151
|
+
function sessionIdFromAttributes(attributes) {
|
|
152
|
+
return SESSION_ID_ATTRIBUTE_KEYS.map((key) => attributes[key]).find(
|
|
153
|
+
(value) => typeof value === "string" && value.length > 0
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
function indexSessionIdsByTrace(spans) {
|
|
157
|
+
const candidatesByTrace = /* @__PURE__ */ new Map();
|
|
158
|
+
for (const span2 of spans) {
|
|
159
|
+
const candidates = candidatesByTrace.get(span2.trace_id) ?? /* @__PURE__ */ new Set();
|
|
160
|
+
for (const key of SESSION_ID_ATTRIBUTE_KEYS) {
|
|
161
|
+
const value = span2.attributes[key];
|
|
162
|
+
if (typeof value === "string" && value.length > 0) candidates.add(value);
|
|
163
|
+
}
|
|
164
|
+
if (candidates.size > 0) candidatesByTrace.set(span2.trace_id, candidates);
|
|
165
|
+
}
|
|
166
|
+
const sessionByTrace = /* @__PURE__ */ new Map();
|
|
167
|
+
const conflicts = [];
|
|
168
|
+
for (const [traceId, candidates] of candidatesByTrace) {
|
|
169
|
+
const sessionIds2 = [...candidates].sort();
|
|
170
|
+
if (sessionIds2.length === 1) {
|
|
171
|
+
sessionByTrace.set(traceId, sessionIds2[0]);
|
|
172
|
+
} else {
|
|
173
|
+
conflicts.push({ traceId, sessionIds: sessionIds2 });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
conflicts.sort((a, b) => a.traceId.localeCompare(b.traceId));
|
|
177
|
+
return { sessionByTrace, conflicts };
|
|
178
|
+
}
|
|
179
|
+
function stampSessionIdentity(spans, fallbackSessionId) {
|
|
180
|
+
const { sessionByTrace, conflicts } = indexSessionIdsByTrace(spans);
|
|
181
|
+
const conflictingTraceIds = new Set(conflicts.map((conflict) => conflict.traceId));
|
|
182
|
+
const traceIds = new Set(spans.map((span2) => span2.trace_id));
|
|
183
|
+
const singleTraceId = traceIds.size === 1 ? traceIds.values().next().value : void 0;
|
|
184
|
+
for (const span2 of spans) {
|
|
185
|
+
if (conflictingTraceIds.has(span2.trace_id)) continue;
|
|
186
|
+
const sessionId = sessionByTrace.get(span2.trace_id) ?? singleTraceId ?? fallbackSessionId;
|
|
187
|
+
if (!sessionIdFromAttributes(span2.attributes)) {
|
|
188
|
+
span2.attributes[ATTR.SESSION_ID] = sessionId;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/execution.ts
|
|
194
|
+
import {
|
|
195
|
+
summarizeExecution
|
|
196
|
+
} from "@tangle-network/agent-eval/contract";
|
|
197
|
+
import {
|
|
198
|
+
otlpRowsToRunRecords,
|
|
199
|
+
otlpToRunRecords
|
|
200
|
+
} from "@tangle-network/agent-eval/traces";
|
|
201
|
+
function runOptions(opts) {
|
|
202
|
+
return {
|
|
203
|
+
experimentId: opts.experimentId ?? "traces-observed",
|
|
204
|
+
candidateId: opts.candidateId ?? "observed",
|
|
205
|
+
fallbackModel: "unknown@otlp"
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function* openInferenceRows(spans) {
|
|
209
|
+
for (const item of spans) yield toOpenInferenceSpan(item);
|
|
210
|
+
}
|
|
211
|
+
function summarizeOtlpExecution(otlpJsonl, opts = {}) {
|
|
212
|
+
const runs = otlpToRunRecords(otlpJsonl, runOptions(opts));
|
|
213
|
+
return summarizeExecution({ runs });
|
|
214
|
+
}
|
|
215
|
+
function summarizeSpanExecution(spans, opts = {}) {
|
|
216
|
+
if (spans.length === 0) throw new Error("summarizeSpanExecution: no spans to summarize");
|
|
217
|
+
return summarizeExecution({
|
|
218
|
+
runs: otlpRowsToRunRecords(openInferenceRows(spans), runOptions(opts))
|
|
219
|
+
});
|
|
220
|
+
}
|
|
252
221
|
|
|
253
222
|
// src/time.ts
|
|
254
223
|
function parseIsoToEpochMs(ts) {
|
|
@@ -271,6 +240,125 @@ function parseSince(value) {
|
|
|
271
240
|
|
|
272
241
|
// src/runtime-store.ts
|
|
273
242
|
import { InMemoryTraceStore } from "@tangle-network/agent-eval";
|
|
243
|
+
|
|
244
|
+
// src/adapters/tool-io.ts
|
|
245
|
+
import { createHash } from "crypto";
|
|
246
|
+
var TOOL_IO_VALUE_MAX_BYTES = 16 * 1024;
|
|
247
|
+
var TOOL_IO_VALUE_KEYS = ["input.value", "output.value"];
|
|
248
|
+
var TOOL_IO_SIDES = ["input", "output"];
|
|
249
|
+
var LEGACY_TRUNCATION_MARKER = /\n\[truncated (\d+) bytes; sha256=[a-f0-9]{64}\]$/;
|
|
250
|
+
var COUNTED_TRUNCATION_MARKER = /\n\[truncated \d+ bytes\]$/;
|
|
251
|
+
var TRUNCATION_MARKER = /\n\[truncated\]$/;
|
|
252
|
+
function canonicalJson(value) {
|
|
253
|
+
const sort = (item) => {
|
|
254
|
+
if (Array.isArray(item)) return item.map(sort);
|
|
255
|
+
if (!item || typeof item !== "object") return item;
|
|
256
|
+
return Object.fromEntries(
|
|
257
|
+
Object.entries(item).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => [key, sort(entry)])
|
|
258
|
+
);
|
|
259
|
+
};
|
|
260
|
+
const json2 = JSON.stringify(sort(value));
|
|
261
|
+
if (json2 === void 0) throw new TypeError("tool I/O value is not JSON-serializable");
|
|
262
|
+
return json2;
|
|
263
|
+
}
|
|
264
|
+
function serialize(value) {
|
|
265
|
+
if (value === void 0) return void 0;
|
|
266
|
+
if (typeof value === "string") {
|
|
267
|
+
const trimmed = value.trim();
|
|
268
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
269
|
+
try {
|
|
270
|
+
return { value: canonicalJson(JSON.parse(trimmed)), mimeType: "application/json" };
|
|
271
|
+
} catch {
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return { value, mimeType: "text/plain" };
|
|
275
|
+
}
|
|
276
|
+
return { value: canonicalJson(value), mimeType: "application/json" };
|
|
277
|
+
}
|
|
278
|
+
function utf8Prefix(value, maxBytes) {
|
|
279
|
+
let bytes = 0;
|
|
280
|
+
let end = 0;
|
|
281
|
+
for (const char of value) {
|
|
282
|
+
const charBytes = Buffer.byteLength(char);
|
|
283
|
+
if (bytes + charBytes > maxBytes) break;
|
|
284
|
+
bytes += charBytes;
|
|
285
|
+
end += char.length;
|
|
286
|
+
}
|
|
287
|
+
return Buffer.from(value.slice(0, end), "utf8").toString("utf8");
|
|
288
|
+
}
|
|
289
|
+
function truncatedValue(value, bytes) {
|
|
290
|
+
const longestMarker = `
|
|
291
|
+
[truncated ${bytes} bytes]`;
|
|
292
|
+
const prefix = utf8Prefix(value, TOOL_IO_VALUE_MAX_BYTES - Buffer.byteLength(longestMarker));
|
|
293
|
+
const omittedBytes = bytes - Buffer.byteLength(prefix);
|
|
294
|
+
return `${prefix}
|
|
295
|
+
[truncated ${omittedBytes} bytes]`;
|
|
296
|
+
}
|
|
297
|
+
function valueAttributes(side, raw) {
|
|
298
|
+
const serialized = serialize(raw);
|
|
299
|
+
if (!serialized) return {};
|
|
300
|
+
const bytes = Buffer.byteLength(serialized.value);
|
|
301
|
+
const sha256 = createHash("sha256").update(serialized.value).digest("hex");
|
|
302
|
+
const truncated = bytes > TOOL_IO_VALUE_MAX_BYTES;
|
|
303
|
+
const value = truncated ? truncatedValue(serialized.value, bytes) : serialized.value;
|
|
304
|
+
return {
|
|
305
|
+
[`${side}.value`]: value,
|
|
306
|
+
[`${side}.mime_type`]: serialized.mimeType,
|
|
307
|
+
[`traces.${side}.bytes`]: bytes,
|
|
308
|
+
[`traces.${side}.sha256`]: sha256,
|
|
309
|
+
[`traces.${side}.truncated`]: truncated
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function toolIoAttributes(io) {
|
|
313
|
+
return {
|
|
314
|
+
...valueAttributes("input", io.input),
|
|
315
|
+
...valueAttributes("output", io.output),
|
|
316
|
+
...io.argsCaptured === void 0 ? {} : { "tool.args_captured": io.argsCaptured }
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function toolArgumentsFromAttributes(attributes) {
|
|
320
|
+
const declared = attributes["tool.args_captured"];
|
|
321
|
+
const explicit = typeof declared === "boolean" ? declared : declared === "true" ? true : declared === "false" ? false : void 0;
|
|
322
|
+
const argsCaptured = explicit ?? attributes["input.value"] !== void 0;
|
|
323
|
+
return {
|
|
324
|
+
args: argsCaptured ? attributes["input.value"] : void 0,
|
|
325
|
+
argsCaptured
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function normalizeToolIoAttributes(attributes) {
|
|
329
|
+
for (const side of TOOL_IO_SIDES) {
|
|
330
|
+
const valueKey = `${side}.value`;
|
|
331
|
+
const mimeKey = `${side}.mime_type`;
|
|
332
|
+
const bytesKey = `traces.${side}.bytes`;
|
|
333
|
+
const digestKey = `traces.${side}.sha256`;
|
|
334
|
+
const truncatedKey = `traces.${side}.truncated`;
|
|
335
|
+
const rawValue = attributes[valueKey];
|
|
336
|
+
if (typeof rawValue !== "string") {
|
|
337
|
+
delete attributes[mimeKey];
|
|
338
|
+
delete attributes[bytesKey];
|
|
339
|
+
delete attributes[digestKey];
|
|
340
|
+
delete attributes[truncatedKey];
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
let value = rawValue.replace(LEGACY_TRUNCATION_MARKER, "\n[truncated]").replace(COUNTED_TRUNCATION_MARKER, "\n[truncated]");
|
|
344
|
+
let truncated = TRUNCATION_MARKER.test(value);
|
|
345
|
+
const unboundedBytes = Buffer.byteLength(value);
|
|
346
|
+
if (unboundedBytes > TOOL_IO_VALUE_MAX_BYTES) {
|
|
347
|
+
value = truncatedValue(value, unboundedBytes);
|
|
348
|
+
truncated = true;
|
|
349
|
+
}
|
|
350
|
+
attributes[valueKey] = value;
|
|
351
|
+
attributes[bytesKey] = Buffer.byteLength(value);
|
|
352
|
+
attributes[digestKey] = createHash("sha256").update(value).digest("hex");
|
|
353
|
+
attributes[truncatedKey] = truncated;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function recordToolOutput(toolSpan, output) {
|
|
357
|
+
if (!toolSpan) return;
|
|
358
|
+
Object.assign(toolSpan.attributes, valueAttributes("output", output));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/runtime-store.ts
|
|
274
362
|
var KIND = {
|
|
275
363
|
AGENT: "agent",
|
|
276
364
|
LLM: "llm",
|
|
@@ -280,75 +368,240 @@ var KIND = {
|
|
|
280
368
|
};
|
|
281
369
|
async function toRuntimeStore(spans) {
|
|
282
370
|
const store = new InMemoryTraceStore();
|
|
283
|
-
const
|
|
371
|
+
const runs = /* @__PURE__ */ new Map();
|
|
372
|
+
for (const item of spans) {
|
|
373
|
+
const startedAt = parseIsoToEpochMs(item.start_time);
|
|
374
|
+
const endedAt = parseIsoToEpochMs(item.end_time);
|
|
375
|
+
const current = runs.get(item.trace_id);
|
|
376
|
+
runs.set(item.trace_id, current ? {
|
|
377
|
+
startedAt: Math.min(current.startedAt, startedAt),
|
|
378
|
+
endedAt: Math.max(current.endedAt, endedAt),
|
|
379
|
+
failed: current.failed || item.status.code === "ERROR"
|
|
380
|
+
} : { startedAt, endedAt, failed: item.status.code === "ERROR" });
|
|
381
|
+
}
|
|
382
|
+
for (const [runId, run] of runs) {
|
|
383
|
+
await store.appendRun({
|
|
384
|
+
runId,
|
|
385
|
+
scenarioId: "session",
|
|
386
|
+
startedAt: run.startedAt,
|
|
387
|
+
endedAt: run.endedAt,
|
|
388
|
+
status: run.failed ? "failed" : "completed"
|
|
389
|
+
});
|
|
390
|
+
}
|
|
284
391
|
for (const s of spans) {
|
|
285
|
-
if (!runIds.has(s.trace_id)) {
|
|
286
|
-
runIds.add(s.trace_id);
|
|
287
|
-
await store.appendRun({ runId: s.trace_id, scenarioId: "session" });
|
|
288
|
-
}
|
|
289
392
|
const kind = KIND[s.attributes["openinference.span.kind"]] ?? "custom";
|
|
393
|
+
const startedAt = parseIsoToEpochMs(s.start_time);
|
|
394
|
+
const endedAt = parseIsoToEpochMs(s.end_time);
|
|
290
395
|
const base = {
|
|
291
396
|
spanId: s.span_id,
|
|
292
397
|
parentSpanId: s.parent_span_id ?? void 0,
|
|
293
398
|
runId: s.trace_id,
|
|
294
399
|
name: s.name,
|
|
295
|
-
startedAt
|
|
296
|
-
endedAt
|
|
400
|
+
startedAt,
|
|
401
|
+
endedAt,
|
|
297
402
|
status: s.status.code === "ERROR" ? "error" : "ok",
|
|
298
403
|
error: s.status.message,
|
|
299
404
|
attributes: s.attributes
|
|
300
405
|
};
|
|
406
|
+
const tool = kind === "tool" ? toolArgumentsFromAttributes(s.attributes) : void 0;
|
|
301
407
|
const span2 = kind === "tool" ? {
|
|
302
408
|
...base,
|
|
303
409
|
kind: "tool",
|
|
304
410
|
toolName: String(s.attributes["tool.name"] ?? "tool"),
|
|
305
|
-
args:
|
|
411
|
+
args: tool.args,
|
|
412
|
+
argsCaptured: tool.argsCaptured,
|
|
413
|
+
latencyMs: Math.max(0, endedAt - startedAt)
|
|
306
414
|
} : { ...base, kind };
|
|
307
415
|
await store.appendSpan(span2);
|
|
308
416
|
}
|
|
309
|
-
return { store, runIds: [...
|
|
417
|
+
return { store, runIds: [...runs.keys()] };
|
|
310
418
|
}
|
|
311
419
|
|
|
312
420
|
// src/pipelines.ts
|
|
313
421
|
import { computeToolUseMetrics } from "@tangle-network/agent-eval";
|
|
314
|
-
import {
|
|
422
|
+
import {
|
|
423
|
+
failureClusterView,
|
|
424
|
+
stuckLoopView
|
|
425
|
+
} from "@tangle-network/agent-eval/pipelines";
|
|
315
426
|
async function runPipelines(spans, opts = {}) {
|
|
316
427
|
const { store, runIds } = await toRuntimeStore(spans);
|
|
317
428
|
const loopEligible = spans.filter((item) => item.attributes["traces.expected_blocking"] !== true);
|
|
318
429
|
const { store: loopStore } = await toRuntimeStore(loopEligible);
|
|
319
|
-
const stuckLoops = await
|
|
320
|
-
|
|
321
|
-
|
|
430
|
+
const [stuckLoops, failureClusters, toolUse] = await Promise.all([
|
|
431
|
+
stuckLoopView(loopStore, { minOccurrences: opts.minLoopOccurrences ?? 3 }),
|
|
432
|
+
failureClusterView(store),
|
|
433
|
+
Promise.all(runIds.map((runId) => computeToolUseMetrics(store, runId)))
|
|
434
|
+
]);
|
|
435
|
+
return { stuckLoops, failureClusters, toolUse };
|
|
322
436
|
}
|
|
323
437
|
|
|
324
|
-
// src/
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
438
|
+
// src/integrity.ts
|
|
439
|
+
import { createHash as createHash2 } from "crypto";
|
|
440
|
+
var receiptStates = /* @__PURE__ */ new WeakMap();
|
|
441
|
+
function receiptIdentity(receipt) {
|
|
442
|
+
return [
|
|
443
|
+
receipt.sourcePath,
|
|
444
|
+
receipt.lineNumber,
|
|
445
|
+
receipt.byteOffset,
|
|
446
|
+
receipt.byteLength,
|
|
447
|
+
receipt.sha256
|
|
448
|
+
];
|
|
330
449
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
450
|
+
function receiptKey(receipt) {
|
|
451
|
+
return JSON.stringify(receiptIdentity(receipt));
|
|
452
|
+
}
|
|
453
|
+
function stateFor(ref, current) {
|
|
454
|
+
let state = receiptStates.get(ref);
|
|
455
|
+
if (!state || state.corruptions !== current) {
|
|
456
|
+
state = {
|
|
457
|
+
corruptions: current,
|
|
458
|
+
seen: new Set(current.map(receiptKey))
|
|
459
|
+
};
|
|
460
|
+
receiptStates.set(ref, state);
|
|
461
|
+
}
|
|
462
|
+
return state;
|
|
463
|
+
}
|
|
464
|
+
function recordSessionCorruption(ref, receipt) {
|
|
465
|
+
const current = ref.integrity?.corruptions ?? [];
|
|
466
|
+
const corruption = { ...receipt, harness: ref.harness, sessionId: ref.sessionId };
|
|
467
|
+
const state = stateFor(ref, current);
|
|
468
|
+
const key = receiptKey(receipt);
|
|
469
|
+
if (state.seen.has(key)) return;
|
|
470
|
+
if (ref.integrity) {
|
|
471
|
+
ref.integrity.corruptions.push(corruption);
|
|
472
|
+
} else {
|
|
473
|
+
ref.integrity = { status: "degraded_not_lossless", corruptions: [corruption] };
|
|
474
|
+
}
|
|
475
|
+
state.seen.add(key);
|
|
476
|
+
state.corruptions = ref.integrity.corruptions;
|
|
477
|
+
}
|
|
478
|
+
function sessionJsonlOptions(ref, options = {}) {
|
|
479
|
+
if (options.corruptionMode === "strict") return { mode: "strict" };
|
|
480
|
+
return {
|
|
481
|
+
mode: "recover",
|
|
482
|
+
onCorruption: (receipt) => recordSessionCorruption(ref, receipt)
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
function sessionIntegrityAttributes(ref) {
|
|
486
|
+
if (!ref.integrity) return {};
|
|
487
|
+
return {
|
|
488
|
+
[ATTR.SESSION_INTEGRITY]: ref.integrity.status,
|
|
489
|
+
[ATTR.CORRUPTION_COUNT]: ref.integrity.corruptions.length,
|
|
490
|
+
[ATTR.CORRUPTION_DIGEST]: corruptionDigest(ref.integrity.corruptions),
|
|
491
|
+
[ATTR.RAW_SOURCE_RETENTION]: "local_source_only"
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function corruptionDigest(receipts) {
|
|
495
|
+
const hash2 = createHash2("sha256");
|
|
496
|
+
for (const receipt of receipts) {
|
|
497
|
+
hash2.update(JSON.stringify(receiptIdentity(receipt)));
|
|
498
|
+
hash2.update("\n");
|
|
499
|
+
}
|
|
500
|
+
return `sha256:${hash2.digest("hex")}`;
|
|
501
|
+
}
|
|
502
|
+
function receiptSpanId(parentSpanId, receipt) {
|
|
503
|
+
const digest = createHash2("sha256").update(JSON.stringify(receiptIdentity(receipt))).digest("hex");
|
|
504
|
+
return `${parentSpanId}:corruption:${digest}`;
|
|
505
|
+
}
|
|
506
|
+
function stampSessionIntegrity(ref, spans) {
|
|
507
|
+
if (!ref.integrity) return;
|
|
508
|
+
const parent = spans.find((item) => item.parent_span_id === null) ?? spans[0];
|
|
509
|
+
if (!parent) return;
|
|
510
|
+
const sessionId = parent.trace_id;
|
|
511
|
+
for (const receipt of ref.integrity.corruptions) {
|
|
512
|
+
receipt.harness = ref.harness;
|
|
513
|
+
receipt.sessionId = sessionId;
|
|
514
|
+
}
|
|
515
|
+
const attributes = sessionIntegrityAttributes(ref);
|
|
516
|
+
Object.assign(parent.attributes, attributes);
|
|
517
|
+
const existingSpanIds = new Set(spans.map((item) => item.span_id));
|
|
518
|
+
for (const receipt of ref.integrity.corruptions) {
|
|
519
|
+
const spanId = receiptSpanId(parent.span_id, receipt);
|
|
520
|
+
if (existingSpanIds.has(spanId)) continue;
|
|
521
|
+
spans.push(span({
|
|
522
|
+
traceId: parent.trace_id,
|
|
523
|
+
spanId,
|
|
524
|
+
parentSpanId: parent.span_id,
|
|
525
|
+
name: "source.corruption.receipt",
|
|
526
|
+
kind: "CHAIN",
|
|
527
|
+
startTime: parent.start_time,
|
|
528
|
+
endTime: parent.start_time,
|
|
529
|
+
status: "ERROR",
|
|
530
|
+
service: typeof parent.attributes["service.name"] === "string" ? parent.attributes["service.name"] : ref.harness,
|
|
531
|
+
extra: {
|
|
532
|
+
[ATTR.SESSION_INTEGRITY]: ref.integrity.status,
|
|
533
|
+
[ATTR.CORRUPTION_RECEIPT_VERSION]: receipt.receiptVersion,
|
|
534
|
+
[ATTR.CORRUPTION_RECEIPT_KIND]: receipt.kind,
|
|
535
|
+
[ATTR.CORRUPTION_SOURCE_PATH]: receipt.sourcePath,
|
|
536
|
+
[ATTR.CORRUPTION_LINE_NUMBER]: receipt.lineNumber,
|
|
537
|
+
[ATTR.CORRUPTION_BYTE_OFFSET]: receipt.byteOffset,
|
|
538
|
+
[ATTR.CORRUPTION_BYTE_LENGTH]: receipt.byteLength,
|
|
539
|
+
[ATTR.CORRUPTION_SHA256]: receipt.sha256,
|
|
540
|
+
[ATTR.RAW_SOURCE_RETENTION]: "local_source_only"
|
|
541
|
+
}
|
|
542
|
+
}));
|
|
543
|
+
existingSpanIds.add(spanId);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/json.ts
|
|
548
|
+
import { readdir, readFile } from "fs/promises";
|
|
549
|
+
var JsonSourceError = class extends Error {
|
|
550
|
+
sourcePath;
|
|
551
|
+
kind;
|
|
552
|
+
code;
|
|
553
|
+
constructor(sourcePath, kind, cause) {
|
|
554
|
+
super(
|
|
555
|
+
kind === "parse" ? `Invalid JSON at ${sourcePath}` : `Unable to read JSON source at ${sourcePath}`,
|
|
556
|
+
{ cause }
|
|
557
|
+
);
|
|
558
|
+
this.name = "JsonSourceError";
|
|
559
|
+
this.sourcePath = sourcePath;
|
|
560
|
+
this.kind = kind;
|
|
561
|
+
this.code = errorCode(cause);
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
function errorCode(error) {
|
|
565
|
+
return error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
566
|
+
}
|
|
567
|
+
function pathSafeParseCause(cause) {
|
|
568
|
+
if (!(cause instanceof Error)) return cause;
|
|
569
|
+
const location = cause.message.match(/(?:at position \d+(?: \(line \d+ column \d+\))?|line \d+ column \d+)/i)?.[0];
|
|
570
|
+
cause.message = location ? `JSON parse failed ${location}` : "JSON parse failed";
|
|
571
|
+
cause.stack = `${cause.name}: ${cause.message}`;
|
|
572
|
+
return cause;
|
|
573
|
+
}
|
|
574
|
+
function isMissingPathError(error) {
|
|
575
|
+
return errorCode(error) === "ENOENT";
|
|
576
|
+
}
|
|
577
|
+
function isMissingJsonSource(error) {
|
|
578
|
+
return error instanceof JsonSourceError && error.kind === "read" && isMissingPathError(error);
|
|
579
|
+
}
|
|
580
|
+
async function readJsonFile(path) {
|
|
581
|
+
let raw;
|
|
582
|
+
try {
|
|
583
|
+
raw = await readFile(path, "utf8");
|
|
584
|
+
} catch (error) {
|
|
585
|
+
throw new JsonSourceError(path, "read", error);
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
return JSON.parse(raw);
|
|
589
|
+
} catch (error) {
|
|
590
|
+
throw new JsonSourceError(path, "parse", pathSafeParseCause(error));
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
async function listJsonFiles(path) {
|
|
594
|
+
try {
|
|
595
|
+
return (await readdir(path)).filter((file) => file.endsWith(".json"));
|
|
596
|
+
} catch (error) {
|
|
597
|
+
throw new JsonSourceError(path, "read", error);
|
|
598
|
+
}
|
|
346
599
|
}
|
|
347
600
|
|
|
348
601
|
// src/adapters/amp.ts
|
|
349
|
-
import { readdir
|
|
602
|
+
import { readdir as readdir2, stat } from "fs/promises";
|
|
350
603
|
import { homedir } from "os";
|
|
351
|
-
import { basename, join
|
|
604
|
+
import { basename, join } from "path";
|
|
352
605
|
var SERVICE = "amp";
|
|
353
606
|
function textOf(content) {
|
|
354
607
|
if (typeof content === "string") return content.slice(0, CONTENT_CAP);
|
|
@@ -359,25 +612,27 @@ function textOf(content) {
|
|
|
359
612
|
var AmpAdapter = class {
|
|
360
613
|
harness = "amp";
|
|
361
614
|
threadsRoot() {
|
|
362
|
-
const base = process.env.XDG_DATA_HOME ??
|
|
363
|
-
return
|
|
615
|
+
const base = process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
|
|
616
|
+
return join(base, "amp", "threads");
|
|
364
617
|
}
|
|
365
618
|
async locate(opts = {}) {
|
|
366
619
|
let files;
|
|
367
620
|
try {
|
|
368
|
-
files = await
|
|
369
|
-
} catch {
|
|
370
|
-
return [];
|
|
621
|
+
files = await readdir2(this.threadsRoot());
|
|
622
|
+
} catch (error) {
|
|
623
|
+
if (isMissingPathError(error)) return [];
|
|
624
|
+
throw error;
|
|
371
625
|
}
|
|
372
626
|
const refs = [];
|
|
373
627
|
for (const f of files) {
|
|
374
628
|
if (!f.startsWith("T-") || !f.endsWith(".json")) continue;
|
|
375
|
-
const path =
|
|
629
|
+
const path = join(this.threadsRoot(), f);
|
|
376
630
|
let st;
|
|
377
631
|
try {
|
|
378
632
|
st = await stat(path);
|
|
379
|
-
} catch {
|
|
380
|
-
continue;
|
|
633
|
+
} catch (error) {
|
|
634
|
+
if (isMissingPathError(error)) continue;
|
|
635
|
+
throw error;
|
|
381
636
|
}
|
|
382
637
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
383
638
|
if (opts.cwd) continue;
|
|
@@ -386,12 +641,7 @@ var AmpAdapter = class {
|
|
|
386
641
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
387
642
|
}
|
|
388
643
|
async parse(ref) {
|
|
389
|
-
|
|
390
|
-
try {
|
|
391
|
-
thread = JSON.parse(await readFile2(ref.path, "utf8"));
|
|
392
|
-
} catch {
|
|
393
|
-
return [];
|
|
394
|
-
}
|
|
644
|
+
const thread = await readJsonFile(ref.path);
|
|
395
645
|
const traceId = thread.id ?? ref.sessionId;
|
|
396
646
|
const rootId = `root:${traceId}`;
|
|
397
647
|
const start = new Date(thread.created ?? 0).toISOString();
|
|
@@ -423,7 +673,6 @@ var AmpAdapter = class {
|
|
|
423
673
|
}
|
|
424
674
|
} else {
|
|
425
675
|
const u = m.usage;
|
|
426
|
-
const inputTokens2 = u ? (u.inputTokens ?? 0) + (u.cacheReadInputTokens ?? 0) + (u.cacheCreationInputTokens ?? 0) || null : null;
|
|
427
676
|
spans.push(
|
|
428
677
|
span({
|
|
429
678
|
traceId,
|
|
@@ -435,8 +684,10 @@ var AmpAdapter = class {
|
|
|
435
684
|
service: SERVICE,
|
|
436
685
|
agent: SERVICE,
|
|
437
686
|
model: u?.model ?? null,
|
|
438
|
-
inputTokens:
|
|
687
|
+
inputTokens: u?.inputTokens ?? null,
|
|
439
688
|
outputTokens: u?.outputTokens ?? null,
|
|
689
|
+
cachedInputTokens: u?.cacheReadInputTokens ?? null,
|
|
690
|
+
cacheWriteInputTokens: u?.cacheCreationInputTokens ?? null,
|
|
440
691
|
step,
|
|
441
692
|
content: textOf(m.content) || null
|
|
442
693
|
})
|
|
@@ -456,14 +707,17 @@ var AmpAdapter = class {
|
|
|
456
707
|
agent: SERVICE,
|
|
457
708
|
tool: b.name,
|
|
458
709
|
step,
|
|
459
|
-
|
|
710
|
+
extra: toolIoAttributes({ input: b.input })
|
|
460
711
|
});
|
|
461
712
|
spans.push(t);
|
|
462
713
|
if (b.id) toolByUseId.set(b.id, t);
|
|
463
714
|
step += 1;
|
|
464
715
|
} else if (b.type === "tool_result" && b.tool_use_id) {
|
|
465
716
|
const t = toolByUseId.get(b.tool_use_id);
|
|
466
|
-
if (t)
|
|
717
|
+
if (t) {
|
|
718
|
+
t.status = b.is_error === true ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
|
|
719
|
+
recordToolOutput(t, b.content ?? b.output);
|
|
720
|
+
}
|
|
467
721
|
}
|
|
468
722
|
}
|
|
469
723
|
}
|
|
@@ -471,6 +725,108 @@ var AmpAdapter = class {
|
|
|
471
725
|
}
|
|
472
726
|
};
|
|
473
727
|
|
|
728
|
+
// src/jsonl.ts
|
|
729
|
+
import { isUtf8 } from "buffer";
|
|
730
|
+
import { createHash as createHash3 } from "crypto";
|
|
731
|
+
import { createReadStream } from "fs";
|
|
732
|
+
var JsonlParseError = class extends SyntaxError {
|
|
733
|
+
sourcePath;
|
|
734
|
+
lineNumber;
|
|
735
|
+
byteOffset;
|
|
736
|
+
byteLength;
|
|
737
|
+
sha256;
|
|
738
|
+
receipt;
|
|
739
|
+
constructor(receipt) {
|
|
740
|
+
super(`Invalid JSONL at ${receipt.sourcePath}:${receipt.lineNumber}`);
|
|
741
|
+
this.name = "JsonlParseError";
|
|
742
|
+
this.sourcePath = receipt.sourcePath;
|
|
743
|
+
this.lineNumber = receipt.lineNumber;
|
|
744
|
+
this.byteOffset = receipt.byteOffset;
|
|
745
|
+
this.byteLength = receipt.byteLength;
|
|
746
|
+
this.sha256 = receipt.sha256;
|
|
747
|
+
this.receipt = receipt;
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
function corruptionReceipt(rawLine, path, lineNumber, byteOffset) {
|
|
751
|
+
return {
|
|
752
|
+
receiptVersion: 1,
|
|
753
|
+
kind: "jsonl_corruption",
|
|
754
|
+
status: "degraded_not_lossless",
|
|
755
|
+
sourcePath: path,
|
|
756
|
+
lineNumber,
|
|
757
|
+
byteOffset,
|
|
758
|
+
byteLength: rawLine.length,
|
|
759
|
+
sha256: createHash3("sha256").update(rawLine).digest("hex"),
|
|
760
|
+
rawBytes: "local_source_only"
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
function parseLine(rawLine, path, lineNumber, byteOffset, options) {
|
|
764
|
+
const jsonBytes = rawLine.at(-1) === 13 ? rawLine.subarray(0, -1) : rawLine;
|
|
765
|
+
if (jsonBytes.length === 0) return void 0;
|
|
766
|
+
if (!isUtf8(jsonBytes)) return handleCorruption(rawLine, path, lineNumber, byteOffset, options);
|
|
767
|
+
const json2 = jsonBytes.toString("utf8");
|
|
768
|
+
if (json2.trim().length === 0) return void 0;
|
|
769
|
+
try {
|
|
770
|
+
return JSON.parse(json2);
|
|
771
|
+
} catch {
|
|
772
|
+
return handleCorruption(rawLine, path, lineNumber, byteOffset, options);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function handleCorruption(rawLine, path, lineNumber, byteOffset, options) {
|
|
776
|
+
const receipt = corruptionReceipt(rawLine, path, lineNumber, byteOffset);
|
|
777
|
+
if (options.mode === "recover") {
|
|
778
|
+
options.onCorruption(receipt);
|
|
779
|
+
return void 0;
|
|
780
|
+
}
|
|
781
|
+
throw new JsonlParseError(receipt);
|
|
782
|
+
}
|
|
783
|
+
async function* readJsonl(path, options = { mode: "strict" }) {
|
|
784
|
+
if (options.mode === "recover" && typeof options.onCorruption !== "function") {
|
|
785
|
+
throw new TypeError("JSONL recover mode requires an onCorruption callback");
|
|
786
|
+
}
|
|
787
|
+
const input = createReadStream(path);
|
|
788
|
+
let lineNumber = 0;
|
|
789
|
+
let byteOffset = 0;
|
|
790
|
+
let fragments = [];
|
|
791
|
+
try {
|
|
792
|
+
for await (const chunk of input) {
|
|
793
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
794
|
+
let start = 0;
|
|
795
|
+
let newline = bytes.indexOf(10);
|
|
796
|
+
while (newline !== -1) {
|
|
797
|
+
fragments.push(bytes.subarray(start, newline));
|
|
798
|
+
const rawLine = fragments.length === 1 ? fragments[0] : Buffer.concat(fragments);
|
|
799
|
+
fragments = [];
|
|
800
|
+
lineNumber += 1;
|
|
801
|
+
const parsed = parseLine(rawLine, path, lineNumber, byteOffset, options);
|
|
802
|
+
if (parsed !== void 0) yield parsed;
|
|
803
|
+
byteOffset += rawLine.length + 1;
|
|
804
|
+
start = newline + 1;
|
|
805
|
+
newline = bytes.indexOf(10, start);
|
|
806
|
+
}
|
|
807
|
+
if (start < bytes.length) fragments.push(bytes.subarray(start));
|
|
808
|
+
}
|
|
809
|
+
if (fragments.length > 0) {
|
|
810
|
+
const rawLine = fragments.length === 1 ? fragments[0] : Buffer.concat(fragments);
|
|
811
|
+
lineNumber += 1;
|
|
812
|
+
const parsed = parseLine(rawLine, path, lineNumber, byteOffset, options);
|
|
813
|
+
if (parsed !== void 0) yield parsed;
|
|
814
|
+
}
|
|
815
|
+
} finally {
|
|
816
|
+
input.destroy();
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
async function takeJsonl(path, limit, options = { mode: "strict" }) {
|
|
820
|
+
if (!Number.isInteger(limit) || limit < 0) throw new RangeError("limit must be a non-negative integer");
|
|
821
|
+
const rows = [];
|
|
822
|
+
if (limit === 0) return rows;
|
|
823
|
+
for await (const row of readJsonl(path, options)) {
|
|
824
|
+
rows.push(row);
|
|
825
|
+
if (rows.length === limit) break;
|
|
826
|
+
}
|
|
827
|
+
return rows;
|
|
828
|
+
}
|
|
829
|
+
|
|
474
830
|
// src/adapters/actor.ts
|
|
475
831
|
var CMD_MARKERS = [
|
|
476
832
|
"<command-name>",
|
|
@@ -491,7 +847,7 @@ var INJECT_MARKERS = [
|
|
|
491
847
|
// Prefix match: Codex adds attributes such as source="goal" to this tag.
|
|
492
848
|
"<codex_internal_context",
|
|
493
849
|
"<subagent_notification>",
|
|
494
|
-
"# AGENTS.md instructions
|
|
850
|
+
"# AGENTS.md instructions",
|
|
495
851
|
"<local-command-stdout>",
|
|
496
852
|
"This session is being continued from a previous conversation",
|
|
497
853
|
// Slash-command skill bodies are expanded into a user turn (no <command-name>
|
|
@@ -564,26 +920,10 @@ function classifyReaction(text) {
|
|
|
564
920
|
var CORRECTIVE_REACTIONS = ["correction", "frustration", "jargon", "structure"];
|
|
565
921
|
|
|
566
922
|
// src/adapters/claude.ts
|
|
567
|
-
import { readdir as
|
|
923
|
+
import { readdir as readdir3, stat as stat2 } from "fs/promises";
|
|
568
924
|
import { homedir as homedir2 } from "os";
|
|
569
|
-
import { basename as basename2, join as
|
|
925
|
+
import { basename as basename2, join as join2 } from "path";
|
|
570
926
|
var SERVICE2 = "claude-code";
|
|
571
|
-
function parseLines(raw) {
|
|
572
|
-
const out = [];
|
|
573
|
-
for (const line of raw.split("\n")) {
|
|
574
|
-
if (!line) continue;
|
|
575
|
-
try {
|
|
576
|
-
out.push(JSON.parse(line));
|
|
577
|
-
} catch {
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
return out;
|
|
581
|
-
}
|
|
582
|
-
function inputTokens(u) {
|
|
583
|
-
if (!u) return null;
|
|
584
|
-
const v = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
|
|
585
|
-
return v > 0 ? v : null;
|
|
586
|
-
}
|
|
587
927
|
function asBlocks(content) {
|
|
588
928
|
return Array.isArray(content) ? content : [];
|
|
589
929
|
}
|
|
@@ -600,129 +940,140 @@ function stringifyToolResult(content) {
|
|
|
600
940
|
}
|
|
601
941
|
return "";
|
|
602
942
|
}
|
|
603
|
-
function
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
943
|
+
function createClaudeStream(startStep) {
|
|
944
|
+
return { spans: [], toolSpanByUseId: /* @__PURE__ */ new Map(), step: startStep, sawUserTurn: false };
|
|
945
|
+
}
|
|
946
|
+
function consumeClaudeEvent(ev, ctx, state) {
|
|
947
|
+
const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
948
|
+
const uid = ev.uuid ?? `${ctx.idPrefix}step${state.step}`;
|
|
949
|
+
if (ev.type === "assistant" && ev.message) {
|
|
950
|
+
const llmId = `${ctx.idPrefix}${uid}`;
|
|
951
|
+
state.spans.push(
|
|
952
|
+
span({
|
|
953
|
+
traceId: ctx.traceId,
|
|
954
|
+
spanId: llmId,
|
|
955
|
+
parentSpanId: ctx.rootParent,
|
|
956
|
+
name: "llm.turn",
|
|
957
|
+
kind: "LLM",
|
|
958
|
+
startTime: ts,
|
|
959
|
+
service: SERVICE2,
|
|
960
|
+
agent: ctx.agent,
|
|
961
|
+
model: ev.message.model ?? null,
|
|
962
|
+
inputTokens: ev.message.usage?.input_tokens ?? null,
|
|
963
|
+
outputTokens: ev.message.usage?.output_tokens ?? null,
|
|
964
|
+
cachedInputTokens: ev.message.usage?.cache_read_input_tokens ?? null,
|
|
965
|
+
cacheWriteInputTokens: ev.message.usage?.cache_creation_input_tokens ?? null,
|
|
966
|
+
step: state.step,
|
|
967
|
+
content: textOf2(ev.message.content) || null
|
|
968
|
+
})
|
|
969
|
+
);
|
|
970
|
+
state.step += 1;
|
|
971
|
+
let toolIdx = 0;
|
|
972
|
+
for (const block of asBlocks(ev.message.content)) {
|
|
973
|
+
if (block.type !== "tool_use" || !block.name) continue;
|
|
974
|
+
const toolSpan = span({
|
|
975
|
+
traceId: ctx.traceId,
|
|
976
|
+
spanId: `${ctx.idPrefix}${uid}:tool:${toolIdx}`,
|
|
977
|
+
parentSpanId: llmId,
|
|
978
|
+
name: `tool.${block.name}`,
|
|
979
|
+
kind: "TOOL",
|
|
980
|
+
startTime: ts,
|
|
981
|
+
service: SERVICE2,
|
|
982
|
+
agent: ctx.agent,
|
|
983
|
+
tool: block.name,
|
|
984
|
+
step: state.step,
|
|
985
|
+
extra: toolIoAttributes({ input: block.input })
|
|
986
|
+
});
|
|
987
|
+
state.spans.push(toolSpan);
|
|
988
|
+
if (block.id) state.toolSpanByUseId.set(block.id, toolSpan);
|
|
989
|
+
toolIdx += 1;
|
|
990
|
+
state.step += 1;
|
|
991
|
+
}
|
|
992
|
+
} else if (ev.type === "user" && ev.message) {
|
|
993
|
+
const prompt = textOf2(ev.message.content);
|
|
994
|
+
if (prompt) {
|
|
995
|
+
const actor = claudeActor({
|
|
996
|
+
text: prompt,
|
|
997
|
+
isSidechain: ev.isSidechain,
|
|
998
|
+
userType: ev.userType ?? null,
|
|
999
|
+
isFirstUserTurn: !state.sawUserTurn
|
|
1000
|
+
});
|
|
1001
|
+
state.sawUserTurn = true;
|
|
1002
|
+
state.spans.push(
|
|
1003
|
+
userPromptSpan({
|
|
615
1004
|
traceId: ctx.traceId,
|
|
616
|
-
spanId:
|
|
1005
|
+
spanId: `${ctx.idPrefix}${uid}:user`,
|
|
617
1006
|
parentSpanId: ctx.rootParent,
|
|
618
|
-
name: "llm.turn",
|
|
619
|
-
kind: "LLM",
|
|
620
1007
|
startTime: ts,
|
|
621
1008
|
service: SERVICE2,
|
|
622
1009
|
agent: ctx.agent,
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
step,
|
|
627
|
-
content: textOf2(ev.message.content) || null
|
|
1010
|
+
step: state.step,
|
|
1011
|
+
content: prompt,
|
|
1012
|
+
actor
|
|
628
1013
|
})
|
|
629
1014
|
);
|
|
630
|
-
step += 1;
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
startTime: ts,
|
|
641
|
-
service: SERVICE2,
|
|
642
|
-
agent: ctx.agent,
|
|
643
|
-
tool: block.name,
|
|
644
|
-
step,
|
|
645
|
-
content: block.input != null ? JSON.stringify(block.input) : null
|
|
646
|
-
});
|
|
647
|
-
spans.push(toolSpan);
|
|
648
|
-
if (block.id) toolSpanByUseId.set(block.id, toolSpan);
|
|
649
|
-
toolIdx += 1;
|
|
650
|
-
step += 1;
|
|
651
|
-
}
|
|
652
|
-
} else if (ev.type === "user" && ev.message) {
|
|
653
|
-
const prompt = textOf2(ev.message.content);
|
|
654
|
-
if (prompt) {
|
|
655
|
-
const actor = claudeActor({
|
|
656
|
-
text: prompt,
|
|
657
|
-
isSidechain: ev.isSidechain,
|
|
658
|
-
userType: ev.userType ?? null,
|
|
659
|
-
isFirstUserTurn: !sawUserTurn
|
|
660
|
-
});
|
|
661
|
-
sawUserTurn = true;
|
|
662
|
-
spans.push(
|
|
663
|
-
userPromptSpan({
|
|
664
|
-
traceId: ctx.traceId,
|
|
665
|
-
spanId: `${ctx.idPrefix}${uid}:user`,
|
|
666
|
-
parentSpanId: ctx.rootParent,
|
|
667
|
-
startTime: ts,
|
|
668
|
-
service: SERVICE2,
|
|
669
|
-
agent: ctx.agent,
|
|
670
|
-
step,
|
|
671
|
-
content: prompt,
|
|
672
|
-
actor
|
|
673
|
-
})
|
|
674
|
-
);
|
|
675
|
-
step += 1;
|
|
676
|
-
}
|
|
677
|
-
for (const block of asBlocks(ev.message.content)) {
|
|
678
|
-
if (block.type !== "tool_result" || !block.tool_use_id) continue;
|
|
679
|
-
backfillResult(toolSpanByUseId.get(block.tool_use_id), ts, block.is_error === true, stringifyToolResult(block.content));
|
|
680
|
-
}
|
|
681
|
-
} else if (ev.type === "attachment" && ev.attachment?.type === "tool_result" && ev.attachment.toolUseID) {
|
|
682
|
-
const err = typeof ev.attachment.exitCode === "number" && ev.attachment.exitCode !== 0;
|
|
683
|
-
backfillResult(toolSpanByUseId.get(ev.attachment.toolUseID), ts, err, ev.attachment.stderr ?? "");
|
|
1015
|
+
state.step += 1;
|
|
1016
|
+
}
|
|
1017
|
+
for (const block of asBlocks(ev.message.content)) {
|
|
1018
|
+
if (block.type !== "tool_result" || !block.tool_use_id) continue;
|
|
1019
|
+
backfillResult(
|
|
1020
|
+
state.toolSpanByUseId.get(block.tool_use_id),
|
|
1021
|
+
ts,
|
|
1022
|
+
block.is_error === true,
|
|
1023
|
+
block.content
|
|
1024
|
+
);
|
|
684
1025
|
}
|
|
1026
|
+
} else if (ev.type === "attachment" && ev.attachment?.type === "tool_result" && ev.attachment.toolUseID) {
|
|
1027
|
+
const err = typeof ev.attachment.exitCode === "number" && ev.attachment.exitCode !== 0;
|
|
1028
|
+
backfillResult(state.toolSpanByUseId.get(ev.attachment.toolUseID), ts, err, ev.attachment.stderr ?? "");
|
|
685
1029
|
}
|
|
686
|
-
return { spans, toolSpanByUseId, nextStep: step };
|
|
687
1030
|
}
|
|
688
|
-
function
|
|
1031
|
+
function finishClaudeStream(state) {
|
|
1032
|
+
return { spans: state.spans, toolSpanByUseId: state.toolSpanByUseId, nextStep: state.step };
|
|
1033
|
+
}
|
|
1034
|
+
function backfillResult(s, endTime, isError, output) {
|
|
689
1035
|
if (!s) return;
|
|
690
1036
|
s.end_time = endTime;
|
|
691
1037
|
const code = isError ? "ERROR" : "OK";
|
|
692
1038
|
s.status = { code };
|
|
1039
|
+
recordToolOutput(s, output);
|
|
1040
|
+
const message = stringifyToolResult(output);
|
|
693
1041
|
if (isError && message) s.status.message = message.slice(0, 500);
|
|
694
1042
|
}
|
|
695
1043
|
var ClaudeAdapter = class {
|
|
696
1044
|
harness = "claude-code";
|
|
697
1045
|
aliases = ["claude", "claudish", "openclaw", "nanoclaw"];
|
|
698
1046
|
root() {
|
|
699
|
-
return
|
|
1047
|
+
return join2(homedir2(), ".claude", "projects");
|
|
700
1048
|
}
|
|
701
1049
|
async locate(opts = {}) {
|
|
702
1050
|
const root = this.root();
|
|
703
1051
|
let projectDirs;
|
|
704
1052
|
try {
|
|
705
|
-
projectDirs = await
|
|
706
|
-
} catch {
|
|
707
|
-
return [];
|
|
1053
|
+
projectDirs = await readdir3(root);
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
if (isMissingPathError(error)) return [];
|
|
1056
|
+
throw error;
|
|
708
1057
|
}
|
|
709
1058
|
const refs = [];
|
|
710
1059
|
for (const dir of projectDirs) {
|
|
711
|
-
const dirPath =
|
|
1060
|
+
const dirPath = join2(root, dir);
|
|
712
1061
|
let files;
|
|
713
1062
|
try {
|
|
714
|
-
files = await
|
|
715
|
-
} catch {
|
|
716
|
-
continue;
|
|
1063
|
+
files = await readdir3(dirPath);
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
if (isMissingPathError(error)) continue;
|
|
1066
|
+
throw error;
|
|
717
1067
|
}
|
|
718
1068
|
for (const f of files) {
|
|
719
1069
|
if (!f.endsWith(".jsonl")) continue;
|
|
720
|
-
const path =
|
|
1070
|
+
const path = join2(dirPath, f);
|
|
721
1071
|
let st;
|
|
722
1072
|
try {
|
|
723
1073
|
st = await stat2(path);
|
|
724
|
-
} catch {
|
|
725
|
-
continue;
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
if (isMissingPathError(error)) continue;
|
|
1076
|
+
throw error;
|
|
726
1077
|
}
|
|
727
1078
|
if (!st.isFile()) continue;
|
|
728
1079
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
@@ -733,11 +1084,37 @@ var ClaudeAdapter = class {
|
|
|
733
1084
|
}
|
|
734
1085
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
735
1086
|
}
|
|
736
|
-
async parse(ref) {
|
|
737
|
-
const
|
|
738
|
-
const
|
|
739
|
-
const
|
|
1087
|
+
async parse(ref, options = {}) {
|
|
1088
|
+
const sourceTraceId = ref.sessionId;
|
|
1089
|
+
const sourceRootId = `root:${sourceTraceId}`;
|
|
1090
|
+
const ctx = {
|
|
1091
|
+
traceId: sourceTraceId,
|
|
1092
|
+
agent: SERVICE2,
|
|
1093
|
+
startStep: 0,
|
|
1094
|
+
idPrefix: "",
|
|
1095
|
+
rootParent: sourceRootId
|
|
1096
|
+
};
|
|
1097
|
+
const state = createClaudeStream(ctx.startStep);
|
|
1098
|
+
let discoveredTraceId;
|
|
1099
|
+
let firstTimestamp;
|
|
1100
|
+
let lastTimestamp;
|
|
1101
|
+
let sawEvent = false;
|
|
1102
|
+
for await (const event of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
|
|
1103
|
+
if (!sawEvent) {
|
|
1104
|
+
firstTimestamp = event.timestamp;
|
|
1105
|
+
sawEvent = true;
|
|
1106
|
+
}
|
|
1107
|
+
lastTimestamp = event.timestamp;
|
|
1108
|
+
if (!discoveredTraceId && event.sessionId) discoveredTraceId = event.sessionId;
|
|
1109
|
+
consumeClaudeEvent(event, ctx, state);
|
|
1110
|
+
}
|
|
1111
|
+
const main = finishClaudeStream(state);
|
|
1112
|
+
const traceId = discoveredTraceId ?? sourceTraceId;
|
|
740
1113
|
const rootId = `root:${traceId}`;
|
|
1114
|
+
for (const item of main.spans) {
|
|
1115
|
+
item.trace_id = traceId;
|
|
1116
|
+
if (item.parent_span_id === sourceRootId) item.parent_span_id = rootId;
|
|
1117
|
+
}
|
|
741
1118
|
const spans = [
|
|
742
1119
|
span({
|
|
743
1120
|
traceId,
|
|
@@ -745,77 +1122,65 @@ var ClaudeAdapter = class {
|
|
|
745
1122
|
parentSpanId: null,
|
|
746
1123
|
name: "session",
|
|
747
1124
|
kind: "AGENT",
|
|
748
|
-
startTime:
|
|
749
|
-
endTime:
|
|
1125
|
+
startTime: firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1126
|
+
endTime: lastTimestamp,
|
|
750
1127
|
service: SERVICE2,
|
|
751
1128
|
agent: SERVICE2
|
|
752
1129
|
})
|
|
753
1130
|
];
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
agent: SERVICE2,
|
|
757
|
-
startStep: 0,
|
|
758
|
-
idPrefix: "",
|
|
759
|
-
rootParent: rootId
|
|
760
|
-
});
|
|
761
|
-
spans.push(...main.spans);
|
|
762
|
-
await this.foldSubagents(ref, traceId, main, spans);
|
|
1131
|
+
appendAll(spans, main.spans);
|
|
1132
|
+
await this.foldSubagents(ref, traceId, main, spans, options);
|
|
763
1133
|
return spans;
|
|
764
1134
|
}
|
|
765
1135
|
/** Parse `<session>/subagents/agent-*.jsonl`, parenting each under its Agent call. */
|
|
766
|
-
async foldSubagents(ref, traceId, main, out) {
|
|
767
|
-
const subDir =
|
|
1136
|
+
async foldSubagents(ref, traceId, main, out, options) {
|
|
1137
|
+
const subDir = join2(ref.path.replace(/\.jsonl$/, ""), "subagents");
|
|
768
1138
|
let files;
|
|
769
1139
|
try {
|
|
770
|
-
files = await
|
|
771
|
-
} catch {
|
|
772
|
-
return;
|
|
1140
|
+
files = await readdir3(subDir);
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
if (isMissingPathError(error)) return;
|
|
1143
|
+
throw error;
|
|
773
1144
|
}
|
|
774
1145
|
let step = main.nextStep;
|
|
775
|
-
for (const
|
|
776
|
-
if (!
|
|
777
|
-
const hash2 = basename2(
|
|
1146
|
+
for (const file of files) {
|
|
1147
|
+
if (!file.endsWith(".jsonl")) continue;
|
|
1148
|
+
const hash2 = basename2(file, ".jsonl");
|
|
1149
|
+
const metaPath = join2(subDir, `${hash2}.meta.json`);
|
|
778
1150
|
let meta = {};
|
|
779
1151
|
try {
|
|
780
|
-
meta =
|
|
781
|
-
} catch {
|
|
1152
|
+
meta = await readJsonFile(metaPath);
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
if (!isMissingJsonSource(error)) throw error;
|
|
782
1155
|
}
|
|
783
1156
|
const parent = meta.toolUseId && main.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
|
|
784
|
-
|
|
785
|
-
try {
|
|
786
|
-
raw = await readFile3(join3(subDir, f), "utf8");
|
|
787
|
-
} catch {
|
|
788
|
-
continue;
|
|
789
|
-
}
|
|
790
|
-
const parsed = parseClaudeStream(parseLines(raw), {
|
|
1157
|
+
const ctx = {
|
|
791
1158
|
traceId,
|
|
792
1159
|
agent: meta.agentType ? `subagent:${meta.agentType}` : "subagent",
|
|
793
1160
|
startStep: step,
|
|
794
1161
|
idPrefix: `${hash2}:`,
|
|
795
1162
|
rootParent: parent
|
|
796
|
-
}
|
|
797
|
-
|
|
1163
|
+
};
|
|
1164
|
+
const state = createClaudeStream(ctx.startStep);
|
|
1165
|
+
for await (const event of readJsonl(
|
|
1166
|
+
join2(subDir, file),
|
|
1167
|
+
sessionJsonlOptions(ref, options)
|
|
1168
|
+
)) {
|
|
1169
|
+
consumeClaudeEvent(event, ctx, state);
|
|
1170
|
+
}
|
|
1171
|
+
const parsed = finishClaudeStream(state);
|
|
1172
|
+
appendAll(out, parsed.spans);
|
|
798
1173
|
step = parsed.nextStep;
|
|
799
1174
|
}
|
|
800
1175
|
}
|
|
801
1176
|
};
|
|
802
1177
|
|
|
803
1178
|
// src/adapters/codex.ts
|
|
804
|
-
import { readdir as
|
|
1179
|
+
import { readdir as readdir4, stat as stat3 } from "fs/promises";
|
|
805
1180
|
import { homedir as homedir3 } from "os";
|
|
806
|
-
import { basename as basename3, join as
|
|
1181
|
+
import { basename as basename3, join as join3 } from "path";
|
|
807
1182
|
var SERVICE3 = "codex";
|
|
808
|
-
|
|
809
|
-
const out = [];
|
|
810
|
-
for (const line of raw.split("\n")) {
|
|
811
|
-
if (!line) continue;
|
|
812
|
-
try {
|
|
813
|
-
out.push(JSON.parse(line));
|
|
814
|
-
} catch {
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
return out;
|
|
818
|
-
}
|
|
1183
|
+
var SESSION_HEAD_LINES = 40;
|
|
819
1184
|
function contentToString(content) {
|
|
820
1185
|
if (typeof content === "string") return content;
|
|
821
1186
|
if (Array.isArray(content)) {
|
|
@@ -826,10 +1191,82 @@ function contentToString(content) {
|
|
|
826
1191
|
function textOf3(content) {
|
|
827
1192
|
return capText(contentToString(content));
|
|
828
1193
|
}
|
|
1194
|
+
function currentTaskStartedAt(meta) {
|
|
1195
|
+
const timestamp = Date.parse(meta?.payload?.timestamp ?? meta?.timestamp ?? "");
|
|
1196
|
+
return Number.isFinite(timestamp) ? Math.floor(timestamp / 1e3) : void 0;
|
|
1197
|
+
}
|
|
1198
|
+
function isCurrentTaskStart(line, startedAt) {
|
|
1199
|
+
return line.type === "event_msg" && line.payload?.type === "task_started" && typeof line.payload.started_at === "number" && Math.abs(line.payload.started_at - startedAt) <= 2;
|
|
1200
|
+
}
|
|
1201
|
+
async function hasCurrentTaskStart(path, options, startedAt) {
|
|
1202
|
+
for await (const line of readJsonl(path, options)) {
|
|
1203
|
+
if (isCurrentTaskStart(line, startedAt)) return true;
|
|
1204
|
+
}
|
|
1205
|
+
return false;
|
|
1206
|
+
}
|
|
1207
|
+
function numericStatus(value) {
|
|
1208
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1209
|
+
if (typeof value === "string" && /^-?\d+$/.test(value.trim())) return Number(value);
|
|
1210
|
+
return void 0;
|
|
1211
|
+
}
|
|
1212
|
+
function explicitOutputError(value) {
|
|
1213
|
+
if (Array.isArray(value)) {
|
|
1214
|
+
let observedSuccess = false;
|
|
1215
|
+
for (const item of value) {
|
|
1216
|
+
const status = explicitOutputError(item);
|
|
1217
|
+
if (status === true) return true;
|
|
1218
|
+
if (status === false) observedSuccess = true;
|
|
1219
|
+
}
|
|
1220
|
+
return observedSuccess ? false : void 0;
|
|
1221
|
+
}
|
|
1222
|
+
if (value && typeof value === "object") {
|
|
1223
|
+
const row = value;
|
|
1224
|
+
for (const key of ["exit_code", "exitCode"]) {
|
|
1225
|
+
const code = numericStatus(row[key]);
|
|
1226
|
+
if (code !== void 0 && ("output" in row || "chunk_id" in row || "wall_time_seconds" in row)) {
|
|
1227
|
+
return code !== 0;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
for (const key of ["timed_out", "timedOut"]) {
|
|
1231
|
+
if (row[key] === true) return true;
|
|
1232
|
+
}
|
|
1233
|
+
if (typeof row.succeeded === "boolean" && ("value" in row || "error" in row)) {
|
|
1234
|
+
return !row.succeeded;
|
|
1235
|
+
}
|
|
1236
|
+
if ((row.type === "input_text" || row.type === "text") && typeof row.text === "string") {
|
|
1237
|
+
return explicitOutputError(row.text);
|
|
1238
|
+
}
|
|
1239
|
+
return void 0;
|
|
1240
|
+
}
|
|
1241
|
+
if (typeof value !== "string") return void 0;
|
|
1242
|
+
const text = value.trim();
|
|
1243
|
+
if (!text) return void 0;
|
|
1244
|
+
if (text.startsWith("{") && text.endsWith("}") || text.startsWith("[") && text.endsWith("]")) {
|
|
1245
|
+
try {
|
|
1246
|
+
const parsedStatus = explicitOutputError(JSON.parse(text));
|
|
1247
|
+
if (parsedStatus !== void 0) return parsedStatus;
|
|
1248
|
+
} catch {
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
const outputStart = text.indexOf("\nOutput:\n");
|
|
1252
|
+
const header = outputStart >= 0 ? text.slice(0, outputStart) : text;
|
|
1253
|
+
if (/^Chunk ID:/i.test(header)) {
|
|
1254
|
+
const exitCode = header.match(/^Process exited with code\s+(-?\d+)\b/im)?.[1];
|
|
1255
|
+
if (exitCode !== void 0) return Number(exitCode) !== 0;
|
|
1256
|
+
}
|
|
1257
|
+
if (/^Script completed\s*\nWall time:/i.test(header)) return false;
|
|
1258
|
+
if (/^Script failed\s*\nWall time:/i.test(header)) return true;
|
|
1259
|
+
const scriptExitCode = header.match(/^Script error:\s*\nExit code:\s*(-?\d+)\b/im)?.[1];
|
|
1260
|
+
if (scriptExitCode !== void 0) return Number(scriptExitCode) !== 0;
|
|
1261
|
+
const commandExitCode = text.match(/^Command failed with exit code\s+(-?\d+)\.?$/i)?.[1];
|
|
1262
|
+
if (commandExitCode !== void 0) return Number(commandExitCode) !== 0;
|
|
1263
|
+
if (/^<tool_error>[\s\S]*<\/tool_error>$/i.test(text)) return true;
|
|
1264
|
+
return void 0;
|
|
1265
|
+
}
|
|
829
1266
|
function outputIsError(output) {
|
|
830
|
-
const
|
|
831
|
-
const
|
|
832
|
-
return { error, message: error ?
|
|
1267
|
+
const error = explicitOutputError(output) === true;
|
|
1268
|
+
const message = typeof output === "string" ? output : JSON.stringify(output ?? "");
|
|
1269
|
+
return { error, message: error ? message.slice(0, 500) : "" };
|
|
833
1270
|
}
|
|
834
1271
|
function singleNestedToolName(input) {
|
|
835
1272
|
if (!input) return null;
|
|
@@ -842,6 +1279,28 @@ function toolInputToString(input) {
|
|
|
842
1279
|
if (input == null) return void 0;
|
|
843
1280
|
return JSON.stringify(input);
|
|
844
1281
|
}
|
|
1282
|
+
var CODEX_SESSION_ID = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
|
|
1283
|
+
function sessionIds(value) {
|
|
1284
|
+
return [...new Set(contentToString(value).match(CODEX_SESSION_ID) ?? [])];
|
|
1285
|
+
}
|
|
1286
|
+
function spawnedSessionIds(output) {
|
|
1287
|
+
const text = contentToString(output);
|
|
1288
|
+
const ids = [...text.matchAll(/["']?agent_id["']?\s*:\s*["']([0-9a-f-]{36})["']/gi)].map((match) => match[1]).filter((id) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id));
|
|
1289
|
+
return [...new Set(ids)];
|
|
1290
|
+
}
|
|
1291
|
+
function multiAgentOperation(name) {
|
|
1292
|
+
const prefix = "multi_agent_v1__";
|
|
1293
|
+
return name.startsWith(prefix) ? name.slice(prefix.length) : null;
|
|
1294
|
+
}
|
|
1295
|
+
function setAgentSessionIds(toolSpan, ids) {
|
|
1296
|
+
if (ids.length === 0) return;
|
|
1297
|
+
const unique = [...new Set(ids)];
|
|
1298
|
+
toolSpan.attributes["traces.codex.agent_session_ids"] = JSON.stringify(unique);
|
|
1299
|
+
toolSpan.attributes["traces.codex.agent_session_count"] = unique.length;
|
|
1300
|
+
if (toolSpan.attributes["traces.codex.agent_operation"] === "spawn_agent") {
|
|
1301
|
+
toolSpan.attributes["traces.child_session_ids"] = JSON.stringify(unique);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
845
1304
|
var verificationCommand = /\b(?:pnpm|npm|yarn|bun)\s+(?:run\s+)?(?:test|typecheck|lint|build|check)(?::[A-Za-z0-9:_-]+)?\b|\b(?:vitest|jest|pytest|tsc|biome|eslint|sha256sum|pdfinfo|pdftotext)\b|\bgo\s+test\b|\bcargo\s+(?:test|check|clippy|build)\b|\bgit\s+(?:status|diff|show|merge-tree)\b|\bgh-drew\s+pr\s+(?:view|checks)\b/i;
|
|
846
1305
|
function hasReadOnlyCurl(input) {
|
|
847
1306
|
if (!/\bcurl\b/i.test(input)) return false;
|
|
@@ -861,36 +1320,40 @@ function isExpectedBlockingTool(name, input) {
|
|
|
861
1320
|
async function* walkRollouts(root) {
|
|
862
1321
|
let years;
|
|
863
1322
|
try {
|
|
864
|
-
years = await
|
|
865
|
-
} catch {
|
|
866
|
-
return;
|
|
1323
|
+
years = await readdir4(root);
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
if (isMissingPathError(error)) return;
|
|
1326
|
+
throw error;
|
|
867
1327
|
}
|
|
868
1328
|
for (const y of years) {
|
|
869
|
-
const yp =
|
|
1329
|
+
const yp = join3(root, y);
|
|
870
1330
|
let months;
|
|
871
1331
|
try {
|
|
872
|
-
months = await
|
|
873
|
-
} catch {
|
|
874
|
-
continue;
|
|
1332
|
+
months = await readdir4(yp);
|
|
1333
|
+
} catch (error) {
|
|
1334
|
+
if (isMissingPathError(error)) continue;
|
|
1335
|
+
throw error;
|
|
875
1336
|
}
|
|
876
1337
|
for (const m of months) {
|
|
877
|
-
const mp =
|
|
1338
|
+
const mp = join3(yp, m);
|
|
878
1339
|
let days;
|
|
879
1340
|
try {
|
|
880
|
-
days = await
|
|
881
|
-
} catch {
|
|
882
|
-
continue;
|
|
1341
|
+
days = await readdir4(mp);
|
|
1342
|
+
} catch (error) {
|
|
1343
|
+
if (isMissingPathError(error)) continue;
|
|
1344
|
+
throw error;
|
|
883
1345
|
}
|
|
884
1346
|
for (const d of days) {
|
|
885
|
-
const dp =
|
|
1347
|
+
const dp = join3(mp, d);
|
|
886
1348
|
let files;
|
|
887
1349
|
try {
|
|
888
|
-
files = await
|
|
889
|
-
} catch {
|
|
890
|
-
continue;
|
|
1350
|
+
files = await readdir4(dp);
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
if (isMissingPathError(error)) continue;
|
|
1353
|
+
throw error;
|
|
891
1354
|
}
|
|
892
1355
|
for (const f of files) {
|
|
893
|
-
if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield
|
|
1356
|
+
if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join3(dp, f);
|
|
894
1357
|
}
|
|
895
1358
|
}
|
|
896
1359
|
}
|
|
@@ -900,7 +1363,7 @@ var CodexAdapter = class {
|
|
|
900
1363
|
harness = "codex";
|
|
901
1364
|
aliases = ["codex-acp"];
|
|
902
1365
|
root() {
|
|
903
|
-
return process.env.CODEX_HOME ?
|
|
1366
|
+
return process.env.CODEX_HOME ? join3(process.env.CODEX_HOME, "sessions") : join3(homedir3(), ".codex", "sessions");
|
|
904
1367
|
}
|
|
905
1368
|
async locate(opts = {}) {
|
|
906
1369
|
const refs = [];
|
|
@@ -908,75 +1371,113 @@ var CodexAdapter = class {
|
|
|
908
1371
|
let st;
|
|
909
1372
|
try {
|
|
910
1373
|
st = await stat3(path);
|
|
911
|
-
} catch {
|
|
912
|
-
continue;
|
|
1374
|
+
} catch (error) {
|
|
1375
|
+
if (isMissingPathError(error)) continue;
|
|
1376
|
+
throw error;
|
|
913
1377
|
}
|
|
914
1378
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
915
|
-
const head = (await readFile4(path, "utf8").catch(() => "")).split("\n", 40);
|
|
916
1379
|
let cwd = null;
|
|
917
1380
|
let id = basename3(path).replace(/^rollout-[\dT-]+-/, "").replace(/\.jsonl$/, "");
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
if (!cwd && l.payload?.cwd) cwd = l.payload.cwd;
|
|
924
|
-
} catch {
|
|
925
|
-
}
|
|
1381
|
+
const ref = { harness: this.harness, sessionId: id, path, cwd, mtimeMs: st.mtimeMs };
|
|
1382
|
+
const head = await takeJsonl(path, SESSION_HEAD_LINES, sessionJsonlOptions(ref));
|
|
1383
|
+
for (const parsed of head) {
|
|
1384
|
+
if (parsed.type === "session_meta" && parsed.payload?.id) id = parsed.payload.id;
|
|
1385
|
+
if (!cwd && parsed.payload?.cwd) cwd = parsed.payload.cwd;
|
|
926
1386
|
if (cwd) break;
|
|
927
1387
|
}
|
|
928
|
-
|
|
929
|
-
|
|
1388
|
+
ref.sessionId = id;
|
|
1389
|
+
ref.cwd = cwd;
|
|
1390
|
+
if (ref.integrity) {
|
|
1391
|
+
ref.integrity.corruptions = ref.integrity.corruptions.map((receipt) => ({ ...receipt, sessionId: id }));
|
|
1392
|
+
}
|
|
1393
|
+
if (opts.cwd && cwd && !cwd.startsWith(opts.cwd)) continue;
|
|
1394
|
+
if (opts.cwd && !cwd && !ref.integrity) continue;
|
|
1395
|
+
refs.push(ref);
|
|
930
1396
|
}
|
|
931
1397
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
932
1398
|
}
|
|
933
|
-
async parse(ref) {
|
|
934
|
-
const
|
|
935
|
-
const
|
|
1399
|
+
async parse(ref, options = {}) {
|
|
1400
|
+
const jsonl2 = sessionJsonlOptions(ref, options);
|
|
1401
|
+
const head = await takeJsonl(ref.path, SESSION_HEAD_LINES, jsonl2);
|
|
1402
|
+
let first = head[0];
|
|
1403
|
+
let meta = head.find((line) => line.type === "session_meta");
|
|
1404
|
+
const taskStartedAt = currentTaskStartedAt(meta);
|
|
1405
|
+
const hasTaskBoundary = taskStartedAt !== void 0 && await hasCurrentTaskStart(ref.path, jsonl2, taskStartedAt);
|
|
1406
|
+
let model = hasTaskBoundary ? null : head.find((line) => line.type === "turn_context")?.payload?.model ?? null;
|
|
1407
|
+
if (!first || !meta) {
|
|
1408
|
+
for await (const line of readJsonl(ref.path, jsonl2)) {
|
|
1409
|
+
first ??= line;
|
|
1410
|
+
if (!meta && line.type === "session_meta") meta = line;
|
|
1411
|
+
if (!model && line.type === "turn_context") model = line.payload?.model ?? null;
|
|
1412
|
+
if (first && meta) break;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
936
1415
|
const traceId = meta?.payload?.id ?? ref.sessionId;
|
|
937
|
-
const
|
|
1416
|
+
const spawnMeta = meta?.payload?.source?.subagent?.thread_spawn;
|
|
1417
|
+
const parentSessionId = meta?.payload?.parent_thread_id ?? spawnMeta?.parent_thread_id;
|
|
1418
|
+
const sessionRole = parentSessionId || meta?.payload?.thread_source === "subagent" ? "child" : "operator";
|
|
938
1419
|
const rootId = `root:${traceId}`;
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1420
|
+
const root = span({
|
|
1421
|
+
traceId,
|
|
1422
|
+
spanId: rootId,
|
|
1423
|
+
parentSpanId: null,
|
|
1424
|
+
name: "session",
|
|
1425
|
+
kind: "AGENT",
|
|
1426
|
+
startTime: meta?.timestamp ?? first?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1427
|
+
service: SERVICE3,
|
|
1428
|
+
agent: SERVICE3,
|
|
1429
|
+
model,
|
|
1430
|
+
extra: {
|
|
1431
|
+
"traces.session.role": sessionRole,
|
|
1432
|
+
...parentSessionId ? { "traces.parent_session_id": parentSessionId } : {},
|
|
1433
|
+
...spawnMeta?.depth != null ? { "traces.codex.agent_depth": spawnMeta.depth } : {},
|
|
1434
|
+
...meta?.payload?.agent_nickname ?? spawnMeta?.agent_nickname ? { "traces.codex.agent_nickname": meta?.payload?.agent_nickname ?? spawnMeta?.agent_nickname } : {},
|
|
1435
|
+
...meta?.payload?.agent_role ?? spawnMeta?.agent_role ? { "traces.codex.agent_role": meta?.payload?.agent_role ?? spawnMeta?.agent_role } : {},
|
|
1436
|
+
...spawnMeta?.agent_path ? { "traces.codex.agent_path": spawnMeta.agent_path } : {}
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
const spans = [root];
|
|
953
1440
|
const toolByCallId = /* @__PURE__ */ new Map();
|
|
954
1441
|
const subagentByThreadId = /* @__PURE__ */ new Map();
|
|
955
1442
|
let step = 0;
|
|
956
1443
|
let lastLlm = rootId;
|
|
957
1444
|
let sawUserTurn = false;
|
|
958
|
-
|
|
1445
|
+
let lastTimestamp;
|
|
1446
|
+
const awaitingModel = model ? [] : [root];
|
|
1447
|
+
let reachedCurrentTask = !hasTaskBoundary;
|
|
1448
|
+
for await (const l of readJsonl(ref.path, jsonl2)) {
|
|
1449
|
+
if (!reachedCurrentTask) {
|
|
1450
|
+
if (isCurrentTaskStart(l, taskStartedAt)) reachedCurrentTask = true;
|
|
1451
|
+
else continue;
|
|
1452
|
+
}
|
|
1453
|
+
lastTimestamp = l.timestamp;
|
|
959
1454
|
const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
960
|
-
if (l.type === "
|
|
1455
|
+
if (!model && l.type === "turn_context" && l.payload?.model) {
|
|
1456
|
+
model = l.payload.model;
|
|
1457
|
+
for (const pending of awaitingModel) pending.attributes["llm.model_name"] = model;
|
|
1458
|
+
awaitingModel.length = 0;
|
|
1459
|
+
} else if (l.type === "event_msg" && l.payload?.type === "token_count") {
|
|
961
1460
|
const u = l.payload.info?.last_token_usage;
|
|
962
1461
|
if (u && (u.input_tokens || u.output_tokens)) {
|
|
963
1462
|
const id = `llm:${step}`;
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
);
|
|
1463
|
+
const llm = span({
|
|
1464
|
+
traceId,
|
|
1465
|
+
spanId: id,
|
|
1466
|
+
parentSpanId: rootId,
|
|
1467
|
+
name: "llm.turn",
|
|
1468
|
+
kind: "LLM",
|
|
1469
|
+
startTime: ts,
|
|
1470
|
+
service: SERVICE3,
|
|
1471
|
+
agent: SERVICE3,
|
|
1472
|
+
model,
|
|
1473
|
+
inputTokens: u.input_tokens ?? null,
|
|
1474
|
+
outputTokens: u.output_tokens ?? null,
|
|
1475
|
+
reasoningTokens: u.reasoning_output_tokens ?? null,
|
|
1476
|
+
cachedInputTokens: u.cached_input_tokens ?? null,
|
|
1477
|
+
step
|
|
1478
|
+
});
|
|
1479
|
+
spans.push(llm);
|
|
1480
|
+
if (!model) awaitingModel.push(llm);
|
|
980
1481
|
lastLlm = id;
|
|
981
1482
|
step += 1;
|
|
982
1483
|
}
|
|
@@ -988,6 +1489,7 @@ var CodexAdapter = class {
|
|
|
988
1489
|
);
|
|
989
1490
|
const nestedName = l.payload.type === "custom_tool_call" ? singleNestedToolName(input) : null;
|
|
990
1491
|
const name = classifyNestedTool(nestedName ?? outerName, input);
|
|
1492
|
+
const agentOperation = multiAgentOperation(nestedName ?? outerName);
|
|
991
1493
|
const toolSpan = span({
|
|
992
1494
|
traceId,
|
|
993
1495
|
spanId: `tool:${callId}`,
|
|
@@ -999,14 +1501,16 @@ var CodexAdapter = class {
|
|
|
999
1501
|
agent: SERVICE3,
|
|
1000
1502
|
tool: name,
|
|
1001
1503
|
step,
|
|
1002
|
-
content: input ?? null,
|
|
1003
1504
|
extra: {
|
|
1505
|
+
...toolIoAttributes({ input }),
|
|
1004
1506
|
"traces.codex.call_type": l.payload.type,
|
|
1005
1507
|
...name !== outerName ? { "traces.codex.outer_tool_name": outerName } : {},
|
|
1006
1508
|
...nestedName ? { "traces.codex.nested_tool_name": nestedName } : {},
|
|
1007
|
-
...isExpectedBlockingTool(name, input) ? { "traces.expected_blocking": true } : {}
|
|
1509
|
+
...isExpectedBlockingTool(name, input) ? { "traces.expected_blocking": true } : {},
|
|
1510
|
+
...agentOperation ? { "traces.codex.agent_operation": agentOperation } : {}
|
|
1008
1511
|
}
|
|
1009
1512
|
});
|
|
1513
|
+
if (agentOperation && agentOperation !== "spawn_agent") setAgentSessionIds(toolSpan, sessionIds(input));
|
|
1010
1514
|
spans.push(toolSpan);
|
|
1011
1515
|
toolByCallId.set(callId, toolSpan);
|
|
1012
1516
|
step += 1;
|
|
@@ -1016,6 +1520,10 @@ var CodexAdapter = class {
|
|
|
1016
1520
|
const { error, message } = outputIsError(l.payload.output);
|
|
1017
1521
|
t.end_time = ts;
|
|
1018
1522
|
t.status = error ? { code: "ERROR", message } : { code: "OK" };
|
|
1523
|
+
recordToolOutput(t, l.payload.output);
|
|
1524
|
+
if (t.attributes["traces.codex.agent_operation"] === "spawn_agent") {
|
|
1525
|
+
setAgentSessionIds(t, spawnedSessionIds(l.payload.output));
|
|
1526
|
+
}
|
|
1019
1527
|
}
|
|
1020
1528
|
} else if (l.type === "event_msg" && l.payload?.type === "sub_agent_activity") {
|
|
1021
1529
|
const threadId = l.payload.agent_thread_id;
|
|
@@ -1035,12 +1543,14 @@ var CodexAdapter = class {
|
|
|
1035
1543
|
agent: SERVICE3,
|
|
1036
1544
|
tool: "Agent",
|
|
1037
1545
|
step,
|
|
1038
|
-
content: JSON.stringify({
|
|
1039
|
-
subagent_type: subagentType,
|
|
1040
|
-
agent_path: agentPath,
|
|
1041
|
-
agent_thread_id: threadId
|
|
1042
|
-
}),
|
|
1043
1546
|
extra: {
|
|
1547
|
+
...toolIoAttributes({
|
|
1548
|
+
input: {
|
|
1549
|
+
subagent_type: subagentType,
|
|
1550
|
+
agent_path: agentPath,
|
|
1551
|
+
agent_thread_id: threadId
|
|
1552
|
+
}
|
|
1553
|
+
}),
|
|
1044
1554
|
"traces.codex.subagent_path": agentPath,
|
|
1045
1555
|
"traces.codex.subagent_thread_id": threadId
|
|
1046
1556
|
}
|
|
@@ -1102,47 +1612,39 @@ var CodexAdapter = class {
|
|
|
1102
1612
|
}
|
|
1103
1613
|
}
|
|
1104
1614
|
}
|
|
1615
|
+
root.end_time = lastTimestamp ?? root.start_time;
|
|
1105
1616
|
return spans;
|
|
1106
1617
|
}
|
|
1107
1618
|
};
|
|
1108
1619
|
|
|
1109
1620
|
// src/adapters/copilot.ts
|
|
1110
|
-
import { readdir as
|
|
1621
|
+
import { readdir as readdir5, stat as stat4 } from "fs/promises";
|
|
1111
1622
|
import { homedir as homedir4 } from "os";
|
|
1112
|
-
import { join as
|
|
1623
|
+
import { join as join4 } from "path";
|
|
1113
1624
|
var SERVICE4 = "github-copilot";
|
|
1114
|
-
function parseLines3(raw) {
|
|
1115
|
-
const out = [];
|
|
1116
|
-
for (const line of raw.split("\n")) {
|
|
1117
|
-
if (!line.trim()) continue;
|
|
1118
|
-
try {
|
|
1119
|
-
out.push(JSON.parse(line));
|
|
1120
|
-
} catch {
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
return out;
|
|
1124
|
-
}
|
|
1125
1625
|
var CopilotAdapter = class {
|
|
1126
1626
|
harness = "github-copilot";
|
|
1127
1627
|
aliases = ["copilot"];
|
|
1128
1628
|
root() {
|
|
1129
|
-
return
|
|
1629
|
+
return join4(process.env.COPILOT_HOME ?? join4(homedir4(), ".copilot"), "session-state");
|
|
1130
1630
|
}
|
|
1131
1631
|
async locate(opts = {}) {
|
|
1132
1632
|
let dirs;
|
|
1133
1633
|
try {
|
|
1134
|
-
dirs = await
|
|
1135
|
-
} catch {
|
|
1136
|
-
return [];
|
|
1634
|
+
dirs = await readdir5(this.root());
|
|
1635
|
+
} catch (error) {
|
|
1636
|
+
if (isMissingPathError(error)) return [];
|
|
1637
|
+
throw error;
|
|
1137
1638
|
}
|
|
1138
1639
|
const refs = [];
|
|
1139
1640
|
for (const id of dirs) {
|
|
1140
|
-
const path =
|
|
1641
|
+
const path = join4(this.root(), id, "events.jsonl");
|
|
1141
1642
|
let st;
|
|
1142
1643
|
try {
|
|
1143
1644
|
st = await stat4(path);
|
|
1144
|
-
} catch {
|
|
1145
|
-
continue;
|
|
1645
|
+
} catch (error) {
|
|
1646
|
+
if (isMissingPathError(error)) continue;
|
|
1647
|
+
throw error;
|
|
1146
1648
|
}
|
|
1147
1649
|
if (!st.isFile()) continue;
|
|
1148
1650
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
@@ -1151,28 +1653,23 @@ var CopilotAdapter = class {
|
|
|
1151
1653
|
}
|
|
1152
1654
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1153
1655
|
}
|
|
1154
|
-
async parse(ref) {
|
|
1155
|
-
const events = parseLines3(await readFile5(ref.path, "utf8"));
|
|
1656
|
+
async parse(ref, options = {}) {
|
|
1156
1657
|
const traceId = ref.sessionId;
|
|
1157
1658
|
const rootId = `root:${traceId}`;
|
|
1158
|
-
const spans = [
|
|
1159
|
-
span({
|
|
1160
|
-
traceId,
|
|
1161
|
-
spanId: rootId,
|
|
1162
|
-
parentSpanId: null,
|
|
1163
|
-
name: "session",
|
|
1164
|
-
kind: "AGENT",
|
|
1165
|
-
startTime: events[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1166
|
-
endTime: events.at(-1)?.timestamp,
|
|
1167
|
-
service: SERVICE4,
|
|
1168
|
-
agent: SERVICE4
|
|
1169
|
-
})
|
|
1170
|
-
];
|
|
1659
|
+
const spans = [];
|
|
1171
1660
|
const toolByCallId = /* @__PURE__ */ new Map();
|
|
1661
|
+
let firstTimestamp;
|
|
1662
|
+
let lastTimestamp;
|
|
1663
|
+
let sawEvent = false;
|
|
1172
1664
|
let step = 0;
|
|
1173
1665
|
let lastLlm = rootId;
|
|
1174
1666
|
let pendingInputTokens = null;
|
|
1175
|
-
for (const ev of
|
|
1667
|
+
for await (const ev of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
|
|
1668
|
+
if (!sawEvent) {
|
|
1669
|
+
firstTimestamp = ev.timestamp;
|
|
1670
|
+
sawEvent = true;
|
|
1671
|
+
}
|
|
1672
|
+
lastTimestamp = ev.timestamp;
|
|
1176
1673
|
const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1177
1674
|
const d = ev.data ?? {};
|
|
1178
1675
|
if (ev.type === "assistant.usage") {
|
|
@@ -1214,7 +1711,7 @@ var CopilotAdapter = class {
|
|
|
1214
1711
|
agent: SERVICE4,
|
|
1215
1712
|
tool: name,
|
|
1216
1713
|
step,
|
|
1217
|
-
|
|
1714
|
+
extra: toolIoAttributes({ input: d.arguments })
|
|
1218
1715
|
});
|
|
1219
1716
|
spans.push(t);
|
|
1220
1717
|
toolByCallId.set(d.toolCallId, t);
|
|
@@ -1225,67 +1722,71 @@ var CopilotAdapter = class {
|
|
|
1225
1722
|
t.end_time = ts;
|
|
1226
1723
|
const err = d.success === false;
|
|
1227
1724
|
t.status = err ? { code: "ERROR", message: (d.error?.message ?? "").slice(0, 500) } : { code: "OK" };
|
|
1725
|
+
recordToolOutput(t, d.output ?? d.result ?? d.error?.message);
|
|
1228
1726
|
}
|
|
1229
1727
|
}
|
|
1230
1728
|
}
|
|
1231
|
-
|
|
1729
|
+
const root = span({
|
|
1730
|
+
traceId,
|
|
1731
|
+
spanId: rootId,
|
|
1732
|
+
parentSpanId: null,
|
|
1733
|
+
name: "session",
|
|
1734
|
+
kind: "AGENT",
|
|
1735
|
+
startTime: firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1736
|
+
endTime: lastTimestamp,
|
|
1737
|
+
service: SERVICE4,
|
|
1738
|
+
agent: SERVICE4
|
|
1739
|
+
});
|
|
1740
|
+
return [root, ...spans];
|
|
1232
1741
|
}
|
|
1233
1742
|
};
|
|
1234
1743
|
|
|
1235
1744
|
// src/adapters/factory.ts
|
|
1236
|
-
import { readdir as
|
|
1745
|
+
import { readdir as readdir6, stat as stat5 } from "fs/promises";
|
|
1237
1746
|
import { homedir as homedir5 } from "os";
|
|
1238
|
-
import { basename as basename4, join as
|
|
1747
|
+
import { basename as basename4, join as join5 } from "path";
|
|
1239
1748
|
var SERVICE5 = "factory";
|
|
1240
1749
|
function textOf4(content) {
|
|
1241
1750
|
return capText(
|
|
1242
1751
|
(content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("")
|
|
1243
1752
|
);
|
|
1244
1753
|
}
|
|
1245
|
-
function parseLines4(raw) {
|
|
1246
|
-
const out = [];
|
|
1247
|
-
for (const line of raw.split("\n")) {
|
|
1248
|
-
if (!line) continue;
|
|
1249
|
-
try {
|
|
1250
|
-
out.push(JSON.parse(line));
|
|
1251
|
-
} catch {
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
return out;
|
|
1255
|
-
}
|
|
1256
1754
|
var FactoryAdapter = class {
|
|
1257
1755
|
harness = "factory";
|
|
1258
1756
|
aliases = ["factory-droids", "droid"];
|
|
1259
1757
|
root() {
|
|
1260
|
-
return
|
|
1758
|
+
return join5(homedir5(), ".factory", "sessions");
|
|
1261
1759
|
}
|
|
1262
1760
|
async locate(opts = {}) {
|
|
1263
1761
|
const root = this.root();
|
|
1264
1762
|
let dirs;
|
|
1265
1763
|
try {
|
|
1266
|
-
dirs = await
|
|
1267
|
-
} catch {
|
|
1268
|
-
return [];
|
|
1764
|
+
dirs = await readdir6(root);
|
|
1765
|
+
} catch (error) {
|
|
1766
|
+
if (isMissingPathError(error)) return [];
|
|
1767
|
+
throw error;
|
|
1269
1768
|
}
|
|
1270
1769
|
const refs = [];
|
|
1271
1770
|
for (const dir of dirs) {
|
|
1272
|
-
const dp =
|
|
1771
|
+
const dp = join5(root, dir);
|
|
1273
1772
|
let files;
|
|
1274
1773
|
try {
|
|
1275
|
-
files = await
|
|
1276
|
-
} catch {
|
|
1277
|
-
continue;
|
|
1774
|
+
files = await readdir6(dp);
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
if (isMissingPathError(error)) continue;
|
|
1777
|
+
throw error;
|
|
1278
1778
|
}
|
|
1279
1779
|
const cwd = dir.replace(/-/g, "/");
|
|
1280
1780
|
if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
|
|
1281
1781
|
for (const f of files) {
|
|
1282
1782
|
if (!f.endsWith(".jsonl")) continue;
|
|
1283
|
-
const path =
|
|
1783
|
+
const path = join5(dp, f);
|
|
1284
1784
|
let st;
|
|
1285
1785
|
try {
|
|
1286
1786
|
st = await stat5(path);
|
|
1287
|
-
} catch {
|
|
1288
|
-
continue;
|
|
1787
|
+
} catch (error) {
|
|
1788
|
+
if (isMissingPathError(error)) continue;
|
|
1789
|
+
throw error;
|
|
1289
1790
|
}
|
|
1290
1791
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
1291
1792
|
refs.push({ harness: this.harness, sessionId: basename4(f, ".jsonl"), path, cwd, mtimeMs: st.mtimeMs });
|
|
@@ -1293,38 +1794,32 @@ var FactoryAdapter = class {
|
|
|
1293
1794
|
}
|
|
1294
1795
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1295
1796
|
}
|
|
1296
|
-
async parse(ref) {
|
|
1297
|
-
const lines = parseLines4(await readFile6(ref.path, "utf8"));
|
|
1298
|
-
const start = lines.find((l) => l.type === "session_start");
|
|
1299
|
-
const traceId = start?.id ?? ref.sessionId;
|
|
1300
|
-
const rootId = `root:${traceId}`;
|
|
1797
|
+
async parse(ref, options = {}) {
|
|
1301
1798
|
let settings = {};
|
|
1302
1799
|
try {
|
|
1303
|
-
settings =
|
|
1304
|
-
} catch {
|
|
1800
|
+
settings = await readJsonFile(ref.path.replace(/\.jsonl$/, ".settings.json"));
|
|
1801
|
+
} catch (error) {
|
|
1802
|
+
if (!isMissingJsonSource(error)) throw error;
|
|
1305
1803
|
}
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
spanId: rootId,
|
|
1310
|
-
parentSpanId: null,
|
|
1311
|
-
name: "session",
|
|
1312
|
-
kind: "AGENT",
|
|
1313
|
-
startTime: start?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1314
|
-
endTime: lines.at(-1)?.timestamp,
|
|
1315
|
-
service: SERVICE5,
|
|
1316
|
-
agent: SERVICE5,
|
|
1317
|
-
model: settings.model ?? null,
|
|
1318
|
-
extra: settings.tokenUsage ? {
|
|
1319
|
-
"session.input_tokens": settings.tokenUsage.inputTokens ?? 0,
|
|
1320
|
-
"session.output_tokens": settings.tokenUsage.outputTokens ?? 0
|
|
1321
|
-
} : void 0
|
|
1322
|
-
})
|
|
1323
|
-
];
|
|
1804
|
+
const sourceTraceId = ref.sessionId;
|
|
1805
|
+
const sourceRootId = `root:${sourceTraceId}`;
|
|
1806
|
+
const spans = [];
|
|
1324
1807
|
const toolByUseId = /* @__PURE__ */ new Map();
|
|
1808
|
+
let firstTimestamp;
|
|
1809
|
+
let lastTimestamp;
|
|
1810
|
+
let sessionLine;
|
|
1811
|
+
let sawLine = false;
|
|
1325
1812
|
let step = 0;
|
|
1326
|
-
let lastLlm =
|
|
1327
|
-
for (const l of
|
|
1813
|
+
let lastLlm = sourceRootId;
|
|
1814
|
+
for await (const l of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
|
|
1815
|
+
if (!sawLine) {
|
|
1816
|
+
firstTimestamp = l.timestamp;
|
|
1817
|
+
sawLine = true;
|
|
1818
|
+
}
|
|
1819
|
+
lastTimestamp = l.timestamp;
|
|
1820
|
+
if (!sessionLine && l.type === "session_start") {
|
|
1821
|
+
sessionLine = { id: l.id, timestamp: l.timestamp };
|
|
1822
|
+
}
|
|
1328
1823
|
if (l.type !== "message" || !l.message) continue;
|
|
1329
1824
|
const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1330
1825
|
const role = l.message.role;
|
|
@@ -1333,9 +1828,9 @@ var FactoryAdapter = class {
|
|
|
1333
1828
|
if (text) {
|
|
1334
1829
|
spans.push(
|
|
1335
1830
|
userPromptSpan({
|
|
1336
|
-
traceId,
|
|
1831
|
+
traceId: sourceTraceId,
|
|
1337
1832
|
spanId: `user:${l.id ?? step}`,
|
|
1338
|
-
parentSpanId:
|
|
1833
|
+
parentSpanId: sourceRootId,
|
|
1339
1834
|
startTime: ts,
|
|
1340
1835
|
content: text,
|
|
1341
1836
|
service: SERVICE5,
|
|
@@ -1349,9 +1844,9 @@ var FactoryAdapter = class {
|
|
|
1349
1844
|
const llmId = `llm:${l.id ?? step}`;
|
|
1350
1845
|
spans.push(
|
|
1351
1846
|
span({
|
|
1352
|
-
traceId,
|
|
1847
|
+
traceId: sourceTraceId,
|
|
1353
1848
|
spanId: llmId,
|
|
1354
|
-
parentSpanId:
|
|
1849
|
+
parentSpanId: sourceRootId,
|
|
1355
1850
|
name: `message.${role ?? "unknown"}`,
|
|
1356
1851
|
kind: "LLM",
|
|
1357
1852
|
startTime: ts,
|
|
@@ -1368,7 +1863,7 @@ var FactoryAdapter = class {
|
|
|
1368
1863
|
for (const b of l.message.content ?? []) {
|
|
1369
1864
|
if (b.type === "tool_use" && b.name) {
|
|
1370
1865
|
const t = span({
|
|
1371
|
-
traceId,
|
|
1866
|
+
traceId: sourceTraceId,
|
|
1372
1867
|
spanId: `tool:${b.id ?? `${l.id}:${step}`}`,
|
|
1373
1868
|
parentSpanId: lastLlm,
|
|
1374
1869
|
name: `tool.${b.name}`,
|
|
@@ -1378,25 +1873,50 @@ var FactoryAdapter = class {
|
|
|
1378
1873
|
agent: SERVICE5,
|
|
1379
1874
|
tool: b.name,
|
|
1380
1875
|
step,
|
|
1381
|
-
|
|
1876
|
+
extra: toolIoAttributes({ input: b.input })
|
|
1382
1877
|
});
|
|
1383
1878
|
spans.push(t);
|
|
1384
1879
|
if (b.id) toolByUseId.set(b.id, t);
|
|
1385
1880
|
step += 1;
|
|
1386
1881
|
} else if (b.type === "tool_result" && b.tool_use_id) {
|
|
1387
1882
|
const t = toolByUseId.get(b.tool_use_id);
|
|
1388
|
-
if (t)
|
|
1883
|
+
if (t) {
|
|
1884
|
+
t.status = b.is_error === true ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
|
|
1885
|
+
recordToolOutput(t, b.content);
|
|
1886
|
+
}
|
|
1389
1887
|
}
|
|
1390
1888
|
}
|
|
1391
1889
|
}
|
|
1392
|
-
|
|
1890
|
+
const traceId = sessionLine?.id ?? sourceTraceId;
|
|
1891
|
+
const rootId = `root:${traceId}`;
|
|
1892
|
+
for (const item of spans) {
|
|
1893
|
+
item.trace_id = traceId;
|
|
1894
|
+
if (item.parent_span_id === sourceRootId) item.parent_span_id = rootId;
|
|
1895
|
+
}
|
|
1896
|
+
const root = span({
|
|
1897
|
+
traceId,
|
|
1898
|
+
spanId: rootId,
|
|
1899
|
+
parentSpanId: null,
|
|
1900
|
+
name: "session",
|
|
1901
|
+
kind: "AGENT",
|
|
1902
|
+
startTime: sessionLine?.timestamp ?? firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1903
|
+
endTime: lastTimestamp,
|
|
1904
|
+
service: SERVICE5,
|
|
1905
|
+
agent: SERVICE5,
|
|
1906
|
+
model: settings.model ?? null,
|
|
1907
|
+
extra: settings.tokenUsage ? {
|
|
1908
|
+
"session.input_tokens": settings.tokenUsage.inputTokens ?? 0,
|
|
1909
|
+
"session.output_tokens": settings.tokenUsage.outputTokens ?? 0
|
|
1910
|
+
} : void 0
|
|
1911
|
+
});
|
|
1912
|
+
return [root, ...spans];
|
|
1393
1913
|
}
|
|
1394
1914
|
};
|
|
1395
1915
|
|
|
1396
1916
|
// src/adapters/forge.ts
|
|
1397
|
-
import { readdir as
|
|
1917
|
+
import { readdir as readdir7, stat as stat6 } from "fs/promises";
|
|
1398
1918
|
import { homedir as homedir6 } from "os";
|
|
1399
|
-
import { basename as basename5, join as
|
|
1919
|
+
import { basename as basename5, join as join6 } from "path";
|
|
1400
1920
|
var SERVICE6 = "forge";
|
|
1401
1921
|
function tokens(t) {
|
|
1402
1922
|
if (!t) return null;
|
|
@@ -1410,7 +1930,7 @@ var ForgeAdapter = class {
|
|
|
1410
1930
|
aliases = ["forgecode"];
|
|
1411
1931
|
/** Directories scanned for `*-dump.json` exports. */
|
|
1412
1932
|
dumpDirs() {
|
|
1413
|
-
return [process.cwd(),
|
|
1933
|
+
return [process.cwd(), join6(homedir6(), ".forge")];
|
|
1414
1934
|
}
|
|
1415
1935
|
async locate(opts = {}) {
|
|
1416
1936
|
const refs = [];
|
|
@@ -1418,18 +1938,20 @@ var ForgeAdapter = class {
|
|
|
1418
1938
|
for (const dir of dirs) {
|
|
1419
1939
|
let files;
|
|
1420
1940
|
try {
|
|
1421
|
-
files = await
|
|
1422
|
-
} catch {
|
|
1423
|
-
continue;
|
|
1941
|
+
files = await readdir7(dir);
|
|
1942
|
+
} catch (error) {
|
|
1943
|
+
if (isMissingPathError(error)) continue;
|
|
1944
|
+
throw error;
|
|
1424
1945
|
}
|
|
1425
1946
|
for (const f of files) {
|
|
1426
1947
|
if (!f.endsWith("-dump.json")) continue;
|
|
1427
|
-
const path =
|
|
1948
|
+
const path = join6(dir, f);
|
|
1428
1949
|
let st;
|
|
1429
1950
|
try {
|
|
1430
1951
|
st = await stat6(path);
|
|
1431
|
-
} catch {
|
|
1432
|
-
continue;
|
|
1952
|
+
} catch (error) {
|
|
1953
|
+
if (isMissingPathError(error)) continue;
|
|
1954
|
+
throw error;
|
|
1433
1955
|
}
|
|
1434
1956
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
1435
1957
|
refs.push({ harness: this.harness, sessionId: basename5(f, ".json"), path, cwd: dir, mtimeMs: st.mtimeMs });
|
|
@@ -1438,12 +1960,7 @@ var ForgeAdapter = class {
|
|
|
1438
1960
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1439
1961
|
}
|
|
1440
1962
|
async parse(ref) {
|
|
1441
|
-
|
|
1442
|
-
try {
|
|
1443
|
-
dump = JSON.parse(await readFile7(ref.path, "utf8"));
|
|
1444
|
-
} catch {
|
|
1445
|
-
return [];
|
|
1446
|
-
}
|
|
1963
|
+
const dump = await readJsonFile(ref.path);
|
|
1447
1964
|
const ctx = dump.context ?? dump;
|
|
1448
1965
|
const traceId = ctx.conversation_id ?? ref.sessionId;
|
|
1449
1966
|
const rootId = `root:${traceId}`;
|
|
@@ -1508,7 +2025,7 @@ var ForgeAdapter = class {
|
|
|
1508
2025
|
agent: SERVICE6,
|
|
1509
2026
|
tool: tc.name ?? "tool",
|
|
1510
2027
|
step,
|
|
1511
|
-
|
|
2028
|
+
extra: toolIoAttributes({ input: tc.arguments })
|
|
1512
2029
|
});
|
|
1513
2030
|
spans.push(t);
|
|
1514
2031
|
toolByCallId.set(id, t);
|
|
@@ -1516,7 +2033,10 @@ var ForgeAdapter = class {
|
|
|
1516
2033
|
}
|
|
1517
2034
|
} else if (entry.tool) {
|
|
1518
2035
|
const t = toolByCallId.get(entry.tool.call_id ?? "");
|
|
1519
|
-
if (t)
|
|
2036
|
+
if (t) {
|
|
2037
|
+
t.status = entry.tool.output?.is_error ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
|
|
2038
|
+
recordToolOutput(t, entry.tool.output?.values ?? entry.tool.output);
|
|
2039
|
+
}
|
|
1520
2040
|
}
|
|
1521
2041
|
}
|
|
1522
2042
|
return spans;
|
|
@@ -1524,9 +2044,9 @@ var ForgeAdapter = class {
|
|
|
1524
2044
|
};
|
|
1525
2045
|
|
|
1526
2046
|
// src/adapters/gemini.ts
|
|
1527
|
-
import { readdir as
|
|
2047
|
+
import { readdir as readdir8, stat as stat7 } from "fs/promises";
|
|
1528
2048
|
import { homedir as homedir7 } from "os";
|
|
1529
|
-
import { join as
|
|
2049
|
+
import { join as join7 } from "path";
|
|
1530
2050
|
function iso(ts, fallback) {
|
|
1531
2051
|
if (typeof ts === "number") return new Date(ts).toISOString();
|
|
1532
2052
|
if (typeof ts === "string" && ts.length > 0) return ts;
|
|
@@ -1551,7 +2071,7 @@ var GeminiFamilyAdapter = class {
|
|
|
1551
2071
|
this.homeDirName = cfg.homeDirName;
|
|
1552
2072
|
}
|
|
1553
2073
|
chatsRoot() {
|
|
1554
|
-
return
|
|
2074
|
+
return join7(homedir7(), this.homeDirName, "tmp");
|
|
1555
2075
|
}
|
|
1556
2076
|
/** `<home>/projects.json` maps `{ <absolute-cwd>: <projectName> }`. The session
|
|
1557
2077
|
* dir name (`tmp/<projectHash>/`) is that projectName for registered projects,
|
|
@@ -1560,10 +2080,12 @@ var GeminiFamilyAdapter = class {
|
|
|
1560
2080
|
async loadProjectCwds() {
|
|
1561
2081
|
const nameToPath = /* @__PURE__ */ new Map();
|
|
1562
2082
|
try {
|
|
1563
|
-
const
|
|
1564
|
-
|
|
2083
|
+
const projects = (await readJsonFile(
|
|
2084
|
+
join7(homedir7(), this.homeDirName, "projects.json")
|
|
2085
|
+
)).projects ?? {};
|
|
1565
2086
|
for (const [path, name] of Object.entries(projects)) nameToPath.set(name, path);
|
|
1566
|
-
} catch {
|
|
2087
|
+
} catch (error) {
|
|
2088
|
+
if (!isMissingJsonSource(error)) throw error;
|
|
1567
2089
|
}
|
|
1568
2090
|
return nameToPath;
|
|
1569
2091
|
}
|
|
@@ -1571,29 +2093,32 @@ var GeminiFamilyAdapter = class {
|
|
|
1571
2093
|
const root = this.chatsRoot();
|
|
1572
2094
|
let projectDirs;
|
|
1573
2095
|
try {
|
|
1574
|
-
projectDirs = await
|
|
1575
|
-
} catch {
|
|
1576
|
-
return [];
|
|
2096
|
+
projectDirs = await readdir8(root);
|
|
2097
|
+
} catch (error) {
|
|
2098
|
+
if (isMissingPathError(error)) return [];
|
|
2099
|
+
throw error;
|
|
1577
2100
|
}
|
|
1578
2101
|
const projectCwds = await this.loadProjectCwds();
|
|
1579
2102
|
const refs = [];
|
|
1580
2103
|
for (const pd of projectDirs) {
|
|
1581
2104
|
const cwd = projectCwds.get(pd) ?? null;
|
|
1582
|
-
const chatsDir =
|
|
2105
|
+
const chatsDir = join7(root, pd, "chats");
|
|
1583
2106
|
let files;
|
|
1584
2107
|
try {
|
|
1585
|
-
files = await
|
|
1586
|
-
} catch {
|
|
1587
|
-
continue;
|
|
2108
|
+
files = await readdir8(chatsDir);
|
|
2109
|
+
} catch (error) {
|
|
2110
|
+
if (isMissingPathError(error)) continue;
|
|
2111
|
+
throw error;
|
|
1588
2112
|
}
|
|
1589
2113
|
for (const f of files) {
|
|
1590
2114
|
if (!f.startsWith("session-") || !f.endsWith(".json")) continue;
|
|
1591
|
-
const path =
|
|
2115
|
+
const path = join7(chatsDir, f);
|
|
1592
2116
|
let st;
|
|
1593
2117
|
try {
|
|
1594
2118
|
st = await stat7(path);
|
|
1595
|
-
} catch {
|
|
1596
|
-
continue;
|
|
2119
|
+
} catch (error) {
|
|
2120
|
+
if (isMissingPathError(error)) continue;
|
|
2121
|
+
throw error;
|
|
1597
2122
|
}
|
|
1598
2123
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
1599
2124
|
if (opts.cwd && (!cwd || !cwd.startsWith(opts.cwd))) continue;
|
|
@@ -1603,12 +2128,7 @@ var GeminiFamilyAdapter = class {
|
|
|
1603
2128
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1604
2129
|
}
|
|
1605
2130
|
async parse(ref) {
|
|
1606
|
-
|
|
1607
|
-
try {
|
|
1608
|
-
session = JSON.parse(await readFile8(ref.path, "utf8"));
|
|
1609
|
-
} catch {
|
|
1610
|
-
return [];
|
|
1611
|
-
}
|
|
2131
|
+
const session = await readJsonFile(ref.path);
|
|
1612
2132
|
const traceId = session.sessionId ?? ref.sessionId;
|
|
1613
2133
|
const messages = session.messages ?? [];
|
|
1614
2134
|
const rootId = `root:${traceId}`;
|
|
@@ -1683,7 +2203,7 @@ var GeminiFamilyAdapter = class {
|
|
|
1683
2203
|
agent: this.service,
|
|
1684
2204
|
tool: tc.name ?? "tool",
|
|
1685
2205
|
step,
|
|
1686
|
-
|
|
2206
|
+
extra: toolIoAttributes({ input: tc.args, output: tc.result })
|
|
1687
2207
|
})
|
|
1688
2208
|
);
|
|
1689
2209
|
step += 1;
|
|
@@ -1699,9 +2219,9 @@ var GeminiAdapter = class extends GeminiFamilyAdapter {
|
|
|
1699
2219
|
};
|
|
1700
2220
|
|
|
1701
2221
|
// src/adapters/opencode.ts
|
|
1702
|
-
import { readdir as
|
|
2222
|
+
import { readdir as readdir9, stat as stat8 } from "fs/promises";
|
|
1703
2223
|
import { homedir as homedir8 } from "os";
|
|
1704
|
-
import { join as
|
|
2224
|
+
import { join as join8 } from "path";
|
|
1705
2225
|
var SERVICE7 = "opencode";
|
|
1706
2226
|
function isoOf(ms) {
|
|
1707
2227
|
return new Date(ms ?? 0).toISOString();
|
|
@@ -1714,25 +2234,27 @@ function textOf7(parts) {
|
|
|
1714
2234
|
var OpencodeAdapter = class {
|
|
1715
2235
|
harness = "opencode";
|
|
1716
2236
|
storage() {
|
|
1717
|
-
const base = process.env.XDG_DATA_HOME ??
|
|
1718
|
-
return
|
|
2237
|
+
const base = process.env.XDG_DATA_HOME ?? join8(homedir8(), ".local", "share");
|
|
2238
|
+
return join8(base, "opencode", "storage");
|
|
1719
2239
|
}
|
|
1720
2240
|
async locate(opts = {}) {
|
|
1721
|
-
const msgRoot =
|
|
2241
|
+
const msgRoot = join8(this.storage(), "message");
|
|
1722
2242
|
let sessionDirs;
|
|
1723
2243
|
try {
|
|
1724
|
-
sessionDirs = await
|
|
1725
|
-
} catch {
|
|
1726
|
-
return [];
|
|
2244
|
+
sessionDirs = await readdir9(msgRoot);
|
|
2245
|
+
} catch (error) {
|
|
2246
|
+
if (isMissingPathError(error)) return [];
|
|
2247
|
+
throw error;
|
|
1727
2248
|
}
|
|
1728
2249
|
const refs = [];
|
|
1729
2250
|
for (const sid of sessionDirs) {
|
|
1730
|
-
const dir =
|
|
2251
|
+
const dir = join8(msgRoot, sid);
|
|
1731
2252
|
let st;
|
|
1732
2253
|
try {
|
|
1733
2254
|
st = await stat8(dir);
|
|
1734
|
-
} catch {
|
|
1735
|
-
continue;
|
|
2255
|
+
} catch (error) {
|
|
2256
|
+
if (isMissingPathError(error)) continue;
|
|
2257
|
+
throw error;
|
|
1736
2258
|
}
|
|
1737
2259
|
if (!st.isDirectory()) continue;
|
|
1738
2260
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
@@ -1744,21 +2266,13 @@ var OpencodeAdapter = class {
|
|
|
1744
2266
|
async parse(ref) {
|
|
1745
2267
|
const traceId = ref.sessionId;
|
|
1746
2268
|
const rootId = `root:${traceId}`;
|
|
1747
|
-
const partRoot =
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
return [];
|
|
1753
|
-
}
|
|
1754
|
-
const messages = [];
|
|
1755
|
-
for (const f of msgFiles) {
|
|
1756
|
-
try {
|
|
1757
|
-
messages.push({ msg: JSON.parse(await readFile9(join9(ref.path, f), "utf8")), file: f });
|
|
1758
|
-
} catch {
|
|
1759
|
-
}
|
|
2269
|
+
const partRoot = join8(this.storage(), "part");
|
|
2270
|
+
const msgFiles = await listJsonFiles(ref.path);
|
|
2271
|
+
const messages = [];
|
|
2272
|
+
for (const f of msgFiles) {
|
|
2273
|
+
messages.push(await readJsonFile(join8(ref.path, f)));
|
|
1760
2274
|
}
|
|
1761
|
-
messages.sort((a, b) => (a.
|
|
2275
|
+
messages.sort((a, b) => (a.time?.created ?? 0) - (b.time?.created ?? 0));
|
|
1762
2276
|
const spans = [
|
|
1763
2277
|
span({
|
|
1764
2278
|
traceId,
|
|
@@ -1766,59 +2280,65 @@ var OpencodeAdapter = class {
|
|
|
1766
2280
|
parentSpanId: null,
|
|
1767
2281
|
name: "session",
|
|
1768
2282
|
kind: "AGENT",
|
|
1769
|
-
startTime: isoOf(messages[0]?.
|
|
1770
|
-
endTime: isoOf(messages.at(-1)?.
|
|
2283
|
+
startTime: isoOf(messages[0]?.time?.created),
|
|
2284
|
+
endTime: isoOf(messages.at(-1)?.time?.completed ?? messages.at(-1)?.time?.created),
|
|
1771
2285
|
service: SERVICE7,
|
|
1772
2286
|
agent: SERVICE7
|
|
1773
2287
|
})
|
|
1774
2288
|
];
|
|
1775
2289
|
let step = 0;
|
|
1776
|
-
for (const
|
|
2290
|
+
for (const msg of messages) {
|
|
1777
2291
|
const mid = msg.id ?? `m${step}`;
|
|
1778
2292
|
const llmId = `llm:${mid}`;
|
|
1779
|
-
const pdir =
|
|
2293
|
+
const pdir = join8(partRoot, mid);
|
|
1780
2294
|
const parts = [];
|
|
2295
|
+
let partFiles = [];
|
|
1781
2296
|
try {
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
}
|
|
1789
|
-
} catch {
|
|
2297
|
+
partFiles = await listJsonFiles(pdir);
|
|
2298
|
+
} catch (error) {
|
|
2299
|
+
if (!isMissingJsonSource(error)) throw error;
|
|
2300
|
+
}
|
|
2301
|
+
for (const pf of partFiles) {
|
|
2302
|
+
parts.push(await readJsonFile(join8(pdir, pf)));
|
|
1790
2303
|
}
|
|
1791
2304
|
const turnText = textOf7(parts);
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
step += 1;
|
|
1811
|
-
if (msg.role === "user" && turnText) {
|
|
2305
|
+
const isUser = msg.role === "user";
|
|
2306
|
+
if (isUser) {
|
|
2307
|
+
if (turnText) {
|
|
2308
|
+
spans.push(
|
|
2309
|
+
userPromptSpan({
|
|
2310
|
+
traceId,
|
|
2311
|
+
spanId: `${llmId}:user`,
|
|
2312
|
+
parentSpanId: rootId,
|
|
2313
|
+
startTime: isoOf(msg.time?.created),
|
|
2314
|
+
service: SERVICE7,
|
|
2315
|
+
agent: SERVICE7,
|
|
2316
|
+
step,
|
|
2317
|
+
content: turnText
|
|
2318
|
+
})
|
|
2319
|
+
);
|
|
2320
|
+
step += 1;
|
|
2321
|
+
}
|
|
2322
|
+
} else {
|
|
1812
2323
|
spans.push(
|
|
1813
|
-
|
|
2324
|
+
span({
|
|
1814
2325
|
traceId,
|
|
1815
|
-
spanId:
|
|
2326
|
+
spanId: llmId,
|
|
1816
2327
|
parentSpanId: rootId,
|
|
2328
|
+
name: `message.${msg.role ?? "unknown"}`,
|
|
2329
|
+
kind: "LLM",
|
|
1817
2330
|
startTime: isoOf(msg.time?.created),
|
|
2331
|
+
endTime: isoOf(msg.time?.completed ?? msg.time?.created),
|
|
1818
2332
|
service: SERVICE7,
|
|
1819
2333
|
agent: SERVICE7,
|
|
2334
|
+
model: msg.modelID ?? null,
|
|
2335
|
+
inputTokens: msg.tokens?.input ?? null,
|
|
2336
|
+
outputTokens: msg.tokens?.output ?? null,
|
|
2337
|
+
reasoningTokens: msg.tokens?.reasoning ?? null,
|
|
2338
|
+
cachedInputTokens: msg.tokens?.cache?.read ?? null,
|
|
2339
|
+
cacheWriteInputTokens: msg.tokens?.cache?.write ?? null,
|
|
1820
2340
|
step,
|
|
1821
|
-
content: turnText
|
|
2341
|
+
content: turnText || null
|
|
1822
2342
|
})
|
|
1823
2343
|
);
|
|
1824
2344
|
step += 1;
|
|
@@ -1830,7 +2350,7 @@ var OpencodeAdapter = class {
|
|
|
1830
2350
|
span({
|
|
1831
2351
|
traceId,
|
|
1832
2352
|
spanId: `tool:${part.callID ?? `${mid}:${step}`}`,
|
|
1833
|
-
parentSpanId: llmId,
|
|
2353
|
+
parentSpanId: isUser ? rootId : llmId,
|
|
1834
2354
|
name: `tool.${part.tool}`,
|
|
1835
2355
|
kind: "TOOL",
|
|
1836
2356
|
startTime: isoOf(msg.time?.created),
|
|
@@ -1841,7 +2361,10 @@ var OpencodeAdapter = class {
|
|
|
1841
2361
|
agent: SERVICE7,
|
|
1842
2362
|
tool: part.tool,
|
|
1843
2363
|
step,
|
|
1844
|
-
|
|
2364
|
+
extra: toolIoAttributes({
|
|
2365
|
+
input: part.state?.input,
|
|
2366
|
+
output: part.state?.output ?? part.state?.error
|
|
2367
|
+
})
|
|
1845
2368
|
})
|
|
1846
2369
|
);
|
|
1847
2370
|
step += 1;
|
|
@@ -1852,21 +2375,10 @@ var OpencodeAdapter = class {
|
|
|
1852
2375
|
};
|
|
1853
2376
|
|
|
1854
2377
|
// src/adapters/pi.ts
|
|
1855
|
-
import { readdir as
|
|
2378
|
+
import { readdir as readdir10, stat as stat9 } from "fs/promises";
|
|
1856
2379
|
import { homedir as homedir9 } from "os";
|
|
1857
|
-
import { basename as basename6, join as
|
|
2380
|
+
import { basename as basename6, join as join9 } from "path";
|
|
1858
2381
|
var SERVICE8 = "pi";
|
|
1859
|
-
function parseLines5(raw) {
|
|
1860
|
-
const out = [];
|
|
1861
|
-
for (const line of raw.split("\n")) {
|
|
1862
|
-
if (!line) continue;
|
|
1863
|
-
try {
|
|
1864
|
-
out.push(JSON.parse(line));
|
|
1865
|
-
} catch {
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
return out;
|
|
1869
|
-
}
|
|
1870
2382
|
function isToolBlock(b) {
|
|
1871
2383
|
return typeof b.type === "string" && /tool[_-]?call|tool[_-]?use|^tool$/i.test(b.type);
|
|
1872
2384
|
}
|
|
@@ -1882,35 +2394,38 @@ function textOf8(content) {
|
|
|
1882
2394
|
var PiAdapter = class {
|
|
1883
2395
|
harness = "pi";
|
|
1884
2396
|
root() {
|
|
1885
|
-
return
|
|
2397
|
+
return join9(homedir9(), ".pi", "agent", "sessions");
|
|
1886
2398
|
}
|
|
1887
2399
|
async locate(opts = {}) {
|
|
1888
2400
|
const root = this.root();
|
|
1889
2401
|
let dirs;
|
|
1890
2402
|
try {
|
|
1891
|
-
dirs = await
|
|
1892
|
-
} catch {
|
|
1893
|
-
return [];
|
|
2403
|
+
dirs = await readdir10(root);
|
|
2404
|
+
} catch (error) {
|
|
2405
|
+
if (isMissingPathError(error)) return [];
|
|
2406
|
+
throw error;
|
|
1894
2407
|
}
|
|
1895
2408
|
const refs = [];
|
|
1896
2409
|
for (const dir of dirs) {
|
|
1897
|
-
const dp =
|
|
2410
|
+
const dp = join9(root, dir);
|
|
1898
2411
|
let files;
|
|
1899
2412
|
try {
|
|
1900
|
-
files = await
|
|
1901
|
-
} catch {
|
|
1902
|
-
continue;
|
|
2413
|
+
files = await readdir10(dp);
|
|
2414
|
+
} catch (error) {
|
|
2415
|
+
if (isMissingPathError(error)) continue;
|
|
2416
|
+
throw error;
|
|
1903
2417
|
}
|
|
1904
2418
|
const cwd = `/${dir.replace(/^-+/, "").replace(/-+$/, "").replace(/-/g, "/")}`;
|
|
1905
2419
|
if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
|
|
1906
2420
|
for (const f of files) {
|
|
1907
2421
|
if (!f.endsWith(".jsonl")) continue;
|
|
1908
|
-
const path =
|
|
2422
|
+
const path = join9(dp, f);
|
|
1909
2423
|
let st;
|
|
1910
2424
|
try {
|
|
1911
2425
|
st = await stat9(path);
|
|
1912
|
-
} catch {
|
|
1913
|
-
continue;
|
|
2426
|
+
} catch (error) {
|
|
2427
|
+
if (isMissingPathError(error)) continue;
|
|
2428
|
+
throw error;
|
|
1914
2429
|
}
|
|
1915
2430
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
1916
2431
|
const id = basename6(f, ".jsonl").replace(/^[\dTZ.-]+_/, "");
|
|
@@ -1919,27 +2434,25 @@ var PiAdapter = class {
|
|
|
1919
2434
|
}
|
|
1920
2435
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1921
2436
|
}
|
|
1922
|
-
async parse(ref) {
|
|
1923
|
-
const
|
|
1924
|
-
const
|
|
1925
|
-
const
|
|
1926
|
-
const rootId = `root:${traceId}`;
|
|
1927
|
-
const spans = [
|
|
1928
|
-
span({
|
|
1929
|
-
traceId,
|
|
1930
|
-
spanId: rootId,
|
|
1931
|
-
parentSpanId: null,
|
|
1932
|
-
name: "session",
|
|
1933
|
-
kind: "AGENT",
|
|
1934
|
-
startTime: sessionLine?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1935
|
-
endTime: lines.at(-1)?.timestamp,
|
|
1936
|
-
service: SERVICE8,
|
|
1937
|
-
agent: SERVICE8
|
|
1938
|
-
})
|
|
1939
|
-
];
|
|
2437
|
+
async parse(ref, options = {}) {
|
|
2438
|
+
const sourceTraceId = ref.sessionId;
|
|
2439
|
+
const sourceRootId = `root:${sourceTraceId}`;
|
|
2440
|
+
const spans = [];
|
|
1940
2441
|
const toolByCallId = /* @__PURE__ */ new Map();
|
|
2442
|
+
let firstTimestamp;
|
|
2443
|
+
let lastTimestamp;
|
|
2444
|
+
let sessionLine;
|
|
2445
|
+
let sawLine = false;
|
|
1941
2446
|
let step = 0;
|
|
1942
|
-
for (const l of
|
|
2447
|
+
for await (const l of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
|
|
2448
|
+
if (!sawLine) {
|
|
2449
|
+
firstTimestamp = l.timestamp;
|
|
2450
|
+
sawLine = true;
|
|
2451
|
+
}
|
|
2452
|
+
lastTimestamp = l.timestamp;
|
|
2453
|
+
if (!sessionLine && l.type === "session") {
|
|
2454
|
+
sessionLine = { id: l.id, timestamp: l.timestamp };
|
|
2455
|
+
}
|
|
1943
2456
|
if (l.type !== "message" || !l.message) continue;
|
|
1944
2457
|
const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1945
2458
|
const mid = l.id ?? `m${step}`;
|
|
@@ -1950,9 +2463,9 @@ var PiAdapter = class {
|
|
|
1950
2463
|
if (prompt) {
|
|
1951
2464
|
spans.push(
|
|
1952
2465
|
userPromptSpan({
|
|
1953
|
-
traceId,
|
|
2466
|
+
traceId: sourceTraceId,
|
|
1954
2467
|
spanId: `${mid}:user`,
|
|
1955
|
-
parentSpanId:
|
|
2468
|
+
parentSpanId: sourceRootId,
|
|
1956
2469
|
startTime: ts,
|
|
1957
2470
|
service: SERVICE8,
|
|
1958
2471
|
agent: SERVICE8,
|
|
@@ -1966,9 +2479,9 @@ var PiAdapter = class {
|
|
|
1966
2479
|
const errored = !!msg.errorMessage;
|
|
1967
2480
|
spans.push(
|
|
1968
2481
|
span({
|
|
1969
|
-
traceId,
|
|
2482
|
+
traceId: sourceTraceId,
|
|
1970
2483
|
spanId: llmId,
|
|
1971
|
-
parentSpanId:
|
|
2484
|
+
parentSpanId: sourceRootId,
|
|
1972
2485
|
name: `message.${msg.role ?? "unknown"}`,
|
|
1973
2486
|
kind: "LLM",
|
|
1974
2487
|
startTime: ts,
|
|
@@ -1990,7 +2503,7 @@ var PiAdapter = class {
|
|
|
1990
2503
|
const name = b.toolName ?? b.name ?? "tool";
|
|
1991
2504
|
const callId = b.id ?? b.callId ?? b.toolCallId ?? `${mid}:${step}`;
|
|
1992
2505
|
const toolSpan = span({
|
|
1993
|
-
traceId,
|
|
2506
|
+
traceId: sourceTraceId,
|
|
1994
2507
|
spanId: `tool:${callId}`,
|
|
1995
2508
|
parentSpanId: llmId,
|
|
1996
2509
|
name: `tool.${name}`,
|
|
@@ -2000,7 +2513,7 @@ var PiAdapter = class {
|
|
|
2000
2513
|
agent: SERVICE8,
|
|
2001
2514
|
tool: name,
|
|
2002
2515
|
step,
|
|
2003
|
-
|
|
2516
|
+
extra: toolIoAttributes({ input: b.input ?? b.args })
|
|
2004
2517
|
});
|
|
2005
2518
|
spans.push(toolSpan);
|
|
2006
2519
|
toolByCallId.set(callId, toolSpan);
|
|
@@ -2012,30 +2525,37 @@ var PiAdapter = class {
|
|
|
2012
2525
|
const err = b.isError === true || b.is_error === true;
|
|
2013
2526
|
t.end_time = ts;
|
|
2014
2527
|
t.status = err ? { code: "ERROR", message: "tool result reported error" } : { code: "OK" };
|
|
2528
|
+
recordToolOutput(t, b.output ?? b.result ?? b.content ?? b.text);
|
|
2015
2529
|
}
|
|
2016
2530
|
}
|
|
2017
2531
|
}
|
|
2018
2532
|
}
|
|
2019
|
-
|
|
2533
|
+
const traceId = sessionLine?.id ?? sourceTraceId;
|
|
2534
|
+
const rootId = `root:${traceId}`;
|
|
2535
|
+
for (const item of spans) {
|
|
2536
|
+
item.trace_id = traceId;
|
|
2537
|
+
if (item.parent_span_id === sourceRootId) item.parent_span_id = rootId;
|
|
2538
|
+
}
|
|
2539
|
+
const root = span({
|
|
2540
|
+
traceId,
|
|
2541
|
+
spanId: rootId,
|
|
2542
|
+
parentSpanId: null,
|
|
2543
|
+
name: "session",
|
|
2544
|
+
kind: "AGENT",
|
|
2545
|
+
startTime: sessionLine?.timestamp ?? firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2546
|
+
endTime: lastTimestamp,
|
|
2547
|
+
service: SERVICE8,
|
|
2548
|
+
agent: SERVICE8
|
|
2549
|
+
});
|
|
2550
|
+
return [root, ...spans];
|
|
2020
2551
|
}
|
|
2021
2552
|
};
|
|
2022
2553
|
|
|
2023
2554
|
// src/adapters/qwen.ts
|
|
2024
|
-
import { readdir as
|
|
2555
|
+
import { readdir as readdir11, stat as stat10 } from "fs/promises";
|
|
2025
2556
|
import { homedir as homedir10 } from "os";
|
|
2026
|
-
import { basename as basename7, join as
|
|
2557
|
+
import { basename as basename7, join as join10 } from "path";
|
|
2027
2558
|
var SERVICE9 = "qwen";
|
|
2028
|
-
function parseLines6(raw) {
|
|
2029
|
-
const out = [];
|
|
2030
|
-
for (const line of raw.split("\n")) {
|
|
2031
|
-
if (!line) continue;
|
|
2032
|
-
try {
|
|
2033
|
-
out.push(JSON.parse(line));
|
|
2034
|
-
} catch {
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
return out;
|
|
2038
|
-
}
|
|
2039
2559
|
function textOf9(parts) {
|
|
2040
2560
|
if (!parts) return "";
|
|
2041
2561
|
return parts.filter((p) => typeof p.text === "string").map((p) => p.text).join("");
|
|
@@ -2044,34 +2564,37 @@ var QwenAdapter = class {
|
|
|
2044
2564
|
harness = "qwen";
|
|
2045
2565
|
aliases = ["qwen-code"];
|
|
2046
2566
|
root() {
|
|
2047
|
-
return
|
|
2567
|
+
return join10(homedir10(), ".qwen", "projects");
|
|
2048
2568
|
}
|
|
2049
2569
|
async locate(opts = {}) {
|
|
2050
2570
|
const root = this.root();
|
|
2051
2571
|
let projectDirs;
|
|
2052
2572
|
try {
|
|
2053
|
-
projectDirs = await
|
|
2054
|
-
} catch {
|
|
2055
|
-
return [];
|
|
2573
|
+
projectDirs = await readdir11(root);
|
|
2574
|
+
} catch (error) {
|
|
2575
|
+
if (isMissingPathError(error)) return [];
|
|
2576
|
+
throw error;
|
|
2056
2577
|
}
|
|
2057
2578
|
const refs = [];
|
|
2058
2579
|
for (const pd of projectDirs) {
|
|
2059
|
-
const chatsDir =
|
|
2580
|
+
const chatsDir = join10(root, pd, "chats");
|
|
2060
2581
|
let files;
|
|
2061
2582
|
try {
|
|
2062
|
-
files = await
|
|
2063
|
-
} catch {
|
|
2064
|
-
continue;
|
|
2583
|
+
files = await readdir11(chatsDir);
|
|
2584
|
+
} catch (error) {
|
|
2585
|
+
if (isMissingPathError(error)) continue;
|
|
2586
|
+
throw error;
|
|
2065
2587
|
}
|
|
2066
2588
|
if (opts.cwd) continue;
|
|
2067
2589
|
for (const f of files) {
|
|
2068
2590
|
if (!f.endsWith(".jsonl")) continue;
|
|
2069
|
-
const path =
|
|
2591
|
+
const path = join10(chatsDir, f);
|
|
2070
2592
|
let st;
|
|
2071
2593
|
try {
|
|
2072
2594
|
st = await stat10(path);
|
|
2073
|
-
} catch {
|
|
2074
|
-
continue;
|
|
2595
|
+
} catch (error) {
|
|
2596
|
+
if (isMissingPathError(error)) continue;
|
|
2597
|
+
throw error;
|
|
2075
2598
|
}
|
|
2076
2599
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
2077
2600
|
refs.push({ harness: this.harness, sessionId: basename7(f, ".jsonl"), path, cwd: null, mtimeMs: st.mtimeMs });
|
|
@@ -2079,36 +2602,32 @@ var QwenAdapter = class {
|
|
|
2079
2602
|
}
|
|
2080
2603
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
2081
2604
|
}
|
|
2082
|
-
async parse(ref) {
|
|
2083
|
-
const
|
|
2084
|
-
const
|
|
2085
|
-
const
|
|
2086
|
-
const spans = [
|
|
2087
|
-
span({
|
|
2088
|
-
traceId,
|
|
2089
|
-
spanId: rootId,
|
|
2090
|
-
parentSpanId: null,
|
|
2091
|
-
name: "session",
|
|
2092
|
-
kind: "AGENT",
|
|
2093
|
-
startTime: records[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2094
|
-
endTime: records.at(-1)?.timestamp,
|
|
2095
|
-
service: SERVICE9,
|
|
2096
|
-
agent: SERVICE9
|
|
2097
|
-
})
|
|
2098
|
-
];
|
|
2605
|
+
async parse(ref, options = {}) {
|
|
2606
|
+
const sourceTraceId = ref.sessionId;
|
|
2607
|
+
const sourceRootId = `root:${sourceTraceId}`;
|
|
2608
|
+
const spans = [];
|
|
2099
2609
|
const openToolsByName = /* @__PURE__ */ new Map();
|
|
2610
|
+
let discoveredTraceId;
|
|
2611
|
+
let firstTimestamp;
|
|
2612
|
+
let lastTimestamp;
|
|
2613
|
+
let sawRecord = false;
|
|
2100
2614
|
let step = 0;
|
|
2101
|
-
|
|
2102
|
-
|
|
2615
|
+
for await (const r of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
|
|
2616
|
+
if (!sawRecord) {
|
|
2617
|
+
firstTimestamp = r.timestamp;
|
|
2618
|
+
sawRecord = true;
|
|
2619
|
+
}
|
|
2620
|
+
lastTimestamp = r.timestamp;
|
|
2621
|
+
if (!discoveredTraceId && r.sessionId) discoveredTraceId = r.sessionId;
|
|
2103
2622
|
const ts = r.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
2104
2623
|
const role = r.message?.role;
|
|
2105
2624
|
if (r.type === "assistant" || role === "model") {
|
|
2106
2625
|
const llmId = `llm:${r.uuid ?? step}`;
|
|
2107
2626
|
spans.push(
|
|
2108
2627
|
span({
|
|
2109
|
-
traceId,
|
|
2628
|
+
traceId: sourceTraceId,
|
|
2110
2629
|
spanId: llmId,
|
|
2111
|
-
parentSpanId:
|
|
2630
|
+
parentSpanId: sourceRootId,
|
|
2112
2631
|
name: "llm.turn",
|
|
2113
2632
|
kind: "LLM",
|
|
2114
2633
|
startTime: ts,
|
|
@@ -2121,13 +2640,12 @@ var QwenAdapter = class {
|
|
|
2121
2640
|
content: capText(textOf9(r.message?.parts)) || null
|
|
2122
2641
|
})
|
|
2123
2642
|
);
|
|
2124
|
-
lastLlm = llmId;
|
|
2125
2643
|
step += 1;
|
|
2126
2644
|
for (const p of r.message?.parts ?? []) {
|
|
2127
2645
|
if (!p.functionCall?.name) continue;
|
|
2128
2646
|
const name = p.functionCall.name;
|
|
2129
2647
|
const t = span({
|
|
2130
|
-
traceId,
|
|
2648
|
+
traceId: sourceTraceId,
|
|
2131
2649
|
spanId: `tool:${name}:${step}`,
|
|
2132
2650
|
parentSpanId: llmId,
|
|
2133
2651
|
name: `tool.${name}`,
|
|
@@ -2137,7 +2655,7 @@ var QwenAdapter = class {
|
|
|
2137
2655
|
agent: SERVICE9,
|
|
2138
2656
|
tool: name,
|
|
2139
2657
|
step,
|
|
2140
|
-
|
|
2658
|
+
extra: toolIoAttributes({ input: p.functionCall.args })
|
|
2141
2659
|
});
|
|
2142
2660
|
spans.push(t);
|
|
2143
2661
|
const q = openToolsByName.get(name) ?? [];
|
|
@@ -2150,9 +2668,9 @@ var QwenAdapter = class {
|
|
|
2150
2668
|
if (prompt) {
|
|
2151
2669
|
spans.push(
|
|
2152
2670
|
userPromptSpan({
|
|
2153
|
-
traceId,
|
|
2671
|
+
traceId: sourceTraceId,
|
|
2154
2672
|
spanId: `user:${r.uuid ?? step}`,
|
|
2155
|
-
parentSpanId:
|
|
2673
|
+
parentSpanId: sourceRootId,
|
|
2156
2674
|
startTime: ts,
|
|
2157
2675
|
service: SERVICE9,
|
|
2158
2676
|
agent: SERVICE9,
|
|
@@ -2171,11 +2689,29 @@ var QwenAdapter = class {
|
|
|
2171
2689
|
if (t) {
|
|
2172
2690
|
t.end_time = ts;
|
|
2173
2691
|
t.status = err ? { code: "ERROR", message: "tool result reported error" } : { code: "OK" };
|
|
2692
|
+
recordToolOutput(t, p.functionResponse?.response ?? r.toolCallResult?.error);
|
|
2174
2693
|
}
|
|
2175
2694
|
}
|
|
2176
2695
|
}
|
|
2177
2696
|
}
|
|
2178
|
-
|
|
2697
|
+
const traceId = discoveredTraceId ?? sourceTraceId;
|
|
2698
|
+
const rootId = `root:${traceId}`;
|
|
2699
|
+
for (const item of spans) {
|
|
2700
|
+
item.trace_id = traceId;
|
|
2701
|
+
if (item.parent_span_id === sourceRootId) item.parent_span_id = rootId;
|
|
2702
|
+
}
|
|
2703
|
+
const root = span({
|
|
2704
|
+
traceId,
|
|
2705
|
+
spanId: rootId,
|
|
2706
|
+
parentSpanId: null,
|
|
2707
|
+
name: "session",
|
|
2708
|
+
kind: "AGENT",
|
|
2709
|
+
startTime: firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2710
|
+
endTime: lastTimestamp,
|
|
2711
|
+
service: SERVICE9,
|
|
2712
|
+
agent: SERVICE9
|
|
2713
|
+
});
|
|
2714
|
+
return [root, ...spans];
|
|
2179
2715
|
}
|
|
2180
2716
|
};
|
|
2181
2717
|
|
|
@@ -2218,7 +2754,7 @@ function selectAdapters(opts) {
|
|
|
2218
2754
|
|
|
2219
2755
|
// src/repo.ts
|
|
2220
2756
|
import { stat as stat11 } from "fs/promises";
|
|
2221
|
-
import { dirname, isAbsolute, join as
|
|
2757
|
+
import { dirname, isAbsolute, join as join11, normalize, parse as parsePath } from "path";
|
|
2222
2758
|
function normalizeRemote(url) {
|
|
2223
2759
|
const raw = url.trim();
|
|
2224
2760
|
if (!raw) return null;
|
|
@@ -2339,7 +2875,7 @@ async function nearestExistingDir(path) {
|
|
|
2339
2875
|
async function findGitRoot(path) {
|
|
2340
2876
|
let cur = await nearestExistingDir(path);
|
|
2341
2877
|
while (cur) {
|
|
2342
|
-
if (await pathStat(
|
|
2878
|
+
if (await pathStat(join11(cur, ".git"))) return cur;
|
|
2343
2879
|
const parent = dirname(cur);
|
|
2344
2880
|
if (parent === cur) return null;
|
|
2345
2881
|
cur = parent;
|
|
@@ -2357,7 +2893,7 @@ async function repairDashedPath(cwd) {
|
|
|
2357
2893
|
while (index < parts.length) {
|
|
2358
2894
|
const nextPart = parts[index];
|
|
2359
2895
|
if (!nextPart) break;
|
|
2360
|
-
const exact =
|
|
2896
|
+
const exact = join11(current, nextPart);
|
|
2361
2897
|
if (!await directoryExists(exact)) break;
|
|
2362
2898
|
current = exact;
|
|
2363
2899
|
index += 1;
|
|
@@ -2367,7 +2903,7 @@ async function repairDashedPath(cwd) {
|
|
|
2367
2903
|
let consumed = 0;
|
|
2368
2904
|
for (let n = parts.length - index; n >= 1; n -= 1) {
|
|
2369
2905
|
const segment = parts.slice(index, index + n).join("-");
|
|
2370
|
-
const candidate =
|
|
2906
|
+
const candidate = join11(current, segment);
|
|
2371
2907
|
if (await directoryExists(candidate)) {
|
|
2372
2908
|
match = candidate;
|
|
2373
2909
|
consumed = n;
|
|
@@ -2575,14 +3111,25 @@ async function stampSpanWorkdirRepoAttrs(spans) {
|
|
|
2575
3111
|
}
|
|
2576
3112
|
|
|
2577
3113
|
// src/session-source.ts
|
|
2578
|
-
async function parseSession(adapter, ref) {
|
|
2579
|
-
const spans = await adapter.parse(ref);
|
|
3114
|
+
async function parseSession(adapter, ref, options = {}) {
|
|
3115
|
+
const spans = await adapter.parse(ref, options);
|
|
3116
|
+
if (spans.length === 0) throw new EmptySessionError(ref.path);
|
|
3117
|
+
stampSessionIntegrity(ref, spans);
|
|
3118
|
+
stampSessionIdentity(spans, ref.sessionId);
|
|
2580
3119
|
const repo = await resolveSessionRepoAttrs(ref.cwd, spans);
|
|
2581
3120
|
if (repo.cwd) ref.cwd = repo.cwd;
|
|
2582
3121
|
stampRepoAttrs(spans, repo.attrs);
|
|
2583
3122
|
await stampSpanWorkdirRepoAttrs(spans);
|
|
2584
3123
|
return spans;
|
|
2585
3124
|
}
|
|
3125
|
+
var EmptySessionError = class extends Error {
|
|
3126
|
+
sourcePath;
|
|
3127
|
+
constructor(sourcePath) {
|
|
3128
|
+
super(`Session parser produced no spans at ${sourcePath}`);
|
|
3129
|
+
this.name = "EmptySessionError";
|
|
3130
|
+
this.sourcePath = sourcePath;
|
|
3131
|
+
}
|
|
3132
|
+
};
|
|
2586
3133
|
function refKey(ref) {
|
|
2587
3134
|
return `${ref.harness}\0${ref.path}\0${ref.sessionId}`;
|
|
2588
3135
|
}
|
|
@@ -2592,7 +3139,7 @@ async function locateSessions(adapter, opts = {}) {
|
|
|
2592
3139
|
const refs = await adapter.locate({ sinceMs: opts.sinceMs });
|
|
2593
3140
|
const byKey = /* @__PURE__ */ new Map();
|
|
2594
3141
|
for (const ref of refs) {
|
|
2595
|
-
if (!cwdMatchesSelection(ref.cwd, cwdSelections)) continue;
|
|
3142
|
+
if (!cwdMatchesSelection(ref.cwd, cwdSelections) && !(ref.cwd === null && ref.integrity)) continue;
|
|
2596
3143
|
byKey.set(refKey(ref), ref);
|
|
2597
3144
|
}
|
|
2598
3145
|
return [...byKey.values()].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
@@ -2604,7 +3151,8 @@ async function* scanSessions(opts) {
|
|
|
2604
3151
|
try {
|
|
2605
3152
|
refs = await locateSessions(adapter, { cwd: opts.cwd, sinceMs: opts.sinceMs });
|
|
2606
3153
|
} catch (err) {
|
|
2607
|
-
opts.onError
|
|
3154
|
+
if (!opts.onError) throw err;
|
|
3155
|
+
opts.onError(err);
|
|
2608
3156
|
continue;
|
|
2609
3157
|
}
|
|
2610
3158
|
if (opts.last && opts.last > 0) refs = refs.slice(0, opts.last);
|
|
@@ -2612,12 +3160,12 @@ async function* scanSessions(opts) {
|
|
|
2612
3160
|
if (opts.signal?.aborted) return;
|
|
2613
3161
|
let spans;
|
|
2614
3162
|
try {
|
|
2615
|
-
spans = await parseSession(adapter, ref);
|
|
3163
|
+
spans = await parseSession(adapter, ref, { corruptionMode: opts.corruptionMode });
|
|
2616
3164
|
} catch (err) {
|
|
2617
|
-
opts.onError
|
|
3165
|
+
if (!opts.onError) throw err;
|
|
3166
|
+
opts.onError(err, ref);
|
|
2618
3167
|
continue;
|
|
2619
3168
|
}
|
|
2620
|
-
if (spans.length === 0) continue;
|
|
2621
3169
|
yield { adapter, ref, spans };
|
|
2622
3170
|
}
|
|
2623
3171
|
}
|
|
@@ -2626,17 +3174,17 @@ async function* scanSessions(opts) {
|
|
|
2626
3174
|
// src/evidence.ts
|
|
2627
3175
|
import { mkdtemp, writeFile } from "fs/promises";
|
|
2628
3176
|
import { tmpdir } from "os";
|
|
2629
|
-
import { join as
|
|
3177
|
+
import { join as join12 } from "path";
|
|
3178
|
+
import {
|
|
3179
|
+
OPENINFERENCE_SPAN_KIND as OPENINFERENCE_SPAN_KIND2,
|
|
3180
|
+
TOOL_NAME as TOOL_NAME2
|
|
3181
|
+
} from "@tangle-network/agent-eval/trace-attributes";
|
|
2630
3182
|
function stringAttr(span2, key) {
|
|
2631
3183
|
const value = span2.attributes[key];
|
|
2632
3184
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2633
3185
|
}
|
|
2634
|
-
function numberAttr(span2, key) {
|
|
2635
|
-
const value = span2.attributes[key];
|
|
2636
|
-
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
2637
|
-
}
|
|
2638
3186
|
function spanKind(span2) {
|
|
2639
|
-
return stringAttr(span2,
|
|
3187
|
+
return stringAttr(span2, OPENINFERENCE_SPAN_KIND2);
|
|
2640
3188
|
}
|
|
2641
3189
|
function repoFromSpans(spans) {
|
|
2642
3190
|
const attrs = {};
|
|
@@ -2662,7 +3210,7 @@ function summarizeTools(spans) {
|
|
|
2662
3210
|
const byTool = /* @__PURE__ */ new Map();
|
|
2663
3211
|
for (const span2 of spans) {
|
|
2664
3212
|
if (spanKind(span2) !== "TOOL") continue;
|
|
2665
|
-
const name = stringAttr(span2,
|
|
3213
|
+
const name = stringAttr(span2, TOOL_NAME2) ?? span2.name.replace(/^tool\./, "");
|
|
2666
3214
|
const current = byTool.get(name) ?? { calls: 0, errors: 0 };
|
|
2667
3215
|
current.calls += 1;
|
|
2668
3216
|
if (span2.status.code === "ERROR") current.errors += 1;
|
|
@@ -2671,7 +3219,6 @@ function summarizeTools(spans) {
|
|
|
2671
3219
|
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));
|
|
2672
3220
|
}
|
|
2673
3221
|
async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
|
|
2674
|
-
const llmSpans = spans.filter((span2) => spanKind(span2) === "LLM");
|
|
2675
3222
|
const toolSpans = spans.filter((span2) => spanKind(span2) === "TOOL");
|
|
2676
3223
|
const erroredToolCallCount = toolSpans.filter((span2) => span2.status.code === "ERROR").length;
|
|
2677
3224
|
const pipelines = await runPipelines(spans, { minLoopOccurrences: opts.minLoopOccurrences });
|
|
@@ -2679,10 +3226,14 @@ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
|
|
|
2679
3226
|
const loopFindings = pipelines.stuckLoops.findings;
|
|
2680
3227
|
const { firstSpanAt, lastSpanAt } = timeBounds(spans);
|
|
2681
3228
|
const repo = repoFromSpans(spans);
|
|
3229
|
+
const execution = summarizeSpanExecution(spans, {
|
|
3230
|
+
experimentId: `traces-policy-evidence:${ref.sessionId}`
|
|
3231
|
+
});
|
|
2682
3232
|
return {
|
|
2683
3233
|
schemaVersion: 1,
|
|
2684
3234
|
kind: "traces.policy_evidence.session",
|
|
2685
3235
|
generatedAt: opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3236
|
+
execution,
|
|
2686
3237
|
session: {
|
|
2687
3238
|
harness: ref.harness,
|
|
2688
3239
|
sessionId: ref.sessionId,
|
|
@@ -2693,12 +3244,12 @@ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
|
|
|
2693
3244
|
repo,
|
|
2694
3245
|
metrics: {
|
|
2695
3246
|
spanCount: spans.length,
|
|
2696
|
-
llmTurnCount:
|
|
3247
|
+
llmTurnCount: execution.execution.modelCalls.events,
|
|
2697
3248
|
toolCallCount: toolSpans.length,
|
|
2698
3249
|
erroredToolCallCount,
|
|
2699
|
-
inputTokens:
|
|
2700
|
-
outputTokens:
|
|
2701
|
-
models:
|
|
3250
|
+
inputTokens: execution.execution.tokenUsage.totals.input,
|
|
3251
|
+
outputTokens: execution.execution.tokenUsage.totals.output,
|
|
3252
|
+
models: execution.execution.models.map(({ model }) => model),
|
|
2702
3253
|
tools: summarizeTools(spans),
|
|
2703
3254
|
firstSpanAt,
|
|
2704
3255
|
lastSpanAt
|
|
@@ -2735,7 +3286,7 @@ function serializePolicyEvidence(records) {
|
|
|
2735
3286
|
`;
|
|
2736
3287
|
}
|
|
2737
3288
|
async function writePolicyEvidenceFile(records, outPath) {
|
|
2738
|
-
const path = outPath ??
|
|
3289
|
+
const path = outPath ?? join12(await mkdtemp(join12(tmpdir(), "traces-evidence-")), "policy-evidence.jsonl");
|
|
2739
3290
|
await writeFile(path, serializePolicyEvidence(records), "utf8");
|
|
2740
3291
|
return path;
|
|
2741
3292
|
}
|
|
@@ -2880,6 +3431,7 @@ function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
|
|
|
2880
3431
|
const report = { redactionCount: 0, byRule: {} };
|
|
2881
3432
|
const out = spans.map((s) => {
|
|
2882
3433
|
const attributes = redactValue(s.attributes, rules, report).value;
|
|
3434
|
+
normalizeToolIoAttributes(attributes);
|
|
2883
3435
|
let status = s.status;
|
|
2884
3436
|
if (status.message) {
|
|
2885
3437
|
const m = redactValue(status.message, rules, report).value;
|
|
@@ -2890,33 +3442,66 @@ function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
|
|
|
2890
3442
|
return { spans: out, report };
|
|
2891
3443
|
}
|
|
2892
3444
|
async function applyRedactor(spans, redactor) {
|
|
2893
|
-
const
|
|
3445
|
+
const capturedKeys = ["content", ...TOOL_IO_VALUE_KEYS];
|
|
3446
|
+
const fields = [];
|
|
2894
3447
|
const texts = [];
|
|
2895
3448
|
spans.forEach((s, i) => {
|
|
2896
|
-
const
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
3449
|
+
for (const key of capturedKeys) {
|
|
3450
|
+
const value = s.attributes[key];
|
|
3451
|
+
if (typeof value === "string" && value.length > 0) {
|
|
3452
|
+
fields.push({ spanIndex: i, key });
|
|
3453
|
+
texts.push(value);
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
if (s.status.message) {
|
|
3457
|
+
fields.push({ spanIndex: i, key: "status.message" });
|
|
3458
|
+
texts.push(s.status.message);
|
|
2900
3459
|
}
|
|
2901
3460
|
});
|
|
2902
3461
|
const out = spans.map((s) => ({ ...s, attributes: { ...s.attributes } }));
|
|
2903
|
-
if (texts.length === 0)
|
|
3462
|
+
if (texts.length === 0) {
|
|
3463
|
+
for (const span2 of out) normalizeToolIoAttributes(span2.attributes);
|
|
3464
|
+
return { spans: out, changed: 0 };
|
|
3465
|
+
}
|
|
2904
3466
|
const scrubbed = await redactor.redactText(texts);
|
|
3467
|
+
if (scrubbed.length !== texts.length) {
|
|
3468
|
+
throw new Error(`redactor ${redactor.name}: expected ${texts.length} values, received ${scrubbed.length}`);
|
|
3469
|
+
}
|
|
2905
3470
|
let changed = 0;
|
|
2906
|
-
|
|
3471
|
+
fields.forEach(({ spanIndex, key }, k) => {
|
|
2907
3472
|
if (scrubbed[k] !== texts[k]) {
|
|
2908
|
-
|
|
3473
|
+
if (key === "status.message") {
|
|
3474
|
+
out[spanIndex].status = { ...out[spanIndex].status, message: scrubbed[k] };
|
|
3475
|
+
} else {
|
|
3476
|
+
out[spanIndex].attributes[key] = scrubbed[k];
|
|
3477
|
+
}
|
|
2909
3478
|
changed += 1;
|
|
2910
3479
|
}
|
|
2911
3480
|
});
|
|
3481
|
+
for (const span2 of out) normalizeToolIoAttributes(span2.attributes);
|
|
2912
3482
|
return { spans: out, changed };
|
|
2913
3483
|
}
|
|
2914
3484
|
|
|
2915
3485
|
// src/file-export.ts
|
|
2916
|
-
import { createHash } from "crypto";
|
|
2917
|
-
import { mkdtemp as mkdtemp2, readFile as
|
|
3486
|
+
import { createHash as createHash4 } from "crypto";
|
|
3487
|
+
import { mkdtemp as mkdtemp2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
2918
3488
|
import { tmpdir as tmpdir2 } from "os";
|
|
2919
|
-
import { join as
|
|
3489
|
+
import { join as join13 } from "path";
|
|
3490
|
+
import {
|
|
3491
|
+
firstNumberAttr,
|
|
3492
|
+
LLM_CACHED_TOKEN_ATTR_KEYS,
|
|
3493
|
+
LLM_CACHED_TOKENS,
|
|
3494
|
+
LLM_CACHE_WRITE_TOKEN_ATTR_KEYS,
|
|
3495
|
+
LLM_CACHE_WRITE_TOKENS,
|
|
3496
|
+
LLM_COST_ATTR_KEYS,
|
|
3497
|
+
LLM_COST_USD,
|
|
3498
|
+
LLM_INPUT_TOKEN_ATTR_KEYS,
|
|
3499
|
+
LLM_INPUT_TOKENS,
|
|
3500
|
+
LLM_OUTPUT_TOKEN_ATTR_KEYS,
|
|
3501
|
+
LLM_OUTPUT_TOKENS,
|
|
3502
|
+
LLM_REASONING_TOKEN_ATTR_KEYS,
|
|
3503
|
+
LLM_REASONING_TOKENS
|
|
3504
|
+
} from "@tangle-network/agent-eval/trace-attributes";
|
|
2920
3505
|
function isObject(value) {
|
|
2921
3506
|
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
2922
3507
|
}
|
|
@@ -2936,7 +3521,7 @@ function stableJson(value) {
|
|
|
2936
3521
|
});
|
|
2937
3522
|
}
|
|
2938
3523
|
function hashId(value, chars = 16) {
|
|
2939
|
-
return
|
|
3524
|
+
return createHash4("sha256").update(stableJson(value)).digest("hex").slice(0, chars);
|
|
2940
3525
|
}
|
|
2941
3526
|
function isoTime(value) {
|
|
2942
3527
|
if (typeof value === "string") {
|
|
@@ -3039,6 +3624,11 @@ function requireOpenInferenceRows(rows) {
|
|
|
3039
3624
|
return rows;
|
|
3040
3625
|
}
|
|
3041
3626
|
function requireIntelligenceSpanRows(rows) {
|
|
3627
|
+
if (rows.length > 0 && rows.every(isOpenInferenceRow)) {
|
|
3628
|
+
throw new Error(
|
|
3629
|
+
"intelligence-spans input is already complete OpenInference; use --format auto or --format openinference to preserve it losslessly"
|
|
3630
|
+
);
|
|
3631
|
+
}
|
|
3042
3632
|
if (!rows.every(isIntelligenceSpanRow)) throw new Error("intelligence-spans input must contain only Intelligence span rows");
|
|
3043
3633
|
return rows;
|
|
3044
3634
|
}
|
|
@@ -3070,6 +3660,7 @@ function policyEvidenceToSpans(records) {
|
|
|
3070
3660
|
"traces.metrics.errored_tool_call_count": record.metrics.erroredToolCallCount,
|
|
3071
3661
|
"traces.metrics.input_tokens": record.metrics.inputTokens,
|
|
3072
3662
|
"traces.metrics.output_tokens": record.metrics.outputTokens,
|
|
3663
|
+
"traces.execution": stableJson(record.execution),
|
|
3073
3664
|
"traces.metrics.models": record.metrics.models,
|
|
3074
3665
|
"traces.metrics.tools": stableJson(record.metrics.tools),
|
|
3075
3666
|
"traces.signals.stuck_loop_count": record.signals.stuckLoopCount,
|
|
@@ -3097,6 +3688,11 @@ function policyEvidenceToSpans(records) {
|
|
|
3097
3688
|
endTime: end,
|
|
3098
3689
|
service: record.session.harness,
|
|
3099
3690
|
agent: "traces",
|
|
3691
|
+
inputTokens: record.execution.execution.tokenUsage.totals.input,
|
|
3692
|
+
outputTokens: record.execution.execution.tokenUsage.totals.output,
|
|
3693
|
+
reasoningTokens: record.execution.execution.tokenUsage.totals.reasoning,
|
|
3694
|
+
cachedInputTokens: record.execution.execution.tokenUsage.totals.cached,
|
|
3695
|
+
cacheWriteInputTokens: record.execution.execution.tokenUsage.totals.cacheWrite,
|
|
3100
3696
|
content: capText(stableJson({
|
|
3101
3697
|
session: record.session,
|
|
3102
3698
|
repo: record.repo,
|
|
@@ -3127,6 +3723,16 @@ function findStringKey(value, keys, depth = 4) {
|
|
|
3127
3723
|
}
|
|
3128
3724
|
return void 0;
|
|
3129
3725
|
}
|
|
3726
|
+
function eventValue(row, keys) {
|
|
3727
|
+
const scopes = [row, objectValue(row.data), objectValue(row.payload)];
|
|
3728
|
+
for (const scope of scopes) {
|
|
3729
|
+
if (!scope) continue;
|
|
3730
|
+
for (const key of keys) {
|
|
3731
|
+
if (scope[key] !== void 0) return scope[key];
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
return void 0;
|
|
3735
|
+
}
|
|
3130
3736
|
function eventTimestamp(row) {
|
|
3131
3737
|
return firstTimeDeep(row, [
|
|
3132
3738
|
"timestamp",
|
|
@@ -3169,7 +3775,8 @@ function eventKind(row, type) {
|
|
|
3169
3775
|
}
|
|
3170
3776
|
function sandboxEventsToSpans(rows, wrapper) {
|
|
3171
3777
|
const context = wrapper ?? {};
|
|
3172
|
-
const
|
|
3778
|
+
const sessionId = findStringKey(context, ["session_id", "sessionId"], 3) ?? findStringKey(rows, ["session_id", "sessionId"], 4);
|
|
3779
|
+
const traceId = findStringKey(context, ["trace_id", "traceId"], 3) ?? findStringKey(rows, ["trace_id", "traceId"], 4) ?? sessionId ?? findStringKey(context, ["run_id", "runId"], 3) ?? findStringKey(rows, ["run_id", "runId"], 4) ?? `sandbox:${hashId(rows, 32)}`;
|
|
3173
3780
|
const service = findStringKey(context, ["service", "harness", "source"], 2) ?? "sandbox-opencode";
|
|
3174
3781
|
const times = rows.flatMap((row) => {
|
|
3175
3782
|
const time = eventTimestamp(row);
|
|
@@ -3205,10 +3812,16 @@ function sandboxEventsToSpans(rows, wrapper) {
|
|
|
3205
3812
|
const { status, message } = eventStatus(row, type);
|
|
3206
3813
|
const { kind, tool } = eventKind(row, type);
|
|
3207
3814
|
const name = kind === "TOOL" && tool ? `tool.${tool}` : `event.${type}`;
|
|
3815
|
+
const toolInput = kind === "TOOL" ? eventValue(row, ["toolInput", "tool_input", "input", "args", "arguments", "full_command"]) : void 0;
|
|
3208
3816
|
const extra = {
|
|
3209
3817
|
"traces.source_format": "sandbox-events",
|
|
3210
3818
|
"traces.event.type": type,
|
|
3211
|
-
"traces.event.index": index
|
|
3819
|
+
"traces.event.index": index,
|
|
3820
|
+
...kind === "TOOL" ? toolIoAttributes({
|
|
3821
|
+
input: toolInput,
|
|
3822
|
+
output: eventValue(row, ["toolOutput", "tool_output", "output", "result"]),
|
|
3823
|
+
argsCaptured: toolInput !== void 0
|
|
3824
|
+
}) : {}
|
|
3212
3825
|
};
|
|
3213
3826
|
copyDefined(extra, {
|
|
3214
3827
|
"traces.event.id": stringValue(row.id),
|
|
@@ -3233,7 +3846,11 @@ function sandboxEventsToSpans(rows, wrapper) {
|
|
|
3233
3846
|
extra
|
|
3234
3847
|
}));
|
|
3235
3848
|
});
|
|
3236
|
-
return spans;
|
|
3849
|
+
if (!sessionId) return spans;
|
|
3850
|
+
return spans.map((item) => ({
|
|
3851
|
+
...item,
|
|
3852
|
+
attributes: { ...item.attributes, [ATTR.SESSION_ID]: sessionId }
|
|
3853
|
+
}));
|
|
3237
3854
|
}
|
|
3238
3855
|
function statusCode(value) {
|
|
3239
3856
|
return value === "OK" || value === "ERROR" || value === "UNSET" ? value : "UNSET";
|
|
@@ -3257,8 +3874,8 @@ function unixNanoTime(value) {
|
|
|
3257
3874
|
}
|
|
3258
3875
|
function firstNumber(row, attrs, keys) {
|
|
3259
3876
|
for (const key of keys) {
|
|
3260
|
-
const value =
|
|
3261
|
-
if (value
|
|
3877
|
+
const value = firstNumberAttr(row, [key]) ?? firstNumberAttr(attrs, [key]);
|
|
3878
|
+
if (value !== null) return value;
|
|
3262
3879
|
}
|
|
3263
3880
|
return void 0;
|
|
3264
3881
|
}
|
|
@@ -3270,6 +3887,12 @@ var COMPUTED_SPAN_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
|
3270
3887
|
"tool.name",
|
|
3271
3888
|
"llm.input_tokens",
|
|
3272
3889
|
"llm.output_tokens",
|
|
3890
|
+
LLM_INPUT_TOKENS,
|
|
3891
|
+
LLM_OUTPUT_TOKENS,
|
|
3892
|
+
LLM_REASONING_TOKENS,
|
|
3893
|
+
LLM_CACHED_TOKENS,
|
|
3894
|
+
LLM_CACHE_WRITE_TOKENS,
|
|
3895
|
+
LLM_COST_USD,
|
|
3273
3896
|
"step",
|
|
3274
3897
|
"content"
|
|
3275
3898
|
]);
|
|
@@ -3284,10 +3907,27 @@ function preserveRawAttributes(attrs) {
|
|
|
3284
3907
|
}
|
|
3285
3908
|
return preserved;
|
|
3286
3909
|
}
|
|
3910
|
+
function capturedToolIo(attrs) {
|
|
3911
|
+
const input = attrs["input.value"] ?? attrs["tool.input"] ?? attrs.tool_input ?? attrs["tool.arguments"] ?? attrs.tool_arguments ?? attrs.arguments ?? attrs.args ?? attrs.full_command;
|
|
3912
|
+
const capture = toolArgumentsFromAttributes({ ...attrs, "input.value": input });
|
|
3913
|
+
return toolIoAttributes({
|
|
3914
|
+
input: capture.args,
|
|
3915
|
+
output: attrs["output.value"] ?? attrs["tool.output"] ?? attrs.tool_output,
|
|
3916
|
+
argsCaptured: capture.argsCaptured
|
|
3917
|
+
});
|
|
3918
|
+
}
|
|
3919
|
+
function isToolTelemetrySpan(attrs) {
|
|
3920
|
+
const spanType = stringValue(attrs["span.type"])?.toLowerCase();
|
|
3921
|
+
return spanType === "tool.execution" || spanType === "tool.blocked_on_user";
|
|
3922
|
+
}
|
|
3923
|
+
function otlpSpanKind(value) {
|
|
3924
|
+
return value === "AGENT" || value === "LLM" || value === "TOOL" || value === "CHAIN" || value === "SPAN" ? value : void 0;
|
|
3925
|
+
}
|
|
3287
3926
|
function intelligenceSpanKind(name, attrs) {
|
|
3288
3927
|
const lowered = name.toLowerCase();
|
|
3289
3928
|
const spanType = stringValue(attrs["span.type"])?.toLowerCase();
|
|
3290
3929
|
if (spanType === "llm_request" || lowered.includes("llm")) return "LLM";
|
|
3930
|
+
if (isToolTelemetrySpan(attrs)) return "CHAIN";
|
|
3291
3931
|
if (spanType === "tool" || lowered.includes("tool") || attrs["tool.name"] != null || attrs.tool_name != null || attrs["mcp.tool.name"] != null || attrs["tool.input"] != null || attrs["tool.output"] != null) {
|
|
3292
3932
|
return "TOOL";
|
|
3293
3933
|
}
|
|
@@ -3302,9 +3942,12 @@ function intelligenceSpansToSpans(rows) {
|
|
|
3302
3942
|
const endTime = unixNanoTime(row.end_unix_nano) ?? isoTime(row.end_time) ?? stringValue(row.end_time) ?? startTime;
|
|
3303
3943
|
const model = stringValue(row.model) ?? stringValue(attrs["llm.model_name"]) ?? stringValue(attrs.model) ?? stringValue(attrs["gen_ai.request.model"]);
|
|
3304
3944
|
const tool = stringValue(attrs["tool.name"]) ?? stringValue(attrs.tool_name) ?? stringValue(attrs["mcp.tool.name"]);
|
|
3305
|
-
const
|
|
3945
|
+
const kind = intelligenceSpanKind(name, attrs);
|
|
3946
|
+
const sourceStatus = objectValue(row.status);
|
|
3947
|
+
const sessionId = stringValue(row.session_id) ?? sessionIdFromAttributes(attrs);
|
|
3306
3948
|
const extra = {
|
|
3307
3949
|
...preserveRawAttributes(attrs),
|
|
3950
|
+
...kind === "TOOL" ? capturedToolIo(attrs) : {},
|
|
3308
3951
|
"traces.source_format": "intelligence-spans",
|
|
3309
3952
|
"traces.row.index": index
|
|
3310
3953
|
};
|
|
@@ -3318,7 +3961,6 @@ function intelligenceSpansToSpans(rows) {
|
|
|
3318
3961
|
"generation.id": stringValue(row.generation),
|
|
3319
3962
|
"cell.id": stringValue(row.cell_id),
|
|
3320
3963
|
"project.key": stringValue(row.project_key),
|
|
3321
|
-
"llm.cost_usd": numberValue(row.cost_usd),
|
|
3322
3964
|
"redaction.version": stringValue(row.redaction_version),
|
|
3323
3965
|
"traces.received_at": isoTime(row.received_at)
|
|
3324
3966
|
});
|
|
@@ -3327,17 +3969,21 @@ function intelligenceSpansToSpans(rows) {
|
|
|
3327
3969
|
spanId: stringValue(row.id) ?? hashId({ row, field: "span" }),
|
|
3328
3970
|
parentSpanId: stringValue(row.parent_span_id) ?? null,
|
|
3329
3971
|
name,
|
|
3330
|
-
kind
|
|
3972
|
+
kind,
|
|
3331
3973
|
startTime,
|
|
3332
3974
|
endTime,
|
|
3333
|
-
status: statusCode(row.status_code),
|
|
3334
|
-
statusMessage: stringValue(row.status_message),
|
|
3975
|
+
status: statusCode(row.status_code ?? sourceStatus?.code),
|
|
3976
|
+
statusMessage: stringValue(row.status_message) ?? stringValue(sourceStatus?.message),
|
|
3335
3977
|
service: stringValue(attrs["service.name"]) ?? stringValue(attrs.service_name) ?? "tangle-intelligence",
|
|
3336
3978
|
agent: stringValue(attrs["agent.name"]) ?? stringValue(attrs["service.name"]) ?? null,
|
|
3337
3979
|
model,
|
|
3338
3980
|
tool,
|
|
3339
|
-
inputTokens: firstNumber(row, attrs, ["input_tokens",
|
|
3340
|
-
outputTokens: firstNumber(row, attrs, ["output_tokens",
|
|
3981
|
+
inputTokens: firstNumber(row, attrs, ["input_tokens", ...LLM_INPUT_TOKEN_ATTR_KEYS]),
|
|
3982
|
+
outputTokens: firstNumber(row, attrs, ["output_tokens", ...LLM_OUTPUT_TOKEN_ATTR_KEYS]),
|
|
3983
|
+
reasoningTokens: firstNumber(row, attrs, LLM_REASONING_TOKEN_ATTR_KEYS),
|
|
3984
|
+
cachedInputTokens: firstNumber(row, attrs, LLM_CACHED_TOKEN_ATTR_KEYS),
|
|
3985
|
+
cacheWriteInputTokens: firstNumber(row, attrs, LLM_CACHE_WRITE_TOKEN_ATTR_KEYS),
|
|
3986
|
+
costUsd: firstNumber(row, attrs, ["cost_usd", ...LLM_COST_ATTR_KEYS]),
|
|
3341
3987
|
step: numberValue(attrs["interaction.sequence"]) ?? index,
|
|
3342
3988
|
content: stringValue(attrs.user_prompt) ?? stringValue(attrs["gen_ai.prompt.messages"]) ?? stringValue(attrs["log.content.task"]) ?? null,
|
|
3343
3989
|
extra
|
|
@@ -3350,15 +3996,21 @@ function openInferenceToSpans(rows) {
|
|
|
3350
3996
|
const resource = objectValue(row.resource);
|
|
3351
3997
|
const resourceAttrs = objectValue(resource?.attributes);
|
|
3352
3998
|
if (resourceAttrs) copyDefined(attrs, resourceAttrs);
|
|
3353
|
-
const
|
|
3354
|
-
|
|
3999
|
+
const name = stringValue(row.name) ?? "span";
|
|
4000
|
+
const declaredKind = otlpSpanKind(attrs["openinference.span.kind"]) ?? otlpSpanKind(row.kind);
|
|
4001
|
+
const kind = isToolTelemetrySpan(attrs) ? "CHAIN" : declaredKind ?? intelligenceSpanKind(name, attrs);
|
|
4002
|
+
if (declaredKind && declaredKind !== kind) {
|
|
4003
|
+
attrs["traces.raw_attribute.openinference.span.kind"] = declaredKind;
|
|
4004
|
+
}
|
|
4005
|
+
attrs["openinference.span.kind"] = kind;
|
|
4006
|
+
if (kind === "TOOL") Object.assign(attrs, capturedToolIo(attrs));
|
|
3355
4007
|
const status = objectValue(row.status);
|
|
3356
4008
|
const message = stringValue(status?.message);
|
|
3357
4009
|
return {
|
|
3358
4010
|
trace_id: stringValue(row.trace_id) ?? hashId(row, 32),
|
|
3359
4011
|
span_id: stringValue(row.span_id) ?? hashId({ row, field: "span" }),
|
|
3360
4012
|
parent_span_id: stringValue(row.parent_span_id) || null,
|
|
3361
|
-
name
|
|
4013
|
+
name,
|
|
3362
4014
|
start_time: stringValue(row.start_time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
3363
4015
|
end_time: stringValue(row.end_time) ?? stringValue(row.start_time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
3364
4016
|
status: { code: statusCode(status?.code), ...message ? { message } : {} },
|
|
@@ -3405,20 +4057,53 @@ function exportTraceEvidenceText(text, opts = {}) {
|
|
|
3405
4057
|
return exportTraceEvidenceRows(parsed.rows, opts, parsed.wrapper);
|
|
3406
4058
|
}
|
|
3407
4059
|
async function exportTraceEvidenceFile(inputPath, opts = {}) {
|
|
3408
|
-
|
|
4060
|
+
if (/\.(?:jsonl|ndjson)$/i.test(inputPath)) {
|
|
4061
|
+
let format;
|
|
4062
|
+
const spans = [];
|
|
4063
|
+
const sandboxRows = [];
|
|
4064
|
+
let redactionCount = 0;
|
|
4065
|
+
const redactionsByRule = {};
|
|
4066
|
+
for await (const row of readJsonl(inputPath)) {
|
|
4067
|
+
format ??= detectFormat([row], opts.format ?? "auto");
|
|
4068
|
+
if (format === "sandbox-events") {
|
|
4069
|
+
sandboxRows.push(row);
|
|
4070
|
+
continue;
|
|
4071
|
+
}
|
|
4072
|
+
const converted = exportTraceEvidenceRows([row], {
|
|
4073
|
+
...opts,
|
|
4074
|
+
sourcePath: inputPath,
|
|
4075
|
+
format
|
|
4076
|
+
});
|
|
4077
|
+
appendAll(spans, converted.spans);
|
|
4078
|
+
redactionCount += converted.redactionCount;
|
|
4079
|
+
for (const [rule, count2] of Object.entries(converted.redactionsByRule)) {
|
|
4080
|
+
redactionsByRule[rule] = (redactionsByRule[rule] ?? 0) + count2;
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
if (!format) throw new Error("input file is empty");
|
|
4084
|
+
if (format === "sandbox-events") {
|
|
4085
|
+
return exportTraceEvidenceRows(sandboxRows, {
|
|
4086
|
+
...opts,
|
|
4087
|
+
sourcePath: inputPath,
|
|
4088
|
+
format
|
|
4089
|
+
});
|
|
4090
|
+
}
|
|
4091
|
+
return { format, spans, redactionCount, redactionsByRule };
|
|
4092
|
+
}
|
|
4093
|
+
const text = await readFile2(inputPath, "utf8");
|
|
3409
4094
|
return exportTraceEvidenceText(text, { ...opts, sourcePath: inputPath });
|
|
3410
4095
|
}
|
|
3411
4096
|
async function writeTraceEvidenceExportFile(inputPath, outPath, opts = {}) {
|
|
3412
4097
|
const result = await exportTraceEvidenceFile(inputPath, opts);
|
|
3413
|
-
const path = outPath ??
|
|
4098
|
+
const path = outPath ?? join13(await mkdtemp2(join13(tmpdir2(), "traces-export-")), "spans.openinference.jsonl");
|
|
3414
4099
|
await writeFile2(path, serializeSpans(result.spans), "utf8");
|
|
3415
4100
|
return { ...result, path };
|
|
3416
4101
|
}
|
|
3417
4102
|
|
|
3418
4103
|
// src/inspect.ts
|
|
3419
|
-
import { mkdtemp as mkdtemp3, readFile as
|
|
4104
|
+
import { mkdtemp as mkdtemp3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
3420
4105
|
import { tmpdir as tmpdir3 } from "os";
|
|
3421
|
-
import { basename as basename8, join as
|
|
4106
|
+
import { basename as basename8, join as join14 } from "path";
|
|
3422
4107
|
var severityRank = {
|
|
3423
4108
|
high: 0,
|
|
3424
4109
|
medium: 1,
|
|
@@ -3681,35 +4366,308 @@ function renderInspectionReport(report) {
|
|
|
3681
4366
|
lines.push("Refs:");
|
|
3682
4367
|
for (const ref of finding2.refs.slice(0, 5)) lines.push(`- ${ref}`);
|
|
3683
4368
|
}
|
|
3684
|
-
});
|
|
3685
|
-
return `${lines.join("\n")}
|
|
3686
|
-
`;
|
|
3687
|
-
}
|
|
3688
|
-
function serializeInspectionReport(report) {
|
|
3689
|
-
return `${JSON.stringify(report, null, 2)}
|
|
3690
|
-
`;
|
|
3691
|
-
}
|
|
3692
|
-
function assertSessionIndex(value) {
|
|
3693
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3694
|
-
throw new Error("session index file must contain a JSON object");
|
|
4369
|
+
});
|
|
4370
|
+
return `${lines.join("\n")}
|
|
4371
|
+
`;
|
|
4372
|
+
}
|
|
4373
|
+
function serializeInspectionReport(report) {
|
|
4374
|
+
return `${JSON.stringify(report, null, 2)}
|
|
4375
|
+
`;
|
|
4376
|
+
}
|
|
4377
|
+
function assertSessionIndex(value) {
|
|
4378
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4379
|
+
throw new Error("session index file must contain a JSON object");
|
|
4380
|
+
}
|
|
4381
|
+
const candidate = value;
|
|
4382
|
+
if (candidate.kind !== "traces.session_index" || candidate.schemaVersion !== 1 || !Array.isArray(candidate.sessions)) {
|
|
4383
|
+
throw new Error("input is not a traces.session_index file with schemaVersion 1");
|
|
4384
|
+
}
|
|
4385
|
+
if (!candidate.totals || typeof candidate.totals !== "object") {
|
|
4386
|
+
throw new Error("session index is missing totals");
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
async function readSessionIndexFile(path) {
|
|
4390
|
+
const parsed = JSON.parse(await readFile3(path, "utf8"));
|
|
4391
|
+
assertSessionIndex(parsed);
|
|
4392
|
+
return parsed;
|
|
4393
|
+
}
|
|
4394
|
+
async function writeInspectionReportFile(report, outPath) {
|
|
4395
|
+
const path = outPath ?? join14(await mkdtemp3(join14(tmpdir3(), "traces-inspect-")), "inspection-report.md");
|
|
4396
|
+
await writeFile3(path, renderInspectionReport(report), "utf8");
|
|
4397
|
+
return path;
|
|
4398
|
+
}
|
|
4399
|
+
|
|
4400
|
+
// src/adoption.ts
|
|
4401
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
4402
|
+
import { join as join15 } from "path";
|
|
4403
|
+
function toolName(s) {
|
|
4404
|
+
const n = s.attributes["tool.name"];
|
|
4405
|
+
if (typeof n === "string") return n;
|
|
4406
|
+
if (s.name.startsWith("tool.")) return s.name.slice("tool.".length);
|
|
4407
|
+
return null;
|
|
4408
|
+
}
|
|
4409
|
+
function parseInput(s) {
|
|
4410
|
+
const captured = toolArgumentsFromAttributes(s.attributes);
|
|
4411
|
+
if (!captured.argsCaptured) return {};
|
|
4412
|
+
if (captured.args && typeof captured.args === "object") {
|
|
4413
|
+
return captured.args;
|
|
4414
|
+
}
|
|
4415
|
+
if (typeof captured.args !== "string") return {};
|
|
4416
|
+
try {
|
|
4417
|
+
const v = JSON.parse(captured.args);
|
|
4418
|
+
return v && typeof v === "object" ? v : {};
|
|
4419
|
+
} catch {
|
|
4420
|
+
return {};
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4423
|
+
function skillNameOf(input) {
|
|
4424
|
+
const v = input.skill ?? input.command;
|
|
4425
|
+
return typeof v === "string" && v.length > 0 ? v : "?";
|
|
4426
|
+
}
|
|
4427
|
+
function subagentTypeOf(input) {
|
|
4428
|
+
const v = input.subagent_type ?? input.agent_type ?? input.type ?? input.name;
|
|
4429
|
+
return typeof v === "string" && v.length > 0 ? v : "?";
|
|
4430
|
+
}
|
|
4431
|
+
function isSpawnAgentTool(name) {
|
|
4432
|
+
return name === "spawn_agent" || name.endsWith("__spawn_agent");
|
|
4433
|
+
}
|
|
4434
|
+
function skillTelemetryCapability(service) {
|
|
4435
|
+
if (service === "claude-code") return "supported";
|
|
4436
|
+
if (service === "codex") return "unsupported";
|
|
4437
|
+
return "unknown";
|
|
4438
|
+
}
|
|
4439
|
+
var MATERIALIZED_SKILL_RE = /<skills_instructions>|(?:^|\n)#{1,3}\s+(?:available\s+)?skills\b|base directory for this skill:/i;
|
|
4440
|
+
function hasMaterializedSkillEvidence(s) {
|
|
4441
|
+
if (s.name !== "message.developer" && s.attributes["tangle.actor"] !== "injected") return false;
|
|
4442
|
+
const content = s.attributes.content;
|
|
4443
|
+
return typeof content === "string" && MATERIALIZED_SKILL_RE.test(content);
|
|
4444
|
+
}
|
|
4445
|
+
var SKILL_DOCUMENT_READ_COMMAND = /\b(?:cat|sed|head|tail|less|more|awk)\b/;
|
|
4446
|
+
function skillDocumentNames(s) {
|
|
4447
|
+
if (s.attributes["openinference.span.kind"] !== "TOOL") return [];
|
|
4448
|
+
if (s.status.code !== "OK" || !SKILL_DOCUMENT_READ_COMMAND.test(String(s.attributes["input.value"] ?? ""))) return [];
|
|
4449
|
+
const input = s.attributes["input.value"];
|
|
4450
|
+
if (typeof input !== "string") return [];
|
|
4451
|
+
const names = /* @__PURE__ */ new Set();
|
|
4452
|
+
for (const match of input.matchAll(/(?:^|[\\/'"])([^\\/'"\s]+)[\\/]SKILL\.md\b/g)) {
|
|
4453
|
+
const name = match[1];
|
|
4454
|
+
if (/^[A-Za-z][A-Za-z0-9-]*$/.test(name)) names.add(name);
|
|
4455
|
+
}
|
|
4456
|
+
return [...names];
|
|
4457
|
+
}
|
|
4458
|
+
function telemetryStatus(measured, unsupported, unknown) {
|
|
4459
|
+
if (measured > 0 && unsupported === 0 && unknown === 0) return "measured";
|
|
4460
|
+
if (measured > 0) return "partial";
|
|
4461
|
+
if (unsupported > 0 && unknown === 0) return "unsupported";
|
|
4462
|
+
return "unknown";
|
|
4463
|
+
}
|
|
4464
|
+
function skillNamesFromRun(record) {
|
|
4465
|
+
const names = [];
|
|
4466
|
+
if (Array.isArray(record.skills)) {
|
|
4467
|
+
for (const skill of record.skills) if (typeof skill === "string" && skill) names.push(skill);
|
|
4468
|
+
}
|
|
4469
|
+
if (typeof record.skill === "string" && record.skill) names.push(record.skill);
|
|
4470
|
+
return names.length > 0 ? names : ["?"];
|
|
4471
|
+
}
|
|
4472
|
+
function sessionIdFromRun(record) {
|
|
4473
|
+
for (const key of ["sessionId", "session_id", "traceId", "trace_id"]) {
|
|
4474
|
+
if (typeof record[key] === "string" && record[key]) return record[key];
|
|
4475
|
+
}
|
|
4476
|
+
return void 0;
|
|
4477
|
+
}
|
|
4478
|
+
function addCounts(target, names) {
|
|
4479
|
+
for (const name of names) target[name] = (target[name] ?? 0) + 1;
|
|
4480
|
+
}
|
|
4481
|
+
function countSkillRunsJsonl(raw) {
|
|
4482
|
+
const out = {};
|
|
4483
|
+
for (const line of raw.split("\n")) {
|
|
4484
|
+
const t = line.trim();
|
|
4485
|
+
if (!t) continue;
|
|
4486
|
+
let o;
|
|
4487
|
+
try {
|
|
4488
|
+
o = JSON.parse(t);
|
|
4489
|
+
} catch {
|
|
4490
|
+
continue;
|
|
4491
|
+
}
|
|
4492
|
+
if (!o || typeof o !== "object") continue;
|
|
4493
|
+
const rec = o;
|
|
4494
|
+
addCounts(out, skillNamesFromRun(rec));
|
|
4495
|
+
}
|
|
4496
|
+
return out;
|
|
4497
|
+
}
|
|
4498
|
+
async function readLoopRuns(cwds, sessionIds2) {
|
|
4499
|
+
const linkedCounts = {};
|
|
4500
|
+
const unlinkedCounts = {};
|
|
4501
|
+
let filesRead = 0;
|
|
4502
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4503
|
+
for (const cwd of cwds) {
|
|
4504
|
+
if (!cwd || seen.has(cwd)) continue;
|
|
4505
|
+
seen.add(cwd);
|
|
4506
|
+
const path = join15(cwd, ".evolve", "skill-runs.jsonl");
|
|
4507
|
+
let raw;
|
|
4508
|
+
try {
|
|
4509
|
+
raw = await readFile4(path, "utf8");
|
|
4510
|
+
} catch {
|
|
4511
|
+
continue;
|
|
4512
|
+
}
|
|
4513
|
+
filesRead += 1;
|
|
4514
|
+
for (const line of raw.split("\n")) {
|
|
4515
|
+
if (!line.trim()) continue;
|
|
4516
|
+
let value;
|
|
4517
|
+
try {
|
|
4518
|
+
value = JSON.parse(line);
|
|
4519
|
+
} catch {
|
|
4520
|
+
continue;
|
|
4521
|
+
}
|
|
4522
|
+
if (!value || typeof value !== "object") continue;
|
|
4523
|
+
const record = value;
|
|
4524
|
+
const target = sessionIds2.has(sessionIdFromRun(record) ?? "") ? linkedCounts : unlinkedCounts;
|
|
4525
|
+
addCounts(target, skillNamesFromRun(record));
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
return { linkedCounts, unlinkedCounts, filesRead };
|
|
4529
|
+
}
|
|
4530
|
+
async function analyzeAdoption(spans, opts = {}) {
|
|
4531
|
+
const { sessionByTrace, conflicts } = indexSessionIdsByTrace(spans);
|
|
4532
|
+
const identifiedSessions = new Set(sessionByTrace.values());
|
|
4533
|
+
const unassignedTraceIds = new Set(
|
|
4534
|
+
spans.map((span2) => span2.trace_id).filter((traceId) => !sessionByTrace.has(traceId))
|
|
4535
|
+
);
|
|
4536
|
+
const executionGroup = (span2) => sessionByTrace.get(span2.trace_id) ?? `trace:${span2.trace_id}`;
|
|
4537
|
+
const sessions = /* @__PURE__ */ new Set();
|
|
4538
|
+
const sessionsWithSkill = /* @__PURE__ */ new Set();
|
|
4539
|
+
const sessionsWithSubagent = /* @__PURE__ */ new Set();
|
|
4540
|
+
const sessionsWithMaterializedSkills = /* @__PURE__ */ new Set();
|
|
4541
|
+
const sessionsWithSkillFileReference = /* @__PURE__ */ new Set();
|
|
4542
|
+
const skillInvocations = {};
|
|
4543
|
+
const skillDocumentReads = {};
|
|
4544
|
+
const subagentSpawns = {};
|
|
4545
|
+
const canonicalSubagentSessions = /* @__PURE__ */ new Set();
|
|
4546
|
+
const fallbackSubagents = /* @__PURE__ */ new Map();
|
|
4547
|
+
const sessionCapabilities = /* @__PURE__ */ new Map();
|
|
4548
|
+
for (const s of spans) {
|
|
4549
|
+
const group = executionGroup(s);
|
|
4550
|
+
sessions.add(group);
|
|
4551
|
+
const observedCapability = skillTelemetryCapability(s.attributes["service.name"]);
|
|
4552
|
+
const currentCapability = sessionCapabilities.get(group) ?? "unknown";
|
|
4553
|
+
if (observedCapability === "supported" || currentCapability === "unknown") {
|
|
4554
|
+
sessionCapabilities.set(group, observedCapability);
|
|
4555
|
+
}
|
|
4556
|
+
if (hasMaterializedSkillEvidence(s)) sessionsWithMaterializedSkills.add(group);
|
|
4557
|
+
const skillDocuments = skillDocumentNames(s);
|
|
4558
|
+
if (skillDocuments.length > 0) {
|
|
4559
|
+
sessionsWithSkillFileReference.add(group);
|
|
4560
|
+
addCounts(skillDocumentReads, skillDocuments);
|
|
4561
|
+
}
|
|
4562
|
+
const tn = toolName(s);
|
|
4563
|
+
if (tn === "Skill") {
|
|
4564
|
+
sessionCapabilities.set(group, "supported");
|
|
4565
|
+
const name = skillNameOf(parseInput(s));
|
|
4566
|
+
skillInvocations[name] = (skillInvocations[name] ?? 0) + 1;
|
|
4567
|
+
sessionsWithSkill.add(group);
|
|
4568
|
+
} else if (tn === "Task" || tn === "Agent") {
|
|
4569
|
+
const type = subagentTypeOf(parseInput(s));
|
|
4570
|
+
subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
|
|
4571
|
+
sessionsWithSubagent.add(group);
|
|
4572
|
+
canonicalSubagentSessions.add(group);
|
|
4573
|
+
} else if (tn && isSpawnAgentTool(tn)) {
|
|
4574
|
+
const types = fallbackSubagents.get(group) ?? [];
|
|
4575
|
+
types.push(subagentTypeOf(parseInput(s)));
|
|
4576
|
+
fallbackSubagents.set(group, types);
|
|
4577
|
+
}
|
|
3695
4578
|
}
|
|
3696
|
-
const
|
|
3697
|
-
|
|
3698
|
-
|
|
4579
|
+
for (const [traceId, types] of fallbackSubagents) {
|
|
4580
|
+
if (canonicalSubagentSessions.has(traceId)) continue;
|
|
4581
|
+
sessionsWithSubagent.add(traceId);
|
|
4582
|
+
for (const type of types) {
|
|
4583
|
+
subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
|
|
4584
|
+
}
|
|
3699
4585
|
}
|
|
3700
|
-
|
|
3701
|
-
|
|
4586
|
+
const {
|
|
4587
|
+
linkedCounts: loopDispatchedRuns,
|
|
4588
|
+
unlinkedCounts: unlinkedLoopDispatchedRuns,
|
|
4589
|
+
filesRead
|
|
4590
|
+
} = await readLoopRuns(opts.cwds ?? [], identifiedSessions);
|
|
4591
|
+
const totalSkillInvocations = Object.values(skillInvocations).reduce((a, b) => a + b, 0);
|
|
4592
|
+
const totalSkillDocumentReads = Object.values(skillDocumentReads).reduce((a, b) => a + b, 0);
|
|
4593
|
+
const totalSubagentSpawns = Object.values(subagentSpawns).reduce((a, b) => a + b, 0);
|
|
4594
|
+
const totalLoopDispatchedRuns = Object.values(loopDispatchedRuns).reduce((a, b) => a + b, 0);
|
|
4595
|
+
const totalUnlinkedLoopDispatchedRuns = Object.values(unlinkedLoopDispatchedRuns).reduce((a, b) => a + b, 0);
|
|
4596
|
+
let skillTelemetrySessions = 0;
|
|
4597
|
+
let skillTelemetryUnsupportedSessions = 0;
|
|
4598
|
+
let skillTelemetryUnknownSessions = 0;
|
|
4599
|
+
for (const traceId of sessions) {
|
|
4600
|
+
const capability = sessionCapabilities.get(traceId) ?? "unknown";
|
|
4601
|
+
if (capability === "supported") skillTelemetrySessions += 1;
|
|
4602
|
+
else if (capability === "unsupported") skillTelemetryUnsupportedSessions += 1;
|
|
4603
|
+
else skillTelemetryUnknownSessions += 1;
|
|
3702
4604
|
}
|
|
4605
|
+
return {
|
|
4606
|
+
executionGroupCount: sessions.size,
|
|
4607
|
+
identifiedSessionCount: identifiedSessions.size,
|
|
4608
|
+
unassignedTraceCount: unassignedTraceIds.size,
|
|
4609
|
+
sessionIdentityConflicts: conflicts,
|
|
4610
|
+
sessionsWithSkill: sessionsWithSkill.size,
|
|
4611
|
+
skillPenetration: skillTelemetrySessions === 0 ? null : sessionsWithSkill.size / skillTelemetrySessions,
|
|
4612
|
+
skillTelemetryStatus: telemetryStatus(
|
|
4613
|
+
skillTelemetrySessions,
|
|
4614
|
+
skillTelemetryUnsupportedSessions,
|
|
4615
|
+
skillTelemetryUnknownSessions
|
|
4616
|
+
),
|
|
4617
|
+
skillTelemetrySessions,
|
|
4618
|
+
sessionsWithMaterializedSkills: sessionsWithMaterializedSkills.size,
|
|
4619
|
+
sessionsWithSkillFileReference: sessionsWithSkillFileReference.size,
|
|
4620
|
+
skillDocumentReads,
|
|
4621
|
+
totalSkillDocumentReads,
|
|
4622
|
+
skillInvocations,
|
|
4623
|
+
totalSkillInvocations,
|
|
4624
|
+
subagentSpawns,
|
|
4625
|
+
totalSubagentSpawns,
|
|
4626
|
+
sessionsWithSubagent: sessionsWithSubagent.size,
|
|
4627
|
+
loopDispatchedRuns,
|
|
4628
|
+
totalLoopDispatchedRuns,
|
|
4629
|
+
skillRunFilesRead: filesRead,
|
|
4630
|
+
unlinkedLoopDispatchedRuns,
|
|
4631
|
+
totalUnlinkedLoopDispatchedRuns
|
|
4632
|
+
};
|
|
3703
4633
|
}
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
async function
|
|
3710
|
-
|
|
3711
|
-
await
|
|
3712
|
-
|
|
4634
|
+
|
|
4635
|
+
// src/analyze.ts
|
|
4636
|
+
import { buildDefaultAnalystRegistry } from "@tangle-network/agent-eval/analyst";
|
|
4637
|
+
import { OtlpFileTraceStore } from "@tangle-network/agent-eval/traces";
|
|
4638
|
+
var GENERATED_TRACE_FILE_CEILING = 512 * 1024 * 1024;
|
|
4639
|
+
async function analyzeSpans(spans, opts = {}) {
|
|
4640
|
+
if (spans.length === 0) throw new Error("analyzeSpans: no spans to analyze");
|
|
4641
|
+
const otlpPath = await writeOtlpFile(spans, opts.otlpOutPath);
|
|
4642
|
+
const runId = opts.runId ?? `traces-${Date.now()}`;
|
|
4643
|
+
const execution = summarizeSpanExecution(spans, {
|
|
4644
|
+
experimentId: runId
|
|
4645
|
+
});
|
|
4646
|
+
const detStore = new OtlpFileTraceStore({
|
|
4647
|
+
path: otlpPath,
|
|
4648
|
+
maxFileBytes: GENERATED_TRACE_FILE_CEILING,
|
|
4649
|
+
perCallByteCeiling: GENERATED_TRACE_FILE_CEILING
|
|
4650
|
+
});
|
|
4651
|
+
await detStore.ensureIndexed();
|
|
4652
|
+
const detRegistry = opts.registry ?? buildDefaultAnalystRegistry({ registry: { log: opts.log } });
|
|
4653
|
+
const result = await detRegistry.run(runId, { traceStore: detStore });
|
|
4654
|
+
if (opts.ai) {
|
|
4655
|
+
const agStore = new OtlpFileTraceStore({ path: otlpPath, maxFileBytes: GENERATED_TRACE_FILE_CEILING });
|
|
4656
|
+
await agStore.ensureIndexed();
|
|
4657
|
+
const agRegistry = buildDefaultAnalystRegistry({
|
|
4658
|
+
ai: opts.ai,
|
|
4659
|
+
model: opts.model,
|
|
4660
|
+
includeBehavioral: false,
|
|
4661
|
+
registry: { log: opts.log }
|
|
4662
|
+
});
|
|
4663
|
+
const agResult = await agRegistry.run(runId, { traceStore: agStore }, {
|
|
4664
|
+
budget: opts.budgetUsd != null ? { totalUsd: opts.budgetUsd } : void 0
|
|
4665
|
+
});
|
|
4666
|
+
result.findings.push(...agResult.findings);
|
|
4667
|
+
result.per_analyst.push(...agResult.per_analyst);
|
|
4668
|
+
result.total_cost_usd += agResult.total_cost_usd;
|
|
4669
|
+
}
|
|
4670
|
+
return { otlpPath, execution, result };
|
|
3713
4671
|
}
|
|
3714
4672
|
|
|
3715
4673
|
// src/reactions.ts
|
|
@@ -3800,6 +4758,8 @@ function analyzeReactions(spans) {
|
|
|
3800
4758
|
|
|
3801
4759
|
// src/report.ts
|
|
3802
4760
|
var SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
4761
|
+
var CORRUPTION_RECEIPT_DISPLAY_LIMIT = 100;
|
|
4762
|
+
var SOURCE_DISPLAY_LIMIT = 20;
|
|
3803
4763
|
var SEVERITY_BADGE = {
|
|
3804
4764
|
critical: "\u{1F534} CRITICAL",
|
|
3805
4765
|
high: "\u{1F7E0} HIGH",
|
|
@@ -3811,23 +4771,107 @@ function deterministicSummaryText(summary) {
|
|
|
3811
4771
|
const parts = [];
|
|
3812
4772
|
if (summary.stuckLoops > 0) parts.push(`${summary.stuckLoops} stuck loop(s)`);
|
|
3813
4773
|
if (summary.reactionSignals > 0) parts.push(`${summary.reactionSignals} human reaction signal(s)`);
|
|
3814
|
-
if (summary.
|
|
4774
|
+
if (summary.failedRuns > 0) parts.push(`${summary.failedRuns} failed run(s)`);
|
|
3815
4775
|
if (parts.length === 0) return "0 deterministic signals";
|
|
3816
4776
|
if (parts.length === 1) return parts[0];
|
|
3817
4777
|
return `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
|
|
3818
4778
|
}
|
|
3819
|
-
function plural(
|
|
3820
|
-
return
|
|
4779
|
+
function plural(count2, singular, pluralForm = `${singular}s`) {
|
|
4780
|
+
return count2 === 1 ? singular : pluralForm;
|
|
4781
|
+
}
|
|
4782
|
+
function tableCell(value) {
|
|
4783
|
+
return value.replace(/\s+/g, " ").replaceAll("|", "\\|").trim();
|
|
4784
|
+
}
|
|
4785
|
+
function count(value) {
|
|
4786
|
+
return Math.round(value).toLocaleString("en-US");
|
|
4787
|
+
}
|
|
4788
|
+
function decimal(value, digits = 1) {
|
|
4789
|
+
return value.toLocaleString("en-US", {
|
|
4790
|
+
minimumFractionDigits: digits,
|
|
4791
|
+
maximumFractionDigits: digits
|
|
4792
|
+
});
|
|
4793
|
+
}
|
|
4794
|
+
function distributionRow(label, distribution, unit = "") {
|
|
4795
|
+
if (distribution.n === 0) return `| ${label} | 0 | not captured | not captured | not captured | not captured | not captured |`;
|
|
4796
|
+
const value = (number) => `${decimal(number)}${unit}`;
|
|
4797
|
+
return `| ${label} | ${count(distribution.n)} | ${value(distribution.min)} | ${value(distribution.mean)} | ${value(distribution.p50)} | ${value(distribution.p95)} | ${value(distribution.max)} |`;
|
|
4798
|
+
}
|
|
4799
|
+
function tokenRows(usage) {
|
|
4800
|
+
const rows = [
|
|
4801
|
+
["Input", usage.totals.input, usage.input],
|
|
4802
|
+
["Output", usage.totals.output, usage.output],
|
|
4803
|
+
["Reasoning (output subset)", usage.totals.reasoning, usage.reasoning],
|
|
4804
|
+
["Cache read", usage.totals.cached, usage.cached],
|
|
4805
|
+
["Cache write", usage.totals.cacheWrite, usage.cacheWrite]
|
|
4806
|
+
];
|
|
4807
|
+
return rows.map(
|
|
4808
|
+
([label, total, values]) => `| ${label} | ${count(total)} | ${count(values.n)} | ${decimal(values.mean)} | ${decimal(values.p50)} | ${decimal(values.p95)} | ${count(values.max)} |`
|
|
4809
|
+
);
|
|
4810
|
+
}
|
|
4811
|
+
function renderExecution(report) {
|
|
4812
|
+
const execution = report.execution;
|
|
4813
|
+
const lines = ["## execution facts", ""];
|
|
4814
|
+
lines.push(
|
|
4815
|
+
`- **Sessions:** ${count(execution.durationMs.n)} | **Model-call runs:** ${count(execution.modelCalls.runs)} | **Model-call events:** ${count(execution.modelCalls.events)} | **Sessions with tool errors:** ${count(execution.failures.runs)}/${count(execution.failures.reportingRuns)} (${decimal(execution.failures.fraction * 100, 2)}%)`
|
|
4816
|
+
);
|
|
4817
|
+
lines.push("- **Task quality:** not measured; these traces do not include comparable task outcome labels.");
|
|
4818
|
+
lines.push("");
|
|
4819
|
+
lines.push("| Time | Sessions measured | Min | Mean | p50 | p95 | Max |");
|
|
4820
|
+
lines.push("|---|---:|---:|---:|---:|---:|---:|");
|
|
4821
|
+
lines.push(distributionRow("Recorded session interval", execution.durationMs, "ms"));
|
|
4822
|
+
lines.push(distributionRow("Queue", execution.queueMs, "ms"));
|
|
4823
|
+
lines.push("");
|
|
4824
|
+
lines.push("Recorded session interval is first-to-last trace time and can include idle time; it is not active agent runtime.");
|
|
4825
|
+
lines.push("");
|
|
4826
|
+
lines.push("### Direct model usage");
|
|
4827
|
+
lines.push("");
|
|
4828
|
+
lines.push("| Token category | Total | Runs measured | Mean | p50 | p95 | Max |");
|
|
4829
|
+
lines.push("|---|---:|---:|---:|---:|---:|---:|");
|
|
4830
|
+
lines.push(...tokenRows(execution.tokenUsage));
|
|
4831
|
+
lines.push("");
|
|
4832
|
+
lines.push("| Model | Runs |");
|
|
4833
|
+
lines.push("|---|---:|");
|
|
4834
|
+
for (const model of execution.models) {
|
|
4835
|
+
lines.push(`| \`${tableCell(model.model)}\` | ${count(model.runs)} |`);
|
|
4836
|
+
}
|
|
4837
|
+
lines.push("");
|
|
4838
|
+
lines.push("### Cost coverage");
|
|
4839
|
+
lines.push("");
|
|
4840
|
+
lines.push("| Source | Runs | Total USD |");
|
|
4841
|
+
lines.push("|---|---:|---:|");
|
|
4842
|
+
lines.push(`| Observed | ${count(report.costProvenance.observed.n)} | $${report.costProvenance.observed.totalUsd.toFixed(6)} |`);
|
|
4843
|
+
lines.push(`| Estimated | ${count(report.costProvenance.estimated.n)} | $${report.costProvenance.estimated.totalUsd.toFixed(6)} |`);
|
|
4844
|
+
lines.push(`| Uncaptured | ${count(report.costProvenance.uncaptured.n)} | not available |`);
|
|
4845
|
+
lines.push("");
|
|
4846
|
+
lines.push(`Known cost coverage: ${decimal(report.costProvenance.knownFraction * 100, 2)}%.`);
|
|
4847
|
+
lines.push("");
|
|
4848
|
+
if (execution.aggregateUsage.runs > 0) {
|
|
4849
|
+
lines.push("### Orchestration-reported usage");
|
|
4850
|
+
lines.push("");
|
|
4851
|
+
lines.push(
|
|
4852
|
+
`Kept separate from direct model usage because orchestration totals can overlap model-call telemetry. ${count(execution.aggregateUsage.runs)} run(s) reported aggregate values.`
|
|
4853
|
+
);
|
|
4854
|
+
lines.push("");
|
|
4855
|
+
lines.push("| Token category | Total | Runs measured | Mean | p50 | p95 | Max |");
|
|
4856
|
+
lines.push("|---|---:|---:|---:|---:|---:|---:|");
|
|
4857
|
+
lines.push(...tokenRows(execution.aggregateUsage.tokenUsage));
|
|
4858
|
+
lines.push("");
|
|
4859
|
+
lines.push(
|
|
4860
|
+
`Aggregate cost: $${execution.aggregateUsage.totalCostUsd.toFixed(6)} across ${count(execution.aggregateUsage.costUsd.n)} reporting run(s).`
|
|
4861
|
+
);
|
|
4862
|
+
lines.push("");
|
|
4863
|
+
}
|
|
4864
|
+
return lines.join("\n");
|
|
3821
4865
|
}
|
|
3822
4866
|
function summarizeDeterministicSignals(pipelines, reactions) {
|
|
3823
4867
|
const stuckLoops = pipelines.stuckLoops.findings.length;
|
|
3824
|
-
const reactionSignals = Object.values(reactions.signals).reduce((total,
|
|
3825
|
-
const
|
|
4868
|
+
const reactionSignals = Object.values(reactions.signals).reduce((total, count2) => total + count2, 0);
|
|
4869
|
+
const failedRuns = pipelines.failureClusters.totalFailures;
|
|
3826
4870
|
return {
|
|
3827
4871
|
stuckLoops,
|
|
3828
4872
|
reactionSignals,
|
|
3829
|
-
|
|
3830
|
-
totalSignals: stuckLoops + reactionSignals +
|
|
4873
|
+
failedRuns,
|
|
4874
|
+
totalSignals: stuckLoops + reactionSignals + failedRuns
|
|
3831
4875
|
};
|
|
3832
4876
|
}
|
|
3833
4877
|
function renderReport(result, meta) {
|
|
@@ -3835,14 +4879,93 @@ function renderReport(result, meta) {
|
|
|
3835
4879
|
const findings = [...result.findings].sort(
|
|
3836
4880
|
(a, b) => (SEVERITY_RANK[a.severity] ?? 9) - (SEVERITY_RANK[b.severity] ?? 9)
|
|
3837
4881
|
);
|
|
3838
|
-
const deterministic = meta.deterministic ?? { stuckLoops: 0, reactionSignals: 0,
|
|
3839
|
-
const findingSummary = deterministic.totalSignals > 0 ? `${findings.length}
|
|
4882
|
+
const deterministic = meta.deterministic ?? { stuckLoops: 0, reactionSignals: 0, failedRuns: 0, totalSignals: 0 };
|
|
4883
|
+
const findingSummary = deterministic.totalSignals > 0 ? `${findings.length} ${plural(findings.length, "finding")}; ${deterministic.totalSignals} raw deterministic ${plural(deterministic.totalSignals, "signal")}` : `${findings.length} ${plural(findings.length, "finding")}`;
|
|
3840
4884
|
lines.push(`# Trace analysis \u2014 ${meta.harness}`);
|
|
3841
4885
|
lines.push("");
|
|
4886
|
+
const sessionSummary = meta.unassignedTraceCount ? `${meta.sessionCount} identified session(s) + ${meta.unassignedTraceCount} ${plural(meta.unassignedTraceCount, "trace")} without a single stable session identity` : `${meta.sessionCount} session(s)`;
|
|
3842
4887
|
lines.push(
|
|
3843
|
-
`${
|
|
4888
|
+
`${sessionSummary}, ${meta.spanCount} spans \u2192 **${findingSummary}** across ${result.per_analyst.length} analyst(s). Analysis cost: $${result.total_cost_usd.toFixed(4)}.`
|
|
3844
4889
|
);
|
|
3845
4890
|
lines.push("");
|
|
4891
|
+
if (meta.unassignedTraceCount) {
|
|
4892
|
+
lines.push(
|
|
4893
|
+
`> ${meta.unassignedTraceCount} ${plural(meta.unassignedTraceCount, "trace")} lacked a single stable session identity. Their spans were analyzed but were not relabeled as sessions.`
|
|
4894
|
+
);
|
|
4895
|
+
lines.push("");
|
|
4896
|
+
}
|
|
4897
|
+
lines.push(renderExecution(meta.execution));
|
|
4898
|
+
lines.push("");
|
|
4899
|
+
if (meta.sources && meta.sources.length > 0) {
|
|
4900
|
+
lines.push("## Selected sessions");
|
|
4901
|
+
lines.push("");
|
|
4902
|
+
lines.push("| Role | Session ID | Parent session | Integrity | Subject (first prompt line) | Source path |");
|
|
4903
|
+
lines.push("|---|---|---|---|---|---|");
|
|
4904
|
+
for (const source of meta.sources.slice(0, SOURCE_DISPLAY_LIMIT)) {
|
|
4905
|
+
const corruptionCount = source.corruptionCount ?? source.corruptions?.length ?? 0;
|
|
4906
|
+
const integrity = source.integrity === "degraded_not_lossless" ? `degraded, not lossless (${corruptionCount} corrupt ${plural(corruptionCount, "record")})` : "complete";
|
|
4907
|
+
lines.push(
|
|
4908
|
+
`| ${source.role} | \`${tableCell(source.sessionId)}\` | ${source.parentSessionId ? `\`${tableCell(source.parentSessionId)}\`` : "\u2014"} | ${integrity} | ${tableCell(source.subject) || "(no prompt captured)"} | \`${tableCell(source.path)}\` |`
|
|
4909
|
+
);
|
|
4910
|
+
}
|
|
4911
|
+
if (meta.sources.length > SOURCE_DISPLAY_LIMIT) {
|
|
4912
|
+
lines.push(
|
|
4913
|
+
`| \u2026 | ${meta.sources.length - SOURCE_DISPLAY_LIMIT} additional sessions omitted | \u2014 | \u2014 | \u2014 | \u2014 |`
|
|
4914
|
+
);
|
|
4915
|
+
}
|
|
4916
|
+
lines.push("");
|
|
4917
|
+
const childCount = meta.sources.filter((source) => source.role === "child").length;
|
|
4918
|
+
if (childCount > 0) {
|
|
4919
|
+
lines.push(
|
|
4920
|
+
`> Scope: ${childCount}/${meta.sources.length} selected session(s) are children. Counts below describe only the selected files, not their parent operator sessions.`
|
|
4921
|
+
);
|
|
4922
|
+
lines.push("");
|
|
4923
|
+
}
|
|
4924
|
+
const totalCorruptionCount = meta.sources.reduce(
|
|
4925
|
+
(total, source) => total + (source.corruptionCount ?? source.corruptions?.length ?? 0),
|
|
4926
|
+
0
|
|
4927
|
+
);
|
|
4928
|
+
const corruptions = [];
|
|
4929
|
+
for (const source of meta.sources) {
|
|
4930
|
+
for (const receipt of source.corruptions ?? []) {
|
|
4931
|
+
if (corruptions.length === CORRUPTION_RECEIPT_DISPLAY_LIMIT) break;
|
|
4932
|
+
corruptions.push(receipt);
|
|
4933
|
+
}
|
|
4934
|
+
if (corruptions.length === CORRUPTION_RECEIPT_DISPLAY_LIMIT) break;
|
|
4935
|
+
}
|
|
4936
|
+
if (totalCorruptionCount > 0) {
|
|
4937
|
+
lines.push("## Source corruption receipts");
|
|
4938
|
+
lines.push("");
|
|
4939
|
+
lines.push(
|
|
4940
|
+
"> Degraded, not lossless: every valid JSONL record was analyzed. Malformed content is fingerprinted, not retained; exact bytes are retrievable only while the local source file still contains that byte range."
|
|
4941
|
+
);
|
|
4942
|
+
lines.push("");
|
|
4943
|
+
for (const source of meta.sources) {
|
|
4944
|
+
const count2 = source.corruptionCount ?? source.corruptions?.length ?? 0;
|
|
4945
|
+
if (count2 === 0) continue;
|
|
4946
|
+
const digest = source.corruptionDigest ? `, digest \`${source.corruptionDigest}\`` : "";
|
|
4947
|
+
lines.push(`- Session \`${tableCell(source.sessionId)}\`: ${count2} ${plural(count2, "receipt")}${digest}.`);
|
|
4948
|
+
}
|
|
4949
|
+
lines.push("");
|
|
4950
|
+
if (corruptions.length > 0) {
|
|
4951
|
+
lines.push("| Session ID | Source path | Line | Byte offset | Byte length | SHA-256 | Raw bytes |");
|
|
4952
|
+
lines.push("|---|---|---:|---:|---:|---|---|");
|
|
4953
|
+
for (const receipt of corruptions) {
|
|
4954
|
+
lines.push(
|
|
4955
|
+
`| \`${tableCell(receipt.sessionId)}\` | \`${tableCell(receipt.sourcePath)}\` | ${receipt.lineNumber} | ${receipt.byteOffset} | ${receipt.byteLength} | \`${receipt.sha256}\` | local source only |`
|
|
4956
|
+
);
|
|
4957
|
+
}
|
|
4958
|
+
lines.push("");
|
|
4959
|
+
}
|
|
4960
|
+
const omitted = totalCorruptionCount - corruptions.length;
|
|
4961
|
+
if (omitted > 0) {
|
|
4962
|
+
lines.push(
|
|
4963
|
+
`_${omitted} additional ${plural(omitted, "receipt")} omitted from this report; all receipts remain in \`source.corruption.receipt\` child spans._`
|
|
4964
|
+
);
|
|
4965
|
+
lines.push("");
|
|
4966
|
+
}
|
|
4967
|
+
}
|
|
4968
|
+
}
|
|
3846
4969
|
lines.push("| Analyst | Status | Findings | Latency |");
|
|
3847
4970
|
lines.push("|---|---|---|---|");
|
|
3848
4971
|
for (const s of result.per_analyst) {
|
|
@@ -3850,7 +4973,7 @@ function renderReport(result, meta) {
|
|
|
3850
4973
|
}
|
|
3851
4974
|
lines.push("");
|
|
3852
4975
|
if (findings.length === 0 && deterministic.totalSignals === 0) {
|
|
3853
|
-
lines.push("_No
|
|
4976
|
+
lines.push("_No supported behavioral findings in the captured fields._");
|
|
3854
4977
|
} else if (findings.length === 0) {
|
|
3855
4978
|
lines.push(
|
|
3856
4979
|
`_No analyst findings. Deterministic checks found ${deterministicSummaryText(deterministic)}; see sections below._`
|
|
@@ -3871,6 +4994,7 @@ function renderReport(result, meta) {
|
|
|
3871
4994
|
if (f.subject) lines.push(`- **Subject:** \`${f.subject}\``);
|
|
3872
4995
|
lines.push(`- **Confidence:** ${f.confidence}`);
|
|
3873
4996
|
if (f.recommended_action) lines.push(`- **Fix:** ${f.recommended_action}`);
|
|
4997
|
+
if (f.validation_plan) lines.push(`- **Check:** ${f.validation_plan}`);
|
|
3874
4998
|
for (const ev of f.evidence_refs.slice(0, 3)) {
|
|
3875
4999
|
const excerpt = ev.excerpt ? ` \u2014 \`${ev.excerpt.slice(0, 200)}\`` : "";
|
|
3876
5000
|
lines.push(`- **Evidence:** ${ev.kind} ${ev.uri}${excerpt}`);
|
|
@@ -3885,20 +5009,79 @@ function renderReport(result, meta) {
|
|
|
3885
5009
|
return lines.join("\n");
|
|
3886
5010
|
}
|
|
3887
5011
|
function renderPipelines(pr) {
|
|
3888
|
-
const lines = ["## loops & waste (deterministic)", ""];
|
|
5012
|
+
const lines = ["## reliability, loops & waste (deterministic)", ""];
|
|
5013
|
+
lines.push(
|
|
5014
|
+
`- **Execution failures:** ${pr.failureClusters.totalFailures}/${pr.failureClusters.totalRuns} run(s)`
|
|
5015
|
+
);
|
|
5016
|
+
if (pr.failureClusters.clusters.length > 0) {
|
|
5017
|
+
lines.push("");
|
|
5018
|
+
lines.push("| Failure class | Runs | Tool | Example trace | Example error |");
|
|
5019
|
+
lines.push("|---|---:|---|---|---|");
|
|
5020
|
+
for (const cluster of pr.failureClusters.clusters.slice(0, 10)) {
|
|
5021
|
+
lines.push(
|
|
5022
|
+
`| ${tableCell(cluster.failureClass)} | ${cluster.runCount} | ${cluster.toolName ? `\`${tableCell(cluster.toolName)}\`` : "not captured"} | ${cluster.exampleRunId ? `\`${tableCell(cluster.exampleRunId)}\`` : "not captured"} | ${tableCell(cluster.exampleError ?? "not captured")} |`
|
|
5023
|
+
);
|
|
5024
|
+
}
|
|
5025
|
+
lines.push("");
|
|
5026
|
+
}
|
|
3889
5027
|
if (pr.stuckLoops.findings.length === 0) {
|
|
3890
|
-
lines.push("- **Stuck loops:** none (no tool called \u22653\xD7 with identical args).");
|
|
5028
|
+
lines.push("- **Stuck loops:** none (no tool called \u22653\xD7 with identical args in a short interval).");
|
|
3891
5029
|
} else {
|
|
3892
5030
|
lines.push(`- **Stuck loops:** ${pr.stuckLoops.findings.length} (${(pr.stuckLoops.affectedRunRatio * 100).toFixed(0)}% of runs affected)`);
|
|
3893
5031
|
for (const f of pr.stuckLoops.findings.sort((a, b) => b.occurrences - a.occurrences).slice(0, 10)) {
|
|
3894
5032
|
lines.push(` - \u{1F501} \`${f.toolName}\` \xD7${f.occurrences} with identical args over ${(f.windowMs / 1e3).toFixed(1)}s`);
|
|
3895
5033
|
}
|
|
3896
5034
|
}
|
|
3897
|
-
|
|
3898
|
-
|
|
5035
|
+
const runsWithTools = pr.toolUse.filter((metrics) => metrics.totalCalls > 0);
|
|
5036
|
+
if (runsWithTools.length > 0) {
|
|
5037
|
+
let totalCalls = 0;
|
|
5038
|
+
let callsWithCapturedArgs = 0;
|
|
5039
|
+
let duplicateCalls = 0;
|
|
5040
|
+
let errorCalls = 0;
|
|
5041
|
+
let followedFailures = 0;
|
|
5042
|
+
const byTool = /* @__PURE__ */ new Map();
|
|
5043
|
+
for (const metrics of runsWithTools) {
|
|
5044
|
+
const toolStats = Object.values(metrics.byTool);
|
|
5045
|
+
const duplicates = toolStats.length > 0 ? toolStats.reduce((total, stats) => total + stats.duplicates, 0) : Math.round(metrics.duplicateRate * metrics.totalCalls);
|
|
5046
|
+
const errors = toolStats.length > 0 ? toolStats.reduce((total, stats) => total + stats.errors, 0) : Math.round(metrics.errorRate * metrics.totalCalls);
|
|
5047
|
+
totalCalls += metrics.totalCalls;
|
|
5048
|
+
callsWithCapturedArgs += metrics.callsWithCapturedArgs;
|
|
5049
|
+
duplicateCalls += duplicates;
|
|
5050
|
+
errorCalls += errors;
|
|
5051
|
+
followedFailures += Math.round(metrics.retryRate * errors);
|
|
5052
|
+
for (const [name, stats] of Object.entries(metrics.byTool)) {
|
|
5053
|
+
const aggregate = byTool.get(name) ?? {
|
|
5054
|
+
calls: 0,
|
|
5055
|
+
captured: 0,
|
|
5056
|
+
duplicates: 0,
|
|
5057
|
+
errors: 0,
|
|
5058
|
+
latencyMs: 0
|
|
5059
|
+
};
|
|
5060
|
+
aggregate.calls += stats.calls;
|
|
5061
|
+
aggregate.captured += stats.callsWithCapturedArgs;
|
|
5062
|
+
aggregate.duplicates += stats.duplicates;
|
|
5063
|
+
aggregate.errors += stats.errors;
|
|
5064
|
+
aggregate.latencyMs += stats.avgLatencyMs * stats.calls;
|
|
5065
|
+
byTool.set(name, aggregate);
|
|
5066
|
+
}
|
|
5067
|
+
}
|
|
5068
|
+
const failureFollowUp = errorCalls > 0 ? `; ${followedFailures}/${errorCalls} failed calls followed by another same-tool call (${(followedFailures / errorCalls * 100).toFixed(0)}%)` : "";
|
|
5069
|
+
const duplicateSummary = callsWithCapturedArgs > 0 ? `${duplicateCalls}/${callsWithCapturedArgs} captured calls repeated exactly` : "exact repeats unavailable";
|
|
3899
5070
|
lines.push(
|
|
3900
|
-
`- **Tool use
|
|
5071
|
+
`- **Tool use:** ${totalCalls} calls across ${runsWithTools.length}/${pr.toolUse.length} traces; ${callsWithCapturedArgs}/${totalCalls} arguments captured; ${duplicateSummary}; ${errorCalls}/${totalCalls} failed${failureFollowUp}`
|
|
3901
5072
|
);
|
|
5073
|
+
if (byTool.size > 0) {
|
|
5074
|
+
lines.push("");
|
|
5075
|
+
lines.push("| Tool | Calls | Args captured | Exact repeats | Errors | Mean latency |");
|
|
5076
|
+
lines.push("|---|---:|---:|---:|---:|---:|");
|
|
5077
|
+
for (const [name, stats] of [...byTool].sort(
|
|
5078
|
+
(left, right) => right[1].calls - left[1].calls || left[0].localeCompare(right[0])
|
|
5079
|
+
)) {
|
|
5080
|
+
lines.push(
|
|
5081
|
+
`| \`${tableCell(name)}\` | ${stats.calls} | ${stats.captured}/${stats.calls} | ${stats.duplicates}/${stats.captured} | ${stats.errors}/${stats.calls} | ${(stats.latencyMs / stats.calls).toFixed(1)}ms |`
|
|
5082
|
+
);
|
|
5083
|
+
}
|
|
5084
|
+
}
|
|
3902
5085
|
}
|
|
3903
5086
|
lines.push("");
|
|
3904
5087
|
return lines.join("\n");
|
|
@@ -3952,26 +5135,66 @@ function renderReactions(rr) {
|
|
|
3952
5135
|
}
|
|
3953
5136
|
function renderAdoption(ar) {
|
|
3954
5137
|
const lines = ["## skill & subagent adoption (deterministic)", ""];
|
|
5138
|
+
const scopeParts = [`${ar.identifiedSessionCount} identified session(s)`];
|
|
5139
|
+
if (ar.unassignedTraceCount > 0) {
|
|
5140
|
+
scopeParts.push(`${ar.unassignedTraceCount} trace(s) without a single stable session identity`);
|
|
5141
|
+
}
|
|
5142
|
+
lines.push(`- **Scope:** ${scopeParts.join(" + ")}`);
|
|
5143
|
+
if (ar.sessionIdentityConflicts.length > 0) {
|
|
5144
|
+
const displayed = ar.sessionIdentityConflicts.slice(0, SOURCE_DISPLAY_LIMIT);
|
|
5145
|
+
const details = displayed.map((conflict) => `\`${conflict.traceId}\` (${conflict.sessionIds.map((id) => `\`${id}\``).join(", ")})`).join("; ");
|
|
5146
|
+
const omitted = ar.sessionIdentityConflicts.length - displayed.length;
|
|
5147
|
+
lines.push(
|
|
5148
|
+
`- **Conflicting session identity:** ${ar.sessionIdentityConflicts.length} trace(s): ${details}${omitted > 0 ? `; ${omitted} more omitted from this report` : ""}`
|
|
5149
|
+
);
|
|
5150
|
+
}
|
|
5151
|
+
lines.push(
|
|
5152
|
+
"- **Prompt, tools, MCP, hooks, and full agent profile:** not assessed; no baseline profile was supplied."
|
|
5153
|
+
);
|
|
5154
|
+
if (ar.skillPenetration === null) {
|
|
5155
|
+
const label = ar.skillTelemetryStatus === "unsupported" ? "uncaptured/unsupported" : "uncaptured/unknown";
|
|
5156
|
+
lines.push(
|
|
5157
|
+
`- **Explicit skill invocation rate:** ${label} (${ar.skillTelemetrySessions}/${ar.executionGroupCount} observed group(s) expose a dedicated Skill event)`
|
|
5158
|
+
);
|
|
5159
|
+
} else {
|
|
5160
|
+
const excluded = ar.executionGroupCount - ar.skillTelemetrySessions;
|
|
5161
|
+
lines.push(
|
|
5162
|
+
`- **Explicit skill invocation rate:** ${(ar.skillPenetration * 100).toFixed(0)}% (${ar.sessionsWithSkill}/${ar.skillTelemetrySessions} measurable group(s)${excluded > 0 ? `; ${excluded} unmeasurable group(s) excluded` : ""})`
|
|
5163
|
+
);
|
|
5164
|
+
}
|
|
3955
5165
|
lines.push(
|
|
3956
|
-
`- **
|
|
5166
|
+
`- **Materialized skill catalogs/instructions:** ${ar.sessionsWithMaterializedSkills}/${ar.executionGroupCount} observed group(s) \xB7 **Sessions with successful skill-document reads:** ${ar.sessionsWithSkillFileReference}/${ar.executionGroupCount}`
|
|
3957
5167
|
);
|
|
3958
5168
|
lines.push(
|
|
3959
|
-
`- **Explicit skill invocations:** ${ar.totalSkillInvocations} (Skill tool spans) \xB7 **
|
|
5169
|
+
`- **Explicit skill invocations:** ${ar.totalSkillInvocations} (Skill tool spans) \xB7 **Session-linked loop runs:** ${ar.totalLoopDispatchedRuns}`
|
|
3960
5170
|
);
|
|
5171
|
+
if (ar.totalUnlinkedLoopDispatchedRuns > 0) {
|
|
5172
|
+
lines.push(
|
|
5173
|
+
`- **Unlinked repository history:** ${ar.totalUnlinkedLoopDispatchedRuns} run(s) from ${ar.skillRunFilesRead} \`.evolve/skill-runs.jsonl\` file(s); not attributed to these sessions.`
|
|
5174
|
+
);
|
|
5175
|
+
}
|
|
5176
|
+
if (ar.totalSkillDocumentReads > 0) {
|
|
5177
|
+
lines.push(`- **Successful skill-document reads:** ${ar.totalSkillDocumentReads}; inspection is not outcome evidence.`);
|
|
5178
|
+
}
|
|
3961
5179
|
lines.push(
|
|
3962
|
-
`- **Subagent spawns:** ${ar.totalSubagentSpawns} across ${ar.sessionsWithSubagent}
|
|
5180
|
+
`- **Subagent spawns observed:** ${ar.totalSubagentSpawns} across ${ar.sessionsWithSubagent} observed group(s); capture completeness depends on source telemetry.`
|
|
3963
5181
|
);
|
|
3964
5182
|
lines.push("");
|
|
3965
5183
|
const skillRows = Object.entries(ar.skillInvocations).sort((a, b) => b[1] - a[1]);
|
|
5184
|
+
const documentRows = Object.entries(ar.skillDocumentReads).sort((a, b) => b[1] - a[1]);
|
|
3966
5185
|
const loopRows = Object.entries(ar.loopDispatchedRuns).sort((a, b) => b[1] - a[1]);
|
|
3967
|
-
if (skillRows.length > 0 || loopRows.length > 0) {
|
|
3968
|
-
lines.push("| Skill | Explicit invocations |
|
|
3969
|
-
lines.push("
|
|
3970
|
-
const names = [.../* @__PURE__ */ new Set([
|
|
3971
|
-
(
|
|
5186
|
+
if (skillRows.length > 0 || documentRows.length > 0 || loopRows.length > 0) {
|
|
5187
|
+
lines.push("| Skill | Explicit invocations | Documents read | Session-linked loop runs |");
|
|
5188
|
+
lines.push("|---|---|---|---|");
|
|
5189
|
+
const names = [.../* @__PURE__ */ new Set([
|
|
5190
|
+
...skillRows.map((r) => r[0]),
|
|
5191
|
+
...documentRows.map((r) => r[0]),
|
|
5192
|
+
...loopRows.map((r) => r[0])
|
|
5193
|
+
])].sort(
|
|
5194
|
+
(a, b) => (ar.skillInvocations[b] ?? 0) + (ar.skillDocumentReads[b] ?? 0) + (ar.loopDispatchedRuns[b] ?? 0) - (ar.skillInvocations[a] ?? 0) - (ar.skillDocumentReads[a] ?? 0) - (ar.loopDispatchedRuns[a] ?? 0)
|
|
3972
5195
|
);
|
|
3973
5196
|
for (const n of names) {
|
|
3974
|
-
lines.push(`| \`${n}\` | ${ar.skillInvocations[n] ?? 0} | ${ar.loopDispatchedRuns[n] ?? 0} |`);
|
|
5197
|
+
lines.push(`| \`${n}\` | ${ar.skillInvocations[n] ?? 0} | ${ar.skillDocumentReads[n] ?? 0} | ${ar.loopDispatchedRuns[n] ?? 0} |`);
|
|
3975
5198
|
}
|
|
3976
5199
|
lines.push("");
|
|
3977
5200
|
}
|
|
@@ -4002,9 +5225,6 @@ var severityRank2 = {
|
|
|
4002
5225
|
low: 3,
|
|
4003
5226
|
info: 4
|
|
4004
5227
|
};
|
|
4005
|
-
function slug(input) {
|
|
4006
|
-
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72) || "item";
|
|
4007
|
-
}
|
|
4008
5228
|
function json(value) {
|
|
4009
5229
|
return `${JSON.stringify(value, null, 2)}
|
|
4010
5230
|
`;
|
|
@@ -4017,13 +5237,76 @@ function evidence(kind, uri, excerpt) {
|
|
|
4017
5237
|
return excerpt ? { kind, uri, excerpt } : { kind, uri };
|
|
4018
5238
|
}
|
|
4019
5239
|
function totalReactionSignals(reactions) {
|
|
4020
|
-
return Object.values(reactions.signals).reduce((sum,
|
|
5240
|
+
return Object.values(reactions.signals).reduce((sum, count2) => sum + count2, 0);
|
|
4021
5241
|
}
|
|
4022
|
-
function
|
|
4023
|
-
|
|
5242
|
+
function traceCoverageFinding(pipelines, adoption) {
|
|
5243
|
+
const toolCalls = pipelines.toolUse.reduce((total, run) => total + run.totalCalls, 0);
|
|
5244
|
+
const capturedToolCalls = pipelines.toolUse.reduce(
|
|
5245
|
+
(total, run) => total + run.callsWithCapturedArgs,
|
|
5246
|
+
0
|
|
5247
|
+
);
|
|
5248
|
+
const missingToolArgs = toolCalls - capturedToolCalls;
|
|
5249
|
+
const traces = pipelines.toolUse.length;
|
|
5250
|
+
const missingSessionIds = adoption.unassignedTraceCount;
|
|
5251
|
+
const sessionIdentityConflicts = adoption.sessionIdentityConflicts;
|
|
5252
|
+
const unmeasurableSkillGroups = adoption.executionGroupCount - adoption.skillTelemetrySessions;
|
|
5253
|
+
if (missingToolArgs === 0 && missingSessionIds === 0 && unmeasurableSkillGroups === 0) {
|
|
5254
|
+
return void 0;
|
|
5255
|
+
}
|
|
5256
|
+
const parts = [];
|
|
5257
|
+
const refs = [];
|
|
5258
|
+
if (missingToolArgs > 0) {
|
|
5259
|
+
parts.push(`${missingToolArgs}/${toolCalls} tool call(s) lacked comparable arguments`);
|
|
5260
|
+
refs.push(evidence("metric", "coverage.tool_arguments", `${capturedToolCalls}/${toolCalls} captured`));
|
|
5261
|
+
}
|
|
5262
|
+
if (missingSessionIds > 0) {
|
|
5263
|
+
parts.push(`${missingSessionIds}/${traces} trace(s) lacked a single stable session identity`);
|
|
5264
|
+
refs.push(evidence("metric", "coverage.session_identity", `${traces - missingSessionIds}/${traces} identified`));
|
|
5265
|
+
}
|
|
5266
|
+
if (sessionIdentityConflicts.length > 0) {
|
|
5267
|
+
refs.push(evidence(
|
|
5268
|
+
"span",
|
|
5269
|
+
"coverage.session_identity_conflicts",
|
|
5270
|
+
sessionIdentityConflicts.map((conflict) => conflict.traceId).join(", ")
|
|
5271
|
+
));
|
|
5272
|
+
}
|
|
5273
|
+
if (unmeasurableSkillGroups > 0) {
|
|
5274
|
+
parts.push(`${unmeasurableSkillGroups}/${adoption.executionGroupCount} execution group(s) lacked measurable skill events`);
|
|
5275
|
+
refs.push(evidence(
|
|
5276
|
+
"metric",
|
|
5277
|
+
"coverage.skill_events",
|
|
5278
|
+
`${adoption.skillTelemetrySessions}/${adoption.executionGroupCount} measurable`
|
|
5279
|
+
));
|
|
5280
|
+
}
|
|
5281
|
+
const argumentCoverage = toolCalls === 0 ? 1 : capturedToolCalls / toolCalls;
|
|
5282
|
+
const sessionCoverage = traces === 0 ? 1 : (traces - missingSessionIds) / traces;
|
|
5283
|
+
const severity = argumentCoverage < 0.5 || sessionCoverage < 0.5 ? "high" : argumentCoverage < 0.9 || sessionCoverage < 0.9 || unmeasurableSkillGroups > 0 ? "medium" : "low";
|
|
5284
|
+
return makeFinding({
|
|
5285
|
+
analyst_id: "traces-deterministic",
|
|
5286
|
+
area: "instrumentation",
|
|
5287
|
+
claim: `Trace capture is incomplete: ${parts.join("; ")}`,
|
|
5288
|
+
severity,
|
|
5289
|
+
rationale: "Missing identity or agent-profile evidence prevents reliable cross-run comparison and can hide tool, prompt, skill, or subagent failures.",
|
|
5290
|
+
evidence_refs: refs,
|
|
5291
|
+
recommended_action: "Update the source telemetry bridge once: emit a stable session ID, complete canonical tool arguments, explicit argument-capture status, and dedicated skill/subagent events before optimizing the agent profile.",
|
|
5292
|
+
validation_plan: "Rerun the same source and require every trace to carry exactly one session identity, every tool call to declare and preserve its full arguments, and every supported execution group to expose skill events.",
|
|
5293
|
+
confidence: 1,
|
|
5294
|
+
metadata: {
|
|
5295
|
+
source: "traces.coverage",
|
|
5296
|
+
toolCalls,
|
|
5297
|
+
capturedToolCalls,
|
|
5298
|
+
traces,
|
|
5299
|
+
missingSessionIds,
|
|
5300
|
+
sessionIdentityConflictCount: sessionIdentityConflicts.length,
|
|
5301
|
+
skillTelemetryGroups: adoption.skillTelemetrySessions,
|
|
5302
|
+
executionGroups: adoption.executionGroupCount
|
|
5303
|
+
}
|
|
5304
|
+
});
|
|
4024
5305
|
}
|
|
4025
5306
|
function deterministicFindings(pipelines, reactions, adoption) {
|
|
4026
5307
|
const findings = [];
|
|
5308
|
+
const coverage = traceCoverageFinding(pipelines, adoption);
|
|
5309
|
+
if (coverage) findings.push(coverage);
|
|
4027
5310
|
const loops = pipelines.stuckLoops.findings;
|
|
4028
5311
|
if (loops.length > 0) {
|
|
4029
5312
|
const top = [...loops].sort((a, b) => b.occurrences - a.occurrences)[0];
|
|
@@ -4043,19 +5326,34 @@ function deterministicFindings(pipelines, reactions, adoption) {
|
|
|
4043
5326
|
metadata: { source: "traces.pipeline.stuckLoopView", loopCount: loops.length }
|
|
4044
5327
|
}));
|
|
4045
5328
|
}
|
|
4046
|
-
const
|
|
4047
|
-
if (
|
|
5329
|
+
const failedRuns = pipelines.failureClusters.totalFailures;
|
|
5330
|
+
if (failedRuns > 0) {
|
|
5331
|
+
const top = pipelines.failureClusters.clusters[0];
|
|
5332
|
+
const failureRate = failedRuns / Math.max(1, pipelines.failureClusters.totalRuns);
|
|
4048
5333
|
findings.push(makeFinding({
|
|
4049
5334
|
analyst_id: "traces-deterministic",
|
|
4050
|
-
area: "
|
|
4051
|
-
claim: `${
|
|
4052
|
-
severity:
|
|
4053
|
-
rationale: "
|
|
4054
|
-
evidence_refs: [
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
5335
|
+
area: "reliability",
|
|
5336
|
+
claim: `${failedRuns}/${pipelines.failureClusters.totalRuns} run(s) had execution errors`,
|
|
5337
|
+
severity: failureRate >= 0.1 ? "high" : "medium",
|
|
5338
|
+
rationale: "Execution errors consume time and tokens, and sparse error telemetry can hide whether the agent recovered or repeated the same failure.",
|
|
5339
|
+
evidence_refs: [
|
|
5340
|
+
evidence(
|
|
5341
|
+
"metric",
|
|
5342
|
+
`pipelines.failure_cluster.${top?.failureClass ?? "unknown"}`,
|
|
5343
|
+
top ? `${top.runCount} run(s); example: ${top.exampleError ?? "error details not captured"}` : `${failedRuns} failed run(s)`
|
|
5344
|
+
),
|
|
5345
|
+
...top?.exampleRunId ? [evidence("span", `trace:${top.exampleRunId}`, top.exampleError)] : []
|
|
5346
|
+
],
|
|
5347
|
+
recommended_action: top?.failureClass === "unknown" ? "Instrument the failing operation name and arguments, then start with the highest-frequency example error and verify the agent changes state before retrying." : "Fix the highest-frequency failure cluster first, then require the agent to inspect the error and change state before retrying.",
|
|
5348
|
+
validation_plan: "Rerun a comparable trace window and require failed runs and repeated failures in the top cluster to decrease.",
|
|
5349
|
+
confidence: 1,
|
|
5350
|
+
metadata: {
|
|
5351
|
+
source: "agent-eval.failureClusterView",
|
|
5352
|
+
failedRuns,
|
|
5353
|
+
totalRuns: pipelines.failureClusters.totalRuns,
|
|
5354
|
+
topFailureClass: top?.failureClass,
|
|
5355
|
+
exampleRunId: top?.exampleRunId
|
|
5356
|
+
}
|
|
4059
5357
|
}));
|
|
4060
5358
|
}
|
|
4061
5359
|
const corrective = reactions.signals.correction + reactions.signals.frustration + reactions.signals.jargon + reactions.signals.structure;
|
|
@@ -4074,7 +5372,8 @@ function deterministicFindings(pipelines, reactions, adoption) {
|
|
|
4074
5372
|
}));
|
|
4075
5373
|
}
|
|
4076
5374
|
const skillRuns = adoption.totalSkillInvocations + adoption.totalLoopDispatchedRuns;
|
|
4077
|
-
|
|
5375
|
+
const allSessionsMeasurable = adoption.skillTelemetrySessions === adoption.executionGroupCount;
|
|
5376
|
+
if (allSessionsMeasurable && adoption.executionGroupCount > 0 && skillRuns === 0) {
|
|
4078
5377
|
findings.push(makeFinding({
|
|
4079
5378
|
analyst_id: "traces-deterministic",
|
|
4080
5379
|
area: "workflow",
|
|
@@ -4085,7 +5384,7 @@ function deterministicFindings(pipelines, reactions, adoption) {
|
|
|
4085
5384
|
recommended_action: "Create or invoke a narrow skill for the repeated workflow, then let traces compare future sessions against this baseline.",
|
|
4086
5385
|
validation_plan: "Rerun traces and require skill-run attribution to appear for repeated workflows.",
|
|
4087
5386
|
confidence: 0.75,
|
|
4088
|
-
metadata: { source: "traces.adoption",
|
|
5387
|
+
metadata: { source: "traces.adoption", executionGroupCount: adoption.executionGroupCount }
|
|
4089
5388
|
}));
|
|
4090
5389
|
}
|
|
4091
5390
|
return findings;
|
|
@@ -4146,41 +5445,6 @@ function normalizeExternalFindings(results) {
|
|
|
4146
5445
|
}
|
|
4147
5446
|
return out;
|
|
4148
5447
|
}
|
|
4149
|
-
function sourceOf(finding2) {
|
|
4150
|
-
if (finding2.analyst_id.startsWith("external:")) return "external";
|
|
4151
|
-
if (finding2.analyst_id === "traces-deterministic") return "deterministic";
|
|
4152
|
-
return "analyst";
|
|
4153
|
-
}
|
|
4154
|
-
function recommendationTitle(finding2) {
|
|
4155
|
-
if (finding2.recommended_action) return finding2.recommended_action.replace(/\.$/, "");
|
|
4156
|
-
return `Investigate ${finding2.area}: ${finding2.claim}`.replace(/\.$/, "");
|
|
4157
|
-
}
|
|
4158
|
-
function buildRecommendations(findings) {
|
|
4159
|
-
return [...findings].filter((finding2) => finding2.severity !== "info").sort((a, b) => severityRank2[a.severity] - severityRank2[b.severity] || b.confidence - a.confidence || a.finding_id.localeCompare(b.finding_id)).slice(0, 20).map((finding2, index) => ({
|
|
4160
|
-
schemaVersion: 1,
|
|
4161
|
-
id: `rec-${index + 1}-${slug(finding2.area)}`,
|
|
4162
|
-
severity: finding2.severity,
|
|
4163
|
-
title: recommendationTitle(finding2),
|
|
4164
|
-
action: finding2.recommended_action ?? `Review and fix: ${finding2.claim}`,
|
|
4165
|
-
rationale: finding2.rationale ?? finding2.claim,
|
|
4166
|
-
evidenceRefs: finding2.evidence_refs,
|
|
4167
|
-
findingIds: [finding2.finding_id],
|
|
4168
|
-
validationPlan: finding2.validation_plan ?? "Rerun `traces improve` on a fresh comparable session window and confirm this finding disappears or drops in severity.",
|
|
4169
|
-
source: sourceOf(finding2)
|
|
4170
|
-
}));
|
|
4171
|
-
}
|
|
4172
|
-
function buildClaims(findings) {
|
|
4173
|
-
return findings.map((finding2, index) => ({
|
|
4174
|
-
schemaVersion: 1,
|
|
4175
|
-
id: `claim-${index + 1}-${slug(finding2.area)}`,
|
|
4176
|
-
severity: finding2.severity,
|
|
4177
|
-
text: finding2.claim,
|
|
4178
|
-
area: finding2.area,
|
|
4179
|
-
sourceFindingId: finding2.finding_id,
|
|
4180
|
-
evidenceRefs: finding2.evidence_refs,
|
|
4181
|
-
validationPlan: finding2.validation_plan
|
|
4182
|
-
}));
|
|
4183
|
-
}
|
|
4184
5448
|
function buildEvidenceRows(findings) {
|
|
4185
5449
|
return findings.flatMap((finding2) => finding2.evidence_refs.map((ref) => ({
|
|
4186
5450
|
schemaVersion: 1,
|
|
@@ -4192,31 +5456,10 @@ function buildEvidenceRows(findings) {
|
|
|
4192
5456
|
evidence: ref
|
|
4193
5457
|
})));
|
|
4194
5458
|
}
|
|
4195
|
-
function renderRecommendations(recommendations) {
|
|
4196
|
-
const lines = ["## recommendations", ""];
|
|
4197
|
-
if (recommendations.length === 0) {
|
|
4198
|
-
lines.push("- No recommendations emitted.");
|
|
4199
|
-
lines.push("");
|
|
4200
|
-
return lines.join("\n");
|
|
4201
|
-
}
|
|
4202
|
-
for (const rec of recommendations.slice(0, 10)) {
|
|
4203
|
-
lines.push(`### ${rec.severity.toUpperCase()} \u2014 ${rec.title}`);
|
|
4204
|
-
lines.push("");
|
|
4205
|
-
lines.push(`- **Action:** ${rec.action}`);
|
|
4206
|
-
lines.push(`- **Why:** ${rec.rationale}`);
|
|
4207
|
-
lines.push(`- **Check:** ${rec.validationPlan}`);
|
|
4208
|
-
for (const ref of rec.evidenceRefs.slice(0, 3)) {
|
|
4209
|
-
lines.push(`- **Evidence:** ${ref.kind} ${ref.uri}${ref.excerpt ? ` \u2014 \`${ref.excerpt.slice(0, 180)}\`` : ""}`);
|
|
4210
|
-
}
|
|
4211
|
-
lines.push("");
|
|
4212
|
-
}
|
|
4213
|
-
return lines.join("\n");
|
|
4214
|
-
}
|
|
4215
5459
|
function renderFindingPacket(packet, title = "Trace finding packet") {
|
|
4216
5460
|
const lines = [`# ${title}`, ""];
|
|
4217
|
-
lines.push(`${packet.findings.length} finding(s)
|
|
5461
|
+
lines.push(`${packet.findings.length} evidence-backed finding(s) with actions and checks.`);
|
|
4218
5462
|
lines.push("");
|
|
4219
|
-
lines.push(renderRecommendations(packet.recommendations));
|
|
4220
5463
|
if (packet.findings.length > 0) {
|
|
4221
5464
|
lines.push("## findings");
|
|
4222
5465
|
lines.push("");
|
|
@@ -4242,33 +5485,10 @@ function buildTraceFindingPacket(opts) {
|
|
|
4242
5485
|
kind: "traces.finding_packet",
|
|
4243
5486
|
generatedAt,
|
|
4244
5487
|
source: opts.source ?? "traces",
|
|
4245
|
-
findings: opts.findings
|
|
4246
|
-
recommendations: buildRecommendations(opts.findings),
|
|
4247
|
-
claims: buildClaims(opts.findings)
|
|
5488
|
+
findings: opts.findings
|
|
4248
5489
|
};
|
|
4249
5490
|
return { ...packet, report: renderFindingPacket(packet, opts.title) };
|
|
4250
5491
|
}
|
|
4251
|
-
function buildDefaultImprovementProposals(input) {
|
|
4252
|
-
return input.recommendations.slice(0, 5).map((recommendation, index) => {
|
|
4253
|
-
const matchingFindings = input.findings.filter((finding2) => recommendation.findingIds.includes(finding2.finding_id));
|
|
4254
|
-
const evidenceRefs = recommendation.evidenceRefs.length > 0 ? recommendation.evidenceRefs : matchingFindings.flatMap((finding2) => finding2.evidence_refs);
|
|
4255
|
-
return {
|
|
4256
|
-
id: `proposal-${index + 1}-${slug(recommendation.title)}`,
|
|
4257
|
-
title: recommendation.title,
|
|
4258
|
-
description: [
|
|
4259
|
-
recommendation.action,
|
|
4260
|
-
"",
|
|
4261
|
-
`Why: ${recommendation.rationale}`,
|
|
4262
|
-
`Validation: ${recommendation.validationPlan}`,
|
|
4263
|
-
"",
|
|
4264
|
-
"This is a proposal-only artifact. It does not mutate code, prompts, skills, tools, MCP config, hooks, subagents, or external systems until a human applies it and reruns validation."
|
|
4265
|
-
].join("\n"),
|
|
4266
|
-
recommendationIds: [recommendation.id],
|
|
4267
|
-
validationCommand: "traces improve --last 1 --dir .traces/improvement",
|
|
4268
|
-
evidenceRefs
|
|
4269
|
-
};
|
|
4270
|
-
});
|
|
4271
|
-
}
|
|
4272
5492
|
function renderExternal(results) {
|
|
4273
5493
|
if (results.length === 0) return "";
|
|
4274
5494
|
const lines = ["## external analyzers", ""];
|
|
@@ -4284,11 +5504,13 @@ function renderInvestigationReport(result, analystResult) {
|
|
|
4284
5504
|
const base = `${renderReport({ ...analystResult, findings: [...result.findings] }, {
|
|
4285
5505
|
harness: result.harness,
|
|
4286
5506
|
sessionCount: result.sessionCount,
|
|
5507
|
+
unassignedTraceCount: result.unassignedTraceCount,
|
|
4287
5508
|
spanCount: result.spanCount,
|
|
4288
5509
|
otlpPath: result.otlpPath,
|
|
4289
|
-
|
|
5510
|
+
execution: result.execution,
|
|
5511
|
+
deterministic: summarizeDeterministicSignals(result.pipelines, result.reactions),
|
|
5512
|
+
sources: result.sources
|
|
4290
5513
|
})}
|
|
4291
|
-
${renderRecommendations(result.recommendations)}
|
|
4292
5514
|
${renderPipelines(result.pipelines)}
|
|
4293
5515
|
${renderReactions(result.reactions)}
|
|
4294
5516
|
${renderAdoption(result.adoption)}`;
|
|
@@ -4365,19 +5587,18 @@ async function runTraceInvestigation(opts) {
|
|
|
4365
5587
|
...deterministicFindings(pipelines, reactions, adoption),
|
|
4366
5588
|
...normalizeExternalFindings(external)
|
|
4367
5589
|
];
|
|
4368
|
-
const recommendations = buildRecommendations(findings);
|
|
4369
|
-
const claims = buildClaims(findings);
|
|
4370
5590
|
const partial = {
|
|
4371
5591
|
schemaVersion: 1,
|
|
4372
5592
|
kind: "traces.investigation",
|
|
4373
5593
|
generatedAt,
|
|
4374
5594
|
harness: opts.harness,
|
|
4375
|
-
sessionCount:
|
|
5595
|
+
sessionCount: adoption.identifiedSessionCount,
|
|
5596
|
+
unassignedTraceCount: adoption.unassignedTraceCount,
|
|
5597
|
+
sources: opts.sources,
|
|
4376
5598
|
spanCount: opts.spans.length,
|
|
4377
5599
|
otlpPath: analysis.otlpPath,
|
|
5600
|
+
execution: analysis.execution,
|
|
4378
5601
|
findings,
|
|
4379
|
-
recommendations,
|
|
4380
|
-
claims,
|
|
4381
5602
|
pipelines,
|
|
4382
5603
|
reactions,
|
|
4383
5604
|
adoption,
|
|
@@ -4409,70 +5630,38 @@ async function runTraceStoreInvestigation(opts) {
|
|
|
4409
5630
|
analystResult
|
|
4410
5631
|
};
|
|
4411
5632
|
}
|
|
4412
|
-
function replayProof(result, proposals) {
|
|
4413
|
-
const deterministic = summarizeDeterministicSignals(result.pipelines, result.reactions);
|
|
4414
|
-
return {
|
|
4415
|
-
schemaVersion: 1,
|
|
4416
|
-
kind: "traces.improvement_replay",
|
|
4417
|
-
generatedAt: result.generatedAt,
|
|
4418
|
-
status: proposals.length > 0 ? "proposal-only" : "not-run",
|
|
4419
|
-
candidateApplied: false,
|
|
4420
|
-
baseline: {
|
|
4421
|
-
spanCount: result.spanCount,
|
|
4422
|
-
findingCount: result.findings.length,
|
|
4423
|
-
recommendationCount: result.recommendations.length,
|
|
4424
|
-
stuckLoops: deterministic.stuckLoops,
|
|
4425
|
-
reactionSignals: deterministic.reactionSignals,
|
|
4426
|
-
toolErrorRuns: deterministic.toolErrorRuns
|
|
4427
|
-
},
|
|
4428
|
-
proposals,
|
|
4429
|
-
note: proposals.length > 0 ? "traces produced reviewable proposals only; apply one and rerun traces on a comparable window to populate after metrics." : "no proposal adapter was configured, so no candidate was applied; recommendations remain reviewable guidance."
|
|
4430
|
-
};
|
|
4431
|
-
}
|
|
4432
5633
|
async function writeTraceImprovementArtifacts(result, outDir) {
|
|
4433
5634
|
const directory = outDir ? resolve(outDir) : await mkdtemp4(join16(tmpdir4(), "traces-improvement-"));
|
|
4434
5635
|
await mkdir(directory, { recursive: true });
|
|
4435
5636
|
const paths = {
|
|
4436
5637
|
directory,
|
|
4437
|
-
|
|
4438
|
-
recommendations: join16(directory, "recommendations.json"),
|
|
4439
|
-
proposals: join16(directory, "proposals.json"),
|
|
5638
|
+
result: join16(directory, "result.json"),
|
|
4440
5639
|
evidence: join16(directory, "evidence.jsonl"),
|
|
4441
|
-
claims: join16(directory, "claims.json"),
|
|
4442
5640
|
report: join16(directory, "report.md"),
|
|
4443
|
-
|
|
5641
|
+
traces: result.otlpPath
|
|
4444
5642
|
};
|
|
5643
|
+
const { report, ...machineResult } = result;
|
|
4445
5644
|
await Promise.all([
|
|
4446
|
-
writeFile4(paths.
|
|
4447
|
-
writeFile4(paths.recommendations, json(result.recommendations), "utf8"),
|
|
4448
|
-
writeFile4(paths.proposals, json(result.proposals), "utf8"),
|
|
5645
|
+
writeFile4(paths.result, json(machineResult), "utf8"),
|
|
4449
5646
|
writeFile4(paths.evidence, jsonl(buildEvidenceRows(result.findings)), "utf8"),
|
|
4450
|
-
writeFile4(paths.
|
|
4451
|
-
writeFile4(paths.report, result.report, "utf8"),
|
|
4452
|
-
writeFile4(paths.replay, json(result.replay), "utf8")
|
|
5647
|
+
writeFile4(paths.report, report, "utf8")
|
|
4453
5648
|
]);
|
|
4454
5649
|
return paths;
|
|
4455
5650
|
}
|
|
4456
|
-
async function
|
|
4457
|
-
const
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
otlpPath: investigation.otlpPath
|
|
4464
|
-
};
|
|
4465
|
-
const proposals = opts.adapter ? await opts.adapter.propose(adapterInput) : buildDefaultImprovementProposals(adapterInput);
|
|
4466
|
-
const replay = replayProof(investigation, proposals);
|
|
5651
|
+
async function runTraceImprovement(opts) {
|
|
5652
|
+
const directory = opts.outDir ? resolve(opts.outDir) : await mkdtemp4(join16(tmpdir4(), "traces-improvement-"));
|
|
5653
|
+
await mkdir(directory, { recursive: true });
|
|
5654
|
+
const investigation = await runTraceInvestigation({
|
|
5655
|
+
...opts,
|
|
5656
|
+
otlpOutPath: opts.otlpOutPath ?? join16(directory, "traces.otlp.jsonl")
|
|
5657
|
+
});
|
|
4467
5658
|
const result = {
|
|
4468
5659
|
...investigation,
|
|
4469
|
-
kind: "traces.improvement"
|
|
4470
|
-
proposals,
|
|
4471
|
-
replay
|
|
5660
|
+
kind: "traces.improvement"
|
|
4472
5661
|
};
|
|
4473
5662
|
return {
|
|
4474
5663
|
...result,
|
|
4475
|
-
artifacts: await writeTraceImprovementArtifacts(result,
|
|
5664
|
+
artifacts: await writeTraceImprovementArtifacts(result, directory)
|
|
4476
5665
|
};
|
|
4477
5666
|
}
|
|
4478
5667
|
async function saveReport(path, report) {
|
|
@@ -4481,7 +5670,7 @@ async function saveReport(path, report) {
|
|
|
4481
5670
|
}
|
|
4482
5671
|
|
|
4483
5672
|
// src/live.ts
|
|
4484
|
-
import { createHash as
|
|
5673
|
+
import { createHash as createHash5 } from "crypto";
|
|
4485
5674
|
var VERIFY_RE = /\b(test|tests|vitest|jest|pytest|go test|cargo test|tsc|typecheck|lint|biome|eslint|build|check:invariants|preflight)\b/i;
|
|
4486
5675
|
var CHANGE_RE = /\b(apply_patch|edit|write|create|delete|rename|replace|update|git apply|sed -i|perl -pi|tee\s|cat\s+>)/i;
|
|
4487
5676
|
var CLAIM_DONE_RE = /\b(done|fixed|complete|completed|implemented|merged|verified|all green|tests pass|ready|works now)\b/i;
|
|
@@ -4501,7 +5690,7 @@ var sleep = (ms, signal) => new Promise((resolve2) => {
|
|
|
4501
5690
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4502
5691
|
});
|
|
4503
5692
|
function hash(parts) {
|
|
4504
|
-
return
|
|
5693
|
+
return createHash5("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
|
|
4505
5694
|
}
|
|
4506
5695
|
function asString(value) {
|
|
4507
5696
|
if (typeof value === "string" && value.trim()) return value;
|
|
@@ -4955,7 +6144,7 @@ async function watchSessions(opts) {
|
|
|
4955
6144
|
}
|
|
4956
6145
|
|
|
4957
6146
|
// src/session-index.ts
|
|
4958
|
-
import { mkdtemp as mkdtemp5, readdir as
|
|
6147
|
+
import { mkdtemp as mkdtemp5, readdir as readdir12, readFile as readFile5, stat as stat12, writeFile as writeFile5 } from "fs/promises";
|
|
4959
6148
|
import { tmpdir as tmpdir5 } from "os";
|
|
4960
6149
|
import { dirname as dirname3, join as join17 } from "path";
|
|
4961
6150
|
function indexRow(record) {
|
|
@@ -4977,7 +6166,8 @@ function indexRow(record) {
|
|
|
4977
6166
|
},
|
|
4978
6167
|
models: record.metrics.models,
|
|
4979
6168
|
tools: record.metrics.tools,
|
|
4980
|
-
signals: record.signals
|
|
6169
|
+
signals: record.signals,
|
|
6170
|
+
execution: record.execution
|
|
4981
6171
|
};
|
|
4982
6172
|
}
|
|
4983
6173
|
function uniqueSorted(values) {
|
|
@@ -5072,7 +6262,7 @@ async function summarizeFile(path, kind) {
|
|
|
5072
6262
|
const base = { kind, path, bytes: Number(s.size), mtimeMs: Number(s.mtimeMs) };
|
|
5073
6263
|
if (!path.endsWith(".md") && !path.endsWith(".jsonl")) return base;
|
|
5074
6264
|
try {
|
|
5075
|
-
const text = await
|
|
6265
|
+
const text = await readFile5(path, "utf8");
|
|
5076
6266
|
const lines = countLines(text);
|
|
5077
6267
|
const jsonl2 = path.endsWith(".jsonl") ? summarizeJsonl(text) : void 0;
|
|
5078
6268
|
return {
|
|
@@ -5091,7 +6281,7 @@ async function walkMarkdown(root, kind, maxFiles = 100) {
|
|
|
5091
6281
|
if (out.length >= maxFiles) return;
|
|
5092
6282
|
let entries;
|
|
5093
6283
|
try {
|
|
5094
|
-
entries = await
|
|
6284
|
+
entries = await readdir12(dir, { withFileTypes: true });
|
|
5095
6285
|
} catch {
|
|
5096
6286
|
return;
|
|
5097
6287
|
}
|
|
@@ -5177,10 +6367,10 @@ async function writeSessionIndexFile(index, outPath) {
|
|
|
5177
6367
|
}
|
|
5178
6368
|
|
|
5179
6369
|
// src/upload-state.ts
|
|
5180
|
-
import {
|
|
5181
|
-
import { mkdir as mkdir2, readFile as readFile15, writeFile as writeFile6 } from "fs/promises";
|
|
6370
|
+
import { mkdir as mkdir2, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
|
|
5182
6371
|
import { homedir as homedir11 } from "os";
|
|
5183
6372
|
import { dirname as dirname4, join as join18 } from "path";
|
|
6373
|
+
import { contentHash } from "@tangle-network/agent-eval";
|
|
5184
6374
|
function stateFile() {
|
|
5185
6375
|
const base = process.env.XDG_STATE_HOME ?? join18(homedir11(), ".local", "state");
|
|
5186
6376
|
return join18(base, "tangle-traces", "uploaded.json");
|
|
@@ -5189,16 +6379,14 @@ function uploadKey(harness, sessionId) {
|
|
|
5189
6379
|
return `${harness}:${sessionId}`;
|
|
5190
6380
|
}
|
|
5191
6381
|
function sessionHash(spans) {
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
}
|
|
5197
|
-
return h.digest("hex").slice(0, 32);
|
|
6382
|
+
return contentHash(spans).slice(0, 32);
|
|
6383
|
+
}
|
|
6384
|
+
function outboundHash(events, identity) {
|
|
6385
|
+
return contentHash({ events, identity }).slice(0, 32);
|
|
5198
6386
|
}
|
|
5199
6387
|
async function loadState() {
|
|
5200
6388
|
try {
|
|
5201
|
-
return JSON.parse(await
|
|
6389
|
+
return JSON.parse(await readFile6(stateFile(), "utf8"));
|
|
5202
6390
|
} catch {
|
|
5203
6391
|
return {};
|
|
5204
6392
|
}
|
|
@@ -5214,7 +6402,7 @@ function alreadyUploaded(state, harness, sessionId, hash2) {
|
|
|
5214
6402
|
}
|
|
5215
6403
|
|
|
5216
6404
|
// src/upload.ts
|
|
5217
|
-
import { readFile as
|
|
6405
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
5218
6406
|
import { createRequire } from "module";
|
|
5219
6407
|
import { hostname } from "os";
|
|
5220
6408
|
import { basename as basename9, join as join19 } from "path";
|
|
@@ -5233,7 +6421,7 @@ async function planUpload(opts = {}) {
|
|
|
5233
6421
|
spans,
|
|
5234
6422
|
redaction: report,
|
|
5235
6423
|
hash: hash2,
|
|
5236
|
-
isNew:
|
|
6424
|
+
isNew: true
|
|
5237
6425
|
});
|
|
5238
6426
|
}
|
|
5239
6427
|
return { items, state, sinceMs: opts.sinceMs };
|
|
@@ -5241,19 +6429,18 @@ async function planUpload(opts = {}) {
|
|
|
5241
6429
|
async function gitBranch(cwd) {
|
|
5242
6430
|
if (!cwd) return void 0;
|
|
5243
6431
|
try {
|
|
5244
|
-
const head = await
|
|
6432
|
+
const head = await readFile7(join19(cwd, ".git", "HEAD"), "utf8");
|
|
5245
6433
|
const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
|
|
5246
6434
|
return m?.[1]?.trim();
|
|
5247
6435
|
} catch {
|
|
5248
6436
|
return void 0;
|
|
5249
6437
|
}
|
|
5250
6438
|
}
|
|
5251
|
-
async function sessionMeta(item
|
|
6439
|
+
async function sessionMeta(item) {
|
|
5252
6440
|
const meta = {
|
|
5253
6441
|
[ATTR.HARNESS]: item.ref.harness,
|
|
5254
6442
|
[ATTR.SESSION_FILE]: basename9(item.ref.path),
|
|
5255
6443
|
[ATTR.HOST]: hostname(),
|
|
5256
|
-
[ATTR.UPLOADED_AT]: uploadedAt,
|
|
5257
6444
|
[ATTR.UPLOADER]: `tangle-traces@${TRACES_VERSION}`,
|
|
5258
6445
|
[ATTR.REDACTION_VERSION]: REDACTION_VERSION,
|
|
5259
6446
|
[ATTR.REDACTION_COUNT]: item.redaction.redactionCount
|
|
@@ -5287,25 +6474,69 @@ function toTraceSpanEvents(spans, rootMeta) {
|
|
|
5287
6474
|
};
|
|
5288
6475
|
});
|
|
5289
6476
|
}
|
|
6477
|
+
var PartialUploadError = class extends Error {
|
|
6478
|
+
sessionKey;
|
|
6479
|
+
accepted;
|
|
6480
|
+
expected;
|
|
6481
|
+
constructor(sessionKey, accepted, expected) {
|
|
6482
|
+
super(`upload: backend accepted ${accepted} of ${expected} spans for ${sessionKey}`);
|
|
6483
|
+
this.name = "PartialUploadError";
|
|
6484
|
+
this.sessionKey = sessionKey;
|
|
6485
|
+
this.accepted = accepted;
|
|
6486
|
+
this.expected = expected;
|
|
6487
|
+
}
|
|
6488
|
+
};
|
|
5290
6489
|
function stripSpanContent(spans) {
|
|
5291
6490
|
return spans.map((s) => {
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
6491
|
+
const attributes = { ...s.attributes };
|
|
6492
|
+
delete attributes.content;
|
|
6493
|
+
for (const key of TOOL_IO_VALUE_KEYS) delete attributes[key];
|
|
6494
|
+
normalizeToolIoAttributes(attributes);
|
|
6495
|
+
const status = s.status.message ? { code: s.status.code } : s.status;
|
|
6496
|
+
return { ...s, attributes, status };
|
|
5295
6497
|
});
|
|
5296
6498
|
}
|
|
5297
|
-
async function
|
|
5298
|
-
let
|
|
6499
|
+
async function prepareOutbound(plan, opts, uploadedAt) {
|
|
6500
|
+
let candidates = plan.items;
|
|
5299
6501
|
if (opts.redactor && !opts.stripContent) {
|
|
5300
|
-
const
|
|
5301
|
-
|
|
6502
|
+
const redactor = opts.redactor;
|
|
6503
|
+
candidates = await Promise.all(
|
|
6504
|
+
candidates.map(async (item) => ({ ...item, spans: (await applyRedactor(item.spans, redactor)).spans }))
|
|
6505
|
+
);
|
|
5302
6506
|
}
|
|
5303
|
-
if (opts.stripContent)
|
|
5304
|
-
|
|
5305
|
-
|
|
6507
|
+
if (opts.stripContent) {
|
|
6508
|
+
candidates = candidates.map((item) => ({ ...item, spans: stripSpanContent(item.spans) }));
|
|
6509
|
+
}
|
|
6510
|
+
const identity = {
|
|
6511
|
+
stripContent: opts.stripContent === true,
|
|
6512
|
+
redactor: opts.stripContent ? null : opts.redactor?.name ?? null,
|
|
6513
|
+
redactionVersion: REDACTION_VERSION
|
|
6514
|
+
};
|
|
6515
|
+
const items = [];
|
|
6516
|
+
for (const item of candidates) {
|
|
6517
|
+
const key = uploadKey(item.ref.harness, item.ref.sessionId);
|
|
6518
|
+
const previous = plan.state[key];
|
|
6519
|
+
const meta = await sessionMeta(item);
|
|
6520
|
+
const eventsAt = (timestamp) => toTraceSpanEvents(item.spans, { ...meta, [ATTR.UPLOADED_AT]: timestamp });
|
|
6521
|
+
let eventTimestamp2 = previous?.uploadedAt ?? uploadedAt;
|
|
6522
|
+
let events = eventsAt(eventTimestamp2);
|
|
6523
|
+
let hash2 = outboundHash(events, identity);
|
|
6524
|
+
if (alreadyUploaded(plan.state, item.ref.harness, item.ref.sessionId, hash2)) continue;
|
|
6525
|
+
if (eventTimestamp2 !== uploadedAt) {
|
|
6526
|
+
eventTimestamp2 = uploadedAt;
|
|
6527
|
+
events = eventsAt(eventTimestamp2);
|
|
6528
|
+
hash2 = outboundHash(events, identity);
|
|
6529
|
+
}
|
|
6530
|
+
items.push({ item, events, hash: hash2 });
|
|
6531
|
+
}
|
|
6532
|
+
return { items, skipped: plan.items.length - items.length };
|
|
6533
|
+
}
|
|
6534
|
+
async function executeUpload(plan, opts = {}) {
|
|
5306
6535
|
const uploadedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6536
|
+
const { items, skipped } = await prepareOutbound(plan, opts, uploadedAt);
|
|
6537
|
+
const redactionCount = items.reduce((n, { item }) => n + item.redaction.redactionCount, 0);
|
|
5307
6538
|
if (opts.dryRun) {
|
|
5308
|
-
const allSpans =
|
|
6539
|
+
const allSpans = items.flatMap(({ item }) => item.spans);
|
|
5309
6540
|
const path = await writeOtlpFile(allSpans, opts.otlpOut);
|
|
5310
6541
|
return {
|
|
5311
6542
|
uploadedSessions: 0,
|
|
@@ -5316,6 +6547,15 @@ async function executeUpload(plan, opts = {}) {
|
|
|
5316
6547
|
otlpPath: path
|
|
5317
6548
|
};
|
|
5318
6549
|
}
|
|
6550
|
+
if (items.length === 0) {
|
|
6551
|
+
return {
|
|
6552
|
+
uploadedSessions: 0,
|
|
6553
|
+
skippedSessions: skipped,
|
|
6554
|
+
acceptedSpans: 0,
|
|
6555
|
+
redactionCount,
|
|
6556
|
+
dryRun: false
|
|
6557
|
+
};
|
|
6558
|
+
}
|
|
5319
6559
|
const client = opts.backend ?? hostedClientFromEnv();
|
|
5320
6560
|
if (!client) {
|
|
5321
6561
|
throw new Error(
|
|
@@ -5324,23 +6564,28 @@ async function executeUpload(plan, opts = {}) {
|
|
|
5324
6564
|
}
|
|
5325
6565
|
const state = plan.state;
|
|
5326
6566
|
let acceptedSpans = 0;
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
const events =
|
|
5330
|
-
const
|
|
6567
|
+
const completed = [];
|
|
6568
|
+
for (const outbound of items) {
|
|
6569
|
+
const { item, events, hash: hash2 } = outbound;
|
|
6570
|
+
const key = uploadKey(item.ref.harness, item.ref.sessionId);
|
|
6571
|
+
const idempotencyKey = `${key}:${hash2}`;
|
|
5331
6572
|
const res = await client.ingestTraces(events, idempotencyKey);
|
|
6573
|
+
if (res.accepted !== events.length) throw new PartialUploadError(key, res.accepted, events.length);
|
|
5332
6574
|
acceptedSpans += res.accepted;
|
|
5333
|
-
|
|
5334
|
-
|
|
6575
|
+
completed.push({ key, item, hash: hash2, spanCount: events.length });
|
|
6576
|
+
opts.log?.(`uploaded ${item.ref.harness} ${item.ref.sessionId.slice(0, 8)} \u2014 ${res.accepted} spans`);
|
|
6577
|
+
}
|
|
6578
|
+
for (const { key, item, hash: hash2, spanCount } of completed) {
|
|
6579
|
+
state[key] = {
|
|
6580
|
+
hash: hash2,
|
|
5335
6581
|
uploadedAt,
|
|
5336
6582
|
harness: item.ref.harness,
|
|
5337
|
-
spanCount
|
|
6583
|
+
spanCount
|
|
5338
6584
|
};
|
|
5339
|
-
opts.log?.(`uploaded ${item.ref.harness} ${item.ref.sessionId.slice(0, 8)} \u2014 ${res.accepted} spans`);
|
|
5340
6585
|
}
|
|
5341
6586
|
await saveState(state);
|
|
5342
6587
|
return {
|
|
5343
|
-
uploadedSessions:
|
|
6588
|
+
uploadedSessions: items.length,
|
|
5344
6589
|
skippedSessions: skipped,
|
|
5345
6590
|
acceptedSpans,
|
|
5346
6591
|
redactionCount,
|
|
@@ -5349,25 +6594,36 @@ async function executeUpload(plan, opts = {}) {
|
|
|
5349
6594
|
}
|
|
5350
6595
|
|
|
5351
6596
|
export {
|
|
5352
|
-
countSkillRunsJsonl,
|
|
5353
|
-
analyzeAdoption,
|
|
5354
6597
|
span,
|
|
5355
6598
|
toOpenInferenceSpan,
|
|
5356
6599
|
serializeSpans,
|
|
5357
6600
|
writeOtlpFile,
|
|
5358
|
-
|
|
6601
|
+
CONTENT_CAP,
|
|
6602
|
+
capText,
|
|
6603
|
+
ACTOR_ATTR,
|
|
6604
|
+
userPromptSpan,
|
|
6605
|
+
appendAll,
|
|
5359
6606
|
ATTR,
|
|
5360
6607
|
INGEST_SOURCE_CLI,
|
|
5361
6608
|
DEFAULT_HARNESS,
|
|
6609
|
+
sessionIdFromAttributes,
|
|
6610
|
+
indexSessionIdsByTrace,
|
|
6611
|
+
stampSessionIdentity,
|
|
6612
|
+
summarizeOtlpExecution,
|
|
6613
|
+
summarizeSpanExecution,
|
|
5362
6614
|
parseIsoToEpochMs,
|
|
5363
6615
|
parseSince,
|
|
5364
6616
|
toRuntimeStore,
|
|
5365
6617
|
runPipelines,
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
6618
|
+
recordSessionCorruption,
|
|
6619
|
+
sessionIntegrityAttributes,
|
|
6620
|
+
stampSessionIntegrity,
|
|
6621
|
+
JsonSourceError,
|
|
6622
|
+
readJsonFile,
|
|
5370
6623
|
AmpAdapter,
|
|
6624
|
+
JsonlParseError,
|
|
6625
|
+
readJsonl,
|
|
6626
|
+
takeJsonl,
|
|
5371
6627
|
textIsSynthetic,
|
|
5372
6628
|
looksLikeAgentPrompt,
|
|
5373
6629
|
claudeActor,
|
|
@@ -5395,6 +6651,7 @@ export {
|
|
|
5395
6651
|
stampRepoAttrs,
|
|
5396
6652
|
stampSpanWorkdirRepoAttrs,
|
|
5397
6653
|
parseSession,
|
|
6654
|
+
EmptySessionError,
|
|
5398
6655
|
locateSessions,
|
|
5399
6656
|
scanSessions,
|
|
5400
6657
|
buildPolicyEvidenceRecord,
|
|
@@ -5419,20 +6676,19 @@ export {
|
|
|
5419
6676
|
serializeInspectionReport,
|
|
5420
6677
|
readSessionIndexFile,
|
|
5421
6678
|
writeInspectionReportFile,
|
|
6679
|
+
countSkillRunsJsonl,
|
|
6680
|
+
analyzeAdoption,
|
|
6681
|
+
analyzeSpans,
|
|
5422
6682
|
REACTIONS,
|
|
5423
6683
|
analyzeReactions,
|
|
5424
|
-
|
|
5425
|
-
renderReport,
|
|
5426
|
-
renderPipelines,
|
|
5427
|
-
renderReactions,
|
|
5428
|
-
renderAdoption,
|
|
6684
|
+
CORRUPTION_RECEIPT_DISPLAY_LIMIT,
|
|
5429
6685
|
buildTraceFindingPacket,
|
|
5430
6686
|
loadTracesConfig,
|
|
5431
6687
|
mergeTracesConfig,
|
|
5432
6688
|
runTraceInvestigation,
|
|
5433
6689
|
runTraceStoreInvestigation,
|
|
5434
6690
|
writeTraceImprovementArtifacts,
|
|
5435
|
-
|
|
6691
|
+
runTraceImprovement,
|
|
5436
6692
|
saveReport,
|
|
5437
6693
|
classifyLiveActions,
|
|
5438
6694
|
defaultTraceLiveAnalysts,
|
|
@@ -5448,10 +6704,12 @@ export {
|
|
|
5448
6704
|
writeSessionIndexFile,
|
|
5449
6705
|
uploadKey,
|
|
5450
6706
|
sessionHash,
|
|
6707
|
+
outboundHash,
|
|
5451
6708
|
loadState,
|
|
5452
6709
|
saveState,
|
|
5453
6710
|
alreadyUploaded,
|
|
5454
6711
|
planUpload,
|
|
5455
6712
|
toTraceSpanEvents,
|
|
6713
|
+
PartialUploadError,
|
|
5456
6714
|
executeUpload
|
|
5457
6715
|
};
|