chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
"""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))
|
||||
Reference in New Issue
Block a user