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
2 changes: 1 addition & 1 deletion agent_codemode/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

"""Agent Codemode."""

__version__ = "0.1.6"
__version__ = "1.0.0"
302 changes: 96 additions & 206 deletions agent_codemode/composition/executor.py

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions agent_codemode/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,9 +323,9 @@ async def handle_execute_code(arguments: dict[str, Any]) -> dict[str, Any]:
await executor.setup()

# Inject context variables if provided
if context and executor._sandbox:
if context and executor.sandbox_client:
for name, value in context.items():
executor._sandbox.set_variable(name, value)
executor.sandbox_client.set_variable(name, value)

try:
execution = await executor.execute(code, timeout=timeout)
Expand Down
4 changes: 2 additions & 2 deletions agent_codemode/toolset.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class CodemodeToolset(AbstractToolset):

registry: ToolRegistry | None = None
config: CodeModeConfig = field(default_factory=CodeModeConfig)
sandbox: Any | None = None # Optional pre-configured sandbox (e.g., EvalSandbox)
sandbox_client: Any | None = None
allow_direct_tool_calls: bool | None = None
allow_discovery_tools: bool = True
tool_reranker: Callable[[list, str, Optional[str]], Awaitable[list]] | None = None
Expand Down Expand Up @@ -190,7 +190,7 @@ async def _ensure_initialized(self) -> None:
self._executor = CodeModeExecutor(
registry=registry,
config=self.config,
sandbox=self.sandbox,
sandbox_client=self.sandbox_client,
)
await self._executor.setup()
logger.info(
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/skills/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -372,10 +372,10 @@ For Pydantic AI agents, use the `AgentSkillsToolset`:
```python
from pydantic_ai import Agent
from agent_skills import AgentSkillsToolset, SandboxExecutor
from code_sandboxes.eval_sandbox import EvalSandbox
from code_sandboxes import CodeSandboxClient

# Create toolset with sandbox execution
sandbox = EvalSandbox()
sandbox = CodeSandboxClient.create(variant="eval")
toolset = AgentSkillsToolset(
directories=["./skills"],
executor=SandboxExecutor(sandbox),
Expand Down
12 changes: 6 additions & 6 deletions examples/simple/agent_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

try:
from agent_skills import AgentSkillsToolset, SandboxExecutor
from code_sandboxes.eval_sandbox import EvalSandbox
from code_sandboxes import CodeSandboxClient

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

# Create shared sandbox for both CodemodeToolset and AgentSkillsToolset
shared_sandbox = None
shared_client = None
skills_toolset = None
if HAS_AGENT_SKILLS:
shared_sandbox = EvalSandbox()
logger.info("Created shared EvalSandbox for codemode and skills toolsets")
shared_client = CodeSandboxClient.create(variant="eval")
logger.info("Created shared CodeSandboxClient for codemode and skills")

toolset = CodemodeToolset(
registry=registry,
config=config,
sandbox=shared_sandbox,
sandbox_client=shared_client,
allow_discovery_tools=True, # Enable discovery tools (search_tools, get_tool_details, list_tool_names, list_servers)
)
toolsets = [toolset]
Expand All @@ -369,7 +369,7 @@ def create_agent(model: str, codemode: bool) -> tuple[Agent, object | None, obje
if HAS_AGENT_SKILLS:
skills_toolset = AgentSkillsToolset(
directories=[str((repo_root / "skills").resolve())],
executor=SandboxExecutor(shared_sandbox),
executor=SandboxExecutor(shared_client),
)
toolsets.append(skills_toolset)
logger.info(
Expand Down
12 changes: 6 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,21 @@ classifiers = [
"Programming Language :: Python :: 3",
]
dependencies = [
"agent_skills",
"code_sandboxes",
"mcp[cli]>=1.0",
"agent-skills",
"code-sandboxes",
"mcp[cli]>=1.10.1,<2",
"pydantic>=2.0",
"httpx>=0.24",
]

[project.optional-dependencies]
pydantic-ai = [
"pydantic-graph>=1.94.0",
"pydantic-ai-slim>=1.94.0",
"pydantic-ai-slim>=2.21.0,<3",
"pydantic-graph>=2.21.0,<3",
]
test = [
"ipykernel",
"jupyter_server>=1.6,<3",
"jupyter-server>=2.10,<3",
"pytest>=7.0",
"pytest-asyncio>=0.21",
"pytest-cov>=4.1",
Expand Down
22 changes: 22 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,28 @@
import pytest


def _rebuild_fastmcp_settings() -> None:
"""Resolve the forward reference in FastMCP's ``Settings`` model.

``mcp.server.fastmcp.server.Settings.lifespan`` is annotated with
``FastMCP``, which is defined further down the same module, and upstream
never calls ``model_rebuild()``. Recent pydantic-settings releases warn
(``IncompleteFieldDefinitionWarning``) when such a model is instantiated,
and this suite turns warnings into errors, so collection fails as soon as
anything constructs a FastMCP server. Rebuilding the model once resolves
the reference for real instead of muting the warning.
"""
try:
from mcp.server.fastmcp.server import Settings
except ImportError: # pragma: no cover - mcp layout changed
return
if not getattr(Settings, "__pydantic_complete__", True):
Settings.model_rebuild()


_rebuild_fastmcp_settings()


@pytest.fixture
def skills_dir(tmp_path: Path) -> Path:
"""Create a temporary skills directory."""
Expand Down
14 changes: 7 additions & 7 deletions tests/test_executor_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@

import asyncio

from code_sandboxes import Sandbox
from code_sandboxes import CodeSandboxClient


async def main():
# Create a sandbox
sandbox = Sandbox.create(variant="eval")
sandbox.start()
client = CodeSandboxClient.create(variant="eval")
client.start()

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

sandbox.set_variable("__executor__", MockExecutor())
client.set_variable("__executor__", MockExecutor())

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

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

sandbox.stop()
client.stop()
print("\n=== Test completed successfully! ===")


Expand Down
45 changes: 27 additions & 18 deletions tests/test_executor_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,65 +10,74 @@
from code_sandboxes import ExecutionResult, Logs, OutputMessage
from code_sandboxes.models import Result

from agent_codemode.composition import executor as executor_module
from agent_codemode.composition.executor import CodeModeExecutor
from agent_codemode.discovery.registry import ToolRegistry


class _StreamingSandbox:
class _StreamingClient:
variant = "jupyter"

def __init__(self) -> None:
self.run_code_calls = 0
self.streaming_called = False

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

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


class _NonStreamingSandbox:
class _FailingStreamingClient:
variant = "jupyter"

def __init__(self) -> None:
self.run_code_calls = 0

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

def execute_code_streaming(self, code: str, **kwargs):
_ = (code, kwargs)
raise RuntimeError("sandbox unavailable")
yield


@pytest.mark.asyncio
async def test_execute_uses_streaming_when_supported():
async def test_execute_uses_streaming_when_supported(monkeypatch):
monkeypatch.setattr(executor_module, "_get_identity_env", lambda: {})
executor = CodeModeExecutor(registry=ToolRegistry())
sandbox = _StreamingSandbox()
executor._sandbox = sandbox
client = _StreamingClient()
executor._sandbox_client = client
executor._setup_done = True

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

assert sandbox.streaming_called is True
assert client.streaming_called is True
assert "status: RUNNING" in result.logs.stdout_text
assert "hello" in result.logs.stdout_text
assert result.results and result.results[0].data["text/plain"] == "42"


@pytest.mark.asyncio
async def test_execute_falls_back_to_run_code_without_streaming():
async def test_execute_reports_streaming_infrastructure_failure(monkeypatch):
monkeypatch.setattr(executor_module, "_get_identity_env", lambda: {})
executor = CodeModeExecutor(registry=ToolRegistry())
sandbox = _NonStreamingSandbox()
executor._sandbox = sandbox
client = _FailingStreamingClient()
executor._sandbox_client = client
executor._setup_done = True

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

assert sandbox.run_code_calls >= 3
assert result.logs.stdout_text == "fallback"
assert client.run_code_calls >= 2
assert result.execution_ok is False
assert result.execution_error == "sandbox unavailable"
6 changes: 3 additions & 3 deletions tests/test_skill_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ def __init__(self):
self.code_calls: list[str] = []
self._started = True

def run_code(self, code: str, **kwargs):
def execute_code(self, code: str, **kwargs):
self.code_calls.append(code)
exec(compile(code, "<sandbox>", "exec"))
from code_sandboxes.models import ExecutionResult
Expand Down Expand Up @@ -310,7 +310,7 @@ def test_list_skills_embeds_catalog(self, tmp_path: Path):

# Inject a mock remote sandbox
sandbox = self.MockRemoteSandbox()
executor._sandbox = sandbox
executor._sandbox_client = sandbox

executor.set_skills_metadata(SAMPLE_SKILLS_METADATA)
executor.generate_skills_in_sandbox()
Expand Down Expand Up @@ -339,7 +339,7 @@ def test_other_bindings_use_direct_execution(self, tmp_path: Path):
executor = CodeModeExecutor(registry=registry, config=config)

sandbox = self.MockRemoteSandbox()
executor._sandbox = sandbox
executor._sandbox_client = sandbox

executor.set_skills_metadata(SAMPLE_SKILLS_METADATA)
executor.generate_skills_in_sandbox()
Expand Down