Skip to content

Commit c0135f6

Browse files
authored
code sandboxes (#7)
* code sandboxes * lint * fix: test * fix: test * lint * lint * fix: tests
1 parent 743d966 commit c0135f6

11 files changed

Lines changed: 174 additions & 253 deletions

File tree

agent_codemode/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55
"""Agent Codemode."""
66

7-
__version__ = "0.1.6"
7+
__version__ = "1.0.0"

agent_codemode/composition/executor.py

Lines changed: 96 additions & 206 deletions
Large diffs are not rendered by default.

agent_codemode/server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,9 @@ async def handle_execute_code(arguments: dict[str, Any]) -> dict[str, Any]:
323323
await executor.setup()
324324

325325
# Inject context variables if provided
326-
if context and executor._sandbox:
326+
if context and executor.sandbox_client:
327327
for name, value in context.items():
328-
executor._sandbox.set_variable(name, value)
328+
executor.sandbox_client.set_variable(name, value)
329329

330330
try:
331331
execution = await executor.execute(code, timeout=timeout)

agent_codemode/toolset.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ class CodemodeToolset(AbstractToolset):
9999

100100
registry: ToolRegistry | None = None
101101
config: CodeModeConfig = field(default_factory=CodeModeConfig)
102-
sandbox: Any | None = None # Optional pre-configured sandbox (e.g., EvalSandbox)
102+
sandbox_client: Any | None = None
103103
allow_direct_tool_calls: bool | None = None
104104
allow_discovery_tools: bool = True
105105
tool_reranker: Callable[[list, str, Optional[str]], Awaitable[list]] | None = None
@@ -190,7 +190,7 @@ async def _ensure_initialized(self) -> None:
190190
self._executor = CodeModeExecutor(
191191
registry=registry,
192192
config=self.config,
193-
sandbox=self.sandbox,
193+
sandbox_client=self.sandbox_client,
194194
)
195195
await self._executor.setup()
196196
logger.info(

docs/docs/skills/index.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,10 +372,10 @@ For Pydantic AI agents, use the `AgentSkillsToolset`:
372372
```python
373373
from pydantic_ai import Agent
374374
from agent_skills import AgentSkillsToolset, SandboxExecutor
375-
from code_sandboxes.eval_sandbox import EvalSandbox
375+
from code_sandboxes import CodeSandboxClient
376376

377377
# Create toolset with sandbox execution
378-
sandbox = EvalSandbox()
378+
sandbox = CodeSandboxClient.create(variant="eval")
379379
toolset = AgentSkillsToolset(
380380
directories=["./skills"],
381381
executor=SandboxExecutor(sandbox),

examples/simple/agent_cli.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535
try:
3636
from agent_skills import AgentSkillsToolset, SandboxExecutor
37-
from code_sandboxes.eval_sandbox import EvalSandbox
37+
from code_sandboxes import CodeSandboxClient
3838

3939
HAS_AGENT_SKILLS = True
4040
except ImportError:
@@ -351,16 +351,16 @@ def create_agent(model: str, codemode: bool) -> tuple[Agent, object | None, obje
351351
)
352352

353353
# Create shared sandbox for both CodemodeToolset and AgentSkillsToolset
354-
shared_sandbox = None
354+
shared_client = None
355355
skills_toolset = None
356356
if HAS_AGENT_SKILLS:
357-
shared_sandbox = EvalSandbox()
358-
logger.info("Created shared EvalSandbox for codemode and skills toolsets")
357+
shared_client = CodeSandboxClient.create(variant="eval")
358+
logger.info("Created shared CodeSandboxClient for codemode and skills")
359359

360360
toolset = CodemodeToolset(
361361
registry=registry,
362362
config=config,
363-
sandbox=shared_sandbox,
363+
sandbox_client=shared_client,
364364
allow_discovery_tools=True, # Enable discovery tools (search_tools, get_tool_details, list_tool_names, list_servers)
365365
)
366366
toolsets = [toolset]
@@ -369,7 +369,7 @@ def create_agent(model: str, codemode: bool) -> tuple[Agent, object | None, obje
369369
if HAS_AGENT_SKILLS:
370370
skills_toolset = AgentSkillsToolset(
371371
directories=[str((repo_root / "skills").resolve())],
372-
executor=SandboxExecutor(shared_sandbox),
372+
executor=SandboxExecutor(shared_client),
373373
)
374374
toolsets.append(skills_toolset)
375375
logger.info(

pyproject.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,21 @@ classifiers = [
2121
"Programming Language :: Python :: 3",
2222
]
2323
dependencies = [
24-
"agent_skills",
25-
"code_sandboxes",
26-
"mcp[cli]>=1.0",
24+
"agent-skills",
25+
"code-sandboxes",
26+
"mcp[cli]>=1.10.1,<2",
2727
"pydantic>=2.0",
2828
"httpx>=0.24",
2929
]
3030

3131
[project.optional-dependencies]
3232
pydantic-ai = [
33-
"pydantic-graph>=1.94.0",
34-
"pydantic-ai-slim>=1.94.0",
33+
"pydantic-ai-slim>=2.21.0,<3",
34+
"pydantic-graph>=2.21.0,<3",
3535
]
3636
test = [
3737
"ipykernel",
38-
"jupyter_server>=1.6,<3",
38+
"jupyter-server>=2.10,<3",
3939
"pytest>=7.0",
4040
"pytest-asyncio>=0.21",
4141
"pytest-cov>=4.1",

tests/conftest.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,28 @@
99
import pytest
1010

1111

12+
def _rebuild_fastmcp_settings() -> None:
13+
"""Resolve the forward reference in FastMCP's ``Settings`` model.
14+
15+
``mcp.server.fastmcp.server.Settings.lifespan`` is annotated with
16+
``FastMCP``, which is defined further down the same module, and upstream
17+
never calls ``model_rebuild()``. Recent pydantic-settings releases warn
18+
(``IncompleteFieldDefinitionWarning``) when such a model is instantiated,
19+
and this suite turns warnings into errors, so collection fails as soon as
20+
anything constructs a FastMCP server. Rebuilding the model once resolves
21+
the reference for real instead of muting the warning.
22+
"""
23+
try:
24+
from mcp.server.fastmcp.server import Settings
25+
except ImportError: # pragma: no cover - mcp layout changed
26+
return
27+
if not getattr(Settings, "__pydantic_complete__", True):
28+
Settings.model_rebuild()
29+
30+
31+
_rebuild_fastmcp_settings()
32+
33+
1234
@pytest.fixture
1335
def skills_dir(tmp_path: Path) -> Path:
1436
"""Create a temporary skills directory."""

tests/test_executor_async.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33

44
import asyncio
55

6-
from code_sandboxes import Sandbox
6+
from code_sandboxes import CodeSandboxClient
77

88

99
async def main():
1010
# Create a sandbox
11-
sandbox = Sandbox.create(variant="eval")
12-
sandbox.start()
11+
client = CodeSandboxClient.create(variant="eval")
12+
client.start()
1313

1414
# Set up an executor mock
1515
class MockExecutor:
@@ -18,11 +18,11 @@ async def call_tool(self, name, args):
1818
await asyncio.sleep(0.01)
1919
return {"status": "success", "data": f"Result for {name}"}
2020

21-
sandbox.set_variable("__executor__", MockExecutor())
21+
client.set_variable("__executor__", MockExecutor())
2222

2323
# First: Define __call_tool__
2424
print("\n=== Step 1: Define __call_tool__ ===")
25-
result1 = sandbox.run_code("""
25+
result1 = client.execute_code("""
2626
async def __call_tool__(tool_name, arguments):
2727
'''Call a tool through the executor.'''
2828
return await __executor__.call_tool(tool_name, arguments)
@@ -34,7 +34,7 @@ async def __call_tool__(tool_name, arguments):
3434

3535
# Second: Use __call_tool__
3636
print("\n=== Step 2: Use __call_tool__ with await ===")
37-
result2 = sandbox.run_code("""
37+
result2 = client.execute_code("""
3838
result = await __call_tool__("test_tool", {"arg": "value"})
3939
print(f"Tool result: {result}")
4040
result
@@ -43,7 +43,7 @@ async def __call_tool__(tool_name, arguments):
4343
print(f"Result 2 - Stdout: {result2.stdout}")
4444
print(f"Result 2 - Results: {result2.results}")
4545

46-
sandbox.stop()
46+
client.stop()
4747
print("\n=== Test completed successfully! ===")
4848

4949

tests/test_executor_streaming.py

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,65 +10,74 @@
1010
from code_sandboxes import ExecutionResult, Logs, OutputMessage
1111
from code_sandboxes.models import Result
1212

13+
from agent_codemode.composition import executor as executor_module
1314
from agent_codemode.composition.executor import CodeModeExecutor
1415
from agent_codemode.discovery.registry import ToolRegistry
1516

1617

17-
class _StreamingSandbox:
18+
class _StreamingClient:
19+
variant = "jupyter"
20+
1821
def __init__(self) -> None:
1922
self.run_code_calls = 0
2023
self.streaming_called = False
2124

22-
def run_code(self, code: str, **kwargs) -> ExecutionResult:
25+
def execute_code(self, code: str, **kwargs) -> ExecutionResult:
2326
_ = (code, kwargs.get("timeout"), kwargs.get("language"), kwargs.get("envs"))
2427
self.run_code_calls += 1
2528
return ExecutionResult(logs=Logs())
2629

27-
def run_code_streaming(self, code: str, **kwargs):
30+
def execute_code_streaming(self, code: str, **kwargs):
2831
_ = (code, kwargs.get("timeout"), kwargs.get("language"), kwargs.get("envs"))
2932
self.streaming_called = True
3033
yield OutputMessage(line="status: RUNNING", timestamp=0.0, error=False)
3134
yield OutputMessage(line="hello", timestamp=0.0, error=False)
3235
yield Result(data={"text/plain": "42"}, is_main_result=True, extra={})
3336

3437

35-
class _NonStreamingSandbox:
38+
class _FailingStreamingClient:
39+
variant = "jupyter"
40+
3641
def __init__(self) -> None:
3742
self.run_code_calls = 0
3843

39-
def run_code(self, code: str, **kwargs) -> ExecutionResult:
44+
def execute_code(self, code: str, **kwargs) -> ExecutionResult:
4045
_ = (code, kwargs.get("timeout"), kwargs.get("language"), kwargs.get("envs"))
4146
self.run_code_calls += 1
42-
if self.run_code_calls >= 3:
43-
return ExecutionResult(
44-
logs=Logs(stdout=[OutputMessage(line="fallback", timestamp=0.0, error=False)]),
45-
)
4647
return ExecutionResult(logs=Logs())
4748

49+
def execute_code_streaming(self, code: str, **kwargs):
50+
_ = (code, kwargs)
51+
raise RuntimeError("sandbox unavailable")
52+
yield
53+
4854

4955
@pytest.mark.asyncio
50-
async def test_execute_uses_streaming_when_supported():
56+
async def test_execute_uses_streaming_when_supported(monkeypatch):
57+
monkeypatch.setattr(executor_module, "_get_identity_env", lambda: {})
5158
executor = CodeModeExecutor(registry=ToolRegistry())
52-
sandbox = _StreamingSandbox()
53-
executor._sandbox = sandbox
59+
client = _StreamingClient()
60+
executor._sandbox_client = client
5461
executor._setup_done = True
5562

5663
result = await executor.execute("print('hi')")
5764

58-
assert sandbox.streaming_called is True
65+
assert client.streaming_called is True
5966
assert "status: RUNNING" in result.logs.stdout_text
6067
assert "hello" in result.logs.stdout_text
6168
assert result.results and result.results[0].data["text/plain"] == "42"
6269

6370

6471
@pytest.mark.asyncio
65-
async def test_execute_falls_back_to_run_code_without_streaming():
72+
async def test_execute_reports_streaming_infrastructure_failure(monkeypatch):
73+
monkeypatch.setattr(executor_module, "_get_identity_env", lambda: {})
6674
executor = CodeModeExecutor(registry=ToolRegistry())
67-
sandbox = _NonStreamingSandbox()
68-
executor._sandbox = sandbox
75+
client = _FailingStreamingClient()
76+
executor._sandbox_client = client
6977
executor._setup_done = True
7078

7179
result = await executor.execute("print('hi')")
7280

73-
assert sandbox.run_code_calls >= 3
74-
assert result.logs.stdout_text == "fallback"
81+
assert client.run_code_calls >= 2
82+
assert result.execution_ok is False
83+
assert result.execution_error == "sandbox unavailable"

0 commit comments

Comments
 (0)