Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/ref/extensions/sandbox/superserve/sandbox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `Sandbox`

::: agents.extensions.sandbox.superserve.sandbox
3 changes: 3 additions & 0 deletions docs/sandbox/clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ For provider-specific setup notes and links for the checked-in extension example
| `E2BSandboxClient` | `openai-agents[e2b]` | [E2B runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/e2b_runner.py) |
| `ModalSandboxClient` | `openai-agents[modal]` | [Modal runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/modal_runner.py) |
| `RunloopSandboxClient` | `openai-agents[runloop]` | [Runloop runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/runloop/runner.py) |
| `SuperserveSandboxClient` | `openai-agents[superserve]` | [Superserve runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/superserve_runner.py) |
| `VercelSandboxClient` | `openai-agents[vercel]` | [Vercel runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/vercel_runner.py) |

</div>
Expand All @@ -113,6 +114,7 @@ Hosted sandbox clients expose provider-specific mount strategies. Choose the bac
| `DaytonaSandboxClient` | Supports rclone-backed cloud storage mounts with `DaytonaCloudBucketMountStrategy`; use it with `S3Mount`, `GCSMount`, `R2Mount`, `AzureBlobMount`, and `BoxMount`. |
| `E2BSandboxClient` | Supports rclone-backed cloud storage mounts with `E2BCloudBucketMountStrategy`; use it with `S3Mount`, `GCSMount`, `R2Mount`, `AzureBlobMount`, and `BoxMount`. |
| `RunloopSandboxClient` | Supports rclone-backed cloud storage mounts with `RunloopCloudBucketMountStrategy`; use it with `S3Mount`, `GCSMount`, `R2Mount`, `AzureBlobMount`, and `BoxMount`. |
| `SuperserveSandboxClient` | No hosted-specific mount strategy is currently exposed. Use manifest files, repos, or other workspace inputs instead. |
| `VercelSandboxClient` | No hosted-specific mount strategy is currently exposed. Use manifest files, repos, or other workspace inputs instead. |

</div>
Expand All @@ -130,6 +132,7 @@ The table below summarizes which remote storage entries each backend can mount d
| `DaytonaSandboxClient` | ✓ | ✓ | ✓ | ✓ | ✓ | - |
| `E2BSandboxClient` | ✓ | ✓ | ✓ | ✓ | ✓ | - |
| `RunloopSandboxClient` | ✓ | ✓ | ✓ | ✓ | ✓ | - |
| `SuperserveSandboxClient` | - | - | - | - | - | - |
| `VercelSandboxClient` | - | - | - | - | - | - |

</div>
Expand Down
38 changes: 37 additions & 1 deletion examples/sandbox/extensions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ They intentionally keep the flow simple:

1. Build a tiny manifest in memory.
2. Create a `SandboxAgent` that inspects that workspace through one shell tool.
3. Run the agent against E2B, Modal, Daytona, Cloudflare, Runloop, Blaxel, or Vercel.
3. Run the agent against E2B, Modal, Daytona, Cloudflare, Runloop, Blaxel, Superserve, or Vercel.

All of these examples require `OPENAI_API_KEY`, because they call the model through the normal
`Runner` path. Each cloud backend also needs its own provider credentials.
Expand Down Expand Up @@ -243,6 +243,42 @@ export DAYTONA_API_KEY=...
uv run python examples/sandbox/extensions/daytona/daytona_runner.py --stream
```

## Superserve

### Setup

Install the repo extra:

```bash
uv sync --extra superserve
```

Create a Superserve account at <https://console.superserve.ai>, generate an API key, and export
the required environment variables:

```bash
export OPENAI_API_KEY=...
export SUPERSERVE_API_KEY=...
```

### Run

```bash
uv run python examples/sandbox/extensions/superserve_runner.py --stream
```

Useful flags:

- `--template <name>` -- use a different template; defaults to `superserve/base`.
Other curated templates: `superserve/python-3.11`, `superserve/node-22`,
`superserve/code-interpreter`, `superserve/python-ml`.
- `--pause-on-exit` -- pause the sandbox on shutdown instead of killing it.
- `--timeout-seconds 300` -- inactivity timeout in seconds (off by default).
- `--skip-snapshot-check` -- skip the pause/resume snapshot round-trip verification.

The example runs a pause/resume round-trip before the agent run to verify that workspace state
survives shutdown.

## Runloop

### Setup
Expand Down
246 changes: 246 additions & 0 deletions examples/sandbox/extensions/superserve_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
"""
Minimal Superserve-backed sandbox example for manual validation.

This example mirrors the other cloud extension runners: it creates a tiny workspace, asks a
sandboxed agent to inspect it through one shell tool, prints a short answer, and verifies that
pause/resume preserves workspace state.
"""

from __future__ import annotations

import argparse
import asyncio
import io
import os
import sys
import tempfile
from pathlib import Path
from typing import cast

from openai.types.responses import ResponseTextDeltaEvent

from agents import ModelSettings, Runner
from agents.run import RunConfig
from agents.sandbox import LocalSnapshotSpec, Manifest, SandboxAgent, SandboxRunConfig
from agents.sandbox.session import BaseSandboxSession

if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[3]))

from examples.sandbox.misc.example_support import text_manifest
from examples.sandbox.misc.workspace_shell import WorkspaceShellCapability

try:
from agents.extensions.sandbox import (
DEFAULT_SUPERSERVE_WORKSPACE_ROOT,
SuperserveSandboxClient,
SuperserveSandboxClientOptions,
)
except Exception as exc: # pragma: no cover - import path depends on optional extras
raise SystemExit(
"Superserve sandbox examples require the optional repo extra.\n"
"Install it with: uv sync --extra superserve"
) from exc


DEFAULT_QUESTION = "Summarize this cloud sandbox workspace in 2 sentences."
DEFAULT_TEMPLATE = "superserve/base"
SNAPSHOT_CHECK_PATH = Path("snapshot-check.txt")
SNAPSHOT_CHECK_CONTENT = "superserve snapshot round-trip ok\n"


def _build_manifest() -> Manifest:
manifest = text_manifest(
{
"README.md": (
"# Superserve Demo Workspace\n\n"
"This workspace exists to validate the Superserve sandbox backend manually.\n"
),
"renewal.md": (
"# Renewal Notes\n\n"
"- Customer: Northwind Health.\n"
"- Renewal date: 2026-04-15.\n"
"- Risk: unresolved SSO setup.\n"
),
"next_steps.md": (
"# Next steps\n\n"
"1. Finish the SSO fix.\n"
"2. Confirm legal language before procurement review.\n"
),
}
)
return Manifest(root=DEFAULT_SUPERSERVE_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 _read_text(session: BaseSandboxSession, path: Path) -> str:
data = await session.read(path)
text = cast(str | bytes, data.read())
if isinstance(text, bytes):
return text.decode("utf-8")
return text


async def _verify_stop_resume(
*,
template: str,
pause_on_exit: bool,
timeout_seconds: int | None,
) -> None:
client = SuperserveSandboxClient()
manifest = _build_manifest()
with tempfile.TemporaryDirectory(prefix="superserve-snapshot-example-") as snapshot_dir:
sandbox = await client.create(
manifest=manifest,
snapshot=LocalSnapshotSpec(base_path=Path(snapshot_dir)),
options=SuperserveSandboxClientOptions(
template=template,
pause_on_exit=pause_on_exit,
timeout_seconds=timeout_seconds,
),
)

try:
await sandbox.start()
await sandbox.write(
SNAPSHOT_CHECK_PATH,
io.BytesIO(SNAPSHOT_CHECK_CONTENT.encode("utf-8")),
)
await sandbox.stop()
finally:
await sandbox.shutdown()

resumed = await client.resume(sandbox.state)
try:
await resumed.start()
restored = await _read_text(resumed, SNAPSHOT_CHECK_PATH)
if restored != SNAPSHOT_CHECK_CONTENT:
raise RuntimeError(
"Snapshot resume verification failed: "
f"expected {SNAPSHOT_CHECK_CONTENT!r}, got {restored!r}"
)
finally:
await resumed.aclose()

print("snapshot round-trip ok")


async def main(
*,
model: str,
question: str,
template: str,
pause_on_exit: bool,
timeout_seconds: int | None,
stream: bool,
skip_snapshot_check: bool,
) -> None:
_require_env("OPENAI_API_KEY")
_require_env("SUPERSERVE_API_KEY")

if not skip_snapshot_check:
await _verify_stop_resume(
template=template,
pause_on_exit=pause_on_exit,
timeout_seconds=timeout_seconds,
)

manifest = _build_manifest()
agent = SandboxAgent(
name="Superserve 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 = SuperserveSandboxClient()
run_config = RunConfig(
sandbox=SandboxRunConfig(
client=client,
options=SuperserveSandboxClientOptions(
template=template,
pause_on_exit=pause_on_exit,
timeout_seconds=timeout_seconds,
),
),
workflow_name="Superserve sandbox example",
)

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()


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--model", default="gpt-5.5", help="Model name to use.")
parser.add_argument("--question", default=DEFAULT_QUESTION, help="Prompt to send to the agent.")
parser.add_argument(
"--template",
default=DEFAULT_TEMPLATE,
help=(
"Superserve template to use. Defaults to `superserve/base`. "
"Other curated templates: superserve/python-3.11, superserve/node-22, "
"superserve/code-interpreter, superserve/python-ml."
),
)
parser.add_argument(
"--pause-on-exit",
action="store_true",
default=False,
help="Pause the Superserve sandbox on shutdown instead of killing it.",
)
parser.add_argument(
"--timeout-seconds",
type=int,
default=None,
help=(
"Optional inactivity timeout in seconds. Superserve sandboxes do not die on their own "
"by default; set this to opt into automatic shutdown."
),
)
parser.add_argument("--stream", action="store_true", default=False, help="Stream the response.")
parser.add_argument(
"--skip-snapshot-check",
action="store_true",
default=False,
help="Skip the pause/resume snapshot round-trip verification.",
)
args = parser.parse_args()

asyncio.run(
main(
model=args.model,
question=args.question,
template=args.template,
pause_on_exit=args.pause_on_exit,
timeout_seconds=args.timeout_seconds,
stream=args.stream,
skip_snapshot_check=args.skip_snapshot_check,
)
)
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ cloudflare = ["aiohttp>=3.12,<4"]
e2b = ["e2b==2.20.0", "e2b-code-interpreter==2.4.1"]
modal = ["modal==1.3.5"]
runloop = ["runloop_api_client>=1.16.0,<2.0.0"]
superserve = ["superserve>=0.7.0,<1"]
vercel = ["vercel>=0.5.6,<0.6"]
s3 = ["boto3>=1.34"]
temporal = [
Expand Down
26 changes: 26 additions & 0 deletions src/agents/extensions/sandbox/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,20 @@
except Exception: # pragma: no cover
_HAS_RUNLOOP = False

try:
from .superserve import (
DEFAULT_SUPERSERVE_WORKSPACE_ROOT as DEFAULT_SUPERSERVE_WORKSPACE_ROOT,
SuperserveSandboxClient as SuperserveSandboxClient,
SuperserveSandboxClientOptions as SuperserveSandboxClientOptions,
SuperserveSandboxSession as SuperserveSandboxSession,
SuperserveSandboxSessionState as SuperserveSandboxSessionState,
SuperserveSandboxTimeouts as SuperserveSandboxTimeouts,
)

_HAS_SUPERSERVE = True
except Exception: # pragma: no cover
_HAS_SUPERSERVE = False

try:
from .vercel import (
VercelSandboxClient as VercelSandboxClient,
Expand Down Expand Up @@ -177,6 +191,18 @@
]
)

if _HAS_SUPERSERVE:
__all__.extend(
[
"DEFAULT_SUPERSERVE_WORKSPACE_ROOT",
"SuperserveSandboxClient",
"SuperserveSandboxClientOptions",
"SuperserveSandboxSession",
"SuperserveSandboxSessionState",
"SuperserveSandboxTimeouts",
]
)

if _HAS_VERCEL:
__all__.extend(
[
Expand Down
Loading