"""Runnable sandbox coding example used by docs/sandbox_agents.md. This example gives the model a tiny repo plus one lazy-loaded skill, then verifies that the agent edited the repo and ran the targeted test command. """ from __future__ import annotations import argparse import asyncio import json import sys from collections.abc import Sequence from pathlib import Path from agents import ModelSettings, Runner from agents.items import ToolCallItem, ToolCallOutputItem from agents.run import RunConfig from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig from agents.sandbox.capabilities import LocalDirLazySkillSource, Skills from agents.sandbox.capabilities.capabilities import Capabilities from agents.sandbox.entries import LocalDir from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient DEFAULT_MODEL = "gpt-5.6-sol" TARGET_TEST_CMD = "sh tests/test_credit_note.sh" DEFAULT_PROMPT = ( "Open `repo/task.md`, use the `$credit-note-fixer` skill, fix the bug, run " f"`{TARGET_TEST_CMD}`, and summarize the change." ) EXAMPLE_DIR = Path(__file__).resolve().parent if __package__ is None or __package__ == "": sys.path.insert(0, str(Path(__file__).resolve().parents[3])) def build_agent(model: str) -> SandboxAgent[None]: return SandboxAgent( name="Sandbox engineer", model=model, instructions=( "Inspect the repo, make the smallest correct change, run the most relevant checks, " "and summarize the file changes and risks. " "Read `repo/task.md` before editing files. Stay grounded in the repository, preserve " "existing behavior, and use the `$credit-note-fixer` skill before editing files. " "When using `apply_patch`, remember that paths are relative to the sandbox workspace " "root, not the shell working directory, so edit files as `repo/credit_note.sh` and " "`repo/tests/test_credit_note.sh`. " f"Run the exact verification command `{TARGET_TEST_CMD}` from `repo/`, then mention " "that command in the final answer." ), default_manifest=Manifest( entries={ "repo": LocalDir(src=EXAMPLE_DIR / "repo"), } ), capabilities=Capabilities.default() + [ Skills( lazy_from=LocalDirLazySkillSource( # This is a host path read by the SDK process. # Requested skills are copied into `skills_path` in the sandbox. source=LocalDir(src=EXAMPLE_DIR / "skills"), ) ), ], model_settings=ModelSettings(tool_choice="required"), ) async def _read_workspace_text(session, path: Path) -> str: handle = await session.read(path) try: payload = handle.read() finally: handle.close() if isinstance(payload, str): return payload return bytes(payload).decode("utf-8", errors="replace") def _tool_call_name(item: ToolCallItem) -> str: raw_item = item.raw_item if isinstance(raw_item, dict): raw_type = raw_item.get("type") name = raw_item.get("name") else: raw_type = getattr(raw_item, "type", None) name = getattr(raw_item, "name", None) if raw_type == "apply_patch_call": return "apply_patch" if isinstance(name, str) and name: return name if isinstance(raw_type, str) and raw_type: return raw_type return "" def _tool_call_arguments(item: ToolCallItem) -> dict[str, object]: raw_item = item.raw_item if isinstance(raw_item, dict): arguments = raw_item.get("arguments") else: arguments = getattr(raw_item, "arguments", None) if not isinstance(arguments, str) or arguments == "": return {} try: parsed = json.loads(arguments) except json.JSONDecodeError: return {"_raw": arguments} if isinstance(parsed, dict): return parsed return {"_value": parsed} def _saw_target_test_command(tool_calls: list[ToolCallItem]) -> bool: for item in tool_calls: if _tool_call_name(item) != "exec_command": continue arguments = _tool_call_arguments(item) cmd = arguments.get("cmd") workdir = arguments.get("workdir") if cmd == TARGET_TEST_CMD and workdir == "repo": return True if isinstance(cmd, str) and TARGET_TEST_CMD in cmd: return True if isinstance(cmd, str) and workdir == "repo" and TARGET_TEST_CMD in cmd: return True return False def _tool_call_debug_lines(tool_calls: list[ToolCallItem]) -> list[str]: lines: list[str] = [] for item in tool_calls: lines.append( f"{_tool_call_name(item)}: {json.dumps(_tool_call_arguments(item), sort_keys=True)}" ) return lines def _tool_output_debug_lines(new_items: Sequence[object]) -> list[str]: lines: list[str] = [] for item in new_items: if not isinstance(item, ToolCallOutputItem): continue output = item.output if isinstance(output, str): rendered = output else: rendered = str(output) lines.append(rendered[:400] if len(rendered) > 400 else rendered) return lines def _saw_target_test_success(new_items: Sequence[object]) -> bool: awaiting_target_output = False for item in new_items: if isinstance(item, ToolCallItem): if _tool_call_name(item) != "exec_command": awaiting_target_output = False continue arguments = _tool_call_arguments(item) cmd = arguments.get("cmd") if isinstance(cmd, str) and TARGET_TEST_CMD in cmd: awaiting_target_output = True continue awaiting_target_output = False continue if awaiting_target_output and isinstance(item, ToolCallOutputItem): output = item.output if isinstance(output, str) and "2 passed" in output: return True awaiting_target_output = False return False async def main(model: str, prompt: str) -> None: agent = build_agent(model) client = UnixLocalSandboxClient() sandbox = await client.create(manifest=agent.default_manifest) try: async with sandbox: result = await Runner.run( agent, prompt, max_turns=12, run_config=RunConfig( sandbox=SandboxRunConfig(session=sandbox), tracing_disabled=True, workflow_name="Sandbox docs coding example", ), ) tool_calls = [item for item in result.new_items if isinstance(item, ToolCallItem)] tool_names = [_tool_call_name(item) for item in tool_calls] if "load_skill" not in tool_names: raise RuntimeError(f"Expected load_skill call, saw: {tool_names}") if "apply_patch" not in tool_names: raise RuntimeError(f"Expected apply_patch call, saw: {tool_names}") if not _saw_target_test_command(tool_calls): raise RuntimeError( "Expected the agent to run the targeted test command.\n" + "\n".join(_tool_call_debug_lines(tool_calls)) ) if not _saw_target_test_success(result.new_items): raise RuntimeError( "Expected the targeted test command to report `2 passed`.\n" "Tool calls:\n" + "\n".join(_tool_call_debug_lines(tool_calls)) + "\nTool outputs:\n" + "\n".join(_tool_output_debug_lines(result.new_items)) ) verification = await sandbox.exec( f"cd repo && {TARGET_TEST_CMD}", shell=True, ) verification_text = verification.stdout.decode( "utf-8", errors="replace" ) + verification.stderr.decode("utf-8", errors="replace") if verification.exit_code != 0 or "2 passed" not in verification_text: raise RuntimeError(f"Post-run verification failed:\n{verification_text}") updated_module = await _read_workspace_text(sandbox, Path("repo/credit_note.sh")) print("=== Final summary ===") print("final_output:", result.final_output) print("tool_calls:", ", ".join(tool_names)) print("verification_command:", TARGET_TEST_CMD) print("verification_result: observed target test output with `2 passed`") print("updated_credit_note.sh:") print(updated_module, end="" if updated_module.endswith("\n") else "\n") finally: await client.delete(sandbox) if __name__ == "__main__": parser = argparse.ArgumentParser( description="Run a self-validating sandbox coding example used by the docs." ) parser.add_argument("--model", default=DEFAULT_MODEL, help="Model name to use.") parser.add_argument("--prompt", default=DEFAULT_PROMPT, help="Prompt to send to the agent.") args = parser.parse_args() asyncio.run(main(args.model, args.prompt))