@workbench-ai/workbench 0.0.46

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.
Files changed (39) hide show
  1. package/dist/adapter-project.d.ts +29 -0
  2. package/dist/adapter-project.d.ts.map +1 -0
  3. package/dist/adapter-project.js +363 -0
  4. package/dist/benchmark-fingerprint.d.ts +6 -0
  5. package/dist/benchmark-fingerprint.d.ts.map +1 -0
  6. package/dist/benchmark-fingerprint.js +101 -0
  7. package/dist/command-model.d.ts +5 -0
  8. package/dist/command-model.d.ts.map +1 -0
  9. package/dist/command-model.js +558 -0
  10. package/dist/dev-open/client.css +8157 -0
  11. package/dist/dev-open/client.js +252596 -0
  12. package/dist/dev-open/fonts/geist-cyrillic-wght-normal.woff2 +0 -0
  13. package/dist/dev-open/fonts/geist-latin-ext-wght-normal.woff2 +0 -0
  14. package/dist/dev-open/fonts/geist-latin-wght-normal.woff2 +0 -0
  15. package/dist/dev-open-server.d.ts +57 -0
  16. package/dist/dev-open-server.d.ts.map +1 -0
  17. package/dist/dev-open-server.js +496 -0
  18. package/dist/index.d.ts +10 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +3943 -0
  21. package/dist/init-scaffold.d.ts +22 -0
  22. package/dist/init-scaffold.d.ts.map +1 -0
  23. package/dist/init-scaffold.js +30 -0
  24. package/dist/init-template-pack.d.ts +19 -0
  25. package/dist/init-template-pack.d.ts.map +1 -0
  26. package/dist/init-template-pack.js +250 -0
  27. package/dist/local-archive.d.ts +23 -0
  28. package/dist/local-archive.d.ts.map +1 -0
  29. package/dist/local-archive.js +741 -0
  30. package/dist/project-source.d.ts +51 -0
  31. package/dist/project-source.d.ts.map +1 -0
  32. package/dist/project-source.js +700 -0
  33. package/dist/workbench.d.ts +3 -0
  34. package/dist/workbench.d.ts.map +1 -0
  35. package/dist/workbench.js +4 -0
  36. package/dist/workspace-snapshot.d.ts +10 -0
  37. package/dist/workspace-snapshot.d.ts.map +1 -0
  38. package/dist/workspace-snapshot.js +81 -0
  39. package/package.json +45 -0
@@ -0,0 +1,741 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { finalizeWorkbenchExecutionTraceForJob, selectExecutionOutputFilesForInspection, } from "@workbench-ai/workbench-core";
4
+ const RUNTIME_DIR = ".workbench/runtime";
5
+ export function localRuntimeDir(workspace) {
6
+ return path.join(workspace, RUNTIME_DIR);
7
+ }
8
+ export async function loadLocalArchive(workspace) {
9
+ const root = localRuntimeDir(workspace);
10
+ const [state, subjects, evaluations, runs, events] = await Promise.all([
11
+ readJson(path.join(root, "state.json"), {}),
12
+ readRecords(path.join(root, "subjects"), "record.json"),
13
+ readFlatRecords(path.join(root, "evaluations")),
14
+ readFlatRecords(path.join(root, "runs")),
15
+ readJson(path.join(root, "events.json"), []),
16
+ ]);
17
+ const subjectFiles = {};
18
+ await Promise.all(subjects.map(async (subject) => {
19
+ subjectFiles[subject.id] = await readSurfaceFiles(path.join(root, "subjects", subject.id, "files"));
20
+ }));
21
+ const snapshot = {
22
+ activeId: typeof state.activeId === "string" ? state.activeId : null,
23
+ subjects: subjects.sort((left, right) => left.ordinal - right.ordinal || left.id.localeCompare(right.id)),
24
+ subjectFiles,
25
+ evaluations: evaluations.sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id)),
26
+ runs: runs.sort((left, right) => left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id)),
27
+ events: events.sort((left, right) => left.at.localeCompare(right.at) || left.id.localeCompare(right.id)),
28
+ };
29
+ validateLocalArchiveSnapshot(snapshot);
30
+ return snapshot;
31
+ }
32
+ export async function saveLocalArchive(workspace, snapshot) {
33
+ const root = localRuntimeDir(workspace);
34
+ await fs.mkdir(root, { recursive: true });
35
+ await writeJson(path.join(root, "state.json"), { activeId: snapshot.activeId });
36
+ await fs.rm(path.join(root, "subjects"), { force: true, recursive: true });
37
+ await fs.rm(path.join(root, "evaluations"), { force: true, recursive: true });
38
+ await fs.rm(path.join(root, "runs"), { force: true, recursive: true });
39
+ await Promise.all([
40
+ fs.mkdir(path.join(root, "subjects"), { recursive: true }),
41
+ fs.mkdir(path.join(root, "evaluations"), { recursive: true }),
42
+ fs.mkdir(path.join(root, "runs"), { recursive: true }),
43
+ ]);
44
+ for (const subject of snapshot.subjects) {
45
+ const subjectRoot = path.join(root, "subjects", subject.id);
46
+ await fs.mkdir(subjectRoot, { recursive: true });
47
+ await writeJson(path.join(subjectRoot, "record.json"), subject);
48
+ await writeSurfaceFiles(path.join(subjectRoot, "files"), snapshot.subjectFiles[subject.id] ?? []);
49
+ }
50
+ for (const evaluation of snapshot.evaluations) {
51
+ await writeJson(path.join(root, "evaluations", `${evaluation.id}.json`), evaluation);
52
+ }
53
+ for (const run of snapshot.runs) {
54
+ await writeJson(path.join(root, "runs", `${run.id}.json`), run);
55
+ }
56
+ await writeJson(path.join(root, "events.json"), snapshot.events);
57
+ }
58
+ export async function saveLocalJobs(workspace, jobs) {
59
+ if (jobs.length === 0) {
60
+ return;
61
+ }
62
+ const root = localRuntimeDir(workspace);
63
+ const jobsDir = path.join(root, "jobs");
64
+ const executionFilesDir = path.join(root, "execution-files");
65
+ await Promise.all([
66
+ fs.mkdir(jobsDir, { recursive: true }),
67
+ fs.mkdir(executionFilesDir, { recursive: true }),
68
+ ]);
69
+ for (const job of jobs) {
70
+ const safeJobId = localRecordName(job.id);
71
+ const traceSourceFiles = filterArchivedExecutionFiles(completedJobOutputFiles(job));
72
+ const outputFiles = selectExecutionOutputFilesForInspection({
73
+ purpose: readExecutionPurpose(job),
74
+ files: traceSourceFiles,
75
+ output: jsonRecord(job.output),
76
+ });
77
+ await writeJson(path.join(jobsDir, `${safeJobId}.json`), archivedLocalJob(job, outputFiles, traceSourceFiles));
78
+ const filesRoot = path.join(executionFilesDir, safeJobId);
79
+ await fs.rm(filesRoot, { force: true, recursive: true });
80
+ await writeSurfaceFiles(filesRoot, outputFiles);
81
+ }
82
+ }
83
+ export async function readLocalExecutionFiles(workspace, jobId) {
84
+ return await readSurfaceFiles(path.join(localRuntimeDir(workspace), "execution-files", localRecordName(jobId)));
85
+ }
86
+ export function upsertLocalSubject(snapshot, subject, files) {
87
+ return {
88
+ ...snapshot,
89
+ subjects: [
90
+ ...snapshot.subjects.filter((entry) => entry.id !== subject.id),
91
+ subject,
92
+ ].sort((left, right) => left.ordinal - right.ordinal || left.id.localeCompare(right.id)),
93
+ subjectFiles: {
94
+ ...snapshot.subjectFiles,
95
+ [subject.id]: files.map((file) => ({ ...file })),
96
+ },
97
+ };
98
+ }
99
+ export function upsertLocalEvaluation(snapshot, evaluation) {
100
+ return {
101
+ ...snapshot,
102
+ evaluations: [
103
+ ...snapshot.evaluations.filter((entry) => entry.id !== evaluation.id),
104
+ evaluation,
105
+ ].sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id)),
106
+ };
107
+ }
108
+ export function appendLocalRun(snapshot, run, events) {
109
+ return {
110
+ ...snapshot,
111
+ runs: [
112
+ ...snapshot.runs.filter((entry) => entry.id !== run.id),
113
+ run,
114
+ ].sort((left, right) => left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id)),
115
+ events: [
116
+ ...snapshot.events,
117
+ ...events,
118
+ ].sort((left, right) => left.at.localeCompare(right.at) || left.id.localeCompare(right.id)),
119
+ };
120
+ }
121
+ export function setLocalActive(snapshot, activeId) {
122
+ return {
123
+ ...snapshot,
124
+ activeId,
125
+ };
126
+ }
127
+ export function readLocalSubject(snapshot, subjectId) {
128
+ const subject = snapshot.subjects.find((entry) => entry.id === subjectId);
129
+ if (!subject) {
130
+ throw new Error(`Subject not found: ${subjectId}`);
131
+ }
132
+ return subject;
133
+ }
134
+ export function readLocalSubjectFiles(snapshot, subjectId) {
135
+ readLocalSubject(snapshot, subjectId);
136
+ return (snapshot.subjectFiles[subjectId] ?? []).map((file) => ({ ...file }));
137
+ }
138
+ function validateLocalArchiveSnapshot(snapshot) {
139
+ const subjectIds = new Set(snapshot.subjects.map((subject) => subject.id));
140
+ if (snapshot.activeId && !subjectIds.has(snapshot.activeId)) {
141
+ throw new Error(`Active subject not found: ${snapshot.activeId}`);
142
+ }
143
+ for (const subject of snapshot.subjects) {
144
+ requireArchiveString(subject.id, "subject.id");
145
+ requireArchiveString(subject.benchmarkFingerprint, `subject ${subject.id}.benchmarkFingerprint`);
146
+ requireArchiveString(subject.subjectFingerprint, `subject ${subject.id}.subjectFingerprint`);
147
+ requireArchiveString(subject.createdAt, `subject ${subject.id}.createdAt`);
148
+ if (!Array.isArray(subject.referenceIds)) {
149
+ throw new Error(`subject ${subject.id}.referenceIds must be an array.`);
150
+ }
151
+ if (!Array.isArray(subject.fileChanges)) {
152
+ throw new Error(`subject ${subject.id}.fileChanges must be an array.`);
153
+ }
154
+ if (subject.baseId && !subjectIds.has(subject.baseId)) {
155
+ throw new Error(`subject ${subject.id}.baseId not found: ${subject.baseId}`);
156
+ }
157
+ }
158
+ for (const evaluation of snapshot.evaluations) {
159
+ requireArchiveString(evaluation.id, "evaluation.id");
160
+ requireArchiveString(evaluation.runId, `evaluation ${evaluation.id}.runId`);
161
+ requireArchiveString(evaluation.benchmarkFingerprint, `evaluation ${evaluation.id}.benchmarkFingerprint`);
162
+ requireArchiveString(evaluation.subjectFingerprint, `evaluation ${evaluation.id}.subjectFingerprint`);
163
+ requireArchiveString(evaluation.subjectId, `evaluation ${evaluation.id}.subjectId`);
164
+ const subject = snapshot.subjects.find((entry) => entry.id === evaluation.subjectId);
165
+ if (!subject) {
166
+ throw new Error(`evaluation ${evaluation.id}.subjectId not found: ${evaluation.subjectId}`);
167
+ }
168
+ if (subject.benchmarkFingerprint !== evaluation.benchmarkFingerprint) {
169
+ throw new Error(`evaluation ${evaluation.id}.benchmarkFingerprint does not match subject ${subject.id}.`);
170
+ }
171
+ if (subject.subjectFingerprint !== evaluation.subjectFingerprint) {
172
+ throw new Error(`evaluation ${evaluation.id}.subjectFingerprint does not match subject ${subject.id}.`);
173
+ }
174
+ }
175
+ for (const run of snapshot.runs) {
176
+ requireArchiveString(run.id, "run.id");
177
+ requireArchiveString(run.workflow, `run ${run.id}.workflow`);
178
+ requireArchiveString(run.benchmarkFingerprint, `run ${run.id}.benchmarkFingerprint`);
179
+ requireArchiveString(run.status, `run ${run.id}.status`);
180
+ requireArchiveString(run.startedAt, `run ${run.id}.startedAt`);
181
+ }
182
+ }
183
+ function requireArchiveString(value, label) {
184
+ if (typeof value !== "string" || value.length === 0) {
185
+ throw new Error(`${label} must be a non-empty string.`);
186
+ }
187
+ }
188
+ function archivedLocalJob(job, outputFiles, traceSourceFiles) {
189
+ const output = jsonRecord(job.output);
190
+ return {
191
+ ...job,
192
+ ...(Object.keys(output).length > 0
193
+ ? { output: { ...output, files: outputFiles } }
194
+ : {}),
195
+ trace: buildLocalJobTrace(job, traceSourceFiles),
196
+ };
197
+ }
198
+ function filterArchivedExecutionFiles(files) {
199
+ return files.filter((file) => file.path.startsWith(".workbench/traces/") ||
200
+ !isWorkbenchReservedArchivePath(file.path));
201
+ }
202
+ function isWorkbenchReservedArchivePath(filePath) {
203
+ return filePath === ".workbench" || filePath.startsWith(".workbench/");
204
+ }
205
+ function buildLocalJobTrace(job, outputFiles) {
206
+ const purpose = readExecutionPurpose(job);
207
+ const role = purpose === "improve" ? "optimizer" : "engine";
208
+ const stageId = purpose ?? "execution";
209
+ const realTrace = readLastExecutionTrace(outputFiles);
210
+ if (realTrace) {
211
+ return normalizeLocalExecutionTrace(realTrace, job, stageId);
212
+ }
213
+ const status = traceStatusForJob(job.status);
214
+ const startedAt = job.startedAt ?? job.createdAt;
215
+ const endedAt = job.finishedAt ?? null;
216
+ const spanId = "job";
217
+ const agentResult = readLastTraceJson(outputFiles, "/agent-result.json");
218
+ const eventCount = numberValue(agentResult.eventCount);
219
+ const sessionId = stringValue(agentResult.sessionId);
220
+ const output = jsonRecord(job.output);
221
+ const usage = traceUsageSummary(output.usage ?? agentResult.usage);
222
+ const events = [
223
+ traceEvent({
224
+ index: 1,
225
+ spanId,
226
+ stageId,
227
+ kind: "status",
228
+ at: startedAt,
229
+ message: `${capitalize(role)} job ${status === "completed" ? "completed" : status}.`,
230
+ attributes: {
231
+ job_id: job.id,
232
+ purpose: purpose ?? "unknown",
233
+ },
234
+ }),
235
+ ];
236
+ if (sessionId || eventCount !== null) {
237
+ events.push(traceEvent({
238
+ index: events.length + 1,
239
+ spanId,
240
+ stageId,
241
+ kind: "note",
242
+ at: endedAt ?? startedAt,
243
+ message: `Agent session${sessionId ? ` ${sessionId}` : ""}${eventCount !== null ? ` recorded ${eventCount} event(s)` : ""}.`,
244
+ attributes: {
245
+ job_id: job.id,
246
+ ...(sessionId ? { session_id: sessionId } : {}),
247
+ ...(eventCount !== null ? { event_count: eventCount } : {}),
248
+ },
249
+ }));
250
+ }
251
+ const outputMessage = localJobOutputMessage(job, output, agentResult);
252
+ if (outputMessage) {
253
+ events.push(traceEvent({
254
+ index: events.length + 1,
255
+ spanId,
256
+ stageId,
257
+ kind: "output",
258
+ at: endedAt ?? startedAt,
259
+ message: outputMessage,
260
+ attributes: {
261
+ job_id: job.id,
262
+ },
263
+ }));
264
+ }
265
+ if (usage) {
266
+ events.push(traceEvent({
267
+ index: events.length + 1,
268
+ spanId,
269
+ stageId,
270
+ kind: "usage",
271
+ at: endedAt ?? startedAt,
272
+ message: usage.total_tokens !== null
273
+ ? `Usage recorded: ${usage.total_tokens} token(s).`
274
+ : "Usage recorded.",
275
+ attributes: {
276
+ job_id: job.id,
277
+ usage: usage,
278
+ },
279
+ }));
280
+ }
281
+ if (job.error) {
282
+ events.push(traceEvent({
283
+ index: events.length + 1,
284
+ spanId,
285
+ stageId,
286
+ kind: "error",
287
+ at: endedAt ?? startedAt,
288
+ message: job.error,
289
+ attributes: { job_id: job.id },
290
+ }));
291
+ }
292
+ const span = {
293
+ id: spanId,
294
+ parent_id: null,
295
+ attempt_number: Math.max(1, job.attempt || 1),
296
+ stage_id: stageId,
297
+ stage_run_index: null,
298
+ kind: purpose === "attempt" || purpose === "improve" ? "turn" : "stage",
299
+ title: `${capitalize(role)} job ${job.id}`,
300
+ status,
301
+ started_at: startedAt,
302
+ ended_at: endedAt,
303
+ attributes: {
304
+ job_id: job.id,
305
+ purpose: purpose ?? "unknown",
306
+ },
307
+ };
308
+ return {
309
+ trace_id: `local-${job.id}`,
310
+ spans: [span],
311
+ events,
312
+ summaries: [traceSummary(job, stageId, status, startedAt, endedAt, usage, outputMessage, eventCount)],
313
+ };
314
+ }
315
+ function readLastExecutionTrace(files) {
316
+ const traceRecord = files
317
+ .filter((file) => file.encoding === "utf8" && file.path.endsWith("/trace.json"))
318
+ .map((file) => parseJsonObject(file.content))
319
+ .filter((record) => Object.keys(record).length > 0)
320
+ .at(-1);
321
+ if (!traceRecord) {
322
+ return null;
323
+ }
324
+ const spans = Array.isArray(traceRecord.spans)
325
+ ? traceRecord.spans.map(readTraceSpan).filter((span) => span !== null)
326
+ : [];
327
+ const events = Array.isArray(traceRecord.events)
328
+ ? traceRecord.events.map(readTraceEvent).filter((event) => event !== null)
329
+ : [];
330
+ const summaries = Array.isArray(traceRecord.summaries)
331
+ ? traceRecord.summaries.map(readTraceSummary).filter((summary) => summary !== null)
332
+ : [];
333
+ if (spans.length === 0 && events.length === 0 && summaries.length === 0) {
334
+ return null;
335
+ }
336
+ return {
337
+ trace_id: stringValue(traceRecord.trace_id) ?? "agent-trace",
338
+ spans,
339
+ events,
340
+ summaries,
341
+ };
342
+ }
343
+ function normalizeLocalExecutionTrace(trace, job, stageId) {
344
+ return finalizeWorkbenchExecutionTraceForJob({
345
+ job,
346
+ stageId,
347
+ trace: {
348
+ trace_id: `local-${job.id}`,
349
+ spans: trace.spans.map((span) => ({
350
+ ...span,
351
+ stage_id: stageId,
352
+ stage_run_index: null,
353
+ attributes: {
354
+ ...span.attributes,
355
+ job_id: job.id,
356
+ },
357
+ })),
358
+ events: trace.events.map((event) => ({
359
+ ...event,
360
+ stage_id: stageId,
361
+ stage_run_index: null,
362
+ attributes: {
363
+ ...event.attributes,
364
+ job_id: job.id,
365
+ },
366
+ })),
367
+ summaries: trace.summaries.map((summary) => ({
368
+ ...summary,
369
+ stage_id: stageId,
370
+ stage_run_index: null,
371
+ })),
372
+ },
373
+ });
374
+ }
375
+ function readTraceSpan(value) {
376
+ const record = jsonRecord(value);
377
+ const id = stringValue(record.id);
378
+ const kind = traceSpanKind(record.kind);
379
+ const status = traceStatus(record.status);
380
+ const startedAt = stringValue(record.started_at);
381
+ if (!id || !kind || !status || !startedAt) {
382
+ return null;
383
+ }
384
+ return {
385
+ id,
386
+ parent_id: stringValue(record.parent_id),
387
+ attempt_number: positiveInteger(record.attempt_number) ?? 1,
388
+ stage_id: stringValue(record.stage_id),
389
+ stage_run_index: integerValue(record.stage_run_index),
390
+ kind,
391
+ title: stringValue(record.title) ?? id,
392
+ status,
393
+ started_at: startedAt,
394
+ ended_at: stringValue(record.ended_at),
395
+ attributes: jsonRecord(record.attributes),
396
+ };
397
+ }
398
+ function readTraceEvent(value) {
399
+ const record = jsonRecord(value);
400
+ const id = stringValue(record.id);
401
+ const spanId = stringValue(record.span_id);
402
+ const kind = traceEventKind(record.kind);
403
+ const at = stringValue(record.at);
404
+ if (!id || !spanId || !kind || !at) {
405
+ return null;
406
+ }
407
+ return {
408
+ id,
409
+ span_id: spanId,
410
+ attempt_number: positiveInteger(record.attempt_number) ?? 1,
411
+ stage_id: stringValue(record.stage_id),
412
+ stage_run_index: integerValue(record.stage_run_index),
413
+ kind,
414
+ at,
415
+ message: stringValue(record.message) ?? kind,
416
+ attributes: jsonRecord(record.attributes),
417
+ };
418
+ }
419
+ function readTraceSummary(value) {
420
+ const record = jsonRecord(value);
421
+ const status = traceStatus(record.status);
422
+ const startedAt = stringValue(record.started_at);
423
+ if (!status || !startedAt) {
424
+ return null;
425
+ }
426
+ return {
427
+ attempt_number: positiveInteger(record.attempt_number) ?? 1,
428
+ stage_id: stringValue(record.stage_id),
429
+ stage_run_index: integerValue(record.stage_run_index),
430
+ status,
431
+ started_at: startedAt,
432
+ ended_at: stringValue(record.ended_at),
433
+ duration_ms: nonNegativeInteger(record.duration_ms) ?? 0,
434
+ tool_call_count: nonNegativeInteger(record.tool_call_count) ?? 0,
435
+ input_tokens: nonNegativeInteger(record.input_tokens),
436
+ output_tokens: nonNegativeInteger(record.output_tokens),
437
+ usage: traceUsageSummary(record.usage),
438
+ final_output_present: record.final_output_present === true,
439
+ error_message: stringValue(record.error_message),
440
+ };
441
+ }
442
+ function completedJobOutputFiles(job) {
443
+ const output = jsonRecord(job.output);
444
+ if (!Array.isArray(output.files)) {
445
+ return [];
446
+ }
447
+ return output.files.filter(isSurfaceSnapshotFile).map((file) => ({ ...file }));
448
+ }
449
+ function isSurfaceSnapshotFile(value) {
450
+ const record = jsonRecord(value);
451
+ return (typeof record.path === "string" &&
452
+ (record.kind === "text" || record.kind === "binary") &&
453
+ (record.encoding === "utf8" || record.encoding === "base64") &&
454
+ typeof record.content === "string" &&
455
+ typeof record.executable === "boolean");
456
+ }
457
+ function readExecutionPurpose(job) {
458
+ const input = jsonRecord(job.input);
459
+ return stringValue(jsonRecord(input.execution).purpose);
460
+ }
461
+ function traceStatusForJob(status) {
462
+ if (status === "succeeded")
463
+ return "completed";
464
+ if (status === "failed")
465
+ return "failed";
466
+ if (status === "cancelled")
467
+ return "canceled";
468
+ if (status === "running")
469
+ return "running";
470
+ return "warning";
471
+ }
472
+ function localJobOutputMessage(job, output, agentResult) {
473
+ const purpose = readExecutionPurpose(job);
474
+ const result = jsonRecord(output.result);
475
+ const score = numberValue(result.score);
476
+ if (purpose === "attempt" && score !== null) {
477
+ const summary = stringValue(result.summary) ?? stringValue(jsonRecord(result.feedback).summary);
478
+ return `Attempt produced score ${score}.${summary ? ` ${summary}` : ""}`.trim();
479
+ }
480
+ const summary = stringValue(output.summary) ?? stringValue(agentResult.finalOutput);
481
+ return summary ? truncateTraceMessage(summary) : null;
482
+ }
483
+ function traceSummary(job, stageId, status, startedAt, endedAt, usage, outputMessage, eventCount) {
484
+ const durationMs = endedAt && Number.isFinite(Date.parse(endedAt)) && Number.isFinite(Date.parse(startedAt))
485
+ ? Math.max(0, Date.parse(endedAt) - Date.parse(startedAt))
486
+ : 0;
487
+ return {
488
+ attempt_number: Math.max(1, job.attempt || 1),
489
+ stage_id: stageId,
490
+ stage_run_index: null,
491
+ status,
492
+ started_at: startedAt,
493
+ ended_at: endedAt,
494
+ duration_ms: durationMs,
495
+ tool_call_count: eventCount ?? 0,
496
+ input_tokens: usage?.input_tokens ?? null,
497
+ output_tokens: usage?.output_tokens ?? null,
498
+ usage,
499
+ final_output_present: Boolean(outputMessage),
500
+ error_message: job.error ?? null,
501
+ };
502
+ }
503
+ function traceEvent(args) {
504
+ return {
505
+ id: `event-${String(args.index).padStart(3, "0")}`,
506
+ span_id: args.spanId,
507
+ attempt_number: 1,
508
+ stage_id: args.stageId,
509
+ stage_run_index: null,
510
+ kind: args.kind,
511
+ at: args.at,
512
+ message: truncateTraceMessage(args.message),
513
+ attributes: args.attributes,
514
+ };
515
+ }
516
+ function traceUsageSummary(value) {
517
+ const record = jsonRecord(value);
518
+ const usage = Object.keys(jsonRecord(record.total)).length > 0
519
+ ? jsonRecord(record.total)
520
+ : Object.keys(jsonRecord(record.optimizer)).length > 0
521
+ ? jsonRecord(record.optimizer)
522
+ : Object.keys(jsonRecord(record.runner)).length > 0
523
+ ? jsonRecord(record.runner)
524
+ : Object.keys(jsonRecord(record.engine)).length > 0
525
+ ? jsonRecord(record.engine)
526
+ : record;
527
+ if (Object.keys(usage).length === 0) {
528
+ return null;
529
+ }
530
+ return {
531
+ provider: stringValue(usage.provider),
532
+ model: stringValue(usage.model),
533
+ input_tokens: numberValue(usage.inputTokens) ?? numberValue(usage.input_tokens),
534
+ uncached_input_tokens: numberValue(usage.uncachedInputTokens) ?? numberValue(usage.uncached_input_tokens),
535
+ cached_input_tokens: numberValue(usage.cachedInputTokens) ?? numberValue(usage.cached_input_tokens),
536
+ cache_creation_input_tokens: numberValue(usage.cacheCreationInputTokens) ?? numberValue(usage.cache_creation_input_tokens),
537
+ cache_read_input_tokens: numberValue(usage.cacheReadInputTokens) ?? numberValue(usage.cache_read_input_tokens),
538
+ output_tokens: numberValue(usage.outputTokens) ?? numberValue(usage.output_tokens),
539
+ reasoning_output_tokens: numberValue(usage.reasoningOutputTokens) ?? numberValue(usage.reasoning_output_tokens),
540
+ total_tokens: numberValue(usage.totalTokens) ?? numberValue(usage.total_tokens),
541
+ total_cost_usd: numberValue(usage.costUsd) ?? numberValue(usage.totalCostUsd) ?? numberValue(usage.total_cost_usd),
542
+ cost_source: stringValue(usage.costSource) ?? stringValue(usage.cost_source),
543
+ pricing_source: stringValue(usage.pricingSource) ?? stringValue(usage.pricing_source),
544
+ };
545
+ }
546
+ function readLastTraceJson(files, suffix) {
547
+ return files
548
+ .filter((file) => file.encoding === "utf8" && file.path.endsWith(suffix))
549
+ .map((file) => parseJsonObject(file.content))
550
+ .filter((record) => Object.keys(record).length > 0)
551
+ .at(-1) ?? {};
552
+ }
553
+ function parseJsonObject(source) {
554
+ try {
555
+ return jsonRecord(JSON.parse(source));
556
+ }
557
+ catch {
558
+ return {};
559
+ }
560
+ }
561
+ function jsonRecord(value) {
562
+ return value && typeof value === "object" && !Array.isArray(value)
563
+ ? value
564
+ : {};
565
+ }
566
+ function stringValue(value) {
567
+ return typeof value === "string" && value.length > 0 ? value : null;
568
+ }
569
+ function numberValue(value) {
570
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
571
+ }
572
+ function integerValue(value) {
573
+ return typeof value === "number" && Number.isInteger(value) ? value : null;
574
+ }
575
+ function positiveInteger(value) {
576
+ const integer = integerValue(value);
577
+ return integer !== null && integer > 0 ? integer : null;
578
+ }
579
+ function nonNegativeInteger(value) {
580
+ const integer = integerValue(value);
581
+ return integer !== null && integer >= 0 ? integer : null;
582
+ }
583
+ function traceSpanKind(value) {
584
+ return value === "hook" ||
585
+ value === "stage" ||
586
+ value === "turn" ||
587
+ value === "tool_call" ||
588
+ value === "assistant_output" ||
589
+ value === "usage" ||
590
+ value === "gate" ||
591
+ value === "action" ||
592
+ value === "error"
593
+ ? value
594
+ : null;
595
+ }
596
+ function traceEventKind(value) {
597
+ return value === "status" ||
598
+ value === "message" ||
599
+ value === "output" ||
600
+ value === "usage" ||
601
+ value === "error" ||
602
+ value === "note"
603
+ ? value
604
+ : null;
605
+ }
606
+ function traceStatus(value) {
607
+ return value === "running" ||
608
+ value === "completed" ||
609
+ value === "failed" ||
610
+ value === "canceled" ||
611
+ value === "warning"
612
+ ? value
613
+ : null;
614
+ }
615
+ function capitalize(value) {
616
+ return value.length > 0 ? `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}` : value;
617
+ }
618
+ function truncateTraceMessage(value) {
619
+ return value.length > 500 ? `${value.slice(0, 497)}...` : value;
620
+ }
621
+ function localRecordName(value) {
622
+ if (!value || /[\\/\\\0]/u.test(value)) {
623
+ throw new Error(`Unsafe local archive record id: ${value}`);
624
+ }
625
+ return value;
626
+ }
627
+ export async function materializeSubjectRoot(workspace, subjectRoot, files) {
628
+ const root = path.join(workspace, normalizeRelativePath(subjectRoot));
629
+ const before = new Set((await readSurfaceFiles(root)).map((file) => file.path));
630
+ await fs.rm(root, { force: true, recursive: true });
631
+ await writeSurfaceFiles(root, files);
632
+ const after = new Set(files.map((file) => file.path));
633
+ return [...new Set([...before, ...after])].sort();
634
+ }
635
+ export function findArchivedFile(files, filePath) {
636
+ const normalized = normalizeRelativePath(filePath);
637
+ return files.find((file) => file.path === normalized) ?? null;
638
+ }
639
+ async function readRecords(root, fileName) {
640
+ const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
641
+ const records = [];
642
+ for (const entry of entries) {
643
+ if (!entry.isDirectory()) {
644
+ continue;
645
+ }
646
+ records.push(await readJson(path.join(root, entry.name, fileName), null));
647
+ }
648
+ return records.filter((entry) => entry != null);
649
+ }
650
+ async function readFlatRecords(root) {
651
+ const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
652
+ const records = [];
653
+ for (const entry of entries) {
654
+ if (entry.isFile() && entry.name.endsWith(".json")) {
655
+ records.push(await readJson(path.join(root, entry.name), null));
656
+ }
657
+ }
658
+ return records.filter((entry) => entry != null);
659
+ }
660
+ async function readJson(filePath, fallback) {
661
+ try {
662
+ return JSON.parse(await fs.readFile(filePath, "utf8"));
663
+ }
664
+ catch (error) {
665
+ if (error.code === "ENOENT") {
666
+ return fallback;
667
+ }
668
+ throw error;
669
+ }
670
+ }
671
+ async function writeJson(filePath, value) {
672
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
673
+ await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
674
+ }
675
+ async function writeSurfaceFiles(root, files) {
676
+ await fs.mkdir(root, { recursive: true });
677
+ for (const file of files) {
678
+ const target = path.join(root, normalizeRelativePath(file.path));
679
+ await fs.mkdir(path.dirname(target), { recursive: true });
680
+ const body = file.encoding === "base64" ? Buffer.from(file.content, "base64") : Buffer.from(file.content, "utf8");
681
+ await fs.writeFile(target, body);
682
+ if (file.executable) {
683
+ await fs.chmod(target, 0o755).catch(() => undefined);
684
+ }
685
+ }
686
+ }
687
+ async function readSurfaceFiles(root) {
688
+ const decoder = new TextDecoder("utf-8", { fatal: true });
689
+ const files = [];
690
+ async function walk(directory) {
691
+ const entries = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
692
+ for (const entry of entries) {
693
+ const absolutePath = path.join(directory, entry.name);
694
+ if (entry.isDirectory()) {
695
+ await walk(absolutePath);
696
+ continue;
697
+ }
698
+ if (!entry.isFile()) {
699
+ continue;
700
+ }
701
+ const body = await fs.readFile(absolutePath);
702
+ const relativePath = normalizeRelativePath(path.relative(root, absolutePath).replace(/\\/gu, "/"));
703
+ const stats = await fs.stat(absolutePath);
704
+ const content = encodeContent(body, decoder);
705
+ files.push({
706
+ path: relativePath,
707
+ kind: content.encoding === "base64" ? "binary" : "text",
708
+ encoding: content.encoding,
709
+ content: content.content,
710
+ executable: (stats.mode & 0o111) !== 0,
711
+ });
712
+ }
713
+ }
714
+ await walk(root);
715
+ return files.sort((left, right) => left.path.localeCompare(right.path));
716
+ }
717
+ function encodeContent(body, decoder) {
718
+ try {
719
+ return {
720
+ encoding: "utf8",
721
+ content: decoder.decode(body),
722
+ };
723
+ }
724
+ catch {
725
+ return {
726
+ encoding: "base64",
727
+ content: body.toString("base64"),
728
+ };
729
+ }
730
+ }
731
+ function normalizeRelativePath(filePath) {
732
+ const normalized = filePath.replace(/\\/gu, "/").replace(/^\/+/u, "");
733
+ if (!normalized || normalized.includes("\0")) {
734
+ throw new Error("File paths must be non-empty relative paths.");
735
+ }
736
+ const parts = normalized.split("/");
737
+ if (parts.some((part) => part === ".." || part === "." || part === "")) {
738
+ throw new Error(`Unsafe relative file path: ${filePath}`);
739
+ }
740
+ return normalized;
741
+ }