@tanstack/ai-sandbox 0.3.4 → 0.5.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 +26 -0
- package/dist/esm/approvals.js +5 -4
- package/dist/esm/approvals.js.map +1 -1
- package/dist/esm/bridge-events.js +7 -6
- package/dist/esm/bridge-events.js.map +1 -1
- package/dist/esm/checkpoint-store.d.ts +147 -0
- package/dist/esm/checkpoint-store.js +267 -0
- package/dist/esm/checkpoint-store.js.map +1 -0
- package/dist/esm/chunk-identity.d.ts +0 -17
- package/dist/esm/chunk-identity.js +28 -5
- package/dist/esm/chunk-identity.js.map +1 -1
- package/dist/esm/contracts.d.ts +19 -0
- package/dist/esm/index.d.ts +11 -1
- package/dist/esm/index.js +12 -7
- package/dist/esm/memory-snapshot-types.d.ts +129 -0
- package/dist/esm/memory-snapshots.d.ts +6 -0
- package/dist/esm/memory-snapshots.js +490 -0
- package/dist/esm/memory-snapshots.js.map +1 -0
- package/dist/esm/middleware.d.ts +33 -1
- package/dist/esm/middleware.js +339 -94
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/ngrok.d.ts +1 -1
- package/dist/esm/sandbox.d.ts +16 -0
- package/dist/esm/sandbox.js +62 -9
- package/dist/esm/sandbox.js.map +1 -1
- package/dist/esm/snapshot-operations.d.ts +65 -0
- package/dist/esm/snapshot-operations.js +317 -0
- package/dist/esm/snapshot-operations.js.map +1 -0
- package/dist/esm/snapshot-tools.d.ts +185 -0
- package/dist/esm/snapshot-tools.js +160 -0
- package/dist/esm/snapshot-tools.js.map +1 -0
- package/dist/esm/snapshots.d.ts +51 -0
- package/dist/esm/snapshots.js +350 -0
- package/dist/esm/snapshots.js.map +1 -0
- package/dist/esm/testkit/checkpoint-conformance.d.ts +2 -0
- package/dist/esm/testkit/checkpoint-conformance.js +453 -0
- package/dist/esm/testkit/checkpoint-conformance.js.map +1 -0
- package/dist/esm/testkit/checkpoint-fork-conformance.d.ts +18 -0
- package/dist/esm/testkit/checkpoint-fork-conformance.js +191 -0
- package/dist/esm/testkit/checkpoint-fork-conformance.js.map +1 -0
- package/dist/esm/testkit/conformance.d.ts +4 -0
- package/dist/esm/testkit/conformance.js +3 -1
- package/dist/esm/testkit/conformance.js.map +1 -1
- package/dist/esm/tool-history.js +4 -5
- package/dist/esm/tool-history.js.map +1 -1
- package/package.json +8 -3
- package/skills/ai-sandbox/SKILL.md +96 -8
- package/src/approvals.ts +13 -12
- package/src/bridge-events.ts +16 -10
- package/src/checkpoint-store.ts +652 -0
- package/src/chunk-identity.ts +36 -5
- package/src/contracts.ts +12 -0
- package/src/index.ts +56 -0
- package/src/memory-snapshot-types.ts +167 -0
- package/src/memory-snapshots.ts +936 -0
- package/src/middleware.ts +610 -160
- package/src/sandbox.ts +107 -6
- package/src/snapshot-operations.ts +540 -0
- package/src/snapshot-tools.ts +208 -0
- package/src/snapshots.ts +711 -0
- package/src/testkit/checkpoint-conformance.ts +472 -0
- package/src/testkit/checkpoint-fork-conformance.ts +299 -0
- package/src/testkit/conformance.ts +7 -0
- package/src/tool-history.ts +9 -13
package/dist/esm/middleware.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { SandboxCapability, provideSandbox, provideSandboxPolicy } from "./capabilities.js";
|
|
2
2
|
import { SandboxInstanceStoreCapability } from "./instance-store.js";
|
|
3
|
+
import { SandboxCheckpointError } from "./checkpoint-store.js";
|
|
4
|
+
import { captureSandboxArtifacts, captureSandboxFiles, resolveSandboxSnapshotPolicy, restoreSandboxFiles } from "./snapshots.js";
|
|
5
|
+
import { resolveAllSecrets, resolveSecret } from "./secrets.js";
|
|
6
|
+
import { computeWorkspaceHash } from "./key.js";
|
|
7
|
+
import { resolveHarnessCwd } from "./harness-cwd.js";
|
|
8
|
+
import "./bootstrap.js";
|
|
9
|
+
import { ensureSandboxWithOutcome } from "./sandbox.js";
|
|
3
10
|
import { ProjectionCapability, provideWorkspaceProjection } from "./projection.js";
|
|
4
11
|
import { provideSandboxDurability, resolveSandboxDurability } from "./durability.js";
|
|
5
|
-
import { computeWorkspaceHash } from "./key.js";
|
|
6
12
|
import { buildFileHookEvent, resolveFileEvents } from "./file-diff.js";
|
|
7
|
-
import { resolveSecret } from "./secrets.js";
|
|
8
13
|
import { createToolHistoryRecorder, stripObservedToolCalls } from "./tool-history.js";
|
|
9
|
-
import { resolveHarnessCwd } from "./harness-cwd.js";
|
|
10
|
-
import "./bootstrap.js";
|
|
11
14
|
import { watchWorkspace } from "./watch.js";
|
|
12
15
|
import { defineChatMiddleware, provideDetachableRun, provideRunDetached, wasCancelRequested } from "@tanstack/ai";
|
|
13
16
|
import { InMemoryLockStore, LocksCapability } from "@tanstack/ai/locks";
|
|
@@ -32,6 +35,52 @@ import { getPendingTurn, getRunDisconnect, getSandboxRuntime } from "@tanstack/a
|
|
|
32
35
|
* chunks), not from here — middleware setup runs before streaming begins.
|
|
33
36
|
*/
|
|
34
37
|
var runState = /* @__PURE__ */ new WeakMap();
|
|
38
|
+
function stopSnapshotLease(state, options = {}) {
|
|
39
|
+
if (options.closePortable) state.snapshotClosed = true;
|
|
40
|
+
if (state.snapshotStop) return state.snapshotStop;
|
|
41
|
+
if (state.snapshotCleaned) return Promise.resolve();
|
|
42
|
+
state.snapshotCleaned = true;
|
|
43
|
+
state.snapshotRenewalGeneration++;
|
|
44
|
+
if (state.snapshotRenewal !== void 0) clearTimeout(state.snapshotRenewal);
|
|
45
|
+
state.snapshotRenewal = void 0;
|
|
46
|
+
const renewTask = state.snapshotRenewTask;
|
|
47
|
+
const captureTask = state.snapshotCaptureTask;
|
|
48
|
+
const lease = state.snapshotLease;
|
|
49
|
+
state.snapshotLease = void 0;
|
|
50
|
+
state.snapshotStop = (async () => {
|
|
51
|
+
await renewTask?.catch(() => {});
|
|
52
|
+
await captureTask?.catch(() => {});
|
|
53
|
+
await lease?.release();
|
|
54
|
+
})();
|
|
55
|
+
return state.snapshotStop;
|
|
56
|
+
}
|
|
57
|
+
function startSnapshotRenewal(state) {
|
|
58
|
+
const lease = state.snapshotLease;
|
|
59
|
+
if (!lease) return;
|
|
60
|
+
const schedule = () => {
|
|
61
|
+
const generation = state.snapshotRenewalGeneration;
|
|
62
|
+
state.snapshotRenewal = setTimeout(() => {
|
|
63
|
+
(async () => {
|
|
64
|
+
state.snapshotRenewal = void 0;
|
|
65
|
+
if (state.snapshotCleaned || generation !== state.snapshotRenewalGeneration) return;
|
|
66
|
+
const renewal = Promise.resolve().then(async () => {
|
|
67
|
+
await lease.renew();
|
|
68
|
+
});
|
|
69
|
+
state.snapshotRenewTask = renewal;
|
|
70
|
+
try {
|
|
71
|
+
await renewal;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
state.snapshotLost = error instanceof Error ? error : new Error(String(error));
|
|
74
|
+
} finally {
|
|
75
|
+
if (state.snapshotRenewTask === renewal) state.snapshotRenewTask = void 0;
|
|
76
|
+
}
|
|
77
|
+
if (state.snapshotLost) await stopSnapshotLease(state).catch(() => {});
|
|
78
|
+
else if (!state.snapshotCleaned && generation === state.snapshotRenewalGeneration) schedule();
|
|
79
|
+
})();
|
|
80
|
+
}, lease.renewAfterMs);
|
|
81
|
+
};
|
|
82
|
+
schedule();
|
|
83
|
+
}
|
|
35
84
|
/**
|
|
36
85
|
* Stop the watcher and drain any in-flight `diff()` promises before teardown,
|
|
37
86
|
* so the final file's diff isn't dropped when a run finishes/aborts/errors
|
|
@@ -50,6 +99,10 @@ async function drainWatcher(state, phase) {
|
|
|
50
99
|
await Promise.allSettled(state.pendingDiffs);
|
|
51
100
|
if (state.watcher) state.logger?.sandbox("sandbox watcher stopped", { phase });
|
|
52
101
|
}
|
|
102
|
+
function canPublishPortableSnapshot(state, lease) {
|
|
103
|
+
if (state.snapshotLost) throw state.snapshotLost;
|
|
104
|
+
return !state.snapshotClosed && !state.snapshotCleaned && state.snapshotLease === lease;
|
|
105
|
+
}
|
|
53
106
|
/**
|
|
54
107
|
* Record the two facts a later attach and the reaper both need, then publish the
|
|
55
108
|
* detach verdict core reads.
|
|
@@ -161,6 +214,25 @@ function withSandbox(definition, options) {
|
|
|
161
214
|
optionalRequires: [SandboxInstanceStoreCapability, LocksCapability],
|
|
162
215
|
async setup(ctx) {
|
|
163
216
|
const ensureCtx = buildEnsureCtx(ctx, options);
|
|
217
|
+
const snapshotConfig = options?.snapshots;
|
|
218
|
+
const snapshotWorkspaceHash = definition.workspace ? computeWorkspaceHash(definition.workspace) : void 0;
|
|
219
|
+
const snapshotPolicy = snapshotConfig ? resolveSandboxSnapshotPolicy(snapshotConfig.policy, snapshotWorkspaceHash) : void 0;
|
|
220
|
+
let snapshotRuntime;
|
|
221
|
+
let snapshotLease;
|
|
222
|
+
if (snapshotConfig) {
|
|
223
|
+
if (!snapshotConfig.persistence?.stores?.messages || !snapshotConfig.persistence.stores.artifacts || !snapshotConfig.persistence.stores.blobs) throw new Error("Sandbox snapshots require persistence stores.messages, stores.artifacts, and stores.blobs");
|
|
224
|
+
const persistenceModule = await import("@tanstack/ai-persistence");
|
|
225
|
+
const persistence = ctx.getOptional(persistenceModule.PersistenceCapability);
|
|
226
|
+
if (persistence === void 0) throw new Error("Sandbox snapshots require withPersistence(snapshots.persistence) before withSandbox");
|
|
227
|
+
if (persistence !== snapshotConfig.persistence) throw new Error("Sandbox snapshots require the same persistence instance passed to withPersistence");
|
|
228
|
+
const completion = ctx.getOptional(persistenceModule.PersistenceCompletionCapability);
|
|
229
|
+
if (!completion) throw new Error("Sandbox snapshots require withPersistence before withSandbox");
|
|
230
|
+
snapshotRuntime = {
|
|
231
|
+
persistence: snapshotConfig.persistence,
|
|
232
|
+
completion
|
|
233
|
+
};
|
|
234
|
+
snapshotLease = await snapshotConfig.checkpoints.acquireWriter(ctx.threadId);
|
|
235
|
+
}
|
|
164
236
|
const durability = resolveSandboxDurability(options);
|
|
165
237
|
if (durability !== void 0) {
|
|
166
238
|
provideSandboxDurability(ctx, durability);
|
|
@@ -170,12 +242,17 @@ function withSandbox(definition, options) {
|
|
|
170
242
|
const logger = runtime?.logger;
|
|
171
243
|
const state = {
|
|
172
244
|
ensureCtx,
|
|
245
|
+
snapshotRenewalGeneration: 0,
|
|
173
246
|
pendingDiffs: [],
|
|
174
247
|
toolHistory: createToolHistoryRecorder(),
|
|
175
248
|
...logger ? { logger } : {},
|
|
176
249
|
...durability ? { durability } : {}
|
|
177
250
|
};
|
|
178
251
|
runState.set(ctx, state);
|
|
252
|
+
if (snapshotLease) {
|
|
253
|
+
state.snapshotLease = snapshotLease;
|
|
254
|
+
startSnapshotRenewal(state);
|
|
255
|
+
}
|
|
179
256
|
if (durability !== void 0) {
|
|
180
257
|
try {
|
|
181
258
|
await durability.runs.createOrResume({
|
|
@@ -199,86 +276,148 @@ function withSandbox(definition, options) {
|
|
|
199
276
|
}
|
|
200
277
|
}
|
|
201
278
|
if (durability !== void 0 && durability.detachOnDisconnect) getRunDisconnect(ctx, { optional: true })?.subscribe(async () => {
|
|
202
|
-
|
|
203
|
-
|
|
279
|
+
const snapshotStop = stopSnapshotLease(state, { closePortable: true });
|
|
280
|
+
snapshotStop.catch(() => {});
|
|
281
|
+
if (await cancelIntent(durability, ctx.runId, false)) {
|
|
282
|
+
await snapshotStop.catch((error) => {
|
|
283
|
+
state.logger?.warn("sandbox snapshot writer release failed", {
|
|
284
|
+
runId: ctx.runId,
|
|
285
|
+
phase: "disconnect",
|
|
286
|
+
error
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (await recordDetach(definition, state, durability, ctx, "disconnect")) {
|
|
292
|
+
try {
|
|
293
|
+
await snapshotStop;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
state.logger?.warn("sandbox snapshot writer release failed", {
|
|
296
|
+
runId: ctx.runId,
|
|
297
|
+
phase: "disconnect",
|
|
298
|
+
error
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
state.logger?.sandbox("sandbox run detached on disconnect; the run continues", { runId: ctx.runId });
|
|
302
|
+
} else await snapshotStop.catch((error) => {
|
|
303
|
+
state.logger?.warn("sandbox snapshot writer release failed", {
|
|
304
|
+
runId: ctx.runId,
|
|
305
|
+
phase: "disconnect",
|
|
306
|
+
error
|
|
307
|
+
});
|
|
308
|
+
});
|
|
204
309
|
});
|
|
205
|
-
|
|
310
|
+
let outcome = "created";
|
|
311
|
+
let handle;
|
|
312
|
+
try {
|
|
313
|
+
if (snapshotConfig) ({handle, outcome} = await ensureSandboxWithOutcome(definition, ensureCtx));
|
|
314
|
+
else handle = await definition.ensure(ensureCtx);
|
|
315
|
+
state.handle = handle;
|
|
316
|
+
state.privateHandle = snapshotConfig ? outcome !== "resumed" : true;
|
|
317
|
+
if (snapshotConfig && outcome !== "resumed") {
|
|
318
|
+
const head = await snapshotConfig.checkpoints.getHead(ctx.threadId);
|
|
319
|
+
if (head) {
|
|
320
|
+
const checkpoint = await snapshotConfig.checkpoints.get(head);
|
|
321
|
+
if (!checkpoint) throw new SandboxCheckpointError("SANDBOX_SNAPSHOT_CHECKPOINT_NOT_FOUND", `Checkpoint '${head}' was not found`);
|
|
322
|
+
await restoreSandboxFiles(handle, {
|
|
323
|
+
blobs: snapshotConfig.persistence.stores.blobs,
|
|
324
|
+
workspaceRoot: definition.workspace?.root ?? "/workspace"
|
|
325
|
+
}, checkpoint, snapshotPolicy);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} catch (error) {
|
|
329
|
+
await stopSnapshotLease(state).catch(() => {});
|
|
330
|
+
if (state.handle && state.privateHandle) await definition.destroy(ensureCtx).catch(() => {});
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
206
333
|
state.handle = handle;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
334
|
+
if (snapshotConfig) {
|
|
335
|
+
state.snapshotConfig = snapshotConfig;
|
|
336
|
+
state.snapshotPolicy = snapshotPolicy;
|
|
337
|
+
state.snapshotRuntime = snapshotRuntime;
|
|
338
|
+
}
|
|
212
339
|
try {
|
|
213
|
-
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
340
|
+
provideSandbox(ctx, handle);
|
|
341
|
+
if (definition.policy) provideSandboxPolicy(ctx, definition.policy);
|
|
342
|
+
if (durability !== void 0 && (ensureCtx.locks === void 0 || ensureCtx.locks instanceof InMemoryLockStore)) logger?.warn("sandbox durability is wired over an InMemoryLockStore: run claims are serialized within this process only and the lease never signals loss, so two hosts can drive one run and duplicate its event log. Use a distributed LockStore via withLocks for any multi-replica deploy.", { runId: ctx.runId });
|
|
343
|
+
const watchRoot = definition.workspace?.root ?? "/workspace";
|
|
344
|
+
let baseSha = "";
|
|
345
|
+
try {
|
|
346
|
+
const shaRes = await handle.process.exec("git rev-parse HEAD", { cwd: watchRoot });
|
|
347
|
+
if (shaRes.exitCode === 0) {
|
|
348
|
+
baseSha = shaRes.stdout.trim();
|
|
349
|
+
logger?.sandbox("sandbox git baseline captured", {
|
|
350
|
+
root: watchRoot,
|
|
351
|
+
baseSha
|
|
352
|
+
});
|
|
353
|
+
} else logger?.sandbox("sandbox git baseline unavailable (non-zero exit)", {
|
|
217
354
|
root: watchRoot,
|
|
218
|
-
|
|
355
|
+
exitCode: shaRes.exitCode,
|
|
356
|
+
stderr: shaRes.stderr
|
|
219
357
|
});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
358
|
+
} catch (error) {
|
|
359
|
+
logger?.warn("sandbox git baseline capture failed", {
|
|
360
|
+
root: watchRoot,
|
|
361
|
+
error
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
const workspace = definition.workspace;
|
|
365
|
+
if (workspace !== void 0) {
|
|
366
|
+
const virtualRoot = workspace.root ?? "/workspace";
|
|
367
|
+
const root = resolveHarnessCwd(handle, virtualRoot);
|
|
368
|
+
const workspaceHash = computeWorkspaceHash(workspace);
|
|
369
|
+
const secrets = workspace.secrets;
|
|
370
|
+
provideWorkspaceProjection(ctx, {
|
|
371
|
+
skills: workspace.skills ?? [],
|
|
372
|
+
plugins: workspace.plugins ?? [],
|
|
373
|
+
resolveSecret: (ref) => {
|
|
374
|
+
if (secrets === void 0) throw new Error(`resolveSecret: no secrets defined on this workspace (ref: "${ref.__secretName}")`);
|
|
375
|
+
return resolveSecret(secrets, ref);
|
|
376
|
+
},
|
|
377
|
+
markerPath: `${root}/.tanstack-projected-${workspaceHash}`,
|
|
378
|
+
root,
|
|
379
|
+
...workspace.scripts !== void 0 ? { scripts: workspace.scripts } : {}
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
const hooks = definition.hooks;
|
|
383
|
+
await hooks?.onReady?.(handle);
|
|
384
|
+
const fe = resolveFileEvents(definition.fileEvents);
|
|
385
|
+
const pendingDiffs = state.pendingDiffs;
|
|
386
|
+
let watcher;
|
|
387
|
+
if (fe.enabled) {
|
|
388
|
+
watcher = await watchWorkspace(handle, {
|
|
389
|
+
onEvent: (event) => {
|
|
390
|
+
const enriched = buildFileHookEvent(handle, watchRoot, baseSha, event, logger);
|
|
391
|
+
dispatchDefinitionHooks(hooks, enriched, logger);
|
|
392
|
+
runtime?.emit(enriched);
|
|
393
|
+
if (fe.diff) pendingDiffs.push(enriched.diff().then((diff) => {
|
|
394
|
+
runtime?.emitFileDiff({
|
|
395
|
+
path: event.path,
|
|
396
|
+
diff
|
|
397
|
+
});
|
|
398
|
+
}).catch((error) => {
|
|
399
|
+
logger?.warn("sandbox file diff emit failed", {
|
|
400
|
+
path: event.path,
|
|
401
|
+
error
|
|
402
|
+
});
|
|
403
|
+
}));
|
|
404
|
+
},
|
|
405
|
+
root: watchRoot,
|
|
406
|
+
...ctx.signal !== void 0 ? { signal: ctx.signal } : {},
|
|
407
|
+
...logger !== void 0 ? { logger } : {}
|
|
408
|
+
});
|
|
409
|
+
logger?.sandbox("sandbox watcher started", {
|
|
410
|
+
root: watchRoot,
|
|
411
|
+
diff: fe.diff
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
if (watcher) state.watcher = watcher;
|
|
225
415
|
} catch (error) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
const workspace = definition.workspace;
|
|
232
|
-
if (workspace !== void 0) {
|
|
233
|
-
const virtualRoot = workspace.root ?? "/workspace";
|
|
234
|
-
const root = resolveHarnessCwd(handle, virtualRoot);
|
|
235
|
-
const workspaceHash = computeWorkspaceHash(workspace);
|
|
236
|
-
const secrets = workspace.secrets;
|
|
237
|
-
provideWorkspaceProjection(ctx, {
|
|
238
|
-
skills: workspace.skills ?? [],
|
|
239
|
-
plugins: workspace.plugins ?? [],
|
|
240
|
-
resolveSecret: (ref) => {
|
|
241
|
-
if (secrets === void 0) throw new Error(`resolveSecret: no secrets defined on this workspace (ref: "${ref.__secretName}")`);
|
|
242
|
-
return resolveSecret(secrets, ref);
|
|
243
|
-
},
|
|
244
|
-
markerPath: `${root}/.tanstack-projected-${workspaceHash}`,
|
|
245
|
-
root,
|
|
246
|
-
...workspace.scripts !== void 0 ? { scripts: workspace.scripts } : {}
|
|
247
|
-
});
|
|
416
|
+
await drainWatcher(state, "error");
|
|
417
|
+
await stopSnapshotLease(state).catch(() => {});
|
|
418
|
+
if (state.privateHandle) await definition.destroy(ensureCtx).catch(() => {});
|
|
419
|
+
throw error;
|
|
248
420
|
}
|
|
249
|
-
const hooks = definition.hooks;
|
|
250
|
-
await hooks?.onReady?.(handle);
|
|
251
|
-
const fe = resolveFileEvents(definition.fileEvents);
|
|
252
|
-
const pendingDiffs = state.pendingDiffs;
|
|
253
|
-
let watcher;
|
|
254
|
-
if (fe.enabled) {
|
|
255
|
-
watcher = await watchWorkspace(handle, {
|
|
256
|
-
onEvent: (event) => {
|
|
257
|
-
const enriched = buildFileHookEvent(handle, watchRoot, baseSha, event, logger);
|
|
258
|
-
dispatchDefinitionHooks(hooks, enriched, logger);
|
|
259
|
-
runtime?.emit(enriched);
|
|
260
|
-
if (fe.diff) pendingDiffs.push(enriched.diff().then((diff) => {
|
|
261
|
-
runtime?.emitFileDiff({
|
|
262
|
-
path: event.path,
|
|
263
|
-
diff
|
|
264
|
-
});
|
|
265
|
-
}).catch((error) => {
|
|
266
|
-
logger?.warn("sandbox file diff emit failed", {
|
|
267
|
-
path: event.path,
|
|
268
|
-
error
|
|
269
|
-
});
|
|
270
|
-
}));
|
|
271
|
-
},
|
|
272
|
-
root: watchRoot,
|
|
273
|
-
...ctx.signal !== void 0 ? { signal: ctx.signal } : {},
|
|
274
|
-
...logger !== void 0 ? { logger } : {}
|
|
275
|
-
});
|
|
276
|
-
logger?.sandbox("sandbox watcher started", {
|
|
277
|
-
root: watchRoot,
|
|
278
|
-
diff: fe.diff
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
if (watcher) state.watcher = watcher;
|
|
282
421
|
},
|
|
283
422
|
onConfig(_ctx, config) {
|
|
284
423
|
const messages = stripObservedToolCalls(config.messages);
|
|
@@ -288,8 +427,13 @@ function withSandbox(definition, options) {
|
|
|
288
427
|
onIteration(ctx) {
|
|
289
428
|
runState.get(ctx)?.toolHistory.reconcile(ctx);
|
|
290
429
|
},
|
|
291
|
-
onChunk(ctx, chunk) {
|
|
292
|
-
runState.get(ctx)
|
|
430
|
+
async onChunk(ctx, chunk) {
|
|
431
|
+
const state = runState.get(ctx);
|
|
432
|
+
state?.toolHistory.observe(chunk, ctx);
|
|
433
|
+
if (state && chunk.type === "RUN_FINISHED" && chunk.outcome?.type === "interrupt") {
|
|
434
|
+
await drainWatcher(state, "pause");
|
|
435
|
+
await stopSnapshotLease(state, { closePortable: true });
|
|
436
|
+
}
|
|
293
437
|
},
|
|
294
438
|
async onFinish(ctx) {
|
|
295
439
|
const state = runState.get(ctx);
|
|
@@ -297,49 +441,150 @@ function withSandbox(definition, options) {
|
|
|
297
441
|
const { handle, ensureCtx } = state;
|
|
298
442
|
state.toolHistory.reconcile(ctx);
|
|
299
443
|
await drainWatcher(state, "finish");
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
444
|
+
let primaryError;
|
|
445
|
+
try {
|
|
446
|
+
const snapshotCaptureTask = Promise.resolve().then(async () => {
|
|
447
|
+
const config = state.snapshotConfig;
|
|
448
|
+
const runtime = state.snapshotRuntime;
|
|
449
|
+
const lease = state.snapshotLease;
|
|
450
|
+
if (!config || !runtime || !handle || !lease) {
|
|
451
|
+
if (state.snapshotLost) throw state.snapshotLost;
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (!canPublishPortableSnapshot(state, lease)) return;
|
|
455
|
+
await runtime.completion.waitForRunCompletion();
|
|
456
|
+
if (!canPublishPortableSnapshot(state, lease)) return;
|
|
457
|
+
const conversation = await runtime.persistence.stores.messages.loadThread(ctx.threadId);
|
|
458
|
+
if (!canPublishPortableSnapshot(state, lease)) return;
|
|
459
|
+
const files = await captureSandboxFiles(handle, {
|
|
460
|
+
blobs: config.persistence.stores.blobs,
|
|
461
|
+
workspaceRoot: definition.workspace?.root ?? "/workspace"
|
|
462
|
+
}, state.snapshotPolicy, definition.workspace?.secrets !== void 0 ? resolveAllSecrets(definition.workspace.secrets) : {});
|
|
463
|
+
if (!canPublishPortableSnapshot(state, lease)) return;
|
|
464
|
+
const artifacts = await captureSandboxArtifacts({
|
|
465
|
+
blobs: config.persistence.stores.blobs,
|
|
466
|
+
artifacts: config.persistence.stores.artifacts
|
|
467
|
+
}, ctx.threadId, definition.workspace?.secrets !== void 0 ? resolveAllSecrets(definition.workspace.secrets) : {});
|
|
468
|
+
if (!canPublishPortableSnapshot(state, lease)) return;
|
|
469
|
+
const parentCheckpointId = await config.checkpoints.getHead(ctx.threadId);
|
|
470
|
+
if (!canPublishPortableSnapshot(state, lease)) return;
|
|
471
|
+
try {
|
|
472
|
+
await config.checkpoints.append({
|
|
473
|
+
checkpoint: {
|
|
474
|
+
id: `checkpoint-${ctx.runId}`,
|
|
475
|
+
threadId: ctx.threadId,
|
|
476
|
+
parentCheckpointId,
|
|
477
|
+
createdAt: Date.now(),
|
|
478
|
+
reason: "automatic",
|
|
479
|
+
sourceRunId: ctx.runId,
|
|
480
|
+
files: files.files,
|
|
481
|
+
conversation,
|
|
482
|
+
artifacts
|
|
483
|
+
},
|
|
484
|
+
expectedHeadId: parentCheckpointId,
|
|
485
|
+
writer: lease
|
|
486
|
+
});
|
|
487
|
+
} catch (error) {
|
|
488
|
+
if (state.snapshotLost) throw state.snapshotLost;
|
|
489
|
+
throw error;
|
|
490
|
+
}
|
|
491
|
+
if (state.snapshotLost) throw state.snapshotLost;
|
|
492
|
+
});
|
|
493
|
+
state.snapshotCaptureTask = snapshotCaptureTask;
|
|
494
|
+
try {
|
|
495
|
+
await snapshotCaptureTask;
|
|
496
|
+
} finally {
|
|
497
|
+
if (state.snapshotCaptureTask === snapshotCaptureTask) state.snapshotCaptureTask = void 0;
|
|
498
|
+
}
|
|
499
|
+
const lifecycle = definition.lifecycle;
|
|
500
|
+
if (lifecycle?.snapshot === "after-run" && handle?.capabilities.snapshots && handle.snapshot) {
|
|
501
|
+
const snapshot = await handle.snapshot(`after-run-${ctx.runId}`);
|
|
502
|
+
const store = ensureCtx.store;
|
|
503
|
+
if (store) {
|
|
504
|
+
const key = definition.key(ensureCtx);
|
|
505
|
+
const existing = await store.get(key);
|
|
506
|
+
if (existing) await store.upsert({
|
|
507
|
+
...existing,
|
|
508
|
+
latestSnapshotId: snapshot.id,
|
|
509
|
+
updatedAt: Date.now()
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (lifecycle?.destroyOnComplete) {
|
|
514
|
+
await definition.destroy(ensureCtx);
|
|
515
|
+
await definition.hooks?.onDestroy?.();
|
|
516
|
+
}
|
|
517
|
+
} catch (error) {
|
|
518
|
+
primaryError = error;
|
|
519
|
+
if (definition.lifecycle?.destroyOnComplete) try {
|
|
520
|
+
await definition.destroy(ensureCtx);
|
|
521
|
+
await definition.hooks?.onDestroy?.();
|
|
522
|
+
} catch (cleanupError) {
|
|
523
|
+
state.logger?.warn("sandbox destroy after terminal failure failed", {
|
|
524
|
+
runId: ctx.runId,
|
|
525
|
+
phase: "finish",
|
|
526
|
+
error: cleanupError
|
|
311
527
|
});
|
|
312
528
|
}
|
|
313
529
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
await
|
|
530
|
+
let snapshotCleanupError;
|
|
531
|
+
try {
|
|
532
|
+
await stopSnapshotLease(state, { closePortable: true });
|
|
533
|
+
} catch (error) {
|
|
534
|
+
snapshotCleanupError = error;
|
|
317
535
|
}
|
|
536
|
+
if (primaryError !== void 0) {
|
|
537
|
+
if (snapshotCleanupError !== void 0) state.logger?.warn("sandbox snapshot writer release failed", {
|
|
538
|
+
runId: ctx.runId,
|
|
539
|
+
phase: "finish",
|
|
540
|
+
error: snapshotCleanupError
|
|
541
|
+
});
|
|
542
|
+
throw primaryError;
|
|
543
|
+
}
|
|
544
|
+
if (snapshotCleanupError !== void 0) throw snapshotCleanupError;
|
|
318
545
|
},
|
|
319
546
|
async onAbort(ctx, info) {
|
|
320
547
|
const state = runState.get(ctx);
|
|
321
548
|
if (!state) return;
|
|
322
549
|
await drainWatcher(state, "abort");
|
|
550
|
+
let releaseError;
|
|
551
|
+
try {
|
|
552
|
+
await stopSnapshotLease(state, { closePortable: true });
|
|
553
|
+
} catch (error) {
|
|
554
|
+
releaseError = error;
|
|
555
|
+
}
|
|
323
556
|
const durability = state.durability;
|
|
324
557
|
const cancelled = await cancelIntent(durability, ctx.runId, info.cancelRequested === true);
|
|
325
558
|
if (durability !== void 0 && !cancelled && durability.detachOnDisconnect) {
|
|
326
|
-
if (await recordDetach(definition, state, durability, ctx, "abort"))
|
|
559
|
+
if (await recordDetach(definition, state, durability, ctx, "abort")) {
|
|
560
|
+
if (releaseError) throw releaseError;
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
327
563
|
await definition.destroy(state.ensureCtx);
|
|
328
564
|
await definition.hooks?.onDestroy?.();
|
|
565
|
+
if (releaseError) throw releaseError;
|
|
329
566
|
return;
|
|
330
567
|
}
|
|
331
568
|
await definition.destroy(state.ensureCtx);
|
|
332
569
|
await definition.hooks?.onDestroy?.();
|
|
570
|
+
if (releaseError) throw releaseError;
|
|
333
571
|
},
|
|
334
572
|
async onError(ctx, info) {
|
|
335
573
|
const state = runState.get(ctx);
|
|
336
574
|
if (!state) return;
|
|
337
575
|
await drainWatcher(state, "error");
|
|
576
|
+
let releaseError;
|
|
577
|
+
try {
|
|
578
|
+
await stopSnapshotLease(state);
|
|
579
|
+
} catch (error) {
|
|
580
|
+
releaseError = error;
|
|
581
|
+
}
|
|
338
582
|
await definition.hooks?.onError?.(info.error);
|
|
339
583
|
if (definition.lifecycle?.destroyOnComplete) {
|
|
340
584
|
await definition.destroy(state.ensureCtx);
|
|
341
585
|
await definition.hooks?.onDestroy?.();
|
|
342
586
|
}
|
|
587
|
+
if (releaseError) throw releaseError;
|
|
343
588
|
}
|
|
344
589
|
});
|
|
345
590
|
}
|