Files
openai--openai-agents-python/examples/sandbox/extensions/runloop/runner.py
T
2026-07-13 12:39:17 +08:00

171 lines
5.6 KiB
Python

"""
Minimal Runloop-backed sandbox example for manual validation.
This mirrors the other cloud extension examples: it creates a tiny workspace, asks a sandboxed
agent to inspect it through one shell tool, and prints a short answer.
"""
import argparse
import asyncio
import os
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
if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[4]))
from examples.sandbox.misc.example_support import text_manifest
from examples.sandbox.misc.workspace_shell import WorkspaceShellCapability
try:
from agents.extensions.sandbox import (
DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT,
DEFAULT_RUNLOOP_WORKSPACE_ROOT,
RunloopSandboxClient,
RunloopSandboxClientOptions,
RunloopUserParameters,
)
except Exception as exc: # pragma: no cover - import path depends on optional extras
raise SystemExit(
"Runloop sandbox examples require the optional repo extra.\n"
"Install it with: uv sync --extra runloop"
) from exc
DEFAULT_QUESTION = "Summarize this cloud sandbox workspace in 2 sentences."
def _build_manifest(*, workspace_root: str) -> Manifest:
manifest = text_manifest(
{
"README.md": (
"# Runloop Demo Workspace\n\n"
"This workspace exists to validate the Runloop sandbox backend manually.\n"
),
"launch.md": (
"# Launch\n\n"
"- Customer: Contoso Logistics.\n"
"- Goal: validate the remote sandbox agent path.\n"
"- Current status: Runloop backend smoke and app-server connectivity are passing.\n"
),
"tasks.md": (
"# Tasks\n\n"
"1. Inspect the workspace files.\n"
"2. Summarize the setup and any notable status in two sentences.\n"
),
}
)
return Manifest(root=workspace_root, entries=manifest.entries)
def _require_env(name: str) -> None:
if os.environ.get(name):
return
raise SystemExit(f"{name} must be set before running this example.")
async def main(
*,
model: str,
question: str,
pause_on_exit: bool,
blueprint_name: str | None,
root: bool,
stream: bool,
) -> None:
_require_env("OPENAI_API_KEY")
_require_env("RUNLOOP_API_KEY")
workspace_root = DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT if root else DEFAULT_RUNLOOP_WORKSPACE_ROOT
manifest = _build_manifest(workspace_root=workspace_root)
agent = SandboxAgent(
name="Runloop Sandbox Assistant",
model=model,
instructions=(
"Answer questions about the sandbox workspace. Inspect the files before answering "
"and keep the response concise. "
"Do not invent files or statuses that are not present in the workspace. Cite the "
"file names you inspected."
),
default_manifest=manifest,
capabilities=[WorkspaceShellCapability()],
model_settings=ModelSettings(tool_choice="required"),
)
client = RunloopSandboxClient()
run_config = RunConfig(
sandbox=SandboxRunConfig(
client=client,
options=RunloopSandboxClientOptions(
blueprint_name=blueprint_name,
pause_on_exit=pause_on_exit,
user_parameters=(RunloopUserParameters(username="root", uid=0) if root else None),
),
),
workflow_name="Runloop sandbox example",
)
try:
if not stream:
result = await Runner.run(agent, question, run_config=run_config)
print(result.final_output)
return
stream_result = Runner.run_streamed(agent, question, run_config=run_config)
saw_text_delta = False
async for event in stream_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)
if saw_text_delta:
print()
finally:
await client.close()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--model", default="gpt-5.6-sol", help="Model name to use.")
parser.add_argument("--question", default=DEFAULT_QUESTION, help="Prompt to send to the agent.")
parser.add_argument(
"--pause-on-exit",
action="store_true",
default=False,
help="Suspend the Runloop devbox on shutdown instead of deleting it.",
)
parser.add_argument(
"--blueprint-name",
default=None,
help="Optional Runloop blueprint name to use when creating the devbox.",
)
parser.add_argument(
"--root",
action="store_true",
default=False,
help="Launch the Runloop devbox as root. The default home/workspace root becomes /root.",
)
parser.add_argument("--stream", action="store_true", default=False, help="Stream the response.")
args = parser.parse_args()
asyncio.run(
main(
model=args.model,
question=args.question,
pause_on_exit=args.pause_on_exit,
blueprint_name=args.blueprint_name,
root=args.root,
stream=args.stream,
)
)