diff --git a/agent_codemode/__version__.py b/agent_codemode/__version__.py index fa8c4c3..862ab2a 100644 --- a/agent_codemode/__version__.py +++ b/agent_codemode/__version__.py @@ -4,4 +4,4 @@ """Agent Codemode.""" -__version__ = "0.1.6" +__version__ = "1.0.0" diff --git a/agent_codemode/composition/executor.py b/agent_codemode/composition/executor.py index f74aebf..959474e 100644 --- a/agent_codemode/composition/executor.py +++ b/agent_codemode/composition/executor.py @@ -24,6 +24,7 @@ """ import logging +import sys import time from pathlib import Path from types import TracebackType @@ -32,7 +33,6 @@ from code_sandboxes import ( # type: ignore[import-untyped] CodeSandboxClient, ExecutionResult, - Sandbox, SandboxConfig, ) @@ -45,7 +45,13 @@ def _outcome_to_execution_result(outcome: Any) -> ExecutionResult: """Convert CodeSandboxClient outcome objects to ExecutionResult.""" - from code_sandboxes.models import CodeError, ExecutionResult, Logs, OutputMessage, Result + from code_sandboxes.models import ( # type: ignore[import-untyped] + CodeError, + ExecutionResult, + Logs, + OutputMessage, + Result, + ) stdout_lines = [line for line in str(getattr(outcome, "stdout", "") or "").splitlines() if line] stderr_lines = [line for line in str(getattr(outcome, "stderr", "") or "").splitlines() if line] @@ -68,12 +74,10 @@ def _outcome_to_execution_result(outcome: Any) -> ExecutionResult: return ExecutionResult( logs=Logs( stdout=[ - OutputMessage(line=line, timestamp=timestamp, error=False) - for line in stdout_lines + OutputMessage(line=line, timestamp=timestamp, error=False) for line in stdout_lines ], stderr=[ - OutputMessage(line=line, timestamp=timestamp, error=True) - for line in stderr_lines + OutputMessage(line=line, timestamp=timestamp, error=True) for line in stderr_lines ], ), results=results, @@ -88,21 +92,24 @@ def _outcome_to_execution_result(outcome: Any) -> ExecutionResult: def _get_identity_env() -> dict[str, str]: """Get identity environment variables from request context. - This function attempts to import the identity context from agent_runtimes. - If not available (standalone codemode usage), returns empty dict. + agent_runtimes is deliberately not imported here: it depends on + agent_codemode, not the other way round, so importing it would invert the + dependency. The module is only looked up in ``sys.modules``. That loses + nothing, because the identities live in a module-level ContextVar that + only agent_runtimes itself can populate — if the module was never + imported, no identity can have been set. Returns: - Dictionary of environment variable names to token values. + Dictionary of environment variable names to token values, empty when + agent_runtimes is not in play (standalone codemode usage). """ + identities = sys.modules.get("agent_runtimes.context.identities") + if identities is None: + return {} try: - import importlib - - get_identity_env = importlib.import_module( - "agent_runtimes.context.identities" - ).get_identity_env - - return get_identity_env() + return identities.get_identity_env() except Exception: + logger.debug("Could not read identity env context", exc_info=True) return {} @@ -141,19 +148,19 @@ def __init__( self, registry: ToolRegistry, config: Optional[CodeModeConfig] = None, - sandbox: Optional[Sandbox] = None, + sandbox_client: Optional[CodeSandboxClient] = None, ): """Initialize the executor. Args: registry: Tool registry with discovered tools. config: Executor configuration. - sandbox: Optional pre-configured sandbox. If not provided, + sandbox_client: Optional pre-configured client. If not provided, creates one based on config. """ self.registry = registry self.config = config or CodeModeConfig() - self._sandbox = sandbox + self._sandbox_client = sandbox_client self._codegen = PythonCodeGenerator(self.config.generated_path) self._setup_done = False self._tool_call_history: list[ToolCallResult] = [] @@ -191,17 +198,28 @@ def _is_local_eval_sandbox(self) -> bool: This checks the actual sandbox instance, not the config, to handle cases where an external sandbox is passed that differs from config. """ - return self._sandbox is not None and hasattr(self._sandbox, "_namespaces") - - def _require_sandbox(self) -> Sandbox: - if self._sandbox is None: - raise RuntimeError("Sandbox is not initialized") - return self._sandbox + client = self._sandbox_client + if client is None: + return False + # Callers may inject any object implementing execute_code (tests use + # lightweight fakes), so probe rather than require these attributes. + # ``CodeSandboxClient.variant`` reads the sandbox config, which older + # code_sandboxes releases never populate; the started sandbox's info + # always carries the variant, so fall back to it. + variant = getattr(client, "variant", None) + if variant is None: + variant = getattr(getattr(client, "info", None), "variant", None) + return getattr(variant, "value", variant) == "eval" + + def _require_sandbox_client(self) -> CodeSandboxClient: + if self._sandbox_client is None: + raise RuntimeError("Code sandbox client is not initialized") + return self._sandbox_client @property - def sandbox(self) -> Optional[Sandbox]: - """Get the sandbox instance.""" - return self._sandbox + def sandbox_client(self) -> Optional[CodeSandboxClient]: + """Get the variant-neutral code sandbox client.""" + return self._sandbox_client async def setup(self) -> None: """Set up the executor. @@ -209,11 +227,9 @@ async def setup(self) -> None: This generates code bindings for all registered tools and prepares the sandbox environment. """ - import sys as _sys - - print( - f"[EXECUTOR.setup] Starting setup, sandbox_variant={self.config.sandbox_variant}", - file=_sys.stderr, + logger.debug( + "Starting setup, sandbox_variant=%s", + self.config.sandbox_variant, ) # Generate code bindings on the host filesystem. Skip when running in @@ -223,7 +239,7 @@ async def setup(self) -> None: self._codegen.generate_from_tools(tools_dict) # Create sandbox if not provided - if self._sandbox is None: + if self._sandbox_client is None: import os # Pass the complete environment to the sandbox @@ -238,14 +254,14 @@ async def setup(self) -> None: sandbox_kwargs: dict[str, Any] = {} if self.config.sandbox_image: sandbox_kwargs["image"] = self.config.sandbox_image - self._sandbox = Sandbox.create( + self._sandbox_client = CodeSandboxClient.create( variant=self.config.sandbox_variant, config=sandbox_config, **sandbox_kwargs, ) # Start the sandbox - self._sandbox.start() + self._sandbox_client.start() # Set up the generated module path in the sandbox await self._setup_sandbox_environment() @@ -254,7 +270,7 @@ async def setup(self) -> None: async def _setup_sandbox_environment(self) -> None: """Set up the sandbox environment for tool execution.""" - if self._sandbox is None: + if self._sandbox_client is None: return generated_path = Path(self.config.generated_path).resolve() @@ -297,10 +313,10 @@ def find_spec(self, name, path=None, target=None): ): sys.meta_path.insert(0, _BlockGeneratedFinder()) """ - self._sandbox.run_code(purge_code) + self._sandbox_client.execute_code(purge_code) # Register the tool caller so ``call_tool`` still works inside # ``execute_code`` if the agent invokes it directly (raw MCP). - self._sandbox.register_tool_caller(self.call_tool) + self._sandbox_client.register_tool_caller(self.call_tool) return # For Jupyter/remote sandboxes, generate tools directly in the sandbox @@ -351,17 +367,12 @@ def find_spec(self, name, path=None, target=None): if skills_path not in sys.path: sys.path.insert(0, str(skills_path)) """ - self._sandbox.run_code(setup_code) + self._sandbox_client.execute_code(setup_code) # Register tool caller with the sandbox - import sys as _sys - - print( - f"[SETUP ENV DEBUG] About to call register_tool_caller, sandbox={self._sandbox} id={id(self._sandbox)}", - file=_sys.stderr, - ) - self._sandbox.register_tool_caller(self.call_tool) - print("[SETUP ENV DEBUG] register_tool_caller called", file=_sys.stderr) + logger.debug("About to register the code sandbox tool caller") + self._sandbox_client.register_tool_caller(self.call_tool) + logger.debug("register_tool_caller called") # Verify __call_tool__ was set verify_code = """ @@ -371,13 +382,14 @@ def find_spec(self, name, path=None, target=None): except NameError: print("[VERIFY] __call_tool__ NOT SET after register_tool_caller!", file=sys.stderr) """ - self._sandbox.run_code(verify_code) + self._sandbox_client.execute_code(verify_code) # For Jupyter/remote sandboxes, set up in-sandbox registry for tool calling # Use actual sandbox type detection, not config - print( - f"[SETUP ENV] is_local_eval={is_local_eval}, config.mcp_proxy_url={self.config.mcp_proxy_url}", - file=_sys.stderr, + logger.debug( + "is_local_eval=%s, config.mcp_proxy_url=%s", + is_local_eval, + self.config.mcp_proxy_url, ) if not is_local_eval: # ======================================================================= @@ -512,7 +524,7 @@ async def __call_tool__(tool_name: str, arguments: dict) -> dict: print(f"[SETUP] HTTP proxy tool caller configured for {{__MCP_PROXY_URL__}}", file=sys.stderr) ''' - self._sandbox.run_code(in_sandbox_http_caller_setup) + self._sandbox_client.execute_code(in_sandbox_http_caller_setup) else: # Direct MCP Client Mode (legacy) - requires agent-codemode in sandbox # This mode is used when MCP servers can be accessed directly from the sandbox @@ -579,7 +591,7 @@ async def call_tool(self, tool_name, arguments): async def __call_tool__(tool_name, arguments): return await __sandbox_registry__.call_tool(tool_name, arguments) ''' - self._sandbox.run_code(in_sandbox_registry_setup) + self._sandbox_client.execute_code(in_sandbox_registry_setup) # Set up the generated client to use __call_tool__ caller_setup_code = """ @@ -590,7 +602,7 @@ async def __call_tool__(tool_name, arguments): import sys print(f"[SETUP] caller_setup_code error: {type(e).__name__}: {e}", file=sys.stderr) """ - self._sandbox.run_code(caller_setup_code) + self._sandbox_client.execute_code(caller_setup_code) async def _generate_tools_in_sandbox(self) -> None: """Generate tool bindings directly in the remote sandbox. @@ -599,7 +611,7 @@ async def _generate_tools_in_sandbox(self) -> None: we send the code generation logic to be executed in the sandbox. This way the generated modules exist in the sandbox's filesystem. """ - if self._sandbox is None: + if self._sandbox_client is None: return # Get tool definitions for code generation @@ -810,7 +822,7 @@ async def {{func_name}}(arguments: Optional[{{input_type}}] = None, **kwargs: An print(f"Generated tool bindings for {{len(__tools_data__)}} tools in {{__generated_path__}}") ''' - self._sandbox.run_code(generation_code) + self._sandbox_client.execute_code(generation_code) # Generate skill bindings in the sandbox if skills metadata is available if self._skills_metadata: @@ -832,7 +844,7 @@ def generate_skills_in_sandbox(self) -> None: has already run — for example when ``wire_skills_into_codemode`` sets the skills metadata *after* initial sandbox setup. """ - if self._sandbox is None or not self._skills_metadata: + if self._sandbox_client is None or not self._skills_metadata: return # Skip for eval sandboxes (they use the on-disk generated files) @@ -1060,7 +1072,7 @@ async def run_skill( print(f"Skills source path: {{__skills_source_path__}}") print("Mode: direct execution (no MCP proxy)") ''' - self._sandbox.run_code(skills_generation_code) + self._sandbox_client.execute_code(skills_generation_code) async def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any: """Call a tool through the registry. @@ -1136,7 +1148,7 @@ async def execute( Raises: RuntimeError: If setup() hasn't been called. """ - if not self._setup_done or self._sandbox is None: + if not self._setup_done or self._sandbox_client is None: raise RuntimeError("Executor not set up. Call setup() first.") self._in_execute = True @@ -1200,147 +1212,19 @@ async def execute( except Exception: pass """ - # Branch based on actual sandbox type (already computed above) - if is_local_eval: - # For eval, we can access _namespaces directly - return await self._execute_local_eval(code, setup_code, timeout) - else: - # For Jupyter/remote sandboxes, use run_code() - return await self._execute_jupyter(code, setup_code, timeout) + return await self._execute_sandbox(code, setup_code, timeout) finally: self._in_execute = False - async def _execute_local_eval( + async def _execute_sandbox( self, code: str, setup_code: str, timeout: Optional[float] = None, ) -> ExecutionResult: - """Execute code in eval sandbox with direct namespace access.""" - import io - import time - from contextlib import redirect_stderr, redirect_stdout - - from code_sandboxes.models import ( # type: ignore[import-untyped] - ExecutionResult, - Logs, - OutputMessage, - ) - - sandbox = self._require_sandbox() - - # Get the namespace directly - namespace = sandbox._namespaces[sandbox._default_context.id] - - # Execute setup_code directly in namespace (avoids async wrapper issues) - exec(setup_code, namespace, namespace) - - # Configure the generated.client tool caller if available - if "__call_tool__" in namespace: - try: - import importlib - - set_tool_caller = importlib.import_module("generated.client").set_tool_caller - set_tool_caller(namespace["__call_tool__"]) - except ImportError: - pass - - # For async code, we need to handle it specially to avoid event loop conflicts - if "await " in code or "async " in code: - # Wrap user code in async function - def _indent_code(value: str, spaces: int) -> str: - indent = " " * spaces - return "\n".join(indent + line for line in value.split("\n")) - - async_wrapper = f""" -async def __user_code__(): -{_indent_code(code, 4)} - return locals() -""" - # Execute the wrapper in namespace - exec(async_wrapper, namespace, namespace) + """Execute through the variant-neutral code sandbox client. - # Capture stdout/stderr - stdout_buffer = io.StringIO() - stderr_buffer = io.StringIO() - - exit_code = None - - # Call the async function directly (we're already in async context) - with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer): - coro = namespace["__user_code__"]() - try: - locals_value = await coro - except SystemExit as exc: - if isinstance(exc.code, int): - exit_code = exc.code - elif exc.code: - exit_code = 1 - else: - exit_code = 0 - locals_value = {} - - # Update namespace with returned locals - if isinstance(locals_value, dict): - for key, value in locals_value.items(): - if key in ( - "__builtins__", - "__name__", - "__doc__", - "__package__", - "__loader__", - "__spec__", - "__annotations__", - "__cached__", - "__file__", - ): - continue - namespace[key] = value - - stdout_lines = stdout_buffer.getvalue().splitlines() - stderr_lines = stderr_buffer.getvalue().splitlines() - timestamp = time.time() - - result = ExecutionResult( - execution_ok=True, - code_error=None, - exit_code=exit_code, - results=[], - logs=Logs( - stdout=[ - OutputMessage(line=line, timestamp=timestamp, error=False) - for line in stdout_lines - ], - stderr=[ - OutputMessage(line=line, timestamp=timestamp, error=True) - for line in stderr_lines - ], - ), - execution_count=sandbox._execution_count[sandbox._default_context.id], - context_id=sandbox._default_context.id, - ) - else: - # For sync code, run in a worker thread so FastAPI's event loop - # stays responsive (e.g. sandbox status WS can emit is_executing). - import asyncio - - result = await asyncio.to_thread( - sandbox.run_code, - code, - timeout=timeout, - ) - - return result - - async def _execute_jupyter( - self, - code: str, - setup_code: str, - timeout: Optional[float] = None, - ) -> ExecutionResult: - """Execute code in Jupyter/remote sandbox using run_code(). - - IMPORTANT: The sandbox.run_code() is synchronous and blocks waiting + IMPORTANT: sandbox execution is synchronous and blocks waiting for the kernel to complete. When the kernel code calls back to the agent-runtimes server (e.g., via MCP proxy for tool calls), we need the event loop to be free to handle those requests. Therefore, we run @@ -1354,8 +1238,7 @@ async def _execute_jupyter( """ import asyncio - sandbox = self._require_sandbox() - sandbox_client = CodeSandboxClient(sandbox) + sandbox_client = self._require_sandbox_client() # Run setup code in thread pool to avoid blocking event loop await asyncio.to_thread( @@ -1382,7 +1265,7 @@ async def _execute_jupyter( timeout=timeout, ) - if hasattr(sandbox, "run_code_streaming"): + if hasattr(sandbox_client, "execute_code_streaming"): from code_sandboxes.models import ( CodeError, Logs, @@ -1395,6 +1278,7 @@ def _collect_streaming_result() -> ExecutionResult: stderr: list[OutputMessage] = [] results: list[Result] = [] code_error: CodeError | None = None + execution_error: str | None = None try: for event in sandbox_client.execute_code_streaming(code, timeout=timeout): @@ -1417,17 +1301,23 @@ def _collect_streaming_result() -> ExecutionResult: ) ) elif hasattr(event, "name") and hasattr(event, "value"): - code_error = CodeError( - name=str(getattr(event, "name", "Error") or "Error"), - value=str(getattr(event, "value", "") or ""), - traceback=str(getattr(event, "traceback", "") or ""), - ) + name = str(getattr(event, "name", "Error") or "Error") + value = str(getattr(event, "value", "") or "") + if name == "SandboxExecutionError": + execution_error = value + else: + code_error = CodeError( + name=name, + value=value, + traceback=str(getattr(event, "traceback", "") or ""), + ) return ExecutionResult( logs=Logs(stdout=stdout, stderr=stderr), results=results, code_error=code_error, - execution_ok=True, + execution_ok=execution_error is None, + execution_error=execution_error, ) except Exception as exc: return ExecutionResult( @@ -1479,9 +1369,9 @@ async def execute_skill( raise ValueError(f"Skill not found: {skill_name}") # Set arguments as variables if provided - if arguments and self._sandbox: + if arguments and self._sandbox_client: for name, value in arguments.items(): - self._sandbox.set_variable(name, value) + self._sandbox_client.set_variable(name, value) return await self.execute(skill.python_code or skill.content) @@ -1496,9 +1386,9 @@ def clear_history(self) -> None: async def cleanup(self) -> None: """Clean up resources.""" - if self._sandbox: - self._sandbox.stop() - self._sandbox = None + if self._sandbox_client: + self._sandbox_client.stop() + self._sandbox_client = None self._setup_done = False async def __aenter__(self) -> "CodeModeExecutor": diff --git a/agent_codemode/server.py b/agent_codemode/server.py index 156b194..f265fd5 100644 --- a/agent_codemode/server.py +++ b/agent_codemode/server.py @@ -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) diff --git a/agent_codemode/toolset.py b/agent_codemode/toolset.py index 9dd1690..b0b66b6 100644 --- a/agent_codemode/toolset.py +++ b/agent_codemode/toolset.py @@ -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 @@ -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( diff --git a/docs/docs/skills/index.mdx b/docs/docs/skills/index.mdx index 0920c32..a3afa68 100644 --- a/docs/docs/skills/index.mdx +++ b/docs/docs/skills/index.mdx @@ -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), diff --git a/examples/simple/agent_cli.py b/examples/simple/agent_cli.py index 686bfbe..1e21504 100644 --- a/examples/simple/agent_cli.py +++ b/examples/simple/agent_cli.py @@ -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: @@ -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] @@ -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( diff --git a/pyproject.toml b/pyproject.toml index 849f52a..31c6a2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/tests/conftest.py b/tests/conftest.py index a6ce79e..68d264e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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.""" diff --git a/tests/test_executor_async.py b/tests/test_executor_async.py index 5c6135b..a844e4f 100644 --- a/tests/test_executor_async.py +++ b/tests/test_executor_async.py @@ -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: @@ -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) @@ -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 @@ -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! ===") diff --git a/tests/test_executor_streaming.py b/tests/test_executor_streaming.py index f498949..08cea32 100644 --- a/tests/test_executor_streaming.py +++ b/tests/test_executor_streaming.py @@ -10,21 +10,24 @@ 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) @@ -32,43 +35,49 @@ def run_code_streaming(self, code: str, **kwargs): 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" diff --git a/tests/test_skill_bindings.py b/tests/test_skill_bindings.py index 425ae8b..4ff70ba 100644 --- a/tests/test_skill_bindings.py +++ b/tests/test_skill_bindings.py @@ -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, "", "exec")) from code_sandboxes.models import ExecutionResult @@ -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() @@ -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()