langsmith
Version:
Client library to connect to the LangSmith Observability and Evaluation Platform.
251 lines (250 loc) • 9.13 kB
JavaScript
/**
* Sandbox class for interacting with a specific sandbox instance.
*/
import { LangSmithDataplaneNotConfiguredError, LangSmithSandboxNotReadyError, } from "./errors.js";
import { handleSandboxHttpError } from "./helpers.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("python-sandbox");
* 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
});
/** Name of the template used to create this sandbox. */
Object.defineProperty(this, "template_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"). */
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
});
Object.defineProperty(this, "_client", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
this.name = data.name;
this.template_name = data.template_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._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;
}
/**
* Execute a command in the sandbox.
*
* @param command - Shell command to execute.
* @param options - Execution options.
* @returns ExecutionResult with stdout, stderr, and exit_code.
* @throws LangSmithDataplaneNotConfiguredError if dataplane_url is not configured.
* @throws SandboxOperationError if command execution fails.
* @throws SandboxConnectionError if connection to sandbox fails.
* @throws SandboxNotReadyError if sandbox is not ready.
*
* @example
* ```typescript
* const result = await sandbox.run("echo hello");
* console.log(result.stdout); // "hello\n"
* console.log(result.exit_code); // 0
* ```
*/
async run(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,
};
}
/**
* 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.
* @throws LangSmithDataplaneNotConfiguredError if dataplane_url is not configured.
* @throws SandboxOperationError if file write fails.
* @throws SandboxConnectionError if connection to sandbox fails.
*
* @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. Supports both absolute paths (e.g., /tmp/file.txt)
* and relative paths (resolved from /home/user/).
* @param timeout - Request timeout in seconds.
* @returns File contents as Uint8Array.
* @throws LangSmithDataplaneNotConfiguredError if dataplane_url is not configured.
* @throws ResourceNotFoundError if the file doesn't exist.
* @throws SandboxOperationError if file read fails.
* @throws SandboxConnectionError if connection to sandbox fails.
*
* @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.
*
* Call this when you're done using the sandbox to clean up resources.
*
* @example
* ```typescript
* const sandbox = await client.createSandbox("python-sandbox");
* try {
* await sandbox.run("echo hello");
* } finally {
* await sandbox.delete();
* }
* ```
*/
async delete() {
await this._client.deleteSandbox(this.name);
}
}