mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 20:01:27 +00:00
Merge pull request #261 from rivet-dev/e2b-autopause-provider
feat: add E2B auto-pause provider lifecycle support
This commit is contained in:
commit
9ce71c03c8
12 changed files with 416 additions and 13 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ export {
|
|||
ProcessTerminalSession,
|
||||
SandboxAgent,
|
||||
SandboxAgentError,
|
||||
SandboxDestroyedError,
|
||||
Session,
|
||||
UnsupportedPermissionReplyError,
|
||||
UnsupportedSessionCategoryError,
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue