Skip to content
Merged
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
14 changes: 10 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ SHELL=/bin/bash
examples examples\:prod examples\:proxy examples-proxy agent agent-nodes agent-nodes\:proxy agent-nodes-proxy agent-notebook agent-document dev-notebook dev-document jupyter-server agent-serve \
docker-build docker-push docker-release agent-runtime-docker-build agent-runtime-docker-push agent-runtime-docker-release node-agent-artifact-build node-agents-docker-build agent-nodes-docker-build agent-nodes-docker-push agent-nodes-docker-start agent-nodes-docker-stop agent-nodes-docker-logs \
agents list-specs specs specs-clone specs-generate specs-format \
specs-sandbox-variants \
loop loop-simple loop-data-acquisition loop-financial loop-demo loop-example-nocodemode

AGENTSPECS_REPO ?= https://github.com/datalayer/agentspecs.git
Expand Down Expand Up @@ -151,9 +152,9 @@ EXAMPLES_LOCAL_ENV = \
VITE_BASE_URL_CODEMODE=http://localhost:8766

BEDROCK_ENV = \
AWS_ACCESS_KEY_ID=${DATALAYER_BEDROCK_AWS_ACCESS_KEY_ID} \
AWS_SECRET_ACCESS_KEY=${DATALAYER_BEDROCK_AWS_SECRET_ACCESS_KEY} \
AWS_DEFAULT_REGION=${DATALAYER_BEDROCK_AWS_DEFAULT_REGION}
AWS_ACCESS_KEY_ID=$${DATALAYER_BEDROCK_AWS_ACCESS_KEY_ID:-$${AWS_ACCESS_KEY_ID}} \
AWS_SECRET_ACCESS_KEY=$${DATALAYER_BEDROCK_AWS_SECRET_ACCESS_KEY:-$${AWS_SECRET_ACCESS_KEY}} \
AWS_DEFAULT_REGION=$${DATALAYER_BEDROCK_AWS_DEFAULT_REGION:-$${AWS_DEFAULT_REGION}}

RUFF_TARGETS = \
agent_runtimes/specs/agents/ \
Expand Down Expand Up @@ -391,7 +392,12 @@ loop-demo-nocodemode: # loop-demo-nocodemode
list-specs: # list specs
agent-runtimes list-specs

specs: specs-clone specs-generate specs-format ## generate Python and TypeScript code from YAML specifications (agents, teams, MCP servers, skills, envvars)
specs: specs-clone specs-sandbox-variants specs-generate specs-format ## generate Python and TypeScript code from YAML specifications (agents, teams, MCP servers, skills, envvars)

specs-sandbox-variants: ## scaffold sandbox example agent specs for all supported sandbox variants
$(call step,Generating sandbox variant example agents)
python scripts/codegen/generate_sandbox_agents.py \
--agents-dir $(AGENTSPECS_DIR)/agentspecs/agents

specs-clone: ## clone/update agentspecs repository
$(call step,Cloning agentspecs repository ($(AGENTSPECS_BRANCH)))
Expand Down
5 changes: 5 additions & 0 deletions agent_runtimes/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,11 @@ def rebuild_codemode(new_servers: list[str | dict[str, str]]) -> Any:
skills_path=skills_folder_path
or str((repo_root / "skills").resolve()),
allow_direct_tool_calls=False,
**(
{}
if os.getenv("AGENT_RUNTIMES_SANDBOX_GPU") is None
else {"sandbox_gpu": os.getenv("AGENT_RUNTIMES_SANDBOX_GPU")}
),
**(
{}
if mcp_proxy_url is None
Expand Down
4 changes: 1 addition & 3 deletions agent_runtimes/chat/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -714,9 +714,7 @@ def _pick_agentspec_interactive() -> str:
if model_spec is not None:
model_ok = model_spec.id in available_model_ids
model_color = GREEN_LIGHT if model_ok else RED
print(
f" model: {model_color}{model_spec.id}{RESET}"
)
print(f" model: {model_color}{model_spec.id}{RESET}")
else:
print(f" model: {GRAY}{model_ref} (custom/unknown){RESET}")

Expand Down
2 changes: 1 addition & 1 deletion agent_runtimes/decorators/datalayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:
# print("function_source:", function_source)
# print("function_call:", function_call)

client = AgentClient(token=token) # Resolves token from param/env/keyring
client = AgentClient(api_key=token) # Resolves token from param/env/keyring
with client.create_runtime(
name=runtime_name_decorated,
snapshot_name=snapshot_name_decorated,
Expand Down
9 changes: 9 additions & 0 deletions agent_runtimes/integrations/codemode.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def __init__(
mcp_manager: Optional[MCPManager] = None,
skills_path: str = "./skills",
sandbox_variant: str = "eval",
sandbox_gpu: str | None = None,
):
"""
Initialize the integration.
Expand All @@ -63,10 +64,13 @@ def __init__(
mcp_manager: Optional MCPManager from agent-runtimes.
skills_path: Directory for skill storage.
sandbox_variant: Sandbox type for code execution.
sandbox_gpu: Optional GPU flavor / accelerator for supported
sandbox variants.
"""
self.mcp_manager = mcp_manager or get_mcp_manager()
self.skills_path = skills_path
self.sandbox_variant = sandbox_variant
self.sandbox_gpu = sandbox_gpu

# Lazy imports for optional dependencies
self._registry = None
Expand Down Expand Up @@ -124,6 +128,11 @@ async def setup(self) -> None:
config = CodeModeConfig(
skills_path=self.skills_path,
sandbox_variant=self.sandbox_variant,
**(
{}
if self.sandbox_gpu is None
else {"sandbox_gpu": self.sandbox_gpu}
),
**({} if mcp_proxy_url is None else {"mcp_proxy_url": mcp_proxy_url}),
)
self._executor = CodeModeExecutor(self._registry, config)
Expand Down
42 changes: 42 additions & 0 deletions agent_runtimes/routes/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ class SandboxExecuteRequest(BaseModel):
timeout: Optional[float] = Field(
default=None, description="Execution timeout in seconds."
)
stream: bool = Field(
default=False,
description=(
"When true, execute via sandbox streaming API and aggregate streamed "
"events into the response."
),
)


class SandboxExecuteResponse(BaseModel):
Expand Down Expand Up @@ -85,6 +92,41 @@ async def execute_sandbox_code(

client = CodeSandboxClient(sandbox)
try:
if request.stream and hasattr(client, "execute_code_streaming_async"):
stdout_lines: list[str] = []
stderr_lines: list[str] = []
results: list[str] = []
error: str | None = None

async for event in client.execute_code_streaming_async(
request.code,
language=request.language,
timeout=request.timeout,
):
if hasattr(event, "line"):
line = getattr(event, "line", "") or ""
if bool(getattr(event, "error", False)):
stderr_lines.append(line)
else:
stdout_lines.append(line)
elif hasattr(event, "data"):
data = getattr(event, "data", {}) or {}
text = data.get("text/plain")
if text is not None:
results.append(str(text))
elif hasattr(event, "name") and hasattr(event, "value"):
error = f"{getattr(event, 'name', 'Error')}: {getattr(event, 'value', '')}"

return SandboxExecuteResponse(
success=error is None,
execution_ok=True,
stdout="\n".join(stdout_lines),
stderr="\n".join(stderr_lines),
results=results,
error=error,
variant=str(client.variant) if client.variant else None,
)

outcome = await client.execute_code_async(
request.code,
language=request.language,
Expand Down
4 changes: 4 additions & 0 deletions agent_runtimes/services/agent_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,10 @@ def replace(match: re.Match[str]) -> str:
if effective_variant:
config_kwargs["sandbox_variant"] = effective_variant

sandbox_gpu = os.getenv("AGENT_RUNTIMES_SANDBOX_GPU")
if sandbox_gpu:
config_kwargs["sandbox_gpu"] = sandbox_gpu

# When discovery tools are disabled, treat this as sandbox-only mode
# and prevent the executor from materializing ``generated/`` bindings
# or extending the sandbox ``sys.path``.
Expand Down
57 changes: 44 additions & 13 deletions agent_runtimes/services/code_sandbox_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
Code Sandbox Manager for Agent Runtimes.

This module provides a centralized manager for code sandbox instances,
allowing runtime configuration of the sandbox variant (eval or jupyter).
allowing runtime configuration of the sandbox variant.

It also provides :class:`ManagedSandbox`, a transparent proxy that
delegates every call to the manager's current sandbox. All consumers
Expand Down Expand Up @@ -55,7 +55,15 @@
logger = logging.getLogger(__name__)


SandboxVariant = Literal["eval", "jupyter"]
SandboxVariant = Literal[
"eval",
"jupyter",
"docker",
"datalayer",
"colab",
"monty",
"modal",
]


@dataclass
Expand Down Expand Up @@ -229,9 +237,10 @@ async def run_code_streaming_async(
envs: Optional[dict[str, str]] = None,
timeout: Optional[float] = None,
) -> AsyncIterator[Any]:
return await self._sandbox().run_code_streaming_async(
async for item in self._sandbox().run_code_streaming_async(
code, language=language, context=context, envs=envs, timeout=timeout
)
):
yield item

def create_context(self, name: Optional[str] = None) -> Any:
return self._sandbox().create_context(name=name)
Expand Down Expand Up @@ -337,11 +346,13 @@ class CodeSandboxManager:
- Automatic sandbox lifecycle management (start/stop)
- Per-agent sandbox isolation (each agent gets its own sandbox)

The manager supports three sandbox variants:
The manager supports sandbox variants:
- eval: Uses Python exec() for code execution (default)
- jupyter: Connects to an *existing* Jupyter server (URL required)
- jupyter: Delegates to code_sandboxes to start its own Jupyter server
on a random free port (no external URL needed)
- docker, datalayer, colab, monty, modal: Delegated to the
code_sandboxes variant factory.
"""

_instance: CodeSandboxManager | None = None
Expand Down Expand Up @@ -734,11 +745,17 @@ def _create_sandbox(self, variant: SandboxVariant | None = None) -> Sandbox:
ValueError: If configuration is invalid.
"""
effective_variant = variant or self._config.variant
try:
from code_sandboxes import Sandbox as CodeSandbox
except (ImportError, AttributeError):
CodeSandbox = None

if effective_variant == "eval":
from code_sandboxes.eval_sandbox import EvalSandbox
if CodeSandbox is None:
from code_sandboxes.eval_sandbox import EvalSandbox

return EvalSandbox()
return EvalSandbox()
return CodeSandbox.create(variant="eval")

elif effective_variant == "jupyter":
# In sidecar mode, companion must provide a concrete Jupyter URL.
Expand All @@ -751,20 +768,34 @@ def _create_sandbox(self, variant: SandboxVariant | None = None) -> Sandbox:
"Jupyter sidecar mode requires jupyter_url before sandbox start"
)

from code_sandboxes.jupyter_sandbox import JupyterSandbox

if self._config.jupyter_url:
return JupyterSandbox(
if CodeSandbox is None:
from code_sandboxes.jupyter_sandbox import JupyterSandbox

return JupyterSandbox(
server_url=self._config.jupyter_url,
token=self._config.jupyter_token,
)
return CodeSandbox.create(
variant="jupyter",
server_url=self._config.jupyter_url,
token=self._config.jupyter_token,
)

# No external URL configured: let code_sandboxes start its own
# local Jupyter server on a free port.
return JupyterSandbox()
if CodeSandbox is None:
from code_sandboxes.jupyter_sandbox import JupyterSandbox

return JupyterSandbox()
return CodeSandbox.create(variant="jupyter")

else:
raise ValueError(f"Unknown sandbox variant: {effective_variant}")
if CodeSandbox is None:
raise ImportError(
"code_sandboxes.Sandbox is required for non-jupyter/eval variants"
)
return CodeSandbox.create(variant=effective_variant)

def stop(self) -> None:
"""Stop the current sandbox if running."""
Expand Down Expand Up @@ -795,7 +826,7 @@ def create_agent_sandbox(

Args:
agent_id: Unique agent identifier.
variant: The sandbox variant (``"eval"`` or ``"jupyter"``).
variant: The sandbox variant.
env_vars: Environment variables to inject into the sandbox
after it starts.

Expand Down
Loading
Loading