From 17da8e345b64a26ed97742befb4ebd0f332a9374 Mon Sep 17 00:00:00 2001 From: Muhammad Hashmi Date: Sun, 19 Apr 2026 12:45:20 -0700 Subject: [PATCH 1/4] build: add daytona extra Signed-off-by: Muhammad Hashmi --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 79bae436f..1200cea8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -145,6 +145,7 @@ agentcore = [ ] code-tools = [ + "daytona", "e2b_code_interpreter", "together>=1.4", ] From 830322ad7b9a6234aa83ee5477a325aafd71190c Mon Sep 17 00:00:00 2001 From: Muhammad Hashmi Date: Sun, 19 Apr 2026 12:53:27 -0700 Subject: [PATCH 2/4] feat: rewrite daytona backend Signed-off-by: Muhammad Hashmi --- rllm/tools/code_tools/daytona_tool.py | 154 ++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 rllm/tools/code_tools/daytona_tool.py diff --git a/rllm/tools/code_tools/daytona_tool.py b/rllm/tools/code_tools/daytona_tool.py new file mode 100644 index 000000000..753dde10e --- /dev/null +++ b/rllm/tools/code_tools/daytona_tool.py @@ -0,0 +1,154 @@ +import os +from typing import Any + +try: + from daytona import ( + CreateSandboxFromSnapshotParams, + Daytona, + DaytonaAuthenticationError, + DaytonaConfig, + ) +except ImportError: + CreateSandboxFromSnapshotParams = None + Daytona = None + DaytonaAuthenticationError = None + DaytonaConfig = None + +from rllm.tools.code_tools.code_tool import CodeTool, CodeToolOutput + +DAYTONA_API_KEY = os.environ.get("DAYTONA_API_KEY", None) + + +class DaytonaPythonInterpreter(CodeTool): + """Execute Python code in a Daytona sandbox""" + + def __init__( + self, + n_sandboxes: int = 1, + api_key: str | None = DAYTONA_API_KEY, + api_url: str | None = None, + snapshot: str | None = None, + env_vars: dict[str, str] | None = None, + ): + if Daytona is None: + raise ImportError("daytona is not installed. Please install it with `pip install daytona`.") + assert n_sandboxes > 0, "Number of sandboxes must be greater than 0" + + super().__init__( + name="daytona_python", + description="A tool that executes python code in a Daytona sandbox and returns standard output/error.", + n_sandboxes=n_sandboxes, + ) + + self.api_key = api_key + self.api_url = api_url + self.snapshot = snapshot + self.env_vars = env_vars + self._init_sandbox() + + def _init_sandbox(self): + """Initialize multiple sandbox environments.""" + config_kwargs: dict[str, Any] = {} + if self.api_key is not None: + config_kwargs["api_key"] = self.api_key + if self.api_url is not None: + config_kwargs["api_url"] = self.api_url + self._client = Daytona(DaytonaConfig(**config_kwargs)) + + params_kwargs: dict[str, Any] = {"language": "python"} + if self.snapshot is not None: + params_kwargs["snapshot"] = self.snapshot + if self.env_vars is not None: + params_kwargs["env_vars"] = dict(self.env_vars) + params = CreateSandboxFromSnapshotParams(**params_kwargs) + + self.sandboxes = [] + self.cur_sandbox_idx = 0 + for _ in range(self.n_sandboxes): + self.sandboxes.append(self._client.create(params)) + + def _kill_sandbox(self): + """Clean up all sandbox resources.""" + for sandbox in getattr(self, "sandboxes", []): + try: + sandbox.delete() + except Exception as e: + print(f"Error deleting sandbox: {e}") + self.sandboxes = [] + + def forward(self, code: str, timeout: int = 20, **kwargs) -> CodeToolOutput: + """ + Execute Python code in one of the sandboxes using round-robin distribution. + + Args: + code: Python code to execute + timeout: Maximum execution time in seconds + **kwargs: Additional parameters including id, max_retries + + Returns: + CodeToolOutput containing execution results, stdout, and stderr + """ + id = kwargs.get("id", None) + max_retries = kwargs.get("max_retries", 3) + + if id: + self.cur_sandbox_idx = id % self.n_sandboxes + else: + self.cur_sandbox_idx = (self.cur_sandbox_idx + 1) % self.n_sandboxes + sandbox = self.sandboxes[self.cur_sandbox_idx] + + for _ in range(max_retries): + try: + response = sandbox.process.code_run(code, timeout=timeout) + break + except DaytonaAuthenticationError as e: + return CodeToolOutput(name=self.name or "daytona_python", error=f"Auth error: {e}") + except Exception: + continue + else: + return CodeToolOutput(name=self.name or "daytona_python", error="Sandbox error, please try again.") + + stdout = None + stderr = None + error = None + if response.exit_code == 0: + stdout = response.result or None + else: + stderr = response.result or None + if response.result: + lines = response.result.strip().splitlines() + if lines: + error = lines[-1] + + return CodeToolOutput(name=self.name or "daytona_python", stdout=stdout, stderr=stderr, error=error) + + @property + def json(self) -> dict[str, Any]: + return { + "type": "function", + "function": { + "name": self.name, + "description": self.description, + "parameters": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "Python code to execute in a Daytona sandbox environment.", + }, + }, + "required": ["code"], + }, + }, + } + + +if __name__ == "__main__": + from pprint import pprint + + interpreter = DaytonaPythonInterpreter() + try: + pprint(interpreter(code="print('Hello, world!')\nimport math\nmath.sqrt(4)")) + pprint(interpreter(code="1/0")) + finally: + interpreter._kill_sandbox() From 15b1e16c3e3dc9f3ba4523b0f5061820fbdcedbd Mon Sep 17 00:00:00 2001 From: Muhammad Hashmi Date: Sun, 19 Apr 2026 12:54:25 -0700 Subject: [PATCH 3/4] feat: wire daytona into interpreter Signed-off-by: Muhammad Hashmi --- rllm/tools/code_tools/python_interpreter.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/rllm/tools/code_tools/python_interpreter.py b/rllm/tools/code_tools/python_interpreter.py index fe8c4311a..f6ed49257 100644 --- a/rllm/tools/code_tools/python_interpreter.py +++ b/rllm/tools/code_tools/python_interpreter.py @@ -1,12 +1,13 @@ from typing import Any, Literal from rllm.tools.code_tools.code_tool import CodeTool, CodeToolOutput +from rllm.tools.code_tools.daytona_tool import DaytonaPythonInterpreter from rllm.tools.code_tools.e2b_tool import E2BPythonInterpreter from rllm.tools.code_tools.lcb_tool import LCBPythonInterpreter from rllm.tools.code_tools.together_tool import TogetherCodeTool # Backend types -BackendType = Literal["local", "e2b", "together", "lcb"] +BackendType = Literal["local", "e2b", "together", "lcb", "daytona"] class PythonInterpreter(CodeTool): @@ -15,7 +16,7 @@ class PythonInterpreter(CodeTool): This class provides a common interface for executing Python code using different backend implementations, including local execution, E2B sandbox, Together API, - and LiveCodeBench environment. + Daytona sandboxes, and LiveCodeBench environment. """ def __init__( @@ -30,9 +31,9 @@ def __init__( Initialize the unified Python interpreter with the specified backend. Args: - backend: The backend to use ("local", "e2b", "together", or "lcb") + backend: The backend to use ("local", "e2b", "together", "lcb", or "daytona") n_sandboxes: Number of concurrent sandboxes/workers to use (for applicable backends) - api_key: API key for cloud-based backends (e2b, together) + api_key: API key for cloud-based backends (e2b, together, daytona) name: The name of the tool description: Description of what the tool does """ @@ -48,11 +49,13 @@ def __init__( def _init_backend(self): """Initialize the selected backend interpreter.""" if self.backend_type == "local": - self.backend: LCBPythonInterpreter | E2BPythonInterpreter | TogetherCodeTool = LCBPythonInterpreter() + self.backend: LCBPythonInterpreter | E2BPythonInterpreter | TogetherCodeTool | DaytonaPythonInterpreter = LCBPythonInterpreter() elif self.backend_type == "e2b": self.backend = E2BPythonInterpreter(n_sandboxes=self.n_sandboxes, api_key=self.api_key) elif self.backend_type == "together": self.backend = TogetherCodeTool(api_key=self.api_key) + elif self.backend_type == "daytona": + self.backend = DaytonaPythonInterpreter(n_sandboxes=self.n_sandboxes, api_key=self.api_key) else: raise ValueError(f"Unsupported backend type: {self.backend_type}") From 726f8ee11a00aad4be03d681b9182abc7a078ab7 Mon Sep 17 00:00:00 2001 From: Muhammad Hashmi Date: Sun, 19 Apr 2026 12:54:56 -0700 Subject: [PATCH 4/4] chore: export and document daytona Signed-off-by: Muhammad Hashmi --- docs/getting-started/installation.md | 4 ++-- rllm/tools/code_tools/__init__.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index a9ae91131..a63ae987a 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -69,7 +69,7 @@ uv pip install -e . rLLM provides additional optional dependencies for specific agent domains and framework integrations. For example: - `web`: Tools for web agents (BrowserGym, Selenium). -- `code-tools`: Sandboxed code execution (E2B, Together). +- `code-tools`: Sandboxed code execution (E2B, Together, Daytona). - `smolagents`: Integration with Hugging Face's smolagents. See the full list of managed extras [here](pyproject.toml). @@ -100,4 +100,4 @@ docker start rllm-container docker exec -it rllm-container bash ``` -For more help, refer to the [GitHub issues page](https://github.com/rllm-org/rllm/issues). \ No newline at end of file +For more help, refer to the [GitHub issues page](https://github.com/rllm-org/rllm/issues). diff --git a/rllm/tools/code_tools/__init__.py b/rllm/tools/code_tools/__init__.py index a7fb5f3f1..6e71d88ba 100644 --- a/rllm/tools/code_tools/__init__.py +++ b/rllm/tools/code_tools/__init__.py @@ -1,3 +1,4 @@ +from rllm.tools.code_tools.daytona_tool import DaytonaPythonInterpreter from rllm.tools.code_tools.e2b_tool import E2BPythonInterpreter from rllm.tools.code_tools.lcb_tool import LCBPythonInterpreter from rllm.tools.code_tools.python_interpreter import PythonInterpreter @@ -5,6 +6,7 @@ __all__ = [ "PythonInterpreter", # New unified interpreter + "DaytonaPythonInterpreter", "E2BPythonInterpreter", # Legacy interpreters for backward compatibility "LCBPythonInterpreter", "TogetherCodeTool",