Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions libs/code/deepagents_code/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,7 @@ def reset_thread(self) -> str:
_SANDBOX_DISPLAY_NAMES: dict[str, str] = {
"agentcore": "AgentCore",
"daytona": "Daytona",
"e2b": "E2B",
"langsmith": "LangSmith",
"modal": "Modal",
"runloop": "Runloop",
Expand Down
4 changes: 3 additions & 1 deletion libs/code/deepagents_code/extras_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ class ExtrasIntrospectionError(RuntimeError):
Keep in sync with `[project.optional-dependencies]` in `pyproject.toml`.
"""

SANDBOX_EXTRAS: frozenset[str] = frozenset({"agentcore", "daytona", "modal", "runloop"})
SANDBOX_EXTRAS: frozenset[str] = frozenset(
{"agentcore", "daytona", "e2b", "modal", "runloop"}
)
"""Optional extras that add sandbox integrations."""

STANDALONE_EXTRAS: frozenset[str] = frozenset({"quickjs"})
Expand Down
88 changes: 82 additions & 6 deletions libs/code/deepagents_code/integrations/sandbox_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def _run_sandbox_setup(backend: SandboxBackendProtocol, setup_script_path: str)
_PROVIDER_TO_WORKING_DIR = {
"agentcore": "/tmp", # noqa: S108 # AgentCore Code Interpreter working directory
"daytona": "/home/daytona",
"e2b": "/home/user",
"langsmith": "/root", # `$HOME` in the LangSmith sandbox
"modal": "/workspace",
"runloop": "/home/user",
Expand All @@ -94,8 +95,8 @@ def create_sandbox(
provider abstraction.

Args:
provider: Sandbox provider (`'agentcore'`, `'daytona'`, `'langsmith'`,
`'modal'`, `'runloop'`)
provider: Sandbox provider (`'agentcore'`, `'daytona'`, `'e2b'`,
`'langsmith'`, `'modal'`, `'runloop'`)
sandbox_id: Optional existing sandbox ID to reuse
snapshot_name: Optional sandbox snapshot name to use or create.
Honored by `'langsmith'` (snapshot) and `'runloop'` (blueprint);
Expand Down Expand Up @@ -179,8 +180,8 @@ def get_default_working_dir(provider: str) -> str:
"""Get the default working directory for a given sandbox provider.

Args:
provider: Sandbox provider name (`'agentcore'`, `'daytona'`, `'langsmith'`,
`'modal'`, `'runloop'`)
provider: Sandbox provider name (`'agentcore'`, `'daytona'`, `'e2b'`,
`'langsmith'`, `'modal'`, `'runloop'`)

Returns:
Default working directory path as string
Expand Down Expand Up @@ -698,6 +699,78 @@ def delete(self, *, sandbox_id: str, **kwargs: Any) -> None: # noqa: ARG002
self._provider.delete(sandbox_id=sandbox_id)


class _E2BProvider(SandboxProvider):
"""E2B sandbox provider — delegates to `langchain_e2b.E2BProvider`."""

def __init__(self) -> None:
e2b_module = _import_provider_module(
"langchain_e2b",
provider="e2b",
package="langchain-e2b",
)

from deepagents_code.model_config import resolve_env_var

api_key = resolve_env_var("E2B_API_KEY")
if not api_key:
msg = (
"No E2B API key found. Set E2B_API_KEY or DEEPAGENTS_CODE_E2B_API_KEY."
)
raise ValueError(msg)
try:
provider = e2b_module.E2BProvider
except AttributeError as exc:
msg = (
"The 'e2b' sandbox provider requires langchain-e2b>=0.0.2. "
"Upgrade with `pip install -U langchain-e2b`."
)
raise ImportError(msg) from exc
self._provider = provider(
api_key=api_key,
resolve_env_var=resolve_env_var,
)

def get_or_create(
self,
*,
sandbox_id: str | None = None,
timeout: int = 180,
**kwargs: Any,
) -> SandboxBackendProtocol:
"""Get or create an E2B sandbox.

Args:
sandbox_id: Existing sandbox ID, or None to create.
timeout: Accepted for parity with other providers and forwarded to
`langchain_e2b.E2BProvider`.
**kwargs: E2B provider options.

Returns:
`E2BSandbox` instance.

Raises:
SandboxNotFoundError: If `sandbox_id` does not exist. `E2BProvider`
translates the SDK's not-found error into a `KeyError`, which is
mapped here.
KeyError: If a `KeyError` is raised while no `sandbox_id` was supplied
(re-raised unchanged rather than mislabeled as not-found).
"""
try:
return self._provider.get_or_create(
sandbox_id=sandbox_id,
timeout=timeout,
**kwargs,
)
except KeyError as e:
if sandbox_id is None:
raise
raise SandboxNotFoundError(sandbox_id) from e

def delete(self, *, sandbox_id: str, **kwargs: Any) -> None: # noqa: ARG002
"""Shut down an E2B sandbox by id."""
self._provider.delete(sandbox_id=sandbox_id)


class _AgentCoreProvider(SandboxProvider):
"""AgentCore Code Interpreter sandbox provider.

Expand Down Expand Up @@ -829,8 +902,8 @@ def _get_provider(provider_name: str) -> SandboxProvider:
"""Get a `SandboxProvider` instance for the specified provider (internal).

Args:
provider_name: Name of the provider (`'agentcore'`, `'daytona'`, `'langsmith'`,
`'modal'`, `'runloop'`)
provider_name: Name of the provider (`'agentcore'`, `'daytona'`, `'e2b'`,
`'langsmith'`, `'modal'`, `'runloop'`)

Returns:
`SandboxProvider` instance
Expand All @@ -842,6 +915,8 @@ def _get_provider(provider_name: str) -> SandboxProvider:
return _AgentCoreProvider()
if provider_name == "daytona":
return _DaytonaProvider()
if provider_name == "e2b":
return _E2BProvider()
if provider_name == "langsmith":
return _LangSmithProvider()
if provider_name == "modal":
Expand Down Expand Up @@ -878,6 +953,7 @@ def verify_sandbox_deps(provider: str) -> None:
backend_modules: dict[str, tuple[str, str]] = {
"agentcore": ("langchain_agentcore_codeinterpreter", "agentcore"),
"daytona": ("langchain_daytona", "daytona"),
"e2b": ("langchain_e2b", "e2b"),
"modal": ("langchain_modal", "modal"),
"runloop": ("langchain_runloop", "runloop"),
}
Expand Down
15 changes: 12 additions & 3 deletions libs/code/deepagents_code/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1023,13 +1023,21 @@ def help_parent(help_fn: Callable[[], None]) -> list[argparse.ArgumentParser]:

parser.add_argument(
"--sandbox",
choices=["none", "agentcore", "modal", "daytona", "runloop", "langsmith"],
choices=[
"none",
"agentcore",
"modal",
"daytona",
"e2b",
"runloop",
"langsmith",
],
default="none",
metavar="TYPE",
help=(
"Remote sandbox for code execution "
"(default: none - local only; langsmith is included, "
"agentcore/modal/daytona/runloop require downloading extras)"
"agentcore/modal/daytona/e2b/runloop require downloading extras)"
),
)

Expand Down Expand Up @@ -1205,7 +1213,8 @@ async def run_textual_cli_async(
assistant_id: Agent identifier for memory storage
auto_approve: Whether to auto-approve tool usage
sandbox_type: Type of sandbox
("none", "agentcore", "modal", "runloop", "daytona", "langsmith")
("none", "agentcore", "modal", "runloop", "daytona", "e2b",
"langsmith")
sandbox_id: Optional existing sandbox ID to reuse.
sandbox_snapshot_name: Snapshot (langsmith) or blueprint (runloop) name.
sandbox_setup: Optional path to setup script to run in the sandbox
Expand Down
3 changes: 2 additions & 1 deletion libs/code/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,11 @@ all-providers = [
# Sandbox providers
agentcore = ["langchain-agentcore-codeinterpreter>=0.0.3,<1.0.0"]
daytona = ["langchain-daytona>=0.0.6"]
e2b = ["langchain-e2b>=0.0.2"]
modal = ["langchain-modal>=0.0.4"]
runloop = ["langchain-runloop>=0.0.6"]
all-sandboxes = [
"deepagents-code[agentcore,daytona,modal,runloop]",
"deepagents-code[agentcore,daytona,e2b,modal,runloop]",
]

# Standalone integrations (not part of any composite extra)
Expand Down
3 changes: 3 additions & 0 deletions libs/code/tests/unit_tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9167,6 +9167,9 @@ async def test_sandbox_sub_title_proper_casing(self) -> None:
app2 = DeepAgentsApp(server_kwargs={"sandbox_type": "agentcore"})
assert app2.sub_title == "Sandbox: AgentCore"

app3 = DeepAgentsApp(server_kwargs={"sandbox_type": "e2b"})
assert app3.sub_title == "Sandbox: E2B"

async def test_explicit_sub_title_overrides_sandbox(self) -> None:
"""An explicitly passed sub_title is not overwritten by sandbox info."""
app = DeepAgentsApp(sub_title="custom", server_kwargs={"sandbox_type": "modal"})
Expand Down
1 change: 1 addition & 0 deletions libs/code/tests/unit_tests/test_install_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ async def test_install_slash_usage_when_no_extra() -> None:
assert "Available extras:" in rendered
assert "quickjs" in rendered
assert "daytona" in rendered
assert "e2b" in rendered
assert "openai" in rendered


Expand Down
7 changes: 6 additions & 1 deletion libs/code/tests/unit_tests/test_launch_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ class TestLaunchDependenciesScreen:
installed=(("langchain-daytona", "0.0.5"),),
missing=(),
),
ExtraDependencyStatus(
name="e2b",
installed=(),
missing=("langchain-e2b",),
),
ExtraDependencyStatus(
name="runloop",
installed=(),
Expand All @@ -194,7 +199,7 @@ async def test_renders_installed_and_available_extras(self) -> None:
assert "Sandboxes: daytona" in content
assert "Available to add" in content
assert "Model providers: bedrock" in content
assert "Sandboxes: runloop" in content
assert "Sandboxes: e2b, runloop" in content
assert "Esc skip setup" in content

async def test_enter_continues(self) -> None:
Expand Down
Loading
Loading