166 lines
5.6 KiB
Python
166 lines
5.6 KiB
Python
"""Show how a sandbox agent can keep using the same interactive Python process.
|
|
|
|
This example uses the Unix-local sandbox with the `Shell` capability. The task only asks
|
|
for a stateful interaction, but the streamed output shows the actual shell tools the agent
|
|
chooses, including the follow-up writes that keep the same process alive.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from openai.types.responses import ResponseTextDeltaEvent
|
|
|
|
from agents import ModelSettings, Runner
|
|
from agents.run import RunConfig
|
|
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig
|
|
from agents.sandbox.capabilities import Shell
|
|
from agents.sandbox.entries import File
|
|
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient
|
|
|
|
if __package__ is None or __package__ == "":
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
|
|
from examples.sandbox.misc.example_support import tool_call_name
|
|
|
|
DEFAULT_MODEL = "gpt-5.6-sol"
|
|
DEFAULT_QUESTION = (
|
|
"Start an interactive Python session. In that same session, compute `5 + 5`, then add "
|
|
"5 more to the previous result. Briefly report the outputs and confirm that you stayed "
|
|
"in one Python process."
|
|
)
|
|
|
|
|
|
def _build_manifest() -> Manifest:
|
|
return Manifest(
|
|
entries={
|
|
"README.md": File(
|
|
content=(
|
|
b"# Unix-local PTY Agent Example\n\n"
|
|
b"This workspace is used by examples/sandbox/unix_local_pty.py.\n"
|
|
)
|
|
),
|
|
}
|
|
)
|
|
|
|
|
|
def _build_agent(model: str) -> SandboxAgent:
|
|
return SandboxAgent(
|
|
name="Unix-local PTY Demo",
|
|
model=model,
|
|
instructions=(
|
|
"Complete the task by inspecting and interacting with the sandbox through the shell "
|
|
"capability. Keep the final answer concise. "
|
|
"Preserve process state when the task depends on it. If you start an interactive "
|
|
"program, continue using that same process instead of launching a second one."
|
|
),
|
|
default_manifest=_build_manifest(),
|
|
capabilities=[Shell()],
|
|
model_settings=ModelSettings(tool_choice="required"),
|
|
)
|
|
|
|
|
|
def _stream_event_banner(event_name: str, raw_item: object) -> str | None:
|
|
_ = raw_item
|
|
if event_name == "tool_called":
|
|
return "[tool call]"
|
|
if event_name == "tool_output":
|
|
return "[tool output]"
|
|
return None
|
|
|
|
|
|
def _raw_item_call_id(raw_item: object) -> str | None:
|
|
if isinstance(raw_item, dict):
|
|
call_id = raw_item.get("call_id") or raw_item.get("id")
|
|
else:
|
|
call_id = getattr(raw_item, "call_id", None) or getattr(raw_item, "id", None)
|
|
return call_id if isinstance(call_id, str) and call_id else None
|
|
|
|
|
|
async def main(model: str, question: str) -> None:
|
|
agent = _build_agent(model)
|
|
client = UnixLocalSandboxClient()
|
|
sandbox = await client.create(manifest=agent.default_manifest)
|
|
|
|
try:
|
|
async with sandbox:
|
|
result = Runner.run_streamed(
|
|
agent,
|
|
question,
|
|
run_config=RunConfig(
|
|
sandbox=SandboxRunConfig(session=sandbox),
|
|
tracing_disabled=True,
|
|
workflow_name="Unix-local PTY example",
|
|
),
|
|
)
|
|
|
|
saw_text_delta = False
|
|
saw_any_text = False
|
|
tool_names_by_call_id: dict[str, str] = {}
|
|
|
|
async for event in result.stream_events():
|
|
if event.type == "raw_response_event" and isinstance(
|
|
event.data, ResponseTextDeltaEvent
|
|
):
|
|
if not saw_text_delta:
|
|
print("assistant> ", end="", flush=True)
|
|
saw_text_delta = True
|
|
print(event.data.delta, end="", flush=True)
|
|
saw_any_text = True
|
|
continue
|
|
|
|
if event.type != "run_item_stream_event":
|
|
continue
|
|
|
|
raw_item = event.item.raw_item
|
|
banner = _stream_event_banner(event.name, raw_item)
|
|
if banner is None:
|
|
continue
|
|
|
|
if saw_text_delta:
|
|
print()
|
|
saw_text_delta = False
|
|
|
|
if event.name == "tool_called":
|
|
tool_name = tool_call_name(raw_item)
|
|
call_id = _raw_item_call_id(raw_item)
|
|
if call_id is not None and tool_name:
|
|
tool_names_by_call_id[call_id] = tool_name
|
|
if tool_name:
|
|
banner = f"{banner} {tool_name}"
|
|
elif event.name == "tool_output":
|
|
call_id = _raw_item_call_id(raw_item)
|
|
output_tool_name = tool_names_by_call_id.get(call_id or "")
|
|
if output_tool_name:
|
|
banner = f"{banner} {output_tool_name}"
|
|
|
|
print(banner)
|
|
|
|
if saw_text_delta:
|
|
print()
|
|
if not saw_any_text:
|
|
print(result.final_output)
|
|
finally:
|
|
await client.delete(sandbox)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(
|
|
description=(
|
|
"Run a Unix-local sandbox agent that demonstrates PTY interaction through the "
|
|
"shell capability."
|
|
)
|
|
)
|
|
parser.add_argument("--model", default=DEFAULT_MODEL, help="Model name to use.")
|
|
parser.add_argument(
|
|
"--question",
|
|
default=DEFAULT_QUESTION,
|
|
help="Prompt to send to the agent.",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
asyncio.run(main(args.model, args.question))
|