Skip to content

Reference

Python SDK

Every public symbol of the drums package (0.3.0), generated from the SDK itself.

drums is a small client of the HTTP API, standard library only, for Python 3.9 and later. Signatures and docstrings on this page are generated from the SDK source by scripts/docs/gen-sdk-reference.py; the notes, raises and examples are added by hand and checked against those signatures.

Install

Note

Install with pip install drums-sdk (PyPI project drums-sdk, version 0.3.0). The import name is drums; the package also installs the drums command (drums version, drums mcp).

Configuration

Environment variableUsed for
DRUMS_URLBase URL; https://beta.drums.run/gw for Drums Cloud. Read when drums is imported. Falls back to DRUMSD_URL, then http://127.0.0.1:7070 (a local drumsd)
DRUMS_TOKENYour API key, sent as Authorization: Bearer
DRUMS_ALLOW_INSECURE_HTTP1 to allow sending the key over plain http:// to a host that is not loopback (off by default)

Both URL and key can be passed instead: drums.connect(url, token).

Transport rules (0.3.0). The key is sent over https://, or over plain http:// to loopback only (localhost, 127.0.0.0/8, ::1, a local drumsd). Any other http:// URL raises InsecureTransport before a request is made; opt in with connect(..., allow_insecure_http=True) or the variable above. A redirect is followed only to the same origin (scheme, host and port), at most drums.MAX_REDIRECTS (5) hops; anything else raises RedirectRefused, so the key is never re-sent to another origin. Neither message carries the key.

Patterns

  • Clean up with with. with drums.connect() as client: closes every desktop the client created when the block ends; so do process exit and SIGTERM/SIGHUP. with client.create(...) as env: and with env.fork(n) as kids: close those desktops.
  • Retries. GET requests are retried while the server is unreachable or answers 503, for up to retry_s (60 s). POST and DELETE are never retried: after a lost reply the outcome is unknown, so call env.refresh() or client.envs() before trying again.
  • Rewards. verify() raises InvalidEpisode by default instead of returning a reward from an invalid episode. With strict=False, check reward.valid before using reward.score.
  • Timeouts. Requests time out after 900 s by default (timeout=); the gateway allows up to 950 s.

HTTP status to exception

The SDK picks the exception class from the HTTP status, then refines it by the reply's code (not_quiescent, checkpoint_stale, task_unsupported). A 403 (forbidden, quota_exceeded, budget_exceeded) is a plain DrumsError with status == 403; read e.body["code"].

Module functions

drums.connect

drums.connect(
    url: str | None = None,
    token: str | None = None,
    *,
    timeout: float = 900.0,
    retry_s: float = 60.0,
    check: bool = True,
    close_on_exit: bool = True,
    handle_signals: bool = True,
    allow_insecure_http: bool | None = None,
) -> Client

Open a client for the gateway (or a drumsd). Use it as a context manager so the desktops it created are closed at the end.

A client for one drumsd. url/token default to $DRUMS_URL / $DRUMS_TOKEN. With check=True the token is tried at once (AuthError / DaemonUnavailable here rather than on the first create). A token is only sent over https, or over plain http to localhost / 127.0.0.0/8 / ::1; any other http URL raises InsecureTransport before a request unless allow_insecure_http=True (or $DRUMS_ALLOW_INSECURE_HTTP=1). close_on_exit=True: the envs this client created and did not close are closed when the process exits (atexit) or when the client is used as a context manager and the block ends. handle_signals=True: SIGTERM and SIGHUP exit through that cleanup too, when their handler is still the default one (never replaces a handler you installed; main thread only). A kill -9 cannot be caught: the server-side leases reap those envs.

ParameterDefaultMeaning
urlstr | NoneNoneBase URL. Default $DRUMS_URL (then $DRUMSD_URL, then http://127.0.0.1:7070), read when drums is imported. For Drums Cloud: https://beta.drums.run/gw.
tokenstr | NoneNoneAPI key. Default $DRUMS_TOKEN.
timeoutfloat900.0Seconds per request (900 by default: a create, fork or verify can take a while).
retry_sfloat60.0How long GET requests are retried while the server is unreachable or answers 503. Other methods are never retried.
checkboolTrueCall health() now, so a bad key raises AuthError here rather than on the first create.
close_on_exitboolTrueClose the desktops this client created when the process exits or the with block ends.
handle_signalsboolTrueTurn SIGTERM and SIGHUP into a normal exit so that cleanup runs (only over default handlers, main thread only).
allow_insecure_httpbool | NoneNoneSend the key over plain http:// to a host that is not loopback. Off by default: such a URL raises InsecureTransport before any request. $DRUMS_ALLOW_INSECURE_HTTP=1 is the same opt-in. https:// URLs and localhost, 127.0.0.0/8, ::1 never need it.
Returns
Client
Raises
  • AuthError (401) with check=True and a bad key
  • DaemonUnavailable when the URL does not answer
  • InsecureTransport for a plain-http URL that is not loopback, unless opted in
HTTP
GET /v1/health (with check=True)
Example
import drums

with drums.connect() as client:          # DRUMS_URL, DRUMS_TOKEN from the environment
    print(client.health()["version"])

drums.env

drums.env(
    task: dict | None = None,
    *,
    from_state: str | None = None,
    checkpoint: str = 'osworld-ubuntu',
    url: str = 'http://127.0.0.1:7070',
    token: str | None = None,
) -> Env

The 0.1 entry point, kept for old code. Prefer drums.connect().create().

0.1 entry point: a new env (from_state wins over checkpoint, default the installed image "osworld-ubuntu"; before 0.2.4 the default was "clean", which no install has). Prefer drums.connect(...).create(...).

ParameterDefaultMeaning
taskdict | NoneNoneOSWorld task JSON.
from_statestr | NoneNoneSaved state to start from (wins over checkpoint).
checkpointstr'osworld-ubuntu'Base image name.
urlstr'http://127.0.0.1:7070'Base URL.
tokenstr | NoneNoneAPI key.
Returns
Env (not owned by a client: close it yourself)
HTTP
POST /v1/slots

drums.check_transport

drums.check_transport(
    url: str,
    token: str | None,
    allow_insecure_http: bool | None = None,
) -> None

The rule the client applies before every request: a key travels over https, or over plain http to loopback only.

Refuse (InsecureTransport) to pair token with a plain-HTTP url whose host is not loopback. No token, an https URL, localhost / 127.0.0.0/8 / ::1, or the opt-in (allow_insecure_http=True, else $DRUMS_ALLOW_INSECURE_HTTP=1) pass. Runs before any request; the error names the host, never the token.

ParameterDefaultMeaning
urlstrrequiredThe base URL to check.
tokenstr | NonerequiredThe key that would be sent; None passes (nothing to protect).
allow_insecure_httpbool | NoneNoneThe opt-in; None reads $DRUMS_ALLOW_INSECURE_HTTP.
Returns
None
Raises
  • InsecureTransport when token is set, the scheme is http and the host is not localhost, 127.0.0.0/8 or ::1; the message names the host and the opt-ins, never the key

drums.Client

class Client

A connection to one gateway or drumsd. Create it with drums.connect().

A connection to one drumsd (use drums.connect). Creates envs, lists them, and reads health and metrics.

Attributes: url, timeout, retry_s, token, allow_insecure_http, close_on_exit

Client.owned_envs

Client.owned_envs() -> list

Ids of the envs this client created and has not closed.

Returns
list[str] of computer ids this client created and has not closed.

Client.close

Client.close(timeout: float = 30.0) -> list

Close every env this client created and has not closed (in parallel). Envs that are already gone are skipped. Returns [(env_id, error or None)]. Saved states are not deleted here (their leases, or fork(keep_state=False), or client.delete_state, take care of them).

ParameterDefaultMeaning
timeoutfloat30.0Seconds per close request.
Returns
list[tuple[str, Exception | None]]
HTTP
DELETE /v1/slots/{id} for each owned computer

Client.call

Client.call(
    method: str,
    path: str,
    body=None,
    raw: bool = False,
    timeout: float | None = None,
    headers: dict | None = None,
)

One raw request. Every other method is built on it; use it for routes the SDK has no method for.

One request. GETs are retried while drumsd is unreachable (up to retry_s: a restarting drumsd comes back and reconciles); other methods are never retried, since their outcome is unknown after a dropped connection. headers are added to the request (e.g. Idempotency-Key). A redirect is followed only to the same origin (scheme, host, port), at most MAX_REDIRECTS hops; any other redirect raises RedirectRefused unfollowed.

ParameterDefaultMeaning
methodstrrequiredGET, POST or DELETE.
pathstrrequiredPath under the base URL, e.g. /v1/checkpoints/<id>.
bodyNoneJSON body (sent as Content-Type: application/json).
rawboolFalseReturn the response bytes instead of parsed JSON.
timeoutfloat | NoneNoneSeconds for this request (default: the client's).
headersdict | NoneNoneExtra request headers, e.g. {"Idempotency-Key": ...} for POST /v1/runs.
Returns
Parsed JSON, or bytes with raw=True.
Raises
  • A DrumsError subclass chosen by HTTP status and code (see Errors)
  • RedirectRefused when the server redirects to another origin (scheme, host or port) or more than drums.MAX_REDIRECTS (5) times: the key is never re-sent elsewhere

Client.health

Client.health() -> dict
Returns
dict: version, launchable images, limits, lease defaults, capacity.
HTTP
GET /v1/health

Client.metrics

Client.metrics() -> dict

Aggregate metrics of the current window: op latency (create/reset/fork/step/verify/...), episodes by verdict, failures and retries by cause, rewards, and cost (host price x wall time, per valid episode / verified reward). The cost counts the whole window's wall time, idle time included: the window starts when drumsd started or at the last reset_metrics(), so call reset_metrics() at the start of a run to measure that run.

Returns
dict of the current window's metrics (measured compute cost, not billed).
HTTP
GET /v1/metrics

Client.reset_metrics

Client.reset_metrics() -> dict

Close the current metrics window (its ledger goes to drumsd's event log) and start a new one.

Returns
dict
HTTP
POST /v1/metrics/reset

Client.images

Client.images() -> list

What envs can be created from: installed images and saved states (API: GET /v1/checkpoints).

Returns
list[dict] of checkpoint records: base images and your saved states.
HTTP
GET /v1/checkpoints

Client.delete_state

Client.delete_state(state) -> dict

Delete a saved state (from save() or fork(); API: checkpoint): its template server stops and its images are freed. Refused (Conflict) while an env made from it still exists. drumsd also deletes a state by itself when its lease runs out (default: 30 min with no env made from it) or, for fork(keep_state=False), when its last env closes.

ParameterDefaultMeaning
staterequiredA State (or its id string) from save() or fork().
Returns
dict {id, deleted, freed_bytes}
Raises
  • Conflict while a computer made from it exists
  • NotFound
HTTP
DELETE /v1/checkpoints/{id}

Client.tasks

Client.tasks(domain: str | None = None) -> list
ParameterDefaultMeaning
domainstr | NoneNoneFilter by OSWorld domain, e.g. os, chrome, libreoffice_calc.
Returns
list[dict] of {id, domain, instruction, supported, unsupported_reason}.
HTTP
GET /v1/tasks
Example
with drums.connect() as client:
    runnable = [t for t in client.tasks(domain="os") if t["supported"]]
    print(len(runnable), runnable[0]["id"])

Client.task

Client.task(task_id: str) -> dict

An OSWorld task JSON from the host's pinned checkout, by id or unique prefix (>= 8 characters).

ParameterDefaultMeaning
task_idstrrequiredTask id or a unique prefix of at least 8 characters.
Returns
dict: the OSWorld task JSON.
Raises
  • NotFound
HTTP
GET /v1/tasks/{id}

Client.envs

Client.envs() -> list

The live envs on the host (drumsd >= 0.4 lists only those; older ones list every env, so destroyed ones are dropped here). These Env objects are not owned by this client: close_on_exit does not close them.

Returns
list[Env] of your live computers on this host (not owned by this client).
HTTP
GET /v1/slots

Client.env_records

Client.env_records(include_destroyed: bool = False, page_size: int = 500)

Every env record, live ones only or with the destroyed ones (their records are kept), as a generator over pages of page_size (GET /v1/slots?limit=&after=&include_destroyed=). An older drumsd answers with one list.

ParameterDefaultMeaning
include_destroyedboolFalseAlso yield closed computers.
page_sizeint500Records per request (1..5000).
Returns
A generator of computer record dicts.
HTTP
GET /v1/slots?limit=&after=&include_destroyed=

Client.runs

Client.runs(kind: str | None = None, status: str | None = None, limit: int = 50) -> dict

The workspace's runs, newest first. Gateway only: a plain drumsd answers 404.

The workspace's runs, newest first (GET /v1/runs): {"items": [run rows], "next_cursor"}. kind filters (e.g. "evaluation"), status too (queued, running, completed, failed, cancelled).

ParameterDefaultMeaning
kindstr | NoneNoneevaluation or scripted_test.
statusstr | NoneNoneA run status (queued, running, completed, failed, cancelled, ...).
limitint501 to 200.
Returns
dict: {items, next_cursor}
HTTP
GET /v1/runs

Client.create_run

Client.create_run(spec: dict, idempotency_key: str | None = None) -> dict

Queue a run from a RunSpec, the document the console's New run form submits. Needs runs:write.

Submit a RunSpec (POST /v1/runs): {"run_id", "operation_id", "status", "created"}. With idempotency_key (the Idempotency-Key header) the same key with the same spec returns the same run.

ParameterDefaultMeaning
specdictrequiredThe RunSpec (see the HTTP reference for its fields).
idempotency_keystr | NoneNoneSent as Idempotency-Key: the same key with the same spec returns the same run instead of a new one.
Returns
dict: {run_id, operation_id, status, created}
Raises
  • DrumsError (403 quota_exceeded / budget_exceeded) when the execution or budget exceeds the workspace limits
  • Conflict (409 wrong_state) when the Idempotency-Key was used with a different body
HTTP
POST /v1/runs

Client.run

Client.run(run_id: str) -> dict

One run (GET /v1/runs/{id}): its row, the attempts summary and the last events.

ParameterDefaultMeaning
run_idstrrequiredA run id of your workspace; another workspace's is NotFound.
Returns
dict: the run, attempts and events
HTTP
GET /v1/runs/{id}

Client.api_keys

Client.api_keys() -> dict

Key metadata only; a key's secret is never returned after it was created.

The workspace's API keys, metadata only (GET /v1/api-keys): {"items": [{id, name, prefix, scopes, ...}]}.

Returns
dict: {items: [{id, name, prefix, scopes, ...}]}
HTTP
GET /v1/api-keys

Client.create_api_key

Client.create_api_key(
    name: str,
    scopes: list,
    expires_in_days: int | None = None,
) -> dict

Mint a key owned by this client's key owner. Owner and admin keys only; the scopes cannot exceed this client's own.

A new API key (POST /v1/api-keys): {"id", "prefix", "key", "expires_at"}. key is the plaintext, shown once; the scopes cannot exceed this client's own.

ParameterDefaultMeaning
namestrrequired1 to 80 characters.
scopeslistrequiredAt least one scope.
expires_in_daysint | NoneNone1 to 365; None for no expiry.
Returns
dict: {id, prefix, key, expires_at}; key is the whole key, shown once
Raises
  • DrumsError (403 forbidden) from a member's key, or for a scope this client lacks
HTTP
POST /v1/api-keys

Client.revoke_api_key

Client.revoke_api_key(key_id: str) -> dict

Revoke a key now; it is refused on its next call. Owner and admin keys only; a key cannot revoke itself.

Revoke an API key (DELETE /v1/api-keys/{id}): {"id", "revoked_at"}. A key cannot revoke itself.

ParameterDefaultMeaning
key_idstrrequiredThe key's id (from api_keys()), not its prefix.
Returns
dict: {id, revoked_at, already}
HTTP
DELETE /v1/api-keys/{id}

Client.audit

Client.audit(
    action: str | None = None,
    limit: int = 50,
    cursor: str | None = None,
) -> dict

The workspace's audit log, newest first. Needs audit:read (owner and admin keys).

The workspace's audit log, newest first (GET /v1/audit): {"items": [events], "next_cursor"}. action filters by prefix (e.g. "mcp", "computer.create").

ParameterDefaultMeaning
actionstr | NoneNoneAn exact action (computer.create) or a prefix (mcp).
limitint501 to 200.
cursorstr | NoneNonenext_cursor of the previous page.
Returns
dict: {items, next_cursor}
HTTP
GET /v1/audit

Client.create

Client.create(
    image: str = 'osworld-ubuntu',
    task=None,
    *,
    from_state=None,
    setup: bool | None = None,
    idle_timeout: float | None = None,
    ttl: float | None = None,
) -> Env

A new env from an image (or from a saved state), with an optional OSWorld task whose setup runs now and after every reset. Raises RestoreFailed / GateFailed / TaskSetupFailed if it did not come up ready (the env still exists on the server, down or quarantined: close it, or let its lease reap it). idle_timeout / ttl (seconds): the env's lease. drumsd destroys it after idle_timeout s with no request naming it, or ttl s after it was created. None = the server's default (30 min idle, no TTL); 0 = no limit.

ParameterDefaultMeaning
imagestr'osworld-ubuntu'Base image name (osworld-ubuntu). See client.images().
taskNoneAn OSWorld task id (or 8+ character prefix), a path to a task JSON file, or the JSON dict. Its setup runs now and after every reset.
from_stateNoneA saved state (State or id) to start from instead of image.
setupbool | NoneNoneRun the task's setup after every restore (default true for a task given here).
idle_timeoutfloat | NoneNoneSeconds without a request before the computer is closed (default 1800, at most the lifetime).
ttlfloat | NoneNoneLifetime in seconds. Through the gateway, None or 0 means the workspace default (3600 s by default) and larger values are capped at the workspace maximum (14400 s by default).
Returns
Env in state ready (owned by this client).
Raises
  • RestoreFailed, GateFailed, TaskSetupFailed (subclasses of EnvFailed) if it did not come up ready; the computer exists on the server until closed
  • TaskUnsupported (422) for a task that needs root
  • Overloaded (429) when the host is full
  • DrumsError with status 403 for quota_exceeded or budget_exceeded
  • NotFound for an unknown image or saved state
HTTP
POST /v1/slots
Example
with drums.connect() as client:
    with client.create(task="e0df059f", ttl=900) as env:
        env.wait_valid()
        print(env.id, env.state, env.episode)

Client.create_many

Client.create_many(
    n: int,
    image: str = 'osworld-ubuntu',
    task=None,
    *,
    from_state=None,
    workers: int = 8,
    idle_timeout: float | None = None,
    ttl: float | None = None,
) -> list

n envs created concurrently. All ready -> the list; otherwise BatchError(ok=[ready envs], failed=[(i, error)]).

ParameterDefaultMeaning
nintrequiredNumber of computers.
imagestr'osworld-ubuntu'Base image name.
taskNoneTask id, path or JSON (fetched once).
from_stateNoneSaved state to start from.
workersint8Parallel create requests.
idle_timeoutfloat | NoneNoneAs in create.
ttlfloat | NoneNoneAs in create.
Returns
list[Env] when all came up.
Raises
  • BatchError(ok=[...], failed=[(i, error)]) if any failed: close ok yourself or let the client close them
HTTP
POST /v1/slots (n times)

Client.map

Client.map(fn, items, workers: int = 8) -> list

fn over items in threads; each result is the return value or the exception it raised (never dropped).

ParameterDefaultMeaning
fnrequiredFunction of one item.
itemsrequiredIterable of items.
workersint8Threads.
Returns
list: each result, or the exception it raised.

drums.Env

class Env

One computer (a desktop). Returned by create(), fork() and envs(); usable as a context manager that closes it.

One isolated desktop environment (API: a slot, /v1/slots/{id}).

Attributes: client, record, steps, last_state

Env.last_forkproperty

Env.last_fork

Deprecated: use the ForkResult that fork() returns.

Deprecated (0.2.4): the last fork()'s raw server reply. Use the ForkResult fork() returns (.state, .checkpoint) or env.last_state.

Env.idproperty

Env.id

The computer's UUID (the id the console shows).

Env.episodeproperty

Env.episode

Episode number; increases with every reset.

Env.stateproperty

Env.state

ready, down, quarantined, destroyed, … as of the last record the client saw.

Env.validityproperty

Env.validity

{verdict: pending | valid | invalid, gates?} from the last record.

Env.taskproperty

Env.task

The task JSON the computer runs, or None.

Env.from_stateproperty

Env.from_state

The image or saved state this computer restores to on reset.

Env.refresh

Env.refresh() -> dict
Returns
dict: the current record (also stored as env.record).
HTTP
GET /v1/slots/{id}

Env.reset

Env.reset() -> Env

New episode from the image or saved state this env was made from (and the task's setup, for an env created with a task).

Returns
Env (self) with a new episode.
Raises
  • Conflict on a quarantined computer
  • EnvGone
  • EnvFailed subclasses if the new episode did not come up
HTTP
POST /v1/slots/{id}/reset

Env.step

Env.step(action, screenshot: bool = False, check: bool = True) -> StepResult

pyautogui code (str) or an action dict: {"type": "pyautogui", "code"}, {"type": "execute", "command", "shell"}, {"type": "launch", "command", "shell"}, {"type": "sleep", "seconds"}. Screen 1920x1080, origin top left, pixels. pyautogui and execute run inside the env as its desktop user user (not root), with OSWorld's prefix (import pyautogui, time). See https://console.drums.run/docs/concepts/computers-and-templates#actions. check=True (default): an action that ran and exited non-zero raises ActionFailed with its exit status, output and error; check=False returns the result as is. A guest that does not answer or times out (120 s) raises GuestError either way.

ParameterDefaultMeaning
actionrequiredpyautogui code as a string, or an action dict: {"type": "pyautogui", "code"}, {"type": "execute", "command", "shell"}, {"type": "launch", "command", "shell"}, {"type": "sleep", "seconds"}.
screenshotboolFalseAlso return a screenshot (result.observation).
checkboolTrueRaise ActionFailed when the action exits non-zero.
Returns
StepResult
Raises
  • ActionFailed (with check=True)
  • GuestError (502) when the desktop does not answer
  • Conflict on a quarantined computer
  • EnvGone
HTTP
POST /v1/slots/{id}/step
Example
r = env.step("pyautogui.click(960, 540)")
r = env.step({"type": "execute", "command": ["ls", "/home/user"]})
print(r.result["output"])

Env.observe

Env.observe() -> Observation
Returns
Observation: the PNG plus meta (episode, state, verdict, task id, instruction, steps, time).
HTTP
GET /v1/slots/{id}/screenshot, then GET /v1/slots/{id}

Env.screenshot

Env.screenshot() -> bytes
Returns
bytes: PNG, 1920 x 1080.
HTTP
GET /v1/slots/{id}/screenshot

Env.gates

Env.gates() -> dict
Returns
dict: the episode's gate report and verdict.
HTTP
GET /v1/slots/{id}/gates

Env.valid

Env.valid(timeout: float = 30.0) -> bool

Wait until the episode's verdict is no longer pending; True only when every gate passed.

ParameterDefaultMeaning
timeoutfloat30.0Seconds to wait for the verdict.
Returns
bool
HTTP
GET /v1/slots/{id}/gates (polled)

Env.wait_valid

Env.wait_valid(timeout: float = 60.0) -> Env

Block until this episode's last gate (processes alive 5 s after resume) has resolved. Raises GateFailed if the episode is invalid. Act after this when your first actions may end processes: a process that exits in the first seconds after a restore cannot be told apart from a bad restore, so the episode would be flagged invalid.

ParameterDefaultMeaning
timeoutfloat60.0Seconds to wait.
Returns
Env (self)
Raises
  • GateFailed if the episode is invalid
  • DrumsError if still pending after timeout
HTTP
GET /v1/slots/{id}/gates (polled)

Env.verify

Env.verify(strict: bool = True) -> Reward

Run the task's checker on the current state. The server waits for the episode's last gate first. strict=True: raises InvalidEpisode (reward attached, flagged, with .invalid_reasons) if the episode is not valid, CheckerError if the checker failed. strict=False: returns the Reward; use .score only when .valid. On a quarantined env the checker is not run: score None, valid False, reason "quarantined: <why>". Note: OSWorld checkers may run a postconfig (save, close a window) that changes the desktop.

ParameterDefaultMeaning
strictboolTrueRaise instead of returning an unusable reward.
Returns
Reward
Raises
  • InvalidEpisode (strict) when the episode is not valid; the reward is attached
  • CheckerError (strict) when the checker failed
HTTP
POST /v1/slots/{id}/verify
Example
reward = env.verify(strict=False)
if reward.valid:
    print("score", reward.score)
else:
    print("do not train on this:", reward.invalid_reasons)

Env.load_task

Env.load_task(task) -> Env

Load an OSWorld task into this ready env (setup runs now and after every reset).

ParameterDefaultMeaning
taskrequiredTask id, path or JSON.
Returns
Env (self)
HTTP
POST /v1/slots/{id}/task

Env.metrics

Env.metrics() -> dict

This env's metrics: op latency (reset/step/fork/verify/...), episodes by verdict, failures and retries by cause, rewards, lifetime, and its share of the host cost by env-seconds (API field: slot_seconds).

Returns
dict
HTTP
GET /v1/slots/{id}/metrics

Env.wait_quiescent

Env.wait_quiescent(timeout: float | None = None, window: float | None = None) -> dict

Block until this env's process tree is safe to capture: no process or thread starting or exiting for window seconds (server default 1 s), no task exiting, no unreaped zombie. fork() and save() already do this themselves, so calling it is optional (useful to time it, or to wait without capturing). Returns the wait record; raises NotQuiescent after timeout seconds (server default 30 s).

ParameterDefaultMeaning
timeoutfloat | NoneNoneSeconds (server default 30).
windowfloat | NoneNoneQuiet window in seconds (server default 1).
Returns
dict: the wait record.
Raises
  • NotQuiescent
HTTP
POST /v1/slots/{id}/quiescent

Env.save

Env.save(
    name: str | None = None,
    quiesce_timeout: float | None = None,
    *,
    idle_timeout: float | None = None,
    ttl: float | None = None,
) -> State

Save the current state (API: capture; this env keeps running). client.create(from_state=...) starts from it. Safe right after an action: drumsd waits until the tree is quiescent and checks that the dump is that tree (NotQuiescent if it cannot within quiesce_timeout, server default 30 s; nothing is captured then). idle_timeout / ttl (seconds): the state's lease. drumsd deletes it after idle_timeout s with no env made from it (counted from its creation or its last env's close), or ttl s after it was made once no env uses it. None = the server's default (30 min idle, no TTL); 0 = no limit.

ParameterDefaultMeaning
namestr | NoneNoneDisplay name for the snapshot.
quiesce_timeoutfloat | NoneNoneSeconds to wait for a quiet process tree.
idle_timeoutfloat | NoneNoneThe saved state's idle lease (deleted after this long with no computer made from it).
ttlfloat | NoneNoneThe saved state's lifetime once unused.
Returns
State: pass it as client.create(from_state=state).
Raises
  • NotQuiescent
  • DrumsError 403 quota_exceeded at the host's saved-state limit
HTTP
POST /v1/slots/{id}/capture

Env.prefork

Env.prefork(
    n: int,
    quiesce_timeout: float | None = None,
    *,
    keep_state: bool = True,
    idle_timeout: float | None = None,
    ttl: float | None = None,
    state_idle_timeout: float | None = None,
    state_ttl: float | None = None,
) -> dict

Prepare the next fork(n) in the background right after an action, so the fork is fast when you call it. Optional; fork() alone always works.

Prepare fork(n) now, in the background (API: prefork), and return at once. Call it right after an action; the next fork(n) with the same keep_state then gets these children if the env took no input since, no process started or exited in it, and its desktop state hash is unchanged (else it is discarded and the fork runs as usual). Any input (step, reset) discards it; so does a prefork nobody claims within the daemon's prefork_max_age_s (default 120 s). The children are the env as it was when it went quiet after the action.

ParameterDefaultMeaning
nintrequiredThe number of copies the coming fork(n) will ask for.
quiesce_timeoutfloat | NoneNoneAs for fork().
keep_stateboolTrueMust match the coming fork() call.
idle_timeoutfloat | NoneNoneChildren's idle timeout.
ttlfloat | NoneNoneChildren's lifetime.
state_idle_timeoutfloat | NoneNoneThe saved state's idle lease.
state_ttlfloat | NoneNoneThe saved state's lifetime once unused.
Returns
dict (the runtime's acknowledgement); the children are handed out by the next fork(n), or discarded after any input or 120 s
HTTP
POST /v1/slots/{id}/prefork on a drumsd; the gateway does not serve this route in this version (404 `not_found`)

Env.fork

Env.fork(
    n: int,
    strict: bool = True,
    quiesce_timeout: float | None = None,
    *,
    keep_state: bool = True,
    idle_timeout: float | None = None,
    ttl: float | None = None,
    state_idle_timeout: float | None = None,
    state_ttl: float | None = None,
) -> ForkResult

Save this state and start n independent children from it, concurrently (API: branch); this env keeps running. Safe right after an action (no sleep needed): the capture waits for a quiescent tree, see save(). Returns a ForkResult: the ready children (a list, as before) with .state, the saved state the fork made (also env.last_state). keep_state=False: drumsd deletes that state as soon as its last env is closed, so a loop does not leak one state (~600 MB of RAM) per fork; keep_state=True (default): it stays until client.delete_state(state) or its lease. idle_timeout/ttl: the children's leases (see create); state_idle_timeout/state_ttl: the state's (see save). strict=True: if any child failed, raises ForkError(ok=[ready children], failed=[EnvFailed...], state).

ParameterDefaultMeaning
nintrequiredNumber of copies (at most the host's fork limit; they count against your live-computer limit).
strictboolTrueRaise ForkError if any child did not come up; with False the failed ones are in .failed.
quiesce_timeoutfloat | NoneNoneSeconds to wait for a quiet process tree before saving.
keep_stateboolTrueFalse: the saved state is deleted when the last child closes (recommended in loops).
idle_timeoutfloat | NoneNoneChildren's idle timeout.
ttlfloat | NoneNoneChildren's lifetime.
state_idle_timeoutfloat | NoneNoneThe saved state's idle lease.
state_ttlfloat | NoneNoneThe saved state's lifetime once unused.
Returns
ForkResult: a list of child Envs with .state, .checkpoint, .failed, .seconds.
Raises
  • ForkError (strict)
  • NotQuiescent
  • Overloaded
  • DrumsError 403 quota_exceeded
HTTP
POST /v1/slots/{id}/branch
Example
with env.fork(2, keep_state=False) as kids:      # closes the children at the end
    rewards = client.map(lambda k: k.verify(strict=False), kids)

Env.branch

Env.branch(
    n: int,
    strict: bool = True,
    quiesce_timeout: float | None = None,
    *,
    keep_state: bool = True,
    idle_timeout: float | None = None,
    ttl: float | None = None,
    state_idle_timeout: float | None = None,
    state_ttl: float | None = None,
) -> ForkResult

Alias of fork (the 0.1 name).

Save this state and start n independent children from it, concurrently (API: branch); this env keeps running. Safe right after an action (no sleep needed): the capture waits for a quiescent tree, see save(). Returns a ForkResult: the ready children (a list, as before) with .state, the saved state the fork made (also env.last_state). keep_state=False: drumsd deletes that state as soon as its last env is closed, so a loop does not leak one state (~600 MB of RAM) per fork; keep_state=True (default): it stays until client.delete_state(state) or its lease. idle_timeout/ttl: the children's leases (see create); state_idle_timeout/state_ttl: the state's (see save). strict=True: if any child failed, raises ForkError(ok=[ready children], failed=[EnvFailed...], state).

ParameterDefaultMeaning
strictboolTrue
quiesce_timeoutfloat | NoneNone
keep_stateboolTrue
idle_timeoutfloat | NoneNone
ttlfloat | NoneNone
state_idle_timeoutfloat | NoneNone
state_ttlfloat | NoneNone

Env.quarantine

Env.quarantine(reason: str = 'client') -> dict
ParameterDefaultMeaning
reasonstr'client'Shown in invalid_reasons.
Returns
dict: the record.
HTTP
POST /v1/slots/{id}/quarantine

Env.release

Env.release() -> dict

Kill a quarantined env's processes; it becomes down and a reset restores it.

Returns
dict: the record (down); call reset() next.
HTTP
POST /v1/slots/{id}/release

Env.close

Env.close() -> dict

Destroy this env (its records are kept on the host). A no-op on an env that is already gone (closed here or elsewhere, or reaped by its lease).

Returns
dict: the destroyed record.
HTTP
DELETE /v1/slots/{id}

Env.guest

Env.guest() -> GuestClient

An OSWorld guest-server-like transport (execute, launch, screenshot, screen_size, platform) over the API.

Returns
GuestClient

drums.ForkResult

class ForkResult(list)

What fork() returns: the ready children, as the plain list fork() always returned, plus the fork's saved state.

state is the saved state the fork made (a State). With fork(..., keep_state=False) drumsd deletes it once its last env is closed; otherwise it stays until client.delete_state(state) or its lease (default 30 min with no env made from it). checkpoint is the server's full record of it, failed the children that did not come up (strict=False only), seconds the fork's time. As a context manager it closes the children at the end.

Attributes: state, checkpoint, failed, seconds

ForkResult.close

ForkResult.close() -> None

Close every child (already closed ones are skipped).

HTTP
DELETE /v1/slots/{id} for each child

ForkResult.delete_state

ForkResult.delete_state() -> dict | None

Close the children, then delete the saved state now (NotFound/EnvGone-safe).

Returns
dict | None
HTTP
DELETE /v1/checkpoints/{id}

drums.State

class State(str)

A saved state id (a str). On Drums Cloud it is the snapshot UUID shown in the console.

A saved state (API: checkpoint id). Pass to client.create(from_state=...); free it with client.delete_state().

drums.StepResult

class StepResult(dict)

A dict with the step reply, plus attribute access.

The guest's reply to an action ('result'), the episode's validity at that moment, and the step's seconds. observation is set when the step asked for a screenshot.

StepResult.resultproperty

StepResult.result

The guest's reply: status, returncode, output, error.

StepResult.secondsproperty

StepResult.seconds

Seconds the step took inside the runtime.

StepResult.observationproperty

StepResult.observation

An Observation when the step asked for a screenshot.

drums.Observation

class Observation

A screenshot with metadata: .png (bytes), .width, .height, .meta.

Attributes: png, meta, width, height

drums.Reward

class Reward

A checker result with the episode's verdict: .score, .valid, .invalid_reasons, .verdict, .bad_gates, .checker_error, .checker_skipped, .raw.

A checker result with the episode's gate verdict. Use score for training only when valid is True.

Attributes: raw, env_id, episode, task_id, score, valid, validity, verdict, bad_gates, checker_error, checker_skipped, gates, seconds, audit, audit_flags, trusted, invalid_reasons

drums.GuestClient

class GuestClient

An OSWorld guest-server-like transport over the API (execute, launch, screenshot, screen_size, platform).

The GuestClient protocol (execute/launch/screenshot/screen_size/platform) that RL backends use, through drumsd.

Attributes: env, base, timeout

GuestClient.execute

GuestClient.execute(command, shell: bool = False, allow_rc=(0,), timeout=None) -> dict
ParameterDefaultMeaning
commandrequiredCommand (list, or string with shell=True).
shellboolFalseRun through sh -c.
allow_rc(0,)Exit codes that do not raise.
timeoutNoneUnused (kept for the protocol).

GuestClient.launch

GuestClient.launch(command, shell: bool = False) -> None
ParameterDefaultMeaning
commandrequiredCommand to start.
shellboolFalseRun through sh -c.

GuestClient.screenshot

GuestClient.screenshot() -> bytes

GuestClient.screen_size

GuestClient.screen_size()

GuestClient.platform

GuestClient.platform(timeout=10) -> str
ParameterDefaultMeaning
timeout10Unused (kept for the protocol).

drums.aio

asyncio variants: the same calls, run in worker threads (the transport is blocking urllib).

Each AsyncClient has its own thread pool (max_threads, default 1024; threads start only when needed), so N envs driven with asyncio.gather really run N calls at once. asyncio.to_thread would use the loop's default executor, min(32, cpu_count + 4) threads, and silently run a 256-env driver 32 calls at a time.

Example (from the docstring)
import asyncio, drums.aio
async def main():
    c = await drums.aio.connect(url, token)
    envs = await asyncio.gather(*[c.create(task="035f41ba") for _ in range(16)], return_exceptions=True)
    rewards = await asyncio.gather(*[e.verify(strict=False) for e in envs if isinstance(e, drums.aio.AsyncEnv)])

drums.aio.connect

async drums.aio.connect(url=None, token=None, *, max_threads: int = 1024, **kw) -> AsyncClient
ParameterDefaultMeaning
urlNoneAs drums.connect.
tokenNoneAs drums.connect.
max_threadsint1024Worker threads for this client's calls.
**kwrequiredPassed to drums.connect.
Returns
AsyncClient
Example
import asyncio, drums.aio

async def main():
    async with await drums.aio.connect() as c:
        envs = await asyncio.gather(*[c.create(task="e0df059f", ttl=900) for _ in range(2)])
        rewards = await asyncio.gather(*[e.verify(strict=False) for e in envs])
        print([r.valid for r in rewards])

asyncio.run(main())

drums.aio.AsyncClient

class AsyncClient

Async wrapper of Client; .sync is the underlying client.

Attributes: sync, pool

AsyncClient.create

async AsyncClient.create(image: str = 'osworld-ubuntu', task=None, **kw) -> AsyncEnv
ParameterDefaultMeaning
imagestr'osworld-ubuntu'As Client.create.
taskNoneAs Client.create.
**kwrequiredfrom_state, setup, idle_timeout, ttl.
Returns
AsyncEnv

AsyncClient.health

async AsyncClient.health()

AsyncClient.metrics

async AsyncClient.metrics()

AsyncClient.task

async AsyncClient.task(task_id)
ParameterDefaultMeaning
task_idrequiredTask id or prefix.

AsyncClient.delete_state

async AsyncClient.delete_state(state)
ParameterDefaultMeaning
staterequiredSaved state.

AsyncClient.close

async AsyncClient.close()

Close every env this client created and has not closed (see Client.close).

drums.aio.AsyncEnv

class AsyncEnv

Async wrapper of Env; .sync is the underlying env.

Attributes: sync

AsyncEnv.idproperty

AsyncEnv.id

AsyncEnv.episodeproperty

AsyncEnv.episode

AsyncEnv.step

async AsyncEnv.step(action, screenshot: bool = False)
ParameterDefaultMeaning
actionrequiredAs Env.step.
screenshotboolFalseAs Env.step.

AsyncEnv.observe

async AsyncEnv.observe()

AsyncEnv.reset

async AsyncEnv.reset()

AsyncEnv.verify

async AsyncEnv.verify(strict: bool = True)
ParameterDefaultMeaning
strictboolTrueAs Env.verify.

AsyncEnv.fork

async AsyncEnv.fork(n: int, strict: bool = True, quiesce_timeout=None, **kw)

As Env.fork (keep_state=, idle_timeout=, ttl=, state_idle_timeout=, state_ttl=): a ForkResult of AsyncEnvs.

ParameterDefaultMeaning
nintrequiredAs Env.fork.
strictboolTrueAs Env.fork.
quiesce_timeoutNoneAs Env.fork.
**kwrequiredkeep_state, idle_timeout, ttl, state_idle_timeout, state_ttl.
Returns
ForkResult of AsyncEnv

AsyncEnv.save

async AsyncEnv.save(name=None, quiesce_timeout=None, **kw)
ParameterDefaultMeaning
nameNoneAs Env.save.
quiesce_timeoutNoneAs Env.save.
**kwrequiredidle_timeout, ttl.

AsyncEnv.wait_quiescent

async AsyncEnv.wait_quiescent(timeout=None, window=None)
ParameterDefaultMeaning
timeoutNoneAs Env.wait_quiescent.
windowNoneAs Env.wait_quiescent.

AsyncEnv.metrics

async AsyncEnv.metrics()

AsyncEnv.close

async AsyncEnv.close()

Exceptions

Every exception is a drums.DrumsError with status (HTTP status, 0 when there was no reply), message and body (the parsed error reply). The hierarchy:

drums exceptions
DrumsError
    AuthError
    BadRequest
        TaskUnsupported
    BatchError
    CheckerError
    Conflict
        CheckpointStale
        NotQuiescent
    DaemonUnavailable
    EnvFailed
        GateFailed
        RestoreFailed
        TaskSetupFailed
    ForkError
    GuestError
        ActionFailed
    InsecureTransport
    InvalidEpisode
    NotFound
        EnvGone
    Overloaded
    RedirectRefused

DrumsError

Subclass of Exception · attributes status, message, body

Base class. status is the HTTP status (0 when no reply), message the server's error text.

AuthError

Subclass of DrumsError

401: missing or wrong token.

BadRequest

Subclass of DrumsError

400: the request is wrong (bad task JSON, bad id, n out of range).

TaskUnsupported

Subclass of BadRequest · attributes task_id, reason

422 task_unsupported: the task needs the agent itself to act as root, and agents get no privilege escalation in this execution profile (https://console.drums.run/docs/concepts/tasks-and-verifiers). Nothing was started. task_id and reason say which and why. A task whose setup alone needs root is supported: drumsd runs that step as trusted provisioning.

BatchError

Subclass of DrumsError · attributes ok, failed

Some of a batch failed. ok are the results that worked, failed a list of (index, exception).

CheckerError

Subclass of DrumsError · attributes reward

The task's checker raised or could not run: there is no score. reward has the details.

Conflict

Subclass of DrumsError

409: the env's state does not allow this operation (e.g. step on a quarantined env; code wrong_state). A destroyed env raises EnvGone instead.

CheckpointStale

Subclass of Conflict · attributes provenance, current

409 checkpoint_stale: the installed image's checkpoint was made by other runtime sources or another image inspection than this drumsd runs with (e.g. an upgrade that changed how the desktop logs in). drumsd refuses envs from it; run the installer again, which provisions the origin anew. provenance is the checkpoint's, current drumsd's.

NotQuiescent

Subclass of Conflict · attributes unstable, quiescence

409 not_quiescent: fork/save/wait_quiescent found no moment when the env's process tree was safe to capture (a process or thread kept starting or exiting, a task was exiting, or a zombie was not reaped) within the timeout. Nothing was captured and the env is still ready. unstable lists what was unstable ({kind, pid, tid, comm}, host pids); quiescence is the full record of the wait and of every dump attempt.

DaemonUnavailable

Subclass of DrumsError

drumsd did not answer (down, restarting, network). For a POST the outcome is unknown: call env.refresh().

EnvFailed

Subclass of DrumsError · attributes record, gates, state, env_id, cause

A create/reset/fork child did not come up ready. record is the env record, gates its gate report, cause the failed gate or error. The env is down or quarantined on the server; nothing was retried beyond what drumsd retries itself (failures before any restored process ran), and those attempts are in record['cost'].

GateFailed

Subclass of EnvFailed

A gate failed: the env is quarantined; its state is not trusted. Close it (or release + reset).

RestoreFailed

Subclass of EnvFailed

The restore failed before the desktop ran (the env is down; a reset tries again from scratch).

TaskSetupFailed

Subclass of EnvFailed

The task's OSWorld setup failed after a good restore.

ForkError

Subclass of DrumsError · attributes ok, failed, checkpoint, state

Some fork children did not come up. ok are the ready children (usable), failed the EnvFailed errors, state the saved state the fork made (also checkpoint, its 0.2 name): delete it with client.delete_state(state) once its envs are closed, unless the fork had keep_state=False (then drumsd deletes it).

GuestError

Subclass of DrumsError

502: the desktop's guest server did not answer or refused the action.

ActionFailed

Subclass of GuestError · attributes result, returncode, output, error, action

step(): the action ran in the desktop and exited non-zero (a pyautogui action that raised, a command that failed). Before 0.2.2 step() returned such a result without raising, so a failed action looked like a done one. returncode, output and error are the guest's; result is the whole guest reply. step(..., check=False) returns it instead (for commands whose exit status is the answer, e.g. test -f).

InsecureTransport

Subclass of DrumsError · attributes host, scheme

Refused before any request: a token would travel over plain HTTP to a host that is not loopback. The message names the host and the opt-ins (--allow-insecure-http, DRUMS_ALLOW_INSECURE_HTTP=1, allow_insecure_http=True); it never carries the token.

InvalidEpisode

Subclass of DrumsError · attributes reward

A reward from an episode that is not valid (a gate failed or is still pending, or the env was quarantined). reward holds it, flagged valid=False, with reward.invalid_reasons. Do not train on it. (Also raised by the legacy client when an env is not ready.)

NotFound

Subclass of DrumsError

404: no such env, image, state or task.

EnvGone

Subclass of NotFound · attributes env_id, closed_reason

410 env_gone: the env was destroyed (closed by a client, or reaped by its lease). Every operation on it raises this; env.close() on it is a no-op. Create a new env to go on. closed_reason says why when it was not a client's close (e.g. "lease: idle 1800 s"). A subclass of NotFound, so except NotFound still catches it. (Against drumsd < 0.4.2, the 409 "slot is destroyed" reply is raised as this too.)

Overloaded

Subclass of DrumsError · attributes reason, admission

429 overloaded: drumsd refused a create, save or fork because the host is near its memory limit (or under memory pressure, or the state tmpfs is nearly full). Nothing was started; try again later. reason is memory, memory_pressure or state_space; admission has the numbers (available, in flight, needed, floor).

RedirectRefused

Subclass of DrumsError · attributes target

Refused: the server answered with a redirect the client will not follow with the token (a different origin, i.e. scheme, host or port, or more than MAX_REDIRECTS hops). The message names the target's scheme, host and port only: never the token, userinfo, path or query string.