Skip to content
Draft
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
3 changes: 3 additions & 0 deletions libs/deepagents-cli/deepagents_cli/integrations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Sandbox integrations for DeepAgents CLI."""

__all__ = ["DaytonaBackend", "RunloopBackend", "ModalBackend"]
46 changes: 46 additions & 0 deletions libs/deepagents-cli/deepagents_cli/integrations/daytona.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Daytona sandbox backend implementation."""

from __future__ import annotations

from typing import TYPE_CHECKING

from deepagents.backends.protocol import ExecuteResponse
from deepagents.backends.sandbox import BaseSandbox

if TYPE_CHECKING:
from daytona import Sandbox


class DaytonaBackend(BaseSandbox):
"""Daytona backend implementation conforming to SandboxBackendProtocol.

This implementation inherits all file operation methods from BaseSandbox
and only implements the execute() method using Daytona's API.
"""

def __init__(self, sandbox: Sandbox) -> None:
"""Initialize the DaytonaBackend with a Daytona sandbox client."""
self._sandbox = sandbox

def execute(
self,
command: str,
*,
timeout: int = 30 * 60,
) -> ExecuteResponse:
"""Execute a command in the sandbox and return ExecuteResponse.

Args:
command: Full shell command string to execute.
timeout: Maximum execution time in seconds (default: 30 minutes).

Returns:
ExecuteResponse with combined output, exit code, optional signal, and truncation flag.
"""
result = self._sandbox.process.exec(command, timeout=timeout)

return ExecuteResponse(
output=result.result, # Daytona combines stdout/stderr
exit_code=result.exit_code,
truncated=False,
)
63 changes: 63 additions & 0 deletions libs/deepagents-cli/deepagents_cli/integrations/modal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Modal sandbox backend implementation."""

from __future__ import annotations

from typing import TYPE_CHECKING

from deepagents.backends.protocol import ExecuteResponse
from deepagents.backends.sandbox import BaseSandbox

if TYPE_CHECKING:
import modal


class ModalBackend(BaseSandbox):
"""Modal backend implementation conforming to SandboxBackendProtocol.

This implementation inherits all file operation methods from BaseSandbox
and only implements the execute() method using Modal's API.
"""

def __init__(self, sandbox: modal.Sandbox) -> None:
"""Initialize the ModalBackend with a Modal sandbox instance.

Args:
sandbox: Active Modal Sandbox instance
"""
self._sandbox = sandbox

def execute(
self,
command: str,
*,
timeout: int = 30 * 60,
) -> ExecuteResponse:
"""Execute a command in the sandbox and return ExecuteResponse.

Args:
command: Full shell command string to execute.
timeout: Maximum execution time in seconds (default: 30 minutes).

Returns:
ExecuteResponse with combined output, exit code, and truncation flag.
"""
# Execute command using Modal's exec API
process = self._sandbox.exec("bash", "-c", command, timeout=timeout)

# Wait for process to complete
process.wait()

# Read stdout and stderr
stdout = process.stdout.read()
stderr = process.stderr.read()

# Combine stdout and stderr (matching runloop.py)
output = stdout or ""
if stderr:
output += "\n" + stderr if output else stderr

return ExecuteResponse(
output=output,
exit_code=process.returncode,
truncated=False, # Modal doesn't provide truncation info
)
81 changes: 81 additions & 0 deletions libs/deepagents-cli/deepagents_cli/integrations/runloop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""BackendProtocol implementation for Runloop."""

try:
import runloop_api_client
except ImportError:
raise ImportError(
"runloop_api_client package is required for RunloopBackend. "
"Install with `pip install runloop_api_client`."
)

import os

from runloop_api_client import Runloop

from deepagents.backends.protocol import ExecuteResponse
from deepagents.backends.sandbox import BaseSandbox


class RunloopBackend(BaseSandbox):
"""Backend that operates on files in a Runloop devbox.

This implementation uses the Runloop API client to execute commands
and manipulate files within a remote devbox environment.
"""

def __init__(
self,
devbox_id: str,
client: Runloop | None = None,
bearer_token: str | None = None,
) -> None:
"""Initialize Runloop protocol.

Args:
devbox_id: ID of the Runloop devbox to operate on.
client: Optional existing Runloop client instance
bearer_token: Optional API key for creating a new client
(defaults to RUNLOOP_API_KEY environment variable)
"""
if client and bearer_token:
raise ValueError("Provide either client or bearer_token, not both.")

if client is None:
bearer_token = bearer_token or os.environ.get("RUNLOOP_API_KEY", None)
if bearer_token is None:
raise ValueError("Either client or bearer_token must be provided.")
client = Runloop(bearer_token=bearer_token)

self._client = client
self._devbox_id = devbox_id

def execute(
self,
command: str,
*,
timeout: int = 30 * 60,
) -> ExecuteResponse:
"""Execute a command in the devbox and return ExecuteResponse.

Args:
command: Full shell command string to execute.
timeout: Maximum execution time in seconds (default: 30 minutes).

Returns:
ExecuteResponse with combined output, exit code, optional signal, and truncation flag.
"""
result = self._client.devboxes.execute_and_await_completion(
devbox_id=self._devbox_id,
command=command,
timeout=timeout,
)
# Combine stdout and stderr
output = result.stdout or ""
if result.stderr:
output += "\n" + result.stderr if output else result.stderr

return ExecuteResponse(
output=output,
exit_code=result.exit_status,
truncated=False, # Runloop doesn't provide truncation info
)
2 changes: 2 additions & 0 deletions libs/deepagents-cli/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ dependencies = [
"langchain-openai>=0.1.0",
"tavily-python",
"python-dotenv",
"daytona>=0.113.0",
"modal>=1.2.0",
]

[project.scripts]
Expand Down
Loading