feat: add E2B auto-pause support with pause/kill/reconnect provider lifecycle

Add `pause()`, `kill()`, and `reconnect()` methods to the SandboxProvider interface so providers can support graceful suspension and permanent deletion as distinct operations. The E2B provider now uses `betaCreate` with `autoPause: true` by default, `betaPause()` for suspension, and surfaces `SandboxDestroyedError` on reconnect to a deleted sandbox. SDK exposes `pauseSandbox()` and `killSandbox()` alongside the existing `destroySandbox()`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nathan Flurry 2026-03-16 14:57:49 -07:00
parent 32f3c6c3bc
commit 77c8f1e3f3
12 changed files with 416 additions and 13 deletions

View file

@ -216,6 +216,18 @@ export class SandboxAgentError extends Error {
}
}
export class SandboxDestroyedError extends Error {
readonly sandboxId: string;
readonly provider: string;
constructor(sandboxId: string, provider: string, options?: { cause?: unknown }) {
super(`Sandbox '${provider}/${sandboxId}' no longer exists and cannot be reconnected.`, options);
this.name = "SandboxDestroyedError";
this.sandboxId = sandboxId;
this.provider = provider;
}
}
export class UnsupportedSessionCategoryError extends Error {
readonly sessionId: string;
readonly category: string;
@ -904,6 +916,7 @@ export class SandboxAgent {
const createdSandbox = !existingSandbox;
if (existingSandbox) {
await provider.reconnect?.(rawSandboxId);
await provider.ensureServer?.(rawSandboxId);
}
@ -1007,6 +1020,50 @@ export class SandboxAgent {
}
}
async pauseSandbox(): Promise<void> {
const provider = this.sandboxProvider;
const rawSandboxId = this.sandboxProviderRawId;
try {
if (provider && rawSandboxId) {
if (provider.pause) {
await provider.pause(rawSandboxId);
} else {
await provider.destroy(rawSandboxId);
}
} else if (!provider || !rawSandboxId) {
throw new Error("SandboxAgent is not attached to a provisioned sandbox.");
}
} finally {
await this.dispose();
this.sandboxProvider = undefined;
this.sandboxProviderId = undefined;
this.sandboxProviderRawId = undefined;
}
}
async killSandbox(): Promise<void> {
const provider = this.sandboxProvider;
const rawSandboxId = this.sandboxProviderRawId;
try {
if (provider && rawSandboxId) {
if (provider.kill) {
await provider.kill(rawSandboxId);
} else {
await provider.destroy(rawSandboxId);
}
} else if (!provider || !rawSandboxId) {
throw new Error("SandboxAgent is not attached to a provisioned sandbox.");
}
} finally {
await this.dispose();
this.sandboxProvider = undefined;
this.sandboxProviderId = undefined;
this.sandboxProviderRawId = undefined;
}
}
async listSessions(request: ListPageRequest = {}): Promise<ListPage<Session>> {
const page = await this.persist.listSessions(request);
return {

View file

@ -3,6 +3,7 @@ export {
ProcessTerminalSession,
SandboxAgent,
SandboxAgentError,
SandboxDestroyedError,
Session,
UnsupportedPermissionReplyError,
UnsupportedSessionCategoryError,

View file

@ -1,13 +1,20 @@
import { Sandbox } from "@e2b/code-interpreter";
import { NotFoundError, Sandbox, type SandboxBetaCreateOpts, type SandboxConnectOpts } from "@e2b/code-interpreter";
import { SandboxDestroyedError } from "../client.ts";
import type { SandboxProvider } from "./types.ts";
import { DEFAULT_AGENTS, SANDBOX_AGENT_INSTALL_SCRIPT } from "./shared.ts";
const DEFAULT_AGENT_PORT = 3000;
const DEFAULT_TIMEOUT_MS = 3_600_000;
type E2BCreateOverrides = Omit<Partial<SandboxBetaCreateOpts>, "timeoutMs" | "autoPause">;
type E2BConnectOverrides = Omit<Partial<SandboxConnectOpts>, "timeoutMs">;
export interface E2BProviderOptions {
create?: Record<string, unknown> | (() => Record<string, unknown> | Promise<Record<string, unknown>>);
connect?: Record<string, unknown> | ((sandboxId: string) => Record<string, unknown> | Promise<Record<string, unknown>>);
create?: E2BCreateOverrides | (() => E2BCreateOverrides | Promise<E2BCreateOverrides>);
connect?: E2BConnectOverrides | ((sandboxId: string) => E2BConnectOverrides | Promise<E2BConnectOverrides>);
agentPort?: number;
timeoutMs?: number;
autoPause?: boolean;
}
async function resolveOptions(value: E2BProviderOptions["create"] | E2BProviderOptions["connect"], sandboxId?: string): Promise<Record<string, unknown>> {
@ -23,13 +30,15 @@ async function resolveOptions(value: E2BProviderOptions["create"] | E2BProviderO
export function e2b(options: E2BProviderOptions = {}): SandboxProvider {
const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const autoPause = options.autoPause ?? true;
return {
name: "e2b",
async create(): Promise<string> {
const createOpts = await resolveOptions(options.create);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sandbox = await Sandbox.create({ allowInternetAccess: true, ...createOpts } as any);
const sandbox = await Sandbox.betaCreate({ allowInternetAccess: true, ...createOpts, timeoutMs, autoPause } as any);
await sandbox.commands.run(`curl -fsSL ${SANDBOX_AGENT_INSTALL_SCRIPT} | sh`).then((r) => {
if (r.exitCode !== 0) throw new Error(`e2b install failed:\n${r.stderr}`);
@ -44,18 +53,37 @@ export function e2b(options: E2BProviderOptions = {}): SandboxProvider {
return sandbox.sandboxId;
},
async destroy(sandboxId: string): Promise<void> {
await this.pause?.(sandboxId);
},
async reconnect(sandboxId: string): Promise<void> {
const connectOpts = await resolveOptions(options.connect, sandboxId);
const sandbox = await Sandbox.connect(sandboxId, connectOpts as any);
try {
await Sandbox.connect(sandboxId, { ...connectOpts, timeoutMs } as SandboxConnectOpts);
} catch (error) {
if (error instanceof NotFoundError) {
throw new SandboxDestroyedError(sandboxId, "e2b", { cause: error });
}
throw error;
}
},
async pause(sandboxId: string): Promise<void> {
const connectOpts = await resolveOptions(options.connect, sandboxId);
const sandbox = await Sandbox.connect(sandboxId, { ...connectOpts, timeoutMs } as SandboxConnectOpts);
await sandbox.betaPause();
},
async kill(sandboxId: string): Promise<void> {
const connectOpts = await resolveOptions(options.connect, sandboxId);
const sandbox = await Sandbox.connect(sandboxId, { ...connectOpts, timeoutMs } as SandboxConnectOpts);
await sandbox.kill();
},
async getUrl(sandboxId: string): Promise<string> {
const connectOpts = await resolveOptions(options.connect, sandboxId);
const sandbox = await Sandbox.connect(sandboxId, connectOpts as any);
const sandbox = await Sandbox.connect(sandboxId, { ...connectOpts, timeoutMs } as SandboxConnectOpts);
return `https://${sandbox.getHost(agentPort)}`;
},
async ensureServer(sandboxId: string): Promise<void> {
const connectOpts = await resolveOptions(options.connect, sandboxId);
const sandbox = await Sandbox.connect(sandboxId, connectOpts as any);
const sandbox = await Sandbox.connect(sandboxId, { ...connectOpts, timeoutMs } as SandboxConnectOpts);
await sandbox.commands.run(`sandbox-agent server --no-token --host 0.0.0.0 --port ${agentPort}`, { background: true, timeoutMs: 0 });
},
};

View file

@ -8,6 +8,25 @@ export interface SandboxProvider {
/** Permanently tear down a sandbox. */
destroy(sandboxId: string): Promise<void>;
/**
* Reconnect to an existing sandbox before the SDK attempts health checks.
* Providers can use this to resume paused sandboxes or surface provider-specific
* reconnect errors.
*/
reconnect?(sandboxId: string): Promise<void>;
/**
* Gracefully stop or pause a sandbox without permanently deleting it.
* When omitted, callers should fall back to `destroy()`.
*/
pause?(sandboxId: string): Promise<void>;
/**
* Permanently delete a sandbox. When omitted, callers should fall back to
* `destroy()`.
*/
kill?(sandboxId: string): Promise<void>;
/**
* Return the sandbox-agent base URL for this sandbox.
* Providers that cannot expose a URL should implement `getFetch()` instead.