Paddock

June 20, 2026 · View on GitHub

Unified Environment Interaction for Agentic Rollouts

← Back to Main README · Overview · Design Principles · Interface · Blackbox Mode · Whitebox Mode · Factory · Comparison

📖 Overview

The Paddock is a single-class abstraction that manages all environment interaction during rollouts. It handles sandbox lease creation, agent/tool calls, pause/resume lifecycle, and cleanup — providing a unified interface whether you're running whitebox Python agents or blackbox HTTP agents. The paddock is the coordination layer that ties together the proxy (token recording), sandbox (isolation), and agent (decision-making) into a coherent rollout lifecycle.

Tip

Think of the Paddock as the "what-to-do" layer, while the Sandbox layer handles "where-it-runs". The paddock doesn't care whether the agent runs in a local bubblewrap namespace or a remote E2B cloud sandbox — it just needs a SandboxLease and, for blackbox rollouts, a blackbox service endpoint.

Rollout Generate Function


     Paddock
        ├── init(traj_id)
        ├── register_agent / call_agent / execute_cmd   (blackbox)
        │   or tool_call                                (whitebox)
        ├── pause / resume                              (blackbox)
        └── terminate(traj_id)


     SandboxProvider
        └── create / terminate sandbox leases

The paddock pattern ensures that every rollout follows the same lifecycle contract (init → interact → terminate), regardless of the agent type or sandbox backend. This makes it safe to swap agent modes and sandbox providers independently — the two axes are truly orthogonal.

✨ Key Design Principles

  • Single Interface — One paddock class per rollout — blackbox or whitebox, never both. This prevents accidental mode mixing and ensures clean lifecycle management. The generate function creates the paddock once and reuses it across all samples in the batch.
  • Lifecycle Management — Every trajectory follows init(traj_id) → interact → terminate(traj_id). The paddock guarantees cleanup even on failures, preventing sandbox leaks. init asks the provider to create() a SandboxLease, and terminate returns that lease through provider terminate().
  • Pluggable Backends — The same paddock code works with bubblewrap or E2B sandboxes. The paddock receives a SandboxProvider at creation time and delegates all placement decisions to it. Swap providers via a single environment variable.
  • State Isolation — Each trajectory maintains its own SandboxState tracking sandbox slot assignment, BlackboxServer endpoint URL, session and instance IDs, current lifecycle phase, and error state. No cross-trajectory contamination.
  • Factory Patterncreate_paddock_from_env() reads DRESSAGE_PADDOCK_MODE, DRESSAGE_SANDBOX_PROVIDER, and other environment variables to wire up the correct paddock implementation with the correct sandbox provider. Zero configuration in application code.

🧱 Interface Hierarchy

The paddock system uses a clean inheritance hierarchy that separates the lifecycle contract from the interaction semantics:

Paddock (abstract base)
├── init(traj_id)          → create sandbox lease, initialize state
└── terminate(traj_id)     → terminate or reclaim sandbox lease

BlackboxPaddock (extends Paddock)
├── register_agent(...)    → start BlackboxServer + backend agent
├── call_agent(...)        → send task prompt, wait for completion
├── execute_cmd(...)       → run shell commands in sandbox
├── pause(...)             → pause generation for weight update
└── resume(...)            → resume generation after update

WhiteboxPaddock (extends Paddock)
└── tool_call(traj_id, tool_id, tool_args) → execute tool in sandbox, return (text, metadata)

Note

Source: dressage/paddock/interface.py. The base Paddock class defines the lifecycle contract; BlackboxPaddock and WhiteboxPaddock add the interaction methods specific to each agent paradigm.

🤖 Blackbox Mode

For HTTP-based agents running inside sandboxes (e.g., opencode, openclaw). The blackbox paddock manages the full lifecycle: sandbox acquisition, BlackboxServer startup, agent process management, turn execution, pause/resume for weight updates, and cleanup.

Complete Lifecycle

BlackboxAgentPaddock

        ├── init(traj_id)         → build SandboxSpec with blackbox service
        │                         → provider.create(...) returns SandboxLease
        │                         → resolve blackbox endpoint + initialize SandboxState
        ├── register_agent(...)   → start BlackboxServer inside sandbox
        │                         → spawn backend agent process (opencode openclaw)
        │                         → configure in-process LLM proxy
        ├── execute_cmd(...)      → run setup commands (clone repos, install deps)
        │                         → optional before/after hooks per sample
        ├── call_agent(...)       → send task prompt to agent
        │                         → wait for agent completion or timeout
        │                         → handle errors, desync detection
        ├── pause / resume        → coordinate with proxy for weight updates
        │                         → GenerationController abort/resume
        └── terminate(traj_id)    → abort active session
                                  → stop backend process
                                  → provider.terminate(lease)

Step Details

MethodWhat It Does
init(traj_id)Builds a SandboxSpec containing a blackbox SandboxServiceSpec, calls provider create(), resolves the blackbox endpoint from lease.endpoints or get_public_url(), then initializes per-trajectory SandboxState. Unless DRESSAGE_BLACKBOX_SKIP_HEALTHCHECK is set, the endpoint is health-checked before use.
register_agent(...)Sends POST /v1/rollout/register to the BlackboxServer inside the sandbox. The server spawns the backend agent process, starts the in-process LLM proxy, and configures session routing. Idempotent — re-registration with same params is a no-op.
execute_cmd(...)Runs optional shell commands inside the sandbox. Used for per-sample setup: cloning repositories, installing dependencies, creating workspace directories. Commands are specified via metadata["blackbox_execute_cmds"]; parsed before_agent / after_agent results are accumulated in metadata["execute_cmds"].
call_agent(...)Sends the task prompt to the agent via POST /v1/sessions/{id}/messages. Blocks until the agent completes or times out. The agent makes LLM calls through BlackboxServer's proxy, which routes through the Dressage proxy for recording.
pause / resumeCoordinates with the proxy's GenerationController for weight updates. pause signals all active generation to abort at token boundaries; resume re-enables generation after the weight update completes.
terminate(traj_id)Cleanly shuts down the trajectory lease through provider terminate(). Guaranteed to run even on errors via try/finally.
State Tracking

Each trajectory maintains a SandboxState object that tracks:

  • Sandbox lease — The provider-created lease (bwrap namespace or E2B sandbox)
  • BlackboxServer URL — Endpoint for the server running inside the sandbox
  • Session ID — Active session identifier for turn attribution
  • Instance ID — Prompt instance identifier for gradient scaling
  • Lifecycle phase — Current state: uninitialized → initialized → registered → active → terminated
  • Error state — Captures failure details for logging and abort handling
Singleton Lifecycle

The blackbox dispatch hook holds a process-lifetime paddock singleton — it is created once at first use and reused across all samples in all batches. Each sample goes through init → interact → terminate, but the paddock instance itself persists. This amortizes the cost of sandbox provider initialization and Ray actor discovery.

The singleton is created by create_paddock_from_env() on first call and cached. Subsequent calls return the same instance. This pattern is safe because the paddock is stateless between trajectories — all per-trajectory state lives in SandboxState.

Customization Hooks

FeatureDescription
metadata["blackbox_execute_cmds"]Per-sample shell commands to run before/after agent calls. Supports only before_agent and after_agent; each stage is a list of objects with name, cmd, boolean required, and optional timeout.
DRESSAGE_PADDOCK_CLASSOverride the default paddock class with module.Class for custom lifecycle logic.
DRESSAGE_BLACKBOX_TYPE / metadata["blackbox_type"]Select the backend agent type: opencode (default, code-editing), openclaw (OpenClaw gateway). Per-sample metadata can override the environment default.
DRESSAGE_BLACKBOX_MAX_STEPSPositive int forwarded to backend_options.proxy.max_steps; 0 disables the backend proxy step limit.
DRESSAGE_BLACKBOX_COMPACT_THRESHOLDPositive int no greater than the context window; controls backend compaction reserve sizing.

Warning

blackbox_dispatch rejects DRESSAGE_PADDOCK_MODE=whitebox at startup — use whitebox_agent generate function instead. This prevents accidental mode mismatches that would silently produce incorrect trajectories.

🔮 Whitebox Mode

For Python agents that call tools directly through the paddock. The whitebox paddock is simpler — it only needs sandbox access for shell/file operations, not a full BlackboxServer.

Lifecycle

WhiteboxToolPaddock

        ├── init(traj_id)              → provider.create(...) sandbox lease
        │                              → setup working directory
        ├── tool_call(traj_id, ...)    → execute tool in sandbox, return (text, metadata)
        │       ├── shell.exec         → run shell commands
        │       ├── file.read          → read file contents
        │       └── file.write         → write file contents
        │       (repeat as needed for multi-turn agent loop)
        └── terminate(traj_id)         → provider.terminate(lease)

Available Tools

The whitebox paddock exposes three tools that Python agents can call during rollout:

ToolArgumentsDescription
shell.exec{"cmd": "..."}Execute a shell command inside the sandbox. Returns (text, metadata), where text is stdout or stderr. Supports timeouts.
file.read{"path": "..."}Read file contents from the sandbox filesystem. Returns (text, metadata).
file.write{"path": "...", "content": "..."}Write content to a file in the sandbox. Returns (text, metadata), with empty text and write metadata.

Agent Variants

Two whitebox agent base classes are available, depending on whether sandbox access is needed:

ClassSandbox?Description
WhiteboxAgentNo sandboxPure proxy agent for lightweight Python tools. Agent calls self.chat(...) to interact with the LLM via proxy. Tools are implemented as Python functions (API calls, retrieval, computation). No sandbox isolation needed.
PaddockWhiteboxAgentWith sandboxExtends WhiteboxAgent with sandbox lifecycle. Agent unpacks text, metadata = await self.paddock.tool_call(self.session_id, tool_id, args) to execute shell commands and file operations inside an isolated sandbox. The paddock manages init/terminate automatically.

Tip

PaddockWhiteboxAgent wraps the init → interact → terminate lifecycle inside the agent class itself, so the rollout hook does not need to manage sandbox state manually. The agent's rollout() method can freely call self.paddock.tool_call(...) without worrying about cleanup.

🏭 Factory

The paddock is created from environment variables using create_paddock_from_env(). This factory reads the configuration and wires up the correct paddock implementation with the correct sandbox provider — zero boilerplate in application code.

from dressage.paddock.factory import create_paddock_from_env

paddock = create_paddock_from_env()
# Returns BlackboxAgentPaddock or WhiteboxToolPaddock
# with the configured SandboxProvider already attached

Configuration

VariableValuesDescription
DRESSAGE_PADDOCK_MODEblackbox | whiteboxAgent interaction paradigm — determines which paddock subclass is created
DRESSAGE_PADDOCK_CLASSmodule.ClassCustom paddock class override — use for specialized lifecycle logic
DRESSAGE_SANDBOX_PROVIDERlocal_bwrap | e2bWhere sandboxes run — determines which SandboxProvider is injected
DRESSAGE_BLACKBOX_TYPEopencode | openclawBackend agent type (blackbox mode only; defaults to opencode)
DRESSAGE_BLACKBOX_MAX_STEPSintPositive int forwarded to backend_options.proxy.max_steps; 0 disables the backend proxy step limit.
DRESSAGE_BLACKBOX_COMPACT_THRESHOLDintPositive int no greater than the context window; controls backend compaction reserve sizing.
DRESSAGE_BLACKBOX_PORTintBlackbox service port requested from the provider.
DRESSAGE_LOCAL_BWRAP_BLACKBOX_PORTintLocal bwrap-specific blackbox service port override.
DRESSAGE_BLACKBOX_SKIP_HEALTHCHECK0 | 1Skip endpoint health checks during blackbox init.
DRESSAGE_PADDOCK_TERMINATE_TIMEOUT_SECfloatBest-effort termination timeout.
DRESSAGE_LOCAL_BWRAP_POOL_MODEblackbox | command_onlyBwrap pool mode — must match paddock mode

Caution

The factory validates that paddock mode and sandbox pool mode are compatible. Using a blackbox paddock with a command_only pool (or a whitebox paddock with a blackbox pool) raises a clear error before any sandbox lease is created.

Whitebox vs Blackbox Mode Comparison

DimensionWhiteboxBlackbox
LLM CallerDressage Python agent via self.chat(...)External agent process (opencode, openclaw)
ToolsPython functions or paddock.tool_call(...)Agent's built-in tools inside sandbox
Paddock Modewhiteboxblackbox
Generate HookWhiteboxAgent or PaddockWhiteboxAgent subclassblackbox_dispatch.generate
Sandbox Needscommand_only pool — shell/file ops onlyblackbox pool — full BlackboxServer endpoint
Proxy IntegrationAgent calls proxy directly via ProxyClientBlackboxServer's LLM proxy routes through Dressage proxy
ComplexitySimpler — more control over agent logicMore flexible — supports real-world agent frameworks
Best ForCustom tools, retrieval, API calls, lightweight envsCode-editing agents, complex environments, production agents
Whitebox Flow                           Blackbox Flow
──────────────                          ──────────────
WhiteboxAgent                           BlackboxServer (in sandbox)
  └─ self.chat(...)                       └─ in-process LLM proxy
     └─ proxy.chat_completions               └─ proxy.chat_completions
  └─ self.paddock.tool_call(...)          └─ agent tools (shell, files, etc.)
     └─ sandbox shell/file ops               └─ agent-internal execution

📁 Package Structure

dressage/paddock/
├── interface.py           # Paddock, BlackboxPaddock, WhiteboxPaddock abstract interfaces
├── blackbox/
│   ├── __init__.py
│   ├── client.py          # BlackboxServer HTTP client
│   ├── paddock.py         # BlackboxAgentPaddock implementation
│   └── common/
│       ├── command.py     # Command execution helpers
│       ├── defaults.py    # Default configuration values
│       ├── http_retry.py  # HTTP retry with backoff
│       ├── state.py       # SandboxState tracking
│       └── utils.py       # Shared utilities
├── whitebox/
│   ├── __init__.py
│   ├── paddock.py         # WhiteboxToolPaddock implementation
│   └── tools.py           # Tool definitions (shell.exec, file.read, file.write)
└── factory.py             # create_paddock_from_env()

🔗 Integration Points

ComponentRelationship
ProxyPaddock coordinates proxy sessions — creates session via ProxyClient, finalizes on trajectory completion
SandboxPaddock uses provider create() / terminate() for sandbox lease management
BlackboxServerBlackbox paddock drives the full BlackboxServer lifecycle: register, call, pause, resume, abort
RolloutGenerate hooks hold a paddock singleton and call init/terminate per trajectory
RecipesRecipe agents use PaddockWhiteboxAgent for sandbox-backed tool execution

← Proxy · Back to Main README · Next: Sandbox →