@tangle-network/traces 0.8.27 → 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-EZDMNNW4.js → chunk-5SBXL62V.js} +1970 -854
- 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 {};
|
|
@@ -28,6 +240,28 @@ function subagentTypeOf(input) {
|
|
|
28
240
|
function isSpawnAgentTool(name) {
|
|
29
241
|
return name === "spawn_agent" || name.endsWith("__spawn_agent");
|
|
30
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
|
+
}
|
|
31
265
|
function countSkillRunsJsonl(raw) {
|
|
32
266
|
const out = {};
|
|
33
267
|
for (const line of raw.split("\n")) {
|
|
@@ -71,29 +305,47 @@ async function readLoopRuns(cwds) {
|
|
|
71
305
|
return { counts, filesRead };
|
|
72
306
|
}
|
|
73
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}`;
|
|
74
314
|
const sessions = /* @__PURE__ */ new Set();
|
|
75
315
|
const sessionsWithSkill = /* @__PURE__ */ new Set();
|
|
76
316
|
const sessionsWithSubagent = /* @__PURE__ */ new Set();
|
|
317
|
+
const sessionsWithMaterializedSkills = /* @__PURE__ */ new Set();
|
|
318
|
+
const sessionsWithSkillFileReference = /* @__PURE__ */ new Set();
|
|
77
319
|
const skillInvocations = {};
|
|
78
320
|
const subagentSpawns = {};
|
|
79
321
|
const canonicalSubagentSessions = /* @__PURE__ */ new Set();
|
|
80
322
|
const fallbackSubagents = /* @__PURE__ */ new Map();
|
|
323
|
+
const sessionCapabilities = /* @__PURE__ */ new Map();
|
|
81
324
|
for (const s of spans) {
|
|
82
|
-
|
|
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);
|
|
83
334
|
const tn = toolName(s);
|
|
84
335
|
if (tn === "Skill") {
|
|
336
|
+
sessionCapabilities.set(group, "supported");
|
|
85
337
|
const name = skillNameOf(parseInput(s));
|
|
86
338
|
skillInvocations[name] = (skillInvocations[name] ?? 0) + 1;
|
|
87
|
-
sessionsWithSkill.add(
|
|
339
|
+
sessionsWithSkill.add(group);
|
|
88
340
|
} else if (tn === "Task" || tn === "Agent") {
|
|
89
341
|
const type = subagentTypeOf(parseInput(s));
|
|
90
342
|
subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
|
|
91
|
-
sessionsWithSubagent.add(
|
|
92
|
-
canonicalSubagentSessions.add(
|
|
343
|
+
sessionsWithSubagent.add(group);
|
|
344
|
+
canonicalSubagentSessions.add(group);
|
|
93
345
|
} else if (tn && isSpawnAgentTool(tn)) {
|
|
94
|
-
const types = fallbackSubagents.get(
|
|
346
|
+
const types = fallbackSubagents.get(group) ?? [];
|
|
95
347
|
types.push(subagentTypeOf(parseInput(s)));
|
|
96
|
-
fallbackSubagents.set(
|
|
348
|
+
fallbackSubagents.set(group, types);
|
|
97
349
|
}
|
|
98
350
|
}
|
|
99
351
|
for (const [traceId, types] of fallbackSubagents) {
|
|
@@ -107,10 +359,30 @@ async function analyzeAdoption(spans, opts = {}) {
|
|
|
107
359
|
const totalSkillInvocations = Object.values(skillInvocations).reduce((a, b) => a + b, 0);
|
|
108
360
|
const totalSubagentSpawns = Object.values(subagentSpawns).reduce((a, b) => a + b, 0);
|
|
109
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
|
+
}
|
|
110
371
|
return {
|
|
111
|
-
|
|
372
|
+
executionGroupCount: sessions.size,
|
|
373
|
+
identifiedSessionCount: identifiedSessions.size,
|
|
374
|
+
unassignedTraceCount: unassignedTraceIds.size,
|
|
375
|
+
sessionIdentityConflicts: conflicts,
|
|
112
376
|
sessionsWithSkill: sessionsWithSkill.size,
|
|
113
|
-
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,
|
|
114
386
|
skillInvocations,
|
|
115
387
|
totalSkillInvocations,
|
|
116
388
|
subagentSpawns,
|
|
@@ -123,16 +395,27 @@ async function analyzeAdoption(spans, opts = {}) {
|
|
|
123
395
|
}
|
|
124
396
|
|
|
125
397
|
// src/otlp.ts
|
|
398
|
+
import {
|
|
399
|
+
applyLlmSpanOtlpAttributes,
|
|
400
|
+
OPENINFERENCE_SPAN_KIND,
|
|
401
|
+
TOOL_NAME
|
|
402
|
+
} from "@tangle-network/agent-eval/trace-attributes";
|
|
126
403
|
function span(input) {
|
|
127
404
|
const attributes = {
|
|
128
|
-
|
|
405
|
+
[OPENINFERENCE_SPAN_KIND]: input.kind
|
|
129
406
|
};
|
|
130
407
|
if (input.service != null) attributes["service.name"] = input.service;
|
|
131
408
|
if (input.agent != null) attributes["agent.name"] = input.agent;
|
|
132
|
-
if (input.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
});
|
|
136
419
|
if (input.step != null) attributes.step = input.step;
|
|
137
420
|
if (input.content != null && input.content.length > 0) attributes["content"] = input.content;
|
|
138
421
|
if (input.extra) Object.assign(attributes, input.extra);
|
|
@@ -162,7 +445,7 @@ function toOpenInferenceSpan(s) {
|
|
|
162
445
|
span_id: s.span_id,
|
|
163
446
|
parent_span_id: s.parent_span_id ?? "",
|
|
164
447
|
name: s.name,
|
|
165
|
-
kind: typeof a[
|
|
448
|
+
kind: typeof a[OPENINFERENCE_SPAN_KIND] === "string" ? a[OPENINFERENCE_SPAN_KIND] : "CHAIN",
|
|
166
449
|
start_time: s.start_time,
|
|
167
450
|
end_time: s.end_time,
|
|
168
451
|
status: { code: s.status.code, message: s.status.message ?? "" },
|
|
@@ -185,20 +468,56 @@ async function writeOtlpFile(spans, outPath) {
|
|
|
185
468
|
return path;
|
|
186
469
|
}
|
|
187
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
|
+
|
|
188
500
|
// src/analyze.ts
|
|
189
501
|
import { buildDefaultAnalystRegistry } from "@tangle-network/agent-eval/analyst";
|
|
190
502
|
import { OtlpFileTraceStore } from "@tangle-network/agent-eval/traces";
|
|
191
|
-
var
|
|
503
|
+
var GENERATED_TRACE_FILE_CEILING = 512 * 1024 * 1024;
|
|
192
504
|
async function analyzeSpans(spans, opts = {}) {
|
|
193
505
|
if (spans.length === 0) throw new Error("analyzeSpans: no spans to analyze");
|
|
194
506
|
const otlpPath = await writeOtlpFile(spans, opts.otlpOutPath);
|
|
195
507
|
const runId = opts.runId ?? `traces-${Date.now()}`;
|
|
196
|
-
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
|
+
});
|
|
197
516
|
await detStore.ensureIndexed();
|
|
198
517
|
const detRegistry = opts.registry ?? buildDefaultAnalystRegistry({ registry: { log: opts.log } });
|
|
199
518
|
const result = await detRegistry.run(runId, { traceStore: detStore });
|
|
200
519
|
if (opts.ai) {
|
|
201
|
-
const agStore = new OtlpFileTraceStore({ path: otlpPath });
|
|
520
|
+
const agStore = new OtlpFileTraceStore({ path: otlpPath, maxFileBytes: GENERATED_TRACE_FILE_CEILING });
|
|
202
521
|
await agStore.ensureIndexed();
|
|
203
522
|
const agRegistry = buildDefaultAnalystRegistry({
|
|
204
523
|
ai: opts.ai,
|
|
@@ -213,42 +532,32 @@ async function analyzeSpans(spans, opts = {}) {
|
|
|
213
532
|
result.per_analyst.push(...agResult.per_analyst);
|
|
214
533
|
result.total_cost_usd += agResult.total_cost_usd;
|
|
215
534
|
}
|
|
216
|
-
return { otlpPath, result };
|
|
535
|
+
return { otlpPath, execution, result };
|
|
217
536
|
}
|
|
218
537
|
|
|
219
|
-
// src/
|
|
220
|
-
var
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
/** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
|
|
243
|
-
* which collided with `tangle.ingest_source`.) */
|
|
244
|
-
SESSION_FILE: "tangle.session_file",
|
|
245
|
-
UPLOADED_AT: "tangle.uploaded_at",
|
|
246
|
-
UPLOADER: "tangle.uploader",
|
|
247
|
-
REDACTION_VERSION: "redaction.version",
|
|
248
|
-
REDACTION_COUNT: "redaction.count"
|
|
249
|
-
};
|
|
250
|
-
var INGEST_SOURCE_CLI = "cli";
|
|
251
|
-
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
|
+
}
|
|
252
561
|
|
|
253
562
|
// src/time.ts
|
|
254
563
|
function parseIsoToEpochMs(ts) {
|
|
@@ -280,73 +589,238 @@ var KIND = {
|
|
|
280
589
|
};
|
|
281
590
|
async function toRuntimeStore(spans) {
|
|
282
591
|
const store = new InMemoryTraceStore();
|
|
283
|
-
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
|
+
}
|
|
284
612
|
for (const s of spans) {
|
|
285
|
-
if (!runIds.has(s.trace_id)) {
|
|
286
|
-
runIds.add(s.trace_id);
|
|
287
|
-
await store.appendRun({ runId: s.trace_id, scenarioId: "session" });
|
|
288
|
-
}
|
|
289
613
|
const kind = KIND[s.attributes["openinference.span.kind"]] ?? "custom";
|
|
614
|
+
const startedAt = parseIsoToEpochMs(s.start_time);
|
|
615
|
+
const endedAt = parseIsoToEpochMs(s.end_time);
|
|
290
616
|
const base = {
|
|
291
617
|
spanId: s.span_id,
|
|
292
618
|
parentSpanId: s.parent_span_id ?? void 0,
|
|
293
619
|
runId: s.trace_id,
|
|
294
620
|
name: s.name,
|
|
295
|
-
startedAt
|
|
296
|
-
endedAt
|
|
621
|
+
startedAt,
|
|
622
|
+
endedAt,
|
|
297
623
|
status: s.status.code === "ERROR" ? "error" : "ok",
|
|
298
624
|
error: s.status.message,
|
|
299
625
|
attributes: s.attributes
|
|
300
626
|
};
|
|
627
|
+
const tool = kind === "tool" ? toolArgumentsFromAttributes(s.attributes) : void 0;
|
|
301
628
|
const span2 = kind === "tool" ? {
|
|
302
629
|
...base,
|
|
303
630
|
kind: "tool",
|
|
304
631
|
toolName: String(s.attributes["tool.name"] ?? "tool"),
|
|
305
|
-
args:
|
|
632
|
+
args: tool.args,
|
|
633
|
+
argsCaptured: tool.argsCaptured,
|
|
634
|
+
latencyMs: Math.max(0, endedAt - startedAt)
|
|
306
635
|
} : { ...base, kind };
|
|
307
636
|
await store.appendSpan(span2);
|
|
308
637
|
}
|
|
309
|
-
return { store, runIds: [...
|
|
638
|
+
return { store, runIds: [...runs.keys()] };
|
|
310
639
|
}
|
|
311
640
|
|
|
312
641
|
// src/pipelines.ts
|
|
313
642
|
import { computeToolUseMetrics } from "@tangle-network/agent-eval";
|
|
314
|
-
import {
|
|
643
|
+
import {
|
|
644
|
+
failureClusterView,
|
|
645
|
+
stuckLoopView
|
|
646
|
+
} from "@tangle-network/agent-eval/pipelines";
|
|
315
647
|
async function runPipelines(spans, opts = {}) {
|
|
316
648
|
const { store, runIds } = await toRuntimeStore(spans);
|
|
317
649
|
const loopEligible = spans.filter((item) => item.attributes["traces.expected_blocking"] !== true);
|
|
318
650
|
const { store: loopStore } = await toRuntimeStore(loopEligible);
|
|
319
|
-
const stuckLoops = await
|
|
320
|
-
|
|
321
|
-
|
|
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 };
|
|
322
657
|
}
|
|
323
658
|
|
|
324
|
-
// src/
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
+
];
|
|
330
670
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
+
}
|
|
346
820
|
}
|
|
347
821
|
|
|
348
822
|
// src/adapters/amp.ts
|
|
349
|
-
import { readdir
|
|
823
|
+
import { readdir as readdir2, stat } from "fs/promises";
|
|
350
824
|
import { homedir } from "os";
|
|
351
825
|
import { basename, join as join2 } from "path";
|
|
352
826
|
var SERVICE = "amp";
|
|
@@ -365,9 +839,10 @@ var AmpAdapter = class {
|
|
|
365
839
|
async locate(opts = {}) {
|
|
366
840
|
let files;
|
|
367
841
|
try {
|
|
368
|
-
files = await
|
|
369
|
-
} catch {
|
|
370
|
-
return [];
|
|
842
|
+
files = await readdir2(this.threadsRoot());
|
|
843
|
+
} catch (error) {
|
|
844
|
+
if (isMissingPathError(error)) return [];
|
|
845
|
+
throw error;
|
|
371
846
|
}
|
|
372
847
|
const refs = [];
|
|
373
848
|
for (const f of files) {
|
|
@@ -376,8 +851,9 @@ var AmpAdapter = class {
|
|
|
376
851
|
let st;
|
|
377
852
|
try {
|
|
378
853
|
st = await stat(path);
|
|
379
|
-
} catch {
|
|
380
|
-
continue;
|
|
854
|
+
} catch (error) {
|
|
855
|
+
if (isMissingPathError(error)) continue;
|
|
856
|
+
throw error;
|
|
381
857
|
}
|
|
382
858
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
383
859
|
if (opts.cwd) continue;
|
|
@@ -386,12 +862,7 @@ var AmpAdapter = class {
|
|
|
386
862
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
387
863
|
}
|
|
388
864
|
async parse(ref) {
|
|
389
|
-
|
|
390
|
-
try {
|
|
391
|
-
thread = JSON.parse(await readFile2(ref.path, "utf8"));
|
|
392
|
-
} catch {
|
|
393
|
-
return [];
|
|
394
|
-
}
|
|
865
|
+
const thread = await readJsonFile(ref.path);
|
|
395
866
|
const traceId = thread.id ?? ref.sessionId;
|
|
396
867
|
const rootId = `root:${traceId}`;
|
|
397
868
|
const start = new Date(thread.created ?? 0).toISOString();
|
|
@@ -423,7 +894,6 @@ var AmpAdapter = class {
|
|
|
423
894
|
}
|
|
424
895
|
} else {
|
|
425
896
|
const u = m.usage;
|
|
426
|
-
const inputTokens2 = u ? (u.inputTokens ?? 0) + (u.cacheReadInputTokens ?? 0) + (u.cacheCreationInputTokens ?? 0) || null : null;
|
|
427
897
|
spans.push(
|
|
428
898
|
span({
|
|
429
899
|
traceId,
|
|
@@ -435,8 +905,10 @@ var AmpAdapter = class {
|
|
|
435
905
|
service: SERVICE,
|
|
436
906
|
agent: SERVICE,
|
|
437
907
|
model: u?.model ?? null,
|
|
438
|
-
inputTokens:
|
|
908
|
+
inputTokens: u?.inputTokens ?? null,
|
|
439
909
|
outputTokens: u?.outputTokens ?? null,
|
|
910
|
+
cachedInputTokens: u?.cacheReadInputTokens ?? null,
|
|
911
|
+
cacheWriteInputTokens: u?.cacheCreationInputTokens ?? null,
|
|
440
912
|
step,
|
|
441
913
|
content: textOf(m.content) || null
|
|
442
914
|
})
|
|
@@ -456,14 +928,17 @@ var AmpAdapter = class {
|
|
|
456
928
|
agent: SERVICE,
|
|
457
929
|
tool: b.name,
|
|
458
930
|
step,
|
|
459
|
-
|
|
931
|
+
extra: toolIoAttributes({ input: b.input })
|
|
460
932
|
});
|
|
461
933
|
spans.push(t);
|
|
462
934
|
if (b.id) toolByUseId.set(b.id, t);
|
|
463
935
|
step += 1;
|
|
464
936
|
} else if (b.type === "tool_result" && b.tool_use_id) {
|
|
465
937
|
const t = toolByUseId.get(b.tool_use_id);
|
|
466
|
-
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
|
+
}
|
|
467
942
|
}
|
|
468
943
|
}
|
|
469
944
|
}
|
|
@@ -471,6 +946,108 @@ var AmpAdapter = class {
|
|
|
471
946
|
}
|
|
472
947
|
};
|
|
473
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
|
+
|
|
474
1051
|
// src/adapters/actor.ts
|
|
475
1052
|
var CMD_MARKERS = [
|
|
476
1053
|
"<command-name>",
|
|
@@ -564,26 +1141,10 @@ function classifyReaction(text) {
|
|
|
564
1141
|
var CORRECTIVE_REACTIONS = ["correction", "frustration", "jargon", "structure"];
|
|
565
1142
|
|
|
566
1143
|
// src/adapters/claude.ts
|
|
567
|
-
import { readdir as
|
|
1144
|
+
import { readdir as readdir3, stat as stat2 } from "fs/promises";
|
|
568
1145
|
import { homedir as homedir2 } from "os";
|
|
569
1146
|
import { basename as basename2, join as join3 } from "path";
|
|
570
1147
|
var SERVICE2 = "claude-code";
|
|
571
|
-
function parseLines(raw) {
|
|
572
|
-
const out = [];
|
|
573
|
-
for (const line of raw.split("\n")) {
|
|
574
|
-
if (!line) continue;
|
|
575
|
-
try {
|
|
576
|
-
out.push(JSON.parse(line));
|
|
577
|
-
} catch {
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
return out;
|
|
581
|
-
}
|
|
582
|
-
function inputTokens(u) {
|
|
583
|
-
if (!u) return null;
|
|
584
|
-
const v = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
|
|
585
|
-
return v > 0 ? v : null;
|
|
586
|
-
}
|
|
587
1148
|
function asBlocks(content) {
|
|
588
1149
|
return Array.isArray(content) ? content : [];
|
|
589
1150
|
}
|
|
@@ -600,96 +1161,104 @@ function stringifyToolResult(content) {
|
|
|
600
1161
|
}
|
|
601
1162
|
return "";
|
|
602
1163
|
}
|
|
603
|
-
function
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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({
|
|
615
1225
|
traceId: ctx.traceId,
|
|
616
|
-
spanId:
|
|
1226
|
+
spanId: `${ctx.idPrefix}${uid}:user`,
|
|
617
1227
|
parentSpanId: ctx.rootParent,
|
|
618
|
-
name: "llm.turn",
|
|
619
|
-
kind: "LLM",
|
|
620
1228
|
startTime: ts,
|
|
621
1229
|
service: SERVICE2,
|
|
622
1230
|
agent: ctx.agent,
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
step,
|
|
627
|
-
content: textOf2(ev.message.content) || null
|
|
1231
|
+
step: state.step,
|
|
1232
|
+
content: prompt,
|
|
1233
|
+
actor
|
|
628
1234
|
})
|
|
629
1235
|
);
|
|
630
|
-
step += 1;
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
startTime: ts,
|
|
641
|
-
service: SERVICE2,
|
|
642
|
-
agent: ctx.agent,
|
|
643
|
-
tool: block.name,
|
|
644
|
-
step,
|
|
645
|
-
content: block.input != null ? JSON.stringify(block.input) : null
|
|
646
|
-
});
|
|
647
|
-
spans.push(toolSpan);
|
|
648
|
-
if (block.id) toolSpanByUseId.set(block.id, toolSpan);
|
|
649
|
-
toolIdx += 1;
|
|
650
|
-
step += 1;
|
|
651
|
-
}
|
|
652
|
-
} else if (ev.type === "user" && ev.message) {
|
|
653
|
-
const prompt = textOf2(ev.message.content);
|
|
654
|
-
if (prompt) {
|
|
655
|
-
const actor = claudeActor({
|
|
656
|
-
text: prompt,
|
|
657
|
-
isSidechain: ev.isSidechain,
|
|
658
|
-
userType: ev.userType ?? null,
|
|
659
|
-
isFirstUserTurn: !sawUserTurn
|
|
660
|
-
});
|
|
661
|
-
sawUserTurn = true;
|
|
662
|
-
spans.push(
|
|
663
|
-
userPromptSpan({
|
|
664
|
-
traceId: ctx.traceId,
|
|
665
|
-
spanId: `${ctx.idPrefix}${uid}:user`,
|
|
666
|
-
parentSpanId: ctx.rootParent,
|
|
667
|
-
startTime: ts,
|
|
668
|
-
service: SERVICE2,
|
|
669
|
-
agent: ctx.agent,
|
|
670
|
-
step,
|
|
671
|
-
content: prompt,
|
|
672
|
-
actor
|
|
673
|
-
})
|
|
674
|
-
);
|
|
675
|
-
step += 1;
|
|
676
|
-
}
|
|
677
|
-
for (const block of asBlocks(ev.message.content)) {
|
|
678
|
-
if (block.type !== "tool_result" || !block.tool_use_id) continue;
|
|
679
|
-
backfillResult(toolSpanByUseId.get(block.tool_use_id), ts, block.is_error === true, stringifyToolResult(block.content));
|
|
680
|
-
}
|
|
681
|
-
} else if (ev.type === "attachment" && ev.attachment?.type === "tool_result" && ev.attachment.toolUseID) {
|
|
682
|
-
const err = typeof ev.attachment.exitCode === "number" && ev.attachment.exitCode !== 0;
|
|
683
|
-
backfillResult(toolSpanByUseId.get(ev.attachment.toolUseID), ts, err, ev.attachment.stderr ?? "");
|
|
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
|
+
);
|
|
684
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 ?? "");
|
|
685
1250
|
}
|
|
686
|
-
return { spans, toolSpanByUseId, nextStep: step };
|
|
687
1251
|
}
|
|
688
|
-
function
|
|
1252
|
+
function finishClaudeStream(state) {
|
|
1253
|
+
return { spans: state.spans, toolSpanByUseId: state.toolSpanByUseId, nextStep: state.step };
|
|
1254
|
+
}
|
|
1255
|
+
function backfillResult(s, endTime, isError, output) {
|
|
689
1256
|
if (!s) return;
|
|
690
1257
|
s.end_time = endTime;
|
|
691
1258
|
const code = isError ? "ERROR" : "OK";
|
|
692
1259
|
s.status = { code };
|
|
1260
|
+
recordToolOutput(s, output);
|
|
1261
|
+
const message = stringifyToolResult(output);
|
|
693
1262
|
if (isError && message) s.status.message = message.slice(0, 500);
|
|
694
1263
|
}
|
|
695
1264
|
var ClaudeAdapter = class {
|
|
@@ -702,18 +1271,20 @@ var ClaudeAdapter = class {
|
|
|
702
1271
|
const root = this.root();
|
|
703
1272
|
let projectDirs;
|
|
704
1273
|
try {
|
|
705
|
-
projectDirs = await
|
|
706
|
-
} catch {
|
|
707
|
-
return [];
|
|
1274
|
+
projectDirs = await readdir3(root);
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
if (isMissingPathError(error)) return [];
|
|
1277
|
+
throw error;
|
|
708
1278
|
}
|
|
709
1279
|
const refs = [];
|
|
710
1280
|
for (const dir of projectDirs) {
|
|
711
1281
|
const dirPath = join3(root, dir);
|
|
712
1282
|
let files;
|
|
713
1283
|
try {
|
|
714
|
-
files = await
|
|
715
|
-
} catch {
|
|
716
|
-
continue;
|
|
1284
|
+
files = await readdir3(dirPath);
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
if (isMissingPathError(error)) continue;
|
|
1287
|
+
throw error;
|
|
717
1288
|
}
|
|
718
1289
|
for (const f of files) {
|
|
719
1290
|
if (!f.endsWith(".jsonl")) continue;
|
|
@@ -721,8 +1292,9 @@ var ClaudeAdapter = class {
|
|
|
721
1292
|
let st;
|
|
722
1293
|
try {
|
|
723
1294
|
st = await stat2(path);
|
|
724
|
-
} catch {
|
|
725
|
-
continue;
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
if (isMissingPathError(error)) continue;
|
|
1297
|
+
throw error;
|
|
726
1298
|
}
|
|
727
1299
|
if (!st.isFile()) continue;
|
|
728
1300
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
@@ -733,11 +1305,37 @@ var ClaudeAdapter = class {
|
|
|
733
1305
|
}
|
|
734
1306
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
735
1307
|
}
|
|
736
|
-
async parse(ref) {
|
|
737
|
-
const
|
|
738
|
-
const
|
|
739
|
-
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;
|
|
740
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
|
+
}
|
|
741
1339
|
const spans = [
|
|
742
1340
|
span({
|
|
743
1341
|
traceId,
|
|
@@ -745,55 +1343,53 @@ var ClaudeAdapter = class {
|
|
|
745
1343
|
parentSpanId: null,
|
|
746
1344
|
name: "session",
|
|
747
1345
|
kind: "AGENT",
|
|
748
|
-
startTime:
|
|
749
|
-
endTime:
|
|
1346
|
+
startTime: firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1347
|
+
endTime: lastTimestamp,
|
|
750
1348
|
service: SERVICE2,
|
|
751
1349
|
agent: SERVICE2
|
|
752
|
-
})
|
|
1350
|
+
}),
|
|
1351
|
+
...main.spans
|
|
753
1352
|
];
|
|
754
|
-
|
|
755
|
-
traceId,
|
|
756
|
-
agent: SERVICE2,
|
|
757
|
-
startStep: 0,
|
|
758
|
-
idPrefix: "",
|
|
759
|
-
rootParent: rootId
|
|
760
|
-
});
|
|
761
|
-
spans.push(...main.spans);
|
|
762
|
-
await this.foldSubagents(ref, traceId, main, spans);
|
|
1353
|
+
await this.foldSubagents(ref, traceId, main, spans, options);
|
|
763
1354
|
return spans;
|
|
764
1355
|
}
|
|
765
1356
|
/** Parse `<session>/subagents/agent-*.jsonl`, parenting each under its Agent call. */
|
|
766
|
-
async foldSubagents(ref, traceId, main, out) {
|
|
1357
|
+
async foldSubagents(ref, traceId, main, out, options) {
|
|
767
1358
|
const subDir = join3(ref.path.replace(/\.jsonl$/, ""), "subagents");
|
|
768
1359
|
let files;
|
|
769
1360
|
try {
|
|
770
|
-
files = await
|
|
771
|
-
} catch {
|
|
772
|
-
return;
|
|
1361
|
+
files = await readdir3(subDir);
|
|
1362
|
+
} catch (error) {
|
|
1363
|
+
if (isMissingPathError(error)) return;
|
|
1364
|
+
throw error;
|
|
773
1365
|
}
|
|
774
1366
|
let step = main.nextStep;
|
|
775
|
-
for (const
|
|
776
|
-
if (!
|
|
777
|
-
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`);
|
|
778
1371
|
let meta = {};
|
|
779
1372
|
try {
|
|
780
|
-
meta =
|
|
781
|
-
} catch {
|
|
1373
|
+
meta = await readJsonFile(metaPath);
|
|
1374
|
+
} catch (error) {
|
|
1375
|
+
if (!isMissingJsonSource(error)) throw error;
|
|
782
1376
|
}
|
|
783
1377
|
const parent = meta.toolUseId && main.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
|
|
784
|
-
|
|
785
|
-
try {
|
|
786
|
-
raw = await readFile3(join3(subDir, f), "utf8");
|
|
787
|
-
} catch {
|
|
788
|
-
continue;
|
|
789
|
-
}
|
|
790
|
-
const parsed = parseClaudeStream(parseLines(raw), {
|
|
1378
|
+
const ctx = {
|
|
791
1379
|
traceId,
|
|
792
1380
|
agent: meta.agentType ? `subagent:${meta.agentType}` : "subagent",
|
|
793
1381
|
startStep: step,
|
|
794
1382
|
idPrefix: `${hash2}:`,
|
|
795
1383
|
rootParent: parent
|
|
796
|
-
}
|
|
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);
|
|
797
1393
|
out.push(...parsed.spans);
|
|
798
1394
|
step = parsed.nextStep;
|
|
799
1395
|
}
|
|
@@ -801,21 +1397,11 @@ var ClaudeAdapter = class {
|
|
|
801
1397
|
};
|
|
802
1398
|
|
|
803
1399
|
// src/adapters/codex.ts
|
|
804
|
-
import { readdir as
|
|
1400
|
+
import { readdir as readdir4, stat as stat3 } from "fs/promises";
|
|
805
1401
|
import { homedir as homedir3 } from "os";
|
|
806
1402
|
import { basename as basename3, join as join4 } from "path";
|
|
807
1403
|
var SERVICE3 = "codex";
|
|
808
|
-
|
|
809
|
-
const out = [];
|
|
810
|
-
for (const line of raw.split("\n")) {
|
|
811
|
-
if (!line) continue;
|
|
812
|
-
try {
|
|
813
|
-
out.push(JSON.parse(line));
|
|
814
|
-
} catch {
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
return out;
|
|
818
|
-
}
|
|
1404
|
+
var SESSION_HEAD_LINES = 40;
|
|
819
1405
|
function contentToString(content) {
|
|
820
1406
|
if (typeof content === "string") return content;
|
|
821
1407
|
if (Array.isArray(content)) {
|
|
@@ -842,6 +1428,28 @@ function toolInputToString(input) {
|
|
|
842
1428
|
if (input == null) return void 0;
|
|
843
1429
|
return JSON.stringify(input);
|
|
844
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
|
+
}
|
|
845
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;
|
|
846
1454
|
function hasReadOnlyCurl(input) {
|
|
847
1455
|
if (!/\bcurl\b/i.test(input)) return false;
|
|
@@ -861,33 +1469,37 @@ function isExpectedBlockingTool(name, input) {
|
|
|
861
1469
|
async function* walkRollouts(root) {
|
|
862
1470
|
let years;
|
|
863
1471
|
try {
|
|
864
|
-
years = await
|
|
865
|
-
} catch {
|
|
866
|
-
return;
|
|
1472
|
+
years = await readdir4(root);
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
if (isMissingPathError(error)) return;
|
|
1475
|
+
throw error;
|
|
867
1476
|
}
|
|
868
1477
|
for (const y of years) {
|
|
869
1478
|
const yp = join4(root, y);
|
|
870
1479
|
let months;
|
|
871
1480
|
try {
|
|
872
|
-
months = await
|
|
873
|
-
} catch {
|
|
874
|
-
continue;
|
|
1481
|
+
months = await readdir4(yp);
|
|
1482
|
+
} catch (error) {
|
|
1483
|
+
if (isMissingPathError(error)) continue;
|
|
1484
|
+
throw error;
|
|
875
1485
|
}
|
|
876
1486
|
for (const m of months) {
|
|
877
1487
|
const mp = join4(yp, m);
|
|
878
1488
|
let days;
|
|
879
1489
|
try {
|
|
880
|
-
days = await
|
|
881
|
-
} catch {
|
|
882
|
-
continue;
|
|
1490
|
+
days = await readdir4(mp);
|
|
1491
|
+
} catch (error) {
|
|
1492
|
+
if (isMissingPathError(error)) continue;
|
|
1493
|
+
throw error;
|
|
883
1494
|
}
|
|
884
1495
|
for (const d of days) {
|
|
885
1496
|
const dp = join4(mp, d);
|
|
886
1497
|
let files;
|
|
887
1498
|
try {
|
|
888
|
-
files = await
|
|
889
|
-
} catch {
|
|
890
|
-
continue;
|
|
1499
|
+
files = await readdir4(dp);
|
|
1500
|
+
} catch (error) {
|
|
1501
|
+
if (isMissingPathError(error)) continue;
|
|
1502
|
+
throw error;
|
|
891
1503
|
}
|
|
892
1504
|
for (const f of files) {
|
|
893
1505
|
if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join4(dp, f);
|
|
@@ -908,75 +1520,106 @@ var CodexAdapter = class {
|
|
|
908
1520
|
let st;
|
|
909
1521
|
try {
|
|
910
1522
|
st = await stat3(path);
|
|
911
|
-
} catch {
|
|
912
|
-
continue;
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
if (isMissingPathError(error)) continue;
|
|
1525
|
+
throw error;
|
|
913
1526
|
}
|
|
914
1527
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
915
|
-
const head = (await readFile4(path, "utf8").catch(() => "")).split("\n", 40);
|
|
916
1528
|
let cwd = null;
|
|
917
1529
|
let id = basename3(path).replace(/^rollout-[\dT-]+-/, "").replace(/\.jsonl$/, "");
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
if (!cwd && l.payload?.cwd) cwd = l.payload.cwd;
|
|
924
|
-
} catch {
|
|
925
|
-
}
|
|
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;
|
|
926
1535
|
if (cwd) break;
|
|
927
1536
|
}
|
|
928
|
-
|
|
929
|
-
|
|
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);
|
|
930
1545
|
}
|
|
931
1546
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
932
1547
|
}
|
|
933
|
-
async parse(ref) {
|
|
934
|
-
const
|
|
935
|
-
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
|
+
}
|
|
936
1562
|
const traceId = meta?.payload?.id ?? ref.sessionId;
|
|
937
|
-
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";
|
|
938
1566
|
const rootId = `root:${traceId}`;
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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];
|
|
953
1587
|
const toolByCallId = /* @__PURE__ */ new Map();
|
|
954
1588
|
const subagentByThreadId = /* @__PURE__ */ new Map();
|
|
955
1589
|
let step = 0;
|
|
956
1590
|
let lastLlm = rootId;
|
|
957
1591
|
let sawUserTurn = false;
|
|
958
|
-
|
|
1592
|
+
let lastTimestamp;
|
|
1593
|
+
const awaitingModel = model ? [] : [root];
|
|
1594
|
+
for await (const l of readJsonl(ref.path, jsonl2)) {
|
|
1595
|
+
lastTimestamp = l.timestamp;
|
|
959
1596
|
const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
960
|
-
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") {
|
|
961
1602
|
const u = l.payload.info?.last_token_usage;
|
|
962
1603
|
if (u && (u.input_tokens || u.output_tokens)) {
|
|
963
1604
|
const id = `llm:${step}`;
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
);
|
|
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);
|
|
980
1623
|
lastLlm = id;
|
|
981
1624
|
step += 1;
|
|
982
1625
|
}
|
|
@@ -988,6 +1631,7 @@ var CodexAdapter = class {
|
|
|
988
1631
|
);
|
|
989
1632
|
const nestedName = l.payload.type === "custom_tool_call" ? singleNestedToolName(input) : null;
|
|
990
1633
|
const name = classifyNestedTool(nestedName ?? outerName, input);
|
|
1634
|
+
const agentOperation = multiAgentOperation(nestedName ?? outerName);
|
|
991
1635
|
const toolSpan = span({
|
|
992
1636
|
traceId,
|
|
993
1637
|
spanId: `tool:${callId}`,
|
|
@@ -999,14 +1643,16 @@ var CodexAdapter = class {
|
|
|
999
1643
|
agent: SERVICE3,
|
|
1000
1644
|
tool: name,
|
|
1001
1645
|
step,
|
|
1002
|
-
content: input ?? null,
|
|
1003
1646
|
extra: {
|
|
1647
|
+
...toolIoAttributes({ input }),
|
|
1004
1648
|
"traces.codex.call_type": l.payload.type,
|
|
1005
1649
|
...name !== outerName ? { "traces.codex.outer_tool_name": outerName } : {},
|
|
1006
1650
|
...nestedName ? { "traces.codex.nested_tool_name": nestedName } : {},
|
|
1007
|
-
...isExpectedBlockingTool(name, input) ? { "traces.expected_blocking": true } : {}
|
|
1651
|
+
...isExpectedBlockingTool(name, input) ? { "traces.expected_blocking": true } : {},
|
|
1652
|
+
...agentOperation ? { "traces.codex.agent_operation": agentOperation } : {}
|
|
1008
1653
|
}
|
|
1009
1654
|
});
|
|
1655
|
+
if (agentOperation && agentOperation !== "spawn_agent") setAgentSessionIds(toolSpan, sessionIds(input));
|
|
1010
1656
|
spans.push(toolSpan);
|
|
1011
1657
|
toolByCallId.set(callId, toolSpan);
|
|
1012
1658
|
step += 1;
|
|
@@ -1016,6 +1662,10 @@ var CodexAdapter = class {
|
|
|
1016
1662
|
const { error, message } = outputIsError(l.payload.output);
|
|
1017
1663
|
t.end_time = ts;
|
|
1018
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
|
+
}
|
|
1019
1669
|
}
|
|
1020
1670
|
} else if (l.type === "event_msg" && l.payload?.type === "sub_agent_activity") {
|
|
1021
1671
|
const threadId = l.payload.agent_thread_id;
|
|
@@ -1035,12 +1685,14 @@ var CodexAdapter = class {
|
|
|
1035
1685
|
agent: SERVICE3,
|
|
1036
1686
|
tool: "Agent",
|
|
1037
1687
|
step,
|
|
1038
|
-
content: JSON.stringify({
|
|
1039
|
-
subagent_type: subagentType,
|
|
1040
|
-
agent_path: agentPath,
|
|
1041
|
-
agent_thread_id: threadId
|
|
1042
|
-
}),
|
|
1043
1688
|
extra: {
|
|
1689
|
+
...toolIoAttributes({
|
|
1690
|
+
input: {
|
|
1691
|
+
subagent_type: subagentType,
|
|
1692
|
+
agent_path: agentPath,
|
|
1693
|
+
agent_thread_id: threadId
|
|
1694
|
+
}
|
|
1695
|
+
}),
|
|
1044
1696
|
"traces.codex.subagent_path": agentPath,
|
|
1045
1697
|
"traces.codex.subagent_thread_id": threadId
|
|
1046
1698
|
}
|
|
@@ -1102,26 +1754,16 @@ var CodexAdapter = class {
|
|
|
1102
1754
|
}
|
|
1103
1755
|
}
|
|
1104
1756
|
}
|
|
1757
|
+
root.end_time = lastTimestamp ?? root.start_time;
|
|
1105
1758
|
return spans;
|
|
1106
1759
|
}
|
|
1107
1760
|
};
|
|
1108
1761
|
|
|
1109
1762
|
// src/adapters/copilot.ts
|
|
1110
|
-
import { readdir as
|
|
1763
|
+
import { readdir as readdir5, stat as stat4 } from "fs/promises";
|
|
1111
1764
|
import { homedir as homedir4 } from "os";
|
|
1112
1765
|
import { join as join5 } from "path";
|
|
1113
1766
|
var SERVICE4 = "github-copilot";
|
|
1114
|
-
function parseLines3(raw) {
|
|
1115
|
-
const out = [];
|
|
1116
|
-
for (const line of raw.split("\n")) {
|
|
1117
|
-
if (!line.trim()) continue;
|
|
1118
|
-
try {
|
|
1119
|
-
out.push(JSON.parse(line));
|
|
1120
|
-
} catch {
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
return out;
|
|
1124
|
-
}
|
|
1125
1767
|
var CopilotAdapter = class {
|
|
1126
1768
|
harness = "github-copilot";
|
|
1127
1769
|
aliases = ["copilot"];
|
|
@@ -1131,9 +1773,10 @@ var CopilotAdapter = class {
|
|
|
1131
1773
|
async locate(opts = {}) {
|
|
1132
1774
|
let dirs;
|
|
1133
1775
|
try {
|
|
1134
|
-
dirs = await
|
|
1135
|
-
} catch {
|
|
1136
|
-
return [];
|
|
1776
|
+
dirs = await readdir5(this.root());
|
|
1777
|
+
} catch (error) {
|
|
1778
|
+
if (isMissingPathError(error)) return [];
|
|
1779
|
+
throw error;
|
|
1137
1780
|
}
|
|
1138
1781
|
const refs = [];
|
|
1139
1782
|
for (const id of dirs) {
|
|
@@ -1141,8 +1784,9 @@ var CopilotAdapter = class {
|
|
|
1141
1784
|
let st;
|
|
1142
1785
|
try {
|
|
1143
1786
|
st = await stat4(path);
|
|
1144
|
-
} catch {
|
|
1145
|
-
continue;
|
|
1787
|
+
} catch (error) {
|
|
1788
|
+
if (isMissingPathError(error)) continue;
|
|
1789
|
+
throw error;
|
|
1146
1790
|
}
|
|
1147
1791
|
if (!st.isFile()) continue;
|
|
1148
1792
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
@@ -1151,28 +1795,23 @@ var CopilotAdapter = class {
|
|
|
1151
1795
|
}
|
|
1152
1796
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1153
1797
|
}
|
|
1154
|
-
async parse(ref) {
|
|
1155
|
-
const events = parseLines3(await readFile5(ref.path, "utf8"));
|
|
1798
|
+
async parse(ref, options = {}) {
|
|
1156
1799
|
const traceId = ref.sessionId;
|
|
1157
1800
|
const rootId = `root:${traceId}`;
|
|
1158
|
-
const spans = [
|
|
1159
|
-
span({
|
|
1160
|
-
traceId,
|
|
1161
|
-
spanId: rootId,
|
|
1162
|
-
parentSpanId: null,
|
|
1163
|
-
name: "session",
|
|
1164
|
-
kind: "AGENT",
|
|
1165
|
-
startTime: events[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1166
|
-
endTime: events.at(-1)?.timestamp,
|
|
1167
|
-
service: SERVICE4,
|
|
1168
|
-
agent: SERVICE4
|
|
1169
|
-
})
|
|
1170
|
-
];
|
|
1801
|
+
const spans = [];
|
|
1171
1802
|
const toolByCallId = /* @__PURE__ */ new Map();
|
|
1803
|
+
let firstTimestamp;
|
|
1804
|
+
let lastTimestamp;
|
|
1805
|
+
let sawEvent = false;
|
|
1172
1806
|
let step = 0;
|
|
1173
1807
|
let lastLlm = rootId;
|
|
1174
1808
|
let pendingInputTokens = null;
|
|
1175
|
-
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;
|
|
1176
1815
|
const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1177
1816
|
const d = ev.data ?? {};
|
|
1178
1817
|
if (ev.type === "assistant.usage") {
|
|
@@ -1214,7 +1853,7 @@ var CopilotAdapter = class {
|
|
|
1214
1853
|
agent: SERVICE4,
|
|
1215
1854
|
tool: name,
|
|
1216
1855
|
step,
|
|
1217
|
-
|
|
1856
|
+
extra: toolIoAttributes({ input: d.arguments })
|
|
1218
1857
|
});
|
|
1219
1858
|
spans.push(t);
|
|
1220
1859
|
toolByCallId.set(d.toolCallId, t);
|
|
@@ -1225,15 +1864,27 @@ var CopilotAdapter = class {
|
|
|
1225
1864
|
t.end_time = ts;
|
|
1226
1865
|
const err = d.success === false;
|
|
1227
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);
|
|
1228
1868
|
}
|
|
1229
1869
|
}
|
|
1230
1870
|
}
|
|
1231
|
-
|
|
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];
|
|
1232
1883
|
}
|
|
1233
1884
|
};
|
|
1234
1885
|
|
|
1235
1886
|
// src/adapters/factory.ts
|
|
1236
|
-
import { readdir as
|
|
1887
|
+
import { readdir as readdir6, stat as stat5 } from "fs/promises";
|
|
1237
1888
|
import { homedir as homedir5 } from "os";
|
|
1238
1889
|
import { basename as basename4, join as join6 } from "path";
|
|
1239
1890
|
var SERVICE5 = "factory";
|
|
@@ -1242,17 +1893,6 @@ function textOf4(content) {
|
|
|
1242
1893
|
(content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("")
|
|
1243
1894
|
);
|
|
1244
1895
|
}
|
|
1245
|
-
function parseLines4(raw) {
|
|
1246
|
-
const out = [];
|
|
1247
|
-
for (const line of raw.split("\n")) {
|
|
1248
|
-
if (!line) continue;
|
|
1249
|
-
try {
|
|
1250
|
-
out.push(JSON.parse(line));
|
|
1251
|
-
} catch {
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
return out;
|
|
1255
|
-
}
|
|
1256
1896
|
var FactoryAdapter = class {
|
|
1257
1897
|
harness = "factory";
|
|
1258
1898
|
aliases = ["factory-droids", "droid"];
|
|
@@ -1263,18 +1903,20 @@ var FactoryAdapter = class {
|
|
|
1263
1903
|
const root = this.root();
|
|
1264
1904
|
let dirs;
|
|
1265
1905
|
try {
|
|
1266
|
-
dirs = await
|
|
1267
|
-
} catch {
|
|
1268
|
-
return [];
|
|
1906
|
+
dirs = await readdir6(root);
|
|
1907
|
+
} catch (error) {
|
|
1908
|
+
if (isMissingPathError(error)) return [];
|
|
1909
|
+
throw error;
|
|
1269
1910
|
}
|
|
1270
1911
|
const refs = [];
|
|
1271
1912
|
for (const dir of dirs) {
|
|
1272
1913
|
const dp = join6(root, dir);
|
|
1273
1914
|
let files;
|
|
1274
1915
|
try {
|
|
1275
|
-
files = await
|
|
1276
|
-
} catch {
|
|
1277
|
-
continue;
|
|
1916
|
+
files = await readdir6(dp);
|
|
1917
|
+
} catch (error) {
|
|
1918
|
+
if (isMissingPathError(error)) continue;
|
|
1919
|
+
throw error;
|
|
1278
1920
|
}
|
|
1279
1921
|
const cwd = dir.replace(/-/g, "/");
|
|
1280
1922
|
if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
|
|
@@ -1284,8 +1926,9 @@ var FactoryAdapter = class {
|
|
|
1284
1926
|
let st;
|
|
1285
1927
|
try {
|
|
1286
1928
|
st = await stat5(path);
|
|
1287
|
-
} catch {
|
|
1288
|
-
continue;
|
|
1929
|
+
} catch (error) {
|
|
1930
|
+
if (isMissingPathError(error)) continue;
|
|
1931
|
+
throw error;
|
|
1289
1932
|
}
|
|
1290
1933
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
1291
1934
|
refs.push({ harness: this.harness, sessionId: basename4(f, ".jsonl"), path, cwd, mtimeMs: st.mtimeMs });
|
|
@@ -1293,38 +1936,32 @@ var FactoryAdapter = class {
|
|
|
1293
1936
|
}
|
|
1294
1937
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1295
1938
|
}
|
|
1296
|
-
async parse(ref) {
|
|
1297
|
-
const lines = parseLines4(await readFile6(ref.path, "utf8"));
|
|
1298
|
-
const start = lines.find((l) => l.type === "session_start");
|
|
1299
|
-
const traceId = start?.id ?? ref.sessionId;
|
|
1300
|
-
const rootId = `root:${traceId}`;
|
|
1939
|
+
async parse(ref, options = {}) {
|
|
1301
1940
|
let settings = {};
|
|
1302
1941
|
try {
|
|
1303
|
-
settings =
|
|
1304
|
-
} catch {
|
|
1942
|
+
settings = await readJsonFile(ref.path.replace(/\.jsonl$/, ".settings.json"));
|
|
1943
|
+
} catch (error) {
|
|
1944
|
+
if (!isMissingJsonSource(error)) throw error;
|
|
1305
1945
|
}
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
spanId: rootId,
|
|
1310
|
-
parentSpanId: null,
|
|
1311
|
-
name: "session",
|
|
1312
|
-
kind: "AGENT",
|
|
1313
|
-
startTime: start?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1314
|
-
endTime: lines.at(-1)?.timestamp,
|
|
1315
|
-
service: SERVICE5,
|
|
1316
|
-
agent: SERVICE5,
|
|
1317
|
-
model: settings.model ?? null,
|
|
1318
|
-
extra: settings.tokenUsage ? {
|
|
1319
|
-
"session.input_tokens": settings.tokenUsage.inputTokens ?? 0,
|
|
1320
|
-
"session.output_tokens": settings.tokenUsage.outputTokens ?? 0
|
|
1321
|
-
} : void 0
|
|
1322
|
-
})
|
|
1323
|
-
];
|
|
1946
|
+
const sourceTraceId = ref.sessionId;
|
|
1947
|
+
const sourceRootId = `root:${sourceTraceId}`;
|
|
1948
|
+
const spans = [];
|
|
1324
1949
|
const toolByUseId = /* @__PURE__ */ new Map();
|
|
1950
|
+
let firstTimestamp;
|
|
1951
|
+
let lastTimestamp;
|
|
1952
|
+
let sessionLine;
|
|
1953
|
+
let sawLine = false;
|
|
1325
1954
|
let step = 0;
|
|
1326
|
-
let lastLlm =
|
|
1327
|
-
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
|
+
}
|
|
1328
1965
|
if (l.type !== "message" || !l.message) continue;
|
|
1329
1966
|
const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1330
1967
|
const role = l.message.role;
|
|
@@ -1333,9 +1970,9 @@ var FactoryAdapter = class {
|
|
|
1333
1970
|
if (text) {
|
|
1334
1971
|
spans.push(
|
|
1335
1972
|
userPromptSpan({
|
|
1336
|
-
traceId,
|
|
1973
|
+
traceId: sourceTraceId,
|
|
1337
1974
|
spanId: `user:${l.id ?? step}`,
|
|
1338
|
-
parentSpanId:
|
|
1975
|
+
parentSpanId: sourceRootId,
|
|
1339
1976
|
startTime: ts,
|
|
1340
1977
|
content: text,
|
|
1341
1978
|
service: SERVICE5,
|
|
@@ -1349,9 +1986,9 @@ var FactoryAdapter = class {
|
|
|
1349
1986
|
const llmId = `llm:${l.id ?? step}`;
|
|
1350
1987
|
spans.push(
|
|
1351
1988
|
span({
|
|
1352
|
-
traceId,
|
|
1989
|
+
traceId: sourceTraceId,
|
|
1353
1990
|
spanId: llmId,
|
|
1354
|
-
parentSpanId:
|
|
1991
|
+
parentSpanId: sourceRootId,
|
|
1355
1992
|
name: `message.${role ?? "unknown"}`,
|
|
1356
1993
|
kind: "LLM",
|
|
1357
1994
|
startTime: ts,
|
|
@@ -1368,7 +2005,7 @@ var FactoryAdapter = class {
|
|
|
1368
2005
|
for (const b of l.message.content ?? []) {
|
|
1369
2006
|
if (b.type === "tool_use" && b.name) {
|
|
1370
2007
|
const t = span({
|
|
1371
|
-
traceId,
|
|
2008
|
+
traceId: sourceTraceId,
|
|
1372
2009
|
spanId: `tool:${b.id ?? `${l.id}:${step}`}`,
|
|
1373
2010
|
parentSpanId: lastLlm,
|
|
1374
2011
|
name: `tool.${b.name}`,
|
|
@@ -1378,23 +2015,48 @@ var FactoryAdapter = class {
|
|
|
1378
2015
|
agent: SERVICE5,
|
|
1379
2016
|
tool: b.name,
|
|
1380
2017
|
step,
|
|
1381
|
-
|
|
2018
|
+
extra: toolIoAttributes({ input: b.input })
|
|
1382
2019
|
});
|
|
1383
2020
|
spans.push(t);
|
|
1384
2021
|
if (b.id) toolByUseId.set(b.id, t);
|
|
1385
2022
|
step += 1;
|
|
1386
2023
|
} else if (b.type === "tool_result" && b.tool_use_id) {
|
|
1387
2024
|
const t = toolByUseId.get(b.tool_use_id);
|
|
1388
|
-
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
|
+
}
|
|
1389
2029
|
}
|
|
1390
2030
|
}
|
|
1391
2031
|
}
|
|
1392
|
-
|
|
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];
|
|
1393
2055
|
}
|
|
1394
2056
|
};
|
|
1395
2057
|
|
|
1396
2058
|
// src/adapters/forge.ts
|
|
1397
|
-
import { readdir as
|
|
2059
|
+
import { readdir as readdir7, stat as stat6 } from "fs/promises";
|
|
1398
2060
|
import { homedir as homedir6 } from "os";
|
|
1399
2061
|
import { basename as basename5, join as join7 } from "path";
|
|
1400
2062
|
var SERVICE6 = "forge";
|
|
@@ -1418,9 +2080,10 @@ var ForgeAdapter = class {
|
|
|
1418
2080
|
for (const dir of dirs) {
|
|
1419
2081
|
let files;
|
|
1420
2082
|
try {
|
|
1421
|
-
files = await
|
|
1422
|
-
} catch {
|
|
1423
|
-
continue;
|
|
2083
|
+
files = await readdir7(dir);
|
|
2084
|
+
} catch (error) {
|
|
2085
|
+
if (isMissingPathError(error)) continue;
|
|
2086
|
+
throw error;
|
|
1424
2087
|
}
|
|
1425
2088
|
for (const f of files) {
|
|
1426
2089
|
if (!f.endsWith("-dump.json")) continue;
|
|
@@ -1428,8 +2091,9 @@ var ForgeAdapter = class {
|
|
|
1428
2091
|
let st;
|
|
1429
2092
|
try {
|
|
1430
2093
|
st = await stat6(path);
|
|
1431
|
-
} catch {
|
|
1432
|
-
continue;
|
|
2094
|
+
} catch (error) {
|
|
2095
|
+
if (isMissingPathError(error)) continue;
|
|
2096
|
+
throw error;
|
|
1433
2097
|
}
|
|
1434
2098
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
1435
2099
|
refs.push({ harness: this.harness, sessionId: basename5(f, ".json"), path, cwd: dir, mtimeMs: st.mtimeMs });
|
|
@@ -1438,12 +2102,7 @@ var ForgeAdapter = class {
|
|
|
1438
2102
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1439
2103
|
}
|
|
1440
2104
|
async parse(ref) {
|
|
1441
|
-
|
|
1442
|
-
try {
|
|
1443
|
-
dump = JSON.parse(await readFile7(ref.path, "utf8"));
|
|
1444
|
-
} catch {
|
|
1445
|
-
return [];
|
|
1446
|
-
}
|
|
2105
|
+
const dump = await readJsonFile(ref.path);
|
|
1447
2106
|
const ctx = dump.context ?? dump;
|
|
1448
2107
|
const traceId = ctx.conversation_id ?? ref.sessionId;
|
|
1449
2108
|
const rootId = `root:${traceId}`;
|
|
@@ -1508,7 +2167,7 @@ var ForgeAdapter = class {
|
|
|
1508
2167
|
agent: SERVICE6,
|
|
1509
2168
|
tool: tc.name ?? "tool",
|
|
1510
2169
|
step,
|
|
1511
|
-
|
|
2170
|
+
extra: toolIoAttributes({ input: tc.arguments })
|
|
1512
2171
|
});
|
|
1513
2172
|
spans.push(t);
|
|
1514
2173
|
toolByCallId.set(id, t);
|
|
@@ -1516,7 +2175,10 @@ var ForgeAdapter = class {
|
|
|
1516
2175
|
}
|
|
1517
2176
|
} else if (entry.tool) {
|
|
1518
2177
|
const t = toolByCallId.get(entry.tool.call_id ?? "");
|
|
1519
|
-
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
|
+
}
|
|
1520
2182
|
}
|
|
1521
2183
|
}
|
|
1522
2184
|
return spans;
|
|
@@ -1524,7 +2186,7 @@ var ForgeAdapter = class {
|
|
|
1524
2186
|
};
|
|
1525
2187
|
|
|
1526
2188
|
// src/adapters/gemini.ts
|
|
1527
|
-
import { readdir as
|
|
2189
|
+
import { readdir as readdir8, stat as stat7 } from "fs/promises";
|
|
1528
2190
|
import { homedir as homedir7 } from "os";
|
|
1529
2191
|
import { join as join8 } from "path";
|
|
1530
2192
|
function iso(ts, fallback) {
|
|
@@ -1560,10 +2222,12 @@ var GeminiFamilyAdapter = class {
|
|
|
1560
2222
|
async loadProjectCwds() {
|
|
1561
2223
|
const nameToPath = /* @__PURE__ */ new Map();
|
|
1562
2224
|
try {
|
|
1563
|
-
const
|
|
1564
|
-
|
|
2225
|
+
const projects = (await readJsonFile(
|
|
2226
|
+
join8(homedir7(), this.homeDirName, "projects.json")
|
|
2227
|
+
)).projects ?? {};
|
|
1565
2228
|
for (const [path, name] of Object.entries(projects)) nameToPath.set(name, path);
|
|
1566
|
-
} catch {
|
|
2229
|
+
} catch (error) {
|
|
2230
|
+
if (!isMissingJsonSource(error)) throw error;
|
|
1567
2231
|
}
|
|
1568
2232
|
return nameToPath;
|
|
1569
2233
|
}
|
|
@@ -1571,9 +2235,10 @@ var GeminiFamilyAdapter = class {
|
|
|
1571
2235
|
const root = this.chatsRoot();
|
|
1572
2236
|
let projectDirs;
|
|
1573
2237
|
try {
|
|
1574
|
-
projectDirs = await
|
|
1575
|
-
} catch {
|
|
1576
|
-
return [];
|
|
2238
|
+
projectDirs = await readdir8(root);
|
|
2239
|
+
} catch (error) {
|
|
2240
|
+
if (isMissingPathError(error)) return [];
|
|
2241
|
+
throw error;
|
|
1577
2242
|
}
|
|
1578
2243
|
const projectCwds = await this.loadProjectCwds();
|
|
1579
2244
|
const refs = [];
|
|
@@ -1582,9 +2247,10 @@ var GeminiFamilyAdapter = class {
|
|
|
1582
2247
|
const chatsDir = join8(root, pd, "chats");
|
|
1583
2248
|
let files;
|
|
1584
2249
|
try {
|
|
1585
|
-
files = await
|
|
1586
|
-
} catch {
|
|
1587
|
-
continue;
|
|
2250
|
+
files = await readdir8(chatsDir);
|
|
2251
|
+
} catch (error) {
|
|
2252
|
+
if (isMissingPathError(error)) continue;
|
|
2253
|
+
throw error;
|
|
1588
2254
|
}
|
|
1589
2255
|
for (const f of files) {
|
|
1590
2256
|
if (!f.startsWith("session-") || !f.endsWith(".json")) continue;
|
|
@@ -1592,8 +2258,9 @@ var GeminiFamilyAdapter = class {
|
|
|
1592
2258
|
let st;
|
|
1593
2259
|
try {
|
|
1594
2260
|
st = await stat7(path);
|
|
1595
|
-
} catch {
|
|
1596
|
-
continue;
|
|
2261
|
+
} catch (error) {
|
|
2262
|
+
if (isMissingPathError(error)) continue;
|
|
2263
|
+
throw error;
|
|
1597
2264
|
}
|
|
1598
2265
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
1599
2266
|
if (opts.cwd && (!cwd || !cwd.startsWith(opts.cwd))) continue;
|
|
@@ -1603,12 +2270,7 @@ var GeminiFamilyAdapter = class {
|
|
|
1603
2270
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1604
2271
|
}
|
|
1605
2272
|
async parse(ref) {
|
|
1606
|
-
|
|
1607
|
-
try {
|
|
1608
|
-
session = JSON.parse(await readFile8(ref.path, "utf8"));
|
|
1609
|
-
} catch {
|
|
1610
|
-
return [];
|
|
1611
|
-
}
|
|
2273
|
+
const session = await readJsonFile(ref.path);
|
|
1612
2274
|
const traceId = session.sessionId ?? ref.sessionId;
|
|
1613
2275
|
const messages = session.messages ?? [];
|
|
1614
2276
|
const rootId = `root:${traceId}`;
|
|
@@ -1683,7 +2345,7 @@ var GeminiFamilyAdapter = class {
|
|
|
1683
2345
|
agent: this.service,
|
|
1684
2346
|
tool: tc.name ?? "tool",
|
|
1685
2347
|
step,
|
|
1686
|
-
|
|
2348
|
+
extra: toolIoAttributes({ input: tc.args, output: tc.result })
|
|
1687
2349
|
})
|
|
1688
2350
|
);
|
|
1689
2351
|
step += 1;
|
|
@@ -1699,7 +2361,7 @@ var GeminiAdapter = class extends GeminiFamilyAdapter {
|
|
|
1699
2361
|
};
|
|
1700
2362
|
|
|
1701
2363
|
// src/adapters/opencode.ts
|
|
1702
|
-
import { readdir as
|
|
2364
|
+
import { readdir as readdir9, stat as stat8 } from "fs/promises";
|
|
1703
2365
|
import { homedir as homedir8 } from "os";
|
|
1704
2366
|
import { join as join9 } from "path";
|
|
1705
2367
|
var SERVICE7 = "opencode";
|
|
@@ -1721,9 +2383,10 @@ var OpencodeAdapter = class {
|
|
|
1721
2383
|
const msgRoot = join9(this.storage(), "message");
|
|
1722
2384
|
let sessionDirs;
|
|
1723
2385
|
try {
|
|
1724
|
-
sessionDirs = await
|
|
1725
|
-
} catch {
|
|
1726
|
-
return [];
|
|
2386
|
+
sessionDirs = await readdir9(msgRoot);
|
|
2387
|
+
} catch (error) {
|
|
2388
|
+
if (isMissingPathError(error)) return [];
|
|
2389
|
+
throw error;
|
|
1727
2390
|
}
|
|
1728
2391
|
const refs = [];
|
|
1729
2392
|
for (const sid of sessionDirs) {
|
|
@@ -1731,8 +2394,9 @@ var OpencodeAdapter = class {
|
|
|
1731
2394
|
let st;
|
|
1732
2395
|
try {
|
|
1733
2396
|
st = await stat8(dir);
|
|
1734
|
-
} catch {
|
|
1735
|
-
continue;
|
|
2397
|
+
} catch (error) {
|
|
2398
|
+
if (isMissingPathError(error)) continue;
|
|
2399
|
+
throw error;
|
|
1736
2400
|
}
|
|
1737
2401
|
if (!st.isDirectory()) continue;
|
|
1738
2402
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
@@ -1745,20 +2409,12 @@ var OpencodeAdapter = class {
|
|
|
1745
2409
|
const traceId = ref.sessionId;
|
|
1746
2410
|
const rootId = `root:${traceId}`;
|
|
1747
2411
|
const partRoot = join9(this.storage(), "part");
|
|
1748
|
-
|
|
1749
|
-
try {
|
|
1750
|
-
msgFiles = (await readdir8(ref.path)).filter((f) => f.endsWith(".json"));
|
|
1751
|
-
} catch {
|
|
1752
|
-
return [];
|
|
1753
|
-
}
|
|
2412
|
+
const msgFiles = await listJsonFiles(ref.path);
|
|
1754
2413
|
const messages = [];
|
|
1755
2414
|
for (const f of msgFiles) {
|
|
1756
|
-
|
|
1757
|
-
messages.push({ msg: JSON.parse(await readFile9(join9(ref.path, f), "utf8")), file: f });
|
|
1758
|
-
} catch {
|
|
1759
|
-
}
|
|
2415
|
+
messages.push(await readJsonFile(join9(ref.path, f)));
|
|
1760
2416
|
}
|
|
1761
|
-
messages.sort((a, b) => (a.
|
|
2417
|
+
messages.sort((a, b) => (a.time?.created ?? 0) - (b.time?.created ?? 0));
|
|
1762
2418
|
const spans = [
|
|
1763
2419
|
span({
|
|
1764
2420
|
traceId,
|
|
@@ -1766,59 +2422,65 @@ var OpencodeAdapter = class {
|
|
|
1766
2422
|
parentSpanId: null,
|
|
1767
2423
|
name: "session",
|
|
1768
2424
|
kind: "AGENT",
|
|
1769
|
-
startTime: isoOf(messages[0]?.
|
|
1770
|
-
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),
|
|
1771
2427
|
service: SERVICE7,
|
|
1772
2428
|
agent: SERVICE7
|
|
1773
2429
|
})
|
|
1774
2430
|
];
|
|
1775
2431
|
let step = 0;
|
|
1776
|
-
for (const
|
|
2432
|
+
for (const msg of messages) {
|
|
1777
2433
|
const mid = msg.id ?? `m${step}`;
|
|
1778
2434
|
const llmId = `llm:${mid}`;
|
|
1779
2435
|
const pdir = join9(partRoot, mid);
|
|
1780
2436
|
const parts = [];
|
|
2437
|
+
let partFiles = [];
|
|
1781
2438
|
try {
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
}
|
|
1789
|
-
} 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)));
|
|
1790
2445
|
}
|
|
1791
2446
|
const turnText = textOf7(parts);
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
step += 1;
|
|
1811
|
-
if (msg.role === "user" && turnText) {
|
|
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 {
|
|
1812
2465
|
spans.push(
|
|
1813
|
-
|
|
2466
|
+
span({
|
|
1814
2467
|
traceId,
|
|
1815
|
-
spanId:
|
|
2468
|
+
spanId: llmId,
|
|
1816
2469
|
parentSpanId: rootId,
|
|
2470
|
+
name: `message.${msg.role ?? "unknown"}`,
|
|
2471
|
+
kind: "LLM",
|
|
1817
2472
|
startTime: isoOf(msg.time?.created),
|
|
2473
|
+
endTime: isoOf(msg.time?.completed ?? msg.time?.created),
|
|
1818
2474
|
service: SERVICE7,
|
|
1819
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,
|
|
1820
2482
|
step,
|
|
1821
|
-
content: turnText
|
|
2483
|
+
content: turnText || null
|
|
1822
2484
|
})
|
|
1823
2485
|
);
|
|
1824
2486
|
step += 1;
|
|
@@ -1830,7 +2492,7 @@ var OpencodeAdapter = class {
|
|
|
1830
2492
|
span({
|
|
1831
2493
|
traceId,
|
|
1832
2494
|
spanId: `tool:${part.callID ?? `${mid}:${step}`}`,
|
|
1833
|
-
parentSpanId: llmId,
|
|
2495
|
+
parentSpanId: isUser ? rootId : llmId,
|
|
1834
2496
|
name: `tool.${part.tool}`,
|
|
1835
2497
|
kind: "TOOL",
|
|
1836
2498
|
startTime: isoOf(msg.time?.created),
|
|
@@ -1841,7 +2503,10 @@ var OpencodeAdapter = class {
|
|
|
1841
2503
|
agent: SERVICE7,
|
|
1842
2504
|
tool: part.tool,
|
|
1843
2505
|
step,
|
|
1844
|
-
|
|
2506
|
+
extra: toolIoAttributes({
|
|
2507
|
+
input: part.state?.input,
|
|
2508
|
+
output: part.state?.output ?? part.state?.error
|
|
2509
|
+
})
|
|
1845
2510
|
})
|
|
1846
2511
|
);
|
|
1847
2512
|
step += 1;
|
|
@@ -1852,21 +2517,10 @@ var OpencodeAdapter = class {
|
|
|
1852
2517
|
};
|
|
1853
2518
|
|
|
1854
2519
|
// src/adapters/pi.ts
|
|
1855
|
-
import { readdir as
|
|
2520
|
+
import { readdir as readdir10, stat as stat9 } from "fs/promises";
|
|
1856
2521
|
import { homedir as homedir9 } from "os";
|
|
1857
2522
|
import { basename as basename6, join as join10 } from "path";
|
|
1858
2523
|
var SERVICE8 = "pi";
|
|
1859
|
-
function parseLines5(raw) {
|
|
1860
|
-
const out = [];
|
|
1861
|
-
for (const line of raw.split("\n")) {
|
|
1862
|
-
if (!line) continue;
|
|
1863
|
-
try {
|
|
1864
|
-
out.push(JSON.parse(line));
|
|
1865
|
-
} catch {
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
return out;
|
|
1869
|
-
}
|
|
1870
2524
|
function isToolBlock(b) {
|
|
1871
2525
|
return typeof b.type === "string" && /tool[_-]?call|tool[_-]?use|^tool$/i.test(b.type);
|
|
1872
2526
|
}
|
|
@@ -1888,18 +2542,20 @@ var PiAdapter = class {
|
|
|
1888
2542
|
const root = this.root();
|
|
1889
2543
|
let dirs;
|
|
1890
2544
|
try {
|
|
1891
|
-
dirs = await
|
|
1892
|
-
} catch {
|
|
1893
|
-
return [];
|
|
2545
|
+
dirs = await readdir10(root);
|
|
2546
|
+
} catch (error) {
|
|
2547
|
+
if (isMissingPathError(error)) return [];
|
|
2548
|
+
throw error;
|
|
1894
2549
|
}
|
|
1895
2550
|
const refs = [];
|
|
1896
2551
|
for (const dir of dirs) {
|
|
1897
2552
|
const dp = join10(root, dir);
|
|
1898
2553
|
let files;
|
|
1899
2554
|
try {
|
|
1900
|
-
files = await
|
|
1901
|
-
} catch {
|
|
1902
|
-
continue;
|
|
2555
|
+
files = await readdir10(dp);
|
|
2556
|
+
} catch (error) {
|
|
2557
|
+
if (isMissingPathError(error)) continue;
|
|
2558
|
+
throw error;
|
|
1903
2559
|
}
|
|
1904
2560
|
const cwd = `/${dir.replace(/^-+/, "").replace(/-+$/, "").replace(/-/g, "/")}`;
|
|
1905
2561
|
if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
|
|
@@ -1909,8 +2565,9 @@ var PiAdapter = class {
|
|
|
1909
2565
|
let st;
|
|
1910
2566
|
try {
|
|
1911
2567
|
st = await stat9(path);
|
|
1912
|
-
} catch {
|
|
1913
|
-
continue;
|
|
2568
|
+
} catch (error) {
|
|
2569
|
+
if (isMissingPathError(error)) continue;
|
|
2570
|
+
throw error;
|
|
1914
2571
|
}
|
|
1915
2572
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
1916
2573
|
const id = basename6(f, ".jsonl").replace(/^[\dTZ.-]+_/, "");
|
|
@@ -1919,27 +2576,25 @@ var PiAdapter = class {
|
|
|
1919
2576
|
}
|
|
1920
2577
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1921
2578
|
}
|
|
1922
|
-
async parse(ref) {
|
|
1923
|
-
const
|
|
1924
|
-
const
|
|
1925
|
-
const
|
|
1926
|
-
const rootId = `root:${traceId}`;
|
|
1927
|
-
const spans = [
|
|
1928
|
-
span({
|
|
1929
|
-
traceId,
|
|
1930
|
-
spanId: rootId,
|
|
1931
|
-
parentSpanId: null,
|
|
1932
|
-
name: "session",
|
|
1933
|
-
kind: "AGENT",
|
|
1934
|
-
startTime: sessionLine?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1935
|
-
endTime: lines.at(-1)?.timestamp,
|
|
1936
|
-
service: SERVICE8,
|
|
1937
|
-
agent: SERVICE8
|
|
1938
|
-
})
|
|
1939
|
-
];
|
|
2579
|
+
async parse(ref, options = {}) {
|
|
2580
|
+
const sourceTraceId = ref.sessionId;
|
|
2581
|
+
const sourceRootId = `root:${sourceTraceId}`;
|
|
2582
|
+
const spans = [];
|
|
1940
2583
|
const toolByCallId = /* @__PURE__ */ new Map();
|
|
2584
|
+
let firstTimestamp;
|
|
2585
|
+
let lastTimestamp;
|
|
2586
|
+
let sessionLine;
|
|
2587
|
+
let sawLine = false;
|
|
1941
2588
|
let step = 0;
|
|
1942
|
-
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
|
+
}
|
|
1943
2598
|
if (l.type !== "message" || !l.message) continue;
|
|
1944
2599
|
const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1945
2600
|
const mid = l.id ?? `m${step}`;
|
|
@@ -1950,9 +2605,9 @@ var PiAdapter = class {
|
|
|
1950
2605
|
if (prompt) {
|
|
1951
2606
|
spans.push(
|
|
1952
2607
|
userPromptSpan({
|
|
1953
|
-
traceId,
|
|
2608
|
+
traceId: sourceTraceId,
|
|
1954
2609
|
spanId: `${mid}:user`,
|
|
1955
|
-
parentSpanId:
|
|
2610
|
+
parentSpanId: sourceRootId,
|
|
1956
2611
|
startTime: ts,
|
|
1957
2612
|
service: SERVICE8,
|
|
1958
2613
|
agent: SERVICE8,
|
|
@@ -1966,9 +2621,9 @@ var PiAdapter = class {
|
|
|
1966
2621
|
const errored = !!msg.errorMessage;
|
|
1967
2622
|
spans.push(
|
|
1968
2623
|
span({
|
|
1969
|
-
traceId,
|
|
2624
|
+
traceId: sourceTraceId,
|
|
1970
2625
|
spanId: llmId,
|
|
1971
|
-
parentSpanId:
|
|
2626
|
+
parentSpanId: sourceRootId,
|
|
1972
2627
|
name: `message.${msg.role ?? "unknown"}`,
|
|
1973
2628
|
kind: "LLM",
|
|
1974
2629
|
startTime: ts,
|
|
@@ -1990,7 +2645,7 @@ var PiAdapter = class {
|
|
|
1990
2645
|
const name = b.toolName ?? b.name ?? "tool";
|
|
1991
2646
|
const callId = b.id ?? b.callId ?? b.toolCallId ?? `${mid}:${step}`;
|
|
1992
2647
|
const toolSpan = span({
|
|
1993
|
-
traceId,
|
|
2648
|
+
traceId: sourceTraceId,
|
|
1994
2649
|
spanId: `tool:${callId}`,
|
|
1995
2650
|
parentSpanId: llmId,
|
|
1996
2651
|
name: `tool.${name}`,
|
|
@@ -2000,7 +2655,7 @@ var PiAdapter = class {
|
|
|
2000
2655
|
agent: SERVICE8,
|
|
2001
2656
|
tool: name,
|
|
2002
2657
|
step,
|
|
2003
|
-
|
|
2658
|
+
extra: toolIoAttributes({ input: b.input ?? b.args })
|
|
2004
2659
|
});
|
|
2005
2660
|
spans.push(toolSpan);
|
|
2006
2661
|
toolByCallId.set(callId, toolSpan);
|
|
@@ -2012,30 +2667,37 @@ var PiAdapter = class {
|
|
|
2012
2667
|
const err = b.isError === true || b.is_error === true;
|
|
2013
2668
|
t.end_time = ts;
|
|
2014
2669
|
t.status = err ? { code: "ERROR", message: "tool result reported error" } : { code: "OK" };
|
|
2670
|
+
recordToolOutput(t, b.output ?? b.result ?? b.content ?? b.text);
|
|
2015
2671
|
}
|
|
2016
2672
|
}
|
|
2017
2673
|
}
|
|
2018
2674
|
}
|
|
2019
|
-
|
|
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];
|
|
2020
2693
|
}
|
|
2021
2694
|
};
|
|
2022
2695
|
|
|
2023
2696
|
// src/adapters/qwen.ts
|
|
2024
|
-
import { readdir as
|
|
2697
|
+
import { readdir as readdir11, stat as stat10 } from "fs/promises";
|
|
2025
2698
|
import { homedir as homedir10 } from "os";
|
|
2026
2699
|
import { basename as basename7, join as join11 } from "path";
|
|
2027
2700
|
var SERVICE9 = "qwen";
|
|
2028
|
-
function parseLines6(raw) {
|
|
2029
|
-
const out = [];
|
|
2030
|
-
for (const line of raw.split("\n")) {
|
|
2031
|
-
if (!line) continue;
|
|
2032
|
-
try {
|
|
2033
|
-
out.push(JSON.parse(line));
|
|
2034
|
-
} catch {
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
return out;
|
|
2038
|
-
}
|
|
2039
2701
|
function textOf9(parts) {
|
|
2040
2702
|
if (!parts) return "";
|
|
2041
2703
|
return parts.filter((p) => typeof p.text === "string").map((p) => p.text).join("");
|
|
@@ -2050,18 +2712,20 @@ var QwenAdapter = class {
|
|
|
2050
2712
|
const root = this.root();
|
|
2051
2713
|
let projectDirs;
|
|
2052
2714
|
try {
|
|
2053
|
-
projectDirs = await
|
|
2054
|
-
} catch {
|
|
2055
|
-
return [];
|
|
2715
|
+
projectDirs = await readdir11(root);
|
|
2716
|
+
} catch (error) {
|
|
2717
|
+
if (isMissingPathError(error)) return [];
|
|
2718
|
+
throw error;
|
|
2056
2719
|
}
|
|
2057
2720
|
const refs = [];
|
|
2058
2721
|
for (const pd of projectDirs) {
|
|
2059
2722
|
const chatsDir = join11(root, pd, "chats");
|
|
2060
2723
|
let files;
|
|
2061
2724
|
try {
|
|
2062
|
-
files = await
|
|
2063
|
-
} catch {
|
|
2064
|
-
continue;
|
|
2725
|
+
files = await readdir11(chatsDir);
|
|
2726
|
+
} catch (error) {
|
|
2727
|
+
if (isMissingPathError(error)) continue;
|
|
2728
|
+
throw error;
|
|
2065
2729
|
}
|
|
2066
2730
|
if (opts.cwd) continue;
|
|
2067
2731
|
for (const f of files) {
|
|
@@ -2070,8 +2734,9 @@ var QwenAdapter = class {
|
|
|
2070
2734
|
let st;
|
|
2071
2735
|
try {
|
|
2072
2736
|
st = await stat10(path);
|
|
2073
|
-
} catch {
|
|
2074
|
-
continue;
|
|
2737
|
+
} catch (error) {
|
|
2738
|
+
if (isMissingPathError(error)) continue;
|
|
2739
|
+
throw error;
|
|
2075
2740
|
}
|
|
2076
2741
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
2077
2742
|
refs.push({ harness: this.harness, sessionId: basename7(f, ".jsonl"), path, cwd: null, mtimeMs: st.mtimeMs });
|
|
@@ -2079,36 +2744,32 @@ var QwenAdapter = class {
|
|
|
2079
2744
|
}
|
|
2080
2745
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
2081
2746
|
}
|
|
2082
|
-
async parse(ref) {
|
|
2083
|
-
const
|
|
2084
|
-
const
|
|
2085
|
-
const
|
|
2086
|
-
const spans = [
|
|
2087
|
-
span({
|
|
2088
|
-
traceId,
|
|
2089
|
-
spanId: rootId,
|
|
2090
|
-
parentSpanId: null,
|
|
2091
|
-
name: "session",
|
|
2092
|
-
kind: "AGENT",
|
|
2093
|
-
startTime: records[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2094
|
-
endTime: records.at(-1)?.timestamp,
|
|
2095
|
-
service: SERVICE9,
|
|
2096
|
-
agent: SERVICE9
|
|
2097
|
-
})
|
|
2098
|
-
];
|
|
2747
|
+
async parse(ref, options = {}) {
|
|
2748
|
+
const sourceTraceId = ref.sessionId;
|
|
2749
|
+
const sourceRootId = `root:${sourceTraceId}`;
|
|
2750
|
+
const spans = [];
|
|
2099
2751
|
const openToolsByName = /* @__PURE__ */ new Map();
|
|
2752
|
+
let discoveredTraceId;
|
|
2753
|
+
let firstTimestamp;
|
|
2754
|
+
let lastTimestamp;
|
|
2755
|
+
let sawRecord = false;
|
|
2100
2756
|
let step = 0;
|
|
2101
|
-
|
|
2102
|
-
|
|
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;
|
|
2103
2764
|
const ts = r.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
2104
2765
|
const role = r.message?.role;
|
|
2105
2766
|
if (r.type === "assistant" || role === "model") {
|
|
2106
2767
|
const llmId = `llm:${r.uuid ?? step}`;
|
|
2107
2768
|
spans.push(
|
|
2108
2769
|
span({
|
|
2109
|
-
traceId,
|
|
2770
|
+
traceId: sourceTraceId,
|
|
2110
2771
|
spanId: llmId,
|
|
2111
|
-
parentSpanId:
|
|
2772
|
+
parentSpanId: sourceRootId,
|
|
2112
2773
|
name: "llm.turn",
|
|
2113
2774
|
kind: "LLM",
|
|
2114
2775
|
startTime: ts,
|
|
@@ -2121,13 +2782,12 @@ var QwenAdapter = class {
|
|
|
2121
2782
|
content: capText(textOf9(r.message?.parts)) || null
|
|
2122
2783
|
})
|
|
2123
2784
|
);
|
|
2124
|
-
lastLlm = llmId;
|
|
2125
2785
|
step += 1;
|
|
2126
2786
|
for (const p of r.message?.parts ?? []) {
|
|
2127
2787
|
if (!p.functionCall?.name) continue;
|
|
2128
2788
|
const name = p.functionCall.name;
|
|
2129
2789
|
const t = span({
|
|
2130
|
-
traceId,
|
|
2790
|
+
traceId: sourceTraceId,
|
|
2131
2791
|
spanId: `tool:${name}:${step}`,
|
|
2132
2792
|
parentSpanId: llmId,
|
|
2133
2793
|
name: `tool.${name}`,
|
|
@@ -2137,7 +2797,7 @@ var QwenAdapter = class {
|
|
|
2137
2797
|
agent: SERVICE9,
|
|
2138
2798
|
tool: name,
|
|
2139
2799
|
step,
|
|
2140
|
-
|
|
2800
|
+
extra: toolIoAttributes({ input: p.functionCall.args })
|
|
2141
2801
|
});
|
|
2142
2802
|
spans.push(t);
|
|
2143
2803
|
const q = openToolsByName.get(name) ?? [];
|
|
@@ -2150,9 +2810,9 @@ var QwenAdapter = class {
|
|
|
2150
2810
|
if (prompt) {
|
|
2151
2811
|
spans.push(
|
|
2152
2812
|
userPromptSpan({
|
|
2153
|
-
traceId,
|
|
2813
|
+
traceId: sourceTraceId,
|
|
2154
2814
|
spanId: `user:${r.uuid ?? step}`,
|
|
2155
|
-
parentSpanId:
|
|
2815
|
+
parentSpanId: sourceRootId,
|
|
2156
2816
|
startTime: ts,
|
|
2157
2817
|
service: SERVICE9,
|
|
2158
2818
|
agent: SERVICE9,
|
|
@@ -2171,11 +2831,29 @@ var QwenAdapter = class {
|
|
|
2171
2831
|
if (t) {
|
|
2172
2832
|
t.end_time = ts;
|
|
2173
2833
|
t.status = err ? { code: "ERROR", message: "tool result reported error" } : { code: "OK" };
|
|
2834
|
+
recordToolOutput(t, p.functionResponse?.response ?? r.toolCallResult?.error);
|
|
2174
2835
|
}
|
|
2175
2836
|
}
|
|
2176
2837
|
}
|
|
2177
2838
|
}
|
|
2178
|
-
|
|
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];
|
|
2179
2857
|
}
|
|
2180
2858
|
};
|
|
2181
2859
|
|
|
@@ -2575,14 +3253,25 @@ async function stampSpanWorkdirRepoAttrs(spans) {
|
|
|
2575
3253
|
}
|
|
2576
3254
|
|
|
2577
3255
|
// src/session-source.ts
|
|
2578
|
-
async function parseSession(adapter, ref) {
|
|
2579
|
-
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);
|
|
2580
3261
|
const repo = await resolveSessionRepoAttrs(ref.cwd, spans);
|
|
2581
3262
|
if (repo.cwd) ref.cwd = repo.cwd;
|
|
2582
3263
|
stampRepoAttrs(spans, repo.attrs);
|
|
2583
3264
|
await stampSpanWorkdirRepoAttrs(spans);
|
|
2584
3265
|
return spans;
|
|
2585
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
|
+
};
|
|
2586
3275
|
function refKey(ref) {
|
|
2587
3276
|
return `${ref.harness}\0${ref.path}\0${ref.sessionId}`;
|
|
2588
3277
|
}
|
|
@@ -2592,7 +3281,7 @@ async function locateSessions(adapter, opts = {}) {
|
|
|
2592
3281
|
const refs = await adapter.locate({ sinceMs: opts.sinceMs });
|
|
2593
3282
|
const byKey = /* @__PURE__ */ new Map();
|
|
2594
3283
|
for (const ref of refs) {
|
|
2595
|
-
if (!cwdMatchesSelection(ref.cwd, cwdSelections)) continue;
|
|
3284
|
+
if (!cwdMatchesSelection(ref.cwd, cwdSelections) && !(ref.cwd === null && ref.integrity)) continue;
|
|
2596
3285
|
byKey.set(refKey(ref), ref);
|
|
2597
3286
|
}
|
|
2598
3287
|
return [...byKey.values()].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
@@ -2604,7 +3293,8 @@ async function* scanSessions(opts) {
|
|
|
2604
3293
|
try {
|
|
2605
3294
|
refs = await locateSessions(adapter, { cwd: opts.cwd, sinceMs: opts.sinceMs });
|
|
2606
3295
|
} catch (err) {
|
|
2607
|
-
opts.onError
|
|
3296
|
+
if (!opts.onError) throw err;
|
|
3297
|
+
opts.onError(err);
|
|
2608
3298
|
continue;
|
|
2609
3299
|
}
|
|
2610
3300
|
if (opts.last && opts.last > 0) refs = refs.slice(0, opts.last);
|
|
@@ -2612,12 +3302,12 @@ async function* scanSessions(opts) {
|
|
|
2612
3302
|
if (opts.signal?.aborted) return;
|
|
2613
3303
|
let spans;
|
|
2614
3304
|
try {
|
|
2615
|
-
spans = await parseSession(adapter, ref);
|
|
3305
|
+
spans = await parseSession(adapter, ref, { corruptionMode: opts.corruptionMode });
|
|
2616
3306
|
} catch (err) {
|
|
2617
|
-
opts.onError
|
|
3307
|
+
if (!opts.onError) throw err;
|
|
3308
|
+
opts.onError(err, ref);
|
|
2618
3309
|
continue;
|
|
2619
3310
|
}
|
|
2620
|
-
if (spans.length === 0) continue;
|
|
2621
3311
|
yield { adapter, ref, spans };
|
|
2622
3312
|
}
|
|
2623
3313
|
}
|
|
@@ -2627,16 +3317,16 @@ async function* scanSessions(opts) {
|
|
|
2627
3317
|
import { mkdtemp, writeFile } from "fs/promises";
|
|
2628
3318
|
import { tmpdir } from "os";
|
|
2629
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";
|
|
2630
3324
|
function stringAttr(span2, key) {
|
|
2631
3325
|
const value = span2.attributes[key];
|
|
2632
3326
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2633
3327
|
}
|
|
2634
|
-
function numberAttr(span2, key) {
|
|
2635
|
-
const value = span2.attributes[key];
|
|
2636
|
-
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
2637
|
-
}
|
|
2638
3328
|
function spanKind(span2) {
|
|
2639
|
-
return stringAttr(span2,
|
|
3329
|
+
return stringAttr(span2, OPENINFERENCE_SPAN_KIND2);
|
|
2640
3330
|
}
|
|
2641
3331
|
function repoFromSpans(spans) {
|
|
2642
3332
|
const attrs = {};
|
|
@@ -2662,7 +3352,7 @@ function summarizeTools(spans) {
|
|
|
2662
3352
|
const byTool = /* @__PURE__ */ new Map();
|
|
2663
3353
|
for (const span2 of spans) {
|
|
2664
3354
|
if (spanKind(span2) !== "TOOL") continue;
|
|
2665
|
-
const name = stringAttr(span2,
|
|
3355
|
+
const name = stringAttr(span2, TOOL_NAME2) ?? span2.name.replace(/^tool\./, "");
|
|
2666
3356
|
const current = byTool.get(name) ?? { calls: 0, errors: 0 };
|
|
2667
3357
|
current.calls += 1;
|
|
2668
3358
|
if (span2.status.code === "ERROR") current.errors += 1;
|
|
@@ -2671,7 +3361,6 @@ function summarizeTools(spans) {
|
|
|
2671
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));
|
|
2672
3362
|
}
|
|
2673
3363
|
async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
|
|
2674
|
-
const llmSpans = spans.filter((span2) => spanKind(span2) === "LLM");
|
|
2675
3364
|
const toolSpans = spans.filter((span2) => spanKind(span2) === "TOOL");
|
|
2676
3365
|
const erroredToolCallCount = toolSpans.filter((span2) => span2.status.code === "ERROR").length;
|
|
2677
3366
|
const pipelines = await runPipelines(spans, { minLoopOccurrences: opts.minLoopOccurrences });
|
|
@@ -2679,10 +3368,14 @@ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
|
|
|
2679
3368
|
const loopFindings = pipelines.stuckLoops.findings;
|
|
2680
3369
|
const { firstSpanAt, lastSpanAt } = timeBounds(spans);
|
|
2681
3370
|
const repo = repoFromSpans(spans);
|
|
3371
|
+
const execution = summarizeSpanExecution(spans, {
|
|
3372
|
+
experimentId: `traces-policy-evidence:${ref.sessionId}`
|
|
3373
|
+
});
|
|
2682
3374
|
return {
|
|
2683
3375
|
schemaVersion: 1,
|
|
2684
3376
|
kind: "traces.policy_evidence.session",
|
|
2685
3377
|
generatedAt: opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3378
|
+
execution,
|
|
2686
3379
|
session: {
|
|
2687
3380
|
harness: ref.harness,
|
|
2688
3381
|
sessionId: ref.sessionId,
|
|
@@ -2693,12 +3386,12 @@ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
|
|
|
2693
3386
|
repo,
|
|
2694
3387
|
metrics: {
|
|
2695
3388
|
spanCount: spans.length,
|
|
2696
|
-
llmTurnCount:
|
|
3389
|
+
llmTurnCount: execution.execution.modelCalls.events,
|
|
2697
3390
|
toolCallCount: toolSpans.length,
|
|
2698
3391
|
erroredToolCallCount,
|
|
2699
|
-
inputTokens:
|
|
2700
|
-
outputTokens:
|
|
2701
|
-
models:
|
|
3392
|
+
inputTokens: execution.execution.tokenUsage.totals.input,
|
|
3393
|
+
outputTokens: execution.execution.tokenUsage.totals.output,
|
|
3394
|
+
models: execution.execution.models.map(({ model }) => model),
|
|
2702
3395
|
tools: summarizeTools(spans),
|
|
2703
3396
|
firstSpanAt,
|
|
2704
3397
|
lastSpanAt
|
|
@@ -2880,6 +3573,7 @@ function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
|
|
|
2880
3573
|
const report = { redactionCount: 0, byRule: {} };
|
|
2881
3574
|
const out = spans.map((s) => {
|
|
2882
3575
|
const attributes = redactValue(s.attributes, rules, report).value;
|
|
3576
|
+
normalizeToolIoAttributes(attributes);
|
|
2883
3577
|
let status = s.status;
|
|
2884
3578
|
if (status.message) {
|
|
2885
3579
|
const m = redactValue(status.message, rules, report).value;
|
|
@@ -2890,33 +3584,66 @@ function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
|
|
|
2890
3584
|
return { spans: out, report };
|
|
2891
3585
|
}
|
|
2892
3586
|
async function applyRedactor(spans, redactor) {
|
|
2893
|
-
const
|
|
3587
|
+
const capturedKeys = ["content", ...TOOL_IO_VALUE_KEYS];
|
|
3588
|
+
const fields = [];
|
|
2894
3589
|
const texts = [];
|
|
2895
3590
|
spans.forEach((s, i) => {
|
|
2896
|
-
const
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
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);
|
|
2900
3601
|
}
|
|
2901
3602
|
});
|
|
2902
3603
|
const out = spans.map((s) => ({ ...s, attributes: { ...s.attributes } }));
|
|
2903
|
-
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
|
+
}
|
|
2904
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
|
+
}
|
|
2905
3612
|
let changed = 0;
|
|
2906
|
-
|
|
3613
|
+
fields.forEach(({ spanIndex, key }, k) => {
|
|
2907
3614
|
if (scrubbed[k] !== texts[k]) {
|
|
2908
|
-
|
|
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
|
+
}
|
|
2909
3620
|
changed += 1;
|
|
2910
3621
|
}
|
|
2911
3622
|
});
|
|
3623
|
+
for (const span2 of out) normalizeToolIoAttributes(span2.attributes);
|
|
2912
3624
|
return { spans: out, changed };
|
|
2913
3625
|
}
|
|
2914
3626
|
|
|
2915
3627
|
// src/file-export.ts
|
|
2916
|
-
import { createHash } from "crypto";
|
|
2917
|
-
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";
|
|
2918
3630
|
import { tmpdir as tmpdir2 } from "os";
|
|
2919
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";
|
|
2920
3647
|
function isObject(value) {
|
|
2921
3648
|
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
2922
3649
|
}
|
|
@@ -2936,7 +3663,7 @@ function stableJson(value) {
|
|
|
2936
3663
|
});
|
|
2937
3664
|
}
|
|
2938
3665
|
function hashId(value, chars = 16) {
|
|
2939
|
-
return
|
|
3666
|
+
return createHash4("sha256").update(stableJson(value)).digest("hex").slice(0, chars);
|
|
2940
3667
|
}
|
|
2941
3668
|
function isoTime(value) {
|
|
2942
3669
|
if (typeof value === "string") {
|
|
@@ -3039,6 +3766,11 @@ function requireOpenInferenceRows(rows) {
|
|
|
3039
3766
|
return rows;
|
|
3040
3767
|
}
|
|
3041
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
|
+
}
|
|
3042
3774
|
if (!rows.every(isIntelligenceSpanRow)) throw new Error("intelligence-spans input must contain only Intelligence span rows");
|
|
3043
3775
|
return rows;
|
|
3044
3776
|
}
|
|
@@ -3070,6 +3802,7 @@ function policyEvidenceToSpans(records) {
|
|
|
3070
3802
|
"traces.metrics.errored_tool_call_count": record.metrics.erroredToolCallCount,
|
|
3071
3803
|
"traces.metrics.input_tokens": record.metrics.inputTokens,
|
|
3072
3804
|
"traces.metrics.output_tokens": record.metrics.outputTokens,
|
|
3805
|
+
"traces.execution": stableJson(record.execution),
|
|
3073
3806
|
"traces.metrics.models": record.metrics.models,
|
|
3074
3807
|
"traces.metrics.tools": stableJson(record.metrics.tools),
|
|
3075
3808
|
"traces.signals.stuck_loop_count": record.signals.stuckLoopCount,
|
|
@@ -3097,6 +3830,11 @@ function policyEvidenceToSpans(records) {
|
|
|
3097
3830
|
endTime: end,
|
|
3098
3831
|
service: record.session.harness,
|
|
3099
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,
|
|
3100
3838
|
content: capText(stableJson({
|
|
3101
3839
|
session: record.session,
|
|
3102
3840
|
repo: record.repo,
|
|
@@ -3127,6 +3865,16 @@ function findStringKey(value, keys, depth = 4) {
|
|
|
3127
3865
|
}
|
|
3128
3866
|
return void 0;
|
|
3129
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
|
+
}
|
|
3130
3878
|
function eventTimestamp(row) {
|
|
3131
3879
|
return firstTimeDeep(row, [
|
|
3132
3880
|
"timestamp",
|
|
@@ -3169,7 +3917,8 @@ function eventKind(row, type) {
|
|
|
3169
3917
|
}
|
|
3170
3918
|
function sandboxEventsToSpans(rows, wrapper) {
|
|
3171
3919
|
const context = wrapper ?? {};
|
|
3172
|
-
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)}`;
|
|
3173
3922
|
const service = findStringKey(context, ["service", "harness", "source"], 2) ?? "sandbox-opencode";
|
|
3174
3923
|
const times = rows.flatMap((row) => {
|
|
3175
3924
|
const time = eventTimestamp(row);
|
|
@@ -3205,10 +3954,16 @@ function sandboxEventsToSpans(rows, wrapper) {
|
|
|
3205
3954
|
const { status, message } = eventStatus(row, type);
|
|
3206
3955
|
const { kind, tool } = eventKind(row, type);
|
|
3207
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;
|
|
3208
3958
|
const extra = {
|
|
3209
3959
|
"traces.source_format": "sandbox-events",
|
|
3210
3960
|
"traces.event.type": type,
|
|
3211
|
-
"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
|
+
}) : {}
|
|
3212
3967
|
};
|
|
3213
3968
|
copyDefined(extra, {
|
|
3214
3969
|
"traces.event.id": stringValue(row.id),
|
|
@@ -3233,7 +3988,11 @@ function sandboxEventsToSpans(rows, wrapper) {
|
|
|
3233
3988
|
extra
|
|
3234
3989
|
}));
|
|
3235
3990
|
});
|
|
3236
|
-
return spans;
|
|
3991
|
+
if (!sessionId) return spans;
|
|
3992
|
+
return spans.map((item) => ({
|
|
3993
|
+
...item,
|
|
3994
|
+
attributes: { ...item.attributes, [ATTR.SESSION_ID]: sessionId }
|
|
3995
|
+
}));
|
|
3237
3996
|
}
|
|
3238
3997
|
function statusCode(value) {
|
|
3239
3998
|
return value === "OK" || value === "ERROR" || value === "UNSET" ? value : "UNSET";
|
|
@@ -3257,8 +4016,8 @@ function unixNanoTime(value) {
|
|
|
3257
4016
|
}
|
|
3258
4017
|
function firstNumber(row, attrs, keys) {
|
|
3259
4018
|
for (const key of keys) {
|
|
3260
|
-
const value =
|
|
3261
|
-
if (value
|
|
4019
|
+
const value = firstNumberAttr(row, [key]) ?? firstNumberAttr(attrs, [key]);
|
|
4020
|
+
if (value !== null) return value;
|
|
3262
4021
|
}
|
|
3263
4022
|
return void 0;
|
|
3264
4023
|
}
|
|
@@ -3270,6 +4029,12 @@ var COMPUTED_SPAN_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
|
3270
4029
|
"tool.name",
|
|
3271
4030
|
"llm.input_tokens",
|
|
3272
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,
|
|
3273
4038
|
"step",
|
|
3274
4039
|
"content"
|
|
3275
4040
|
]);
|
|
@@ -3284,10 +4049,27 @@ function preserveRawAttributes(attrs) {
|
|
|
3284
4049
|
}
|
|
3285
4050
|
return preserved;
|
|
3286
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
|
+
}
|
|
3287
4068
|
function intelligenceSpanKind(name, attrs) {
|
|
3288
4069
|
const lowered = name.toLowerCase();
|
|
3289
4070
|
const spanType = stringValue(attrs["span.type"])?.toLowerCase();
|
|
3290
4071
|
if (spanType === "llm_request" || lowered.includes("llm")) return "LLM";
|
|
4072
|
+
if (isToolTelemetrySpan(attrs)) return "CHAIN";
|
|
3291
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) {
|
|
3292
4074
|
return "TOOL";
|
|
3293
4075
|
}
|
|
@@ -3302,9 +4084,12 @@ function intelligenceSpansToSpans(rows) {
|
|
|
3302
4084
|
const endTime = unixNanoTime(row.end_unix_nano) ?? isoTime(row.end_time) ?? stringValue(row.end_time) ?? startTime;
|
|
3303
4085
|
const model = stringValue(row.model) ?? stringValue(attrs["llm.model_name"]) ?? stringValue(attrs.model) ?? stringValue(attrs["gen_ai.request.model"]);
|
|
3304
4086
|
const tool = stringValue(attrs["tool.name"]) ?? stringValue(attrs.tool_name) ?? stringValue(attrs["mcp.tool.name"]);
|
|
3305
|
-
const
|
|
4087
|
+
const kind = intelligenceSpanKind(name, attrs);
|
|
4088
|
+
const sourceStatus = objectValue(row.status);
|
|
4089
|
+
const sessionId = stringValue(row.session_id) ?? sessionIdFromAttributes(attrs);
|
|
3306
4090
|
const extra = {
|
|
3307
4091
|
...preserveRawAttributes(attrs),
|
|
4092
|
+
...kind === "TOOL" ? capturedToolIo(attrs) : {},
|
|
3308
4093
|
"traces.source_format": "intelligence-spans",
|
|
3309
4094
|
"traces.row.index": index
|
|
3310
4095
|
};
|
|
@@ -3318,7 +4103,6 @@ function intelligenceSpansToSpans(rows) {
|
|
|
3318
4103
|
"generation.id": stringValue(row.generation),
|
|
3319
4104
|
"cell.id": stringValue(row.cell_id),
|
|
3320
4105
|
"project.key": stringValue(row.project_key),
|
|
3321
|
-
"llm.cost_usd": numberValue(row.cost_usd),
|
|
3322
4106
|
"redaction.version": stringValue(row.redaction_version),
|
|
3323
4107
|
"traces.received_at": isoTime(row.received_at)
|
|
3324
4108
|
});
|
|
@@ -3327,17 +4111,21 @@ function intelligenceSpansToSpans(rows) {
|
|
|
3327
4111
|
spanId: stringValue(row.id) ?? hashId({ row, field: "span" }),
|
|
3328
4112
|
parentSpanId: stringValue(row.parent_span_id) ?? null,
|
|
3329
4113
|
name,
|
|
3330
|
-
kind
|
|
4114
|
+
kind,
|
|
3331
4115
|
startTime,
|
|
3332
4116
|
endTime,
|
|
3333
|
-
status: statusCode(row.status_code),
|
|
3334
|
-
statusMessage: stringValue(row.status_message),
|
|
4117
|
+
status: statusCode(row.status_code ?? sourceStatus?.code),
|
|
4118
|
+
statusMessage: stringValue(row.status_message) ?? stringValue(sourceStatus?.message),
|
|
3335
4119
|
service: stringValue(attrs["service.name"]) ?? stringValue(attrs.service_name) ?? "tangle-intelligence",
|
|
3336
4120
|
agent: stringValue(attrs["agent.name"]) ?? stringValue(attrs["service.name"]) ?? null,
|
|
3337
4121
|
model,
|
|
3338
4122
|
tool,
|
|
3339
|
-
inputTokens: firstNumber(row, attrs, ["input_tokens",
|
|
3340
|
-
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]),
|
|
3341
4129
|
step: numberValue(attrs["interaction.sequence"]) ?? index,
|
|
3342
4130
|
content: stringValue(attrs.user_prompt) ?? stringValue(attrs["gen_ai.prompt.messages"]) ?? stringValue(attrs["log.content.task"]) ?? null,
|
|
3343
4131
|
extra
|
|
@@ -3350,15 +4138,21 @@ function openInferenceToSpans(rows) {
|
|
|
3350
4138
|
const resource = objectValue(row.resource);
|
|
3351
4139
|
const resourceAttrs = objectValue(resource?.attributes);
|
|
3352
4140
|
if (resourceAttrs) copyDefined(attrs, resourceAttrs);
|
|
3353
|
-
const
|
|
3354
|
-
|
|
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));
|
|
3355
4149
|
const status = objectValue(row.status);
|
|
3356
4150
|
const message = stringValue(status?.message);
|
|
3357
4151
|
return {
|
|
3358
4152
|
trace_id: stringValue(row.trace_id) ?? hashId(row, 32),
|
|
3359
4153
|
span_id: stringValue(row.span_id) ?? hashId({ row, field: "span" }),
|
|
3360
4154
|
parent_span_id: stringValue(row.parent_span_id) || null,
|
|
3361
|
-
name
|
|
4155
|
+
name,
|
|
3362
4156
|
start_time: stringValue(row.start_time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
3363
4157
|
end_time: stringValue(row.end_time) ?? stringValue(row.start_time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
3364
4158
|
status: { code: statusCode(status?.code), ...message ? { message } : {} },
|
|
@@ -3405,7 +4199,40 @@ function exportTraceEvidenceText(text, opts = {}) {
|
|
|
3405
4199
|
return exportTraceEvidenceRows(parsed.rows, opts, parsed.wrapper);
|
|
3406
4200
|
}
|
|
3407
4201
|
async function exportTraceEvidenceFile(inputPath, opts = {}) {
|
|
3408
|
-
|
|
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");
|
|
3409
4236
|
return exportTraceEvidenceText(text, { ...opts, sourcePath: inputPath });
|
|
3410
4237
|
}
|
|
3411
4238
|
async function writeTraceEvidenceExportFile(inputPath, outPath, opts = {}) {
|
|
@@ -3416,7 +4243,7 @@ async function writeTraceEvidenceExportFile(inputPath, outPath, opts = {}) {
|
|
|
3416
4243
|
}
|
|
3417
4244
|
|
|
3418
4245
|
// src/inspect.ts
|
|
3419
|
-
import { mkdtemp as mkdtemp3, readFile as
|
|
4246
|
+
import { mkdtemp as mkdtemp3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
3420
4247
|
import { tmpdir as tmpdir3 } from "os";
|
|
3421
4248
|
import { basename as basename8, join as join15 } from "path";
|
|
3422
4249
|
var severityRank = {
|
|
@@ -3695,14 +4522,14 @@ function assertSessionIndex(value) {
|
|
|
3695
4522
|
}
|
|
3696
4523
|
const candidate = value;
|
|
3697
4524
|
if (candidate.kind !== "traces.session_index" || candidate.schemaVersion !== 1 || !Array.isArray(candidate.sessions)) {
|
|
3698
|
-
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");
|
|
3699
4526
|
}
|
|
3700
4527
|
if (!candidate.totals || typeof candidate.totals !== "object") {
|
|
3701
4528
|
throw new Error("session index is missing totals");
|
|
3702
4529
|
}
|
|
3703
4530
|
}
|
|
3704
4531
|
async function readSessionIndexFile(path) {
|
|
3705
|
-
const parsed = JSON.parse(await
|
|
4532
|
+
const parsed = JSON.parse(await readFile4(path, "utf8"));
|
|
3706
4533
|
assertSessionIndex(parsed);
|
|
3707
4534
|
return parsed;
|
|
3708
4535
|
}
|
|
@@ -3800,6 +4627,8 @@ function analyzeReactions(spans) {
|
|
|
3800
4627
|
|
|
3801
4628
|
// src/report.ts
|
|
3802
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;
|
|
3803
4632
|
var SEVERITY_BADGE = {
|
|
3804
4633
|
critical: "\u{1F534} CRITICAL",
|
|
3805
4634
|
high: "\u{1F7E0} HIGH",
|
|
@@ -3811,23 +4640,105 @@ function deterministicSummaryText(summary) {
|
|
|
3811
4640
|
const parts = [];
|
|
3812
4641
|
if (summary.stuckLoops > 0) parts.push(`${summary.stuckLoops} stuck loop(s)`);
|
|
3813
4642
|
if (summary.reactionSignals > 0) parts.push(`${summary.reactionSignals} human reaction signal(s)`);
|
|
3814
|
-
if (summary.
|
|
4643
|
+
if (summary.failedRuns > 0) parts.push(`${summary.failedRuns} failed run(s)`);
|
|
3815
4644
|
if (parts.length === 0) return "0 deterministic signals";
|
|
3816
4645
|
if (parts.length === 1) return parts[0];
|
|
3817
4646
|
return `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
|
|
3818
4647
|
}
|
|
3819
|
-
function plural(
|
|
3820
|
-
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");
|
|
3821
4732
|
}
|
|
3822
4733
|
function summarizeDeterministicSignals(pipelines, reactions) {
|
|
3823
4734
|
const stuckLoops = pipelines.stuckLoops.findings.length;
|
|
3824
|
-
const reactionSignals = Object.values(reactions.signals).reduce((total,
|
|
3825
|
-
const
|
|
4735
|
+
const reactionSignals = Object.values(reactions.signals).reduce((total, count2) => total + count2, 0);
|
|
4736
|
+
const failedRuns = pipelines.failureClusters.totalFailures;
|
|
3826
4737
|
return {
|
|
3827
4738
|
stuckLoops,
|
|
3828
4739
|
reactionSignals,
|
|
3829
|
-
|
|
3830
|
-
totalSignals: stuckLoops + reactionSignals +
|
|
4740
|
+
failedRuns,
|
|
4741
|
+
totalSignals: stuckLoops + reactionSignals + failedRuns
|
|
3831
4742
|
};
|
|
3832
4743
|
}
|
|
3833
4744
|
function renderReport(result, meta) {
|
|
@@ -3835,14 +4746,93 @@ function renderReport(result, meta) {
|
|
|
3835
4746
|
const findings = [...result.findings].sort(
|
|
3836
4747
|
(a, b) => (SEVERITY_RANK[a.severity] ?? 9) - (SEVERITY_RANK[b.severity] ?? 9)
|
|
3837
4748
|
);
|
|
3838
|
-
const deterministic = meta.deterministic ?? { stuckLoops: 0, reactionSignals: 0,
|
|
3839
|
-
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")}`;
|
|
3840
4751
|
lines.push(`# Trace analysis \u2014 ${meta.harness}`);
|
|
3841
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)`;
|
|
3842
4754
|
lines.push(
|
|
3843
|
-
`${
|
|
4755
|
+
`${sessionSummary}, ${meta.spanCount} spans \u2192 **${findingSummary}** across ${result.per_analyst.length} analyst(s). Analysis cost: $${result.total_cost_usd.toFixed(4)}.`
|
|
3844
4756
|
);
|
|
3845
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
|
+
}
|
|
3846
4836
|
lines.push("| Analyst | Status | Findings | Latency |");
|
|
3847
4837
|
lines.push("|---|---|---|---|");
|
|
3848
4838
|
for (const s of result.per_analyst) {
|
|
@@ -3850,7 +4840,7 @@ function renderReport(result, meta) {
|
|
|
3850
4840
|
}
|
|
3851
4841
|
lines.push("");
|
|
3852
4842
|
if (findings.length === 0 && deterministic.totalSignals === 0) {
|
|
3853
|
-
lines.push("_No
|
|
4843
|
+
lines.push("_No supported behavioral findings in the captured fields._");
|
|
3854
4844
|
} else if (findings.length === 0) {
|
|
3855
4845
|
lines.push(
|
|
3856
4846
|
`_No analyst findings. Deterministic checks found ${deterministicSummaryText(deterministic)}; see sections below._`
|
|
@@ -3871,6 +4861,7 @@ function renderReport(result, meta) {
|
|
|
3871
4861
|
if (f.subject) lines.push(`- **Subject:** \`${f.subject}\``);
|
|
3872
4862
|
lines.push(`- **Confidence:** ${f.confidence}`);
|
|
3873
4863
|
if (f.recommended_action) lines.push(`- **Fix:** ${f.recommended_action}`);
|
|
4864
|
+
if (f.validation_plan) lines.push(`- **Check:** ${f.validation_plan}`);
|
|
3874
4865
|
for (const ev of f.evidence_refs.slice(0, 3)) {
|
|
3875
4866
|
const excerpt = ev.excerpt ? ` \u2014 \`${ev.excerpt.slice(0, 200)}\`` : "";
|
|
3876
4867
|
lines.push(`- **Evidence:** ${ev.kind} ${ev.uri}${excerpt}`);
|
|
@@ -3885,20 +4876,79 @@ function renderReport(result, meta) {
|
|
|
3885
4876
|
return lines.join("\n");
|
|
3886
4877
|
}
|
|
3887
4878
|
function renderPipelines(pr) {
|
|
3888
|
-
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
|
+
}
|
|
3889
4894
|
if (pr.stuckLoops.findings.length === 0) {
|
|
3890
|
-
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).");
|
|
3891
4896
|
} else {
|
|
3892
4897
|
lines.push(`- **Stuck loops:** ${pr.stuckLoops.findings.length} (${(pr.stuckLoops.affectedRunRatio * 100).toFixed(0)}% of runs affected)`);
|
|
3893
4898
|
for (const f of pr.stuckLoops.findings.sort((a, b) => b.occurrences - a.occurrences).slice(0, 10)) {
|
|
3894
4899
|
lines.push(` - \u{1F501} \`${f.toolName}\` \xD7${f.occurrences} with identical args over ${(f.windowMs / 1e3).toFixed(1)}s`);
|
|
3895
4900
|
}
|
|
3896
4901
|
}
|
|
3897
|
-
|
|
3898
|
-
|
|
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";
|
|
3899
4937
|
lines.push(
|
|
3900
|
-
`- **Tool use
|
|
4938
|
+
`- **Tool use:** ${totalCalls} calls across ${runsWithTools.length}/${pr.toolUse.length} traces; ${callsWithCapturedArgs}/${totalCalls} arguments captured; ${duplicateSummary}; ${errorCalls}/${totalCalls} failed${failureFollowUp}`
|
|
3901
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
|
+
}
|
|
3902
4952
|
}
|
|
3903
4953
|
lines.push("");
|
|
3904
4954
|
return lines.join("\n");
|
|
@@ -3952,14 +5002,41 @@ function renderReactions(rr) {
|
|
|
3952
5002
|
}
|
|
3953
5003
|
function renderAdoption(ar) {
|
|
3954
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
|
+
}
|
|
3955
5032
|
lines.push(
|
|
3956
|
-
`- **
|
|
5033
|
+
`- **Materialized skill catalogs/instructions:** ${ar.sessionsWithMaterializedSkills}/${ar.executionGroupCount} observed group(s) \xB7 **SKILL.md tool references:** ${ar.sessionsWithSkillFileReference}/${ar.executionGroupCount} observed group(s)`
|
|
3957
5034
|
);
|
|
3958
5035
|
lines.push(
|
|
3959
5036
|
`- **Explicit skill invocations:** ${ar.totalSkillInvocations} (Skill tool spans) \xB7 **Loop-dispatched runs:** ${ar.totalLoopDispatchedRuns} (from ${ar.skillRunFilesRead} \`.evolve/skill-runs.jsonl\` file(s))`
|
|
3960
5037
|
);
|
|
3961
5038
|
lines.push(
|
|
3962
|
-
`- **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.`
|
|
3963
5040
|
);
|
|
3964
5041
|
lines.push("");
|
|
3965
5042
|
const skillRows = Object.entries(ar.skillInvocations).sort((a, b) => b[1] - a[1]);
|
|
@@ -4002,9 +5079,6 @@ var severityRank2 = {
|
|
|
4002
5079
|
low: 3,
|
|
4003
5080
|
info: 4
|
|
4004
5081
|
};
|
|
4005
|
-
function slug(input) {
|
|
4006
|
-
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72) || "item";
|
|
4007
|
-
}
|
|
4008
5082
|
function json(value) {
|
|
4009
5083
|
return `${JSON.stringify(value, null, 2)}
|
|
4010
5084
|
`;
|
|
@@ -4017,13 +5091,76 @@ function evidence(kind, uri, excerpt) {
|
|
|
4017
5091
|
return excerpt ? { kind, uri, excerpt } : { kind, uri };
|
|
4018
5092
|
}
|
|
4019
5093
|
function totalReactionSignals(reactions) {
|
|
4020
|
-
return Object.values(reactions.signals).reduce((sum,
|
|
5094
|
+
return Object.values(reactions.signals).reduce((sum, count2) => sum + count2, 0);
|
|
4021
5095
|
}
|
|
4022
|
-
function
|
|
4023
|
-
|
|
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
|
+
});
|
|
4024
5159
|
}
|
|
4025
5160
|
function deterministicFindings(pipelines, reactions, adoption) {
|
|
4026
5161
|
const findings = [];
|
|
5162
|
+
const coverage = traceCoverageFinding(pipelines, adoption);
|
|
5163
|
+
if (coverage) findings.push(coverage);
|
|
4027
5164
|
const loops = pipelines.stuckLoops.findings;
|
|
4028
5165
|
if (loops.length > 0) {
|
|
4029
5166
|
const top = [...loops].sort((a, b) => b.occurrences - a.occurrences)[0];
|
|
@@ -4043,19 +5180,34 @@ function deterministicFindings(pipelines, reactions, adoption) {
|
|
|
4043
5180
|
metadata: { source: "traces.pipeline.stuckLoopView", loopCount: loops.length }
|
|
4044
5181
|
}));
|
|
4045
5182
|
}
|
|
4046
|
-
const
|
|
4047
|
-
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);
|
|
4048
5187
|
findings.push(makeFinding({
|
|
4049
5188
|
analyst_id: "traces-deterministic",
|
|
4050
|
-
area: "
|
|
4051
|
-
claim: `${
|
|
4052
|
-
severity:
|
|
4053
|
-
rationale: "
|
|
4054
|
-
evidence_refs: [
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
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
|
+
}
|
|
4059
5211
|
}));
|
|
4060
5212
|
}
|
|
4061
5213
|
const corrective = reactions.signals.correction + reactions.signals.frustration + reactions.signals.jargon + reactions.signals.structure;
|
|
@@ -4074,7 +5226,8 @@ function deterministicFindings(pipelines, reactions, adoption) {
|
|
|
4074
5226
|
}));
|
|
4075
5227
|
}
|
|
4076
5228
|
const skillRuns = adoption.totalSkillInvocations + adoption.totalLoopDispatchedRuns;
|
|
4077
|
-
|
|
5229
|
+
const allSessionsMeasurable = adoption.skillTelemetrySessions === adoption.executionGroupCount;
|
|
5230
|
+
if (allSessionsMeasurable && adoption.executionGroupCount > 0 && skillRuns === 0) {
|
|
4078
5231
|
findings.push(makeFinding({
|
|
4079
5232
|
analyst_id: "traces-deterministic",
|
|
4080
5233
|
area: "workflow",
|
|
@@ -4085,7 +5238,7 @@ function deterministicFindings(pipelines, reactions, adoption) {
|
|
|
4085
5238
|
recommended_action: "Create or invoke a narrow skill for the repeated workflow, then let traces compare future sessions against this baseline.",
|
|
4086
5239
|
validation_plan: "Rerun traces and require skill-run attribution to appear for repeated workflows.",
|
|
4087
5240
|
confidence: 0.75,
|
|
4088
|
-
metadata: { source: "traces.adoption",
|
|
5241
|
+
metadata: { source: "traces.adoption", executionGroupCount: adoption.executionGroupCount }
|
|
4089
5242
|
}));
|
|
4090
5243
|
}
|
|
4091
5244
|
return findings;
|
|
@@ -4146,41 +5299,6 @@ function normalizeExternalFindings(results) {
|
|
|
4146
5299
|
}
|
|
4147
5300
|
return out;
|
|
4148
5301
|
}
|
|
4149
|
-
function sourceOf(finding2) {
|
|
4150
|
-
if (finding2.analyst_id.startsWith("external:")) return "external";
|
|
4151
|
-
if (finding2.analyst_id === "traces-deterministic") return "deterministic";
|
|
4152
|
-
return "analyst";
|
|
4153
|
-
}
|
|
4154
|
-
function recommendationTitle(finding2) {
|
|
4155
|
-
if (finding2.recommended_action) return finding2.recommended_action.replace(/\.$/, "");
|
|
4156
|
-
return `Investigate ${finding2.area}: ${finding2.claim}`.replace(/\.$/, "");
|
|
4157
|
-
}
|
|
4158
|
-
function buildRecommendations(findings) {
|
|
4159
|
-
return [...findings].filter((finding2) => finding2.severity !== "info").sort((a, b) => severityRank2[a.severity] - severityRank2[b.severity] || b.confidence - a.confidence || a.finding_id.localeCompare(b.finding_id)).slice(0, 20).map((finding2, index) => ({
|
|
4160
|
-
schemaVersion: 1,
|
|
4161
|
-
id: `rec-${index + 1}-${slug(finding2.area)}`,
|
|
4162
|
-
severity: finding2.severity,
|
|
4163
|
-
title: recommendationTitle(finding2),
|
|
4164
|
-
action: finding2.recommended_action ?? `Review and fix: ${finding2.claim}`,
|
|
4165
|
-
rationale: finding2.rationale ?? finding2.claim,
|
|
4166
|
-
evidenceRefs: finding2.evidence_refs,
|
|
4167
|
-
findingIds: [finding2.finding_id],
|
|
4168
|
-
validationPlan: finding2.validation_plan ?? "Rerun `traces improve` on a fresh comparable session window and confirm this finding disappears or drops in severity.",
|
|
4169
|
-
source: sourceOf(finding2)
|
|
4170
|
-
}));
|
|
4171
|
-
}
|
|
4172
|
-
function buildClaims(findings) {
|
|
4173
|
-
return findings.map((finding2, index) => ({
|
|
4174
|
-
schemaVersion: 1,
|
|
4175
|
-
id: `claim-${index + 1}-${slug(finding2.area)}`,
|
|
4176
|
-
severity: finding2.severity,
|
|
4177
|
-
text: finding2.claim,
|
|
4178
|
-
area: finding2.area,
|
|
4179
|
-
sourceFindingId: finding2.finding_id,
|
|
4180
|
-
evidenceRefs: finding2.evidence_refs,
|
|
4181
|
-
validationPlan: finding2.validation_plan
|
|
4182
|
-
}));
|
|
4183
|
-
}
|
|
4184
5302
|
function buildEvidenceRows(findings) {
|
|
4185
5303
|
return findings.flatMap((finding2) => finding2.evidence_refs.map((ref) => ({
|
|
4186
5304
|
schemaVersion: 1,
|
|
@@ -4192,31 +5310,10 @@ function buildEvidenceRows(findings) {
|
|
|
4192
5310
|
evidence: ref
|
|
4193
5311
|
})));
|
|
4194
5312
|
}
|
|
4195
|
-
function renderRecommendations(recommendations) {
|
|
4196
|
-
const lines = ["## recommendations", ""];
|
|
4197
|
-
if (recommendations.length === 0) {
|
|
4198
|
-
lines.push("- No recommendations emitted.");
|
|
4199
|
-
lines.push("");
|
|
4200
|
-
return lines.join("\n");
|
|
4201
|
-
}
|
|
4202
|
-
for (const rec of recommendations.slice(0, 10)) {
|
|
4203
|
-
lines.push(`### ${rec.severity.toUpperCase()} \u2014 ${rec.title}`);
|
|
4204
|
-
lines.push("");
|
|
4205
|
-
lines.push(`- **Action:** ${rec.action}`);
|
|
4206
|
-
lines.push(`- **Why:** ${rec.rationale}`);
|
|
4207
|
-
lines.push(`- **Check:** ${rec.validationPlan}`);
|
|
4208
|
-
for (const ref of rec.evidenceRefs.slice(0, 3)) {
|
|
4209
|
-
lines.push(`- **Evidence:** ${ref.kind} ${ref.uri}${ref.excerpt ? ` \u2014 \`${ref.excerpt.slice(0, 180)}\`` : ""}`);
|
|
4210
|
-
}
|
|
4211
|
-
lines.push("");
|
|
4212
|
-
}
|
|
4213
|
-
return lines.join("\n");
|
|
4214
|
-
}
|
|
4215
5313
|
function renderFindingPacket(packet, title = "Trace finding packet") {
|
|
4216
5314
|
const lines = [`# ${title}`, ""];
|
|
4217
|
-
lines.push(`${packet.findings.length} finding(s)
|
|
5315
|
+
lines.push(`${packet.findings.length} evidence-backed finding(s) with actions and checks.`);
|
|
4218
5316
|
lines.push("");
|
|
4219
|
-
lines.push(renderRecommendations(packet.recommendations));
|
|
4220
5317
|
if (packet.findings.length > 0) {
|
|
4221
5318
|
lines.push("## findings");
|
|
4222
5319
|
lines.push("");
|
|
@@ -4242,33 +5339,10 @@ function buildTraceFindingPacket(opts) {
|
|
|
4242
5339
|
kind: "traces.finding_packet",
|
|
4243
5340
|
generatedAt,
|
|
4244
5341
|
source: opts.source ?? "traces",
|
|
4245
|
-
findings: opts.findings
|
|
4246
|
-
recommendations: buildRecommendations(opts.findings),
|
|
4247
|
-
claims: buildClaims(opts.findings)
|
|
5342
|
+
findings: opts.findings
|
|
4248
5343
|
};
|
|
4249
5344
|
return { ...packet, report: renderFindingPacket(packet, opts.title) };
|
|
4250
5345
|
}
|
|
4251
|
-
function buildDefaultImprovementProposals(input) {
|
|
4252
|
-
return input.recommendations.slice(0, 5).map((recommendation, index) => {
|
|
4253
|
-
const matchingFindings = input.findings.filter((finding2) => recommendation.findingIds.includes(finding2.finding_id));
|
|
4254
|
-
const evidenceRefs = recommendation.evidenceRefs.length > 0 ? recommendation.evidenceRefs : matchingFindings.flatMap((finding2) => finding2.evidence_refs);
|
|
4255
|
-
return {
|
|
4256
|
-
id: `proposal-${index + 1}-${slug(recommendation.title)}`,
|
|
4257
|
-
title: recommendation.title,
|
|
4258
|
-
description: [
|
|
4259
|
-
recommendation.action,
|
|
4260
|
-
"",
|
|
4261
|
-
`Why: ${recommendation.rationale}`,
|
|
4262
|
-
`Validation: ${recommendation.validationPlan}`,
|
|
4263
|
-
"",
|
|
4264
|
-
"This is a proposal-only artifact. It does not mutate code, prompts, skills, tools, MCP config, hooks, subagents, or external systems until a human applies it and reruns validation."
|
|
4265
|
-
].join("\n"),
|
|
4266
|
-
recommendationIds: [recommendation.id],
|
|
4267
|
-
validationCommand: "traces improve --last 1 --dir .traces/improvement",
|
|
4268
|
-
evidenceRefs
|
|
4269
|
-
};
|
|
4270
|
-
});
|
|
4271
|
-
}
|
|
4272
5346
|
function renderExternal(results) {
|
|
4273
5347
|
if (results.length === 0) return "";
|
|
4274
5348
|
const lines = ["## external analyzers", ""];
|
|
@@ -4284,11 +5358,13 @@ function renderInvestigationReport(result, analystResult) {
|
|
|
4284
5358
|
const base = `${renderReport({ ...analystResult, findings: [...result.findings] }, {
|
|
4285
5359
|
harness: result.harness,
|
|
4286
5360
|
sessionCount: result.sessionCount,
|
|
5361
|
+
unassignedTraceCount: result.unassignedTraceCount,
|
|
4287
5362
|
spanCount: result.spanCount,
|
|
4288
5363
|
otlpPath: result.otlpPath,
|
|
4289
|
-
|
|
5364
|
+
execution: result.execution,
|
|
5365
|
+
deterministic: summarizeDeterministicSignals(result.pipelines, result.reactions),
|
|
5366
|
+
sources: result.sources
|
|
4290
5367
|
})}
|
|
4291
|
-
${renderRecommendations(result.recommendations)}
|
|
4292
5368
|
${renderPipelines(result.pipelines)}
|
|
4293
5369
|
${renderReactions(result.reactions)}
|
|
4294
5370
|
${renderAdoption(result.adoption)}`;
|
|
@@ -4365,19 +5441,18 @@ async function runTraceInvestigation(opts) {
|
|
|
4365
5441
|
...deterministicFindings(pipelines, reactions, adoption),
|
|
4366
5442
|
...normalizeExternalFindings(external)
|
|
4367
5443
|
];
|
|
4368
|
-
const recommendations = buildRecommendations(findings);
|
|
4369
|
-
const claims = buildClaims(findings);
|
|
4370
5444
|
const partial = {
|
|
4371
5445
|
schemaVersion: 1,
|
|
4372
5446
|
kind: "traces.investigation",
|
|
4373
5447
|
generatedAt,
|
|
4374
5448
|
harness: opts.harness,
|
|
4375
|
-
sessionCount:
|
|
5449
|
+
sessionCount: adoption.identifiedSessionCount,
|
|
5450
|
+
unassignedTraceCount: adoption.unassignedTraceCount,
|
|
5451
|
+
sources: opts.sources,
|
|
4376
5452
|
spanCount: opts.spans.length,
|
|
4377
5453
|
otlpPath: analysis.otlpPath,
|
|
5454
|
+
execution: analysis.execution,
|
|
4378
5455
|
findings,
|
|
4379
|
-
recommendations,
|
|
4380
|
-
claims,
|
|
4381
5456
|
pipelines,
|
|
4382
5457
|
reactions,
|
|
4383
5458
|
adoption,
|
|
@@ -4409,70 +5484,38 @@ async function runTraceStoreInvestigation(opts) {
|
|
|
4409
5484
|
analystResult
|
|
4410
5485
|
};
|
|
4411
5486
|
}
|
|
4412
|
-
function replayProof(result, proposals) {
|
|
4413
|
-
const deterministic = summarizeDeterministicSignals(result.pipelines, result.reactions);
|
|
4414
|
-
return {
|
|
4415
|
-
schemaVersion: 1,
|
|
4416
|
-
kind: "traces.improvement_replay",
|
|
4417
|
-
generatedAt: result.generatedAt,
|
|
4418
|
-
status: proposals.length > 0 ? "proposal-only" : "not-run",
|
|
4419
|
-
candidateApplied: false,
|
|
4420
|
-
baseline: {
|
|
4421
|
-
spanCount: result.spanCount,
|
|
4422
|
-
findingCount: result.findings.length,
|
|
4423
|
-
recommendationCount: result.recommendations.length,
|
|
4424
|
-
stuckLoops: deterministic.stuckLoops,
|
|
4425
|
-
reactionSignals: deterministic.reactionSignals,
|
|
4426
|
-
toolErrorRuns: deterministic.toolErrorRuns
|
|
4427
|
-
},
|
|
4428
|
-
proposals,
|
|
4429
|
-
note: proposals.length > 0 ? "traces produced reviewable proposals only; apply one and rerun traces on a comparable window to populate after metrics." : "no proposal adapter was configured, so no candidate was applied; recommendations remain reviewable guidance."
|
|
4430
|
-
};
|
|
4431
|
-
}
|
|
4432
5487
|
async function writeTraceImprovementArtifacts(result, outDir) {
|
|
4433
5488
|
const directory = outDir ? resolve(outDir) : await mkdtemp4(join16(tmpdir4(), "traces-improvement-"));
|
|
4434
5489
|
await mkdir(directory, { recursive: true });
|
|
4435
5490
|
const paths = {
|
|
4436
5491
|
directory,
|
|
4437
|
-
|
|
4438
|
-
recommendations: join16(directory, "recommendations.json"),
|
|
4439
|
-
proposals: join16(directory, "proposals.json"),
|
|
5492
|
+
result: join16(directory, "result.json"),
|
|
4440
5493
|
evidence: join16(directory, "evidence.jsonl"),
|
|
4441
|
-
claims: join16(directory, "claims.json"),
|
|
4442
5494
|
report: join16(directory, "report.md"),
|
|
4443
|
-
|
|
5495
|
+
traces: result.otlpPath
|
|
4444
5496
|
};
|
|
5497
|
+
const { report, ...machineResult } = result;
|
|
4445
5498
|
await Promise.all([
|
|
4446
|
-
writeFile4(paths.
|
|
4447
|
-
writeFile4(paths.recommendations, json(result.recommendations), "utf8"),
|
|
4448
|
-
writeFile4(paths.proposals, json(result.proposals), "utf8"),
|
|
5499
|
+
writeFile4(paths.result, json(machineResult), "utf8"),
|
|
4449
5500
|
writeFile4(paths.evidence, jsonl(buildEvidenceRows(result.findings)), "utf8"),
|
|
4450
|
-
writeFile4(paths.
|
|
4451
|
-
writeFile4(paths.report, result.report, "utf8"),
|
|
4452
|
-
writeFile4(paths.replay, json(result.replay), "utf8")
|
|
5501
|
+
writeFile4(paths.report, report, "utf8")
|
|
4453
5502
|
]);
|
|
4454
5503
|
return paths;
|
|
4455
5504
|
}
|
|
4456
|
-
async function
|
|
4457
|
-
const
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
otlpPath: investigation.otlpPath
|
|
4464
|
-
};
|
|
4465
|
-
const proposals = opts.adapter ? await opts.adapter.propose(adapterInput) : buildDefaultImprovementProposals(adapterInput);
|
|
4466
|
-
const replay = replayProof(investigation, proposals);
|
|
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
|
+
});
|
|
4467
5512
|
const result = {
|
|
4468
5513
|
...investigation,
|
|
4469
|
-
kind: "traces.improvement"
|
|
4470
|
-
proposals,
|
|
4471
|
-
replay
|
|
5514
|
+
kind: "traces.improvement"
|
|
4472
5515
|
};
|
|
4473
5516
|
return {
|
|
4474
5517
|
...result,
|
|
4475
|
-
artifacts: await writeTraceImprovementArtifacts(result,
|
|
5518
|
+
artifacts: await writeTraceImprovementArtifacts(result, directory)
|
|
4476
5519
|
};
|
|
4477
5520
|
}
|
|
4478
5521
|
async function saveReport(path, report) {
|
|
@@ -4481,7 +5524,7 @@ async function saveReport(path, report) {
|
|
|
4481
5524
|
}
|
|
4482
5525
|
|
|
4483
5526
|
// src/live.ts
|
|
4484
|
-
import { createHash as
|
|
5527
|
+
import { createHash as createHash5 } from "crypto";
|
|
4485
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;
|
|
4486
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;
|
|
4487
5530
|
var CLAIM_DONE_RE = /\b(done|fixed|complete|completed|implemented|merged|verified|all green|tests pass|ready|works now)\b/i;
|
|
@@ -4501,7 +5544,7 @@ var sleep = (ms, signal) => new Promise((resolve2) => {
|
|
|
4501
5544
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4502
5545
|
});
|
|
4503
5546
|
function hash(parts) {
|
|
4504
|
-
return
|
|
5547
|
+
return createHash5("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
|
|
4505
5548
|
}
|
|
4506
5549
|
function asString(value) {
|
|
4507
5550
|
if (typeof value === "string" && value.trim()) return value;
|
|
@@ -4955,7 +5998,7 @@ async function watchSessions(opts) {
|
|
|
4955
5998
|
}
|
|
4956
5999
|
|
|
4957
6000
|
// src/session-index.ts
|
|
4958
|
-
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";
|
|
4959
6002
|
import { tmpdir as tmpdir5 } from "os";
|
|
4960
6003
|
import { dirname as dirname3, join as join17 } from "path";
|
|
4961
6004
|
function indexRow(record) {
|
|
@@ -4977,7 +6020,8 @@ function indexRow(record) {
|
|
|
4977
6020
|
},
|
|
4978
6021
|
models: record.metrics.models,
|
|
4979
6022
|
tools: record.metrics.tools,
|
|
4980
|
-
signals: record.signals
|
|
6023
|
+
signals: record.signals,
|
|
6024
|
+
execution: record.execution
|
|
4981
6025
|
};
|
|
4982
6026
|
}
|
|
4983
6027
|
function uniqueSorted(values) {
|
|
@@ -5072,7 +6116,7 @@ async function summarizeFile(path, kind) {
|
|
|
5072
6116
|
const base = { kind, path, bytes: Number(s.size), mtimeMs: Number(s.mtimeMs) };
|
|
5073
6117
|
if (!path.endsWith(".md") && !path.endsWith(".jsonl")) return base;
|
|
5074
6118
|
try {
|
|
5075
|
-
const text = await
|
|
6119
|
+
const text = await readFile5(path, "utf8");
|
|
5076
6120
|
const lines = countLines(text);
|
|
5077
6121
|
const jsonl2 = path.endsWith(".jsonl") ? summarizeJsonl(text) : void 0;
|
|
5078
6122
|
return {
|
|
@@ -5091,7 +6135,7 @@ async function walkMarkdown(root, kind, maxFiles = 100) {
|
|
|
5091
6135
|
if (out.length >= maxFiles) return;
|
|
5092
6136
|
let entries;
|
|
5093
6137
|
try {
|
|
5094
|
-
entries = await
|
|
6138
|
+
entries = await readdir12(dir, { withFileTypes: true });
|
|
5095
6139
|
} catch {
|
|
5096
6140
|
return;
|
|
5097
6141
|
}
|
|
@@ -5177,10 +6221,10 @@ async function writeSessionIndexFile(index, outPath) {
|
|
|
5177
6221
|
}
|
|
5178
6222
|
|
|
5179
6223
|
// src/upload-state.ts
|
|
5180
|
-
import {
|
|
5181
|
-
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";
|
|
5182
6225
|
import { homedir as homedir11 } from "os";
|
|
5183
6226
|
import { dirname as dirname4, join as join18 } from "path";
|
|
6227
|
+
import { contentHash } from "@tangle-network/agent-eval";
|
|
5184
6228
|
function stateFile() {
|
|
5185
6229
|
const base = process.env.XDG_STATE_HOME ?? join18(homedir11(), ".local", "state");
|
|
5186
6230
|
return join18(base, "tangle-traces", "uploaded.json");
|
|
@@ -5189,16 +6233,14 @@ function uploadKey(harness, sessionId) {
|
|
|
5189
6233
|
return `${harness}:${sessionId}`;
|
|
5190
6234
|
}
|
|
5191
6235
|
function sessionHash(spans) {
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
}
|
|
5197
|
-
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);
|
|
5198
6240
|
}
|
|
5199
6241
|
async function loadState() {
|
|
5200
6242
|
try {
|
|
5201
|
-
return JSON.parse(await
|
|
6243
|
+
return JSON.parse(await readFile6(stateFile(), "utf8"));
|
|
5202
6244
|
} catch {
|
|
5203
6245
|
return {};
|
|
5204
6246
|
}
|
|
@@ -5214,7 +6256,7 @@ function alreadyUploaded(state, harness, sessionId, hash2) {
|
|
|
5214
6256
|
}
|
|
5215
6257
|
|
|
5216
6258
|
// src/upload.ts
|
|
5217
|
-
import { readFile as
|
|
6259
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
5218
6260
|
import { createRequire } from "module";
|
|
5219
6261
|
import { hostname } from "os";
|
|
5220
6262
|
import { basename as basename9, join as join19 } from "path";
|
|
@@ -5233,7 +6275,7 @@ async function planUpload(opts = {}) {
|
|
|
5233
6275
|
spans,
|
|
5234
6276
|
redaction: report,
|
|
5235
6277
|
hash: hash2,
|
|
5236
|
-
isNew:
|
|
6278
|
+
isNew: true
|
|
5237
6279
|
});
|
|
5238
6280
|
}
|
|
5239
6281
|
return { items, state, sinceMs: opts.sinceMs };
|
|
@@ -5241,19 +6283,18 @@ async function planUpload(opts = {}) {
|
|
|
5241
6283
|
async function gitBranch(cwd) {
|
|
5242
6284
|
if (!cwd) return void 0;
|
|
5243
6285
|
try {
|
|
5244
|
-
const head = await
|
|
6286
|
+
const head = await readFile7(join19(cwd, ".git", "HEAD"), "utf8");
|
|
5245
6287
|
const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
|
|
5246
6288
|
return m?.[1]?.trim();
|
|
5247
6289
|
} catch {
|
|
5248
6290
|
return void 0;
|
|
5249
6291
|
}
|
|
5250
6292
|
}
|
|
5251
|
-
async function sessionMeta(item
|
|
6293
|
+
async function sessionMeta(item) {
|
|
5252
6294
|
const meta = {
|
|
5253
6295
|
[ATTR.HARNESS]: item.ref.harness,
|
|
5254
6296
|
[ATTR.SESSION_FILE]: basename9(item.ref.path),
|
|
5255
6297
|
[ATTR.HOST]: hostname(),
|
|
5256
|
-
[ATTR.UPLOADED_AT]: uploadedAt,
|
|
5257
6298
|
[ATTR.UPLOADER]: `tangle-traces@${TRACES_VERSION}`,
|
|
5258
6299
|
[ATTR.REDACTION_VERSION]: REDACTION_VERSION,
|
|
5259
6300
|
[ATTR.REDACTION_COUNT]: item.redaction.redactionCount
|
|
@@ -5287,25 +6328,69 @@ function toTraceSpanEvents(spans, rootMeta) {
|
|
|
5287
6328
|
};
|
|
5288
6329
|
});
|
|
5289
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
|
+
};
|
|
5290
6343
|
function stripSpanContent(spans) {
|
|
5291
6344
|
return spans.map((s) => {
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
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 };
|
|
5295
6351
|
});
|
|
5296
6352
|
}
|
|
5297
|
-
async function
|
|
5298
|
-
let
|
|
6353
|
+
async function prepareOutbound(plan, opts, uploadedAt) {
|
|
6354
|
+
let candidates = plan.items;
|
|
5299
6355
|
if (opts.redactor && !opts.stripContent) {
|
|
5300
|
-
const
|
|
5301
|
-
|
|
6356
|
+
const redactor = opts.redactor;
|
|
6357
|
+
candidates = await Promise.all(
|
|
6358
|
+
candidates.map(async (item) => ({ ...item, spans: (await applyRedactor(item.spans, redactor)).spans }))
|
|
6359
|
+
);
|
|
5302
6360
|
}
|
|
5303
|
-
if (opts.stripContent)
|
|
5304
|
-
|
|
5305
|
-
|
|
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 = {}) {
|
|
5306
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);
|
|
5307
6392
|
if (opts.dryRun) {
|
|
5308
|
-
const allSpans =
|
|
6393
|
+
const allSpans = items.flatMap(({ item }) => item.spans);
|
|
5309
6394
|
const path = await writeOtlpFile(allSpans, opts.otlpOut);
|
|
5310
6395
|
return {
|
|
5311
6396
|
uploadedSessions: 0,
|
|
@@ -5316,6 +6401,15 @@ async function executeUpload(plan, opts = {}) {
|
|
|
5316
6401
|
otlpPath: path
|
|
5317
6402
|
};
|
|
5318
6403
|
}
|
|
6404
|
+
if (items.length === 0) {
|
|
6405
|
+
return {
|
|
6406
|
+
uploadedSessions: 0,
|
|
6407
|
+
skippedSessions: skipped,
|
|
6408
|
+
acceptedSpans: 0,
|
|
6409
|
+
redactionCount,
|
|
6410
|
+
dryRun: false
|
|
6411
|
+
};
|
|
6412
|
+
}
|
|
5319
6413
|
const client = opts.backend ?? hostedClientFromEnv();
|
|
5320
6414
|
if (!client) {
|
|
5321
6415
|
throw new Error(
|
|
@@ -5324,23 +6418,28 @@ async function executeUpload(plan, opts = {}) {
|
|
|
5324
6418
|
}
|
|
5325
6419
|
const state = plan.state;
|
|
5326
6420
|
let acceptedSpans = 0;
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
const events =
|
|
5330
|
-
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}`;
|
|
5331
6426
|
const res = await client.ingestTraces(events, idempotencyKey);
|
|
6427
|
+
if (res.accepted !== events.length) throw new PartialUploadError(key, res.accepted, events.length);
|
|
5332
6428
|
acceptedSpans += res.accepted;
|
|
5333
|
-
|
|
5334
|
-
|
|
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,
|
|
5335
6435
|
uploadedAt,
|
|
5336
6436
|
harness: item.ref.harness,
|
|
5337
|
-
spanCount
|
|
6437
|
+
spanCount
|
|
5338
6438
|
};
|
|
5339
|
-
opts.log?.(`uploaded ${item.ref.harness} ${item.ref.sessionId.slice(0, 8)} \u2014 ${res.accepted} spans`);
|
|
5340
6439
|
}
|
|
5341
6440
|
await saveState(state);
|
|
5342
6441
|
return {
|
|
5343
|
-
uploadedSessions:
|
|
6442
|
+
uploadedSessions: items.length,
|
|
5344
6443
|
skippedSessions: skipped,
|
|
5345
6444
|
acceptedSpans,
|
|
5346
6445
|
redactionCount,
|
|
@@ -5349,25 +6448,38 @@ async function executeUpload(plan, opts = {}) {
|
|
|
5349
6448
|
}
|
|
5350
6449
|
|
|
5351
6450
|
export {
|
|
6451
|
+
ATTR,
|
|
6452
|
+
INGEST_SOURCE_CLI,
|
|
6453
|
+
DEFAULT_HARNESS,
|
|
6454
|
+
sessionIdFromAttributes,
|
|
6455
|
+
indexSessionIdsByTrace,
|
|
6456
|
+
stampSessionIdentity,
|
|
5352
6457
|
countSkillRunsJsonl,
|
|
5353
6458
|
analyzeAdoption,
|
|
5354
6459
|
span,
|
|
5355
6460
|
toOpenInferenceSpan,
|
|
5356
6461
|
serializeSpans,
|
|
5357
6462
|
writeOtlpFile,
|
|
6463
|
+
summarizeOtlpExecution,
|
|
6464
|
+
summarizeSpanExecution,
|
|
5358
6465
|
analyzeSpans,
|
|
5359
|
-
ATTR,
|
|
5360
|
-
INGEST_SOURCE_CLI,
|
|
5361
|
-
DEFAULT_HARNESS,
|
|
5362
|
-
parseIsoToEpochMs,
|
|
5363
|
-
parseSince,
|
|
5364
|
-
toRuntimeStore,
|
|
5365
|
-
runPipelines,
|
|
5366
6466
|
CONTENT_CAP,
|
|
5367
6467
|
capText,
|
|
5368
6468
|
ACTOR_ATTR,
|
|
5369
6469
|
userPromptSpan,
|
|
6470
|
+
parseIsoToEpochMs,
|
|
6471
|
+
parseSince,
|
|
6472
|
+
toRuntimeStore,
|
|
6473
|
+
runPipelines,
|
|
6474
|
+
recordSessionCorruption,
|
|
6475
|
+
sessionIntegrityAttributes,
|
|
6476
|
+
stampSessionIntegrity,
|
|
6477
|
+
JsonSourceError,
|
|
6478
|
+
readJsonFile,
|
|
5370
6479
|
AmpAdapter,
|
|
6480
|
+
JsonlParseError,
|
|
6481
|
+
readJsonl,
|
|
6482
|
+
takeJsonl,
|
|
5371
6483
|
textIsSynthetic,
|
|
5372
6484
|
looksLikeAgentPrompt,
|
|
5373
6485
|
claudeActor,
|
|
@@ -5395,6 +6507,7 @@ export {
|
|
|
5395
6507
|
stampRepoAttrs,
|
|
5396
6508
|
stampSpanWorkdirRepoAttrs,
|
|
5397
6509
|
parseSession,
|
|
6510
|
+
EmptySessionError,
|
|
5398
6511
|
locateSessions,
|
|
5399
6512
|
scanSessions,
|
|
5400
6513
|
buildPolicyEvidenceRecord,
|
|
@@ -5421,6 +6534,7 @@ export {
|
|
|
5421
6534
|
writeInspectionReportFile,
|
|
5422
6535
|
REACTIONS,
|
|
5423
6536
|
analyzeReactions,
|
|
6537
|
+
CORRUPTION_RECEIPT_DISPLAY_LIMIT,
|
|
5424
6538
|
summarizeDeterministicSignals,
|
|
5425
6539
|
renderReport,
|
|
5426
6540
|
renderPipelines,
|
|
@@ -5432,7 +6546,7 @@ export {
|
|
|
5432
6546
|
runTraceInvestigation,
|
|
5433
6547
|
runTraceStoreInvestigation,
|
|
5434
6548
|
writeTraceImprovementArtifacts,
|
|
5435
|
-
|
|
6549
|
+
runTraceImprovement,
|
|
5436
6550
|
saveReport,
|
|
5437
6551
|
classifyLiveActions,
|
|
5438
6552
|
defaultTraceLiveAnalysts,
|
|
@@ -5448,10 +6562,12 @@ export {
|
|
|
5448
6562
|
writeSessionIndexFile,
|
|
5449
6563
|
uploadKey,
|
|
5450
6564
|
sessionHash,
|
|
6565
|
+
outboundHash,
|
|
5451
6566
|
loadState,
|
|
5452
6567
|
saveState,
|
|
5453
6568
|
alreadyUploaded,
|
|
5454
6569
|
planUpload,
|
|
5455
6570
|
toTraceSpanEvents,
|
|
6571
|
+
PartialUploadError,
|
|
5456
6572
|
executeUpload
|
|
5457
6573
|
};
|