UNPKG

langsmith

Version:

Client library to connect to the LangSmith Observability and Evaluation Platform.

415 lines (414 loc) 14.8 kB
/** * Sandbox class for interacting with a specific sandbox instance. */ import { LangSmithDataplaneNotConfiguredError, LangSmithSandboxNotReadyError, } from "./errors.js"; import { handleSandboxHttpError } from "./helpers.js"; import { CommandHandle } from "./command_handle.js"; import { reconnectWsStream, runWsStream } from "./ws_execute.js"; /** * Represents an active sandbox for running commands and file operations. * * This class is typically obtained from SandboxClient.createSandbox() and * provides methods for command execution and file I/O within the sandbox * environment. * * @example * ```typescript * const sandbox = await client.createSandbox(snapshot.id); * try { * const result = await sandbox.run("python --version"); * console.log(result.stdout); * } finally { * await sandbox.delete(); * } * ``` */ export class Sandbox { /** @internal */ constructor(data, client) { /** Display name (can be updated). */ Object.defineProperty(this, "name", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** URL for data plane operations (file I/O, command execution). */ Object.defineProperty(this, "dataplane_url", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Provisioning status ("provisioning", "ready", "failed", "stopped"). */ Object.defineProperty(this, "status", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Human-readable status message (e.g., error details when failed). */ Object.defineProperty(this, "status_message", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Unique identifier (UUID). Remains constant even if name changes. */ Object.defineProperty(this, "id", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Timestamp when the sandbox was created. */ Object.defineProperty(this, "created_at", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Timestamp when the sandbox was last updated. */ Object.defineProperty(this, "updated_at", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** * Idle timeout TTL in seconds (`0` means disabled). * New sandboxes receive a server-side default of `600` seconds (10 minutes) * when the caller did not set `idleTtlSeconds` explicitly. The launcher * stops the sandbox after this many idle seconds. */ Object.defineProperty(this, "idle_ttl_seconds", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** * Seconds after the sandbox enters the `stopped` state before it (and * its filesystem clone) are permanently deleted (`0` means disabled). */ Object.defineProperty(this, "delete_after_stop_seconds", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** * Timestamp when the sandbox transitioned to `stopped`, or `undefined` * while running. The deletion deadline is * `stopped_at + delete_after_stop_seconds`. */ Object.defineProperty(this, "stopped_at", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Snapshot ID used to create this sandbox. */ Object.defineProperty(this, "snapshot_id", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Number of vCPUs allocated. */ Object.defineProperty(this, "vCpus", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Memory allocation in bytes. */ Object.defineProperty(this, "mem_bytes", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Root filesystem capacity in bytes. */ Object.defineProperty(this, "fs_capacity_bytes", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_client", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.name = data.name; this.dataplane_url = data.dataplane_url; this.status = data.status; this.status_message = data.status_message; this.id = data.id; this.created_at = data.created_at; this.updated_at = data.updated_at; this.idle_ttl_seconds = data.idle_ttl_seconds; this.delete_after_stop_seconds = data.delete_after_stop_seconds; this.stopped_at = data.stopped_at ?? undefined; this.snapshot_id = data.snapshot_id; this.vCpus = data.vcpus; this.mem_bytes = data.mem_bytes; this.fs_capacity_bytes = data.fs_capacity_bytes; this._client = client; } /** * Validate and return the dataplane URL. * @throws LangSmithSandboxNotReadyError if sandbox status is not "ready". * @throws LangSmithDataplaneNotConfiguredError if dataplane_url is not configured. */ requireDataplaneUrl() { if (this.status && this.status !== "ready") { throw new LangSmithSandboxNotReadyError(`Sandbox '${this.name}' is not ready (status: ${this.status}). ` + "Use waitForSandbox() to wait for the sandbox to become ready."); } if (!this.dataplane_url) { throw new LangSmithDataplaneNotConfiguredError(`Sandbox '${this.name}' does not have a dataplane_url configured. ` + "Runtime operations require a dataplane URL."); } return this.dataplane_url; } async run(command, options = {}) { const { wait = true, onStdout, onStderr, idleTimeout, killOnDisconnect, ttlSeconds, pty, ...restOptions } = options; const hasCallbacks = onStdout !== undefined || onStderr !== undefined; if (!wait || hasCallbacks) { // WebSocket required for streaming / non-blocking const handle = await this._runWs(command, { ...restOptions, idleTimeout, killOnDisconnect, ttlSeconds, pty, onStdout, onStderr, }); if (!wait) { return handle; } // wait=true with callbacks: drain stream and return result return handle.result; } // wait=true, no callbacks: try WS, fall back to HTTP try { const handle = await this._runWs(command, { ...restOptions, idleTimeout, killOnDisconnect, ttlSeconds, pty, }); return await handle.result; } catch (e) { // Fall back to HTTP on connection errors or missing ws package const name = e != null && typeof e === "object" ? e.name : ""; const message = e != null && typeof e === "object" ? (e.message ?? "") : ""; if (name === "LangSmithSandboxConnectionError" || name === "LangSmithSandboxServerReloadError" || message.includes("'ws' package")) { return this._runHttp(command, restOptions); } throw e; } } /** * Execute a command via WebSocket streaming. * @internal */ async _runWs(command, options = {}) { const { timeout = 60, env, cwd, shell = "/bin/bash", onStdout, onStderr, idleTimeout, killOnDisconnect, ttlSeconds, pty, } = options; const dataplaneUrl = this.requireDataplaneUrl(); const clientHeaders = this._client.getDefaultHeaders(); const [stream, control] = await runWsStream(dataplaneUrl, this._client.getApiKey(), command, { timeout, env, cwd, shell, onStdout, onStderr, idleTimeout, killOnDisconnect, ttlSeconds, pty, ...(Object.keys(clientHeaders).length > 0 ? { headers: clientHeaders } : {}), }); const handle = new CommandHandle(stream, control, this); await handle._ensureStarted(); return handle; } /** * Execute a command via HTTP POST (blocking). * @internal */ async _runHttp(command, options = {}) { const { timeout = 60, env, cwd, shell = "/bin/bash" } = options; const dataplaneUrl = this.requireDataplaneUrl(); const url = `${dataplaneUrl}/execute`; const payload = { command, timeout, shell, }; if (env !== undefined) { payload.env = env; } if (cwd !== undefined) { payload.cwd = cwd; } const response = await this._client._fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(payload), signal: AbortSignal.timeout((timeout + 10) * 1000), }); if (!response.ok) { await handleSandboxHttpError(response); } const data = await response.json(); return { stdout: data.stdout ?? "", stderr: data.stderr ?? "", exit_code: data.exit_code ?? -1, }; } /** * Reconnect to a running command by its command ID. * * Returns a new CommandHandle that resumes output from the given offsets. * * @param commandId - The server-assigned command ID. * @param options - Reconnection options with byte offsets. * @returns A new CommandHandle. */ async reconnect(commandId, options = {}) { const { stdoutOffset = 0, stderrOffset = 0 } = options; const dataplaneUrl = this.requireDataplaneUrl(); const clientHeaders = this._client.getDefaultHeaders(); const [stream, control] = await reconnectWsStream(dataplaneUrl, this._client.getApiKey(), commandId, { stdoutOffset, stderrOffset, ...(Object.keys(clientHeaders).length > 0 ? { headers: clientHeaders } : {}), }); return new CommandHandle(stream, control, this, { commandId, stdoutOffset, stderrOffset, }); } /** * Write content to a file in the sandbox. * * @param path - Target file path in the sandbox. * @param content - File content (string or bytes). * @param timeout - Request timeout in seconds. * * @example * ```typescript * await sandbox.write("/tmp/script.py", 'print("Hello!")'); * ``` */ async write(path, content, timeout = 60) { const dataplaneUrl = this.requireDataplaneUrl(); const url = `${dataplaneUrl}/upload?path=${encodeURIComponent(path)}`; // Ensure content is bytes for multipart upload const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content; const formData = new FormData(); // Create a copy to ensure we have a plain ArrayBuffer (not SharedArrayBuffer) const buffer = new Uint8Array(bytes).buffer; const blob = new Blob([buffer], { type: "application/octet-stream" }); formData.append("file", blob, "file"); const response = await this._client._fetch(url, { method: "POST", body: formData, signal: AbortSignal.timeout(timeout * 1000), }); if (!response.ok) { await handleSandboxHttpError(response); } } /** * Read a file from the sandbox. * * @param path - File path to read. * @param timeout - Request timeout in seconds. * @returns File contents as Uint8Array. * * @example * ```typescript * const content = await sandbox.read("/tmp/output.txt"); * const text = new TextDecoder().decode(content); * console.log(text); * ``` */ async read(path, timeout = 60) { const dataplaneUrl = this.requireDataplaneUrl(); const url = `${dataplaneUrl}/download?path=${encodeURIComponent(path)}`; const response = await this._client._fetch(url, { method: "GET", signal: AbortSignal.timeout(timeout * 1000), }); if (!response.ok) { await handleSandboxHttpError(response); } const buffer = await response.arrayBuffer(); return new Uint8Array(buffer); } /** * Delete this sandbox. * * @example * ```typescript * const sandbox = await client.createSandbox(snapshot.id); * try { * await sandbox.run("echo hello"); * } finally { * await sandbox.delete(); * } * ``` */ async delete() { await this._client.deleteSandbox(this.name); } /** * Start a stopped sandbox and wait until ready. * * Updates this sandbox's status and dataplane_url in place. * * @param timeout - Timeout in seconds when waiting for ready. Default: 120. */ async start(options = {}) { const refreshed = await this._client.startSandbox(this.name, options); this.status = refreshed.status; this.dataplane_url = refreshed.dataplane_url; } /** * Stop a running sandbox (preserves sandbox files for later restart). */ async stop() { await this._client.stopSandbox(this.name); this.status = "stopped"; this.dataplane_url = undefined; } /** * Capture a snapshot from this sandbox. * * @param name - Snapshot name. * @param options - Capture options (timeout). * @returns Snapshot in "ready" status. */ async captureSnapshot(name, options = {}) { return this._client.captureSnapshot(this.name, name, options); } }