From b78d7bbb6d644a362d7b74db20e1f44023fb3a12 Mon Sep 17 00:00:00 2001 From: Vijit Dhingra Date: Tue, 17 Mar 2026 13:45:08 -0700 Subject: [PATCH 1/8] add more examples --- examples/README.md | 5 +- examples/daytona-quickstart.py | 185 ++++++++++++++++++ examples/e2b-quickstart.py | 36 ++-- examples/modal-quickstart.py | 138 +++++++++++++ examples/vercel-quickstart.py | 142 ++++++++++++++ .../runtimeuse-client-python/pyproject.toml | 3 + packages/runtimeuse/src/session.ts | 2 - 7 files changed, 496 insertions(+), 15 deletions(-) create mode 100644 examples/daytona-quickstart.py create mode 100644 examples/modal-quickstart.py create mode 100644 examples/vercel-quickstart.py diff --git a/examples/README.md b/examples/README.md index 1dc4521..c8f4ced 100644 --- a/examples/README.md +++ b/examples/README.md @@ -9,5 +9,6 @@ Each example is a single, self-contained `.py` file. Setup instructions (depende | File | Provider | Description | |------|----------|-------------| | [e2b-quickstart.py](e2b-quickstart.py) | [E2B](https://e2b.dev) | Run Claude Code in an E2B cloud sandbox | - -More provider examples coming soon. +| [daytona-quickstart.py](daytona-quickstart.py) | [Daytona](https://daytona.io) | Run Claude Code in a Daytona cloud sandbox | +| [modal-quickstart.py](modal-quickstart.py) | [Modal](https://modal.com) | Run Claude Code in a Modal sandbox | +| [vercel-quickstart.py](vercel-quickstart.py) | [Vercel Sandbox](https://vercel.com/docs/vercel-sandbox) | Run Claude Code in a Vercel Sandbox | diff --git a/examples/daytona-quickstart.py b/examples/daytona-quickstart.py new file mode 100644 index 0000000..d64ca25 --- /dev/null +++ b/examples/daytona-quickstart.py @@ -0,0 +1,185 @@ +""" +Daytona Quickstart -- Run Claude Code in a Daytona cloud sandbox using runtimeuse. + +Setup: + pip install runtimeuse-client daytona + +Environment variables: + DAYTONA_API_KEY - your Daytona API key (https://daytona.io) + ANTHROPIC_API_KEY - your Anthropic API key + +Usage: + python daytona-quickstart.py +""" + +from __future__ import annotations + +import asyncio +import os + +from daytona import ( + CreateSandboxFromImageParams, + Daytona, + DaytonaConfig, + Image, + Sandbox, + SessionExecuteRequest, +) + +from runtimeuse_client import ( + RuntimeEnvironmentDownloadableInterface, + RuntimeUseClient, + QueryOptions, + AssistantMessageInterface, + TextResult, +) + +WORKDIR = "/home/daytona" +_SERVER_READY_SIGNAL = "RuntimeUse server listening on port" +_SERVER_STARTUP_TIMEOUT_S = 120 +_SESSION_ID = "runtimeuse" + + +def _get_env_or_fail(name: str) -> str: + value = os.environ.get(name) + if not value: + raise RuntimeError(f"{name} environment variable is not set") + return value + + +def _http_to_ws(url: str) -> str: + if url.startswith("https://"): + return "wss://" + url[len("https://") :] + if url.startswith("http://"): + return "ws://" + url[len("http://") :] + return url + + +def create_sandbox() -> tuple[Daytona, Sandbox]: + """Create a Daytona sandbox and return (daytona, sandbox). + + This is intentionally synchronous so that Daytona's internal use of + ``asyncio.run()`` (for streaming snapshot-build logs) does not conflict + with an already-running event loop. + """ + + daytona_api_key = _get_env_or_fail("DAYTONA_API_KEY") + anthropic_api_key = _get_env_or_fail("ANTHROPIC_API_KEY") + + image = Image.base("node:lts").run_commands( + "apt-get update && apt-get install -y unzip", + "npm install -g @anthropic-ai/claude-code", + ) + + daytona = Daytona(config=DaytonaConfig(api_key=daytona_api_key)) + + print("Creating Daytona sandbox (this may take a few minutes the first time)...") + sandbox = daytona.create( + CreateSandboxFromImageParams( + image=image, + env_vars={"ANTHROPIC_API_KEY": anthropic_api_key}, + public=True, + ), + timeout=300, + on_snapshot_create_logs=lambda chunk: print(chunk, end=""), + ) + print(f"Sandbox created: {sandbox.id}") + + return daytona, sandbox + + +async def _start_server_and_wait(sandbox: Sandbox) -> str: + """Start the runtimeuse server, stream logs, and wait for the ready signal. + + Returns the WebSocket URL once the server is listening. + """ + + anthropic_api_key = _get_env_or_fail("ANTHROPIC_API_KEY") + + sandbox.process.create_session(_SESSION_ID) + exec_resp = sandbox.process.execute_session_command( + _SESSION_ID, + SessionExecuteRequest( + command=f"export ANTHROPIC_API_KEY={anthropic_api_key} && npx -y runtimeuse --agent claude", + run_async=True, + ), + ) + + ready = asyncio.Event() + + def _on_stdout(log: str) -> None: + print(f"[runtimeuse] {log}") + if _SERVER_READY_SIGNAL in log: + ready.set() + + def _on_stderr(log: str) -> None: + print(f"[runtimeuse:err] {log}") + + log_task = asyncio.create_task( + sandbox.process.get_session_command_logs_async( + _SESSION_ID, + exec_resp.cmd_id, + _on_stdout, + _on_stderr, + ) + ) + + print("Waiting for runtimeuse server to start...") + try: + await asyncio.wait_for(ready.wait(), timeout=_SERVER_STARTUP_TIMEOUT_S) + except asyncio.TimeoutError: + log_task.cancel() + raise RuntimeError( + f"runtimeuse server did not start within {_SERVER_STARTUP_TIMEOUT_S}s" + ) + + preview = sandbox.create_signed_preview_url(8080, expires_in_seconds=3600) + ws_url = _http_to_ws(preview.url) + print(f"Sandbox ready at {ws_url}") + return ws_url + + +async def _run_query(ws_url: str) -> None: + client = RuntimeUseClient(ws_url=ws_url) + print(f"Connected to {ws_url}") + + async def on_message(msg: AssistantMessageInterface) -> None: + for block in msg.text_blocks: + print(f"[assistant] {block}") + + prompt = "Summarize the contents of the codex repository and list your favorite file in the repository." + print(f"Sending query: {prompt}") + + result = await client.query( + prompt=prompt, + options=QueryOptions( + system_prompt="You are a helpful assistant.", + model="claude-sonnet-4-20250514", + on_assistant_message=on_message, + pre_agent_downloadables=[ + RuntimeEnvironmentDownloadableInterface( + download_url="https://github.com/openai/codex/archive/refs/heads/main.zip", + working_dir=WORKDIR, + ) + ], + ), + ) + + print("\n--- Final Result ---") + assert isinstance(result.data, TextResult) + print(result.data.text) + + +def main() -> None: + daytona, sandbox = create_sandbox() + try: + ws_url = asyncio.run(_start_server_and_wait(sandbox)) + asyncio.run(_run_query(ws_url)) + finally: + daytona.delete(sandbox) + print("Sandbox deleted.") + + +if __name__ == "__main__": + # asyncio.run(_run_query("wss://8080-zgcun375m7vgilui.daytonaproxy01.net")) + main() diff --git a/examples/e2b-quickstart.py b/examples/e2b-quickstart.py index 937abc0..64b9e93 100644 --- a/examples/e2b-quickstart.py +++ b/examples/e2b-quickstart.py @@ -21,12 +21,15 @@ from e2b_code_interpreter import Sandbox from runtimeuse_client import ( + RuntimeEnvironmentDownloadableInterface, RuntimeUseClient, QueryOptions, AssistantMessageInterface, TextResult, ) +WORKDIR = "/runtimeuse" + def _get_env_or_fail(name: str) -> str: value = os.environ.get(name) @@ -35,22 +38,14 @@ def _get_env_or_fail(name: str) -> str: return value -def create_sandbox() -> tuple[Sandbox, str]: - """Build an E2B template with runtimeuse + Claude Code and return (sandbox, ws_url).""" - e2b_api_key = _get_env_or_fail("E2B_API_KEY") +def _create_template_with_alias(alias: str): anthropic_api_key = _get_env_or_fail("ANTHROPIC_API_KEY") - - alias = "runtimeuse-quickstart-claude" start_cmd = "npx -y runtimeuse --agent claude" - print( - f"Building E2B template '{alias}' (this may take a few minutes the first time)..." - ) - template = ( Template() .from_node_image("lts") - .apt_install(["unzip"]) + .set_workdir(WORKDIR) .npm_install(["@anthropic-ai/claude-code"], g=True) .set_envs({"ANTHROPIC_API_KEY": anthropic_api_key}) .set_start_cmd(start_cmd, wait_for_port(8080)) @@ -64,6 +59,19 @@ def create_sandbox() -> tuple[Sandbox, str]: on_build_logs=default_build_logger(), ) + +def create_sandbox() -> tuple[Sandbox, str]: + """Build an E2B template with runtimeuse + Claude Code and return (sandbox, ws_url).""" + + alias = "runtimeuse-quickstart-claude" + e2b_api_key = _get_env_or_fail("E2B_API_KEY") + + print( + f"Building E2B template '{alias}' (this may take a few minutes the first time)..." + ) + + _create_template_with_alias(alias) + sandbox = Sandbox.create(template=alias, api_key=e2b_api_key) ws_url = f"wss://{sandbox.get_host(8080)}" print(f"Sandbox ready at {ws_url}") @@ -81,7 +89,7 @@ async def on_message(msg: AssistantMessageInterface) -> None: for block in msg.text_blocks: print(f"[assistant] {block}") - prompt = "What files are in the current directory? List them." + prompt = "Summarize the contents of the codex repository and list your favorite file in the repository." print(f"Sending query: {prompt}") result = await client.query( @@ -90,6 +98,12 @@ async def on_message(msg: AssistantMessageInterface) -> None: system_prompt="You are a helpful assistant.", model="claude-sonnet-4-20250514", on_assistant_message=on_message, + pre_agent_downloadables=[ + RuntimeEnvironmentDownloadableInterface( + download_url="https://github.com/openai/codex/archive/refs/heads/main.zip", + working_dir=WORKDIR, + ) + ], ), ) diff --git a/examples/modal-quickstart.py b/examples/modal-quickstart.py new file mode 100644 index 0000000..b609826 --- /dev/null +++ b/examples/modal-quickstart.py @@ -0,0 +1,138 @@ +""" +Modal Quickstart -- Run Claude Code in a Modal sandbox using runtimeuse. + +Setup: + pip install runtimeuse-client modal + + Authenticate with Modal by running `modal token set` or by setting + MODAL_TOKEN_ID and MODAL_TOKEN_SECRET environment variables. + +Environment variables: + ANTHROPIC_API_KEY - your Anthropic API key + +Usage: + python modal-quickstart.py +""" + +from __future__ import annotations + +import asyncio +import os + +import modal + +from runtimeuse_client import ( + RuntimeEnvironmentDownloadableInterface, + RuntimeUseClient, + QueryOptions, + AssistantMessageInterface, + TextResult, +) + +WORKDIR = "/runtimeuse" +_SERVER_READY_SIGNAL = "RuntimeUse server listening on port" + + +def _get_env_or_fail(name: str) -> str: + value = os.environ.get(name) + if not value: + raise RuntimeError(f"{name} environment variable is not set") + return value + + +def _http_to_ws(url: str) -> str: + if url.startswith("https://"): + return "wss://" + url[len("https://"):] + if url.startswith("http://"): + return "ws://" + url[len("http://"):] + return url + + +def create_sandbox() -> tuple[modal.Sandbox, str]: + """Create a Modal Sandbox with runtimeuse + Claude Code and return (sandbox, ws_url).""" + + anthropic_api_key = _get_env_or_fail("ANTHROPIC_API_KEY") + + app = modal.App.lookup("runtimeuse-quickstart", create_if_missing=True) + + image = modal.Image.from_registry("node:lts").run_commands( + "apt-get update && apt-get install -y unzip", + "npm install -g @anthropic-ai/claude-code", + ) + + secret = modal.Secret.from_dict({"ANTHROPIC_API_KEY": anthropic_api_key}) + + print("Creating Modal Sandbox (this may take a few minutes the first time)...") + with modal.enable_output(): + sandbox = modal.Sandbox.create( + app=app, + image=image, + secrets=[secret], + workdir=WORKDIR, + encrypted_ports=[8080], + timeout=600, + ) + print(f"Sandbox created: {sandbox.object_id}") + + print("Starting runtimeuse server...") + process = sandbox.exec( + "npx", "-y", "runtimeuse", "--agent", "claude", + env={"ANTHROPIC_API_KEY": anthropic_api_key}, + ) + + print("Waiting for runtimeuse server to start...") + for line in process.stdout: + print(f"[runtimeuse] {line}", end="") + if _SERVER_READY_SIGNAL in line: + break + + tunnel = sandbox.tunnels()[8080] + ws_url = _http_to_ws(tunnel.url) + print(f"Sandbox ready at {ws_url}") + + return sandbox, ws_url + + +async def _run_query(ws_url: str) -> None: + client = RuntimeUseClient(ws_url=ws_url) + print(f"Connected to {ws_url}") + + async def on_message(msg: AssistantMessageInterface) -> None: + for block in msg.text_blocks: + print(f"[assistant] {block}") + + prompt = "Summarize the contents of the codex repository and list your favorite file in the repository." + print(f"Sending query: {prompt}") + + result = await client.query( + prompt=prompt, + options=QueryOptions( + system_prompt="You are a helpful assistant.", + model="claude-sonnet-4-20250514", + on_assistant_message=on_message, + pre_agent_downloadables=[ + RuntimeEnvironmentDownloadableInterface( + download_url="https://github.com/openai/codex/archive/refs/heads/main.zip", + working_dir=WORKDIR, + ) + ], + ), + ) + + print("\n--- Final Result ---") + assert isinstance(result.data, TextResult) + print(result.data.text) + + +def main() -> None: + sandbox, ws_url = create_sandbox() + try: + asyncio.run(_run_query(ws_url)) + finally: + sandbox.terminate() + sandbox.detach() + print("Sandbox terminated.") + + +if __name__ == "__main__": + main() diff --git a/examples/vercel-quickstart.py b/examples/vercel-quickstart.py new file mode 100644 index 0000000..58dede3 --- /dev/null +++ b/examples/vercel-quickstart.py @@ -0,0 +1,142 @@ +""" +Vercel Quickstart -- Run Claude Code in a Vercel Sandbox using runtimeuse. + +Setup: + pip install runtimeuse-client vercel python-dotenv + npm i -g vercel # needed for auth setup + vercel link # link to a Vercel project + vercel env pull # creates .env.local with OIDC token + + Alternatively, set VERCEL_TOKEN to a Vercel access token. + +Environment variables: + VERCEL_TOKEN - your Vercel access token + VERCEL_PROJECT_ID - your Vercel project ID + VERCEL_TEAM_ID - your Vercel team ID + ANTHROPIC_API_KEY - your Anthropic API key + +Usage: + python vercel-quickstart.py +""" + +from __future__ import annotations + +import asyncio +import os +from pathlib import Path + +from dotenv import load_dotenv + +load_dotenv() +load_dotenv(Path.cwd() / ".env.local") + +from vercel.sandbox import Sandbox + +from runtimeuse_client import ( + RuntimeEnvironmentDownloadableInterface, + RuntimeUseClient, + QueryOptions, + AssistantMessageInterface, + TextResult, +) + +WORKDIR = "/vercel/sandbox" +_SERVER_READY_SIGNAL = "RuntimeUse server listening on port" + + +def _get_env_or_fail(name: str) -> str: + value = os.environ.get(name) + if not value: + raise RuntimeError(f"{name} environment variable is not set") + return value + + +def _http_to_ws(url: str) -> str: + if url.startswith("https://"): + return "wss://" + url[len("https://") :] + if url.startswith("http://"): + return "ws://" + url[len("http://") :] + return url + + +def create_sandbox() -> tuple[Sandbox, str]: + """Create a Vercel Sandbox, install deps, start runtimeuse, and return (sandbox, ws_url).""" + + anthropic_api_key = _get_env_or_fail("ANTHROPIC_API_KEY") + + print("Creating Vercel Sandbox...") + sandbox = Sandbox.create( + runtime="node24", + ports=[8081], + env={"ANTHROPIC_API_KEY": anthropic_api_key}, + ) + print(f"Sandbox created: {sandbox.sandbox_id}") + + print("Installing dependencies...") + sandbox.run_command("sudo", ["dnf", "install", "-y", "unzip"]) + sandbox.run_command("npm", ["install", "-g", "@anthropic-ai/claude-code"]) + + print("Starting runtimeuse server...") + cmd = sandbox.run_command_detached( + "npx", + ["-y", "runtimeuse", "--agent", "claude", "--port", "8081"], + ) + + print("Waiting for runtimeuse server to start...") + for log in cmd.logs(): + if log.stream == "stdout": + print(f"[runtimeuse] {log.data}", end="") + else: + print(f"[runtimeuse:err] {log.data}", end="") + if _SERVER_READY_SIGNAL in log.data: + break + + domain = sandbox.domain(8081) + ws_url = _http_to_ws(domain) + print(f"Sandbox ready at {ws_url}") + + return sandbox, ws_url + + +async def _run_query(ws_url: str) -> None: + client = RuntimeUseClient(ws_url=ws_url) + print(f"Connected to {ws_url}") + + async def on_message(msg: AssistantMessageInterface) -> None: + for block in msg.text_blocks: + print(f"[assistant] {block}") + + prompt = "Summarize the contents of the codex repository and list your favorite file in the repository." + print(f"Sending query: {prompt}") + + result = await client.query( + prompt=prompt, + options=QueryOptions( + system_prompt="You are a helpful assistant.", + model="claude-sonnet-4-20250514", + on_assistant_message=on_message, + pre_agent_downloadables=[ + RuntimeEnvironmentDownloadableInterface( + download_url="https://github.com/openai/codex/archive/refs/heads/main.zip", + working_dir=WORKDIR, + ) + ], + ), + ) + + print("\n--- Final Result ---") + assert isinstance(result.data, TextResult) + print(result.data.text) + + +def main() -> None: + sandbox, ws_url = create_sandbox() + try: + asyncio.run(_run_query(ws_url)) + finally: + sandbox.stop() + print("Sandbox stopped.") + + +if __name__ == "__main__": + main() diff --git a/packages/runtimeuse-client-python/pyproject.toml b/packages/runtimeuse-client-python/pyproject.toml index 753a19c..9423982 100644 --- a/packages/runtimeuse-client-python/pyproject.toml +++ b/packages/runtimeuse-client-python/pyproject.toml @@ -26,6 +26,9 @@ dev = [ "daytona", "e2b", "e2b-code-interpreter", + "modal", + "python-dotenv", + "vercel", ] [tool.hatch.build.targets.wheel] diff --git a/packages/runtimeuse/src/session.ts b/packages/runtimeuse/src/session.ts index 043adbd..ff43012 100644 --- a/packages/runtimeuse/src/session.ts +++ b/packages/runtimeuse/src/session.ts @@ -46,8 +46,6 @@ export class WebSocketSession { error: String(error), metadata: {}, }); - - // todo: maybe close ws on error since nothing will happen after? } }); From e5fce501add219b6cb877e7bdeab341610736f7e Mon Sep 17 00:00:00 2001 From: Vijit Dhingra Date: Tue, 17 Mar 2026 14:58:43 -0700 Subject: [PATCH 2/8] update runtime error reporting and sandbox startup examples Preserve structured failure metadata so clients can surface actionable diagnostics, and make the sandbox quickstarts start Claude with the required environment already configured. Made-with: Cursor --- examples/daytona-quickstart.py | 11 +- examples/modal-quickstart.py | 81 +++++++++-- .../src/runtimeuse_client/exceptions.py | 14 ++ packages/runtimeuse/src/claude-handler.ts | 7 +- packages/runtimeuse/src/error-utils.ts | 135 ++++++++++++++++++ packages/runtimeuse/src/session.test.ts | 48 ++++++- packages/runtimeuse/src/session.ts | 13 +- 7 files changed, 278 insertions(+), 31 deletions(-) create mode 100644 packages/runtimeuse/src/error-utils.ts diff --git a/examples/daytona-quickstart.py b/examples/daytona-quickstart.py index d64ca25..affc3f8 100644 --- a/examples/daytona-quickstart.py +++ b/examples/daytona-quickstart.py @@ -77,7 +77,11 @@ def create_sandbox() -> tuple[Daytona, Sandbox]: sandbox = daytona.create( CreateSandboxFromImageParams( image=image, - env_vars={"ANTHROPIC_API_KEY": anthropic_api_key}, + env_vars={ + "ANTHROPIC_API_KEY": anthropic_api_key, + "IS_SANDBOX": "1", + "CLAUDE_SKIP_ROOT_CHECK": "1", + }, public=True, ), timeout=300, @@ -94,13 +98,11 @@ async def _start_server_and_wait(sandbox: Sandbox) -> str: Returns the WebSocket URL once the server is listening. """ - anthropic_api_key = _get_env_or_fail("ANTHROPIC_API_KEY") - sandbox.process.create_session(_SESSION_ID) exec_resp = sandbox.process.execute_session_command( _SESSION_ID, SessionExecuteRequest( - command=f"export ANTHROPIC_API_KEY={anthropic_api_key} && npx -y runtimeuse --agent claude", + command=f"npx -y runtimeuse --agent claude", run_async=True, ), ) @@ -181,5 +183,4 @@ def main() -> None: if __name__ == "__main__": - # asyncio.run(_run_query("wss://8080-zgcun375m7vgilui.daytonaproxy01.net")) main() diff --git a/examples/modal-quickstart.py b/examples/modal-quickstart.py index b609826..7f7fbb8 100644 --- a/examples/modal-quickstart.py +++ b/examples/modal-quickstart.py @@ -18,8 +18,12 @@ import asyncio import os +import queue +import threading +import time import modal +from modal.exception import ClientClosed from runtimeuse_client import ( RuntimeEnvironmentDownloadableInterface, @@ -31,6 +35,7 @@ WORKDIR = "/runtimeuse" _SERVER_READY_SIGNAL = "RuntimeUse server listening on port" +_SERVER_STARTUP_TIMEOUT_S = 120 def _get_env_or_fail(name: str) -> str: @@ -42,12 +47,63 @@ def _get_env_or_fail(name: str) -> str: def _http_to_ws(url: str) -> str: if url.startswith("https://"): - return "wss://" + url[len("https://"):] + return "wss://" + url[len("https://") :] if url.startswith("http://"): - return "ws://" + url[len("http://"):] + return "ws://" + url[len("http://") :] return url +def _wait_for_server_ready(process) -> None: + log_queue: queue.Queue[tuple[str, str]] = queue.Queue() + ready = threading.Event() + + def _pump_stream(prefix: str, stream) -> None: + try: + for line in stream: + log_queue.put((prefix, line)) + if _SERVER_READY_SIGNAL in line: + ready.set() + except ClientClosed: + # The example tears down the sandbox after the query finishes, which can + # close the underlying Modal client while daemon threads are still + # draining logs. + return + + for prefix, stream in ( + ("[runtimeuse]", process.stdout), + ("[runtimeuse:err]", process.stderr), + ): + threading.Thread( + target=_pump_stream, + args=(prefix, stream), + daemon=True, + ).start() + + deadline = time.monotonic() + _SERVER_STARTUP_TIMEOUT_S + while time.monotonic() < deadline: + while True: + try: + prefix, line = log_queue.get_nowait() + except queue.Empty: + break + print(f"{prefix} {line}", end="") + + if ready.is_set(): + return + + exit_code = process.poll() + if exit_code is not None: + raise RuntimeError( + f"runtimeuse server exited before becoming ready (exit code {exit_code})" + ) + + time.sleep(0.2) + + raise RuntimeError( + f"runtimeuse server did not start within {_SERVER_STARTUP_TIMEOUT_S}s" + ) + + def create_sandbox() -> tuple[modal.Sandbox, str]: """Create a Modal Sandbox with runtimeuse + Claude Code and return (sandbox, ws_url).""" @@ -60,7 +116,13 @@ def create_sandbox() -> tuple[modal.Sandbox, str]: "npm install -g @anthropic-ai/claude-code", ) - secret = modal.Secret.from_dict({"ANTHROPIC_API_KEY": anthropic_api_key}) + secret = modal.Secret.from_dict( + { + "ANTHROPIC_API_KEY": anthropic_api_key, + "IS_SANDBOX": "1", + "CLAUDE_SKIP_ROOT_CHECK": "1", + } + ) print("Creating Modal Sandbox (this may take a few minutes the first time)...") with modal.enable_output(): @@ -75,16 +137,17 @@ def create_sandbox() -> tuple[modal.Sandbox, str]: print(f"Sandbox created: {sandbox.object_id}") print("Starting runtimeuse server...") + process = sandbox.exec( - "npx", "-y", "runtimeuse", "--agent", "claude", - env={"ANTHROPIC_API_KEY": anthropic_api_key}, + "npx", + "-y", + "runtimeuse", + "--agent", + "claude", ) print("Waiting for runtimeuse server to start...") - for line in process.stdout: - print(f"[runtimeuse] {line}", end="") - if _SERVER_READY_SIGNAL in line: - break + _wait_for_server_ready(process) tunnel = sandbox.tunnels()[8080] ws_url = _http_to_ws(tunnel.url) diff --git a/packages/runtimeuse-client-python/src/runtimeuse_client/exceptions.py b/packages/runtimeuse-client-python/src/runtimeuse_client/exceptions.py index 971211d..7ea739e 100644 --- a/packages/runtimeuse-client-python/src/runtimeuse_client/exceptions.py +++ b/packages/runtimeuse-client-python/src/runtimeuse_client/exceptions.py @@ -1,3 +1,6 @@ +import json + + class CancelledException(Exception): """Raised when an agent runtime invocation is cancelled.""" @@ -11,3 +14,14 @@ def __init__(self, error: str, metadata: dict | None = None): super().__init__(error) self.error = error self.metadata = metadata + + def __str__(self) -> str: + if not self.metadata: + return self.error + + try: + metadata_str = json.dumps(self.metadata, sort_keys=True, default=str) + except TypeError: + metadata_str = str(self.metadata) + + return f"{self.error}\nmetadata: {metadata_str}" diff --git a/packages/runtimeuse/src/claude-handler.ts b/packages/runtimeuse/src/claude-handler.ts index 65273c7..0a6ff91 100644 --- a/packages/runtimeuse/src/claude-handler.ts +++ b/packages/runtimeuse/src/claude-handler.ts @@ -28,6 +28,9 @@ export const claudeHandler: AgentHandler = { sender: MessageSender, ): Promise { const abortController = new AbortController(); + let resultText: string | undefined; + let structuredOutput: Record | undefined; + const metadata: Record = {}; const onAbort = () => abortController.abort(); invocation.signal.addEventListener("abort", onAbort, { once: true }); @@ -64,10 +67,6 @@ export const claudeHandler: AgentHandler = { options: queryOptions as Parameters[0]["options"], }); - let resultText: string | undefined; - let structuredOutput: Record | undefined; - const metadata: Record = {}; - for await (const message of conversation) { if (message.type === "assistant") { const text = extractTextFromContent( diff --git a/packages/runtimeuse/src/error-utils.ts b/packages/runtimeuse/src/error-utils.ts new file mode 100644 index 0000000..80dbbd2 --- /dev/null +++ b/packages/runtimeuse/src/error-utils.ts @@ -0,0 +1,135 @@ +const MAX_DEPTH = 4; +const MAX_ARRAY_ITEMS = 20; +const MAX_OBJECT_KEYS = 20; +const MAX_STRING_LENGTH = 4_000; + +type ErrorWithMetadata = Error & { + cause?: unknown; + metadata?: unknown; +}; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function truncateString(value: string): string { + if (value.length <= MAX_STRING_LENGTH) { + return value; + } + return `${value.slice(0, MAX_STRING_LENGTH)}... [truncated ${value.length - MAX_STRING_LENGTH} chars]`; +} + +function toSerializable(value: unknown, depth = 0): unknown { + if (value == null || typeof value === "number" || typeof value === "boolean") { + return value; + } + + if (typeof value === "string") { + return truncateString(value); + } + + if (depth >= MAX_DEPTH) { + return "[max depth reached]"; + } + + if (Array.isArray(value)) { + return value.slice(0, MAX_ARRAY_ITEMS).map((item) => toSerializable(item, depth + 1)); + } + + if (value instanceof Error) { + return serializeErrorMetadata(value); + } + + if (isRecord(value)) { + const entries = Object.entries(value).slice(0, MAX_OBJECT_KEYS); + return Object.fromEntries( + entries.map(([key, entryValue]) => [ + key, + toSerializable(entryValue, depth + 1), + ]), + ); + } + + return String(value); +} + +export function getErrorMessage(error: unknown): string { + if (error instanceof Error && error.message) { + return error.message; + } + if (typeof error === "string") { + return error; + } + const serializable = toSerializable(error); + if (typeof serializable === "string") { + return serializable; + } + return JSON.stringify(serializable); +} + +export function serializeErrorMetadata(error: unknown): Record { + if (error instanceof Error) { + const typedError = error as ErrorWithMetadata; + const metadata: Record = { + error_name: error.name, + }; + + if (error.stack) { + metadata.stack = truncateString(error.stack); + } + if (typedError.cause !== undefined) { + metadata.cause = toSerializable(typedError.cause); + } + if (isRecord(typedError.metadata)) { + const serializedMetadata = toSerializable(typedError.metadata); + if (isRecord(serializedMetadata)) { + Object.assign(metadata, serializedMetadata); + } + } + + const extraEntries = Object.entries(typedError).filter( + ([key]) => !["name", "message", "stack", "cause", "metadata"].includes(key), + ); + if (extraEntries.length > 0) { + metadata.error_details = Object.fromEntries( + extraEntries.map(([key, value]) => [key, toSerializable(value)]), + ); + } + + return metadata; + } + + if (typeof error === "string") { + return { error_type: "string" }; + } + + if (error && typeof error === "object") { + return { + error_type: "object", + error_details: toSerializable(error), + }; + } + + return { error_type: typeof error }; +} + +export function withErrorMetadata( + error: unknown, + metadata: Record, +): Error { + if (error instanceof Error) { + const typedError = error as ErrorWithMetadata; + const existingMetadata = isRecord(typedError.metadata) + ? typedError.metadata + : {}; + typedError.metadata = { + ...existingMetadata, + ...metadata, + }; + return error; + } + + const wrapped = new Error(getErrorMessage(error)); + (wrapped as ErrorWithMetadata).metadata = metadata; + return wrapped; +} diff --git a/packages/runtimeuse/src/session.test.ts b/packages/runtimeuse/src/session.test.ts index 6e71421..d53ce56 100644 --- a/packages/runtimeuse/src/session.test.ts +++ b/packages/runtimeuse/src/session.test.ts @@ -276,6 +276,35 @@ describe("WebSocketSession", () => { expectSentError(ws, "agent crashed"); }); + + it("includes structured metadata when agent throws", async () => { + const error = Object.assign(new Error("agent crashed"), { + metadata: { + handler: "claude", + session_id: "abc123", + stderr_tail: "permission denied", + }, + code: "ERR_AGENT_CRASH", + }); + mockHandlerRun.mockRejectedValueOnce(error); + + const { session, ws } = createSession(); + const done = session.run(); + sendMessage(ws, INVOCATION_MSG); + await done; + + const sent = parseSentMessages(ws); + const runtimeError = sent.find((m) => m.message_type === "error_message"); + expect(runtimeError).toBeDefined(); + expect(runtimeError!.metadata).toMatchObject({ + error_name: "Error", + handler: "claude", + session_id: "abc123", + }); + expect(runtimeError!.metadata.error_details).toMatchObject({ + code: "ERR_AGENT_CRASH", + }); + }); }); describe("finalization", () => { @@ -389,9 +418,12 @@ describe("WebSocketSession", () => { }); it("redacts secrets from error messages", async () => { - mockHandlerRun.mockRejectedValueOnce( - new Error("crash with secret123 in trace"), - ); + const error = Object.assign(new Error("crash with secret123 in trace"), { + metadata: { + stderr_tail: "secret123 appeared in stderr", + }, + }); + mockHandlerRun.mockRejectedValueOnce(error); const { session, ws } = createSession(); const done = session.run(); @@ -399,10 +431,12 @@ describe("WebSocketSession", () => { await done; const sent = parseSentMessages(ws); - const error = sent.find((m) => m.message_type === "error_message"); - expect(error).toBeDefined(); - expect(error!.error).not.toContain("secret123"); - expect(error!.error).toContain("[REDACTED]"); + const runtimeError = sent.find((m) => m.message_type === "error_message"); + expect(runtimeError).toBeDefined(); + expect(runtimeError!.error).not.toContain("secret123"); + expect(runtimeError!.error).toContain("[REDACTED]"); + expect(JSON.stringify(runtimeError!.metadata)).not.toContain("secret123"); + expect(JSON.stringify(runtimeError!.metadata)).toContain("[REDACTED]"); }); }); diff --git a/packages/runtimeuse/src/session.ts b/packages/runtimeuse/src/session.ts index ff43012..e62e6c2 100644 --- a/packages/runtimeuse/src/session.ts +++ b/packages/runtimeuse/src/session.ts @@ -4,6 +4,7 @@ import type { AgentHandler } from "./agent-handler.js"; import { ArtifactManager } from "./artifact-manager.js"; import type { UploadTracker } from "./upload-tracker.js"; import type { InvocationMessage, IncomingMessage, OutgoingMessage } from "./types.js"; +import { getErrorMessage, serializeErrorMetadata } from "./error-utils.js"; import { redactSecrets, sleep } from "./utils.js"; import { createLogger, createRedactingLogger, defaultLogger, type Logger } from "./logger.js"; import { InvocationRunner } from "./invocation-runner.js"; @@ -43,8 +44,8 @@ export class WebSocketSession { this.logger.error("Error processing message:", error); this.send({ message_type: "error_message", - error: String(error), - metadata: {}, + error: getErrorMessage(error), + metadata: serializeErrorMetadata(error), }); } }); @@ -87,8 +88,8 @@ export class WebSocketSession { this.logger.error("Error uploading artifact:", error); this.send({ message_type: "error_message", - error: String(error), - metadata: {}, + error: getErrorMessage(error), + metadata: serializeErrorMetadata(error), }); } break; @@ -143,8 +144,8 @@ export class WebSocketSession { this.logger.error("Error in agent execution:", error); this.send({ message_type: "error_message", - error: error instanceof Error ? error.message : JSON.stringify(error), - metadata: {}, + error: getErrorMessage(error), + metadata: serializeErrorMetadata(error), }); } } From 5929b619f40b76bf8bbc6a7fe4b29bb82ff5dbe9 Mon Sep 17 00:00:00 2001 From: Vijit Dhingra Date: Tue, 17 Mar 2026 15:05:30 -0700 Subject: [PATCH 3/8] update example sandbox env defaults Align the remaining sandbox quickstarts with the Claude sandbox environment flags and ignore generated Python bytecode files in the repo. Made-with: Cursor --- .gitignore | 5 +++++ examples/README.md | 2 +- examples/e2b-quickstart.py | 8 +++++++- examples/vercel-quickstart.py | 6 +++++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index de05013..8129a8b 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,11 @@ Thumbs.db # Scratch files scratch_* +# Python bytecode +__pycache__/ +*.pyc +*.pyo + # Logs and temp *.log npm-debug.log* diff --git a/examples/README.md b/examples/README.md index c8f4ced..5a550e9 100644 --- a/examples/README.md +++ b/examples/README.md @@ -10,5 +10,5 @@ Each example is a single, self-contained `.py` file. Setup instructions (depende |------|----------|-------------| | [e2b-quickstart.py](e2b-quickstart.py) | [E2B](https://e2b.dev) | Run Claude Code in an E2B cloud sandbox | | [daytona-quickstart.py](daytona-quickstart.py) | [Daytona](https://daytona.io) | Run Claude Code in a Daytona cloud sandbox | -| [modal-quickstart.py](modal-quickstart.py) | [Modal](https://modal.com) | Run Claude Code in a Modal sandbox | | [vercel-quickstart.py](vercel-quickstart.py) | [Vercel Sandbox](https://vercel.com/docs/vercel-sandbox) | Run Claude Code in a Vercel Sandbox | +| [modal-quickstart.py](modal-quickstart.py) | [Modal](https://modal.com) | Run Claude Code in a Modal sandbox | diff --git a/examples/e2b-quickstart.py b/examples/e2b-quickstart.py index 64b9e93..36073d9 100644 --- a/examples/e2b-quickstart.py +++ b/examples/e2b-quickstart.py @@ -47,7 +47,13 @@ def _create_template_with_alias(alias: str): .from_node_image("lts") .set_workdir(WORKDIR) .npm_install(["@anthropic-ai/claude-code"], g=True) - .set_envs({"ANTHROPIC_API_KEY": anthropic_api_key}) + .set_envs( + { + "ANTHROPIC_API_KEY": anthropic_api_key, + "IS_SANDBOX": "1", + "CLAUDE_SKIP_ROOT_CHECK": "1", + } + ) .set_start_cmd(start_cmd, wait_for_port(8080)) ) diff --git a/examples/vercel-quickstart.py b/examples/vercel-quickstart.py index 58dede3..c020a5e 100644 --- a/examples/vercel-quickstart.py +++ b/examples/vercel-quickstart.py @@ -68,7 +68,11 @@ def create_sandbox() -> tuple[Sandbox, str]: sandbox = Sandbox.create( runtime="node24", ports=[8081], - env={"ANTHROPIC_API_KEY": anthropic_api_key}, + env={ + "ANTHROPIC_API_KEY": anthropic_api_key, + "IS_SANDBOX": "1", + "CLAUDE_SKIP_ROOT_CHECK": "1", + }, ) print(f"Sandbox created: {sandbox.sandbox_id}") From b11edd74dd85b5ddfcc81498385e322975c170e0 Mon Sep 17 00:00:00 2001 From: Vijit Dhingra Date: Tue, 17 Mar 2026 15:52:11 -0700 Subject: [PATCH 4/8] update quickstart docs and typed example flows Replace toy prompts with richer codex repository examples across the docs and show structured output with a typed Vercel quickstart example. Made-with: Cursor --- README.md | 23 +++++++-- docs/content/docs/quickstart.mdx | 54 +++++++++++++++++++-- examples/vercel-quickstart.py | 22 +++++++-- packages/runtimeuse-client-python/README.md | 50 +++++++++++++++++-- packages/runtimeuse/README.md | 3 ++ 5 files changed, 134 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 27c2e07..4ec4d7b 100644 --- a/README.md +++ b/README.md @@ -14,28 +14,43 @@ Run AI agents inside sandboxes and communicate with them over WebSocket. ### 1. Start the runtime (inside a sandbox) ```bash +export OPENAI_API_KEY=your_openai_api_key npx -y runtimeuse ``` -This starts a WebSocket server on port 8080 using the OpenAI agent handler by default. Use `--agent claude` for Claude. The Claude handler also requires the `claude` CLI to be installed in the sandbox, for example with `npm install -g @anthropic-ai/claude-code`. +This starts a WebSocket server on port 8080 using the default OpenAI handler. For fuller Claude-based sandbox examples, see [`examples/`](./examples). ### 2. Connect from Python ```python import asyncio -from runtimeuse_client import RuntimeUseClient, QueryOptions +from runtimeuse_client import ( + QueryOptions, + RuntimeEnvironmentDownloadableInterface, + RuntimeUseClient, + TextResult, +) + +WORKDIR = "/runtimeuse" async def main(): client = RuntimeUseClient(ws_url="ws://localhost:8080") result = await client.query( - prompt="What is 2 + 2?", + prompt="Summarize the contents of the codex repository.", options=QueryOptions( system_prompt="You are a helpful assistant.", - model="gpt-4.1", + model="gpt-5.4", + pre_agent_downloadables=[ + RuntimeEnvironmentDownloadableInterface( + download_url="https://github.com/openai/codex/archive/refs/heads/main.zip", + working_dir=WORKDIR, + ) + ], ), ) + assert isinstance(result.data, TextResult) print(result.data.text) asyncio.run(main()) diff --git a/docs/content/docs/quickstart.mdx b/docs/content/docs/quickstart.mdx index 581dc86..5c213d6 100644 --- a/docs/content/docs/quickstart.mdx +++ b/docs/content/docs/quickstart.mdx @@ -37,19 +37,33 @@ For local development, connect directly to the runtime's WebSocket URL: ```python import asyncio -from runtimeuse_client import RuntimeUseClient, QueryOptions +from runtimeuse_client import ( + QueryOptions, + RuntimeEnvironmentDownloadableInterface, + RuntimeUseClient, + TextResult, +) + +WORKDIR = "/runtimeuse" async def main(): client = RuntimeUseClient(ws_url="ws://localhost:8080") result = await client.query( - prompt="What is 2 + 2?", + prompt="Summarize the contents of the codex repository and list your favorite file in the repository.", options=QueryOptions( system_prompt="You are a helpful assistant.", model="gpt-4.1", + pre_agent_downloadables=[ + RuntimeEnvironmentDownloadableInterface( + download_url="https://github.com/openai/codex/archive/refs/heads/main.zip", + working_dir=WORKDIR, + ) + ], ), ) + assert isinstance(result.data, TextResult) print(result.data.text) asyncio.run(main()) @@ -61,7 +75,23 @@ In a real sandbox integration, your sandbox provider gives you the runtime URL a ```python import asyncio -from runtimeuse_client import RuntimeUseClient, QueryOptions +import json + +from pydantic import BaseModel + +from runtimeuse_client import ( + QueryOptions, + RuntimeEnvironmentDownloadableInterface, + RuntimeUseClient, + StructuredOutputResult, +) + +WORKDIR = "/runtimeuse" + + +class RepoStats(BaseModel): + file_count: int + char_count: int async def main(): # Pseudocode: start the runtime inside your sandbox provider @@ -72,14 +102,28 @@ async def main(): client = RuntimeUseClient(ws_url=ws_url) result = await client.query( - prompt="Summarize the repository.", + prompt="Inspect the codex repository and return the total file count and total character count across all files as JSON.", options=QueryOptions( system_prompt="You are a helpful assistant.", model="gpt-4.1", + pre_agent_downloadables=[ + RuntimeEnvironmentDownloadableInterface( + download_url="https://github.com/openai/codex/archive/refs/heads/main.zip", + working_dir=WORKDIR, + ) + ], + output_format_json_schema_str=json.dumps( + { + "type": "json_schema", + "schema": RepoStats.model_json_schema(), + } + ), ), ) - print(result.data.text) + assert isinstance(result.data, StructuredOutputResult) + stats = RepoStats.model_validate(result.data.structured_output) + print(stats.model_dump()) asyncio.run(main()) ``` diff --git a/examples/vercel-quickstart.py b/examples/vercel-quickstart.py index c020a5e..b390b17 100644 --- a/examples/vercel-quickstart.py +++ b/examples/vercel-quickstart.py @@ -22,10 +22,12 @@ from __future__ import annotations import asyncio +import json import os from pathlib import Path from dotenv import load_dotenv +from pydantic import BaseModel load_dotenv() load_dotenv(Path.cwd() / ".env.local") @@ -37,13 +39,18 @@ RuntimeUseClient, QueryOptions, AssistantMessageInterface, - TextResult, + StructuredOutputResult, ) WORKDIR = "/vercel/sandbox" _SERVER_READY_SIGNAL = "RuntimeUse server listening on port" +class RepoStats(BaseModel): + file_count: int + char_count: int + + def _get_env_or_fail(name: str) -> str: value = os.environ.get(name) if not value: @@ -110,7 +117,7 @@ async def on_message(msg: AssistantMessageInterface) -> None: for block in msg.text_blocks: print(f"[assistant] {block}") - prompt = "Summarize the contents of the codex repository and list your favorite file in the repository." + prompt = "Inspect the codex repository and return the total file count and total character count across all files as JSON." print(f"Sending query: {prompt}") result = await client.query( @@ -125,12 +132,19 @@ async def on_message(msg: AssistantMessageInterface) -> None: working_dir=WORKDIR, ) ], + output_format_json_schema_str=json.dumps( + { + "type": "json_schema", + "schema": RepoStats.model_json_schema(), + } + ), ), ) print("\n--- Final Result ---") - assert isinstance(result.data, TextResult) - print(result.data.text) + assert isinstance(result.data, StructuredOutputResult) + stats = RepoStats.model_validate(result.data.structured_output) + print(stats.model_dump()) def main() -> None: diff --git a/packages/runtimeuse-client-python/README.md b/packages/runtimeuse-client-python/README.md index 3eabb6f..9ddf0e4 100644 --- a/packages/runtimeuse-client-python/README.md +++ b/packages/runtimeuse-client-python/README.md @@ -16,7 +16,16 @@ Start the runtime inside any sandbox, then connect from outside: ```python import asyncio -from runtimeuse_client import RuntimeUseClient, QueryOptions, TextResult, StructuredOutputResult +from runtimeuse_client import ( + AssistantMessageInterface, + QueryOptions, + RuntimeEnvironmentDownloadableInterface, + RuntimeUseClient, + StructuredOutputResult, + TextResult, +) + +WORKDIR = "/runtimeuse" async def main(): # Start the runtime in a sandbox (provider-specific) @@ -26,12 +35,23 @@ async def main(): client = RuntimeUseClient(ws_url=ws_url) + async def on_assistant(msg: AssistantMessageInterface) -> None: + for block in msg.text_blocks: + print(f"[assistant] {block}") + # Text response (no output schema) result = await client.query( - prompt="What is the capital of France?", + prompt="Summarize the contents of the codex repository and list your favorite file in the repository.", options=QueryOptions( system_prompt="You are a helpful assistant.", model="gpt-4.1", + on_assistant_message=on_assistant, + pre_agent_downloadables=[ + RuntimeEnvironmentDownloadableInterface( + download_url="https://github.com/openai/codex/archive/refs/heads/main.zip", + working_dir=WORKDIR, + ) + ], ), ) assert isinstance(result.data, TextResult) @@ -39,11 +59,30 @@ async def main(): # Structured response (with output schema) result = await client.query( - prompt="Return the capital of France.", + prompt="Inspect the codex repository and return the total file count and total character count across all files as JSON.", options=QueryOptions( system_prompt="You are a helpful assistant.", model="gpt-4.1", - output_format_json_schema_str='{"type":"json_schema","schema":{"type":"object"}}', + pre_agent_downloadables=[ + RuntimeEnvironmentDownloadableInterface( + download_url="https://github.com/openai/codex/archive/refs/heads/main.zip", + working_dir=WORKDIR, + ) + ], + output_format_json_schema_str=""" +{ + "type": "json_schema", + "schema": { + "type": "object", + "properties": { + "file_count": { "type": "integer" }, + "char_count": { "type": "integer" } + }, + "required": ["file_count", "char_count"], + "additionalProperties": false + } +} +""", ), ) assert isinstance(result.data, StructuredOutputResult) @@ -71,10 +110,11 @@ Manages the WebSocket connection to the agent runtime and runs the message loop: client = RuntimeUseClient(ws_url="ws://localhost:8080") result = await client.query( - prompt="Do the thing.", + prompt="Summarize the contents of the codex repository.", options=QueryOptions( system_prompt="You are a helpful assistant.", model="gpt-4.1", + pre_agent_downloadables=[downloadable], # optional output_format_json_schema_str='...', # optional -- omit for text response on_assistant_message=on_assistant, # optional on_artifact_upload_request=on_artifact, # optional -- return ArtifactUploadResult diff --git a/packages/runtimeuse/README.md b/packages/runtimeuse/README.md index cfc911a..da81446 100644 --- a/packages/runtimeuse/README.md +++ b/packages/runtimeuse/README.md @@ -15,6 +15,7 @@ npm install runtimeuse Run the runtime inside any sandbox: ```bash +export OPENAI_API_KEY=your_openai_api_key npx -y runtimeuse ``` @@ -39,6 +40,8 @@ const server = new RuntimeUseServer({ handler: openaiHandler, port: 8080 }); await server.startListening(); ``` +Pair this with the richer Python client examples in [`runtimeuse-client`](../runtimeuse-client-python/README.md), including streamed assistant messages and `pre_agent_downloadables` for bootstrapping files into the sandbox before invocation. + ### Custom Handler Implement `AgentHandler` to plug in your own agent: From fdc5ff35bc69639993d2ee9a1a9b06d853882a42 Mon Sep 17 00:00:00 2001 From: Vijit Dhingra Date: Tue, 17 Mar 2026 19:02:15 -0700 Subject: [PATCH 5/8] improve docs: condense intro, add terminal SVG, polish copy across all pages - Replace code snippet on intro page with terminal trace SVG - Add per-provider pip install and env var comments to quickstart tabs - Use Pydantic model for structured output example in python-client - Improve descriptions and headings across all doc pages Made-with: Cursor --- docs/content/docs/agent-runtime.mdx | 123 +++++++++++++++ docs/content/docs/index.mdx | 47 +++--- docs/content/docs/meta.json | 8 + docs/content/docs/python-client.mdx | 215 ++++++++++++++++++++++++++ docs/content/docs/quickstart.mdx | 229 +++++++++++++++++----------- docs/public/terminal.svg | 48 ++++++ 6 files changed, 553 insertions(+), 117 deletions(-) create mode 100644 docs/content/docs/agent-runtime.mdx create mode 100644 docs/content/docs/meta.json create mode 100644 docs/content/docs/python-client.mdx create mode 100644 docs/public/terminal.svg diff --git a/docs/content/docs/agent-runtime.mdx b/docs/content/docs/agent-runtime.mdx new file mode 100644 index 0000000..1975f8a --- /dev/null +++ b/docs/content/docs/agent-runtime.mdx @@ -0,0 +1,123 @@ +--- +title: Agent Runtime +description: CLI flags, built-in agent handlers, and custom handler authoring for the runtimeuse server. +--- + +The [agent runtime](https://www.npmjs.com/package/runtimeuse) is the process that runs inside the sandbox. It exposes a WebSocket server, receives invocations from the Python client, and delegates work to an agent handler. + +## CLI + +```bash +npx -y runtimeuse # OpenAI handler on port 8080 +npx -y runtimeuse --agent claude # Claude handler +npx -y runtimeuse --port 3000 # custom port +npx -y runtimeuse --handler ./my-handler.js # custom handler entrypoint +``` + +## Built-in Handlers + +- `openai`: the default handler, uses the [OpenAI Agents SDK](https://openai.github.io/openai-agents-js/) with shell and web search tools. +- `claude`: uses [Claude Agents SDK](https://platform.claude.com/docs/en/agent-sdk/overview) with Claude Code preset. + +### OpenAI Handler + +Requires `OPENAI_API_KEY` to be set in the environment. The handler runs the agent with shell access and web search enabled. + +```bash +export OPENAI_API_KEY=your_openai_api_key +npx -y runtimeuse +``` + +### Claude Handler + +Requires the `@anthropic-ai/claude-code` CLI and `ANTHROPIC_API_KEY`. Always set `IS_SANDBOX=1` and `CLAUDE_SKIP_ROOT_CHECK=1` in the sandbox environment. + +```bash +npm install -g @anthropic-ai/claude-code +export ANTHROPIC_API_KEY=your_anthropic_api_key +export IS_SANDBOX=1 +export CLAUDE_SKIP_ROOT_CHECK=1 +npx -y runtimeuse --agent claude +``` + +## Programmatic Startup + +If you want to embed RuntimeUse directly in your own Node process, start it programmatically: + +```typescript +import { RuntimeUseServer, openaiHandler } from "runtimeuse"; + +const server = new RuntimeUseServer({ + handler: openaiHandler, + port: 8080, +}); + +await server.startListening(); +``` + +## Custom Handlers + +When the built-in handlers are not enough, you can pass your own handler to `RuntimeUseServer`: + +```typescript +import { RuntimeUseServer } from "runtimeuse"; +import type { + AgentHandler, + AgentInvocation, + AgentResult, + MessageSender, +} from "runtimeuse"; + +const handler: AgentHandler = { + async run( + invocation: AgentInvocation, + sender: MessageSender, + ): Promise { + sender.sendAssistantMessage(["Running agent..."]); + + const output = await myAgent( + invocation.systemPrompt, + invocation.userPrompt, + ); + + return { + type: "structured_output", + structuredOutput: output, + metadata: { duration_ms: 1500 }, + }; + }, +}; + +const server = new RuntimeUseServer({ handler, port: 8080 }); +await server.startListening(); +``` + +### Handler Contracts + +Your handler receives an `AgentInvocation` with: + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `systemPrompt` | `string` | System prompt for the agent. | +| `userPrompt` | `string` | User prompt sent from the Python client. | +| `model` | `string` | Model name passed by the client. | +| `outputFormat` | `{ type: "json_schema"; schema: ... } \| undefined` | Present when the client requests structured output. Pass to your agent to enforce the schema. | +| `signal` | `AbortSignal` | Fires when the client sends a cancel message. Pass to any async operations that support cancellation. | +| `logger` | `Logger` | Use `invocation.logger.log(msg)` to emit log lines visible in sandbox logs. | + +Use `MessageSender` to stream intermediate output before returning the final result: + +- `sendAssistantMessage(textBlocks: string[])`: emit text blocks the Python client receives via `on_assistant_message`. +- `sendErrorMessage(error: string, metadata?: Record)`: signal a non-fatal error before aborting. + +Return an `AgentResult` from your handler: + +```typescript +// Text result +return { type: "text", text: "...", metadata: { duration_ms: 100 } }; + +// Structured output result +return { type: "structured_output", structuredOutput: { file_count: 42 }, metadata: {} }; +``` + +`metadata` is optional and is passed through to `result.metadata` on the Python side. diff --git a/docs/content/docs/index.mdx b/docs/content/docs/index.mdx index a2b4a8f..a63f7ae 100644 --- a/docs/content/docs/index.mdx +++ b/docs/content/docs/index.mdx @@ -1,43 +1,34 @@ --- title: Introduction -description: Run AI agents in sandboxes and communicate with them over WebSocket. +description: Run AI agents (Claude Code, OpenAI Agents, and more) in any sandbox, controlled from Python over WebSocket. --- -## What is RuntimeUse? +[RuntimeUse](https://github.com/getlark/runtimeuse) is an open-source runtime and client library for running AI agents inside isolated sandboxes and controlling them from Python over WebSocket. -RuntimeUse lets you run an AI agent inside any sandbox and communicate with it over WebSocket. It handles the runtime lifecycle for you: file downloads, pre-commands, artifact uploads, cancellation, and structured results. -It is made up of two parts: +RuntimeUse terminal -1. **`runtimeuse`**: the TypeScript runtime that runs inside the sandbox and exposes a WebSocket server. -2. **`runtimeuse-client`**: the Python client that connects from outside the sandbox and sends invocations. -Today, the recommended path is to run the runtime in the sandbox and use the Python client from your application code. + -## Built-in Agent Handlers -The runtime ships with two built-in handlers: +## Why RuntimeUse -- **`openai`** (default) — uses `@openai/agents` SDK -- **`claude`** — uses `@anthropic-ai/claude-agent-sdk` with Claude Code tools and `bypassPermissions` mode +- Your agent needs filesystem, CLI, or network access inside an isolated runtime. +- Your application should stay outside the sandbox while still controlling the run. +- You don't want to build infrastructure for interacting with your agent in sandbox. -Switch between them with `--agent openai` or `--agent claude`. +## Features -The Claude handler also requires the `claude` CLI to be installed in the sandbox, for example: +- **Task invocations**: send a prompt to any agent runtime and receive a result over WebSocket as text or typed JSON. +- **pre_agent_downloadables**: fetch code, repos, or data into the sandbox before the run starts. +- **Pre-commands**: run bash commands before the agent starts executing. +- **Artifact uploads**: move generated files out of the sandbox with a presigned URL handshake. +- **Streaming and cancellation**: receive progress updates and stop runs cleanly. +- **Secret-aware execution**: redact sensitive values before they leave the sandbox. -```bash -npm install -g @anthropic-ai/claude-code -``` -## Key Features - -- **Sandbox-agnostic** — works with any provider that can run `npx` and expose a port -- **Artifact management** — files written to the artifacts directory are automatically detected and uploaded through a presigned URL handshake with the client -- **Secret redaction** — secret values are recursively replaced before they leave the sandbox -- **Pre-commands** — run shell commands before agent invocation with automatic secret redaction and abort support - - - - - - diff --git a/docs/content/docs/meta.json b/docs/content/docs/meta.json new file mode 100644 index 0000000..15935df --- /dev/null +++ b/docs/content/docs/meta.json @@ -0,0 +1,8 @@ +{ + "pages": [ + "index", + "quickstart", + "python-client", + "agent-runtime" + ] +} \ No newline at end of file diff --git a/docs/content/docs/python-client.mdx b/docs/content/docs/python-client.mdx new file mode 100644 index 0000000..586c95d --- /dev/null +++ b/docs/content/docs/python-client.mdx @@ -0,0 +1,215 @@ +--- +title: Python Client +description: Connect to agent runtime in sandbox from Python. +--- + +The [Python client](https://pypi.org/project/runtimeuse-client/) is the control plane for RuntimeUse. It connects to the sandbox runtime, sends the invocation, and turns runtime messages into a single `QueryResult`. + +```bash +pip install runtimeuse-client +``` + +## Basic Query + +```python +import asyncio + +from runtimeuse_client import QueryOptions, RuntimeUseClient, TextResult + + +async def main() -> None: + client = RuntimeUseClient(ws_url="ws://localhost:8080") + + result = await client.query( + prompt="What is 2 + 2", + options=QueryOptions( + system_prompt="You are a helpful assistant.", + model="gpt-4.1", + ), + ) + + assert isinstance(result.data, TextResult) + print(result.data.text) + print(result.metadata) + + +asyncio.run(main()) +``` + +`query()` returns a `QueryResult` with: + +- `data`: either `TextResult` (`.text`) or `StructuredOutputResult` (`.structured_output`) +- `metadata`: execution metadata returned by the runtime (includes token usage when available) + +## Return Structured JSON + +Pass `output_format_json_schema_str` when your application needs machine-readable output instead of free-form text. The result will be a `StructuredOutputResult`. + +```python +import json + +from pydantic import BaseModel +from runtimeuse_client import StructuredOutputResult + + +class RepoStats(BaseModel): + file_count: int + char_count: int + + +result = await client.query( + prompt="Inspect the repository and return the total file count and character count as JSON.", + options=QueryOptions( + system_prompt="You are a helpful assistant.", + model="gpt-4.1", + output_format_json_schema_str=json.dumps( + { + "type": "json_schema", + "schema": RepoStats.model_json_schema(), + } + ), + ), +) + +assert isinstance(result.data, StructuredOutputResult) +stats = RepoStats.model_validate(result.data.structured_output) +print(stats) +``` + +## Download Files into the Sandbox + +Use `pre_agent_downloadables` to fetch a repository, zip archive, or any URL into the sandbox before the agent runs. This is the primary way to give the agent access to a codebase or dataset. + +```python +from runtimeuse_client import RuntimeEnvironmentDownloadableInterface + +result = await client.query( + prompt="Summarize the contents of this repository and list your favorite file.", + options=QueryOptions( + system_prompt="You are a helpful assistant.", + model="gpt-4.1", + pre_agent_downloadables=[ + RuntimeEnvironmentDownloadableInterface( + download_url="https://github.com/openai/codex/archive/refs/heads/main.zip", + working_dir="/runtimeuse", + ) + ], + ), +) +``` + +The runtime downloads and extracts the file before handing control to the agent. + +## Upload Artifacts + +When the runtime requests an artifact upload, return a presigned URL and content type from `on_artifact_upload_request`. Set `artifacts_dir` to tell the runtime which sandbox directory contains the files to upload - both options must be provided together. + +```python +from runtimeuse_client import ArtifactUploadResult + + +async def on_artifact_upload_request(request) -> ArtifactUploadResult: + presigned_url = await create_presigned_url(request.filename) + return ArtifactUploadResult( + presigned_url=presigned_url, + content_type="application/octet-stream", + ) + + +result = await client.query( + prompt="Generate a report and save it as report.txt.", + options=QueryOptions( + system_prompt="You are a helpful assistant.", + model="gpt-4.1", + artifacts_dir="/runtimeuse/output", + on_artifact_upload_request=on_artifact_upload_request, + ), +) +``` + +## Stream Assistant Messages + +Use `on_assistant_message` when you want intermediate progress while the run is still happening. + +```python +async def on_assistant_message(msg) -> None: + for block in msg.text_blocks: + print(f"[assistant] {block}") + + +result = await client.query( + prompt="Inspect this repository.", + options=QueryOptions( + system_prompt="You are a helpful assistant.", + model="gpt-4.1", + on_assistant_message=on_assistant_message, + ), +) +``` + +## Cancel a Run + +Call `client.abort()` from another coroutine to cancel an in-flight query. The client sends a cancel message to the runtime and `query()` raises `CancelledException`. + +```python +import asyncio +from runtimeuse_client import CancelledException + + +async def cancel_soon(client: RuntimeUseClient) -> None: + await asyncio.sleep(5) + client.abort() + + +try: + asyncio.create_task(cancel_soon(client)) + await client.query(prompt="Do the thing.", options=options) +except CancelledException: + print("Run was cancelled") +``` + +## Set a Timeout + +Use `timeout` (in seconds) to limit how long a query can run. If the limit is exceeded, `query()` raises `TimeoutError`. + +```python +result = await client.query( + prompt="Do the thing.", + options=QueryOptions( + system_prompt="You are a helpful assistant.", + model="gpt-4.1", + timeout=120, + ), +) +``` + +## Redact Secrets + +Pass `secrets_to_redact` to strip sensitive strings from any output or logs that leave the sandbox. + +```python +result = await client.query( + prompt="Check the API status.", + options=QueryOptions( + system_prompt="You are a helpful assistant.", + model="gpt-4.1", + secrets_to_redact=["sk-live-abc123", "my_db_password"], + ), +) +``` + +## Handle Errors + +`query()` raises `AgentRuntimeError` if the runtime sends back an error. The exception carries `.error` (the error message) and `.metadata`. + +```python +from runtimeuse_client import AgentRuntimeError + +try: + result = await client.query(prompt="Do the thing.", options=options) +except AgentRuntimeError as e: + print(f"Runtime error: {e.error}") + print(f"Metadata: {e.metadata}") +``` + + diff --git a/docs/content/docs/quickstart.mdx b/docs/content/docs/quickstart.mdx index 5c213d6..84274e5 100644 --- a/docs/content/docs/quickstart.mdx +++ b/docs/content/docs/quickstart.mdx @@ -1,42 +1,157 @@ --- title: Quickstart -description: Get the runtime running in a sandbox and connect to it from Python. +description: Start the runtime, connect from Python, and run your first prompt. --- -## Step 1: Start the Runtime in a Sandbox - -Inside any sandbox that can run `npx` and expose a port: +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; + +## 1. Start the Runtime + + + + ```bash + export OPENAI_API_KEY=your_openai_api_key + npx -y runtimeuse + ``` + + This starts the OpenAI agent on port `8080`. To use Claude instead: + + ```bash + npm install -g @anthropic-ai/claude-code + export ANTHROPIC_API_KEY=your_anthropic_api_key + npx -y runtimeuse --agent claude + ``` + + ```python + ws_url = "ws://localhost:8080" + ``` + + + ```bash + pip install e2b e2b-code-interpreter + ``` + + ```python + # Required env vars: E2B_API_KEY, ANTHROPIC_API_KEY + from e2b import Template, wait_for_port + from e2b_code_interpreter import Sandbox + + template = ( + Template() + .from_node_image("lts") + .set_workdir("/runtimeuse") + .npm_install(["@anthropic-ai/claude-code"], g=True) + .set_envs( + { + "ANTHROPIC_API_KEY": anthropic_api_key, + "IS_SANDBOX": "1", + "CLAUDE_SKIP_ROOT_CHECK": "1", + } + ) + .set_start_cmd("npx -y runtimeuse --agent claude", wait_for_port(8080)) + ) -```bash -npx -y runtimeuse -``` + sandbox = Sandbox.create(template="runtimeuse-quickstart-claude", api_key=e2b_api_key) + ws_url = f"wss://{sandbox.get_host(8080)}" + ``` + + Full example: [examples/e2b-quickstart.py](https://github.com/getlark/runtimeuse/blob/main/examples/e2b-quickstart.py) + + + ```bash + pip install daytona + ``` + + ```python + # Required env vars: DAYTONA_API_KEY, ANTHROPIC_API_KEY + from daytona import SessionExecuteRequest + + sandbox.process.create_session("runtimeuse") + sandbox.process.execute_session_command( + "runtimeuse", + SessionExecuteRequest( + command="npx -y runtimeuse --agent claude", + run_async=True, + ), + ) -This starts a WebSocket server on port 8080 using the OpenAI agent handler by default. + preview = sandbox.create_signed_preview_url(8080, expires_in_seconds=3600) + ws_url = _http_to_ws(preview.url) + ``` + + Full example: [examples/daytona-quickstart.py](https://github.com/getlark/runtimeuse/blob/main/examples/daytona-quickstart.py) + + + ```bash + pip install vercel python-dotenv + ``` + + ```python + # Required env vars: VERCEL_TOKEN, VERCEL_PROJECT_ID, VERCEL_TEAM_ID, ANTHROPIC_API_KEY + from vercel.sandbox import Sandbox + + sandbox = Sandbox.create( + runtime="node24", + ports=[8081], + env={ + "ANTHROPIC_API_KEY": anthropic_api_key, + "IS_SANDBOX": "1", + "CLAUDE_SKIP_ROOT_CHECK": "1", + }, + ) -To use Claude instead: + sandbox.run_command("sudo", ["dnf", "install", "-y", "unzip"]) + sandbox.run_command("npm", ["install", "-g", "@anthropic-ai/claude-code"]) + sandbox.run_command_detached( + "npx", + ["-y", "runtimeuse", "--agent", "claude", "--port", "8081"], + ) -```bash -npx -y runtimeuse --agent claude -``` + ws_url = _http_to_ws(sandbox.domain(8081)) + ``` + + Full example: [examples/vercel-quickstart.py](https://github.com/getlark/runtimeuse/blob/main/examples/vercel-quickstart.py) + + + ```bash + pip install modal + ``` + + ```python + # Required env vars: ANTHROPIC_API_KEY + # Authenticate with Modal: `modal token set` or set MODAL_TOKEN_ID + MODAL_TOKEN_SECRET + import modal + + sandbox = modal.Sandbox.create( + app=app, + image=image, + secrets=[secret], + workdir="/runtimeuse", + encrypted_ports=[8080], + timeout=600, + ) -The Claude handler requires the `claude` CLI to be installed in the sandbox, for example with: + sandbox.exec("npx", "-y", "runtimeuse", "--agent", "claude") + ws_url = _http_to_ws(sandbox.tunnels()[8080].url) + ``` -```bash -npm install -g @anthropic-ai/claude-code -``` + Full example: [examples/modal-quickstart.py](https://github.com/getlark/runtimeuse/blob/main/examples/modal-quickstart.py) + + -## Step 2: Install the Python Client +## 2. Install the Client ```bash pip install runtimeuse-client ``` -## Step 3: Connect from Python +## 3. Connect and Query -For local development, connect directly to the runtime's WebSocket URL: +Once you have a `ws_url`, the client flow is the same across providers: ```python import asyncio + from runtimeuse_client import ( QueryOptions, RuntimeEnvironmentDownloadableInterface, @@ -44,20 +159,19 @@ from runtimeuse_client import ( TextResult, ) -WORKDIR = "/runtimeuse" -async def main(): - client = RuntimeUseClient(ws_url="ws://localhost:8080") +async def main(ws_url: str) -> None: + client = RuntimeUseClient(ws_url=ws_url) result = await client.query( - prompt="Summarize the contents of the codex repository and list your favorite file in the repository.", + prompt="Summarize the contents of this repository and list your favorite file.", options=QueryOptions( system_prompt="You are a helpful assistant.", - model="gpt-4.1", + model="claude-sonnet-4-20250514", pre_agent_downloadables=[ RuntimeEnvironmentDownloadableInterface( download_url="https://github.com/openai/codex/archive/refs/heads/main.zip", - working_dir=WORKDIR, + working_dir="/runtimeuse", ) ], ), @@ -66,69 +180,6 @@ async def main(): assert isinstance(result.data, TextResult) print(result.data.text) -asyncio.run(main()) -``` - -## Step 4: Use a Sandbox URL in Production-Like Flows - -In a real sandbox integration, your sandbox provider gives you the runtime URL after starting the process inside the sandbox. The Python client then connects from outside the sandbox: - -```python -import asyncio -import json - -from pydantic import BaseModel - -from runtimeuse_client import ( - QueryOptions, - RuntimeEnvironmentDownloadableInterface, - RuntimeUseClient, - StructuredOutputResult, -) - -WORKDIR = "/runtimeuse" - - -class RepoStats(BaseModel): - file_count: int - char_count: int - -async def main(): - # Pseudocode: start the runtime inside your sandbox provider - sandbox = Sandbox.create() - sandbox.run("npx -y runtimeuse") - ws_url = sandbox.get_url(8080) - - client = RuntimeUseClient(ws_url=ws_url) - - result = await client.query( - prompt="Inspect the codex repository and return the total file count and total character count across all files as JSON.", - options=QueryOptions( - system_prompt="You are a helpful assistant.", - model="gpt-4.1", - pre_agent_downloadables=[ - RuntimeEnvironmentDownloadableInterface( - download_url="https://github.com/openai/codex/archive/refs/heads/main.zip", - working_dir=WORKDIR, - ) - ], - output_format_json_schema_str=json.dumps( - { - "type": "json_schema", - "schema": RepoStats.model_json_schema(), - } - ), - ), - ) - - assert isinstance(result.data, StructuredOutputResult) - stats = RepoStats.model_validate(result.data.structured_output) - print(stats.model_dump()) -asyncio.run(main()) +asyncio.run(main(ws_url)) ``` - -## Next Steps - -- See the [runtime package README](https://github.com/getlark/runtimeuse/tree/main/packages/runtimeuse) for runtime configuration and custom handlers. -- See the [Python client README](https://github.com/getlark/runtimeuse/tree/main/packages/runtimeuse-client-python) for structured output, artifact uploads, and cancellation. diff --git a/docs/public/terminal.svg b/docs/public/terminal.svg new file mode 100644 index 0000000..516f143 --- /dev/null +++ b/docs/public/terminal.svg @@ -0,0 +1,48 @@ + + + + + + + + + + $ + npx -y runtimeuse --agent=claude + + + + + ws://localhost:8080 + + + + + connected to client + + + + + received invocation message + + + + + spawning claude agent + + + + + received agent message, forwarding to client + + + + + received result, forwarding to client + + + + + agent runtime finished... + + From 9295b873b81665069e8d99b97afe79f3e51993f8 Mon Sep 17 00:00:00 2001 From: Vijit Dhingra Date: Tue, 17 Mar 2026 19:03:59 -0700 Subject: [PATCH 6/8] different headings --- docs/content/docs/index.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/docs/index.mdx b/docs/content/docs/index.mdx index a63f7ae..59ba9a0 100644 --- a/docs/content/docs/index.mdx +++ b/docs/content/docs/index.mdx @@ -16,13 +16,13 @@ description: Run AI agents (Claude Code, OpenAI Agents, and more) in any sandbox /> -## Why RuntimeUse +## When to use - Your agent needs filesystem, CLI, or network access inside an isolated runtime. - Your application should stay outside the sandbox while still controlling the run. - You don't want to build infrastructure for interacting with your agent in sandbox. -## Features +## What it handles - **Task invocations**: send a prompt to any agent runtime and receive a result over WebSocket as text or typed JSON. - **pre_agent_downloadables**: fetch code, repos, or data into the sandbox before the run starts. From 4bfb9fc1baf5d9dae8fdef499bcf3487f3a15e8a Mon Sep 17 00:00:00 2001 From: Vijit Dhingra Date: Tue, 17 Mar 2026 19:31:27 -0700 Subject: [PATCH 7/8] expand quickstart tabs with full sandbox setup, simplify example working dirs - Add image/secret setup to Daytona and Modal quickstart tabs - Default to Claude agent in Local tab - Remove WORKDIR constants from examples, use "." instead Made-with: Cursor --- docs/content/docs/quickstart.mdx | 58 +++++++++++++++++++++++++++----- examples/daytona-quickstart.py | 4 +-- examples/e2b-quickstart.py | 5 +-- examples/modal-quickstart.py | 4 +-- examples/vercel-quickstart.py | 3 +- 5 files changed, 54 insertions(+), 20 deletions(-) diff --git a/docs/content/docs/quickstart.mdx b/docs/content/docs/quickstart.mdx index 84274e5..f27ba74 100644 --- a/docs/content/docs/quickstart.mdx +++ b/docs/content/docs/quickstart.mdx @@ -9,19 +9,18 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; - ```bash - export OPENAI_API_KEY=your_openai_api_key - npx -y runtimeuse - ``` - - This starts the OpenAI agent on port `8080`. To use Claude instead: - ```bash npm install -g @anthropic-ai/claude-code export ANTHROPIC_API_KEY=your_anthropic_api_key npx -y runtimeuse --agent claude ``` + This starts the Claude Code agent on port `8080`. To use OpenAI agent instead: + ```bash + export OPENAI_API_KEY=your_openai_api_key + npx -y runtimeuse + ``` + ```python ws_url = "ws://localhost:8080" ``` @@ -64,7 +63,33 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; ```python # Required env vars: DAYTONA_API_KEY, ANTHROPIC_API_KEY - from daytona import SessionExecuteRequest + from daytona import ( + CreateSandboxFromImageParams, + Daytona, + DaytonaConfig, + Image, + SessionExecuteRequest, + ) + + image = Image.base("node:lts").run_commands( + "apt-get update && apt-get install -y unzip", + "npm install -g @anthropic-ai/claude-code", + ) + + daytona = Daytona(config=DaytonaConfig(api_key=daytona_api_key)) + + sandbox = daytona.create( + CreateSandboxFromImageParams( + image=image, + env_vars={ + "ANTHROPIC_API_KEY": anthropic_api_key, + "IS_SANDBOX": "1", + "CLAUDE_SKIP_ROOT_CHECK": "1", + }, + public=True, + ), + timeout=600, + ) sandbox.process.create_session("runtimeuse") sandbox.process.execute_session_command( @@ -122,6 +147,21 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; # Authenticate with Modal: `modal token set` or set MODAL_TOKEN_ID + MODAL_TOKEN_SECRET import modal + app = modal.App.lookup("runtimeuse-quickstart", create_if_missing=True) + + image = modal.Image.from_registry("node:lts").run_commands( + "apt-get update && apt-get install -y unzip", + "npm install -g @anthropic-ai/claude-code", + ) + + secret = modal.Secret.from_dict( + { + "ANTHROPIC_API_KEY": anthropic_api_key, + "IS_SANDBOX": "1", + "CLAUDE_SKIP_ROOT_CHECK": "1", + } + ) + sandbox = modal.Sandbox.create( app=app, image=image, @@ -167,7 +207,7 @@ async def main(ws_url: str) -> None: prompt="Summarize the contents of this repository and list your favorite file.", options=QueryOptions( system_prompt="You are a helpful assistant.", - model="claude-sonnet-4-20250514", + model="claude-sonnet-4-20250514", # gpt-5.4 for openai pre_agent_downloadables=[ RuntimeEnvironmentDownloadableInterface( download_url="https://github.com/openai/codex/archive/refs/heads/main.zip", diff --git a/examples/daytona-quickstart.py b/examples/daytona-quickstart.py index affc3f8..58cde2d 100644 --- a/examples/daytona-quickstart.py +++ b/examples/daytona-quickstart.py @@ -34,7 +34,7 @@ TextResult, ) -WORKDIR = "/home/daytona" + _SERVER_READY_SIGNAL = "RuntimeUse server listening on port" _SERVER_STARTUP_TIMEOUT_S = 120 _SESSION_ID = "runtimeuse" @@ -161,7 +161,7 @@ async def on_message(msg: AssistantMessageInterface) -> None: pre_agent_downloadables=[ RuntimeEnvironmentDownloadableInterface( download_url="https://github.com/openai/codex/archive/refs/heads/main.zip", - working_dir=WORKDIR, + working_dir=".", ) ], ), diff --git a/examples/e2b-quickstart.py b/examples/e2b-quickstart.py index 36073d9..d25416b 100644 --- a/examples/e2b-quickstart.py +++ b/examples/e2b-quickstart.py @@ -28,8 +28,6 @@ TextResult, ) -WORKDIR = "/runtimeuse" - def _get_env_or_fail(name: str) -> str: value = os.environ.get(name) @@ -45,7 +43,6 @@ def _create_template_with_alias(alias: str): template = ( Template() .from_node_image("lts") - .set_workdir(WORKDIR) .npm_install(["@anthropic-ai/claude-code"], g=True) .set_envs( { @@ -107,7 +104,7 @@ async def on_message(msg: AssistantMessageInterface) -> None: pre_agent_downloadables=[ RuntimeEnvironmentDownloadableInterface( download_url="https://github.com/openai/codex/archive/refs/heads/main.zip", - working_dir=WORKDIR, + working_dir=".", ) ], ), diff --git a/examples/modal-quickstart.py b/examples/modal-quickstart.py index 7f7fbb8..da4c128 100644 --- a/examples/modal-quickstart.py +++ b/examples/modal-quickstart.py @@ -33,7 +33,6 @@ TextResult, ) -WORKDIR = "/runtimeuse" _SERVER_READY_SIGNAL = "RuntimeUse server listening on port" _SERVER_STARTUP_TIMEOUT_S = 120 @@ -130,7 +129,6 @@ def create_sandbox() -> tuple[modal.Sandbox, str]: app=app, image=image, secrets=[secret], - workdir=WORKDIR, encrypted_ports=[8080], timeout=600, ) @@ -176,7 +174,7 @@ async def on_message(msg: AssistantMessageInterface) -> None: pre_agent_downloadables=[ RuntimeEnvironmentDownloadableInterface( download_url="https://github.com/openai/codex/archive/refs/heads/main.zip", - working_dir=WORKDIR, + working_dir=".", ) ], ), diff --git a/examples/vercel-quickstart.py b/examples/vercel-quickstart.py index b390b17..940428e 100644 --- a/examples/vercel-quickstart.py +++ b/examples/vercel-quickstart.py @@ -42,7 +42,6 @@ StructuredOutputResult, ) -WORKDIR = "/vercel/sandbox" _SERVER_READY_SIGNAL = "RuntimeUse server listening on port" @@ -129,7 +128,7 @@ async def on_message(msg: AssistantMessageInterface) -> None: pre_agent_downloadables=[ RuntimeEnvironmentDownloadableInterface( download_url="https://github.com/openai/codex/archive/refs/heads/main.zip", - working_dir=WORKDIR, + working_dir=".", ) ], output_format_json_schema_str=json.dumps( From 6a6e20f5dff5d8194b8f543d80442458b5b61d07 Mon Sep 17 00:00:00 2001 From: Vijit Dhingra Date: Tue, 17 Mar 2026 19:32:33 -0700 Subject: [PATCH 8/8] bump runtimeuse and runtimeuse-client to 0.6.0 Made-with: Cursor --- packages/runtimeuse-client-python/pyproject.toml | 2 +- packages/runtimeuse/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runtimeuse-client-python/pyproject.toml b/packages/runtimeuse-client-python/pyproject.toml index 9423982..3ca787a 100644 --- a/packages/runtimeuse-client-python/pyproject.toml +++ b/packages/runtimeuse-client-python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "runtimeuse-client" -version = "0.5.0" +version = "0.6.0" description = "Client library for AI agent runtime communication over WebSocket" readme = "README.md" license = {"text" = "FSL"} diff --git a/packages/runtimeuse/package.json b/packages/runtimeuse/package.json index 9a39cf6..3c90a85 100644 --- a/packages/runtimeuse/package.json +++ b/packages/runtimeuse/package.json @@ -1,6 +1,6 @@ { "name": "runtimeuse", - "version": "0.5.0", + "version": "0.6.0", "description": "AI agent runtime with WebSocket protocol, artifact handling, and secret management", "license": "FSL", "type": "module",