Skip to content

Commit 731b01e

Browse files
fix(deepagents): polish sandbox interfaces (#194)
* fix(deepagents): polish sandbox interfaces * revert * global ref * format * changesets * tweak CI * fix
1 parent d31f6cb commit 731b01e

File tree

17 files changed

+329
-757
lines changed

17 files changed

+329
-757
lines changed

.changeset/slimy-sites-shout.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@langchain/node-vfs": patch
3+
"@langchain/daytona": patch
4+
"@langchain/modal": patch
5+
"@langchain/deno": patch
6+
"deepagents": patch
7+
---
8+
9+
fix(deepagents): polish sandbox interfaces

libs/deepagents/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<div align="center">
22
<a href="https://docs.langchain.com/oss/python/deepagents/overview#deep-agents-overview">
33
<picture>
4-
<source media="(prefers-color-scheme: light)" srcset=".github/images/logo-dark.svg">
5-
<source media="(prefers-color-scheme: dark)" srcset=".github/images/logo-light.svg">
6-
<img alt="Deep Agents Logo" src=".github/images/logo-dark.svg" width="80%">
4+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/langchain-ai/deepagentsjs/refs/heads/main/.github/images/logo-dark.svg">
5+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/langchain-ai/deepagentsjs/refs/heads/main/.github/images/logo-light.svg">
6+
<img alt="Deep Agents Logo" src="https://raw.githubusercontent.com/langchain-ai/deepagentsjs/refs/heads/main/.github/images/logo-dark.svg" width="80%">
77
</picture>
88
</a>
99
</div>

libs/deepagents/src/backends/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ export type {
2727
SandboxListOptions,
2828
SandboxGetOrCreateOptions,
2929
SandboxDeleteOptions,
30-
SandboxProvider,
30+
// Sandbox error types
31+
SandboxErrorCode,
3132
} from "./protocol.js";
3233

33-
// Export type guard
34-
export { isSandboxBackend } from "./protocol.js";
34+
// Export type guard and error class
35+
export { isSandboxBackend, SandboxError } from "./protocol.js";
3536

3637
export { StateBackend } from "./state.js";
3738
export { StoreBackend } from "./store.js";

libs/deepagents/src/backends/protocol.ts

Lines changed: 65 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -385,123 +385,87 @@ export interface SandboxDeleteOptions {
385385
}
386386

387387
/**
388-
* Abstract interface for sandbox provider implementations.
388+
* Common error codes shared across all sandbox provider implementations.
389389
*
390-
* Defines the lifecycle management interface for sandbox providers. Implementations
391-
* should integrate with their respective SDKs to provide standardized sandbox
392-
* lifecycle operations (list, getOrCreate, delete).
393-
*
394-
* This interface separates lifecycle management from sandbox execution:
395-
* - `SandboxProvider` handles lifecycle (list, create, delete)
396-
* - `SandboxBackendProtocol` handles execution (execute, file operations)
397-
*
398-
* @typeParam MetadataT - Type of the metadata field in sandbox listings.
399-
* Providers can define their own interface for type-safe metadata access.
390+
* These represent the core error conditions that any sandbox provider may encounter.
391+
* Provider-specific error codes should extend this type with additional codes.
400392
*
401393
* @example
402394
* ```typescript
403-
* interface MyMetadata {
404-
* status: "running" | "stopped";
405-
* template: string;
406-
* }
395+
* // Provider-specific error code type extending the common codes:
396+
* type MySandboxErrorCode = SandboxErrorCode | "CUSTOM_ERROR";
397+
* ```
398+
*/
399+
export type SandboxErrorCode =
400+
/** Sandbox has not been initialized - call initialize() first */
401+
| "NOT_INITIALIZED"
402+
/** Sandbox is already initialized - cannot initialize twice */
403+
| "ALREADY_INITIALIZED"
404+
/** Command execution timed out */
405+
| "COMMAND_TIMEOUT"
406+
/** Command execution failed */
407+
| "COMMAND_FAILED"
408+
/** File operation (read/write) failed */
409+
| "FILE_OPERATION_FAILED";
410+
411+
const SANDBOX_ERROR_SYMBOL = Symbol.for("sandbox.error");
412+
413+
/**
414+
* Custom error class for sandbox operations.
407415
*
408-
* class MySandboxProvider implements SandboxProvider<MyMetadata> {
409-
* async list(options?: SandboxListOptions): Promise<SandboxListResponse<MyMetadata>> {
410-
* // Query provider API
411-
* return { items: [...], cursor: null };
412-
* }
416+
* @param message - Human-readable error description
417+
* @param code - Structured error code for programmatic handling
418+
* @returns SandboxError with message and code
413419
*
414-
* async getOrCreate(options?: SandboxGetOrCreateOptions): Promise<SandboxBackendProtocol> {
415-
* if (options?.sandboxId) {
416-
* return this.get(options.sandboxId);
420+
* @example
421+
* ```typescript
422+
* try {
423+
* await sandbox.execute("some command");
424+
* } catch (error) {
425+
* if (error instanceof SandboxError) {
426+
* switch (error.code) {
427+
* case "NOT_INITIALIZED":
428+
* await sandbox.initialize();
429+
* break;
430+
* case "COMMAND_TIMEOUT":
431+
* console.error("Command took too long");
432+
* break;
433+
* default:
434+
* throw error;
417435
* }
418-
* return this.create();
419-
* }
420-
*
421-
* async delete(options: SandboxDeleteOptions): Promise<void> {
422-
* // Idempotent - no error if already deleted
423-
* await this.client.delete(options.sandboxId);
424436
* }
425437
* }
426-
*
427-
* // Usage
428-
* const provider = new MySandboxProvider();
429-
* const sandbox = await provider.getOrCreate();
430-
* const result = await sandbox.execute("echo hello");
431-
* await provider.delete({ sandboxId: sandbox.id });
432438
* ```
433439
*/
434-
export interface SandboxProvider<MetadataT = Record<string, unknown>> {
435-
/**
436-
* List available sandboxes with optional pagination.
437-
*
438-
* @param options - Optional list options including cursor for pagination
439-
* @returns Paginated list of sandbox metadata
440-
*
441-
* @example
442-
* ```typescript
443-
* // First page
444-
* const response = await provider.list();
445-
* for (const sandbox of response.items) {
446-
* console.log(sandbox.sandboxId);
447-
* }
448-
*
449-
* // Next page if available
450-
* if (response.cursor) {
451-
* const nextPage = await provider.list({ cursor: response.cursor });
452-
* }
453-
* ```
454-
*/
455-
list(options?: SandboxListOptions): Promise<SandboxListResponse<MetadataT>>;
440+
export class SandboxError extends Error {
441+
/** Symbol for identifying sandbox error instances */
442+
[SANDBOX_ERROR_SYMBOL] = true as const;
456443

457-
/**
458-
* Get an existing sandbox or create a new one.
459-
*
460-
* If sandboxId is provided, retrieves the existing sandbox. If the sandbox
461-
* doesn't exist, throws an error (does NOT create a new one).
462-
*
463-
* If sandboxId is undefined, creates a new sandbox instance.
464-
*
465-
* @param options - Optional options including sandboxId to retrieve
466-
* @returns A sandbox instance implementing SandboxBackendProtocol
467-
* @throws Error if sandboxId is provided but the sandbox doesn't exist
468-
*
469-
* @example
470-
* ```typescript
471-
* // Create a new sandbox
472-
* const sandbox = await provider.getOrCreate();
473-
* console.log(sandbox.id); // "sb_new123"
474-
*
475-
* // Reconnect to existing sandbox
476-
* const existing = await provider.getOrCreate({ sandboxId: "sb_new123" });
477-
*
478-
* // Use the sandbox
479-
* const result = await sandbox.execute("node --version");
480-
* ```
481-
*/
482-
getOrCreate(
483-
options?: SandboxGetOrCreateOptions,
484-
): Promise<SandboxBackendProtocol>;
444+
/** Error name for instanceof checks and logging */
445+
override readonly name: string = "SandboxError";
485446

486447
/**
487-
* Delete a sandbox instance.
488-
*
489-
* This permanently destroys the sandbox and all its associated data.
490-
* The operation is idempotent - calling delete on a non-existent sandbox
491-
* should succeed without raising an error.
492-
*
493-
* @param options - Options including the sandboxId to delete
494-
*
495-
* @example
496-
* ```typescript
497-
* // Simple deletion
498-
* await provider.delete({ sandboxId: "sb_123" });
448+
* Creates a new SandboxError.
499449
*
500-
* // Safe to call multiple times (idempotent)
501-
* await provider.delete({ sandboxId: "sb_123" }); // No error
502-
* ```
450+
* @param message - Human-readable error description
451+
* @param code - Structured error code for programmatic handling
503452
*/
504-
delete(options: SandboxDeleteOptions): Promise<void>;
453+
constructor(
454+
message: string,
455+
public readonly code: string,
456+
public readonly cause?: Error,
457+
) {
458+
super(message);
459+
Object.setPrototypeOf(this, SandboxError.prototype);
460+
}
461+
462+
static isInstance(error: unknown): error is SandboxError {
463+
return (
464+
typeof error === "object" &&
465+
error !== null &&
466+
(error as Record<symbol, unknown>)[SANDBOX_ERROR_SYMBOL] === true
467+
);
468+
}
505469
}
506470

507471
/**

0 commit comments

Comments
 (0)