@workbench-ai/workbench 0.0.50 → 0.0.51

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.
@@ -1,4 +1,4 @@
1
- import { type CandidateRecord, type EvaluationScorecard, type HostedWorkbenchJob, type RunSummary, type RuntimeEvent, type SurfaceSnapshotFile, type WorkbenchExecutionTrace, type WorkbenchTraceSession } from "@workbench-ai/workbench-core";
1
+ import { type CandidateRecord, type EvaluationScorecard, type HostedWorkbenchJob, type RunSummary, type RuntimeEvent, type SurfaceSnapshotFile, type WorkbenchRuntimeBundle, type WorkbenchRuntimeBundleStats, type WorkbenchRuntimeImportResult, type WorkbenchExecutionTrace, type WorkbenchTraceSession } from "@workbench-ai/workbench-core";
2
2
  export interface LocalArchiveSnapshot {
3
3
  activeId: string | null;
4
4
  candidates: CandidateRecord[];
@@ -23,6 +23,10 @@ export declare function loadLocalArchive(workspace: string): Promise<LocalArchiv
23
23
  export declare function loadLocalArchiveIndex(workspace: string): Promise<LocalArchiveIndex>;
24
24
  export declare function saveLocalArchive(workspace: string, snapshot: LocalArchiveSnapshot): Promise<void>;
25
25
  export declare function saveLocalJobs(workspace: string, jobs: readonly HostedWorkbenchJob[]): Promise<void>;
26
+ export declare function exportLocalRuntimeBundle(workspace: string): Promise<WorkbenchRuntimeBundle>;
27
+ export declare function importLocalRuntimeBundle(workspace: string, bundle: WorkbenchRuntimeBundle): Promise<WorkbenchRuntimeImportResult>;
28
+ export declare function runtimeBundleStats(bundle: WorkbenchRuntimeBundle): WorkbenchRuntimeBundleStats;
29
+ export declare function sanitizeRuntimeJobForExchange(job: HostedWorkbenchJob): HostedWorkbenchJob;
26
30
  export declare function readLocalExecutionFiles(workspace: string, jobId: string): Promise<SurfaceSnapshotFile[]>;
27
31
  export declare function readLocalCandidateRecord(workspace: string, candidateId: string): Promise<CandidateRecord>;
28
32
  export declare function readLocalCandidateFilesForId(workspace: string, candidateId: string): Promise<SurfaceSnapshotFile[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"local-archive.d.ts","sourceRoot":"","sources":["../src/local-archive.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"local-archive.d.ts","sourceRoot":"","sources":["../src/local-archive.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
- import { buildWorkbenchTraceSessionsFromFiles, candidateRecordWithoutDerivedFields, selectExecutionOutputFilesForInspection, } from "@workbench-ai/workbench-core";
3
+ import { buildWorkbenchTraceSessionsFromFiles, candidateRecordWithoutDerivedFields, sanitizeWorkbenchRuntimeCandidateForExchange, sanitizeWorkbenchRuntimeJobForExchange, selectExecutionOutputFilesForInspection, workbenchRuntimeBundleStats, workbenchSurfaceFilesEqualForExchange, } from "@workbench-ai/workbench-core";
4
4
  const RUNTIME_DIR = ".workbench/runtime";
5
5
  const CANDIDATE_RECORDS_DIR = "candidates";
6
6
  export function localRuntimeDir(workspace) {
@@ -66,6 +66,142 @@ export async function saveLocalArchive(workspace, snapshot) {
66
66
  await writeJson(path.join(root, "events.json"), snapshot.events);
67
67
  }
68
68
  export async function saveLocalJobs(workspace, jobs) {
69
+ if (jobs.length === 0) {
70
+ return;
71
+ }
72
+ await writeArchivedLocalJobs(workspace, jobs, new Map());
73
+ }
74
+ export async function exportLocalRuntimeBundle(workspace) {
75
+ const snapshot = await loadLocalArchive(workspace);
76
+ const jobs = (await readLocalJobs(workspace)).map(sanitizeRuntimeJobForExchange);
77
+ const executionFiles = await Promise.all(jobs.map(async (job) => ({
78
+ jobId: job.id,
79
+ files: await readLocalExecutionFiles(workspace, job.id),
80
+ })));
81
+ return {
82
+ schema: "workbench.runtime.bundle.v1",
83
+ activeId: snapshot.activeId,
84
+ candidates: snapshot.candidates.map(sanitizeWorkbenchRuntimeCandidateForExchange),
85
+ candidateFiles: Object.entries(snapshot.candidateFiles).map(([candidateId, files]) => ({
86
+ candidateId,
87
+ files: copySurfaceFiles(files),
88
+ })),
89
+ evaluations: snapshot.evaluations.map((evaluation) => ({ ...evaluation })),
90
+ runs: snapshot.runs.map((run) => ({ ...run })),
91
+ jobs,
92
+ executionFiles,
93
+ events: snapshot.events.map((event) => ({ ...event })),
94
+ };
95
+ }
96
+ export async function importLocalRuntimeBundle(workspace, bundle) {
97
+ validateRuntimeBundleSchema(bundle);
98
+ const snapshot = await loadLocalArchive(workspace);
99
+ const existingJobs = (await readLocalJobs(workspace)).map(sanitizeRuntimeJobForExchange);
100
+ let changed = false;
101
+ const existingCandidates = snapshot.candidates.map(sanitizeWorkbenchRuntimeCandidateForExchange);
102
+ if (JSON.stringify(existingCandidates) !== JSON.stringify(snapshot.candidates)) {
103
+ changed = true;
104
+ }
105
+ const incomingCandidates = bundle.candidates.map(sanitizeWorkbenchRuntimeCandidateForExchange);
106
+ const candidates = mergeRecordsById(existingCandidates, incomingCandidates, (candidate) => candidate.id, (didChange) => {
107
+ changed ||= didChange;
108
+ }).sort(compareLocalCandidateRecords);
109
+ const candidateFiles = { ...snapshot.candidateFiles };
110
+ for (const group of bundle.candidateFiles) {
111
+ const candidateId = localRecordName(group.candidateId);
112
+ const files = copySurfaceFiles(group.files);
113
+ const existing = candidateFiles[candidateId];
114
+ if (existing) {
115
+ if (!workbenchSurfaceFilesEqualForExchange(existing, files)) {
116
+ throw new Error(`Runtime history conflict for candidate files ${candidateId}.`);
117
+ }
118
+ }
119
+ else {
120
+ changed = true;
121
+ }
122
+ candidateFiles[candidateId] = files;
123
+ }
124
+ const candidateIds = new Set(candidates.map((candidate) => candidate.id));
125
+ const activeId = bundle.activeId && candidateIds.has(bundle.activeId)
126
+ ? bundle.activeId
127
+ : snapshot.activeId && candidateIds.has(snapshot.activeId)
128
+ ? snapshot.activeId
129
+ : null;
130
+ if (activeId !== snapshot.activeId) {
131
+ changed = true;
132
+ }
133
+ const evaluations = mergeRecordsById(snapshot.evaluations, bundle.evaluations, (evaluation) => evaluation.id, (didChange) => {
134
+ changed ||= didChange;
135
+ }).sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id));
136
+ const runs = mergeRecordsById(snapshot.runs, bundle.runs, (run) => run.id, (didChange) => {
137
+ changed ||= didChange;
138
+ }).sort((left, right) => left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id));
139
+ const events = mergeRecordsById(snapshot.events, bundle.events, runtimeEventKey, (didChange) => {
140
+ changed ||= didChange;
141
+ }).sort((left, right) => left.at.localeCompare(right.at) || left.id.localeCompare(right.id));
142
+ const executionFilesByJobId = new Map();
143
+ await Promise.all(existingJobs.map(async (job) => {
144
+ executionFilesByJobId.set(job.id, await readLocalExecutionFiles(workspace, job.id));
145
+ }));
146
+ for (const group of bundle.executionFiles) {
147
+ const jobId = localRecordName(group.jobId);
148
+ const files = copySurfaceFiles(group.files);
149
+ const existing = executionFilesByJobId.get(jobId);
150
+ if (existing) {
151
+ if (!workbenchSurfaceFilesEqualForExchange(existing, files)) {
152
+ throw new Error(`Runtime history conflict for execution files ${jobId}.`);
153
+ }
154
+ }
155
+ else {
156
+ changed = true;
157
+ }
158
+ executionFilesByJobId.set(jobId, files);
159
+ }
160
+ const jobs = mergeRecordsById(existingJobs, bundle.jobs.map(sanitizeRuntimeJobForExchange), (job) => job.id, (didChange) => {
161
+ changed ||= didChange;
162
+ }, runtimeJobsEqualForExchange).sort((left, right) => (left.startedAt ?? left.createdAt).localeCompare(right.startedAt ?? right.createdAt) ||
163
+ left.id.localeCompare(right.id));
164
+ await saveLocalArchive(workspace, {
165
+ activeId,
166
+ candidates,
167
+ candidateFiles,
168
+ evaluations,
169
+ runs,
170
+ events,
171
+ });
172
+ await writeArchivedLocalJobs(workspace, jobs, executionFilesByJobId);
173
+ return {
174
+ changed,
175
+ stats: runtimeBundleStats({
176
+ schema: "workbench.runtime.bundle.v1",
177
+ activeId,
178
+ candidates,
179
+ candidateFiles: Object.entries(candidateFiles).map(([candidateId, files]) => ({
180
+ candidateId,
181
+ files,
182
+ })),
183
+ evaluations,
184
+ runs,
185
+ jobs,
186
+ executionFiles: [...executionFilesByJobId.entries()].map(([jobId, files]) => ({
187
+ jobId,
188
+ files,
189
+ })),
190
+ events,
191
+ }),
192
+ };
193
+ }
194
+ export function runtimeBundleStats(bundle) {
195
+ return workbenchRuntimeBundleStats(bundle);
196
+ }
197
+ export function sanitizeRuntimeJobForExchange(job) {
198
+ return sanitizeWorkbenchRuntimeJobForExchange(job);
199
+ }
200
+ function sanitizeRuntimeJobForArchive(job) {
201
+ const { leaseUntil: _leaseUntil, wakeupLeaseUntil: _wakeupLeaseUntil, hostId: _hostId, workerId: _workerId, claimTokenHash: _claimTokenHash, ...portable } = job;
202
+ return { ...portable };
203
+ }
204
+ async function writeArchivedLocalJobs(workspace, jobs, executionFilesByJobId) {
69
205
  if (jobs.length === 0) {
70
206
  return;
71
207
  }
@@ -77,14 +213,18 @@ export async function saveLocalJobs(workspace, jobs) {
77
213
  fs.mkdir(executionFilesDir, { recursive: true }),
78
214
  ]);
79
215
  for (const job of jobs) {
216
+ const sanitizedJob = sanitizeRuntimeJobForArchive(job);
80
217
  const safeJobId = localRecordName(job.id);
81
- const traceSourceFiles = filterArchivedExecutionFiles(completedJobOutputFiles(job));
82
- const outputFiles = selectExecutionOutputFilesForInspection({
83
- purpose: readExecutionPurpose(job),
84
- files: traceSourceFiles,
85
- output: jsonRecord(job.output),
86
- });
87
- await writeJson(path.join(jobsDir, `${safeJobId}.json`), archivedLocalJob(job, outputFiles, traceSourceFiles));
218
+ const explicitOutputFiles = executionFilesByJobId.get(job.id);
219
+ const traceSourceFiles = filterArchivedExecutionFiles(completedJobOutputFiles(sanitizedJob));
220
+ const outputFiles = explicitOutputFiles
221
+ ? copySurfaceFiles(explicitOutputFiles)
222
+ : selectExecutionOutputFilesForInspection({
223
+ purpose: readExecutionPurpose(sanitizedJob),
224
+ files: traceSourceFiles,
225
+ output: jsonRecord(sanitizedJob.output),
226
+ });
227
+ await writeJson(path.join(jobsDir, `${safeJobId}.json`), archivedLocalJob(sanitizedJob, outputFiles, traceSourceFiles.length > 0 ? traceSourceFiles : outputFiles));
88
228
  const filesRoot = path.join(executionFilesDir, safeJobId);
89
229
  await fs.rm(filesRoot, { force: true, recursive: true });
90
230
  await writeSurfaceFiles(filesRoot, outputFiles);
@@ -187,6 +327,70 @@ export function readLocalCandidateFiles(snapshot, candidateId) {
187
327
  function validateLocalArchiveSnapshot(snapshot) {
188
328
  validateLocalArchiveIndex(snapshot);
189
329
  }
330
+ function validateRuntimeBundleSchema(bundle) {
331
+ if (!bundle || bundle.schema !== "workbench.runtime.bundle.v1") {
332
+ throw new Error("Unsupported Workbench runtime bundle.");
333
+ }
334
+ }
335
+ function mergeRecordsById(existing, incoming, idFor, markChanged, equal = runtimeRecordsEqual) {
336
+ const records = new Map();
337
+ for (const record of existing) {
338
+ records.set(localRecordName(idFor(record)), record);
339
+ }
340
+ for (const record of incoming) {
341
+ const id = localRecordName(idFor(record));
342
+ const previous = records.get(id);
343
+ if (!previous || !equal(previous, record)) {
344
+ if (previous) {
345
+ throw new Error(`Runtime history conflict for id ${id}.`);
346
+ }
347
+ markChanged(true);
348
+ }
349
+ records.set(id, record);
350
+ }
351
+ return [...records.values()];
352
+ }
353
+ function runtimeRecordsEqual(left, right) {
354
+ return JSON.stringify(canonicalRuntimeJson(left)) ===
355
+ JSON.stringify(canonicalRuntimeJson(right));
356
+ }
357
+ function runtimeJobsEqualForExchange(left, right) {
358
+ return runtimeRecordsEqual(runtimeComparableJob(left), runtimeComparableJob(right));
359
+ }
360
+ function runtimeComparableJob(job) {
361
+ const comparable = sanitizeRuntimeJobForExchange(job);
362
+ const output = comparable.output;
363
+ if (!output || typeof output !== "object" || Array.isArray(output)) {
364
+ return comparable;
365
+ }
366
+ const { files: _files, fileSet: _fileSet, ...portableOutput } = output;
367
+ return {
368
+ ...comparable,
369
+ output: portableOutput,
370
+ };
371
+ }
372
+ function canonicalRuntimeJson(value) {
373
+ if (Array.isArray(value)) {
374
+ return value.map(canonicalRuntimeJson);
375
+ }
376
+ if (value && typeof value === "object") {
377
+ return Object.fromEntries(Object.keys(value)
378
+ .sort()
379
+ .map((key) => [key, canonicalRuntimeJson(value[key])]));
380
+ }
381
+ return value;
382
+ }
383
+ function runtimeEventKey(event) {
384
+ return [
385
+ event.runId ?? "_",
386
+ event.jobId ?? "_",
387
+ event.at,
388
+ event.id,
389
+ ].join("#");
390
+ }
391
+ function copySurfaceFiles(files) {
392
+ return files.map((file) => ({ ...file }));
393
+ }
190
394
  function validateLocalArchiveIndex(snapshot) {
191
395
  const candidateIds = new Set(snapshot.candidates.map((candidate) => candidate.id));
192
396
  if (snapshot.activeId && !candidateIds.has(snapshot.activeId)) {
@@ -260,16 +464,41 @@ function compareLocalCandidateRecords(left, right) {
260
464
  }
261
465
  function archivedLocalJob(job, outputFiles, traceSourceFiles) {
262
466
  const output = jsonRecord(job.output);
263
- const traceSessions = buildLocalJobTraceSessions(job, traceSourceFiles);
467
+ const existingTrace = readExistingTrace(job);
468
+ const existingTraceSessions = readExistingTraceSessions(job);
469
+ const traceSessions = existingTraceSessions.length > 0
470
+ ? existingTraceSessions
471
+ : buildLocalJobTraceSessions(job, traceSourceFiles);
264
472
  return {
265
473
  ...job,
266
474
  ...(Object.keys(output).length > 0
267
475
  ? { output: { ...output, files: traceSourceFiles } }
268
476
  : {}),
269
- trace: buildLocalJobTrace(job),
477
+ trace: existingTrace ?? buildLocalJobTrace(job),
270
478
  traceSessions,
271
479
  };
272
480
  }
481
+ function readExistingTrace(job) {
482
+ const trace = job.trace;
483
+ if (!trace || typeof trace !== "object" || Array.isArray(trace)) {
484
+ return null;
485
+ }
486
+ return {
487
+ trace_id: typeof trace.trace_id === "string" && trace.trace_id.length > 0
488
+ ? trace.trace_id
489
+ : job.id,
490
+ spans: Array.isArray(trace.spans) ? trace.spans : [],
491
+ events: Array.isArray(trace.events) ? trace.events : [],
492
+ summaries: Array.isArray(trace.summaries) ? trace.summaries : [],
493
+ };
494
+ }
495
+ function readExistingTraceSessions(job) {
496
+ const sessions = job.traceSessions;
497
+ if (!Array.isArray(sessions)) {
498
+ return [];
499
+ }
500
+ return sessions.map((session) => ({ ...session }));
501
+ }
273
502
  function filterArchivedExecutionFiles(files) {
274
503
  return files.filter((file) => file.path.startsWith(".workbench/traces/") ||
275
504
  !isWorkbenchReservedArchivePath(file.path));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workbench-ai/workbench",
3
- "version": "0.0.50",
3
+ "version": "0.0.51",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,9 +21,9 @@
21
21
  ],
22
22
  "dependencies": {
23
23
  "yaml": "^2.8.2",
24
- "@workbench-ai/workbench-built-in-adapters": "0.0.50",
25
- "@workbench-ai/workbench-protocol": "0.0.50",
26
- "@workbench-ai/workbench-core": "0.0.50"
24
+ "@workbench-ai/workbench-core": "0.0.51",
25
+ "@workbench-ai/workbench-protocol": "0.0.51",
26
+ "@workbench-ai/workbench-built-in-adapters": "0.0.51"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@tailwindcss/postcss": "^4.2.2",