@tangle-network/traces 0.8.26 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -16
- package/dist/{chunk-6N2KVDOU.js → chunk-5SBXL62V.js} +1989 -852
- package/dist/cli.js +104 -35
- package/dist/index.d.ts +221 -131
- package/dist/index.js +35 -3
- package/package.json +2 -2
|
@@ -1,6 +1,214 @@
|
|
|
1
|
+
// src/attributes.ts
|
|
2
|
+
var ATTR = {
|
|
3
|
+
/** = the trace/session id. Wire contract (dedup linkage). */
|
|
4
|
+
SESSION_ID: "tangle.sessionId",
|
|
5
|
+
/** "cli" marks a CLI upload so the platform can drop a re-upload of a session
|
|
6
|
+
* that also streamed live. Wire contract. */
|
|
7
|
+
INGEST_SOURCE: "tangle.ingest_source",
|
|
8
|
+
HARNESS: "tangle.harness",
|
|
9
|
+
CWD: "tangle.cwd",
|
|
10
|
+
/** THE per-session grouping key the spine derives subjects from
|
|
11
|
+
* (`deriveSubjectKey` reads `tangle.subject.key` first). Resolves to the git
|
|
12
|
+
* remote (host/owner/repo) when readable, else the cwd path basename. */
|
|
13
|
+
SUBJECT_KEY: "tangle.subject.key",
|
|
14
|
+
/** Normalized git remote (e.g. `github.com/tangle-network/agent-dev-container`). */
|
|
15
|
+
GIT_REPOSITORY: "git.repository",
|
|
16
|
+
/** Current branch at conversion time. */
|
|
17
|
+
GIT_BRANCH_NAME: "git.branch",
|
|
18
|
+
/** HEAD short sha at conversion time. */
|
|
19
|
+
GIT_COMMIT: "git.commit",
|
|
20
|
+
/** How traces resolved the session cwd: recorded cwd, repaired cwd, or span path. */
|
|
21
|
+
REPO_RESOLUTION_SOURCE: "traces.repo_resolution_source",
|
|
22
|
+
GIT_BRANCH: "tangle.git_branch",
|
|
23
|
+
HOST: "tangle.host",
|
|
24
|
+
/** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
|
|
25
|
+
* which collided with `tangle.ingest_source`.) */
|
|
26
|
+
SESSION_FILE: "tangle.session_file",
|
|
27
|
+
UPLOADED_AT: "tangle.uploaded_at",
|
|
28
|
+
UPLOADER: "tangle.uploader",
|
|
29
|
+
REDACTION_VERSION: "redaction.version",
|
|
30
|
+
REDACTION_COUNT: "redaction.count",
|
|
31
|
+
/** Upload contract: bounded root summary plus location-only receipt child spans.
|
|
32
|
+
* Receipt hashes are deliberate integrity evidence; raw malformed bytes never upload. */
|
|
33
|
+
SESSION_INTEGRITY: "traces.session.integrity",
|
|
34
|
+
CORRUPTION_COUNT: "traces.session.corruption_count",
|
|
35
|
+
CORRUPTION_DIGEST: "traces.session.corruption_digest",
|
|
36
|
+
CORRUPTION_RECEIPT_VERSION: "traces.session.corruption.receipt_version",
|
|
37
|
+
CORRUPTION_RECEIPT_KIND: "traces.session.corruption.kind",
|
|
38
|
+
CORRUPTION_SOURCE_PATH: "traces.session.corruption.source_path",
|
|
39
|
+
CORRUPTION_LINE_NUMBER: "traces.session.corruption.line_number",
|
|
40
|
+
CORRUPTION_BYTE_OFFSET: "traces.session.corruption.byte_offset",
|
|
41
|
+
CORRUPTION_BYTE_LENGTH: "traces.session.corruption.byte_length",
|
|
42
|
+
CORRUPTION_SHA256: "traces.session.corruption.sha256",
|
|
43
|
+
RAW_SOURCE_RETENTION: "traces.session.raw_source_retention"
|
|
44
|
+
};
|
|
45
|
+
var INGEST_SOURCE_CLI = "cli";
|
|
46
|
+
var DEFAULT_HARNESS = "claude-code";
|
|
47
|
+
var SESSION_ID_ATTRIBUTE_KEYS = [ATTR.SESSION_ID, "session.id", "traces.session.id"];
|
|
48
|
+
function sessionIdFromAttributes(attributes) {
|
|
49
|
+
return SESSION_ID_ATTRIBUTE_KEYS.map((key) => attributes[key]).find(
|
|
50
|
+
(value) => typeof value === "string" && value.length > 0
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
function indexSessionIdsByTrace(spans) {
|
|
54
|
+
const candidatesByTrace = /* @__PURE__ */ new Map();
|
|
55
|
+
for (const span2 of spans) {
|
|
56
|
+
const candidates = candidatesByTrace.get(span2.trace_id) ?? /* @__PURE__ */ new Set();
|
|
57
|
+
for (const key of SESSION_ID_ATTRIBUTE_KEYS) {
|
|
58
|
+
const value = span2.attributes[key];
|
|
59
|
+
if (typeof value === "string" && value.length > 0) candidates.add(value);
|
|
60
|
+
}
|
|
61
|
+
if (candidates.size > 0) candidatesByTrace.set(span2.trace_id, candidates);
|
|
62
|
+
}
|
|
63
|
+
const sessionByTrace = /* @__PURE__ */ new Map();
|
|
64
|
+
const conflicts = [];
|
|
65
|
+
for (const [traceId, candidates] of candidatesByTrace) {
|
|
66
|
+
const sessionIds2 = [...candidates].sort();
|
|
67
|
+
if (sessionIds2.length === 1) {
|
|
68
|
+
sessionByTrace.set(traceId, sessionIds2[0]);
|
|
69
|
+
} else {
|
|
70
|
+
conflicts.push({ traceId, sessionIds: sessionIds2 });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
conflicts.sort((a, b) => a.traceId.localeCompare(b.traceId));
|
|
74
|
+
return { sessionByTrace, conflicts };
|
|
75
|
+
}
|
|
76
|
+
function stampSessionIdentity(spans, fallbackSessionId) {
|
|
77
|
+
const { sessionByTrace, conflicts } = indexSessionIdsByTrace(spans);
|
|
78
|
+
const conflictingTraceIds = new Set(conflicts.map((conflict) => conflict.traceId));
|
|
79
|
+
const traceIds = new Set(spans.map((span2) => span2.trace_id));
|
|
80
|
+
const singleTraceId = traceIds.size === 1 ? traceIds.values().next().value : void 0;
|
|
81
|
+
for (const span2 of spans) {
|
|
82
|
+
if (conflictingTraceIds.has(span2.trace_id)) continue;
|
|
83
|
+
const sessionId = sessionByTrace.get(span2.trace_id) ?? singleTraceId ?? fallbackSessionId;
|
|
84
|
+
if (!sessionIdFromAttributes(span2.attributes)) {
|
|
85
|
+
span2.attributes[ATTR.SESSION_ID] = sessionId;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
1
90
|
// src/adoption.ts
|
|
2
91
|
import { readFile } from "fs/promises";
|
|
3
92
|
import { join } from "path";
|
|
93
|
+
|
|
94
|
+
// src/adapters/tool-io.ts
|
|
95
|
+
import { createHash } from "crypto";
|
|
96
|
+
var TOOL_IO_VALUE_MAX_BYTES = 16 * 1024;
|
|
97
|
+
var TOOL_IO_VALUE_KEYS = ["input.value", "output.value"];
|
|
98
|
+
var TOOL_IO_SIDES = ["input", "output"];
|
|
99
|
+
var LEGACY_TRUNCATION_MARKER = /\n\[truncated (\d+) bytes; sha256=[a-f0-9]{64}\]$/;
|
|
100
|
+
var COUNTED_TRUNCATION_MARKER = /\n\[truncated \d+ bytes\]$/;
|
|
101
|
+
var TRUNCATION_MARKER = /\n\[truncated\]$/;
|
|
102
|
+
function canonicalJson(value) {
|
|
103
|
+
const sort = (item) => {
|
|
104
|
+
if (Array.isArray(item)) return item.map(sort);
|
|
105
|
+
if (!item || typeof item !== "object") return item;
|
|
106
|
+
return Object.fromEntries(
|
|
107
|
+
Object.entries(item).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => [key, sort(entry)])
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
const json2 = JSON.stringify(sort(value));
|
|
111
|
+
if (json2 === void 0) throw new TypeError("tool I/O value is not JSON-serializable");
|
|
112
|
+
return json2;
|
|
113
|
+
}
|
|
114
|
+
function serialize(value) {
|
|
115
|
+
if (value === void 0) return void 0;
|
|
116
|
+
if (typeof value === "string") {
|
|
117
|
+
const trimmed = value.trim();
|
|
118
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
119
|
+
try {
|
|
120
|
+
return { value: canonicalJson(JSON.parse(trimmed)), mimeType: "application/json" };
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { value, mimeType: "text/plain" };
|
|
125
|
+
}
|
|
126
|
+
return { value: canonicalJson(value), mimeType: "application/json" };
|
|
127
|
+
}
|
|
128
|
+
function utf8Prefix(value, maxBytes) {
|
|
129
|
+
let bytes = 0;
|
|
130
|
+
let end = 0;
|
|
131
|
+
for (const char of value) {
|
|
132
|
+
const charBytes = Buffer.byteLength(char);
|
|
133
|
+
if (bytes + charBytes > maxBytes) break;
|
|
134
|
+
bytes += charBytes;
|
|
135
|
+
end += char.length;
|
|
136
|
+
}
|
|
137
|
+
return Buffer.from(value.slice(0, end), "utf8").toString("utf8");
|
|
138
|
+
}
|
|
139
|
+
function truncatedValue(value, bytes) {
|
|
140
|
+
const longestMarker = `
|
|
141
|
+
[truncated ${bytes} bytes]`;
|
|
142
|
+
const prefix = utf8Prefix(value, TOOL_IO_VALUE_MAX_BYTES - Buffer.byteLength(longestMarker));
|
|
143
|
+
const omittedBytes = bytes - Buffer.byteLength(prefix);
|
|
144
|
+
return `${prefix}
|
|
145
|
+
[truncated ${omittedBytes} bytes]`;
|
|
146
|
+
}
|
|
147
|
+
function valueAttributes(side, raw) {
|
|
148
|
+
const serialized = serialize(raw);
|
|
149
|
+
if (!serialized) return {};
|
|
150
|
+
const bytes = Buffer.byteLength(serialized.value);
|
|
151
|
+
const sha256 = createHash("sha256").update(serialized.value).digest("hex");
|
|
152
|
+
const truncated = bytes > TOOL_IO_VALUE_MAX_BYTES;
|
|
153
|
+
const value = truncated ? truncatedValue(serialized.value, bytes) : serialized.value;
|
|
154
|
+
return {
|
|
155
|
+
[`${side}.value`]: value,
|
|
156
|
+
[`${side}.mime_type`]: serialized.mimeType,
|
|
157
|
+
[`traces.${side}.bytes`]: bytes,
|
|
158
|
+
[`traces.${side}.sha256`]: sha256,
|
|
159
|
+
[`traces.${side}.truncated`]: truncated
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function toolIoAttributes(io) {
|
|
163
|
+
return {
|
|
164
|
+
...valueAttributes("input", io.input),
|
|
165
|
+
...valueAttributes("output", io.output),
|
|
166
|
+
...io.argsCaptured === void 0 ? {} : { "tool.args_captured": io.argsCaptured }
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function toolArgumentsFromAttributes(attributes) {
|
|
170
|
+
const declared = attributes["tool.args_captured"];
|
|
171
|
+
const explicit = typeof declared === "boolean" ? declared : declared === "true" ? true : declared === "false" ? false : void 0;
|
|
172
|
+
const argsCaptured = explicit ?? attributes["input.value"] !== void 0;
|
|
173
|
+
return {
|
|
174
|
+
args: argsCaptured ? attributes["input.value"] : void 0,
|
|
175
|
+
argsCaptured
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function normalizeToolIoAttributes(attributes) {
|
|
179
|
+
for (const side of TOOL_IO_SIDES) {
|
|
180
|
+
const valueKey = `${side}.value`;
|
|
181
|
+
const mimeKey = `${side}.mime_type`;
|
|
182
|
+
const bytesKey = `traces.${side}.bytes`;
|
|
183
|
+
const digestKey = `traces.${side}.sha256`;
|
|
184
|
+
const truncatedKey = `traces.${side}.truncated`;
|
|
185
|
+
const rawValue = attributes[valueKey];
|
|
186
|
+
if (typeof rawValue !== "string") {
|
|
187
|
+
delete attributes[mimeKey];
|
|
188
|
+
delete attributes[bytesKey];
|
|
189
|
+
delete attributes[digestKey];
|
|
190
|
+
delete attributes[truncatedKey];
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
let value = rawValue.replace(LEGACY_TRUNCATION_MARKER, "\n[truncated]").replace(COUNTED_TRUNCATION_MARKER, "\n[truncated]");
|
|
194
|
+
let truncated = TRUNCATION_MARKER.test(value);
|
|
195
|
+
const unboundedBytes = Buffer.byteLength(value);
|
|
196
|
+
if (unboundedBytes > TOOL_IO_VALUE_MAX_BYTES) {
|
|
197
|
+
value = truncatedValue(value, unboundedBytes);
|
|
198
|
+
truncated = true;
|
|
199
|
+
}
|
|
200
|
+
attributes[valueKey] = value;
|
|
201
|
+
attributes[bytesKey] = Buffer.byteLength(value);
|
|
202
|
+
attributes[digestKey] = createHash("sha256").update(value).digest("hex");
|
|
203
|
+
attributes[truncatedKey] = truncated;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function recordToolOutput(toolSpan, output) {
|
|
207
|
+
if (!toolSpan) return;
|
|
208
|
+
Object.assign(toolSpan.attributes, valueAttributes("output", output));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/adoption.ts
|
|
4
212
|
function toolName(s) {
|
|
5
213
|
const n = s.attributes["tool.name"];
|
|
6
214
|
if (typeof n === "string") return n;
|
|
@@ -8,10 +216,14 @@ function toolName(s) {
|
|
|
8
216
|
return null;
|
|
9
217
|
}
|
|
10
218
|
function parseInput(s) {
|
|
11
|
-
const
|
|
12
|
-
if (
|
|
219
|
+
const captured = toolArgumentsFromAttributes(s.attributes);
|
|
220
|
+
if (!captured.argsCaptured) return {};
|
|
221
|
+
if (captured.args && typeof captured.args === "object") {
|
|
222
|
+
return captured.args;
|
|
223
|
+
}
|
|
224
|
+
if (typeof captured.args !== "string") return {};
|
|
13
225
|
try {
|
|
14
|
-
const v = JSON.parse(
|
|
226
|
+
const v = JSON.parse(captured.args);
|
|
15
227
|
return v && typeof v === "object" ? v : {};
|
|
16
228
|
} catch {
|
|
17
229
|
return {};
|
|
@@ -22,9 +234,34 @@ function skillNameOf(input) {
|
|
|
22
234
|
return typeof v === "string" && v.length > 0 ? v : "?";
|
|
23
235
|
}
|
|
24
236
|
function subagentTypeOf(input) {
|
|
25
|
-
const v = input.subagent_type;
|
|
237
|
+
const v = input.subagent_type ?? input.agent_type ?? input.type ?? input.name;
|
|
26
238
|
return typeof v === "string" && v.length > 0 ? v : "?";
|
|
27
239
|
}
|
|
240
|
+
function isSpawnAgentTool(name) {
|
|
241
|
+
return name === "spawn_agent" || name.endsWith("__spawn_agent");
|
|
242
|
+
}
|
|
243
|
+
function skillTelemetryCapability(service) {
|
|
244
|
+
if (service === "claude-code") return "supported";
|
|
245
|
+
if (service === "codex") return "unsupported";
|
|
246
|
+
return "unknown";
|
|
247
|
+
}
|
|
248
|
+
var MATERIALIZED_SKILL_RE = /<skills_instructions>|(?:^|\n)#{1,3}\s+(?:available\s+)?skills\b|base directory for this skill:/i;
|
|
249
|
+
function hasMaterializedSkillEvidence(s) {
|
|
250
|
+
if (s.name !== "message.developer" && s.attributes["tangle.actor"] !== "injected") return false;
|
|
251
|
+
const content = s.attributes.content;
|
|
252
|
+
return typeof content === "string" && MATERIALIZED_SKILL_RE.test(content);
|
|
253
|
+
}
|
|
254
|
+
function hasSkillFileReference(s) {
|
|
255
|
+
if (s.attributes["openinference.span.kind"] !== "TOOL") return false;
|
|
256
|
+
const input = s.attributes["input.value"];
|
|
257
|
+
return typeof input === "string" && input.includes("SKILL.md");
|
|
258
|
+
}
|
|
259
|
+
function telemetryStatus(measured, unsupported, unknown) {
|
|
260
|
+
if (measured > 0 && unsupported === 0 && unknown === 0) return "measured";
|
|
261
|
+
if (measured > 0) return "partial";
|
|
262
|
+
if (unsupported > 0 && unknown === 0) return "unsupported";
|
|
263
|
+
return "unknown";
|
|
264
|
+
}
|
|
28
265
|
function countSkillRunsJsonl(raw) {
|
|
29
266
|
const out = {};
|
|
30
267
|
for (const line of raw.split("\n")) {
|
|
@@ -68,32 +305,84 @@ async function readLoopRuns(cwds) {
|
|
|
68
305
|
return { counts, filesRead };
|
|
69
306
|
}
|
|
70
307
|
async function analyzeAdoption(spans, opts = {}) {
|
|
308
|
+
const { sessionByTrace, conflicts } = indexSessionIdsByTrace(spans);
|
|
309
|
+
const identifiedSessions = new Set(sessionByTrace.values());
|
|
310
|
+
const unassignedTraceIds = new Set(
|
|
311
|
+
spans.map((span2) => span2.trace_id).filter((traceId) => !sessionByTrace.has(traceId))
|
|
312
|
+
);
|
|
313
|
+
const executionGroup = (span2) => sessionByTrace.get(span2.trace_id) ?? `trace:${span2.trace_id}`;
|
|
71
314
|
const sessions = /* @__PURE__ */ new Set();
|
|
72
315
|
const sessionsWithSkill = /* @__PURE__ */ new Set();
|
|
73
316
|
const sessionsWithSubagent = /* @__PURE__ */ new Set();
|
|
317
|
+
const sessionsWithMaterializedSkills = /* @__PURE__ */ new Set();
|
|
318
|
+
const sessionsWithSkillFileReference = /* @__PURE__ */ new Set();
|
|
74
319
|
const skillInvocations = {};
|
|
75
320
|
const subagentSpawns = {};
|
|
321
|
+
const canonicalSubagentSessions = /* @__PURE__ */ new Set();
|
|
322
|
+
const fallbackSubagents = /* @__PURE__ */ new Map();
|
|
323
|
+
const sessionCapabilities = /* @__PURE__ */ new Map();
|
|
76
324
|
for (const s of spans) {
|
|
77
|
-
|
|
325
|
+
const group = executionGroup(s);
|
|
326
|
+
sessions.add(group);
|
|
327
|
+
const observedCapability = skillTelemetryCapability(s.attributes["service.name"]);
|
|
328
|
+
const currentCapability = sessionCapabilities.get(group) ?? "unknown";
|
|
329
|
+
if (observedCapability === "supported" || currentCapability === "unknown") {
|
|
330
|
+
sessionCapabilities.set(group, observedCapability);
|
|
331
|
+
}
|
|
332
|
+
if (hasMaterializedSkillEvidence(s)) sessionsWithMaterializedSkills.add(group);
|
|
333
|
+
if (hasSkillFileReference(s)) sessionsWithSkillFileReference.add(group);
|
|
78
334
|
const tn = toolName(s);
|
|
79
335
|
if (tn === "Skill") {
|
|
336
|
+
sessionCapabilities.set(group, "supported");
|
|
80
337
|
const name = skillNameOf(parseInput(s));
|
|
81
338
|
skillInvocations[name] = (skillInvocations[name] ?? 0) + 1;
|
|
82
|
-
sessionsWithSkill.add(
|
|
339
|
+
sessionsWithSkill.add(group);
|
|
83
340
|
} else if (tn === "Task" || tn === "Agent") {
|
|
84
341
|
const type = subagentTypeOf(parseInput(s));
|
|
85
342
|
subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
|
|
86
|
-
sessionsWithSubagent.add(
|
|
343
|
+
sessionsWithSubagent.add(group);
|
|
344
|
+
canonicalSubagentSessions.add(group);
|
|
345
|
+
} else if (tn && isSpawnAgentTool(tn)) {
|
|
346
|
+
const types = fallbackSubagents.get(group) ?? [];
|
|
347
|
+
types.push(subagentTypeOf(parseInput(s)));
|
|
348
|
+
fallbackSubagents.set(group, types);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
for (const [traceId, types] of fallbackSubagents) {
|
|
352
|
+
if (canonicalSubagentSessions.has(traceId)) continue;
|
|
353
|
+
sessionsWithSubagent.add(traceId);
|
|
354
|
+
for (const type of types) {
|
|
355
|
+
subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
|
|
87
356
|
}
|
|
88
357
|
}
|
|
89
358
|
const { counts: loopDispatchedRuns, filesRead } = await readLoopRuns(opts.cwds ?? []);
|
|
90
359
|
const totalSkillInvocations = Object.values(skillInvocations).reduce((a, b) => a + b, 0);
|
|
91
360
|
const totalSubagentSpawns = Object.values(subagentSpawns).reduce((a, b) => a + b, 0);
|
|
92
361
|
const totalLoopDispatchedRuns = Object.values(loopDispatchedRuns).reduce((a, b) => a + b, 0);
|
|
362
|
+
let skillTelemetrySessions = 0;
|
|
363
|
+
let skillTelemetryUnsupportedSessions = 0;
|
|
364
|
+
let skillTelemetryUnknownSessions = 0;
|
|
365
|
+
for (const traceId of sessions) {
|
|
366
|
+
const capability = sessionCapabilities.get(traceId) ?? "unknown";
|
|
367
|
+
if (capability === "supported") skillTelemetrySessions += 1;
|
|
368
|
+
else if (capability === "unsupported") skillTelemetryUnsupportedSessions += 1;
|
|
369
|
+
else skillTelemetryUnknownSessions += 1;
|
|
370
|
+
}
|
|
93
371
|
return {
|
|
94
|
-
|
|
372
|
+
executionGroupCount: sessions.size,
|
|
373
|
+
identifiedSessionCount: identifiedSessions.size,
|
|
374
|
+
unassignedTraceCount: unassignedTraceIds.size,
|
|
375
|
+
sessionIdentityConflicts: conflicts,
|
|
95
376
|
sessionsWithSkill: sessionsWithSkill.size,
|
|
96
|
-
skillPenetration:
|
|
377
|
+
skillPenetration: skillTelemetrySessions === 0 ? null : sessionsWithSkill.size / skillTelemetrySessions,
|
|
378
|
+
skillTelemetryStatus: telemetryStatus(
|
|
379
|
+
skillTelemetrySessions,
|
|
380
|
+
skillTelemetryUnsupportedSessions,
|
|
381
|
+
skillTelemetryUnknownSessions
|
|
382
|
+
),
|
|
383
|
+
skillTelemetrySessions,
|
|
384
|
+
sessionsWithMaterializedSkills: sessionsWithMaterializedSkills.size,
|
|
385
|
+
sessionsWithSkillFileReference: sessionsWithSkillFileReference.size,
|
|
97
386
|
skillInvocations,
|
|
98
387
|
totalSkillInvocations,
|
|
99
388
|
subagentSpawns,
|
|
@@ -106,16 +395,27 @@ async function analyzeAdoption(spans, opts = {}) {
|
|
|
106
395
|
}
|
|
107
396
|
|
|
108
397
|
// src/otlp.ts
|
|
398
|
+
import {
|
|
399
|
+
applyLlmSpanOtlpAttributes,
|
|
400
|
+
OPENINFERENCE_SPAN_KIND,
|
|
401
|
+
TOOL_NAME
|
|
402
|
+
} from "@tangle-network/agent-eval/trace-attributes";
|
|
109
403
|
function span(input) {
|
|
110
404
|
const attributes = {
|
|
111
|
-
|
|
405
|
+
[OPENINFERENCE_SPAN_KIND]: input.kind
|
|
112
406
|
};
|
|
113
407
|
if (input.service != null) attributes["service.name"] = input.service;
|
|
114
408
|
if (input.agent != null) attributes["agent.name"] = input.agent;
|
|
115
|
-
if (input.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
409
|
+
if (input.tool != null) attributes[TOOL_NAME] = input.tool;
|
|
410
|
+
applyLlmSpanOtlpAttributes(attributes, {
|
|
411
|
+
model: input.model ?? void 0,
|
|
412
|
+
inputTokens: input.inputTokens ?? void 0,
|
|
413
|
+
outputTokens: input.outputTokens ?? void 0,
|
|
414
|
+
reasoningTokens: input.reasoningTokens ?? void 0,
|
|
415
|
+
cachedTokens: input.cachedInputTokens ?? void 0,
|
|
416
|
+
cacheWriteTokens: input.cacheWriteInputTokens ?? void 0,
|
|
417
|
+
costUsd: input.costUsd ?? void 0
|
|
418
|
+
});
|
|
119
419
|
if (input.step != null) attributes.step = input.step;
|
|
120
420
|
if (input.content != null && input.content.length > 0) attributes["content"] = input.content;
|
|
121
421
|
if (input.extra) Object.assign(attributes, input.extra);
|
|
@@ -145,7 +445,7 @@ function toOpenInferenceSpan(s) {
|
|
|
145
445
|
span_id: s.span_id,
|
|
146
446
|
parent_span_id: s.parent_span_id ?? "",
|
|
147
447
|
name: s.name,
|
|
148
|
-
kind: typeof a[
|
|
448
|
+
kind: typeof a[OPENINFERENCE_SPAN_KIND] === "string" ? a[OPENINFERENCE_SPAN_KIND] : "CHAIN",
|
|
149
449
|
start_time: s.start_time,
|
|
150
450
|
end_time: s.end_time,
|
|
151
451
|
status: { code: s.status.code, message: s.status.message ?? "" },
|
|
@@ -168,20 +468,56 @@ async function writeOtlpFile(spans, outPath) {
|
|
|
168
468
|
return path;
|
|
169
469
|
}
|
|
170
470
|
|
|
471
|
+
// src/execution.ts
|
|
472
|
+
import {
|
|
473
|
+
summarizeExecution
|
|
474
|
+
} from "@tangle-network/agent-eval/contract";
|
|
475
|
+
import {
|
|
476
|
+
otlpRowsToRunRecords,
|
|
477
|
+
otlpToRunRecords
|
|
478
|
+
} from "@tangle-network/agent-eval/traces";
|
|
479
|
+
function runOptions(opts) {
|
|
480
|
+
return {
|
|
481
|
+
experimentId: opts.experimentId ?? "traces-observed",
|
|
482
|
+
candidateId: opts.candidateId ?? "observed",
|
|
483
|
+
fallbackModel: "unknown@otlp"
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function* openInferenceRows(spans) {
|
|
487
|
+
for (const item of spans) yield toOpenInferenceSpan(item);
|
|
488
|
+
}
|
|
489
|
+
function summarizeOtlpExecution(otlpJsonl, opts = {}) {
|
|
490
|
+
const runs = otlpToRunRecords(otlpJsonl, runOptions(opts));
|
|
491
|
+
return summarizeExecution({ runs });
|
|
492
|
+
}
|
|
493
|
+
function summarizeSpanExecution(spans, opts = {}) {
|
|
494
|
+
if (spans.length === 0) throw new Error("summarizeSpanExecution: no spans to summarize");
|
|
495
|
+
return summarizeExecution({
|
|
496
|
+
runs: otlpRowsToRunRecords(openInferenceRows(spans), runOptions(opts))
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
171
500
|
// src/analyze.ts
|
|
172
501
|
import { buildDefaultAnalystRegistry } from "@tangle-network/agent-eval/analyst";
|
|
173
502
|
import { OtlpFileTraceStore } from "@tangle-network/agent-eval/traces";
|
|
174
|
-
var
|
|
503
|
+
var GENERATED_TRACE_FILE_CEILING = 512 * 1024 * 1024;
|
|
175
504
|
async function analyzeSpans(spans, opts = {}) {
|
|
176
505
|
if (spans.length === 0) throw new Error("analyzeSpans: no spans to analyze");
|
|
177
506
|
const otlpPath = await writeOtlpFile(spans, opts.otlpOutPath);
|
|
178
507
|
const runId = opts.runId ?? `traces-${Date.now()}`;
|
|
179
|
-
const
|
|
508
|
+
const execution = summarizeSpanExecution(spans, {
|
|
509
|
+
experimentId: runId
|
|
510
|
+
});
|
|
511
|
+
const detStore = new OtlpFileTraceStore({
|
|
512
|
+
path: otlpPath,
|
|
513
|
+
maxFileBytes: GENERATED_TRACE_FILE_CEILING,
|
|
514
|
+
perCallByteCeiling: GENERATED_TRACE_FILE_CEILING
|
|
515
|
+
});
|
|
180
516
|
await detStore.ensureIndexed();
|
|
181
517
|
const detRegistry = opts.registry ?? buildDefaultAnalystRegistry({ registry: { log: opts.log } });
|
|
182
518
|
const result = await detRegistry.run(runId, { traceStore: detStore });
|
|
183
519
|
if (opts.ai) {
|
|
184
|
-
const agStore = new OtlpFileTraceStore({ path: otlpPath });
|
|
520
|
+
const agStore = new OtlpFileTraceStore({ path: otlpPath, maxFileBytes: GENERATED_TRACE_FILE_CEILING });
|
|
185
521
|
await agStore.ensureIndexed();
|
|
186
522
|
const agRegistry = buildDefaultAnalystRegistry({
|
|
187
523
|
ai: opts.ai,
|
|
@@ -196,42 +532,32 @@ async function analyzeSpans(spans, opts = {}) {
|
|
|
196
532
|
result.per_analyst.push(...agResult.per_analyst);
|
|
197
533
|
result.total_cost_usd += agResult.total_cost_usd;
|
|
198
534
|
}
|
|
199
|
-
return { otlpPath, result };
|
|
535
|
+
return { otlpPath, execution, result };
|
|
200
536
|
}
|
|
201
537
|
|
|
202
|
-
// src/
|
|
203
|
-
var
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
/** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
|
|
226
|
-
* which collided with `tangle.ingest_source`.) */
|
|
227
|
-
SESSION_FILE: "tangle.session_file",
|
|
228
|
-
UPLOADED_AT: "tangle.uploaded_at",
|
|
229
|
-
UPLOADER: "tangle.uploader",
|
|
230
|
-
REDACTION_VERSION: "redaction.version",
|
|
231
|
-
REDACTION_COUNT: "redaction.count"
|
|
232
|
-
};
|
|
233
|
-
var INGEST_SOURCE_CLI = "cli";
|
|
234
|
-
var DEFAULT_HARNESS = "claude-code";
|
|
538
|
+
// src/adapters/conversation.ts
|
|
539
|
+
var CONTENT_CAP = 8e3;
|
|
540
|
+
function capText(raw) {
|
|
541
|
+
const t = raw.trim();
|
|
542
|
+
if (t.length <= CONTENT_CAP) return t;
|
|
543
|
+
return `${t.slice(0, CONTENT_CAP)}\u2026 [+${t.length - CONTENT_CAP} chars]`;
|
|
544
|
+
}
|
|
545
|
+
var ACTOR_ATTR = "tangle.actor";
|
|
546
|
+
function userPromptSpan(o) {
|
|
547
|
+
return span({
|
|
548
|
+
traceId: o.traceId,
|
|
549
|
+
spanId: o.spanId,
|
|
550
|
+
parentSpanId: o.parentSpanId,
|
|
551
|
+
name: "user.prompt",
|
|
552
|
+
kind: "CHAIN",
|
|
553
|
+
startTime: o.startTime,
|
|
554
|
+
service: o.service ?? null,
|
|
555
|
+
agent: o.agent ?? null,
|
|
556
|
+
step: o.step,
|
|
557
|
+
content: o.content,
|
|
558
|
+
extra: { [ACTOR_ATTR]: o.actor ?? "human" }
|
|
559
|
+
});
|
|
560
|
+
}
|
|
235
561
|
|
|
236
562
|
// src/time.ts
|
|
237
563
|
function parseIsoToEpochMs(ts) {
|
|
@@ -263,73 +589,238 @@ var KIND = {
|
|
|
263
589
|
};
|
|
264
590
|
async function toRuntimeStore(spans) {
|
|
265
591
|
const store = new InMemoryTraceStore();
|
|
266
|
-
const
|
|
592
|
+
const runs = /* @__PURE__ */ new Map();
|
|
593
|
+
for (const item of spans) {
|
|
594
|
+
const startedAt = parseIsoToEpochMs(item.start_time);
|
|
595
|
+
const endedAt = parseIsoToEpochMs(item.end_time);
|
|
596
|
+
const current = runs.get(item.trace_id);
|
|
597
|
+
runs.set(item.trace_id, current ? {
|
|
598
|
+
startedAt: Math.min(current.startedAt, startedAt),
|
|
599
|
+
endedAt: Math.max(current.endedAt, endedAt),
|
|
600
|
+
failed: current.failed || item.status.code === "ERROR"
|
|
601
|
+
} : { startedAt, endedAt, failed: item.status.code === "ERROR" });
|
|
602
|
+
}
|
|
603
|
+
for (const [runId, run] of runs) {
|
|
604
|
+
await store.appendRun({
|
|
605
|
+
runId,
|
|
606
|
+
scenarioId: "session",
|
|
607
|
+
startedAt: run.startedAt,
|
|
608
|
+
endedAt: run.endedAt,
|
|
609
|
+
status: run.failed ? "failed" : "completed"
|
|
610
|
+
});
|
|
611
|
+
}
|
|
267
612
|
for (const s of spans) {
|
|
268
|
-
if (!runIds.has(s.trace_id)) {
|
|
269
|
-
runIds.add(s.trace_id);
|
|
270
|
-
await store.appendRun({ runId: s.trace_id, scenarioId: "session" });
|
|
271
|
-
}
|
|
272
613
|
const kind = KIND[s.attributes["openinference.span.kind"]] ?? "custom";
|
|
614
|
+
const startedAt = parseIsoToEpochMs(s.start_time);
|
|
615
|
+
const endedAt = parseIsoToEpochMs(s.end_time);
|
|
273
616
|
const base = {
|
|
274
617
|
spanId: s.span_id,
|
|
275
618
|
parentSpanId: s.parent_span_id ?? void 0,
|
|
276
619
|
runId: s.trace_id,
|
|
277
620
|
name: s.name,
|
|
278
|
-
startedAt
|
|
279
|
-
endedAt
|
|
621
|
+
startedAt,
|
|
622
|
+
endedAt,
|
|
280
623
|
status: s.status.code === "ERROR" ? "error" : "ok",
|
|
281
624
|
error: s.status.message,
|
|
282
625
|
attributes: s.attributes
|
|
283
626
|
};
|
|
627
|
+
const tool = kind === "tool" ? toolArgumentsFromAttributes(s.attributes) : void 0;
|
|
284
628
|
const span2 = kind === "tool" ? {
|
|
285
629
|
...base,
|
|
286
630
|
kind: "tool",
|
|
287
631
|
toolName: String(s.attributes["tool.name"] ?? "tool"),
|
|
288
|
-
args:
|
|
632
|
+
args: tool.args,
|
|
633
|
+
argsCaptured: tool.argsCaptured,
|
|
634
|
+
latencyMs: Math.max(0, endedAt - startedAt)
|
|
289
635
|
} : { ...base, kind };
|
|
290
636
|
await store.appendSpan(span2);
|
|
291
637
|
}
|
|
292
|
-
return { store, runIds: [...
|
|
638
|
+
return { store, runIds: [...runs.keys()] };
|
|
293
639
|
}
|
|
294
640
|
|
|
295
641
|
// src/pipelines.ts
|
|
296
642
|
import { computeToolUseMetrics } from "@tangle-network/agent-eval";
|
|
297
|
-
import {
|
|
643
|
+
import {
|
|
644
|
+
failureClusterView,
|
|
645
|
+
stuckLoopView
|
|
646
|
+
} from "@tangle-network/agent-eval/pipelines";
|
|
298
647
|
async function runPipelines(spans, opts = {}) {
|
|
299
648
|
const { store, runIds } = await toRuntimeStore(spans);
|
|
300
649
|
const loopEligible = spans.filter((item) => item.attributes["traces.expected_blocking"] !== true);
|
|
301
650
|
const { store: loopStore } = await toRuntimeStore(loopEligible);
|
|
302
|
-
const stuckLoops = await
|
|
303
|
-
|
|
304
|
-
|
|
651
|
+
const [stuckLoops, failureClusters, toolUse] = await Promise.all([
|
|
652
|
+
stuckLoopView(loopStore, { minOccurrences: opts.minLoopOccurrences ?? 3 }),
|
|
653
|
+
failureClusterView(store),
|
|
654
|
+
Promise.all(runIds.map((runId) => computeToolUseMetrics(store, runId)))
|
|
655
|
+
]);
|
|
656
|
+
return { stuckLoops, failureClusters, toolUse };
|
|
305
657
|
}
|
|
306
658
|
|
|
307
|
-
// src/
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
659
|
+
// src/integrity.ts
|
|
660
|
+
import { createHash as createHash2 } from "crypto";
|
|
661
|
+
var receiptStates = /* @__PURE__ */ new WeakMap();
|
|
662
|
+
function receiptIdentity(receipt) {
|
|
663
|
+
return [
|
|
664
|
+
receipt.sourcePath,
|
|
665
|
+
receipt.lineNumber,
|
|
666
|
+
receipt.byteOffset,
|
|
667
|
+
receipt.byteLength,
|
|
668
|
+
receipt.sha256
|
|
669
|
+
];
|
|
313
670
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
671
|
+
function receiptKey(receipt) {
|
|
672
|
+
return JSON.stringify(receiptIdentity(receipt));
|
|
673
|
+
}
|
|
674
|
+
function stateFor(ref, current) {
|
|
675
|
+
let state = receiptStates.get(ref);
|
|
676
|
+
if (!state || state.corruptions !== current) {
|
|
677
|
+
state = {
|
|
678
|
+
corruptions: current,
|
|
679
|
+
seen: new Set(current.map(receiptKey))
|
|
680
|
+
};
|
|
681
|
+
receiptStates.set(ref, state);
|
|
682
|
+
}
|
|
683
|
+
return state;
|
|
684
|
+
}
|
|
685
|
+
function recordSessionCorruption(ref, receipt) {
|
|
686
|
+
const current = ref.integrity?.corruptions ?? [];
|
|
687
|
+
const corruption = { ...receipt, harness: ref.harness, sessionId: ref.sessionId };
|
|
688
|
+
const state = stateFor(ref, current);
|
|
689
|
+
const key = receiptKey(receipt);
|
|
690
|
+
if (state.seen.has(key)) return;
|
|
691
|
+
if (ref.integrity) {
|
|
692
|
+
ref.integrity.corruptions.push(corruption);
|
|
693
|
+
} else {
|
|
694
|
+
ref.integrity = { status: "degraded_not_lossless", corruptions: [corruption] };
|
|
695
|
+
}
|
|
696
|
+
state.seen.add(key);
|
|
697
|
+
state.corruptions = ref.integrity.corruptions;
|
|
698
|
+
}
|
|
699
|
+
function sessionJsonlOptions(ref, options = {}) {
|
|
700
|
+
if (options.corruptionMode === "strict") return { mode: "strict" };
|
|
701
|
+
return {
|
|
702
|
+
mode: "recover",
|
|
703
|
+
onCorruption: (receipt) => recordSessionCorruption(ref, receipt)
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
function sessionIntegrityAttributes(ref) {
|
|
707
|
+
if (!ref.integrity) return {};
|
|
708
|
+
return {
|
|
709
|
+
[ATTR.SESSION_INTEGRITY]: ref.integrity.status,
|
|
710
|
+
[ATTR.CORRUPTION_COUNT]: ref.integrity.corruptions.length,
|
|
711
|
+
[ATTR.CORRUPTION_DIGEST]: corruptionDigest(ref.integrity.corruptions),
|
|
712
|
+
[ATTR.RAW_SOURCE_RETENTION]: "local_source_only"
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function corruptionDigest(receipts) {
|
|
716
|
+
const hash2 = createHash2("sha256");
|
|
717
|
+
for (const receipt of receipts) {
|
|
718
|
+
hash2.update(JSON.stringify(receiptIdentity(receipt)));
|
|
719
|
+
hash2.update("\n");
|
|
720
|
+
}
|
|
721
|
+
return `sha256:${hash2.digest("hex")}`;
|
|
722
|
+
}
|
|
723
|
+
function receiptSpanId(parentSpanId, receipt) {
|
|
724
|
+
const digest = createHash2("sha256").update(JSON.stringify(receiptIdentity(receipt))).digest("hex");
|
|
725
|
+
return `${parentSpanId}:corruption:${digest}`;
|
|
726
|
+
}
|
|
727
|
+
function stampSessionIntegrity(ref, spans) {
|
|
728
|
+
if (!ref.integrity) return;
|
|
729
|
+
const parent = spans.find((item) => item.parent_span_id === null) ?? spans[0];
|
|
730
|
+
if (!parent) return;
|
|
731
|
+
const sessionId = parent.trace_id;
|
|
732
|
+
for (const receipt of ref.integrity.corruptions) {
|
|
733
|
+
receipt.harness = ref.harness;
|
|
734
|
+
receipt.sessionId = sessionId;
|
|
735
|
+
}
|
|
736
|
+
const attributes = sessionIntegrityAttributes(ref);
|
|
737
|
+
Object.assign(parent.attributes, attributes);
|
|
738
|
+
const existingSpanIds = new Set(spans.map((item) => item.span_id));
|
|
739
|
+
for (const receipt of ref.integrity.corruptions) {
|
|
740
|
+
const spanId = receiptSpanId(parent.span_id, receipt);
|
|
741
|
+
if (existingSpanIds.has(spanId)) continue;
|
|
742
|
+
spans.push(span({
|
|
743
|
+
traceId: parent.trace_id,
|
|
744
|
+
spanId,
|
|
745
|
+
parentSpanId: parent.span_id,
|
|
746
|
+
name: "source.corruption.receipt",
|
|
747
|
+
kind: "CHAIN",
|
|
748
|
+
startTime: parent.start_time,
|
|
749
|
+
endTime: parent.start_time,
|
|
750
|
+
status: "ERROR",
|
|
751
|
+
service: typeof parent.attributes["service.name"] === "string" ? parent.attributes["service.name"] : ref.harness,
|
|
752
|
+
extra: {
|
|
753
|
+
[ATTR.SESSION_INTEGRITY]: ref.integrity.status,
|
|
754
|
+
[ATTR.CORRUPTION_RECEIPT_VERSION]: receipt.receiptVersion,
|
|
755
|
+
[ATTR.CORRUPTION_RECEIPT_KIND]: receipt.kind,
|
|
756
|
+
[ATTR.CORRUPTION_SOURCE_PATH]: receipt.sourcePath,
|
|
757
|
+
[ATTR.CORRUPTION_LINE_NUMBER]: receipt.lineNumber,
|
|
758
|
+
[ATTR.CORRUPTION_BYTE_OFFSET]: receipt.byteOffset,
|
|
759
|
+
[ATTR.CORRUPTION_BYTE_LENGTH]: receipt.byteLength,
|
|
760
|
+
[ATTR.CORRUPTION_SHA256]: receipt.sha256,
|
|
761
|
+
[ATTR.RAW_SOURCE_RETENTION]: "local_source_only"
|
|
762
|
+
}
|
|
763
|
+
}));
|
|
764
|
+
existingSpanIds.add(spanId);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/json.ts
|
|
769
|
+
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
770
|
+
var JsonSourceError = class extends Error {
|
|
771
|
+
sourcePath;
|
|
772
|
+
kind;
|
|
773
|
+
code;
|
|
774
|
+
constructor(sourcePath, kind, cause) {
|
|
775
|
+
super(
|
|
776
|
+
kind === "parse" ? `Invalid JSON at ${sourcePath}` : `Unable to read JSON source at ${sourcePath}`,
|
|
777
|
+
{ cause }
|
|
778
|
+
);
|
|
779
|
+
this.name = "JsonSourceError";
|
|
780
|
+
this.sourcePath = sourcePath;
|
|
781
|
+
this.kind = kind;
|
|
782
|
+
this.code = errorCode(cause);
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
function errorCode(error) {
|
|
786
|
+
return error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
787
|
+
}
|
|
788
|
+
function pathSafeParseCause(cause) {
|
|
789
|
+
if (!(cause instanceof Error)) return cause;
|
|
790
|
+
const location = cause.message.match(/(?:at position \d+(?: \(line \d+ column \d+\))?|line \d+ column \d+)/i)?.[0];
|
|
791
|
+
cause.message = location ? `JSON parse failed ${location}` : "JSON parse failed";
|
|
792
|
+
cause.stack = `${cause.name}: ${cause.message}`;
|
|
793
|
+
return cause;
|
|
794
|
+
}
|
|
795
|
+
function isMissingPathError(error) {
|
|
796
|
+
return errorCode(error) === "ENOENT";
|
|
797
|
+
}
|
|
798
|
+
function isMissingJsonSource(error) {
|
|
799
|
+
return error instanceof JsonSourceError && error.kind === "read" && isMissingPathError(error);
|
|
800
|
+
}
|
|
801
|
+
async function readJsonFile(path) {
|
|
802
|
+
let raw;
|
|
803
|
+
try {
|
|
804
|
+
raw = await readFile2(path, "utf8");
|
|
805
|
+
} catch (error) {
|
|
806
|
+
throw new JsonSourceError(path, "read", error);
|
|
807
|
+
}
|
|
808
|
+
try {
|
|
809
|
+
return JSON.parse(raw);
|
|
810
|
+
} catch (error) {
|
|
811
|
+
throw new JsonSourceError(path, "parse", pathSafeParseCause(error));
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
async function listJsonFiles(path) {
|
|
815
|
+
try {
|
|
816
|
+
return (await readdir(path)).filter((file) => file.endsWith(".json"));
|
|
817
|
+
} catch (error) {
|
|
818
|
+
throw new JsonSourceError(path, "read", error);
|
|
819
|
+
}
|
|
329
820
|
}
|
|
330
821
|
|
|
331
822
|
// src/adapters/amp.ts
|
|
332
|
-
import { readdir
|
|
823
|
+
import { readdir as readdir2, stat } from "fs/promises";
|
|
333
824
|
import { homedir } from "os";
|
|
334
825
|
import { basename, join as join2 } from "path";
|
|
335
826
|
var SERVICE = "amp";
|
|
@@ -348,9 +839,10 @@ var AmpAdapter = class {
|
|
|
348
839
|
async locate(opts = {}) {
|
|
349
840
|
let files;
|
|
350
841
|
try {
|
|
351
|
-
files = await
|
|
352
|
-
} catch {
|
|
353
|
-
return [];
|
|
842
|
+
files = await readdir2(this.threadsRoot());
|
|
843
|
+
} catch (error) {
|
|
844
|
+
if (isMissingPathError(error)) return [];
|
|
845
|
+
throw error;
|
|
354
846
|
}
|
|
355
847
|
const refs = [];
|
|
356
848
|
for (const f of files) {
|
|
@@ -359,8 +851,9 @@ var AmpAdapter = class {
|
|
|
359
851
|
let st;
|
|
360
852
|
try {
|
|
361
853
|
st = await stat(path);
|
|
362
|
-
} catch {
|
|
363
|
-
continue;
|
|
854
|
+
} catch (error) {
|
|
855
|
+
if (isMissingPathError(error)) continue;
|
|
856
|
+
throw error;
|
|
364
857
|
}
|
|
365
858
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
366
859
|
if (opts.cwd) continue;
|
|
@@ -369,12 +862,7 @@ var AmpAdapter = class {
|
|
|
369
862
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
370
863
|
}
|
|
371
864
|
async parse(ref) {
|
|
372
|
-
|
|
373
|
-
try {
|
|
374
|
-
thread = JSON.parse(await readFile2(ref.path, "utf8"));
|
|
375
|
-
} catch {
|
|
376
|
-
return [];
|
|
377
|
-
}
|
|
865
|
+
const thread = await readJsonFile(ref.path);
|
|
378
866
|
const traceId = thread.id ?? ref.sessionId;
|
|
379
867
|
const rootId = `root:${traceId}`;
|
|
380
868
|
const start = new Date(thread.created ?? 0).toISOString();
|
|
@@ -406,7 +894,6 @@ var AmpAdapter = class {
|
|
|
406
894
|
}
|
|
407
895
|
} else {
|
|
408
896
|
const u = m.usage;
|
|
409
|
-
const inputTokens2 = u ? (u.inputTokens ?? 0) + (u.cacheReadInputTokens ?? 0) + (u.cacheCreationInputTokens ?? 0) || null : null;
|
|
410
897
|
spans.push(
|
|
411
898
|
span({
|
|
412
899
|
traceId,
|
|
@@ -418,8 +905,10 @@ var AmpAdapter = class {
|
|
|
418
905
|
service: SERVICE,
|
|
419
906
|
agent: SERVICE,
|
|
420
907
|
model: u?.model ?? null,
|
|
421
|
-
inputTokens:
|
|
908
|
+
inputTokens: u?.inputTokens ?? null,
|
|
422
909
|
outputTokens: u?.outputTokens ?? null,
|
|
910
|
+
cachedInputTokens: u?.cacheReadInputTokens ?? null,
|
|
911
|
+
cacheWriteInputTokens: u?.cacheCreationInputTokens ?? null,
|
|
423
912
|
step,
|
|
424
913
|
content: textOf(m.content) || null
|
|
425
914
|
})
|
|
@@ -439,14 +928,17 @@ var AmpAdapter = class {
|
|
|
439
928
|
agent: SERVICE,
|
|
440
929
|
tool: b.name,
|
|
441
930
|
step,
|
|
442
|
-
|
|
931
|
+
extra: toolIoAttributes({ input: b.input })
|
|
443
932
|
});
|
|
444
933
|
spans.push(t);
|
|
445
934
|
if (b.id) toolByUseId.set(b.id, t);
|
|
446
935
|
step += 1;
|
|
447
936
|
} else if (b.type === "tool_result" && b.tool_use_id) {
|
|
448
937
|
const t = toolByUseId.get(b.tool_use_id);
|
|
449
|
-
if (t)
|
|
938
|
+
if (t) {
|
|
939
|
+
t.status = b.is_error === true ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
|
|
940
|
+
recordToolOutput(t, b.content ?? b.output);
|
|
941
|
+
}
|
|
450
942
|
}
|
|
451
943
|
}
|
|
452
944
|
}
|
|
@@ -454,6 +946,108 @@ var AmpAdapter = class {
|
|
|
454
946
|
}
|
|
455
947
|
};
|
|
456
948
|
|
|
949
|
+
// src/jsonl.ts
|
|
950
|
+
import { isUtf8 } from "buffer";
|
|
951
|
+
import { createHash as createHash3 } from "crypto";
|
|
952
|
+
import { createReadStream } from "fs";
|
|
953
|
+
var JsonlParseError = class extends SyntaxError {
|
|
954
|
+
sourcePath;
|
|
955
|
+
lineNumber;
|
|
956
|
+
byteOffset;
|
|
957
|
+
byteLength;
|
|
958
|
+
sha256;
|
|
959
|
+
receipt;
|
|
960
|
+
constructor(receipt) {
|
|
961
|
+
super(`Invalid JSONL at ${receipt.sourcePath}:${receipt.lineNumber}`);
|
|
962
|
+
this.name = "JsonlParseError";
|
|
963
|
+
this.sourcePath = receipt.sourcePath;
|
|
964
|
+
this.lineNumber = receipt.lineNumber;
|
|
965
|
+
this.byteOffset = receipt.byteOffset;
|
|
966
|
+
this.byteLength = receipt.byteLength;
|
|
967
|
+
this.sha256 = receipt.sha256;
|
|
968
|
+
this.receipt = receipt;
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
function corruptionReceipt(rawLine, path, lineNumber, byteOffset) {
|
|
972
|
+
return {
|
|
973
|
+
receiptVersion: 1,
|
|
974
|
+
kind: "jsonl_corruption",
|
|
975
|
+
status: "degraded_not_lossless",
|
|
976
|
+
sourcePath: path,
|
|
977
|
+
lineNumber,
|
|
978
|
+
byteOffset,
|
|
979
|
+
byteLength: rawLine.length,
|
|
980
|
+
sha256: createHash3("sha256").update(rawLine).digest("hex"),
|
|
981
|
+
rawBytes: "local_source_only"
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
function parseLine(rawLine, path, lineNumber, byteOffset, options) {
|
|
985
|
+
const jsonBytes = rawLine.at(-1) === 13 ? rawLine.subarray(0, -1) : rawLine;
|
|
986
|
+
if (jsonBytes.length === 0) return void 0;
|
|
987
|
+
if (!isUtf8(jsonBytes)) return handleCorruption(rawLine, path, lineNumber, byteOffset, options);
|
|
988
|
+
const json2 = jsonBytes.toString("utf8");
|
|
989
|
+
if (json2.trim().length === 0) return void 0;
|
|
990
|
+
try {
|
|
991
|
+
return JSON.parse(json2);
|
|
992
|
+
} catch {
|
|
993
|
+
return handleCorruption(rawLine, path, lineNumber, byteOffset, options);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
function handleCorruption(rawLine, path, lineNumber, byteOffset, options) {
|
|
997
|
+
const receipt = corruptionReceipt(rawLine, path, lineNumber, byteOffset);
|
|
998
|
+
if (options.mode === "recover") {
|
|
999
|
+
options.onCorruption(receipt);
|
|
1000
|
+
return void 0;
|
|
1001
|
+
}
|
|
1002
|
+
throw new JsonlParseError(receipt);
|
|
1003
|
+
}
|
|
1004
|
+
async function* readJsonl(path, options = { mode: "strict" }) {
|
|
1005
|
+
if (options.mode === "recover" && typeof options.onCorruption !== "function") {
|
|
1006
|
+
throw new TypeError("JSONL recover mode requires an onCorruption callback");
|
|
1007
|
+
}
|
|
1008
|
+
const input = createReadStream(path);
|
|
1009
|
+
let lineNumber = 0;
|
|
1010
|
+
let byteOffset = 0;
|
|
1011
|
+
let fragments = [];
|
|
1012
|
+
try {
|
|
1013
|
+
for await (const chunk of input) {
|
|
1014
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1015
|
+
let start = 0;
|
|
1016
|
+
let newline = bytes.indexOf(10);
|
|
1017
|
+
while (newline !== -1) {
|
|
1018
|
+
fragments.push(bytes.subarray(start, newline));
|
|
1019
|
+
const rawLine = fragments.length === 1 ? fragments[0] : Buffer.concat(fragments);
|
|
1020
|
+
fragments = [];
|
|
1021
|
+
lineNumber += 1;
|
|
1022
|
+
const parsed = parseLine(rawLine, path, lineNumber, byteOffset, options);
|
|
1023
|
+
if (parsed !== void 0) yield parsed;
|
|
1024
|
+
byteOffset += rawLine.length + 1;
|
|
1025
|
+
start = newline + 1;
|
|
1026
|
+
newline = bytes.indexOf(10, start);
|
|
1027
|
+
}
|
|
1028
|
+
if (start < bytes.length) fragments.push(bytes.subarray(start));
|
|
1029
|
+
}
|
|
1030
|
+
if (fragments.length > 0) {
|
|
1031
|
+
const rawLine = fragments.length === 1 ? fragments[0] : Buffer.concat(fragments);
|
|
1032
|
+
lineNumber += 1;
|
|
1033
|
+
const parsed = parseLine(rawLine, path, lineNumber, byteOffset, options);
|
|
1034
|
+
if (parsed !== void 0) yield parsed;
|
|
1035
|
+
}
|
|
1036
|
+
} finally {
|
|
1037
|
+
input.destroy();
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
async function takeJsonl(path, limit, options = { mode: "strict" }) {
|
|
1041
|
+
if (!Number.isInteger(limit) || limit < 0) throw new RangeError("limit must be a non-negative integer");
|
|
1042
|
+
const rows = [];
|
|
1043
|
+
if (limit === 0) return rows;
|
|
1044
|
+
for await (const row of readJsonl(path, options)) {
|
|
1045
|
+
rows.push(row);
|
|
1046
|
+
if (rows.length === limit) break;
|
|
1047
|
+
}
|
|
1048
|
+
return rows;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
457
1051
|
// src/adapters/actor.ts
|
|
458
1052
|
var CMD_MARKERS = [
|
|
459
1053
|
"<command-name>",
|
|
@@ -471,6 +1065,10 @@ var INJECT_MARKERS = [
|
|
|
471
1065
|
// not a person. (Observed in Claude Code task/loop sessions.)
|
|
472
1066
|
"<task-notification>",
|
|
473
1067
|
"<task-prompt>",
|
|
1068
|
+
// Prefix match: Codex adds attributes such as source="goal" to this tag.
|
|
1069
|
+
"<codex_internal_context",
|
|
1070
|
+
"<subagent_notification>",
|
|
1071
|
+
"# AGENTS.md instructions for ",
|
|
474
1072
|
"<local-command-stdout>",
|
|
475
1073
|
"This session is being continued from a previous conversation",
|
|
476
1074
|
// Slash-command skill bodies are expanded into a user turn (no <command-name>
|
|
@@ -543,26 +1141,10 @@ function classifyReaction(text) {
|
|
|
543
1141
|
var CORRECTIVE_REACTIONS = ["correction", "frustration", "jargon", "structure"];
|
|
544
1142
|
|
|
545
1143
|
// src/adapters/claude.ts
|
|
546
|
-
import { readdir as
|
|
1144
|
+
import { readdir as readdir3, stat as stat2 } from "fs/promises";
|
|
547
1145
|
import { homedir as homedir2 } from "os";
|
|
548
1146
|
import { basename as basename2, join as join3 } from "path";
|
|
549
1147
|
var SERVICE2 = "claude-code";
|
|
550
|
-
function parseLines(raw) {
|
|
551
|
-
const out = [];
|
|
552
|
-
for (const line of raw.split("\n")) {
|
|
553
|
-
if (!line) continue;
|
|
554
|
-
try {
|
|
555
|
-
out.push(JSON.parse(line));
|
|
556
|
-
} catch {
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
return out;
|
|
560
|
-
}
|
|
561
|
-
function inputTokens(u) {
|
|
562
|
-
if (!u) return null;
|
|
563
|
-
const v = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
|
|
564
|
-
return v > 0 ? v : null;
|
|
565
|
-
}
|
|
566
1148
|
function asBlocks(content) {
|
|
567
1149
|
return Array.isArray(content) ? content : [];
|
|
568
1150
|
}
|
|
@@ -579,96 +1161,104 @@ function stringifyToolResult(content) {
|
|
|
579
1161
|
}
|
|
580
1162
|
return "";
|
|
581
1163
|
}
|
|
582
|
-
function
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
1164
|
+
function createClaudeStream(startStep) {
|
|
1165
|
+
return { spans: [], toolSpanByUseId: /* @__PURE__ */ new Map(), step: startStep, sawUserTurn: false };
|
|
1166
|
+
}
|
|
1167
|
+
function consumeClaudeEvent(ev, ctx, state) {
|
|
1168
|
+
const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1169
|
+
const uid = ev.uuid ?? `${ctx.idPrefix}step${state.step}`;
|
|
1170
|
+
if (ev.type === "assistant" && ev.message) {
|
|
1171
|
+
const llmId = `${ctx.idPrefix}${uid}`;
|
|
1172
|
+
state.spans.push(
|
|
1173
|
+
span({
|
|
1174
|
+
traceId: ctx.traceId,
|
|
1175
|
+
spanId: llmId,
|
|
1176
|
+
parentSpanId: ctx.rootParent,
|
|
1177
|
+
name: "llm.turn",
|
|
1178
|
+
kind: "LLM",
|
|
1179
|
+
startTime: ts,
|
|
1180
|
+
service: SERVICE2,
|
|
1181
|
+
agent: ctx.agent,
|
|
1182
|
+
model: ev.message.model ?? null,
|
|
1183
|
+
inputTokens: ev.message.usage?.input_tokens ?? null,
|
|
1184
|
+
outputTokens: ev.message.usage?.output_tokens ?? null,
|
|
1185
|
+
cachedInputTokens: ev.message.usage?.cache_read_input_tokens ?? null,
|
|
1186
|
+
cacheWriteInputTokens: ev.message.usage?.cache_creation_input_tokens ?? null,
|
|
1187
|
+
step: state.step,
|
|
1188
|
+
content: textOf2(ev.message.content) || null
|
|
1189
|
+
})
|
|
1190
|
+
);
|
|
1191
|
+
state.step += 1;
|
|
1192
|
+
let toolIdx = 0;
|
|
1193
|
+
for (const block of asBlocks(ev.message.content)) {
|
|
1194
|
+
if (block.type !== "tool_use" || !block.name) continue;
|
|
1195
|
+
const toolSpan = span({
|
|
1196
|
+
traceId: ctx.traceId,
|
|
1197
|
+
spanId: `${ctx.idPrefix}${uid}:tool:${toolIdx}`,
|
|
1198
|
+
parentSpanId: llmId,
|
|
1199
|
+
name: `tool.${block.name}`,
|
|
1200
|
+
kind: "TOOL",
|
|
1201
|
+
startTime: ts,
|
|
1202
|
+
service: SERVICE2,
|
|
1203
|
+
agent: ctx.agent,
|
|
1204
|
+
tool: block.name,
|
|
1205
|
+
step: state.step,
|
|
1206
|
+
extra: toolIoAttributes({ input: block.input })
|
|
1207
|
+
});
|
|
1208
|
+
state.spans.push(toolSpan);
|
|
1209
|
+
if (block.id) state.toolSpanByUseId.set(block.id, toolSpan);
|
|
1210
|
+
toolIdx += 1;
|
|
1211
|
+
state.step += 1;
|
|
1212
|
+
}
|
|
1213
|
+
} else if (ev.type === "user" && ev.message) {
|
|
1214
|
+
const prompt = textOf2(ev.message.content);
|
|
1215
|
+
if (prompt) {
|
|
1216
|
+
const actor = claudeActor({
|
|
1217
|
+
text: prompt,
|
|
1218
|
+
isSidechain: ev.isSidechain,
|
|
1219
|
+
userType: ev.userType ?? null,
|
|
1220
|
+
isFirstUserTurn: !state.sawUserTurn
|
|
1221
|
+
});
|
|
1222
|
+
state.sawUserTurn = true;
|
|
1223
|
+
state.spans.push(
|
|
1224
|
+
userPromptSpan({
|
|
594
1225
|
traceId: ctx.traceId,
|
|
595
|
-
spanId:
|
|
1226
|
+
spanId: `${ctx.idPrefix}${uid}:user`,
|
|
596
1227
|
parentSpanId: ctx.rootParent,
|
|
597
|
-
name: "llm.turn",
|
|
598
|
-
kind: "LLM",
|
|
599
1228
|
startTime: ts,
|
|
600
1229
|
service: SERVICE2,
|
|
601
1230
|
agent: ctx.agent,
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
step,
|
|
606
|
-
content: textOf2(ev.message.content) || null
|
|
1231
|
+
step: state.step,
|
|
1232
|
+
content: prompt,
|
|
1233
|
+
actor
|
|
607
1234
|
})
|
|
608
1235
|
);
|
|
609
|
-
step += 1;
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
startTime: ts,
|
|
620
|
-
service: SERVICE2,
|
|
621
|
-
agent: ctx.agent,
|
|
622
|
-
tool: block.name,
|
|
623
|
-
step,
|
|
624
|
-
content: block.input != null ? JSON.stringify(block.input) : null
|
|
625
|
-
});
|
|
626
|
-
spans.push(toolSpan);
|
|
627
|
-
if (block.id) toolSpanByUseId.set(block.id, toolSpan);
|
|
628
|
-
toolIdx += 1;
|
|
629
|
-
step += 1;
|
|
630
|
-
}
|
|
631
|
-
} else if (ev.type === "user" && ev.message) {
|
|
632
|
-
const prompt = textOf2(ev.message.content);
|
|
633
|
-
if (prompt) {
|
|
634
|
-
const actor = claudeActor({
|
|
635
|
-
text: prompt,
|
|
636
|
-
isSidechain: ev.isSidechain,
|
|
637
|
-
userType: ev.userType ?? null,
|
|
638
|
-
isFirstUserTurn: !sawUserTurn
|
|
639
|
-
});
|
|
640
|
-
sawUserTurn = true;
|
|
641
|
-
spans.push(
|
|
642
|
-
userPromptSpan({
|
|
643
|
-
traceId: ctx.traceId,
|
|
644
|
-
spanId: `${ctx.idPrefix}${uid}:user`,
|
|
645
|
-
parentSpanId: ctx.rootParent,
|
|
646
|
-
startTime: ts,
|
|
647
|
-
service: SERVICE2,
|
|
648
|
-
agent: ctx.agent,
|
|
649
|
-
step,
|
|
650
|
-
content: prompt,
|
|
651
|
-
actor
|
|
652
|
-
})
|
|
653
|
-
);
|
|
654
|
-
step += 1;
|
|
655
|
-
}
|
|
656
|
-
for (const block of asBlocks(ev.message.content)) {
|
|
657
|
-
if (block.type !== "tool_result" || !block.tool_use_id) continue;
|
|
658
|
-
backfillResult(toolSpanByUseId.get(block.tool_use_id), ts, block.is_error === true, stringifyToolResult(block.content));
|
|
659
|
-
}
|
|
660
|
-
} else if (ev.type === "attachment" && ev.attachment?.type === "tool_result" && ev.attachment.toolUseID) {
|
|
661
|
-
const err = typeof ev.attachment.exitCode === "number" && ev.attachment.exitCode !== 0;
|
|
662
|
-
backfillResult(toolSpanByUseId.get(ev.attachment.toolUseID), ts, err, ev.attachment.stderr ?? "");
|
|
1236
|
+
state.step += 1;
|
|
1237
|
+
}
|
|
1238
|
+
for (const block of asBlocks(ev.message.content)) {
|
|
1239
|
+
if (block.type !== "tool_result" || !block.tool_use_id) continue;
|
|
1240
|
+
backfillResult(
|
|
1241
|
+
state.toolSpanByUseId.get(block.tool_use_id),
|
|
1242
|
+
ts,
|
|
1243
|
+
block.is_error === true,
|
|
1244
|
+
block.content
|
|
1245
|
+
);
|
|
663
1246
|
}
|
|
1247
|
+
} else if (ev.type === "attachment" && ev.attachment?.type === "tool_result" && ev.attachment.toolUseID) {
|
|
1248
|
+
const err = typeof ev.attachment.exitCode === "number" && ev.attachment.exitCode !== 0;
|
|
1249
|
+
backfillResult(state.toolSpanByUseId.get(ev.attachment.toolUseID), ts, err, ev.attachment.stderr ?? "");
|
|
664
1250
|
}
|
|
665
|
-
return { spans, toolSpanByUseId, nextStep: step };
|
|
666
1251
|
}
|
|
667
|
-
function
|
|
1252
|
+
function finishClaudeStream(state) {
|
|
1253
|
+
return { spans: state.spans, toolSpanByUseId: state.toolSpanByUseId, nextStep: state.step };
|
|
1254
|
+
}
|
|
1255
|
+
function backfillResult(s, endTime, isError, output) {
|
|
668
1256
|
if (!s) return;
|
|
669
1257
|
s.end_time = endTime;
|
|
670
1258
|
const code = isError ? "ERROR" : "OK";
|
|
671
1259
|
s.status = { code };
|
|
1260
|
+
recordToolOutput(s, output);
|
|
1261
|
+
const message = stringifyToolResult(output);
|
|
672
1262
|
if (isError && message) s.status.message = message.slice(0, 500);
|
|
673
1263
|
}
|
|
674
1264
|
var ClaudeAdapter = class {
|
|
@@ -681,18 +1271,20 @@ var ClaudeAdapter = class {
|
|
|
681
1271
|
const root = this.root();
|
|
682
1272
|
let projectDirs;
|
|
683
1273
|
try {
|
|
684
|
-
projectDirs = await
|
|
685
|
-
} catch {
|
|
686
|
-
return [];
|
|
1274
|
+
projectDirs = await readdir3(root);
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
if (isMissingPathError(error)) return [];
|
|
1277
|
+
throw error;
|
|
687
1278
|
}
|
|
688
1279
|
const refs = [];
|
|
689
1280
|
for (const dir of projectDirs) {
|
|
690
1281
|
const dirPath = join3(root, dir);
|
|
691
1282
|
let files;
|
|
692
1283
|
try {
|
|
693
|
-
files = await
|
|
694
|
-
} catch {
|
|
695
|
-
continue;
|
|
1284
|
+
files = await readdir3(dirPath);
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
if (isMissingPathError(error)) continue;
|
|
1287
|
+
throw error;
|
|
696
1288
|
}
|
|
697
1289
|
for (const f of files) {
|
|
698
1290
|
if (!f.endsWith(".jsonl")) continue;
|
|
@@ -700,8 +1292,9 @@ var ClaudeAdapter = class {
|
|
|
700
1292
|
let st;
|
|
701
1293
|
try {
|
|
702
1294
|
st = await stat2(path);
|
|
703
|
-
} catch {
|
|
704
|
-
continue;
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
if (isMissingPathError(error)) continue;
|
|
1297
|
+
throw error;
|
|
705
1298
|
}
|
|
706
1299
|
if (!st.isFile()) continue;
|
|
707
1300
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
@@ -712,11 +1305,37 @@ var ClaudeAdapter = class {
|
|
|
712
1305
|
}
|
|
713
1306
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
714
1307
|
}
|
|
715
|
-
async parse(ref) {
|
|
716
|
-
const
|
|
717
|
-
const
|
|
718
|
-
const
|
|
1308
|
+
async parse(ref, options = {}) {
|
|
1309
|
+
const sourceTraceId = ref.sessionId;
|
|
1310
|
+
const sourceRootId = `root:${sourceTraceId}`;
|
|
1311
|
+
const ctx = {
|
|
1312
|
+
traceId: sourceTraceId,
|
|
1313
|
+
agent: SERVICE2,
|
|
1314
|
+
startStep: 0,
|
|
1315
|
+
idPrefix: "",
|
|
1316
|
+
rootParent: sourceRootId
|
|
1317
|
+
};
|
|
1318
|
+
const state = createClaudeStream(ctx.startStep);
|
|
1319
|
+
let discoveredTraceId;
|
|
1320
|
+
let firstTimestamp;
|
|
1321
|
+
let lastTimestamp;
|
|
1322
|
+
let sawEvent = false;
|
|
1323
|
+
for await (const event of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
|
|
1324
|
+
if (!sawEvent) {
|
|
1325
|
+
firstTimestamp = event.timestamp;
|
|
1326
|
+
sawEvent = true;
|
|
1327
|
+
}
|
|
1328
|
+
lastTimestamp = event.timestamp;
|
|
1329
|
+
if (!discoveredTraceId && event.sessionId) discoveredTraceId = event.sessionId;
|
|
1330
|
+
consumeClaudeEvent(event, ctx, state);
|
|
1331
|
+
}
|
|
1332
|
+
const main = finishClaudeStream(state);
|
|
1333
|
+
const traceId = discoveredTraceId ?? sourceTraceId;
|
|
719
1334
|
const rootId = `root:${traceId}`;
|
|
1335
|
+
for (const item of main.spans) {
|
|
1336
|
+
item.trace_id = traceId;
|
|
1337
|
+
if (item.parent_span_id === sourceRootId) item.parent_span_id = rootId;
|
|
1338
|
+
}
|
|
720
1339
|
const spans = [
|
|
721
1340
|
span({
|
|
722
1341
|
traceId,
|
|
@@ -724,55 +1343,53 @@ var ClaudeAdapter = class {
|
|
|
724
1343
|
parentSpanId: null,
|
|
725
1344
|
name: "session",
|
|
726
1345
|
kind: "AGENT",
|
|
727
|
-
startTime:
|
|
728
|
-
endTime:
|
|
1346
|
+
startTime: firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1347
|
+
endTime: lastTimestamp,
|
|
729
1348
|
service: SERVICE2,
|
|
730
1349
|
agent: SERVICE2
|
|
731
|
-
})
|
|
1350
|
+
}),
|
|
1351
|
+
...main.spans
|
|
732
1352
|
];
|
|
733
|
-
|
|
734
|
-
traceId,
|
|
735
|
-
agent: SERVICE2,
|
|
736
|
-
startStep: 0,
|
|
737
|
-
idPrefix: "",
|
|
738
|
-
rootParent: rootId
|
|
739
|
-
});
|
|
740
|
-
spans.push(...main.spans);
|
|
741
|
-
await this.foldSubagents(ref, traceId, main, spans);
|
|
1353
|
+
await this.foldSubagents(ref, traceId, main, spans, options);
|
|
742
1354
|
return spans;
|
|
743
1355
|
}
|
|
744
1356
|
/** Parse `<session>/subagents/agent-*.jsonl`, parenting each under its Agent call. */
|
|
745
|
-
async foldSubagents(ref, traceId, main, out) {
|
|
1357
|
+
async foldSubagents(ref, traceId, main, out, options) {
|
|
746
1358
|
const subDir = join3(ref.path.replace(/\.jsonl$/, ""), "subagents");
|
|
747
1359
|
let files;
|
|
748
1360
|
try {
|
|
749
|
-
files = await
|
|
750
|
-
} catch {
|
|
751
|
-
return;
|
|
1361
|
+
files = await readdir3(subDir);
|
|
1362
|
+
} catch (error) {
|
|
1363
|
+
if (isMissingPathError(error)) return;
|
|
1364
|
+
throw error;
|
|
752
1365
|
}
|
|
753
1366
|
let step = main.nextStep;
|
|
754
|
-
for (const
|
|
755
|
-
if (!
|
|
756
|
-
const hash2 = basename2(
|
|
1367
|
+
for (const file of files) {
|
|
1368
|
+
if (!file.endsWith(".jsonl")) continue;
|
|
1369
|
+
const hash2 = basename2(file, ".jsonl");
|
|
1370
|
+
const metaPath = join3(subDir, `${hash2}.meta.json`);
|
|
757
1371
|
let meta = {};
|
|
758
1372
|
try {
|
|
759
|
-
meta =
|
|
760
|
-
} catch {
|
|
1373
|
+
meta = await readJsonFile(metaPath);
|
|
1374
|
+
} catch (error) {
|
|
1375
|
+
if (!isMissingJsonSource(error)) throw error;
|
|
761
1376
|
}
|
|
762
1377
|
const parent = meta.toolUseId && main.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
|
|
763
|
-
|
|
764
|
-
try {
|
|
765
|
-
raw = await readFile3(join3(subDir, f), "utf8");
|
|
766
|
-
} catch {
|
|
767
|
-
continue;
|
|
768
|
-
}
|
|
769
|
-
const parsed = parseClaudeStream(parseLines(raw), {
|
|
1378
|
+
const ctx = {
|
|
770
1379
|
traceId,
|
|
771
1380
|
agent: meta.agentType ? `subagent:${meta.agentType}` : "subagent",
|
|
772
1381
|
startStep: step,
|
|
773
1382
|
idPrefix: `${hash2}:`,
|
|
774
1383
|
rootParent: parent
|
|
775
|
-
}
|
|
1384
|
+
};
|
|
1385
|
+
const state = createClaudeStream(ctx.startStep);
|
|
1386
|
+
for await (const event of readJsonl(
|
|
1387
|
+
join3(subDir, file),
|
|
1388
|
+
sessionJsonlOptions(ref, options)
|
|
1389
|
+
)) {
|
|
1390
|
+
consumeClaudeEvent(event, ctx, state);
|
|
1391
|
+
}
|
|
1392
|
+
const parsed = finishClaudeStream(state);
|
|
776
1393
|
out.push(...parsed.spans);
|
|
777
1394
|
step = parsed.nextStep;
|
|
778
1395
|
}
|
|
@@ -780,21 +1397,11 @@ var ClaudeAdapter = class {
|
|
|
780
1397
|
};
|
|
781
1398
|
|
|
782
1399
|
// src/adapters/codex.ts
|
|
783
|
-
import { readdir as
|
|
1400
|
+
import { readdir as readdir4, stat as stat3 } from "fs/promises";
|
|
784
1401
|
import { homedir as homedir3 } from "os";
|
|
785
1402
|
import { basename as basename3, join as join4 } from "path";
|
|
786
1403
|
var SERVICE3 = "codex";
|
|
787
|
-
|
|
788
|
-
const out = [];
|
|
789
|
-
for (const line of raw.split("\n")) {
|
|
790
|
-
if (!line) continue;
|
|
791
|
-
try {
|
|
792
|
-
out.push(JSON.parse(line));
|
|
793
|
-
} catch {
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
return out;
|
|
797
|
-
}
|
|
1404
|
+
var SESSION_HEAD_LINES = 40;
|
|
798
1405
|
function contentToString(content) {
|
|
799
1406
|
if (typeof content === "string") return content;
|
|
800
1407
|
if (Array.isArray(content)) {
|
|
@@ -821,6 +1428,28 @@ function toolInputToString(input) {
|
|
|
821
1428
|
if (input == null) return void 0;
|
|
822
1429
|
return JSON.stringify(input);
|
|
823
1430
|
}
|
|
1431
|
+
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;
|
|
1432
|
+
function sessionIds(value) {
|
|
1433
|
+
return [...new Set(contentToString(value).match(CODEX_SESSION_ID) ?? [])];
|
|
1434
|
+
}
|
|
1435
|
+
function spawnedSessionIds(output) {
|
|
1436
|
+
const text = contentToString(output);
|
|
1437
|
+
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));
|
|
1438
|
+
return [...new Set(ids)];
|
|
1439
|
+
}
|
|
1440
|
+
function multiAgentOperation(name) {
|
|
1441
|
+
const prefix = "multi_agent_v1__";
|
|
1442
|
+
return name.startsWith(prefix) ? name.slice(prefix.length) : null;
|
|
1443
|
+
}
|
|
1444
|
+
function setAgentSessionIds(toolSpan, ids) {
|
|
1445
|
+
if (ids.length === 0) return;
|
|
1446
|
+
const unique = [...new Set(ids)];
|
|
1447
|
+
toolSpan.attributes["traces.codex.agent_session_ids"] = JSON.stringify(unique);
|
|
1448
|
+
toolSpan.attributes["traces.codex.agent_session_count"] = unique.length;
|
|
1449
|
+
if (toolSpan.attributes["traces.codex.agent_operation"] === "spawn_agent") {
|
|
1450
|
+
toolSpan.attributes["traces.child_session_ids"] = JSON.stringify(unique);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
824
1453
|
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;
|
|
825
1454
|
function hasReadOnlyCurl(input) {
|
|
826
1455
|
if (!/\bcurl\b/i.test(input)) return false;
|
|
@@ -840,33 +1469,37 @@ function isExpectedBlockingTool(name, input) {
|
|
|
840
1469
|
async function* walkRollouts(root) {
|
|
841
1470
|
let years;
|
|
842
1471
|
try {
|
|
843
|
-
years = await
|
|
844
|
-
} catch {
|
|
845
|
-
return;
|
|
1472
|
+
years = await readdir4(root);
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
if (isMissingPathError(error)) return;
|
|
1475
|
+
throw error;
|
|
846
1476
|
}
|
|
847
1477
|
for (const y of years) {
|
|
848
1478
|
const yp = join4(root, y);
|
|
849
1479
|
let months;
|
|
850
1480
|
try {
|
|
851
|
-
months = await
|
|
852
|
-
} catch {
|
|
853
|
-
continue;
|
|
1481
|
+
months = await readdir4(yp);
|
|
1482
|
+
} catch (error) {
|
|
1483
|
+
if (isMissingPathError(error)) continue;
|
|
1484
|
+
throw error;
|
|
854
1485
|
}
|
|
855
1486
|
for (const m of months) {
|
|
856
1487
|
const mp = join4(yp, m);
|
|
857
1488
|
let days;
|
|
858
1489
|
try {
|
|
859
|
-
days = await
|
|
860
|
-
} catch {
|
|
861
|
-
continue;
|
|
1490
|
+
days = await readdir4(mp);
|
|
1491
|
+
} catch (error) {
|
|
1492
|
+
if (isMissingPathError(error)) continue;
|
|
1493
|
+
throw error;
|
|
862
1494
|
}
|
|
863
1495
|
for (const d of days) {
|
|
864
1496
|
const dp = join4(mp, d);
|
|
865
1497
|
let files;
|
|
866
1498
|
try {
|
|
867
|
-
files = await
|
|
868
|
-
} catch {
|
|
869
|
-
continue;
|
|
1499
|
+
files = await readdir4(dp);
|
|
1500
|
+
} catch (error) {
|
|
1501
|
+
if (isMissingPathError(error)) continue;
|
|
1502
|
+
throw error;
|
|
870
1503
|
}
|
|
871
1504
|
for (const f of files) {
|
|
872
1505
|
if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join4(dp, f);
|
|
@@ -887,75 +1520,106 @@ var CodexAdapter = class {
|
|
|
887
1520
|
let st;
|
|
888
1521
|
try {
|
|
889
1522
|
st = await stat3(path);
|
|
890
|
-
} catch {
|
|
891
|
-
continue;
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
if (isMissingPathError(error)) continue;
|
|
1525
|
+
throw error;
|
|
892
1526
|
}
|
|
893
1527
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
894
|
-
const head = (await readFile4(path, "utf8").catch(() => "")).split("\n", 40);
|
|
895
1528
|
let cwd = null;
|
|
896
1529
|
let id = basename3(path).replace(/^rollout-[\dT-]+-/, "").replace(/\.jsonl$/, "");
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
if (!cwd && l.payload?.cwd) cwd = l.payload.cwd;
|
|
903
|
-
} catch {
|
|
904
|
-
}
|
|
1530
|
+
const ref = { harness: this.harness, sessionId: id, path, cwd, mtimeMs: st.mtimeMs };
|
|
1531
|
+
const head = await takeJsonl(path, SESSION_HEAD_LINES, sessionJsonlOptions(ref));
|
|
1532
|
+
for (const parsed of head) {
|
|
1533
|
+
if (parsed.type === "session_meta" && parsed.payload?.id) id = parsed.payload.id;
|
|
1534
|
+
if (!cwd && parsed.payload?.cwd) cwd = parsed.payload.cwd;
|
|
905
1535
|
if (cwd) break;
|
|
906
1536
|
}
|
|
907
|
-
|
|
908
|
-
|
|
1537
|
+
ref.sessionId = id;
|
|
1538
|
+
ref.cwd = cwd;
|
|
1539
|
+
if (ref.integrity) {
|
|
1540
|
+
ref.integrity.corruptions = ref.integrity.corruptions.map((receipt) => ({ ...receipt, sessionId: id }));
|
|
1541
|
+
}
|
|
1542
|
+
if (opts.cwd && cwd && !cwd.startsWith(opts.cwd)) continue;
|
|
1543
|
+
if (opts.cwd && !cwd && !ref.integrity) continue;
|
|
1544
|
+
refs.push(ref);
|
|
909
1545
|
}
|
|
910
1546
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
911
1547
|
}
|
|
912
|
-
async parse(ref) {
|
|
913
|
-
const
|
|
914
|
-
const
|
|
1548
|
+
async parse(ref, options = {}) {
|
|
1549
|
+
const jsonl2 = sessionJsonlOptions(ref, options);
|
|
1550
|
+
const head = await takeJsonl(ref.path, SESSION_HEAD_LINES, jsonl2);
|
|
1551
|
+
let first = head[0];
|
|
1552
|
+
let meta = head.find((line) => line.type === "session_meta");
|
|
1553
|
+
let model = head.find((line) => line.type === "turn_context")?.payload?.model ?? null;
|
|
1554
|
+
if (!first || !meta) {
|
|
1555
|
+
for await (const line of readJsonl(ref.path, jsonl2)) {
|
|
1556
|
+
first ??= line;
|
|
1557
|
+
if (!meta && line.type === "session_meta") meta = line;
|
|
1558
|
+
if (!model && line.type === "turn_context") model = line.payload?.model ?? null;
|
|
1559
|
+
if (first && meta) break;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
915
1562
|
const traceId = meta?.payload?.id ?? ref.sessionId;
|
|
916
|
-
const
|
|
1563
|
+
const spawnMeta = meta?.payload?.source?.subagent?.thread_spawn;
|
|
1564
|
+
const parentSessionId = meta?.payload?.parent_thread_id ?? spawnMeta?.parent_thread_id;
|
|
1565
|
+
const sessionRole = parentSessionId || meta?.payload?.thread_source === "subagent" ? "child" : "operator";
|
|
917
1566
|
const rootId = `root:${traceId}`;
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1567
|
+
const root = span({
|
|
1568
|
+
traceId,
|
|
1569
|
+
spanId: rootId,
|
|
1570
|
+
parentSpanId: null,
|
|
1571
|
+
name: "session",
|
|
1572
|
+
kind: "AGENT",
|
|
1573
|
+
startTime: meta?.timestamp ?? first?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1574
|
+
service: SERVICE3,
|
|
1575
|
+
agent: SERVICE3,
|
|
1576
|
+
model,
|
|
1577
|
+
extra: {
|
|
1578
|
+
"traces.session.role": sessionRole,
|
|
1579
|
+
...parentSessionId ? { "traces.parent_session_id": parentSessionId } : {},
|
|
1580
|
+
...spawnMeta?.depth != null ? { "traces.codex.agent_depth": spawnMeta.depth } : {},
|
|
1581
|
+
...meta?.payload?.agent_nickname ?? spawnMeta?.agent_nickname ? { "traces.codex.agent_nickname": meta?.payload?.agent_nickname ?? spawnMeta?.agent_nickname } : {},
|
|
1582
|
+
...meta?.payload?.agent_role ?? spawnMeta?.agent_role ? { "traces.codex.agent_role": meta?.payload?.agent_role ?? spawnMeta?.agent_role } : {},
|
|
1583
|
+
...spawnMeta?.agent_path ? { "traces.codex.agent_path": spawnMeta.agent_path } : {}
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
const spans = [root];
|
|
932
1587
|
const toolByCallId = /* @__PURE__ */ new Map();
|
|
933
1588
|
const subagentByThreadId = /* @__PURE__ */ new Map();
|
|
934
1589
|
let step = 0;
|
|
935
1590
|
let lastLlm = rootId;
|
|
936
1591
|
let sawUserTurn = false;
|
|
937
|
-
|
|
1592
|
+
let lastTimestamp;
|
|
1593
|
+
const awaitingModel = model ? [] : [root];
|
|
1594
|
+
for await (const l of readJsonl(ref.path, jsonl2)) {
|
|
1595
|
+
lastTimestamp = l.timestamp;
|
|
938
1596
|
const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
939
|
-
if (l.type === "
|
|
1597
|
+
if (!model && l.type === "turn_context" && l.payload?.model) {
|
|
1598
|
+
model = l.payload.model;
|
|
1599
|
+
for (const pending of awaitingModel) pending.attributes["llm.model_name"] = model;
|
|
1600
|
+
awaitingModel.length = 0;
|
|
1601
|
+
} else if (l.type === "event_msg" && l.payload?.type === "token_count") {
|
|
940
1602
|
const u = l.payload.info?.last_token_usage;
|
|
941
1603
|
if (u && (u.input_tokens || u.output_tokens)) {
|
|
942
1604
|
const id = `llm:${step}`;
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
);
|
|
1605
|
+
const llm = span({
|
|
1606
|
+
traceId,
|
|
1607
|
+
spanId: id,
|
|
1608
|
+
parentSpanId: rootId,
|
|
1609
|
+
name: "llm.turn",
|
|
1610
|
+
kind: "LLM",
|
|
1611
|
+
startTime: ts,
|
|
1612
|
+
service: SERVICE3,
|
|
1613
|
+
agent: SERVICE3,
|
|
1614
|
+
model,
|
|
1615
|
+
inputTokens: u.input_tokens ?? null,
|
|
1616
|
+
outputTokens: u.output_tokens ?? null,
|
|
1617
|
+
reasoningTokens: u.reasoning_output_tokens ?? null,
|
|
1618
|
+
cachedInputTokens: u.cached_input_tokens ?? null,
|
|
1619
|
+
step
|
|
1620
|
+
});
|
|
1621
|
+
spans.push(llm);
|
|
1622
|
+
if (!model) awaitingModel.push(llm);
|
|
959
1623
|
lastLlm = id;
|
|
960
1624
|
step += 1;
|
|
961
1625
|
}
|
|
@@ -967,6 +1631,7 @@ var CodexAdapter = class {
|
|
|
967
1631
|
);
|
|
968
1632
|
const nestedName = l.payload.type === "custom_tool_call" ? singleNestedToolName(input) : null;
|
|
969
1633
|
const name = classifyNestedTool(nestedName ?? outerName, input);
|
|
1634
|
+
const agentOperation = multiAgentOperation(nestedName ?? outerName);
|
|
970
1635
|
const toolSpan = span({
|
|
971
1636
|
traceId,
|
|
972
1637
|
spanId: `tool:${callId}`,
|
|
@@ -978,14 +1643,16 @@ var CodexAdapter = class {
|
|
|
978
1643
|
agent: SERVICE3,
|
|
979
1644
|
tool: name,
|
|
980
1645
|
step,
|
|
981
|
-
content: input ?? null,
|
|
982
1646
|
extra: {
|
|
1647
|
+
...toolIoAttributes({ input }),
|
|
983
1648
|
"traces.codex.call_type": l.payload.type,
|
|
984
1649
|
...name !== outerName ? { "traces.codex.outer_tool_name": outerName } : {},
|
|
985
1650
|
...nestedName ? { "traces.codex.nested_tool_name": nestedName } : {},
|
|
986
|
-
...isExpectedBlockingTool(name, input) ? { "traces.expected_blocking": true } : {}
|
|
1651
|
+
...isExpectedBlockingTool(name, input) ? { "traces.expected_blocking": true } : {},
|
|
1652
|
+
...agentOperation ? { "traces.codex.agent_operation": agentOperation } : {}
|
|
987
1653
|
}
|
|
988
1654
|
});
|
|
1655
|
+
if (agentOperation && agentOperation !== "spawn_agent") setAgentSessionIds(toolSpan, sessionIds(input));
|
|
989
1656
|
spans.push(toolSpan);
|
|
990
1657
|
toolByCallId.set(callId, toolSpan);
|
|
991
1658
|
step += 1;
|
|
@@ -995,6 +1662,10 @@ var CodexAdapter = class {
|
|
|
995
1662
|
const { error, message } = outputIsError(l.payload.output);
|
|
996
1663
|
t.end_time = ts;
|
|
997
1664
|
t.status = error ? { code: "ERROR", message } : { code: "OK" };
|
|
1665
|
+
recordToolOutput(t, l.payload.output);
|
|
1666
|
+
if (t.attributes["traces.codex.agent_operation"] === "spawn_agent") {
|
|
1667
|
+
setAgentSessionIds(t, spawnedSessionIds(l.payload.output));
|
|
1668
|
+
}
|
|
998
1669
|
}
|
|
999
1670
|
} else if (l.type === "event_msg" && l.payload?.type === "sub_agent_activity") {
|
|
1000
1671
|
const threadId = l.payload.agent_thread_id;
|
|
@@ -1014,12 +1685,14 @@ var CodexAdapter = class {
|
|
|
1014
1685
|
agent: SERVICE3,
|
|
1015
1686
|
tool: "Agent",
|
|
1016
1687
|
step,
|
|
1017
|
-
content: JSON.stringify({
|
|
1018
|
-
subagent_type: subagentType,
|
|
1019
|
-
agent_path: agentPath,
|
|
1020
|
-
agent_thread_id: threadId
|
|
1021
|
-
}),
|
|
1022
1688
|
extra: {
|
|
1689
|
+
...toolIoAttributes({
|
|
1690
|
+
input: {
|
|
1691
|
+
subagent_type: subagentType,
|
|
1692
|
+
agent_path: agentPath,
|
|
1693
|
+
agent_thread_id: threadId
|
|
1694
|
+
}
|
|
1695
|
+
}),
|
|
1023
1696
|
"traces.codex.subagent_path": agentPath,
|
|
1024
1697
|
"traces.codex.subagent_thread_id": threadId
|
|
1025
1698
|
}
|
|
@@ -1081,26 +1754,16 @@ var CodexAdapter = class {
|
|
|
1081
1754
|
}
|
|
1082
1755
|
}
|
|
1083
1756
|
}
|
|
1757
|
+
root.end_time = lastTimestamp ?? root.start_time;
|
|
1084
1758
|
return spans;
|
|
1085
1759
|
}
|
|
1086
1760
|
};
|
|
1087
1761
|
|
|
1088
1762
|
// src/adapters/copilot.ts
|
|
1089
|
-
import { readdir as
|
|
1763
|
+
import { readdir as readdir5, stat as stat4 } from "fs/promises";
|
|
1090
1764
|
import { homedir as homedir4 } from "os";
|
|
1091
1765
|
import { join as join5 } from "path";
|
|
1092
1766
|
var SERVICE4 = "github-copilot";
|
|
1093
|
-
function parseLines3(raw) {
|
|
1094
|
-
const out = [];
|
|
1095
|
-
for (const line of raw.split("\n")) {
|
|
1096
|
-
if (!line.trim()) continue;
|
|
1097
|
-
try {
|
|
1098
|
-
out.push(JSON.parse(line));
|
|
1099
|
-
} catch {
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
return out;
|
|
1103
|
-
}
|
|
1104
1767
|
var CopilotAdapter = class {
|
|
1105
1768
|
harness = "github-copilot";
|
|
1106
1769
|
aliases = ["copilot"];
|
|
@@ -1110,9 +1773,10 @@ var CopilotAdapter = class {
|
|
|
1110
1773
|
async locate(opts = {}) {
|
|
1111
1774
|
let dirs;
|
|
1112
1775
|
try {
|
|
1113
|
-
dirs = await
|
|
1114
|
-
} catch {
|
|
1115
|
-
return [];
|
|
1776
|
+
dirs = await readdir5(this.root());
|
|
1777
|
+
} catch (error) {
|
|
1778
|
+
if (isMissingPathError(error)) return [];
|
|
1779
|
+
throw error;
|
|
1116
1780
|
}
|
|
1117
1781
|
const refs = [];
|
|
1118
1782
|
for (const id of dirs) {
|
|
@@ -1120,8 +1784,9 @@ var CopilotAdapter = class {
|
|
|
1120
1784
|
let st;
|
|
1121
1785
|
try {
|
|
1122
1786
|
st = await stat4(path);
|
|
1123
|
-
} catch {
|
|
1124
|
-
continue;
|
|
1787
|
+
} catch (error) {
|
|
1788
|
+
if (isMissingPathError(error)) continue;
|
|
1789
|
+
throw error;
|
|
1125
1790
|
}
|
|
1126
1791
|
if (!st.isFile()) continue;
|
|
1127
1792
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
@@ -1130,28 +1795,23 @@ var CopilotAdapter = class {
|
|
|
1130
1795
|
}
|
|
1131
1796
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1132
1797
|
}
|
|
1133
|
-
async parse(ref) {
|
|
1134
|
-
const events = parseLines3(await readFile5(ref.path, "utf8"));
|
|
1798
|
+
async parse(ref, options = {}) {
|
|
1135
1799
|
const traceId = ref.sessionId;
|
|
1136
1800
|
const rootId = `root:${traceId}`;
|
|
1137
|
-
const spans = [
|
|
1138
|
-
span({
|
|
1139
|
-
traceId,
|
|
1140
|
-
spanId: rootId,
|
|
1141
|
-
parentSpanId: null,
|
|
1142
|
-
name: "session",
|
|
1143
|
-
kind: "AGENT",
|
|
1144
|
-
startTime: events[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1145
|
-
endTime: events.at(-1)?.timestamp,
|
|
1146
|
-
service: SERVICE4,
|
|
1147
|
-
agent: SERVICE4
|
|
1148
|
-
})
|
|
1149
|
-
];
|
|
1801
|
+
const spans = [];
|
|
1150
1802
|
const toolByCallId = /* @__PURE__ */ new Map();
|
|
1803
|
+
let firstTimestamp;
|
|
1804
|
+
let lastTimestamp;
|
|
1805
|
+
let sawEvent = false;
|
|
1151
1806
|
let step = 0;
|
|
1152
1807
|
let lastLlm = rootId;
|
|
1153
1808
|
let pendingInputTokens = null;
|
|
1154
|
-
for (const ev of
|
|
1809
|
+
for await (const ev of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
|
|
1810
|
+
if (!sawEvent) {
|
|
1811
|
+
firstTimestamp = ev.timestamp;
|
|
1812
|
+
sawEvent = true;
|
|
1813
|
+
}
|
|
1814
|
+
lastTimestamp = ev.timestamp;
|
|
1155
1815
|
const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1156
1816
|
const d = ev.data ?? {};
|
|
1157
1817
|
if (ev.type === "assistant.usage") {
|
|
@@ -1193,7 +1853,7 @@ var CopilotAdapter = class {
|
|
|
1193
1853
|
agent: SERVICE4,
|
|
1194
1854
|
tool: name,
|
|
1195
1855
|
step,
|
|
1196
|
-
|
|
1856
|
+
extra: toolIoAttributes({ input: d.arguments })
|
|
1197
1857
|
});
|
|
1198
1858
|
spans.push(t);
|
|
1199
1859
|
toolByCallId.set(d.toolCallId, t);
|
|
@@ -1204,15 +1864,27 @@ var CopilotAdapter = class {
|
|
|
1204
1864
|
t.end_time = ts;
|
|
1205
1865
|
const err = d.success === false;
|
|
1206
1866
|
t.status = err ? { code: "ERROR", message: (d.error?.message ?? "").slice(0, 500) } : { code: "OK" };
|
|
1867
|
+
recordToolOutput(t, d.output ?? d.result ?? d.error?.message);
|
|
1207
1868
|
}
|
|
1208
1869
|
}
|
|
1209
1870
|
}
|
|
1210
|
-
|
|
1871
|
+
const root = span({
|
|
1872
|
+
traceId,
|
|
1873
|
+
spanId: rootId,
|
|
1874
|
+
parentSpanId: null,
|
|
1875
|
+
name: "session",
|
|
1876
|
+
kind: "AGENT",
|
|
1877
|
+
startTime: firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1878
|
+
endTime: lastTimestamp,
|
|
1879
|
+
service: SERVICE4,
|
|
1880
|
+
agent: SERVICE4
|
|
1881
|
+
});
|
|
1882
|
+
return [root, ...spans];
|
|
1211
1883
|
}
|
|
1212
1884
|
};
|
|
1213
1885
|
|
|
1214
1886
|
// src/adapters/factory.ts
|
|
1215
|
-
import { readdir as
|
|
1887
|
+
import { readdir as readdir6, stat as stat5 } from "fs/promises";
|
|
1216
1888
|
import { homedir as homedir5 } from "os";
|
|
1217
1889
|
import { basename as basename4, join as join6 } from "path";
|
|
1218
1890
|
var SERVICE5 = "factory";
|
|
@@ -1221,17 +1893,6 @@ function textOf4(content) {
|
|
|
1221
1893
|
(content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("")
|
|
1222
1894
|
);
|
|
1223
1895
|
}
|
|
1224
|
-
function parseLines4(raw) {
|
|
1225
|
-
const out = [];
|
|
1226
|
-
for (const line of raw.split("\n")) {
|
|
1227
|
-
if (!line) continue;
|
|
1228
|
-
try {
|
|
1229
|
-
out.push(JSON.parse(line));
|
|
1230
|
-
} catch {
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
return out;
|
|
1234
|
-
}
|
|
1235
1896
|
var FactoryAdapter = class {
|
|
1236
1897
|
harness = "factory";
|
|
1237
1898
|
aliases = ["factory-droids", "droid"];
|
|
@@ -1242,18 +1903,20 @@ var FactoryAdapter = class {
|
|
|
1242
1903
|
const root = this.root();
|
|
1243
1904
|
let dirs;
|
|
1244
1905
|
try {
|
|
1245
|
-
dirs = await
|
|
1246
|
-
} catch {
|
|
1247
|
-
return [];
|
|
1906
|
+
dirs = await readdir6(root);
|
|
1907
|
+
} catch (error) {
|
|
1908
|
+
if (isMissingPathError(error)) return [];
|
|
1909
|
+
throw error;
|
|
1248
1910
|
}
|
|
1249
1911
|
const refs = [];
|
|
1250
1912
|
for (const dir of dirs) {
|
|
1251
1913
|
const dp = join6(root, dir);
|
|
1252
1914
|
let files;
|
|
1253
1915
|
try {
|
|
1254
|
-
files = await
|
|
1255
|
-
} catch {
|
|
1256
|
-
continue;
|
|
1916
|
+
files = await readdir6(dp);
|
|
1917
|
+
} catch (error) {
|
|
1918
|
+
if (isMissingPathError(error)) continue;
|
|
1919
|
+
throw error;
|
|
1257
1920
|
}
|
|
1258
1921
|
const cwd = dir.replace(/-/g, "/");
|
|
1259
1922
|
if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
|
|
@@ -1263,8 +1926,9 @@ var FactoryAdapter = class {
|
|
|
1263
1926
|
let st;
|
|
1264
1927
|
try {
|
|
1265
1928
|
st = await stat5(path);
|
|
1266
|
-
} catch {
|
|
1267
|
-
continue;
|
|
1929
|
+
} catch (error) {
|
|
1930
|
+
if (isMissingPathError(error)) continue;
|
|
1931
|
+
throw error;
|
|
1268
1932
|
}
|
|
1269
1933
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
1270
1934
|
refs.push({ harness: this.harness, sessionId: basename4(f, ".jsonl"), path, cwd, mtimeMs: st.mtimeMs });
|
|
@@ -1272,38 +1936,32 @@ var FactoryAdapter = class {
|
|
|
1272
1936
|
}
|
|
1273
1937
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1274
1938
|
}
|
|
1275
|
-
async parse(ref) {
|
|
1276
|
-
const lines = parseLines4(await readFile6(ref.path, "utf8"));
|
|
1277
|
-
const start = lines.find((l) => l.type === "session_start");
|
|
1278
|
-
const traceId = start?.id ?? ref.sessionId;
|
|
1279
|
-
const rootId = `root:${traceId}`;
|
|
1939
|
+
async parse(ref, options = {}) {
|
|
1280
1940
|
let settings = {};
|
|
1281
1941
|
try {
|
|
1282
|
-
settings =
|
|
1283
|
-
} catch {
|
|
1942
|
+
settings = await readJsonFile(ref.path.replace(/\.jsonl$/, ".settings.json"));
|
|
1943
|
+
} catch (error) {
|
|
1944
|
+
if (!isMissingJsonSource(error)) throw error;
|
|
1284
1945
|
}
|
|
1285
|
-
const
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
spanId: rootId,
|
|
1289
|
-
parentSpanId: null,
|
|
1290
|
-
name: "session",
|
|
1291
|
-
kind: "AGENT",
|
|
1292
|
-
startTime: start?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1293
|
-
endTime: lines.at(-1)?.timestamp,
|
|
1294
|
-
service: SERVICE5,
|
|
1295
|
-
agent: SERVICE5,
|
|
1296
|
-
model: settings.model ?? null,
|
|
1297
|
-
extra: settings.tokenUsage ? {
|
|
1298
|
-
"session.input_tokens": settings.tokenUsage.inputTokens ?? 0,
|
|
1299
|
-
"session.output_tokens": settings.tokenUsage.outputTokens ?? 0
|
|
1300
|
-
} : void 0
|
|
1301
|
-
})
|
|
1302
|
-
];
|
|
1946
|
+
const sourceTraceId = ref.sessionId;
|
|
1947
|
+
const sourceRootId = `root:${sourceTraceId}`;
|
|
1948
|
+
const spans = [];
|
|
1303
1949
|
const toolByUseId = /* @__PURE__ */ new Map();
|
|
1950
|
+
let firstTimestamp;
|
|
1951
|
+
let lastTimestamp;
|
|
1952
|
+
let sessionLine;
|
|
1953
|
+
let sawLine = false;
|
|
1304
1954
|
let step = 0;
|
|
1305
|
-
let lastLlm =
|
|
1306
|
-
for (const l of
|
|
1955
|
+
let lastLlm = sourceRootId;
|
|
1956
|
+
for await (const l of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
|
|
1957
|
+
if (!sawLine) {
|
|
1958
|
+
firstTimestamp = l.timestamp;
|
|
1959
|
+
sawLine = true;
|
|
1960
|
+
}
|
|
1961
|
+
lastTimestamp = l.timestamp;
|
|
1962
|
+
if (!sessionLine && l.type === "session_start") {
|
|
1963
|
+
sessionLine = { id: l.id, timestamp: l.timestamp };
|
|
1964
|
+
}
|
|
1307
1965
|
if (l.type !== "message" || !l.message) continue;
|
|
1308
1966
|
const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1309
1967
|
const role = l.message.role;
|
|
@@ -1312,9 +1970,9 @@ var FactoryAdapter = class {
|
|
|
1312
1970
|
if (text) {
|
|
1313
1971
|
spans.push(
|
|
1314
1972
|
userPromptSpan({
|
|
1315
|
-
traceId,
|
|
1973
|
+
traceId: sourceTraceId,
|
|
1316
1974
|
spanId: `user:${l.id ?? step}`,
|
|
1317
|
-
parentSpanId:
|
|
1975
|
+
parentSpanId: sourceRootId,
|
|
1318
1976
|
startTime: ts,
|
|
1319
1977
|
content: text,
|
|
1320
1978
|
service: SERVICE5,
|
|
@@ -1328,9 +1986,9 @@ var FactoryAdapter = class {
|
|
|
1328
1986
|
const llmId = `llm:${l.id ?? step}`;
|
|
1329
1987
|
spans.push(
|
|
1330
1988
|
span({
|
|
1331
|
-
traceId,
|
|
1989
|
+
traceId: sourceTraceId,
|
|
1332
1990
|
spanId: llmId,
|
|
1333
|
-
parentSpanId:
|
|
1991
|
+
parentSpanId: sourceRootId,
|
|
1334
1992
|
name: `message.${role ?? "unknown"}`,
|
|
1335
1993
|
kind: "LLM",
|
|
1336
1994
|
startTime: ts,
|
|
@@ -1347,7 +2005,7 @@ var FactoryAdapter = class {
|
|
|
1347
2005
|
for (const b of l.message.content ?? []) {
|
|
1348
2006
|
if (b.type === "tool_use" && b.name) {
|
|
1349
2007
|
const t = span({
|
|
1350
|
-
traceId,
|
|
2008
|
+
traceId: sourceTraceId,
|
|
1351
2009
|
spanId: `tool:${b.id ?? `${l.id}:${step}`}`,
|
|
1352
2010
|
parentSpanId: lastLlm,
|
|
1353
2011
|
name: `tool.${b.name}`,
|
|
@@ -1357,23 +2015,48 @@ var FactoryAdapter = class {
|
|
|
1357
2015
|
agent: SERVICE5,
|
|
1358
2016
|
tool: b.name,
|
|
1359
2017
|
step,
|
|
1360
|
-
|
|
2018
|
+
extra: toolIoAttributes({ input: b.input })
|
|
1361
2019
|
});
|
|
1362
2020
|
spans.push(t);
|
|
1363
2021
|
if (b.id) toolByUseId.set(b.id, t);
|
|
1364
2022
|
step += 1;
|
|
1365
2023
|
} else if (b.type === "tool_result" && b.tool_use_id) {
|
|
1366
2024
|
const t = toolByUseId.get(b.tool_use_id);
|
|
1367
|
-
if (t)
|
|
2025
|
+
if (t) {
|
|
2026
|
+
t.status = b.is_error === true ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
|
|
2027
|
+
recordToolOutput(t, b.content);
|
|
2028
|
+
}
|
|
1368
2029
|
}
|
|
1369
2030
|
}
|
|
1370
2031
|
}
|
|
1371
|
-
|
|
2032
|
+
const traceId = sessionLine?.id ?? sourceTraceId;
|
|
2033
|
+
const rootId = `root:${traceId}`;
|
|
2034
|
+
for (const item of spans) {
|
|
2035
|
+
item.trace_id = traceId;
|
|
2036
|
+
if (item.parent_span_id === sourceRootId) item.parent_span_id = rootId;
|
|
2037
|
+
}
|
|
2038
|
+
const root = span({
|
|
2039
|
+
traceId,
|
|
2040
|
+
spanId: rootId,
|
|
2041
|
+
parentSpanId: null,
|
|
2042
|
+
name: "session",
|
|
2043
|
+
kind: "AGENT",
|
|
2044
|
+
startTime: sessionLine?.timestamp ?? firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2045
|
+
endTime: lastTimestamp,
|
|
2046
|
+
service: SERVICE5,
|
|
2047
|
+
agent: SERVICE5,
|
|
2048
|
+
model: settings.model ?? null,
|
|
2049
|
+
extra: settings.tokenUsage ? {
|
|
2050
|
+
"session.input_tokens": settings.tokenUsage.inputTokens ?? 0,
|
|
2051
|
+
"session.output_tokens": settings.tokenUsage.outputTokens ?? 0
|
|
2052
|
+
} : void 0
|
|
2053
|
+
});
|
|
2054
|
+
return [root, ...spans];
|
|
1372
2055
|
}
|
|
1373
2056
|
};
|
|
1374
2057
|
|
|
1375
2058
|
// src/adapters/forge.ts
|
|
1376
|
-
import { readdir as
|
|
2059
|
+
import { readdir as readdir7, stat as stat6 } from "fs/promises";
|
|
1377
2060
|
import { homedir as homedir6 } from "os";
|
|
1378
2061
|
import { basename as basename5, join as join7 } from "path";
|
|
1379
2062
|
var SERVICE6 = "forge";
|
|
@@ -1397,9 +2080,10 @@ var ForgeAdapter = class {
|
|
|
1397
2080
|
for (const dir of dirs) {
|
|
1398
2081
|
let files;
|
|
1399
2082
|
try {
|
|
1400
|
-
files = await
|
|
1401
|
-
} catch {
|
|
1402
|
-
continue;
|
|
2083
|
+
files = await readdir7(dir);
|
|
2084
|
+
} catch (error) {
|
|
2085
|
+
if (isMissingPathError(error)) continue;
|
|
2086
|
+
throw error;
|
|
1403
2087
|
}
|
|
1404
2088
|
for (const f of files) {
|
|
1405
2089
|
if (!f.endsWith("-dump.json")) continue;
|
|
@@ -1407,8 +2091,9 @@ var ForgeAdapter = class {
|
|
|
1407
2091
|
let st;
|
|
1408
2092
|
try {
|
|
1409
2093
|
st = await stat6(path);
|
|
1410
|
-
} catch {
|
|
1411
|
-
continue;
|
|
2094
|
+
} catch (error) {
|
|
2095
|
+
if (isMissingPathError(error)) continue;
|
|
2096
|
+
throw error;
|
|
1412
2097
|
}
|
|
1413
2098
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
1414
2099
|
refs.push({ harness: this.harness, sessionId: basename5(f, ".json"), path, cwd: dir, mtimeMs: st.mtimeMs });
|
|
@@ -1417,12 +2102,7 @@ var ForgeAdapter = class {
|
|
|
1417
2102
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1418
2103
|
}
|
|
1419
2104
|
async parse(ref) {
|
|
1420
|
-
|
|
1421
|
-
try {
|
|
1422
|
-
dump = JSON.parse(await readFile7(ref.path, "utf8"));
|
|
1423
|
-
} catch {
|
|
1424
|
-
return [];
|
|
1425
|
-
}
|
|
2105
|
+
const dump = await readJsonFile(ref.path);
|
|
1426
2106
|
const ctx = dump.context ?? dump;
|
|
1427
2107
|
const traceId = ctx.conversation_id ?? ref.sessionId;
|
|
1428
2108
|
const rootId = `root:${traceId}`;
|
|
@@ -1487,7 +2167,7 @@ var ForgeAdapter = class {
|
|
|
1487
2167
|
agent: SERVICE6,
|
|
1488
2168
|
tool: tc.name ?? "tool",
|
|
1489
2169
|
step,
|
|
1490
|
-
|
|
2170
|
+
extra: toolIoAttributes({ input: tc.arguments })
|
|
1491
2171
|
});
|
|
1492
2172
|
spans.push(t);
|
|
1493
2173
|
toolByCallId.set(id, t);
|
|
@@ -1495,7 +2175,10 @@ var ForgeAdapter = class {
|
|
|
1495
2175
|
}
|
|
1496
2176
|
} else if (entry.tool) {
|
|
1497
2177
|
const t = toolByCallId.get(entry.tool.call_id ?? "");
|
|
1498
|
-
if (t)
|
|
2178
|
+
if (t) {
|
|
2179
|
+
t.status = entry.tool.output?.is_error ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
|
|
2180
|
+
recordToolOutput(t, entry.tool.output?.values ?? entry.tool.output);
|
|
2181
|
+
}
|
|
1499
2182
|
}
|
|
1500
2183
|
}
|
|
1501
2184
|
return spans;
|
|
@@ -1503,7 +2186,7 @@ var ForgeAdapter = class {
|
|
|
1503
2186
|
};
|
|
1504
2187
|
|
|
1505
2188
|
// src/adapters/gemini.ts
|
|
1506
|
-
import { readdir as
|
|
2189
|
+
import { readdir as readdir8, stat as stat7 } from "fs/promises";
|
|
1507
2190
|
import { homedir as homedir7 } from "os";
|
|
1508
2191
|
import { join as join8 } from "path";
|
|
1509
2192
|
function iso(ts, fallback) {
|
|
@@ -1539,10 +2222,12 @@ var GeminiFamilyAdapter = class {
|
|
|
1539
2222
|
async loadProjectCwds() {
|
|
1540
2223
|
const nameToPath = /* @__PURE__ */ new Map();
|
|
1541
2224
|
try {
|
|
1542
|
-
const
|
|
1543
|
-
|
|
2225
|
+
const projects = (await readJsonFile(
|
|
2226
|
+
join8(homedir7(), this.homeDirName, "projects.json")
|
|
2227
|
+
)).projects ?? {};
|
|
1544
2228
|
for (const [path, name] of Object.entries(projects)) nameToPath.set(name, path);
|
|
1545
|
-
} catch {
|
|
2229
|
+
} catch (error) {
|
|
2230
|
+
if (!isMissingJsonSource(error)) throw error;
|
|
1546
2231
|
}
|
|
1547
2232
|
return nameToPath;
|
|
1548
2233
|
}
|
|
@@ -1550,9 +2235,10 @@ var GeminiFamilyAdapter = class {
|
|
|
1550
2235
|
const root = this.chatsRoot();
|
|
1551
2236
|
let projectDirs;
|
|
1552
2237
|
try {
|
|
1553
|
-
projectDirs = await
|
|
1554
|
-
} catch {
|
|
1555
|
-
return [];
|
|
2238
|
+
projectDirs = await readdir8(root);
|
|
2239
|
+
} catch (error) {
|
|
2240
|
+
if (isMissingPathError(error)) return [];
|
|
2241
|
+
throw error;
|
|
1556
2242
|
}
|
|
1557
2243
|
const projectCwds = await this.loadProjectCwds();
|
|
1558
2244
|
const refs = [];
|
|
@@ -1561,9 +2247,10 @@ var GeminiFamilyAdapter = class {
|
|
|
1561
2247
|
const chatsDir = join8(root, pd, "chats");
|
|
1562
2248
|
let files;
|
|
1563
2249
|
try {
|
|
1564
|
-
files = await
|
|
1565
|
-
} catch {
|
|
1566
|
-
continue;
|
|
2250
|
+
files = await readdir8(chatsDir);
|
|
2251
|
+
} catch (error) {
|
|
2252
|
+
if (isMissingPathError(error)) continue;
|
|
2253
|
+
throw error;
|
|
1567
2254
|
}
|
|
1568
2255
|
for (const f of files) {
|
|
1569
2256
|
if (!f.startsWith("session-") || !f.endsWith(".json")) continue;
|
|
@@ -1571,8 +2258,9 @@ var GeminiFamilyAdapter = class {
|
|
|
1571
2258
|
let st;
|
|
1572
2259
|
try {
|
|
1573
2260
|
st = await stat7(path);
|
|
1574
|
-
} catch {
|
|
1575
|
-
continue;
|
|
2261
|
+
} catch (error) {
|
|
2262
|
+
if (isMissingPathError(error)) continue;
|
|
2263
|
+
throw error;
|
|
1576
2264
|
}
|
|
1577
2265
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
1578
2266
|
if (opts.cwd && (!cwd || !cwd.startsWith(opts.cwd))) continue;
|
|
@@ -1582,12 +2270,7 @@ var GeminiFamilyAdapter = class {
|
|
|
1582
2270
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1583
2271
|
}
|
|
1584
2272
|
async parse(ref) {
|
|
1585
|
-
|
|
1586
|
-
try {
|
|
1587
|
-
session = JSON.parse(await readFile8(ref.path, "utf8"));
|
|
1588
|
-
} catch {
|
|
1589
|
-
return [];
|
|
1590
|
-
}
|
|
2273
|
+
const session = await readJsonFile(ref.path);
|
|
1591
2274
|
const traceId = session.sessionId ?? ref.sessionId;
|
|
1592
2275
|
const messages = session.messages ?? [];
|
|
1593
2276
|
const rootId = `root:${traceId}`;
|
|
@@ -1662,7 +2345,7 @@ var GeminiFamilyAdapter = class {
|
|
|
1662
2345
|
agent: this.service,
|
|
1663
2346
|
tool: tc.name ?? "tool",
|
|
1664
2347
|
step,
|
|
1665
|
-
|
|
2348
|
+
extra: toolIoAttributes({ input: tc.args, output: tc.result })
|
|
1666
2349
|
})
|
|
1667
2350
|
);
|
|
1668
2351
|
step += 1;
|
|
@@ -1678,7 +2361,7 @@ var GeminiAdapter = class extends GeminiFamilyAdapter {
|
|
|
1678
2361
|
};
|
|
1679
2362
|
|
|
1680
2363
|
// src/adapters/opencode.ts
|
|
1681
|
-
import { readdir as
|
|
2364
|
+
import { readdir as readdir9, stat as stat8 } from "fs/promises";
|
|
1682
2365
|
import { homedir as homedir8 } from "os";
|
|
1683
2366
|
import { join as join9 } from "path";
|
|
1684
2367
|
var SERVICE7 = "opencode";
|
|
@@ -1700,9 +2383,10 @@ var OpencodeAdapter = class {
|
|
|
1700
2383
|
const msgRoot = join9(this.storage(), "message");
|
|
1701
2384
|
let sessionDirs;
|
|
1702
2385
|
try {
|
|
1703
|
-
sessionDirs = await
|
|
1704
|
-
} catch {
|
|
1705
|
-
return [];
|
|
2386
|
+
sessionDirs = await readdir9(msgRoot);
|
|
2387
|
+
} catch (error) {
|
|
2388
|
+
if (isMissingPathError(error)) return [];
|
|
2389
|
+
throw error;
|
|
1706
2390
|
}
|
|
1707
2391
|
const refs = [];
|
|
1708
2392
|
for (const sid of sessionDirs) {
|
|
@@ -1710,8 +2394,9 @@ var OpencodeAdapter = class {
|
|
|
1710
2394
|
let st;
|
|
1711
2395
|
try {
|
|
1712
2396
|
st = await stat8(dir);
|
|
1713
|
-
} catch {
|
|
1714
|
-
continue;
|
|
2397
|
+
} catch (error) {
|
|
2398
|
+
if (isMissingPathError(error)) continue;
|
|
2399
|
+
throw error;
|
|
1715
2400
|
}
|
|
1716
2401
|
if (!st.isDirectory()) continue;
|
|
1717
2402
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
@@ -1724,20 +2409,12 @@ var OpencodeAdapter = class {
|
|
|
1724
2409
|
const traceId = ref.sessionId;
|
|
1725
2410
|
const rootId = `root:${traceId}`;
|
|
1726
2411
|
const partRoot = join9(this.storage(), "part");
|
|
1727
|
-
|
|
1728
|
-
try {
|
|
1729
|
-
msgFiles = (await readdir8(ref.path)).filter((f) => f.endsWith(".json"));
|
|
1730
|
-
} catch {
|
|
1731
|
-
return [];
|
|
1732
|
-
}
|
|
2412
|
+
const msgFiles = await listJsonFiles(ref.path);
|
|
1733
2413
|
const messages = [];
|
|
1734
2414
|
for (const f of msgFiles) {
|
|
1735
|
-
|
|
1736
|
-
messages.push({ msg: JSON.parse(await readFile9(join9(ref.path, f), "utf8")), file: f });
|
|
1737
|
-
} catch {
|
|
1738
|
-
}
|
|
2415
|
+
messages.push(await readJsonFile(join9(ref.path, f)));
|
|
1739
2416
|
}
|
|
1740
|
-
messages.sort((a, b) => (a.
|
|
2417
|
+
messages.sort((a, b) => (a.time?.created ?? 0) - (b.time?.created ?? 0));
|
|
1741
2418
|
const spans = [
|
|
1742
2419
|
span({
|
|
1743
2420
|
traceId,
|
|
@@ -1745,59 +2422,65 @@ var OpencodeAdapter = class {
|
|
|
1745
2422
|
parentSpanId: null,
|
|
1746
2423
|
name: "session",
|
|
1747
2424
|
kind: "AGENT",
|
|
1748
|
-
startTime: isoOf(messages[0]?.
|
|
1749
|
-
endTime: isoOf(messages.at(-1)?.
|
|
2425
|
+
startTime: isoOf(messages[0]?.time?.created),
|
|
2426
|
+
endTime: isoOf(messages.at(-1)?.time?.completed ?? messages.at(-1)?.time?.created),
|
|
1750
2427
|
service: SERVICE7,
|
|
1751
2428
|
agent: SERVICE7
|
|
1752
2429
|
})
|
|
1753
2430
|
];
|
|
1754
2431
|
let step = 0;
|
|
1755
|
-
for (const
|
|
2432
|
+
for (const msg of messages) {
|
|
1756
2433
|
const mid = msg.id ?? `m${step}`;
|
|
1757
2434
|
const llmId = `llm:${mid}`;
|
|
1758
2435
|
const pdir = join9(partRoot, mid);
|
|
1759
2436
|
const parts = [];
|
|
2437
|
+
let partFiles = [];
|
|
1760
2438
|
try {
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
}
|
|
1768
|
-
} catch {
|
|
2439
|
+
partFiles = await listJsonFiles(pdir);
|
|
2440
|
+
} catch (error) {
|
|
2441
|
+
if (!isMissingJsonSource(error)) throw error;
|
|
2442
|
+
}
|
|
2443
|
+
for (const pf of partFiles) {
|
|
2444
|
+
parts.push(await readJsonFile(join9(pdir, pf)));
|
|
1769
2445
|
}
|
|
1770
2446
|
const turnText = textOf7(parts);
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
step += 1;
|
|
1790
|
-
if (msg.role === "user" && turnText) {
|
|
2447
|
+
const isUser = msg.role === "user";
|
|
2448
|
+
if (isUser) {
|
|
2449
|
+
if (turnText) {
|
|
2450
|
+
spans.push(
|
|
2451
|
+
userPromptSpan({
|
|
2452
|
+
traceId,
|
|
2453
|
+
spanId: `${llmId}:user`,
|
|
2454
|
+
parentSpanId: rootId,
|
|
2455
|
+
startTime: isoOf(msg.time?.created),
|
|
2456
|
+
service: SERVICE7,
|
|
2457
|
+
agent: SERVICE7,
|
|
2458
|
+
step,
|
|
2459
|
+
content: turnText
|
|
2460
|
+
})
|
|
2461
|
+
);
|
|
2462
|
+
step += 1;
|
|
2463
|
+
}
|
|
2464
|
+
} else {
|
|
1791
2465
|
spans.push(
|
|
1792
|
-
|
|
2466
|
+
span({
|
|
1793
2467
|
traceId,
|
|
1794
|
-
spanId:
|
|
2468
|
+
spanId: llmId,
|
|
1795
2469
|
parentSpanId: rootId,
|
|
2470
|
+
name: `message.${msg.role ?? "unknown"}`,
|
|
2471
|
+
kind: "LLM",
|
|
1796
2472
|
startTime: isoOf(msg.time?.created),
|
|
2473
|
+
endTime: isoOf(msg.time?.completed ?? msg.time?.created),
|
|
1797
2474
|
service: SERVICE7,
|
|
1798
2475
|
agent: SERVICE7,
|
|
2476
|
+
model: msg.modelID ?? null,
|
|
2477
|
+
inputTokens: msg.tokens?.input ?? null,
|
|
2478
|
+
outputTokens: msg.tokens?.output ?? null,
|
|
2479
|
+
reasoningTokens: msg.tokens?.reasoning ?? null,
|
|
2480
|
+
cachedInputTokens: msg.tokens?.cache?.read ?? null,
|
|
2481
|
+
cacheWriteInputTokens: msg.tokens?.cache?.write ?? null,
|
|
1799
2482
|
step,
|
|
1800
|
-
content: turnText
|
|
2483
|
+
content: turnText || null
|
|
1801
2484
|
})
|
|
1802
2485
|
);
|
|
1803
2486
|
step += 1;
|
|
@@ -1809,7 +2492,7 @@ var OpencodeAdapter = class {
|
|
|
1809
2492
|
span({
|
|
1810
2493
|
traceId,
|
|
1811
2494
|
spanId: `tool:${part.callID ?? `${mid}:${step}`}`,
|
|
1812
|
-
parentSpanId: llmId,
|
|
2495
|
+
parentSpanId: isUser ? rootId : llmId,
|
|
1813
2496
|
name: `tool.${part.tool}`,
|
|
1814
2497
|
kind: "TOOL",
|
|
1815
2498
|
startTime: isoOf(msg.time?.created),
|
|
@@ -1820,7 +2503,10 @@ var OpencodeAdapter = class {
|
|
|
1820
2503
|
agent: SERVICE7,
|
|
1821
2504
|
tool: part.tool,
|
|
1822
2505
|
step,
|
|
1823
|
-
|
|
2506
|
+
extra: toolIoAttributes({
|
|
2507
|
+
input: part.state?.input,
|
|
2508
|
+
output: part.state?.output ?? part.state?.error
|
|
2509
|
+
})
|
|
1824
2510
|
})
|
|
1825
2511
|
);
|
|
1826
2512
|
step += 1;
|
|
@@ -1831,21 +2517,10 @@ var OpencodeAdapter = class {
|
|
|
1831
2517
|
};
|
|
1832
2518
|
|
|
1833
2519
|
// src/adapters/pi.ts
|
|
1834
|
-
import { readdir as
|
|
2520
|
+
import { readdir as readdir10, stat as stat9 } from "fs/promises";
|
|
1835
2521
|
import { homedir as homedir9 } from "os";
|
|
1836
2522
|
import { basename as basename6, join as join10 } from "path";
|
|
1837
2523
|
var SERVICE8 = "pi";
|
|
1838
|
-
function parseLines5(raw) {
|
|
1839
|
-
const out = [];
|
|
1840
|
-
for (const line of raw.split("\n")) {
|
|
1841
|
-
if (!line) continue;
|
|
1842
|
-
try {
|
|
1843
|
-
out.push(JSON.parse(line));
|
|
1844
|
-
} catch {
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
return out;
|
|
1848
|
-
}
|
|
1849
2524
|
function isToolBlock(b) {
|
|
1850
2525
|
return typeof b.type === "string" && /tool[_-]?call|tool[_-]?use|^tool$/i.test(b.type);
|
|
1851
2526
|
}
|
|
@@ -1867,18 +2542,20 @@ var PiAdapter = class {
|
|
|
1867
2542
|
const root = this.root();
|
|
1868
2543
|
let dirs;
|
|
1869
2544
|
try {
|
|
1870
|
-
dirs = await
|
|
1871
|
-
} catch {
|
|
1872
|
-
return [];
|
|
2545
|
+
dirs = await readdir10(root);
|
|
2546
|
+
} catch (error) {
|
|
2547
|
+
if (isMissingPathError(error)) return [];
|
|
2548
|
+
throw error;
|
|
1873
2549
|
}
|
|
1874
2550
|
const refs = [];
|
|
1875
2551
|
for (const dir of dirs) {
|
|
1876
2552
|
const dp = join10(root, dir);
|
|
1877
2553
|
let files;
|
|
1878
2554
|
try {
|
|
1879
|
-
files = await
|
|
1880
|
-
} catch {
|
|
1881
|
-
continue;
|
|
2555
|
+
files = await readdir10(dp);
|
|
2556
|
+
} catch (error) {
|
|
2557
|
+
if (isMissingPathError(error)) continue;
|
|
2558
|
+
throw error;
|
|
1882
2559
|
}
|
|
1883
2560
|
const cwd = `/${dir.replace(/^-+/, "").replace(/-+$/, "").replace(/-/g, "/")}`;
|
|
1884
2561
|
if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
|
|
@@ -1888,8 +2565,9 @@ var PiAdapter = class {
|
|
|
1888
2565
|
let st;
|
|
1889
2566
|
try {
|
|
1890
2567
|
st = await stat9(path);
|
|
1891
|
-
} catch {
|
|
1892
|
-
continue;
|
|
2568
|
+
} catch (error) {
|
|
2569
|
+
if (isMissingPathError(error)) continue;
|
|
2570
|
+
throw error;
|
|
1893
2571
|
}
|
|
1894
2572
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
1895
2573
|
const id = basename6(f, ".jsonl").replace(/^[\dTZ.-]+_/, "");
|
|
@@ -1898,27 +2576,25 @@ var PiAdapter = class {
|
|
|
1898
2576
|
}
|
|
1899
2577
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1900
2578
|
}
|
|
1901
|
-
async parse(ref) {
|
|
1902
|
-
const
|
|
1903
|
-
const
|
|
1904
|
-
const
|
|
1905
|
-
const rootId = `root:${traceId}`;
|
|
1906
|
-
const spans = [
|
|
1907
|
-
span({
|
|
1908
|
-
traceId,
|
|
1909
|
-
spanId: rootId,
|
|
1910
|
-
parentSpanId: null,
|
|
1911
|
-
name: "session",
|
|
1912
|
-
kind: "AGENT",
|
|
1913
|
-
startTime: sessionLine?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1914
|
-
endTime: lines.at(-1)?.timestamp,
|
|
1915
|
-
service: SERVICE8,
|
|
1916
|
-
agent: SERVICE8
|
|
1917
|
-
})
|
|
1918
|
-
];
|
|
2579
|
+
async parse(ref, options = {}) {
|
|
2580
|
+
const sourceTraceId = ref.sessionId;
|
|
2581
|
+
const sourceRootId = `root:${sourceTraceId}`;
|
|
2582
|
+
const spans = [];
|
|
1919
2583
|
const toolByCallId = /* @__PURE__ */ new Map();
|
|
2584
|
+
let firstTimestamp;
|
|
2585
|
+
let lastTimestamp;
|
|
2586
|
+
let sessionLine;
|
|
2587
|
+
let sawLine = false;
|
|
1920
2588
|
let step = 0;
|
|
1921
|
-
for (const l of
|
|
2589
|
+
for await (const l of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
|
|
2590
|
+
if (!sawLine) {
|
|
2591
|
+
firstTimestamp = l.timestamp;
|
|
2592
|
+
sawLine = true;
|
|
2593
|
+
}
|
|
2594
|
+
lastTimestamp = l.timestamp;
|
|
2595
|
+
if (!sessionLine && l.type === "session") {
|
|
2596
|
+
sessionLine = { id: l.id, timestamp: l.timestamp };
|
|
2597
|
+
}
|
|
1922
2598
|
if (l.type !== "message" || !l.message) continue;
|
|
1923
2599
|
const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1924
2600
|
const mid = l.id ?? `m${step}`;
|
|
@@ -1929,9 +2605,9 @@ var PiAdapter = class {
|
|
|
1929
2605
|
if (prompt) {
|
|
1930
2606
|
spans.push(
|
|
1931
2607
|
userPromptSpan({
|
|
1932
|
-
traceId,
|
|
2608
|
+
traceId: sourceTraceId,
|
|
1933
2609
|
spanId: `${mid}:user`,
|
|
1934
|
-
parentSpanId:
|
|
2610
|
+
parentSpanId: sourceRootId,
|
|
1935
2611
|
startTime: ts,
|
|
1936
2612
|
service: SERVICE8,
|
|
1937
2613
|
agent: SERVICE8,
|
|
@@ -1945,9 +2621,9 @@ var PiAdapter = class {
|
|
|
1945
2621
|
const errored = !!msg.errorMessage;
|
|
1946
2622
|
spans.push(
|
|
1947
2623
|
span({
|
|
1948
|
-
traceId,
|
|
2624
|
+
traceId: sourceTraceId,
|
|
1949
2625
|
spanId: llmId,
|
|
1950
|
-
parentSpanId:
|
|
2626
|
+
parentSpanId: sourceRootId,
|
|
1951
2627
|
name: `message.${msg.role ?? "unknown"}`,
|
|
1952
2628
|
kind: "LLM",
|
|
1953
2629
|
startTime: ts,
|
|
@@ -1969,7 +2645,7 @@ var PiAdapter = class {
|
|
|
1969
2645
|
const name = b.toolName ?? b.name ?? "tool";
|
|
1970
2646
|
const callId = b.id ?? b.callId ?? b.toolCallId ?? `${mid}:${step}`;
|
|
1971
2647
|
const toolSpan = span({
|
|
1972
|
-
traceId,
|
|
2648
|
+
traceId: sourceTraceId,
|
|
1973
2649
|
spanId: `tool:${callId}`,
|
|
1974
2650
|
parentSpanId: llmId,
|
|
1975
2651
|
name: `tool.${name}`,
|
|
@@ -1979,7 +2655,7 @@ var PiAdapter = class {
|
|
|
1979
2655
|
agent: SERVICE8,
|
|
1980
2656
|
tool: name,
|
|
1981
2657
|
step,
|
|
1982
|
-
|
|
2658
|
+
extra: toolIoAttributes({ input: b.input ?? b.args })
|
|
1983
2659
|
});
|
|
1984
2660
|
spans.push(toolSpan);
|
|
1985
2661
|
toolByCallId.set(callId, toolSpan);
|
|
@@ -1991,30 +2667,37 @@ var PiAdapter = class {
|
|
|
1991
2667
|
const err = b.isError === true || b.is_error === true;
|
|
1992
2668
|
t.end_time = ts;
|
|
1993
2669
|
t.status = err ? { code: "ERROR", message: "tool result reported error" } : { code: "OK" };
|
|
2670
|
+
recordToolOutput(t, b.output ?? b.result ?? b.content ?? b.text);
|
|
1994
2671
|
}
|
|
1995
2672
|
}
|
|
1996
2673
|
}
|
|
1997
2674
|
}
|
|
1998
|
-
|
|
2675
|
+
const traceId = sessionLine?.id ?? sourceTraceId;
|
|
2676
|
+
const rootId = `root:${traceId}`;
|
|
2677
|
+
for (const item of spans) {
|
|
2678
|
+
item.trace_id = traceId;
|
|
2679
|
+
if (item.parent_span_id === sourceRootId) item.parent_span_id = rootId;
|
|
2680
|
+
}
|
|
2681
|
+
const root = span({
|
|
2682
|
+
traceId,
|
|
2683
|
+
spanId: rootId,
|
|
2684
|
+
parentSpanId: null,
|
|
2685
|
+
name: "session",
|
|
2686
|
+
kind: "AGENT",
|
|
2687
|
+
startTime: sessionLine?.timestamp ?? firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2688
|
+
endTime: lastTimestamp,
|
|
2689
|
+
service: SERVICE8,
|
|
2690
|
+
agent: SERVICE8
|
|
2691
|
+
});
|
|
2692
|
+
return [root, ...spans];
|
|
1999
2693
|
}
|
|
2000
2694
|
};
|
|
2001
2695
|
|
|
2002
2696
|
// src/adapters/qwen.ts
|
|
2003
|
-
import { readdir as
|
|
2697
|
+
import { readdir as readdir11, stat as stat10 } from "fs/promises";
|
|
2004
2698
|
import { homedir as homedir10 } from "os";
|
|
2005
2699
|
import { basename as basename7, join as join11 } from "path";
|
|
2006
2700
|
var SERVICE9 = "qwen";
|
|
2007
|
-
function parseLines6(raw) {
|
|
2008
|
-
const out = [];
|
|
2009
|
-
for (const line of raw.split("\n")) {
|
|
2010
|
-
if (!line) continue;
|
|
2011
|
-
try {
|
|
2012
|
-
out.push(JSON.parse(line));
|
|
2013
|
-
} catch {
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
return out;
|
|
2017
|
-
}
|
|
2018
2701
|
function textOf9(parts) {
|
|
2019
2702
|
if (!parts) return "";
|
|
2020
2703
|
return parts.filter((p) => typeof p.text === "string").map((p) => p.text).join("");
|
|
@@ -2029,18 +2712,20 @@ var QwenAdapter = class {
|
|
|
2029
2712
|
const root = this.root();
|
|
2030
2713
|
let projectDirs;
|
|
2031
2714
|
try {
|
|
2032
|
-
projectDirs = await
|
|
2033
|
-
} catch {
|
|
2034
|
-
return [];
|
|
2715
|
+
projectDirs = await readdir11(root);
|
|
2716
|
+
} catch (error) {
|
|
2717
|
+
if (isMissingPathError(error)) return [];
|
|
2718
|
+
throw error;
|
|
2035
2719
|
}
|
|
2036
2720
|
const refs = [];
|
|
2037
2721
|
for (const pd of projectDirs) {
|
|
2038
2722
|
const chatsDir = join11(root, pd, "chats");
|
|
2039
2723
|
let files;
|
|
2040
2724
|
try {
|
|
2041
|
-
files = await
|
|
2042
|
-
} catch {
|
|
2043
|
-
continue;
|
|
2725
|
+
files = await readdir11(chatsDir);
|
|
2726
|
+
} catch (error) {
|
|
2727
|
+
if (isMissingPathError(error)) continue;
|
|
2728
|
+
throw error;
|
|
2044
2729
|
}
|
|
2045
2730
|
if (opts.cwd) continue;
|
|
2046
2731
|
for (const f of files) {
|
|
@@ -2049,8 +2734,9 @@ var QwenAdapter = class {
|
|
|
2049
2734
|
let st;
|
|
2050
2735
|
try {
|
|
2051
2736
|
st = await stat10(path);
|
|
2052
|
-
} catch {
|
|
2053
|
-
continue;
|
|
2737
|
+
} catch (error) {
|
|
2738
|
+
if (isMissingPathError(error)) continue;
|
|
2739
|
+
throw error;
|
|
2054
2740
|
}
|
|
2055
2741
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
2056
2742
|
refs.push({ harness: this.harness, sessionId: basename7(f, ".jsonl"), path, cwd: null, mtimeMs: st.mtimeMs });
|
|
@@ -2058,36 +2744,32 @@ var QwenAdapter = class {
|
|
|
2058
2744
|
}
|
|
2059
2745
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
2060
2746
|
}
|
|
2061
|
-
async parse(ref) {
|
|
2062
|
-
const
|
|
2063
|
-
const
|
|
2064
|
-
const
|
|
2065
|
-
const spans = [
|
|
2066
|
-
span({
|
|
2067
|
-
traceId,
|
|
2068
|
-
spanId: rootId,
|
|
2069
|
-
parentSpanId: null,
|
|
2070
|
-
name: "session",
|
|
2071
|
-
kind: "AGENT",
|
|
2072
|
-
startTime: records[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2073
|
-
endTime: records.at(-1)?.timestamp,
|
|
2074
|
-
service: SERVICE9,
|
|
2075
|
-
agent: SERVICE9
|
|
2076
|
-
})
|
|
2077
|
-
];
|
|
2747
|
+
async parse(ref, options = {}) {
|
|
2748
|
+
const sourceTraceId = ref.sessionId;
|
|
2749
|
+
const sourceRootId = `root:${sourceTraceId}`;
|
|
2750
|
+
const spans = [];
|
|
2078
2751
|
const openToolsByName = /* @__PURE__ */ new Map();
|
|
2752
|
+
let discoveredTraceId;
|
|
2753
|
+
let firstTimestamp;
|
|
2754
|
+
let lastTimestamp;
|
|
2755
|
+
let sawRecord = false;
|
|
2079
2756
|
let step = 0;
|
|
2080
|
-
|
|
2081
|
-
|
|
2757
|
+
for await (const r of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
|
|
2758
|
+
if (!sawRecord) {
|
|
2759
|
+
firstTimestamp = r.timestamp;
|
|
2760
|
+
sawRecord = true;
|
|
2761
|
+
}
|
|
2762
|
+
lastTimestamp = r.timestamp;
|
|
2763
|
+
if (!discoveredTraceId && r.sessionId) discoveredTraceId = r.sessionId;
|
|
2082
2764
|
const ts = r.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
2083
2765
|
const role = r.message?.role;
|
|
2084
2766
|
if (r.type === "assistant" || role === "model") {
|
|
2085
2767
|
const llmId = `llm:${r.uuid ?? step}`;
|
|
2086
2768
|
spans.push(
|
|
2087
2769
|
span({
|
|
2088
|
-
traceId,
|
|
2770
|
+
traceId: sourceTraceId,
|
|
2089
2771
|
spanId: llmId,
|
|
2090
|
-
parentSpanId:
|
|
2772
|
+
parentSpanId: sourceRootId,
|
|
2091
2773
|
name: "llm.turn",
|
|
2092
2774
|
kind: "LLM",
|
|
2093
2775
|
startTime: ts,
|
|
@@ -2100,13 +2782,12 @@ var QwenAdapter = class {
|
|
|
2100
2782
|
content: capText(textOf9(r.message?.parts)) || null
|
|
2101
2783
|
})
|
|
2102
2784
|
);
|
|
2103
|
-
lastLlm = llmId;
|
|
2104
2785
|
step += 1;
|
|
2105
2786
|
for (const p of r.message?.parts ?? []) {
|
|
2106
2787
|
if (!p.functionCall?.name) continue;
|
|
2107
2788
|
const name = p.functionCall.name;
|
|
2108
2789
|
const t = span({
|
|
2109
|
-
traceId,
|
|
2790
|
+
traceId: sourceTraceId,
|
|
2110
2791
|
spanId: `tool:${name}:${step}`,
|
|
2111
2792
|
parentSpanId: llmId,
|
|
2112
2793
|
name: `tool.${name}`,
|
|
@@ -2116,7 +2797,7 @@ var QwenAdapter = class {
|
|
|
2116
2797
|
agent: SERVICE9,
|
|
2117
2798
|
tool: name,
|
|
2118
2799
|
step,
|
|
2119
|
-
|
|
2800
|
+
extra: toolIoAttributes({ input: p.functionCall.args })
|
|
2120
2801
|
});
|
|
2121
2802
|
spans.push(t);
|
|
2122
2803
|
const q = openToolsByName.get(name) ?? [];
|
|
@@ -2129,9 +2810,9 @@ var QwenAdapter = class {
|
|
|
2129
2810
|
if (prompt) {
|
|
2130
2811
|
spans.push(
|
|
2131
2812
|
userPromptSpan({
|
|
2132
|
-
traceId,
|
|
2813
|
+
traceId: sourceTraceId,
|
|
2133
2814
|
spanId: `user:${r.uuid ?? step}`,
|
|
2134
|
-
parentSpanId:
|
|
2815
|
+
parentSpanId: sourceRootId,
|
|
2135
2816
|
startTime: ts,
|
|
2136
2817
|
service: SERVICE9,
|
|
2137
2818
|
agent: SERVICE9,
|
|
@@ -2150,11 +2831,29 @@ var QwenAdapter = class {
|
|
|
2150
2831
|
if (t) {
|
|
2151
2832
|
t.end_time = ts;
|
|
2152
2833
|
t.status = err ? { code: "ERROR", message: "tool result reported error" } : { code: "OK" };
|
|
2834
|
+
recordToolOutput(t, p.functionResponse?.response ?? r.toolCallResult?.error);
|
|
2153
2835
|
}
|
|
2154
2836
|
}
|
|
2155
2837
|
}
|
|
2156
2838
|
}
|
|
2157
|
-
|
|
2839
|
+
const traceId = discoveredTraceId ?? sourceTraceId;
|
|
2840
|
+
const rootId = `root:${traceId}`;
|
|
2841
|
+
for (const item of spans) {
|
|
2842
|
+
item.trace_id = traceId;
|
|
2843
|
+
if (item.parent_span_id === sourceRootId) item.parent_span_id = rootId;
|
|
2844
|
+
}
|
|
2845
|
+
const root = span({
|
|
2846
|
+
traceId,
|
|
2847
|
+
spanId: rootId,
|
|
2848
|
+
parentSpanId: null,
|
|
2849
|
+
name: "session",
|
|
2850
|
+
kind: "AGENT",
|
|
2851
|
+
startTime: firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2852
|
+
endTime: lastTimestamp,
|
|
2853
|
+
service: SERVICE9,
|
|
2854
|
+
agent: SERVICE9
|
|
2855
|
+
});
|
|
2856
|
+
return [root, ...spans];
|
|
2158
2857
|
}
|
|
2159
2858
|
};
|
|
2160
2859
|
|
|
@@ -2554,14 +3253,25 @@ async function stampSpanWorkdirRepoAttrs(spans) {
|
|
|
2554
3253
|
}
|
|
2555
3254
|
|
|
2556
3255
|
// src/session-source.ts
|
|
2557
|
-
async function parseSession(adapter, ref) {
|
|
2558
|
-
const spans = await adapter.parse(ref);
|
|
3256
|
+
async function parseSession(adapter, ref, options = {}) {
|
|
3257
|
+
const spans = await adapter.parse(ref, options);
|
|
3258
|
+
if (spans.length === 0) throw new EmptySessionError(ref.path);
|
|
3259
|
+
stampSessionIntegrity(ref, spans);
|
|
3260
|
+
stampSessionIdentity(spans, ref.sessionId);
|
|
2559
3261
|
const repo = await resolveSessionRepoAttrs(ref.cwd, spans);
|
|
2560
3262
|
if (repo.cwd) ref.cwd = repo.cwd;
|
|
2561
3263
|
stampRepoAttrs(spans, repo.attrs);
|
|
2562
3264
|
await stampSpanWorkdirRepoAttrs(spans);
|
|
2563
3265
|
return spans;
|
|
2564
3266
|
}
|
|
3267
|
+
var EmptySessionError = class extends Error {
|
|
3268
|
+
sourcePath;
|
|
3269
|
+
constructor(sourcePath) {
|
|
3270
|
+
super(`Session parser produced no spans at ${sourcePath}`);
|
|
3271
|
+
this.name = "EmptySessionError";
|
|
3272
|
+
this.sourcePath = sourcePath;
|
|
3273
|
+
}
|
|
3274
|
+
};
|
|
2565
3275
|
function refKey(ref) {
|
|
2566
3276
|
return `${ref.harness}\0${ref.path}\0${ref.sessionId}`;
|
|
2567
3277
|
}
|
|
@@ -2571,7 +3281,7 @@ async function locateSessions(adapter, opts = {}) {
|
|
|
2571
3281
|
const refs = await adapter.locate({ sinceMs: opts.sinceMs });
|
|
2572
3282
|
const byKey = /* @__PURE__ */ new Map();
|
|
2573
3283
|
for (const ref of refs) {
|
|
2574
|
-
if (!cwdMatchesSelection(ref.cwd, cwdSelections)) continue;
|
|
3284
|
+
if (!cwdMatchesSelection(ref.cwd, cwdSelections) && !(ref.cwd === null && ref.integrity)) continue;
|
|
2575
3285
|
byKey.set(refKey(ref), ref);
|
|
2576
3286
|
}
|
|
2577
3287
|
return [...byKey.values()].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
@@ -2583,7 +3293,8 @@ async function* scanSessions(opts) {
|
|
|
2583
3293
|
try {
|
|
2584
3294
|
refs = await locateSessions(adapter, { cwd: opts.cwd, sinceMs: opts.sinceMs });
|
|
2585
3295
|
} catch (err) {
|
|
2586
|
-
opts.onError
|
|
3296
|
+
if (!opts.onError) throw err;
|
|
3297
|
+
opts.onError(err);
|
|
2587
3298
|
continue;
|
|
2588
3299
|
}
|
|
2589
3300
|
if (opts.last && opts.last > 0) refs = refs.slice(0, opts.last);
|
|
@@ -2591,12 +3302,12 @@ async function* scanSessions(opts) {
|
|
|
2591
3302
|
if (opts.signal?.aborted) return;
|
|
2592
3303
|
let spans;
|
|
2593
3304
|
try {
|
|
2594
|
-
spans = await parseSession(adapter, ref);
|
|
3305
|
+
spans = await parseSession(adapter, ref, { corruptionMode: opts.corruptionMode });
|
|
2595
3306
|
} catch (err) {
|
|
2596
|
-
opts.onError
|
|
3307
|
+
if (!opts.onError) throw err;
|
|
3308
|
+
opts.onError(err, ref);
|
|
2597
3309
|
continue;
|
|
2598
3310
|
}
|
|
2599
|
-
if (spans.length === 0) continue;
|
|
2600
3311
|
yield { adapter, ref, spans };
|
|
2601
3312
|
}
|
|
2602
3313
|
}
|
|
@@ -2606,16 +3317,16 @@ async function* scanSessions(opts) {
|
|
|
2606
3317
|
import { mkdtemp, writeFile } from "fs/promises";
|
|
2607
3318
|
import { tmpdir } from "os";
|
|
2608
3319
|
import { join as join13 } from "path";
|
|
3320
|
+
import {
|
|
3321
|
+
OPENINFERENCE_SPAN_KIND as OPENINFERENCE_SPAN_KIND2,
|
|
3322
|
+
TOOL_NAME as TOOL_NAME2
|
|
3323
|
+
} from "@tangle-network/agent-eval/trace-attributes";
|
|
2609
3324
|
function stringAttr(span2, key) {
|
|
2610
3325
|
const value = span2.attributes[key];
|
|
2611
3326
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2612
3327
|
}
|
|
2613
|
-
function numberAttr(span2, key) {
|
|
2614
|
-
const value = span2.attributes[key];
|
|
2615
|
-
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
2616
|
-
}
|
|
2617
3328
|
function spanKind(span2) {
|
|
2618
|
-
return stringAttr(span2,
|
|
3329
|
+
return stringAttr(span2, OPENINFERENCE_SPAN_KIND2);
|
|
2619
3330
|
}
|
|
2620
3331
|
function repoFromSpans(spans) {
|
|
2621
3332
|
const attrs = {};
|
|
@@ -2641,7 +3352,7 @@ function summarizeTools(spans) {
|
|
|
2641
3352
|
const byTool = /* @__PURE__ */ new Map();
|
|
2642
3353
|
for (const span2 of spans) {
|
|
2643
3354
|
if (spanKind(span2) !== "TOOL") continue;
|
|
2644
|
-
const name = stringAttr(span2,
|
|
3355
|
+
const name = stringAttr(span2, TOOL_NAME2) ?? span2.name.replace(/^tool\./, "");
|
|
2645
3356
|
const current = byTool.get(name) ?? { calls: 0, errors: 0 };
|
|
2646
3357
|
current.calls += 1;
|
|
2647
3358
|
if (span2.status.code === "ERROR") current.errors += 1;
|
|
@@ -2650,7 +3361,6 @@ function summarizeTools(spans) {
|
|
|
2650
3361
|
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));
|
|
2651
3362
|
}
|
|
2652
3363
|
async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
|
|
2653
|
-
const llmSpans = spans.filter((span2) => spanKind(span2) === "LLM");
|
|
2654
3364
|
const toolSpans = spans.filter((span2) => spanKind(span2) === "TOOL");
|
|
2655
3365
|
const erroredToolCallCount = toolSpans.filter((span2) => span2.status.code === "ERROR").length;
|
|
2656
3366
|
const pipelines = await runPipelines(spans, { minLoopOccurrences: opts.minLoopOccurrences });
|
|
@@ -2658,10 +3368,14 @@ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
|
|
|
2658
3368
|
const loopFindings = pipelines.stuckLoops.findings;
|
|
2659
3369
|
const { firstSpanAt, lastSpanAt } = timeBounds(spans);
|
|
2660
3370
|
const repo = repoFromSpans(spans);
|
|
3371
|
+
const execution = summarizeSpanExecution(spans, {
|
|
3372
|
+
experimentId: `traces-policy-evidence:${ref.sessionId}`
|
|
3373
|
+
});
|
|
2661
3374
|
return {
|
|
2662
3375
|
schemaVersion: 1,
|
|
2663
3376
|
kind: "traces.policy_evidence.session",
|
|
2664
3377
|
generatedAt: opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3378
|
+
execution,
|
|
2665
3379
|
session: {
|
|
2666
3380
|
harness: ref.harness,
|
|
2667
3381
|
sessionId: ref.sessionId,
|
|
@@ -2672,12 +3386,12 @@ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
|
|
|
2672
3386
|
repo,
|
|
2673
3387
|
metrics: {
|
|
2674
3388
|
spanCount: spans.length,
|
|
2675
|
-
llmTurnCount:
|
|
3389
|
+
llmTurnCount: execution.execution.modelCalls.events,
|
|
2676
3390
|
toolCallCount: toolSpans.length,
|
|
2677
3391
|
erroredToolCallCount,
|
|
2678
|
-
inputTokens:
|
|
2679
|
-
outputTokens:
|
|
2680
|
-
models:
|
|
3392
|
+
inputTokens: execution.execution.tokenUsage.totals.input,
|
|
3393
|
+
outputTokens: execution.execution.tokenUsage.totals.output,
|
|
3394
|
+
models: execution.execution.models.map(({ model }) => model),
|
|
2681
3395
|
tools: summarizeTools(spans),
|
|
2682
3396
|
firstSpanAt,
|
|
2683
3397
|
lastSpanAt
|
|
@@ -2859,6 +3573,7 @@ function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
|
|
|
2859
3573
|
const report = { redactionCount: 0, byRule: {} };
|
|
2860
3574
|
const out = spans.map((s) => {
|
|
2861
3575
|
const attributes = redactValue(s.attributes, rules, report).value;
|
|
3576
|
+
normalizeToolIoAttributes(attributes);
|
|
2862
3577
|
let status = s.status;
|
|
2863
3578
|
if (status.message) {
|
|
2864
3579
|
const m = redactValue(status.message, rules, report).value;
|
|
@@ -2869,33 +3584,66 @@ function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
|
|
|
2869
3584
|
return { spans: out, report };
|
|
2870
3585
|
}
|
|
2871
3586
|
async function applyRedactor(spans, redactor) {
|
|
2872
|
-
const
|
|
3587
|
+
const capturedKeys = ["content", ...TOOL_IO_VALUE_KEYS];
|
|
3588
|
+
const fields = [];
|
|
2873
3589
|
const texts = [];
|
|
2874
3590
|
spans.forEach((s, i) => {
|
|
2875
|
-
const
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
3591
|
+
for (const key of capturedKeys) {
|
|
3592
|
+
const value = s.attributes[key];
|
|
3593
|
+
if (typeof value === "string" && value.length > 0) {
|
|
3594
|
+
fields.push({ spanIndex: i, key });
|
|
3595
|
+
texts.push(value);
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
if (s.status.message) {
|
|
3599
|
+
fields.push({ spanIndex: i, key: "status.message" });
|
|
3600
|
+
texts.push(s.status.message);
|
|
2879
3601
|
}
|
|
2880
3602
|
});
|
|
2881
3603
|
const out = spans.map((s) => ({ ...s, attributes: { ...s.attributes } }));
|
|
2882
|
-
if (texts.length === 0)
|
|
3604
|
+
if (texts.length === 0) {
|
|
3605
|
+
for (const span2 of out) normalizeToolIoAttributes(span2.attributes);
|
|
3606
|
+
return { spans: out, changed: 0 };
|
|
3607
|
+
}
|
|
2883
3608
|
const scrubbed = await redactor.redactText(texts);
|
|
3609
|
+
if (scrubbed.length !== texts.length) {
|
|
3610
|
+
throw new Error(`redactor ${redactor.name}: expected ${texts.length} values, received ${scrubbed.length}`);
|
|
3611
|
+
}
|
|
2884
3612
|
let changed = 0;
|
|
2885
|
-
|
|
3613
|
+
fields.forEach(({ spanIndex, key }, k) => {
|
|
2886
3614
|
if (scrubbed[k] !== texts[k]) {
|
|
2887
|
-
|
|
3615
|
+
if (key === "status.message") {
|
|
3616
|
+
out[spanIndex].status = { ...out[spanIndex].status, message: scrubbed[k] };
|
|
3617
|
+
} else {
|
|
3618
|
+
out[spanIndex].attributes[key] = scrubbed[k];
|
|
3619
|
+
}
|
|
2888
3620
|
changed += 1;
|
|
2889
3621
|
}
|
|
2890
3622
|
});
|
|
3623
|
+
for (const span2 of out) normalizeToolIoAttributes(span2.attributes);
|
|
2891
3624
|
return { spans: out, changed };
|
|
2892
3625
|
}
|
|
2893
3626
|
|
|
2894
3627
|
// src/file-export.ts
|
|
2895
|
-
import { createHash } from "crypto";
|
|
2896
|
-
import { mkdtemp as mkdtemp2, readFile as
|
|
3628
|
+
import { createHash as createHash4 } from "crypto";
|
|
3629
|
+
import { mkdtemp as mkdtemp2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
2897
3630
|
import { tmpdir as tmpdir2 } from "os";
|
|
2898
3631
|
import { join as join14 } from "path";
|
|
3632
|
+
import {
|
|
3633
|
+
firstNumberAttr,
|
|
3634
|
+
LLM_CACHED_TOKEN_ATTR_KEYS,
|
|
3635
|
+
LLM_CACHED_TOKENS,
|
|
3636
|
+
LLM_CACHE_WRITE_TOKEN_ATTR_KEYS,
|
|
3637
|
+
LLM_CACHE_WRITE_TOKENS,
|
|
3638
|
+
LLM_COST_ATTR_KEYS,
|
|
3639
|
+
LLM_COST_USD,
|
|
3640
|
+
LLM_INPUT_TOKEN_ATTR_KEYS,
|
|
3641
|
+
LLM_INPUT_TOKENS,
|
|
3642
|
+
LLM_OUTPUT_TOKEN_ATTR_KEYS,
|
|
3643
|
+
LLM_OUTPUT_TOKENS,
|
|
3644
|
+
LLM_REASONING_TOKEN_ATTR_KEYS,
|
|
3645
|
+
LLM_REASONING_TOKENS
|
|
3646
|
+
} from "@tangle-network/agent-eval/trace-attributes";
|
|
2899
3647
|
function isObject(value) {
|
|
2900
3648
|
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
2901
3649
|
}
|
|
@@ -2915,7 +3663,7 @@ function stableJson(value) {
|
|
|
2915
3663
|
});
|
|
2916
3664
|
}
|
|
2917
3665
|
function hashId(value, chars = 16) {
|
|
2918
|
-
return
|
|
3666
|
+
return createHash4("sha256").update(stableJson(value)).digest("hex").slice(0, chars);
|
|
2919
3667
|
}
|
|
2920
3668
|
function isoTime(value) {
|
|
2921
3669
|
if (typeof value === "string") {
|
|
@@ -3018,6 +3766,11 @@ function requireOpenInferenceRows(rows) {
|
|
|
3018
3766
|
return rows;
|
|
3019
3767
|
}
|
|
3020
3768
|
function requireIntelligenceSpanRows(rows) {
|
|
3769
|
+
if (rows.length > 0 && rows.every(isOpenInferenceRow)) {
|
|
3770
|
+
throw new Error(
|
|
3771
|
+
"intelligence-spans input is already complete OpenInference; use --format auto or --format openinference to preserve it losslessly"
|
|
3772
|
+
);
|
|
3773
|
+
}
|
|
3021
3774
|
if (!rows.every(isIntelligenceSpanRow)) throw new Error("intelligence-spans input must contain only Intelligence span rows");
|
|
3022
3775
|
return rows;
|
|
3023
3776
|
}
|
|
@@ -3049,6 +3802,7 @@ function policyEvidenceToSpans(records) {
|
|
|
3049
3802
|
"traces.metrics.errored_tool_call_count": record.metrics.erroredToolCallCount,
|
|
3050
3803
|
"traces.metrics.input_tokens": record.metrics.inputTokens,
|
|
3051
3804
|
"traces.metrics.output_tokens": record.metrics.outputTokens,
|
|
3805
|
+
"traces.execution": stableJson(record.execution),
|
|
3052
3806
|
"traces.metrics.models": record.metrics.models,
|
|
3053
3807
|
"traces.metrics.tools": stableJson(record.metrics.tools),
|
|
3054
3808
|
"traces.signals.stuck_loop_count": record.signals.stuckLoopCount,
|
|
@@ -3076,6 +3830,11 @@ function policyEvidenceToSpans(records) {
|
|
|
3076
3830
|
endTime: end,
|
|
3077
3831
|
service: record.session.harness,
|
|
3078
3832
|
agent: "traces",
|
|
3833
|
+
inputTokens: record.execution.execution.tokenUsage.totals.input,
|
|
3834
|
+
outputTokens: record.execution.execution.tokenUsage.totals.output,
|
|
3835
|
+
reasoningTokens: record.execution.execution.tokenUsage.totals.reasoning,
|
|
3836
|
+
cachedInputTokens: record.execution.execution.tokenUsage.totals.cached,
|
|
3837
|
+
cacheWriteInputTokens: record.execution.execution.tokenUsage.totals.cacheWrite,
|
|
3079
3838
|
content: capText(stableJson({
|
|
3080
3839
|
session: record.session,
|
|
3081
3840
|
repo: record.repo,
|
|
@@ -3106,6 +3865,16 @@ function findStringKey(value, keys, depth = 4) {
|
|
|
3106
3865
|
}
|
|
3107
3866
|
return void 0;
|
|
3108
3867
|
}
|
|
3868
|
+
function eventValue(row, keys) {
|
|
3869
|
+
const scopes = [row, objectValue(row.data), objectValue(row.payload)];
|
|
3870
|
+
for (const scope of scopes) {
|
|
3871
|
+
if (!scope) continue;
|
|
3872
|
+
for (const key of keys) {
|
|
3873
|
+
if (scope[key] !== void 0) return scope[key];
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
return void 0;
|
|
3877
|
+
}
|
|
3109
3878
|
function eventTimestamp(row) {
|
|
3110
3879
|
return firstTimeDeep(row, [
|
|
3111
3880
|
"timestamp",
|
|
@@ -3148,7 +3917,8 @@ function eventKind(row, type) {
|
|
|
3148
3917
|
}
|
|
3149
3918
|
function sandboxEventsToSpans(rows, wrapper) {
|
|
3150
3919
|
const context = wrapper ?? {};
|
|
3151
|
-
const
|
|
3920
|
+
const sessionId = findStringKey(context, ["session_id", "sessionId"], 3) ?? findStringKey(rows, ["session_id", "sessionId"], 4);
|
|
3921
|
+
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)}`;
|
|
3152
3922
|
const service = findStringKey(context, ["service", "harness", "source"], 2) ?? "sandbox-opencode";
|
|
3153
3923
|
const times = rows.flatMap((row) => {
|
|
3154
3924
|
const time = eventTimestamp(row);
|
|
@@ -3184,10 +3954,16 @@ function sandboxEventsToSpans(rows, wrapper) {
|
|
|
3184
3954
|
const { status, message } = eventStatus(row, type);
|
|
3185
3955
|
const { kind, tool } = eventKind(row, type);
|
|
3186
3956
|
const name = kind === "TOOL" && tool ? `tool.${tool}` : `event.${type}`;
|
|
3957
|
+
const toolInput = kind === "TOOL" ? eventValue(row, ["toolInput", "tool_input", "input", "args", "arguments", "full_command"]) : void 0;
|
|
3187
3958
|
const extra = {
|
|
3188
3959
|
"traces.source_format": "sandbox-events",
|
|
3189
3960
|
"traces.event.type": type,
|
|
3190
|
-
"traces.event.index": index
|
|
3961
|
+
"traces.event.index": index,
|
|
3962
|
+
...kind === "TOOL" ? toolIoAttributes({
|
|
3963
|
+
input: toolInput,
|
|
3964
|
+
output: eventValue(row, ["toolOutput", "tool_output", "output", "result"]),
|
|
3965
|
+
argsCaptured: toolInput !== void 0
|
|
3966
|
+
}) : {}
|
|
3191
3967
|
};
|
|
3192
3968
|
copyDefined(extra, {
|
|
3193
3969
|
"traces.event.id": stringValue(row.id),
|
|
@@ -3212,7 +3988,11 @@ function sandboxEventsToSpans(rows, wrapper) {
|
|
|
3212
3988
|
extra
|
|
3213
3989
|
}));
|
|
3214
3990
|
});
|
|
3215
|
-
return spans;
|
|
3991
|
+
if (!sessionId) return spans;
|
|
3992
|
+
return spans.map((item) => ({
|
|
3993
|
+
...item,
|
|
3994
|
+
attributes: { ...item.attributes, [ATTR.SESSION_ID]: sessionId }
|
|
3995
|
+
}));
|
|
3216
3996
|
}
|
|
3217
3997
|
function statusCode(value) {
|
|
3218
3998
|
return value === "OK" || value === "ERROR" || value === "UNSET" ? value : "UNSET";
|
|
@@ -3236,8 +4016,8 @@ function unixNanoTime(value) {
|
|
|
3236
4016
|
}
|
|
3237
4017
|
function firstNumber(row, attrs, keys) {
|
|
3238
4018
|
for (const key of keys) {
|
|
3239
|
-
const value =
|
|
3240
|
-
if (value
|
|
4019
|
+
const value = firstNumberAttr(row, [key]) ?? firstNumberAttr(attrs, [key]);
|
|
4020
|
+
if (value !== null) return value;
|
|
3241
4021
|
}
|
|
3242
4022
|
return void 0;
|
|
3243
4023
|
}
|
|
@@ -3249,6 +4029,12 @@ var COMPUTED_SPAN_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
|
3249
4029
|
"tool.name",
|
|
3250
4030
|
"llm.input_tokens",
|
|
3251
4031
|
"llm.output_tokens",
|
|
4032
|
+
LLM_INPUT_TOKENS,
|
|
4033
|
+
LLM_OUTPUT_TOKENS,
|
|
4034
|
+
LLM_REASONING_TOKENS,
|
|
4035
|
+
LLM_CACHED_TOKENS,
|
|
4036
|
+
LLM_CACHE_WRITE_TOKENS,
|
|
4037
|
+
LLM_COST_USD,
|
|
3252
4038
|
"step",
|
|
3253
4039
|
"content"
|
|
3254
4040
|
]);
|
|
@@ -3263,10 +4049,27 @@ function preserveRawAttributes(attrs) {
|
|
|
3263
4049
|
}
|
|
3264
4050
|
return preserved;
|
|
3265
4051
|
}
|
|
4052
|
+
function capturedToolIo(attrs) {
|
|
4053
|
+
const input = attrs["input.value"] ?? attrs["tool.input"] ?? attrs.tool_input ?? attrs["tool.arguments"] ?? attrs.tool_arguments ?? attrs.arguments ?? attrs.args ?? attrs.full_command;
|
|
4054
|
+
const capture = toolArgumentsFromAttributes({ ...attrs, "input.value": input });
|
|
4055
|
+
return toolIoAttributes({
|
|
4056
|
+
input: capture.args,
|
|
4057
|
+
output: attrs["output.value"] ?? attrs["tool.output"] ?? attrs.tool_output,
|
|
4058
|
+
argsCaptured: capture.argsCaptured
|
|
4059
|
+
});
|
|
4060
|
+
}
|
|
4061
|
+
function isToolTelemetrySpan(attrs) {
|
|
4062
|
+
const spanType = stringValue(attrs["span.type"])?.toLowerCase();
|
|
4063
|
+
return spanType === "tool.execution" || spanType === "tool.blocked_on_user";
|
|
4064
|
+
}
|
|
4065
|
+
function otlpSpanKind(value) {
|
|
4066
|
+
return value === "AGENT" || value === "LLM" || value === "TOOL" || value === "CHAIN" || value === "SPAN" ? value : void 0;
|
|
4067
|
+
}
|
|
3266
4068
|
function intelligenceSpanKind(name, attrs) {
|
|
3267
4069
|
const lowered = name.toLowerCase();
|
|
3268
4070
|
const spanType = stringValue(attrs["span.type"])?.toLowerCase();
|
|
3269
4071
|
if (spanType === "llm_request" || lowered.includes("llm")) return "LLM";
|
|
4072
|
+
if (isToolTelemetrySpan(attrs)) return "CHAIN";
|
|
3270
4073
|
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) {
|
|
3271
4074
|
return "TOOL";
|
|
3272
4075
|
}
|
|
@@ -3281,9 +4084,12 @@ function intelligenceSpansToSpans(rows) {
|
|
|
3281
4084
|
const endTime = unixNanoTime(row.end_unix_nano) ?? isoTime(row.end_time) ?? stringValue(row.end_time) ?? startTime;
|
|
3282
4085
|
const model = stringValue(row.model) ?? stringValue(attrs["llm.model_name"]) ?? stringValue(attrs.model) ?? stringValue(attrs["gen_ai.request.model"]);
|
|
3283
4086
|
const tool = stringValue(attrs["tool.name"]) ?? stringValue(attrs.tool_name) ?? stringValue(attrs["mcp.tool.name"]);
|
|
3284
|
-
const
|
|
4087
|
+
const kind = intelligenceSpanKind(name, attrs);
|
|
4088
|
+
const sourceStatus = objectValue(row.status);
|
|
4089
|
+
const sessionId = stringValue(row.session_id) ?? sessionIdFromAttributes(attrs);
|
|
3285
4090
|
const extra = {
|
|
3286
4091
|
...preserveRawAttributes(attrs),
|
|
4092
|
+
...kind === "TOOL" ? capturedToolIo(attrs) : {},
|
|
3287
4093
|
"traces.source_format": "intelligence-spans",
|
|
3288
4094
|
"traces.row.index": index
|
|
3289
4095
|
};
|
|
@@ -3297,7 +4103,6 @@ function intelligenceSpansToSpans(rows) {
|
|
|
3297
4103
|
"generation.id": stringValue(row.generation),
|
|
3298
4104
|
"cell.id": stringValue(row.cell_id),
|
|
3299
4105
|
"project.key": stringValue(row.project_key),
|
|
3300
|
-
"llm.cost_usd": numberValue(row.cost_usd),
|
|
3301
4106
|
"redaction.version": stringValue(row.redaction_version),
|
|
3302
4107
|
"traces.received_at": isoTime(row.received_at)
|
|
3303
4108
|
});
|
|
@@ -3306,17 +4111,21 @@ function intelligenceSpansToSpans(rows) {
|
|
|
3306
4111
|
spanId: stringValue(row.id) ?? hashId({ row, field: "span" }),
|
|
3307
4112
|
parentSpanId: stringValue(row.parent_span_id) ?? null,
|
|
3308
4113
|
name,
|
|
3309
|
-
kind
|
|
4114
|
+
kind,
|
|
3310
4115
|
startTime,
|
|
3311
4116
|
endTime,
|
|
3312
|
-
status: statusCode(row.status_code),
|
|
3313
|
-
statusMessage: stringValue(row.status_message),
|
|
4117
|
+
status: statusCode(row.status_code ?? sourceStatus?.code),
|
|
4118
|
+
statusMessage: stringValue(row.status_message) ?? stringValue(sourceStatus?.message),
|
|
3314
4119
|
service: stringValue(attrs["service.name"]) ?? stringValue(attrs.service_name) ?? "tangle-intelligence",
|
|
3315
4120
|
agent: stringValue(attrs["agent.name"]) ?? stringValue(attrs["service.name"]) ?? null,
|
|
3316
4121
|
model,
|
|
3317
4122
|
tool,
|
|
3318
|
-
inputTokens: firstNumber(row, attrs, ["input_tokens",
|
|
3319
|
-
outputTokens: firstNumber(row, attrs, ["output_tokens",
|
|
4123
|
+
inputTokens: firstNumber(row, attrs, ["input_tokens", ...LLM_INPUT_TOKEN_ATTR_KEYS]),
|
|
4124
|
+
outputTokens: firstNumber(row, attrs, ["output_tokens", ...LLM_OUTPUT_TOKEN_ATTR_KEYS]),
|
|
4125
|
+
reasoningTokens: firstNumber(row, attrs, LLM_REASONING_TOKEN_ATTR_KEYS),
|
|
4126
|
+
cachedInputTokens: firstNumber(row, attrs, LLM_CACHED_TOKEN_ATTR_KEYS),
|
|
4127
|
+
cacheWriteInputTokens: firstNumber(row, attrs, LLM_CACHE_WRITE_TOKEN_ATTR_KEYS),
|
|
4128
|
+
costUsd: firstNumber(row, attrs, ["cost_usd", ...LLM_COST_ATTR_KEYS]),
|
|
3320
4129
|
step: numberValue(attrs["interaction.sequence"]) ?? index,
|
|
3321
4130
|
content: stringValue(attrs.user_prompt) ?? stringValue(attrs["gen_ai.prompt.messages"]) ?? stringValue(attrs["log.content.task"]) ?? null,
|
|
3322
4131
|
extra
|
|
@@ -3329,15 +4138,21 @@ function openInferenceToSpans(rows) {
|
|
|
3329
4138
|
const resource = objectValue(row.resource);
|
|
3330
4139
|
const resourceAttrs = objectValue(resource?.attributes);
|
|
3331
4140
|
if (resourceAttrs) copyDefined(attrs, resourceAttrs);
|
|
3332
|
-
const
|
|
3333
|
-
|
|
4141
|
+
const name = stringValue(row.name) ?? "span";
|
|
4142
|
+
const declaredKind = otlpSpanKind(attrs["openinference.span.kind"]) ?? otlpSpanKind(row.kind);
|
|
4143
|
+
const kind = isToolTelemetrySpan(attrs) ? "CHAIN" : declaredKind ?? intelligenceSpanKind(name, attrs);
|
|
4144
|
+
if (declaredKind && declaredKind !== kind) {
|
|
4145
|
+
attrs["traces.raw_attribute.openinference.span.kind"] = declaredKind;
|
|
4146
|
+
}
|
|
4147
|
+
attrs["openinference.span.kind"] = kind;
|
|
4148
|
+
if (kind === "TOOL") Object.assign(attrs, capturedToolIo(attrs));
|
|
3334
4149
|
const status = objectValue(row.status);
|
|
3335
4150
|
const message = stringValue(status?.message);
|
|
3336
4151
|
return {
|
|
3337
4152
|
trace_id: stringValue(row.trace_id) ?? hashId(row, 32),
|
|
3338
4153
|
span_id: stringValue(row.span_id) ?? hashId({ row, field: "span" }),
|
|
3339
4154
|
parent_span_id: stringValue(row.parent_span_id) || null,
|
|
3340
|
-
name
|
|
4155
|
+
name,
|
|
3341
4156
|
start_time: stringValue(row.start_time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
3342
4157
|
end_time: stringValue(row.end_time) ?? stringValue(row.start_time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
3343
4158
|
status: { code: statusCode(status?.code), ...message ? { message } : {} },
|
|
@@ -3384,7 +4199,40 @@ function exportTraceEvidenceText(text, opts = {}) {
|
|
|
3384
4199
|
return exportTraceEvidenceRows(parsed.rows, opts, parsed.wrapper);
|
|
3385
4200
|
}
|
|
3386
4201
|
async function exportTraceEvidenceFile(inputPath, opts = {}) {
|
|
3387
|
-
|
|
4202
|
+
if (/\.(?:jsonl|ndjson)$/i.test(inputPath)) {
|
|
4203
|
+
let format;
|
|
4204
|
+
const spans = [];
|
|
4205
|
+
const sandboxRows = [];
|
|
4206
|
+
let redactionCount = 0;
|
|
4207
|
+
const redactionsByRule = {};
|
|
4208
|
+
for await (const row of readJsonl(inputPath)) {
|
|
4209
|
+
format ??= detectFormat([row], opts.format ?? "auto");
|
|
4210
|
+
if (format === "sandbox-events") {
|
|
4211
|
+
sandboxRows.push(row);
|
|
4212
|
+
continue;
|
|
4213
|
+
}
|
|
4214
|
+
const converted = exportTraceEvidenceRows([row], {
|
|
4215
|
+
...opts,
|
|
4216
|
+
sourcePath: inputPath,
|
|
4217
|
+
format
|
|
4218
|
+
});
|
|
4219
|
+
spans.push(...converted.spans);
|
|
4220
|
+
redactionCount += converted.redactionCount;
|
|
4221
|
+
for (const [rule, count2] of Object.entries(converted.redactionsByRule)) {
|
|
4222
|
+
redactionsByRule[rule] = (redactionsByRule[rule] ?? 0) + count2;
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
if (!format) throw new Error("input file is empty");
|
|
4226
|
+
if (format === "sandbox-events") {
|
|
4227
|
+
return exportTraceEvidenceRows(sandboxRows, {
|
|
4228
|
+
...opts,
|
|
4229
|
+
sourcePath: inputPath,
|
|
4230
|
+
format
|
|
4231
|
+
});
|
|
4232
|
+
}
|
|
4233
|
+
return { format, spans, redactionCount, redactionsByRule };
|
|
4234
|
+
}
|
|
4235
|
+
const text = await readFile3(inputPath, "utf8");
|
|
3388
4236
|
return exportTraceEvidenceText(text, { ...opts, sourcePath: inputPath });
|
|
3389
4237
|
}
|
|
3390
4238
|
async function writeTraceEvidenceExportFile(inputPath, outPath, opts = {}) {
|
|
@@ -3395,7 +4243,7 @@ async function writeTraceEvidenceExportFile(inputPath, outPath, opts = {}) {
|
|
|
3395
4243
|
}
|
|
3396
4244
|
|
|
3397
4245
|
// src/inspect.ts
|
|
3398
|
-
import { mkdtemp as mkdtemp3, readFile as
|
|
4246
|
+
import { mkdtemp as mkdtemp3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
3399
4247
|
import { tmpdir as tmpdir3 } from "os";
|
|
3400
4248
|
import { basename as basename8, join as join15 } from "path";
|
|
3401
4249
|
var severityRank = {
|
|
@@ -3674,14 +4522,14 @@ function assertSessionIndex(value) {
|
|
|
3674
4522
|
}
|
|
3675
4523
|
const candidate = value;
|
|
3676
4524
|
if (candidate.kind !== "traces.session_index" || candidate.schemaVersion !== 1 || !Array.isArray(candidate.sessions)) {
|
|
3677
|
-
throw new Error("input is not a traces.session_index
|
|
4525
|
+
throw new Error("input is not a traces.session_index file with schemaVersion 1");
|
|
3678
4526
|
}
|
|
3679
4527
|
if (!candidate.totals || typeof candidate.totals !== "object") {
|
|
3680
4528
|
throw new Error("session index is missing totals");
|
|
3681
4529
|
}
|
|
3682
4530
|
}
|
|
3683
4531
|
async function readSessionIndexFile(path) {
|
|
3684
|
-
const parsed = JSON.parse(await
|
|
4532
|
+
const parsed = JSON.parse(await readFile4(path, "utf8"));
|
|
3685
4533
|
assertSessionIndex(parsed);
|
|
3686
4534
|
return parsed;
|
|
3687
4535
|
}
|
|
@@ -3779,6 +4627,8 @@ function analyzeReactions(spans) {
|
|
|
3779
4627
|
|
|
3780
4628
|
// src/report.ts
|
|
3781
4629
|
var SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
4630
|
+
var CORRUPTION_RECEIPT_DISPLAY_LIMIT = 100;
|
|
4631
|
+
var SOURCE_DISPLAY_LIMIT = 20;
|
|
3782
4632
|
var SEVERITY_BADGE = {
|
|
3783
4633
|
critical: "\u{1F534} CRITICAL",
|
|
3784
4634
|
high: "\u{1F7E0} HIGH",
|
|
@@ -3790,23 +4640,105 @@ function deterministicSummaryText(summary) {
|
|
|
3790
4640
|
const parts = [];
|
|
3791
4641
|
if (summary.stuckLoops > 0) parts.push(`${summary.stuckLoops} stuck loop(s)`);
|
|
3792
4642
|
if (summary.reactionSignals > 0) parts.push(`${summary.reactionSignals} human reaction signal(s)`);
|
|
3793
|
-
if (summary.
|
|
4643
|
+
if (summary.failedRuns > 0) parts.push(`${summary.failedRuns} failed run(s)`);
|
|
3794
4644
|
if (parts.length === 0) return "0 deterministic signals";
|
|
3795
4645
|
if (parts.length === 1) return parts[0];
|
|
3796
4646
|
return `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
|
|
3797
4647
|
}
|
|
3798
|
-
function plural(
|
|
3799
|
-
return
|
|
4648
|
+
function plural(count2, singular, pluralForm = `${singular}s`) {
|
|
4649
|
+
return count2 === 1 ? singular : pluralForm;
|
|
4650
|
+
}
|
|
4651
|
+
function tableCell(value) {
|
|
4652
|
+
return value.replace(/\s+/g, " ").replaceAll("|", "\\|").trim();
|
|
4653
|
+
}
|
|
4654
|
+
function count(value) {
|
|
4655
|
+
return Math.round(value).toLocaleString("en-US");
|
|
4656
|
+
}
|
|
4657
|
+
function decimal(value, digits = 1) {
|
|
4658
|
+
return value.toLocaleString("en-US", {
|
|
4659
|
+
minimumFractionDigits: digits,
|
|
4660
|
+
maximumFractionDigits: digits
|
|
4661
|
+
});
|
|
4662
|
+
}
|
|
4663
|
+
function distributionRow(label, distribution, unit = "") {
|
|
4664
|
+
if (distribution.n === 0) return `| ${label} | 0 | not captured | not captured | not captured | not captured | not captured |`;
|
|
4665
|
+
const value = (number) => `${decimal(number)}${unit}`;
|
|
4666
|
+
return `| ${label} | ${count(distribution.n)} | ${value(distribution.min)} | ${value(distribution.mean)} | ${value(distribution.p50)} | ${value(distribution.p95)} | ${value(distribution.max)} |`;
|
|
4667
|
+
}
|
|
4668
|
+
function tokenRows(usage) {
|
|
4669
|
+
const rows = [
|
|
4670
|
+
["Input", usage.totals.input, usage.input],
|
|
4671
|
+
["Output", usage.totals.output, usage.output],
|
|
4672
|
+
["Reasoning (output subset)", usage.totals.reasoning, usage.reasoning],
|
|
4673
|
+
["Cache read", usage.totals.cached, usage.cached],
|
|
4674
|
+
["Cache write", usage.totals.cacheWrite, usage.cacheWrite]
|
|
4675
|
+
];
|
|
4676
|
+
return rows.map(
|
|
4677
|
+
([label, total, values]) => `| ${label} | ${count(total)} | ${count(values.n)} | ${decimal(values.mean)} | ${decimal(values.p50)} | ${decimal(values.p95)} | ${count(values.max)} |`
|
|
4678
|
+
);
|
|
4679
|
+
}
|
|
4680
|
+
function renderExecution(report) {
|
|
4681
|
+
const execution = report.execution;
|
|
4682
|
+
const lines = ["## execution facts", ""];
|
|
4683
|
+
lines.push(
|
|
4684
|
+
`- **Runs:** ${count(execution.durationMs.n)} | **Model-call runs:** ${count(execution.modelCalls.runs)} | **Model-call events:** ${count(execution.modelCalls.events)} | **Failed runs:** ${count(execution.failures.runs)}/${count(execution.failures.reportingRuns)} (${decimal(execution.failures.fraction * 100, 2)}%)`
|
|
4685
|
+
);
|
|
4686
|
+
lines.push("- **Task quality:** not measured; these traces do not include comparable task outcome labels.");
|
|
4687
|
+
lines.push("");
|
|
4688
|
+
lines.push("| Time | Runs measured | Min | Mean | p50 | p95 | Max |");
|
|
4689
|
+
lines.push("|---|---:|---:|---:|---:|---:|---:|");
|
|
4690
|
+
lines.push(distributionRow("Duration", execution.durationMs, "ms"));
|
|
4691
|
+
lines.push(distributionRow("Queue", execution.queueMs, "ms"));
|
|
4692
|
+
lines.push("");
|
|
4693
|
+
lines.push("### Direct model usage");
|
|
4694
|
+
lines.push("");
|
|
4695
|
+
lines.push("| Token category | Total | Runs measured | Mean | p50 | p95 | Max |");
|
|
4696
|
+
lines.push("|---|---:|---:|---:|---:|---:|---:|");
|
|
4697
|
+
lines.push(...tokenRows(execution.tokenUsage));
|
|
4698
|
+
lines.push("");
|
|
4699
|
+
lines.push("| Model | Runs |");
|
|
4700
|
+
lines.push("|---|---:|");
|
|
4701
|
+
for (const model of execution.models) {
|
|
4702
|
+
lines.push(`| \`${tableCell(model.model)}\` | ${count(model.runs)} |`);
|
|
4703
|
+
}
|
|
4704
|
+
lines.push("");
|
|
4705
|
+
lines.push("### Cost coverage");
|
|
4706
|
+
lines.push("");
|
|
4707
|
+
lines.push("| Source | Runs | Total USD |");
|
|
4708
|
+
lines.push("|---|---:|---:|");
|
|
4709
|
+
lines.push(`| Observed | ${count(report.costProvenance.observed.n)} | $${report.costProvenance.observed.totalUsd.toFixed(6)} |`);
|
|
4710
|
+
lines.push(`| Estimated | ${count(report.costProvenance.estimated.n)} | $${report.costProvenance.estimated.totalUsd.toFixed(6)} |`);
|
|
4711
|
+
lines.push(`| Uncaptured | ${count(report.costProvenance.uncaptured.n)} | not available |`);
|
|
4712
|
+
lines.push("");
|
|
4713
|
+
lines.push(`Known cost coverage: ${decimal(report.costProvenance.knownFraction * 100, 2)}%.`);
|
|
4714
|
+
lines.push("");
|
|
4715
|
+
if (execution.aggregateUsage.runs > 0) {
|
|
4716
|
+
lines.push("### Orchestration-reported usage");
|
|
4717
|
+
lines.push("");
|
|
4718
|
+
lines.push(
|
|
4719
|
+
`Kept separate from direct model usage because orchestration totals can overlap model-call telemetry. ${count(execution.aggregateUsage.runs)} run(s) reported aggregate values.`
|
|
4720
|
+
);
|
|
4721
|
+
lines.push("");
|
|
4722
|
+
lines.push("| Token category | Total | Runs measured | Mean | p50 | p95 | Max |");
|
|
4723
|
+
lines.push("|---|---:|---:|---:|---:|---:|---:|");
|
|
4724
|
+
lines.push(...tokenRows(execution.aggregateUsage.tokenUsage));
|
|
4725
|
+
lines.push("");
|
|
4726
|
+
lines.push(
|
|
4727
|
+
`Aggregate cost: $${execution.aggregateUsage.totalCostUsd.toFixed(6)} across ${count(execution.aggregateUsage.costUsd.n)} reporting run(s).`
|
|
4728
|
+
);
|
|
4729
|
+
lines.push("");
|
|
4730
|
+
}
|
|
4731
|
+
return lines.join("\n");
|
|
3800
4732
|
}
|
|
3801
4733
|
function summarizeDeterministicSignals(pipelines, reactions) {
|
|
3802
4734
|
const stuckLoops = pipelines.stuckLoops.findings.length;
|
|
3803
|
-
const reactionSignals = Object.values(reactions.signals).reduce((total,
|
|
3804
|
-
const
|
|
4735
|
+
const reactionSignals = Object.values(reactions.signals).reduce((total, count2) => total + count2, 0);
|
|
4736
|
+
const failedRuns = pipelines.failureClusters.totalFailures;
|
|
3805
4737
|
return {
|
|
3806
4738
|
stuckLoops,
|
|
3807
4739
|
reactionSignals,
|
|
3808
|
-
|
|
3809
|
-
totalSignals: stuckLoops + reactionSignals +
|
|
4740
|
+
failedRuns,
|
|
4741
|
+
totalSignals: stuckLoops + reactionSignals + failedRuns
|
|
3810
4742
|
};
|
|
3811
4743
|
}
|
|
3812
4744
|
function renderReport(result, meta) {
|
|
@@ -3814,14 +4746,93 @@ function renderReport(result, meta) {
|
|
|
3814
4746
|
const findings = [...result.findings].sort(
|
|
3815
4747
|
(a, b) => (SEVERITY_RANK[a.severity] ?? 9) - (SEVERITY_RANK[b.severity] ?? 9)
|
|
3816
4748
|
);
|
|
3817
|
-
const deterministic = meta.deterministic ?? { stuckLoops: 0, reactionSignals: 0,
|
|
3818
|
-
const findingSummary = deterministic.totalSignals > 0 ? `${findings.length}
|
|
4749
|
+
const deterministic = meta.deterministic ?? { stuckLoops: 0, reactionSignals: 0, failedRuns: 0, totalSignals: 0 };
|
|
4750
|
+
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")}`;
|
|
3819
4751
|
lines.push(`# Trace analysis \u2014 ${meta.harness}`);
|
|
3820
4752
|
lines.push("");
|
|
4753
|
+
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)`;
|
|
3821
4754
|
lines.push(
|
|
3822
|
-
`${
|
|
4755
|
+
`${sessionSummary}, ${meta.spanCount} spans \u2192 **${findingSummary}** across ${result.per_analyst.length} analyst(s). Analysis cost: $${result.total_cost_usd.toFixed(4)}.`
|
|
3823
4756
|
);
|
|
3824
4757
|
lines.push("");
|
|
4758
|
+
if (meta.unassignedTraceCount) {
|
|
4759
|
+
lines.push(
|
|
4760
|
+
`> ${meta.unassignedTraceCount} ${plural(meta.unassignedTraceCount, "trace")} lacked a single stable session identity. Their spans were analyzed but were not relabeled as sessions.`
|
|
4761
|
+
);
|
|
4762
|
+
lines.push("");
|
|
4763
|
+
}
|
|
4764
|
+
lines.push(renderExecution(meta.execution));
|
|
4765
|
+
lines.push("");
|
|
4766
|
+
if (meta.sources && meta.sources.length > 0) {
|
|
4767
|
+
lines.push("## Selected sessions");
|
|
4768
|
+
lines.push("");
|
|
4769
|
+
lines.push("| Role | Session ID | Parent session | Integrity | Subject (first prompt line) | Source path |");
|
|
4770
|
+
lines.push("|---|---|---|---|---|---|");
|
|
4771
|
+
for (const source of meta.sources.slice(0, SOURCE_DISPLAY_LIMIT)) {
|
|
4772
|
+
const corruptionCount = source.corruptionCount ?? source.corruptions?.length ?? 0;
|
|
4773
|
+
const integrity = source.integrity === "degraded_not_lossless" ? `degraded, not lossless (${corruptionCount} corrupt ${plural(corruptionCount, "record")})` : "complete";
|
|
4774
|
+
lines.push(
|
|
4775
|
+
`| ${source.role} | \`${tableCell(source.sessionId)}\` | ${source.parentSessionId ? `\`${tableCell(source.parentSessionId)}\`` : "\u2014"} | ${integrity} | ${tableCell(source.subject) || "(no prompt captured)"} | \`${tableCell(source.path)}\` |`
|
|
4776
|
+
);
|
|
4777
|
+
}
|
|
4778
|
+
if (meta.sources.length > SOURCE_DISPLAY_LIMIT) {
|
|
4779
|
+
lines.push(
|
|
4780
|
+
`| \u2026 | ${meta.sources.length - SOURCE_DISPLAY_LIMIT} additional sessions omitted | \u2014 | \u2014 | \u2014 | \u2014 |`
|
|
4781
|
+
);
|
|
4782
|
+
}
|
|
4783
|
+
lines.push("");
|
|
4784
|
+
const childCount = meta.sources.filter((source) => source.role === "child").length;
|
|
4785
|
+
if (childCount > 0) {
|
|
4786
|
+
lines.push(
|
|
4787
|
+
`> Scope: ${childCount}/${meta.sources.length} selected session(s) are children. Counts below describe only the selected files, not their parent operator sessions.`
|
|
4788
|
+
);
|
|
4789
|
+
lines.push("");
|
|
4790
|
+
}
|
|
4791
|
+
const totalCorruptionCount = meta.sources.reduce(
|
|
4792
|
+
(total, source) => total + (source.corruptionCount ?? source.corruptions?.length ?? 0),
|
|
4793
|
+
0
|
|
4794
|
+
);
|
|
4795
|
+
const corruptions = [];
|
|
4796
|
+
for (const source of meta.sources) {
|
|
4797
|
+
for (const receipt of source.corruptions ?? []) {
|
|
4798
|
+
if (corruptions.length === CORRUPTION_RECEIPT_DISPLAY_LIMIT) break;
|
|
4799
|
+
corruptions.push(receipt);
|
|
4800
|
+
}
|
|
4801
|
+
if (corruptions.length === CORRUPTION_RECEIPT_DISPLAY_LIMIT) break;
|
|
4802
|
+
}
|
|
4803
|
+
if (totalCorruptionCount > 0) {
|
|
4804
|
+
lines.push("## Source corruption receipts");
|
|
4805
|
+
lines.push("");
|
|
4806
|
+
lines.push(
|
|
4807
|
+
"> 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."
|
|
4808
|
+
);
|
|
4809
|
+
lines.push("");
|
|
4810
|
+
for (const source of meta.sources) {
|
|
4811
|
+
const count2 = source.corruptionCount ?? source.corruptions?.length ?? 0;
|
|
4812
|
+
if (count2 === 0) continue;
|
|
4813
|
+
const digest = source.corruptionDigest ? `, digest \`${source.corruptionDigest}\`` : "";
|
|
4814
|
+
lines.push(`- Session \`${tableCell(source.sessionId)}\`: ${count2} ${plural(count2, "receipt")}${digest}.`);
|
|
4815
|
+
}
|
|
4816
|
+
lines.push("");
|
|
4817
|
+
if (corruptions.length > 0) {
|
|
4818
|
+
lines.push("| Session ID | Source path | Line | Byte offset | Byte length | SHA-256 | Raw bytes |");
|
|
4819
|
+
lines.push("|---|---|---:|---:|---:|---|---|");
|
|
4820
|
+
for (const receipt of corruptions) {
|
|
4821
|
+
lines.push(
|
|
4822
|
+
`| \`${tableCell(receipt.sessionId)}\` | \`${tableCell(receipt.sourcePath)}\` | ${receipt.lineNumber} | ${receipt.byteOffset} | ${receipt.byteLength} | \`${receipt.sha256}\` | local source only |`
|
|
4823
|
+
);
|
|
4824
|
+
}
|
|
4825
|
+
lines.push("");
|
|
4826
|
+
}
|
|
4827
|
+
const omitted = totalCorruptionCount - corruptions.length;
|
|
4828
|
+
if (omitted > 0) {
|
|
4829
|
+
lines.push(
|
|
4830
|
+
`_${omitted} additional ${plural(omitted, "receipt")} omitted from this report; all receipts remain in \`source.corruption.receipt\` child spans._`
|
|
4831
|
+
);
|
|
4832
|
+
lines.push("");
|
|
4833
|
+
}
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
3825
4836
|
lines.push("| Analyst | Status | Findings | Latency |");
|
|
3826
4837
|
lines.push("|---|---|---|---|");
|
|
3827
4838
|
for (const s of result.per_analyst) {
|
|
@@ -3829,7 +4840,7 @@ function renderReport(result, meta) {
|
|
|
3829
4840
|
}
|
|
3830
4841
|
lines.push("");
|
|
3831
4842
|
if (findings.length === 0 && deterministic.totalSignals === 0) {
|
|
3832
|
-
lines.push("_No
|
|
4843
|
+
lines.push("_No supported behavioral findings in the captured fields._");
|
|
3833
4844
|
} else if (findings.length === 0) {
|
|
3834
4845
|
lines.push(
|
|
3835
4846
|
`_No analyst findings. Deterministic checks found ${deterministicSummaryText(deterministic)}; see sections below._`
|
|
@@ -3850,6 +4861,7 @@ function renderReport(result, meta) {
|
|
|
3850
4861
|
if (f.subject) lines.push(`- **Subject:** \`${f.subject}\``);
|
|
3851
4862
|
lines.push(`- **Confidence:** ${f.confidence}`);
|
|
3852
4863
|
if (f.recommended_action) lines.push(`- **Fix:** ${f.recommended_action}`);
|
|
4864
|
+
if (f.validation_plan) lines.push(`- **Check:** ${f.validation_plan}`);
|
|
3853
4865
|
for (const ev of f.evidence_refs.slice(0, 3)) {
|
|
3854
4866
|
const excerpt = ev.excerpt ? ` \u2014 \`${ev.excerpt.slice(0, 200)}\`` : "";
|
|
3855
4867
|
lines.push(`- **Evidence:** ${ev.kind} ${ev.uri}${excerpt}`);
|
|
@@ -3864,20 +4876,79 @@ function renderReport(result, meta) {
|
|
|
3864
4876
|
return lines.join("\n");
|
|
3865
4877
|
}
|
|
3866
4878
|
function renderPipelines(pr) {
|
|
3867
|
-
const lines = ["## loops & waste (deterministic)", ""];
|
|
4879
|
+
const lines = ["## reliability, loops & waste (deterministic)", ""];
|
|
4880
|
+
lines.push(
|
|
4881
|
+
`- **Execution failures:** ${pr.failureClusters.totalFailures}/${pr.failureClusters.totalRuns} run(s)`
|
|
4882
|
+
);
|
|
4883
|
+
if (pr.failureClusters.clusters.length > 0) {
|
|
4884
|
+
lines.push("");
|
|
4885
|
+
lines.push("| Failure class | Runs | Tool | Example trace | Example error |");
|
|
4886
|
+
lines.push("|---|---:|---|---|---|");
|
|
4887
|
+
for (const cluster of pr.failureClusters.clusters.slice(0, 10)) {
|
|
4888
|
+
lines.push(
|
|
4889
|
+
`| ${tableCell(cluster.failureClass)} | ${cluster.runCount} | ${cluster.toolName ? `\`${tableCell(cluster.toolName)}\`` : "not captured"} | ${cluster.exampleRunId ? `\`${tableCell(cluster.exampleRunId)}\`` : "not captured"} | ${tableCell(cluster.exampleError ?? "not captured")} |`
|
|
4890
|
+
);
|
|
4891
|
+
}
|
|
4892
|
+
lines.push("");
|
|
4893
|
+
}
|
|
3868
4894
|
if (pr.stuckLoops.findings.length === 0) {
|
|
3869
|
-
lines.push("- **Stuck loops:** none (no tool called \u22653\xD7 with identical args).");
|
|
4895
|
+
lines.push("- **Stuck loops:** none (no tool called \u22653\xD7 with identical args in a short interval).");
|
|
3870
4896
|
} else {
|
|
3871
4897
|
lines.push(`- **Stuck loops:** ${pr.stuckLoops.findings.length} (${(pr.stuckLoops.affectedRunRatio * 100).toFixed(0)}% of runs affected)`);
|
|
3872
4898
|
for (const f of pr.stuckLoops.findings.sort((a, b) => b.occurrences - a.occurrences).slice(0, 10)) {
|
|
3873
4899
|
lines.push(` - \u{1F501} \`${f.toolName}\` \xD7${f.occurrences} with identical args over ${(f.windowMs / 1e3).toFixed(1)}s`);
|
|
3874
4900
|
}
|
|
3875
4901
|
}
|
|
3876
|
-
|
|
3877
|
-
|
|
4902
|
+
const runsWithTools = pr.toolUse.filter((metrics) => metrics.totalCalls > 0);
|
|
4903
|
+
if (runsWithTools.length > 0) {
|
|
4904
|
+
let totalCalls = 0;
|
|
4905
|
+
let callsWithCapturedArgs = 0;
|
|
4906
|
+
let duplicateCalls = 0;
|
|
4907
|
+
let errorCalls = 0;
|
|
4908
|
+
let followedFailures = 0;
|
|
4909
|
+
const byTool = /* @__PURE__ */ new Map();
|
|
4910
|
+
for (const metrics of runsWithTools) {
|
|
4911
|
+
const toolStats = Object.values(metrics.byTool);
|
|
4912
|
+
const duplicates = toolStats.length > 0 ? toolStats.reduce((total, stats) => total + stats.duplicates, 0) : Math.round(metrics.duplicateRate * metrics.totalCalls);
|
|
4913
|
+
const errors = toolStats.length > 0 ? toolStats.reduce((total, stats) => total + stats.errors, 0) : Math.round(metrics.errorRate * metrics.totalCalls);
|
|
4914
|
+
totalCalls += metrics.totalCalls;
|
|
4915
|
+
callsWithCapturedArgs += metrics.callsWithCapturedArgs;
|
|
4916
|
+
duplicateCalls += duplicates;
|
|
4917
|
+
errorCalls += errors;
|
|
4918
|
+
followedFailures += Math.round(metrics.retryRate * errors);
|
|
4919
|
+
for (const [name, stats] of Object.entries(metrics.byTool)) {
|
|
4920
|
+
const aggregate = byTool.get(name) ?? {
|
|
4921
|
+
calls: 0,
|
|
4922
|
+
captured: 0,
|
|
4923
|
+
duplicates: 0,
|
|
4924
|
+
errors: 0,
|
|
4925
|
+
latencyMs: 0
|
|
4926
|
+
};
|
|
4927
|
+
aggregate.calls += stats.calls;
|
|
4928
|
+
aggregate.captured += stats.callsWithCapturedArgs;
|
|
4929
|
+
aggregate.duplicates += stats.duplicates;
|
|
4930
|
+
aggregate.errors += stats.errors;
|
|
4931
|
+
aggregate.latencyMs += stats.avgLatencyMs * stats.calls;
|
|
4932
|
+
byTool.set(name, aggregate);
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4935
|
+
const failureFollowUp = errorCalls > 0 ? `; ${followedFailures}/${errorCalls} failed calls followed by another same-tool call (${(followedFailures / errorCalls * 100).toFixed(0)}%)` : "";
|
|
4936
|
+
const duplicateSummary = callsWithCapturedArgs > 0 ? `${duplicateCalls}/${callsWithCapturedArgs} captured calls repeated exactly` : "exact repeats unavailable";
|
|
3878
4937
|
lines.push(
|
|
3879
|
-
`- **Tool use
|
|
4938
|
+
`- **Tool use:** ${totalCalls} calls across ${runsWithTools.length}/${pr.toolUse.length} traces; ${callsWithCapturedArgs}/${totalCalls} arguments captured; ${duplicateSummary}; ${errorCalls}/${totalCalls} failed${failureFollowUp}`
|
|
3880
4939
|
);
|
|
4940
|
+
if (byTool.size > 0) {
|
|
4941
|
+
lines.push("");
|
|
4942
|
+
lines.push("| Tool | Calls | Args captured | Exact repeats | Errors | Mean latency |");
|
|
4943
|
+
lines.push("|---|---:|---:|---:|---:|---:|");
|
|
4944
|
+
for (const [name, stats] of [...byTool].sort(
|
|
4945
|
+
(left, right) => right[1].calls - left[1].calls || left[0].localeCompare(right[0])
|
|
4946
|
+
)) {
|
|
4947
|
+
lines.push(
|
|
4948
|
+
`| \`${tableCell(name)}\` | ${stats.calls} | ${stats.captured}/${stats.calls} | ${stats.duplicates}/${stats.captured} | ${stats.errors}/${stats.calls} | ${(stats.latencyMs / stats.calls).toFixed(1)}ms |`
|
|
4949
|
+
);
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
3881
4952
|
}
|
|
3882
4953
|
lines.push("");
|
|
3883
4954
|
return lines.join("\n");
|
|
@@ -3931,14 +5002,41 @@ function renderReactions(rr) {
|
|
|
3931
5002
|
}
|
|
3932
5003
|
function renderAdoption(ar) {
|
|
3933
5004
|
const lines = ["## skill & subagent adoption (deterministic)", ""];
|
|
5005
|
+
const scopeParts = [`${ar.identifiedSessionCount} identified session(s)`];
|
|
5006
|
+
if (ar.unassignedTraceCount > 0) {
|
|
5007
|
+
scopeParts.push(`${ar.unassignedTraceCount} trace(s) without a single stable session identity`);
|
|
5008
|
+
}
|
|
5009
|
+
lines.push(`- **Scope:** ${scopeParts.join(" + ")}`);
|
|
5010
|
+
if (ar.sessionIdentityConflicts.length > 0) {
|
|
5011
|
+
const displayed = ar.sessionIdentityConflicts.slice(0, SOURCE_DISPLAY_LIMIT);
|
|
5012
|
+
const details = displayed.map((conflict) => `\`${conflict.traceId}\` (${conflict.sessionIds.map((id) => `\`${id}\``).join(", ")})`).join("; ");
|
|
5013
|
+
const omitted = ar.sessionIdentityConflicts.length - displayed.length;
|
|
5014
|
+
lines.push(
|
|
5015
|
+
`- **Conflicting session identity:** ${ar.sessionIdentityConflicts.length} trace(s): ${details}${omitted > 0 ? `; ${omitted} more omitted from this report` : ""}`
|
|
5016
|
+
);
|
|
5017
|
+
}
|
|
5018
|
+
lines.push(
|
|
5019
|
+
"- **Prompt, tools, MCP, hooks, and full agent profile:** not assessed; no baseline profile was supplied."
|
|
5020
|
+
);
|
|
5021
|
+
if (ar.skillPenetration === null) {
|
|
5022
|
+
const label = ar.skillTelemetryStatus === "unsupported" ? "uncaptured/unsupported" : "uncaptured/unknown";
|
|
5023
|
+
lines.push(
|
|
5024
|
+
`- **Explicit skill invocation rate:** ${label} (${ar.skillTelemetrySessions}/${ar.executionGroupCount} observed group(s) expose a dedicated Skill event)`
|
|
5025
|
+
);
|
|
5026
|
+
} else {
|
|
5027
|
+
const excluded = ar.executionGroupCount - ar.skillTelemetrySessions;
|
|
5028
|
+
lines.push(
|
|
5029
|
+
`- **Explicit skill invocation rate:** ${(ar.skillPenetration * 100).toFixed(0)}% (${ar.sessionsWithSkill}/${ar.skillTelemetrySessions} measurable group(s)${excluded > 0 ? `; ${excluded} unmeasurable group(s) excluded` : ""})`
|
|
5030
|
+
);
|
|
5031
|
+
}
|
|
3934
5032
|
lines.push(
|
|
3935
|
-
`- **
|
|
5033
|
+
`- **Materialized skill catalogs/instructions:** ${ar.sessionsWithMaterializedSkills}/${ar.executionGroupCount} observed group(s) \xB7 **SKILL.md tool references:** ${ar.sessionsWithSkillFileReference}/${ar.executionGroupCount} observed group(s)`
|
|
3936
5034
|
);
|
|
3937
5035
|
lines.push(
|
|
3938
5036
|
`- **Explicit skill invocations:** ${ar.totalSkillInvocations} (Skill tool spans) \xB7 **Loop-dispatched runs:** ${ar.totalLoopDispatchedRuns} (from ${ar.skillRunFilesRead} \`.evolve/skill-runs.jsonl\` file(s))`
|
|
3939
5037
|
);
|
|
3940
5038
|
lines.push(
|
|
3941
|
-
`- **Subagent spawns:** ${ar.totalSubagentSpawns} across ${ar.sessionsWithSubagent}
|
|
5039
|
+
`- **Subagent spawns observed:** ${ar.totalSubagentSpawns} across ${ar.sessionsWithSubagent} observed group(s); capture completeness depends on source telemetry.`
|
|
3942
5040
|
);
|
|
3943
5041
|
lines.push("");
|
|
3944
5042
|
const skillRows = Object.entries(ar.skillInvocations).sort((a, b) => b[1] - a[1]);
|
|
@@ -3981,9 +5079,6 @@ var severityRank2 = {
|
|
|
3981
5079
|
low: 3,
|
|
3982
5080
|
info: 4
|
|
3983
5081
|
};
|
|
3984
|
-
function slug(input) {
|
|
3985
|
-
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72) || "item";
|
|
3986
|
-
}
|
|
3987
5082
|
function json(value) {
|
|
3988
5083
|
return `${JSON.stringify(value, null, 2)}
|
|
3989
5084
|
`;
|
|
@@ -3996,13 +5091,76 @@ function evidence(kind, uri, excerpt) {
|
|
|
3996
5091
|
return excerpt ? { kind, uri, excerpt } : { kind, uri };
|
|
3997
5092
|
}
|
|
3998
5093
|
function totalReactionSignals(reactions) {
|
|
3999
|
-
return Object.values(reactions.signals).reduce((sum,
|
|
5094
|
+
return Object.values(reactions.signals).reduce((sum, count2) => sum + count2, 0);
|
|
4000
5095
|
}
|
|
4001
|
-
function
|
|
4002
|
-
|
|
5096
|
+
function traceCoverageFinding(pipelines, adoption) {
|
|
5097
|
+
const toolCalls = pipelines.toolUse.reduce((total, run) => total + run.totalCalls, 0);
|
|
5098
|
+
const capturedToolCalls = pipelines.toolUse.reduce(
|
|
5099
|
+
(total, run) => total + run.callsWithCapturedArgs,
|
|
5100
|
+
0
|
|
5101
|
+
);
|
|
5102
|
+
const missingToolArgs = toolCalls - capturedToolCalls;
|
|
5103
|
+
const traces = pipelines.toolUse.length;
|
|
5104
|
+
const missingSessionIds = adoption.unassignedTraceCount;
|
|
5105
|
+
const sessionIdentityConflicts = adoption.sessionIdentityConflicts;
|
|
5106
|
+
const unmeasurableSkillGroups = adoption.executionGroupCount - adoption.skillTelemetrySessions;
|
|
5107
|
+
if (missingToolArgs === 0 && missingSessionIds === 0 && unmeasurableSkillGroups === 0) {
|
|
5108
|
+
return void 0;
|
|
5109
|
+
}
|
|
5110
|
+
const parts = [];
|
|
5111
|
+
const refs = [];
|
|
5112
|
+
if (missingToolArgs > 0) {
|
|
5113
|
+
parts.push(`${missingToolArgs}/${toolCalls} tool call(s) lacked comparable arguments`);
|
|
5114
|
+
refs.push(evidence("metric", "coverage.tool_arguments", `${capturedToolCalls}/${toolCalls} captured`));
|
|
5115
|
+
}
|
|
5116
|
+
if (missingSessionIds > 0) {
|
|
5117
|
+
parts.push(`${missingSessionIds}/${traces} trace(s) lacked a single stable session identity`);
|
|
5118
|
+
refs.push(evidence("metric", "coverage.session_identity", `${traces - missingSessionIds}/${traces} identified`));
|
|
5119
|
+
}
|
|
5120
|
+
if (sessionIdentityConflicts.length > 0) {
|
|
5121
|
+
refs.push(evidence(
|
|
5122
|
+
"span",
|
|
5123
|
+
"coverage.session_identity_conflicts",
|
|
5124
|
+
sessionIdentityConflicts.map((conflict) => conflict.traceId).join(", ")
|
|
5125
|
+
));
|
|
5126
|
+
}
|
|
5127
|
+
if (unmeasurableSkillGroups > 0) {
|
|
5128
|
+
parts.push(`${unmeasurableSkillGroups}/${adoption.executionGroupCount} execution group(s) lacked measurable skill events`);
|
|
5129
|
+
refs.push(evidence(
|
|
5130
|
+
"metric",
|
|
5131
|
+
"coverage.skill_events",
|
|
5132
|
+
`${adoption.skillTelemetrySessions}/${adoption.executionGroupCount} measurable`
|
|
5133
|
+
));
|
|
5134
|
+
}
|
|
5135
|
+
const argumentCoverage = toolCalls === 0 ? 1 : capturedToolCalls / toolCalls;
|
|
5136
|
+
const sessionCoverage = traces === 0 ? 1 : (traces - missingSessionIds) / traces;
|
|
5137
|
+
const severity = argumentCoverage < 0.5 || sessionCoverage < 0.5 ? "high" : argumentCoverage < 0.9 || sessionCoverage < 0.9 || unmeasurableSkillGroups > 0 ? "medium" : "low";
|
|
5138
|
+
return makeFinding({
|
|
5139
|
+
analyst_id: "traces-deterministic",
|
|
5140
|
+
area: "instrumentation",
|
|
5141
|
+
claim: `Trace capture is incomplete: ${parts.join("; ")}`,
|
|
5142
|
+
severity,
|
|
5143
|
+
rationale: "Missing identity or agent-profile evidence prevents reliable cross-run comparison and can hide tool, prompt, skill, or subagent failures.",
|
|
5144
|
+
evidence_refs: refs,
|
|
5145
|
+
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.",
|
|
5146
|
+
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.",
|
|
5147
|
+
confidence: 1,
|
|
5148
|
+
metadata: {
|
|
5149
|
+
source: "traces.coverage",
|
|
5150
|
+
toolCalls,
|
|
5151
|
+
capturedToolCalls,
|
|
5152
|
+
traces,
|
|
5153
|
+
missingSessionIds,
|
|
5154
|
+
sessionIdentityConflictCount: sessionIdentityConflicts.length,
|
|
5155
|
+
skillTelemetryGroups: adoption.skillTelemetrySessions,
|
|
5156
|
+
executionGroups: adoption.executionGroupCount
|
|
5157
|
+
}
|
|
5158
|
+
});
|
|
4003
5159
|
}
|
|
4004
5160
|
function deterministicFindings(pipelines, reactions, adoption) {
|
|
4005
5161
|
const findings = [];
|
|
5162
|
+
const coverage = traceCoverageFinding(pipelines, adoption);
|
|
5163
|
+
if (coverage) findings.push(coverage);
|
|
4006
5164
|
const loops = pipelines.stuckLoops.findings;
|
|
4007
5165
|
if (loops.length > 0) {
|
|
4008
5166
|
const top = [...loops].sort((a, b) => b.occurrences - a.occurrences)[0];
|
|
@@ -4022,19 +5180,34 @@ function deterministicFindings(pipelines, reactions, adoption) {
|
|
|
4022
5180
|
metadata: { source: "traces.pipeline.stuckLoopView", loopCount: loops.length }
|
|
4023
5181
|
}));
|
|
4024
5182
|
}
|
|
4025
|
-
const
|
|
4026
|
-
if (
|
|
5183
|
+
const failedRuns = pipelines.failureClusters.totalFailures;
|
|
5184
|
+
if (failedRuns > 0) {
|
|
5185
|
+
const top = pipelines.failureClusters.clusters[0];
|
|
5186
|
+
const failureRate = failedRuns / Math.max(1, pipelines.failureClusters.totalRuns);
|
|
4027
5187
|
findings.push(makeFinding({
|
|
4028
5188
|
analyst_id: "traces-deterministic",
|
|
4029
|
-
area: "
|
|
4030
|
-
claim: `${
|
|
4031
|
-
severity:
|
|
4032
|
-
rationale: "
|
|
4033
|
-
evidence_refs: [
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
5189
|
+
area: "reliability",
|
|
5190
|
+
claim: `${failedRuns}/${pipelines.failureClusters.totalRuns} run(s) had execution errors`,
|
|
5191
|
+
severity: failureRate >= 0.1 ? "high" : "medium",
|
|
5192
|
+
rationale: "Execution errors consume time and tokens, and sparse error telemetry can hide whether the agent recovered or repeated the same failure.",
|
|
5193
|
+
evidence_refs: [
|
|
5194
|
+
evidence(
|
|
5195
|
+
"metric",
|
|
5196
|
+
`pipelines.failure_cluster.${top?.failureClass ?? "unknown"}`,
|
|
5197
|
+
top ? `${top.runCount} run(s); example: ${top.exampleError ?? "error details not captured"}` : `${failedRuns} failed run(s)`
|
|
5198
|
+
),
|
|
5199
|
+
...top?.exampleRunId ? [evidence("span", `trace:${top.exampleRunId}`, top.exampleError)] : []
|
|
5200
|
+
],
|
|
5201
|
+
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.",
|
|
5202
|
+
validation_plan: "Rerun a comparable trace window and require failed runs and repeated failures in the top cluster to decrease.",
|
|
5203
|
+
confidence: 1,
|
|
5204
|
+
metadata: {
|
|
5205
|
+
source: "agent-eval.failureClusterView",
|
|
5206
|
+
failedRuns,
|
|
5207
|
+
totalRuns: pipelines.failureClusters.totalRuns,
|
|
5208
|
+
topFailureClass: top?.failureClass,
|
|
5209
|
+
exampleRunId: top?.exampleRunId
|
|
5210
|
+
}
|
|
4038
5211
|
}));
|
|
4039
5212
|
}
|
|
4040
5213
|
const corrective = reactions.signals.correction + reactions.signals.frustration + reactions.signals.jargon + reactions.signals.structure;
|
|
@@ -4053,7 +5226,8 @@ function deterministicFindings(pipelines, reactions, adoption) {
|
|
|
4053
5226
|
}));
|
|
4054
5227
|
}
|
|
4055
5228
|
const skillRuns = adoption.totalSkillInvocations + adoption.totalLoopDispatchedRuns;
|
|
4056
|
-
|
|
5229
|
+
const allSessionsMeasurable = adoption.skillTelemetrySessions === adoption.executionGroupCount;
|
|
5230
|
+
if (allSessionsMeasurable && adoption.executionGroupCount > 0 && skillRuns === 0) {
|
|
4057
5231
|
findings.push(makeFinding({
|
|
4058
5232
|
analyst_id: "traces-deterministic",
|
|
4059
5233
|
area: "workflow",
|
|
@@ -4064,7 +5238,7 @@ function deterministicFindings(pipelines, reactions, adoption) {
|
|
|
4064
5238
|
recommended_action: "Create or invoke a narrow skill for the repeated workflow, then let traces compare future sessions against this baseline.",
|
|
4065
5239
|
validation_plan: "Rerun traces and require skill-run attribution to appear for repeated workflows.",
|
|
4066
5240
|
confidence: 0.75,
|
|
4067
|
-
metadata: { source: "traces.adoption",
|
|
5241
|
+
metadata: { source: "traces.adoption", executionGroupCount: adoption.executionGroupCount }
|
|
4068
5242
|
}));
|
|
4069
5243
|
}
|
|
4070
5244
|
return findings;
|
|
@@ -4125,41 +5299,6 @@ function normalizeExternalFindings(results) {
|
|
|
4125
5299
|
}
|
|
4126
5300
|
return out;
|
|
4127
5301
|
}
|
|
4128
|
-
function sourceOf(finding2) {
|
|
4129
|
-
if (finding2.analyst_id.startsWith("external:")) return "external";
|
|
4130
|
-
if (finding2.analyst_id === "traces-deterministic") return "deterministic";
|
|
4131
|
-
return "analyst";
|
|
4132
|
-
}
|
|
4133
|
-
function recommendationTitle(finding2) {
|
|
4134
|
-
if (finding2.recommended_action) return finding2.recommended_action.replace(/\.$/, "");
|
|
4135
|
-
return `Investigate ${finding2.area}: ${finding2.claim}`.replace(/\.$/, "");
|
|
4136
|
-
}
|
|
4137
|
-
function buildRecommendations(findings) {
|
|
4138
|
-
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) => ({
|
|
4139
|
-
schemaVersion: 1,
|
|
4140
|
-
id: `rec-${index + 1}-${slug(finding2.area)}`,
|
|
4141
|
-
severity: finding2.severity,
|
|
4142
|
-
title: recommendationTitle(finding2),
|
|
4143
|
-
action: finding2.recommended_action ?? `Review and fix: ${finding2.claim}`,
|
|
4144
|
-
rationale: finding2.rationale ?? finding2.claim,
|
|
4145
|
-
evidenceRefs: finding2.evidence_refs,
|
|
4146
|
-
findingIds: [finding2.finding_id],
|
|
4147
|
-
validationPlan: finding2.validation_plan ?? "Rerun `traces improve` on a fresh comparable session window and confirm this finding disappears or drops in severity.",
|
|
4148
|
-
source: sourceOf(finding2)
|
|
4149
|
-
}));
|
|
4150
|
-
}
|
|
4151
|
-
function buildClaims(findings) {
|
|
4152
|
-
return findings.map((finding2, index) => ({
|
|
4153
|
-
schemaVersion: 1,
|
|
4154
|
-
id: `claim-${index + 1}-${slug(finding2.area)}`,
|
|
4155
|
-
severity: finding2.severity,
|
|
4156
|
-
text: finding2.claim,
|
|
4157
|
-
area: finding2.area,
|
|
4158
|
-
sourceFindingId: finding2.finding_id,
|
|
4159
|
-
evidenceRefs: finding2.evidence_refs,
|
|
4160
|
-
validationPlan: finding2.validation_plan
|
|
4161
|
-
}));
|
|
4162
|
-
}
|
|
4163
5302
|
function buildEvidenceRows(findings) {
|
|
4164
5303
|
return findings.flatMap((finding2) => finding2.evidence_refs.map((ref) => ({
|
|
4165
5304
|
schemaVersion: 1,
|
|
@@ -4171,31 +5310,10 @@ function buildEvidenceRows(findings) {
|
|
|
4171
5310
|
evidence: ref
|
|
4172
5311
|
})));
|
|
4173
5312
|
}
|
|
4174
|
-
function renderRecommendations(recommendations) {
|
|
4175
|
-
const lines = ["## recommendations", ""];
|
|
4176
|
-
if (recommendations.length === 0) {
|
|
4177
|
-
lines.push("- No recommendations emitted.");
|
|
4178
|
-
lines.push("");
|
|
4179
|
-
return lines.join("\n");
|
|
4180
|
-
}
|
|
4181
|
-
for (const rec of recommendations.slice(0, 10)) {
|
|
4182
|
-
lines.push(`### ${rec.severity.toUpperCase()} \u2014 ${rec.title}`);
|
|
4183
|
-
lines.push("");
|
|
4184
|
-
lines.push(`- **Action:** ${rec.action}`);
|
|
4185
|
-
lines.push(`- **Why:** ${rec.rationale}`);
|
|
4186
|
-
lines.push(`- **Check:** ${rec.validationPlan}`);
|
|
4187
|
-
for (const ref of rec.evidenceRefs.slice(0, 3)) {
|
|
4188
|
-
lines.push(`- **Evidence:** ${ref.kind} ${ref.uri}${ref.excerpt ? ` \u2014 \`${ref.excerpt.slice(0, 180)}\`` : ""}`);
|
|
4189
|
-
}
|
|
4190
|
-
lines.push("");
|
|
4191
|
-
}
|
|
4192
|
-
return lines.join("\n");
|
|
4193
|
-
}
|
|
4194
5313
|
function renderFindingPacket(packet, title = "Trace finding packet") {
|
|
4195
5314
|
const lines = [`# ${title}`, ""];
|
|
4196
|
-
lines.push(`${packet.findings.length} finding(s)
|
|
5315
|
+
lines.push(`${packet.findings.length} evidence-backed finding(s) with actions and checks.`);
|
|
4197
5316
|
lines.push("");
|
|
4198
|
-
lines.push(renderRecommendations(packet.recommendations));
|
|
4199
5317
|
if (packet.findings.length > 0) {
|
|
4200
5318
|
lines.push("## findings");
|
|
4201
5319
|
lines.push("");
|
|
@@ -4221,33 +5339,10 @@ function buildTraceFindingPacket(opts) {
|
|
|
4221
5339
|
kind: "traces.finding_packet",
|
|
4222
5340
|
generatedAt,
|
|
4223
5341
|
source: opts.source ?? "traces",
|
|
4224
|
-
findings: opts.findings
|
|
4225
|
-
recommendations: buildRecommendations(opts.findings),
|
|
4226
|
-
claims: buildClaims(opts.findings)
|
|
5342
|
+
findings: opts.findings
|
|
4227
5343
|
};
|
|
4228
5344
|
return { ...packet, report: renderFindingPacket(packet, opts.title) };
|
|
4229
5345
|
}
|
|
4230
|
-
function buildDefaultImprovementProposals(input) {
|
|
4231
|
-
return input.recommendations.slice(0, 5).map((recommendation, index) => {
|
|
4232
|
-
const matchingFindings = input.findings.filter((finding2) => recommendation.findingIds.includes(finding2.finding_id));
|
|
4233
|
-
const evidenceRefs = recommendation.evidenceRefs.length > 0 ? recommendation.evidenceRefs : matchingFindings.flatMap((finding2) => finding2.evidence_refs);
|
|
4234
|
-
return {
|
|
4235
|
-
id: `proposal-${index + 1}-${slug(recommendation.title)}`,
|
|
4236
|
-
title: recommendation.title,
|
|
4237
|
-
description: [
|
|
4238
|
-
recommendation.action,
|
|
4239
|
-
"",
|
|
4240
|
-
`Why: ${recommendation.rationale}`,
|
|
4241
|
-
`Validation: ${recommendation.validationPlan}`,
|
|
4242
|
-
"",
|
|
4243
|
-
"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."
|
|
4244
|
-
].join("\n"),
|
|
4245
|
-
recommendationIds: [recommendation.id],
|
|
4246
|
-
validationCommand: "traces improve --last 1 --dir .traces/improvement",
|
|
4247
|
-
evidenceRefs
|
|
4248
|
-
};
|
|
4249
|
-
});
|
|
4250
|
-
}
|
|
4251
5346
|
function renderExternal(results) {
|
|
4252
5347
|
if (results.length === 0) return "";
|
|
4253
5348
|
const lines = ["## external analyzers", ""];
|
|
@@ -4263,11 +5358,13 @@ function renderInvestigationReport(result, analystResult) {
|
|
|
4263
5358
|
const base = `${renderReport({ ...analystResult, findings: [...result.findings] }, {
|
|
4264
5359
|
harness: result.harness,
|
|
4265
5360
|
sessionCount: result.sessionCount,
|
|
5361
|
+
unassignedTraceCount: result.unassignedTraceCount,
|
|
4266
5362
|
spanCount: result.spanCount,
|
|
4267
5363
|
otlpPath: result.otlpPath,
|
|
4268
|
-
|
|
5364
|
+
execution: result.execution,
|
|
5365
|
+
deterministic: summarizeDeterministicSignals(result.pipelines, result.reactions),
|
|
5366
|
+
sources: result.sources
|
|
4269
5367
|
})}
|
|
4270
|
-
${renderRecommendations(result.recommendations)}
|
|
4271
5368
|
${renderPipelines(result.pipelines)}
|
|
4272
5369
|
${renderReactions(result.reactions)}
|
|
4273
5370
|
${renderAdoption(result.adoption)}`;
|
|
@@ -4344,19 +5441,18 @@ async function runTraceInvestigation(opts) {
|
|
|
4344
5441
|
...deterministicFindings(pipelines, reactions, adoption),
|
|
4345
5442
|
...normalizeExternalFindings(external)
|
|
4346
5443
|
];
|
|
4347
|
-
const recommendations = buildRecommendations(findings);
|
|
4348
|
-
const claims = buildClaims(findings);
|
|
4349
5444
|
const partial = {
|
|
4350
5445
|
schemaVersion: 1,
|
|
4351
5446
|
kind: "traces.investigation",
|
|
4352
5447
|
generatedAt,
|
|
4353
5448
|
harness: opts.harness,
|
|
4354
|
-
sessionCount:
|
|
5449
|
+
sessionCount: adoption.identifiedSessionCount,
|
|
5450
|
+
unassignedTraceCount: adoption.unassignedTraceCount,
|
|
5451
|
+
sources: opts.sources,
|
|
4355
5452
|
spanCount: opts.spans.length,
|
|
4356
5453
|
otlpPath: analysis.otlpPath,
|
|
5454
|
+
execution: analysis.execution,
|
|
4357
5455
|
findings,
|
|
4358
|
-
recommendations,
|
|
4359
|
-
claims,
|
|
4360
5456
|
pipelines,
|
|
4361
5457
|
reactions,
|
|
4362
5458
|
adoption,
|
|
@@ -4388,70 +5484,38 @@ async function runTraceStoreInvestigation(opts) {
|
|
|
4388
5484
|
analystResult
|
|
4389
5485
|
};
|
|
4390
5486
|
}
|
|
4391
|
-
function replayProof(result, proposals) {
|
|
4392
|
-
const deterministic = summarizeDeterministicSignals(result.pipelines, result.reactions);
|
|
4393
|
-
return {
|
|
4394
|
-
schemaVersion: 1,
|
|
4395
|
-
kind: "traces.improvement_replay",
|
|
4396
|
-
generatedAt: result.generatedAt,
|
|
4397
|
-
status: proposals.length > 0 ? "proposal-only" : "not-run",
|
|
4398
|
-
candidateApplied: false,
|
|
4399
|
-
baseline: {
|
|
4400
|
-
spanCount: result.spanCount,
|
|
4401
|
-
findingCount: result.findings.length,
|
|
4402
|
-
recommendationCount: result.recommendations.length,
|
|
4403
|
-
stuckLoops: deterministic.stuckLoops,
|
|
4404
|
-
reactionSignals: deterministic.reactionSignals,
|
|
4405
|
-
toolErrorRuns: deterministic.toolErrorRuns
|
|
4406
|
-
},
|
|
4407
|
-
proposals,
|
|
4408
|
-
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."
|
|
4409
|
-
};
|
|
4410
|
-
}
|
|
4411
5487
|
async function writeTraceImprovementArtifacts(result, outDir) {
|
|
4412
5488
|
const directory = outDir ? resolve(outDir) : await mkdtemp4(join16(tmpdir4(), "traces-improvement-"));
|
|
4413
5489
|
await mkdir(directory, { recursive: true });
|
|
4414
5490
|
const paths = {
|
|
4415
5491
|
directory,
|
|
4416
|
-
|
|
4417
|
-
recommendations: join16(directory, "recommendations.json"),
|
|
4418
|
-
proposals: join16(directory, "proposals.json"),
|
|
5492
|
+
result: join16(directory, "result.json"),
|
|
4419
5493
|
evidence: join16(directory, "evidence.jsonl"),
|
|
4420
|
-
claims: join16(directory, "claims.json"),
|
|
4421
5494
|
report: join16(directory, "report.md"),
|
|
4422
|
-
|
|
5495
|
+
traces: result.otlpPath
|
|
4423
5496
|
};
|
|
5497
|
+
const { report, ...machineResult } = result;
|
|
4424
5498
|
await Promise.all([
|
|
4425
|
-
writeFile4(paths.
|
|
4426
|
-
writeFile4(paths.recommendations, json(result.recommendations), "utf8"),
|
|
4427
|
-
writeFile4(paths.proposals, json(result.proposals), "utf8"),
|
|
5499
|
+
writeFile4(paths.result, json(machineResult), "utf8"),
|
|
4428
5500
|
writeFile4(paths.evidence, jsonl(buildEvidenceRows(result.findings)), "utf8"),
|
|
4429
|
-
writeFile4(paths.
|
|
4430
|
-
writeFile4(paths.report, result.report, "utf8"),
|
|
4431
|
-
writeFile4(paths.replay, json(result.replay), "utf8")
|
|
5501
|
+
writeFile4(paths.report, report, "utf8")
|
|
4432
5502
|
]);
|
|
4433
5503
|
return paths;
|
|
4434
5504
|
}
|
|
4435
|
-
async function
|
|
4436
|
-
const
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
otlpPath: investigation.otlpPath
|
|
4443
|
-
};
|
|
4444
|
-
const proposals = opts.adapter ? await opts.adapter.propose(adapterInput) : buildDefaultImprovementProposals(adapterInput);
|
|
4445
|
-
const replay = replayProof(investigation, proposals);
|
|
5505
|
+
async function runTraceImprovement(opts) {
|
|
5506
|
+
const directory = opts.outDir ? resolve(opts.outDir) : await mkdtemp4(join16(tmpdir4(), "traces-improvement-"));
|
|
5507
|
+
await mkdir(directory, { recursive: true });
|
|
5508
|
+
const investigation = await runTraceInvestigation({
|
|
5509
|
+
...opts,
|
|
5510
|
+
otlpOutPath: opts.otlpOutPath ?? join16(directory, "traces.otlp.jsonl")
|
|
5511
|
+
});
|
|
4446
5512
|
const result = {
|
|
4447
5513
|
...investigation,
|
|
4448
|
-
kind: "traces.improvement"
|
|
4449
|
-
proposals,
|
|
4450
|
-
replay
|
|
5514
|
+
kind: "traces.improvement"
|
|
4451
5515
|
};
|
|
4452
5516
|
return {
|
|
4453
5517
|
...result,
|
|
4454
|
-
artifacts: await writeTraceImprovementArtifacts(result,
|
|
5518
|
+
artifacts: await writeTraceImprovementArtifacts(result, directory)
|
|
4455
5519
|
};
|
|
4456
5520
|
}
|
|
4457
5521
|
async function saveReport(path, report) {
|
|
@@ -4460,7 +5524,7 @@ async function saveReport(path, report) {
|
|
|
4460
5524
|
}
|
|
4461
5525
|
|
|
4462
5526
|
// src/live.ts
|
|
4463
|
-
import { createHash as
|
|
5527
|
+
import { createHash as createHash5 } from "crypto";
|
|
4464
5528
|
var VERIFY_RE = /\b(test|tests|vitest|jest|pytest|go test|cargo test|tsc|typecheck|lint|biome|eslint|build|check:invariants|preflight)\b/i;
|
|
4465
5529
|
var CHANGE_RE = /\b(apply_patch|edit|write|create|delete|rename|replace|update|git apply|sed -i|perl -pi|tee\s|cat\s+>)/i;
|
|
4466
5530
|
var CLAIM_DONE_RE = /\b(done|fixed|complete|completed|implemented|merged|verified|all green|tests pass|ready|works now)\b/i;
|
|
@@ -4480,7 +5544,7 @@ var sleep = (ms, signal) => new Promise((resolve2) => {
|
|
|
4480
5544
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4481
5545
|
});
|
|
4482
5546
|
function hash(parts) {
|
|
4483
|
-
return
|
|
5547
|
+
return createHash5("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
|
|
4484
5548
|
}
|
|
4485
5549
|
function asString(value) {
|
|
4486
5550
|
if (typeof value === "string" && value.trim()) return value;
|
|
@@ -4934,7 +5998,7 @@ async function watchSessions(opts) {
|
|
|
4934
5998
|
}
|
|
4935
5999
|
|
|
4936
6000
|
// src/session-index.ts
|
|
4937
|
-
import { mkdtemp as mkdtemp5, readdir as
|
|
6001
|
+
import { mkdtemp as mkdtemp5, readdir as readdir12, readFile as readFile5, stat as stat12, writeFile as writeFile5 } from "fs/promises";
|
|
4938
6002
|
import { tmpdir as tmpdir5 } from "os";
|
|
4939
6003
|
import { dirname as dirname3, join as join17 } from "path";
|
|
4940
6004
|
function indexRow(record) {
|
|
@@ -4956,7 +6020,8 @@ function indexRow(record) {
|
|
|
4956
6020
|
},
|
|
4957
6021
|
models: record.metrics.models,
|
|
4958
6022
|
tools: record.metrics.tools,
|
|
4959
|
-
signals: record.signals
|
|
6023
|
+
signals: record.signals,
|
|
6024
|
+
execution: record.execution
|
|
4960
6025
|
};
|
|
4961
6026
|
}
|
|
4962
6027
|
function uniqueSorted(values) {
|
|
@@ -5051,7 +6116,7 @@ async function summarizeFile(path, kind) {
|
|
|
5051
6116
|
const base = { kind, path, bytes: Number(s.size), mtimeMs: Number(s.mtimeMs) };
|
|
5052
6117
|
if (!path.endsWith(".md") && !path.endsWith(".jsonl")) return base;
|
|
5053
6118
|
try {
|
|
5054
|
-
const text = await
|
|
6119
|
+
const text = await readFile5(path, "utf8");
|
|
5055
6120
|
const lines = countLines(text);
|
|
5056
6121
|
const jsonl2 = path.endsWith(".jsonl") ? summarizeJsonl(text) : void 0;
|
|
5057
6122
|
return {
|
|
@@ -5070,7 +6135,7 @@ async function walkMarkdown(root, kind, maxFiles = 100) {
|
|
|
5070
6135
|
if (out.length >= maxFiles) return;
|
|
5071
6136
|
let entries;
|
|
5072
6137
|
try {
|
|
5073
|
-
entries = await
|
|
6138
|
+
entries = await readdir12(dir, { withFileTypes: true });
|
|
5074
6139
|
} catch {
|
|
5075
6140
|
return;
|
|
5076
6141
|
}
|
|
@@ -5156,10 +6221,10 @@ async function writeSessionIndexFile(index, outPath) {
|
|
|
5156
6221
|
}
|
|
5157
6222
|
|
|
5158
6223
|
// src/upload-state.ts
|
|
5159
|
-
import {
|
|
5160
|
-
import { mkdir as mkdir2, readFile as readFile15, writeFile as writeFile6 } from "fs/promises";
|
|
6224
|
+
import { mkdir as mkdir2, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
|
|
5161
6225
|
import { homedir as homedir11 } from "os";
|
|
5162
6226
|
import { dirname as dirname4, join as join18 } from "path";
|
|
6227
|
+
import { contentHash } from "@tangle-network/agent-eval";
|
|
5163
6228
|
function stateFile() {
|
|
5164
6229
|
const base = process.env.XDG_STATE_HOME ?? join18(homedir11(), ".local", "state");
|
|
5165
6230
|
return join18(base, "tangle-traces", "uploaded.json");
|
|
@@ -5168,16 +6233,14 @@ function uploadKey(harness, sessionId) {
|
|
|
5168
6233
|
return `${harness}:${sessionId}`;
|
|
5169
6234
|
}
|
|
5170
6235
|
function sessionHash(spans) {
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
}
|
|
5176
|
-
return h.digest("hex").slice(0, 32);
|
|
6236
|
+
return contentHash(spans).slice(0, 32);
|
|
6237
|
+
}
|
|
6238
|
+
function outboundHash(events, identity) {
|
|
6239
|
+
return contentHash({ events, identity }).slice(0, 32);
|
|
5177
6240
|
}
|
|
5178
6241
|
async function loadState() {
|
|
5179
6242
|
try {
|
|
5180
|
-
return JSON.parse(await
|
|
6243
|
+
return JSON.parse(await readFile6(stateFile(), "utf8"));
|
|
5181
6244
|
} catch {
|
|
5182
6245
|
return {};
|
|
5183
6246
|
}
|
|
@@ -5193,7 +6256,7 @@ function alreadyUploaded(state, harness, sessionId, hash2) {
|
|
|
5193
6256
|
}
|
|
5194
6257
|
|
|
5195
6258
|
// src/upload.ts
|
|
5196
|
-
import { readFile as
|
|
6259
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
5197
6260
|
import { createRequire } from "module";
|
|
5198
6261
|
import { hostname } from "os";
|
|
5199
6262
|
import { basename as basename9, join as join19 } from "path";
|
|
@@ -5212,7 +6275,7 @@ async function planUpload(opts = {}) {
|
|
|
5212
6275
|
spans,
|
|
5213
6276
|
redaction: report,
|
|
5214
6277
|
hash: hash2,
|
|
5215
|
-
isNew:
|
|
6278
|
+
isNew: true
|
|
5216
6279
|
});
|
|
5217
6280
|
}
|
|
5218
6281
|
return { items, state, sinceMs: opts.sinceMs };
|
|
@@ -5220,19 +6283,18 @@ async function planUpload(opts = {}) {
|
|
|
5220
6283
|
async function gitBranch(cwd) {
|
|
5221
6284
|
if (!cwd) return void 0;
|
|
5222
6285
|
try {
|
|
5223
|
-
const head = await
|
|
6286
|
+
const head = await readFile7(join19(cwd, ".git", "HEAD"), "utf8");
|
|
5224
6287
|
const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
|
|
5225
6288
|
return m?.[1]?.trim();
|
|
5226
6289
|
} catch {
|
|
5227
6290
|
return void 0;
|
|
5228
6291
|
}
|
|
5229
6292
|
}
|
|
5230
|
-
async function sessionMeta(item
|
|
6293
|
+
async function sessionMeta(item) {
|
|
5231
6294
|
const meta = {
|
|
5232
6295
|
[ATTR.HARNESS]: item.ref.harness,
|
|
5233
6296
|
[ATTR.SESSION_FILE]: basename9(item.ref.path),
|
|
5234
6297
|
[ATTR.HOST]: hostname(),
|
|
5235
|
-
[ATTR.UPLOADED_AT]: uploadedAt,
|
|
5236
6298
|
[ATTR.UPLOADER]: `tangle-traces@${TRACES_VERSION}`,
|
|
5237
6299
|
[ATTR.REDACTION_VERSION]: REDACTION_VERSION,
|
|
5238
6300
|
[ATTR.REDACTION_COUNT]: item.redaction.redactionCount
|
|
@@ -5266,25 +6328,69 @@ function toTraceSpanEvents(spans, rootMeta) {
|
|
|
5266
6328
|
};
|
|
5267
6329
|
});
|
|
5268
6330
|
}
|
|
6331
|
+
var PartialUploadError = class extends Error {
|
|
6332
|
+
sessionKey;
|
|
6333
|
+
accepted;
|
|
6334
|
+
expected;
|
|
6335
|
+
constructor(sessionKey, accepted, expected) {
|
|
6336
|
+
super(`upload: backend accepted ${accepted} of ${expected} spans for ${sessionKey}`);
|
|
6337
|
+
this.name = "PartialUploadError";
|
|
6338
|
+
this.sessionKey = sessionKey;
|
|
6339
|
+
this.accepted = accepted;
|
|
6340
|
+
this.expected = expected;
|
|
6341
|
+
}
|
|
6342
|
+
};
|
|
5269
6343
|
function stripSpanContent(spans) {
|
|
5270
6344
|
return spans.map((s) => {
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
6345
|
+
const attributes = { ...s.attributes };
|
|
6346
|
+
delete attributes.content;
|
|
6347
|
+
for (const key of TOOL_IO_VALUE_KEYS) delete attributes[key];
|
|
6348
|
+
normalizeToolIoAttributes(attributes);
|
|
6349
|
+
const status = s.status.message ? { code: s.status.code } : s.status;
|
|
6350
|
+
return { ...s, attributes, status };
|
|
5274
6351
|
});
|
|
5275
6352
|
}
|
|
5276
|
-
async function
|
|
5277
|
-
let
|
|
6353
|
+
async function prepareOutbound(plan, opts, uploadedAt) {
|
|
6354
|
+
let candidates = plan.items;
|
|
5278
6355
|
if (opts.redactor && !opts.stripContent) {
|
|
5279
|
-
const
|
|
5280
|
-
|
|
6356
|
+
const redactor = opts.redactor;
|
|
6357
|
+
candidates = await Promise.all(
|
|
6358
|
+
candidates.map(async (item) => ({ ...item, spans: (await applyRedactor(item.spans, redactor)).spans }))
|
|
6359
|
+
);
|
|
5281
6360
|
}
|
|
5282
|
-
if (opts.stripContent)
|
|
5283
|
-
|
|
5284
|
-
|
|
6361
|
+
if (opts.stripContent) {
|
|
6362
|
+
candidates = candidates.map((item) => ({ ...item, spans: stripSpanContent(item.spans) }));
|
|
6363
|
+
}
|
|
6364
|
+
const identity = {
|
|
6365
|
+
stripContent: opts.stripContent === true,
|
|
6366
|
+
redactor: opts.stripContent ? null : opts.redactor?.name ?? null,
|
|
6367
|
+
redactionVersion: REDACTION_VERSION
|
|
6368
|
+
};
|
|
6369
|
+
const items = [];
|
|
6370
|
+
for (const item of candidates) {
|
|
6371
|
+
const key = uploadKey(item.ref.harness, item.ref.sessionId);
|
|
6372
|
+
const previous = plan.state[key];
|
|
6373
|
+
const meta = await sessionMeta(item);
|
|
6374
|
+
const eventsAt = (timestamp) => toTraceSpanEvents(item.spans, { ...meta, [ATTR.UPLOADED_AT]: timestamp });
|
|
6375
|
+
let eventTimestamp2 = previous?.uploadedAt ?? uploadedAt;
|
|
6376
|
+
let events = eventsAt(eventTimestamp2);
|
|
6377
|
+
let hash2 = outboundHash(events, identity);
|
|
6378
|
+
if (alreadyUploaded(plan.state, item.ref.harness, item.ref.sessionId, hash2)) continue;
|
|
6379
|
+
if (eventTimestamp2 !== uploadedAt) {
|
|
6380
|
+
eventTimestamp2 = uploadedAt;
|
|
6381
|
+
events = eventsAt(eventTimestamp2);
|
|
6382
|
+
hash2 = outboundHash(events, identity);
|
|
6383
|
+
}
|
|
6384
|
+
items.push({ item, events, hash: hash2 });
|
|
6385
|
+
}
|
|
6386
|
+
return { items, skipped: plan.items.length - items.length };
|
|
6387
|
+
}
|
|
6388
|
+
async function executeUpload(plan, opts = {}) {
|
|
5285
6389
|
const uploadedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6390
|
+
const { items, skipped } = await prepareOutbound(plan, opts, uploadedAt);
|
|
6391
|
+
const redactionCount = items.reduce((n, { item }) => n + item.redaction.redactionCount, 0);
|
|
5286
6392
|
if (opts.dryRun) {
|
|
5287
|
-
const allSpans =
|
|
6393
|
+
const allSpans = items.flatMap(({ item }) => item.spans);
|
|
5288
6394
|
const path = await writeOtlpFile(allSpans, opts.otlpOut);
|
|
5289
6395
|
return {
|
|
5290
6396
|
uploadedSessions: 0,
|
|
@@ -5295,6 +6401,15 @@ async function executeUpload(plan, opts = {}) {
|
|
|
5295
6401
|
otlpPath: path
|
|
5296
6402
|
};
|
|
5297
6403
|
}
|
|
6404
|
+
if (items.length === 0) {
|
|
6405
|
+
return {
|
|
6406
|
+
uploadedSessions: 0,
|
|
6407
|
+
skippedSessions: skipped,
|
|
6408
|
+
acceptedSpans: 0,
|
|
6409
|
+
redactionCount,
|
|
6410
|
+
dryRun: false
|
|
6411
|
+
};
|
|
6412
|
+
}
|
|
5298
6413
|
const client = opts.backend ?? hostedClientFromEnv();
|
|
5299
6414
|
if (!client) {
|
|
5300
6415
|
throw new Error(
|
|
@@ -5303,23 +6418,28 @@ async function executeUpload(plan, opts = {}) {
|
|
|
5303
6418
|
}
|
|
5304
6419
|
const state = plan.state;
|
|
5305
6420
|
let acceptedSpans = 0;
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
const events =
|
|
5309
|
-
const
|
|
6421
|
+
const completed = [];
|
|
6422
|
+
for (const outbound of items) {
|
|
6423
|
+
const { item, events, hash: hash2 } = outbound;
|
|
6424
|
+
const key = uploadKey(item.ref.harness, item.ref.sessionId);
|
|
6425
|
+
const idempotencyKey = `${key}:${hash2}`;
|
|
5310
6426
|
const res = await client.ingestTraces(events, idempotencyKey);
|
|
6427
|
+
if (res.accepted !== events.length) throw new PartialUploadError(key, res.accepted, events.length);
|
|
5311
6428
|
acceptedSpans += res.accepted;
|
|
5312
|
-
|
|
5313
|
-
|
|
6429
|
+
completed.push({ key, item, hash: hash2, spanCount: events.length });
|
|
6430
|
+
opts.log?.(`uploaded ${item.ref.harness} ${item.ref.sessionId.slice(0, 8)} \u2014 ${res.accepted} spans`);
|
|
6431
|
+
}
|
|
6432
|
+
for (const { key, item, hash: hash2, spanCount } of completed) {
|
|
6433
|
+
state[key] = {
|
|
6434
|
+
hash: hash2,
|
|
5314
6435
|
uploadedAt,
|
|
5315
6436
|
harness: item.ref.harness,
|
|
5316
|
-
spanCount
|
|
6437
|
+
spanCount
|
|
5317
6438
|
};
|
|
5318
|
-
opts.log?.(`uploaded ${item.ref.harness} ${item.ref.sessionId.slice(0, 8)} \u2014 ${res.accepted} spans`);
|
|
5319
6439
|
}
|
|
5320
6440
|
await saveState(state);
|
|
5321
6441
|
return {
|
|
5322
|
-
uploadedSessions:
|
|
6442
|
+
uploadedSessions: items.length,
|
|
5323
6443
|
skippedSessions: skipped,
|
|
5324
6444
|
acceptedSpans,
|
|
5325
6445
|
redactionCount,
|
|
@@ -5328,25 +6448,38 @@ async function executeUpload(plan, opts = {}) {
|
|
|
5328
6448
|
}
|
|
5329
6449
|
|
|
5330
6450
|
export {
|
|
6451
|
+
ATTR,
|
|
6452
|
+
INGEST_SOURCE_CLI,
|
|
6453
|
+
DEFAULT_HARNESS,
|
|
6454
|
+
sessionIdFromAttributes,
|
|
6455
|
+
indexSessionIdsByTrace,
|
|
6456
|
+
stampSessionIdentity,
|
|
5331
6457
|
countSkillRunsJsonl,
|
|
5332
6458
|
analyzeAdoption,
|
|
5333
6459
|
span,
|
|
5334
6460
|
toOpenInferenceSpan,
|
|
5335
6461
|
serializeSpans,
|
|
5336
6462
|
writeOtlpFile,
|
|
6463
|
+
summarizeOtlpExecution,
|
|
6464
|
+
summarizeSpanExecution,
|
|
5337
6465
|
analyzeSpans,
|
|
5338
|
-
ATTR,
|
|
5339
|
-
INGEST_SOURCE_CLI,
|
|
5340
|
-
DEFAULT_HARNESS,
|
|
5341
|
-
parseIsoToEpochMs,
|
|
5342
|
-
parseSince,
|
|
5343
|
-
toRuntimeStore,
|
|
5344
|
-
runPipelines,
|
|
5345
6466
|
CONTENT_CAP,
|
|
5346
6467
|
capText,
|
|
5347
6468
|
ACTOR_ATTR,
|
|
5348
6469
|
userPromptSpan,
|
|
6470
|
+
parseIsoToEpochMs,
|
|
6471
|
+
parseSince,
|
|
6472
|
+
toRuntimeStore,
|
|
6473
|
+
runPipelines,
|
|
6474
|
+
recordSessionCorruption,
|
|
6475
|
+
sessionIntegrityAttributes,
|
|
6476
|
+
stampSessionIntegrity,
|
|
6477
|
+
JsonSourceError,
|
|
6478
|
+
readJsonFile,
|
|
5349
6479
|
AmpAdapter,
|
|
6480
|
+
JsonlParseError,
|
|
6481
|
+
readJsonl,
|
|
6482
|
+
takeJsonl,
|
|
5350
6483
|
textIsSynthetic,
|
|
5351
6484
|
looksLikeAgentPrompt,
|
|
5352
6485
|
claudeActor,
|
|
@@ -5374,6 +6507,7 @@ export {
|
|
|
5374
6507
|
stampRepoAttrs,
|
|
5375
6508
|
stampSpanWorkdirRepoAttrs,
|
|
5376
6509
|
parseSession,
|
|
6510
|
+
EmptySessionError,
|
|
5377
6511
|
locateSessions,
|
|
5378
6512
|
scanSessions,
|
|
5379
6513
|
buildPolicyEvidenceRecord,
|
|
@@ -5400,6 +6534,7 @@ export {
|
|
|
5400
6534
|
writeInspectionReportFile,
|
|
5401
6535
|
REACTIONS,
|
|
5402
6536
|
analyzeReactions,
|
|
6537
|
+
CORRUPTION_RECEIPT_DISPLAY_LIMIT,
|
|
5403
6538
|
summarizeDeterministicSignals,
|
|
5404
6539
|
renderReport,
|
|
5405
6540
|
renderPipelines,
|
|
@@ -5411,7 +6546,7 @@ export {
|
|
|
5411
6546
|
runTraceInvestigation,
|
|
5412
6547
|
runTraceStoreInvestigation,
|
|
5413
6548
|
writeTraceImprovementArtifacts,
|
|
5414
|
-
|
|
6549
|
+
runTraceImprovement,
|
|
5415
6550
|
saveReport,
|
|
5416
6551
|
classifyLiveActions,
|
|
5417
6552
|
defaultTraceLiveAnalysts,
|
|
@@ -5427,10 +6562,12 @@ export {
|
|
|
5427
6562
|
writeSessionIndexFile,
|
|
5428
6563
|
uploadKey,
|
|
5429
6564
|
sessionHash,
|
|
6565
|
+
outboundHash,
|
|
5430
6566
|
loadState,
|
|
5431
6567
|
saveState,
|
|
5432
6568
|
alreadyUploaded,
|
|
5433
6569
|
planUpload,
|
|
5434
6570
|
toTraceSpanEvents,
|
|
6571
|
+
PartialUploadError,
|
|
5435
6572
|
executeUpload
|
|
5436
6573
|
};
|