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 variable | Used for |
|---|---|
DRUMS_URL | Base 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_TOKEN | Your API key, sent as Authorization: Bearer |
DRUMS_ALLOW_INSECURE_HTTP | 1 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:andwith 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 callenv.refresh()orclient.envs()before trying again. - Rewards.
verify()raisesInvalidEpisodeby default instead of returning a reward from an invalid episode. Withstrict=False, checkreward.validbefore usingreward.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"].
| HTTP status | Exception |
|---|---|
| 400 | BadRequest |
| 401 | AuthError |
| 404 | NotFound |
| 409 | Conflict |
| 410 | EnvGone |
| 422 | BadRequest |
| 429 | Overloaded |
| 502 | GuestError |
| 503 | DaemonUnavailable |
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,
) -> ClientOpen 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.
| Parameter | Default | Meaning |
|---|---|---|
| urlstr | None | None | Base 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 | None | None | API key. Default $DRUMS_TOKEN. |
| timeoutfloat | 900.0 | Seconds per request (900 by default: a create, fork or verify can take a while). |
| retry_sfloat | 60.0 | How long GET requests are retried while the server is unreachable or answers 503. Other methods are never retried. |
| checkbool | True | Call health() now, so a bad key raises AuthError here rather than on the first create. |
| close_on_exitbool | True | Close the desktops this client created when the process exits or the with block ends. |
| handle_signalsbool | True | Turn SIGTERM and SIGHUP into a normal exit so that cleanup runs (only over default handlers, main thread only). |
| allow_insecure_httpbool | None | None | Send 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. |
ClientAuthError(401) withcheck=Trueand a bad keyDaemonUnavailablewhen the URL does not answerInsecureTransportfor a plain-http URL that is not loopback, unless opted in
- GET /v1/health (with check=True)
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,
) -> EnvThe 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(...).
| Parameter | Default | Meaning |
|---|---|---|
| taskdict | None | None | OSWorld task JSON. |
| from_statestr | None | None | Saved state to start from (wins over checkpoint). |
| checkpointstr | 'osworld-ubuntu' | Base image name. |
| urlstr | 'http://127.0.0.1:7070' | Base URL. |
| tokenstr | None | None | API key. |
Env(not owned by a client: close it yourself)- POST /v1/slots
drums.check_transport
drums.check_transport(
url: str,
token: str | None,
allow_insecure_http: bool | None = None,
) -> NoneThe 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.
| Parameter | Default | Meaning |
|---|---|---|
| urlstr | required | The base URL to check. |
| tokenstr | None | required | The key that would be sent; None passes (nothing to protect). |
| allow_insecure_httpbool | None | None | The opt-in; None reads $DRUMS_ALLOW_INSECURE_HTTP. |
NoneInsecureTransportwhentokenis set, the scheme ishttpand the host is notlocalhost,127.0.0.0/8or::1; the message names the host and the opt-ins, never the key
drums.Client
class ClientA 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() -> listIds of the envs this client created and has not closed.
list[str]of computer ids this client created and has not closed.
Client.close
Client.close(timeout: float = 30.0) -> listClose 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).
| Parameter | Default | Meaning |
|---|---|---|
| timeoutfloat | 30.0 | Seconds per close request. |
list[tuple[str, Exception | None]]- 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.
| Parameter | Default | Meaning |
|---|---|---|
| methodstr | required | GET, POST or DELETE. |
| pathstr | required | Path under the base URL, e.g. /v1/checkpoints/<id>. |
| body | None | JSON body (sent as Content-Type: application/json). |
| rawbool | False | Return the response bytes instead of parsed JSON. |
| timeoutfloat | None | None | Seconds for this request (default: the client's). |
| headersdict | None | None | Extra request headers, e.g. {"Idempotency-Key": ...} for POST /v1/runs. |
- Parsed JSON, or bytes with
raw=True. - A
DrumsErrorsubclass chosen by HTTP status andcode(see Errors) RedirectRefusedwhen the server redirects to another origin (scheme, host or port) or more thandrums.MAX_REDIRECTS(5) times: the key is never re-sent elsewhere
- A
Client.health
Client.health() -> dictdict: version, launchable images, limits, lease defaults, capacity.- GET /v1/health
Client.metrics
Client.metrics() -> dictAggregate 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.
dictof the current window's metrics (measured compute cost, not billed).- GET /v1/metrics
Client.reset_metrics
Client.reset_metrics() -> dictClose the current metrics window (its ledger goes to drumsd's event log) and start a new one.
Client.images
Client.images() -> listWhat envs can be created from: installed images and saved states (API: GET /v1/checkpoints).
list[dict]of checkpoint records: base images and your saved states.- GET /v1/checkpoints
Client.delete_state
Client.delete_state(state) -> dictDelete 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.
| Parameter | Default | Meaning |
|---|---|---|
| state | required | A State (or its id string) from save() or fork(). |
dict{id, deleted, freed_bytes}Conflictwhile a computer made from it existsNotFound
- DELETE /v1/checkpoints/{id}
Client.tasks
Client.tasks(domain: str | None = None) -> list| Parameter | Default | Meaning |
|---|---|---|
| domainstr | None | None | Filter by OSWorld domain, e.g. os, chrome, libreoffice_calc. |
list[dict]of{id, domain, instruction, supported, unsupported_reason}.- GET /v1/tasks
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) -> dictAn OSWorld task JSON from the host's pinned checkout, by id or unique prefix (>= 8 characters).
| Parameter | Default | Meaning |
|---|---|---|
| task_idstr | required | Task id or a unique prefix of at least 8 characters. |
dict: the OSWorld task JSON.NotFound
- GET /v1/tasks/{id}
Client.envs
Client.envs() -> listThe 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.
list[Env]of your live computers on this host (not owned by this client).- 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.
| Parameter | Default | Meaning |
|---|---|---|
| include_destroyedbool | False | Also yield closed computers. |
| page_sizeint | 500 | Records per request (1..5000). |
- A generator of computer record dicts.
- GET /v1/slots?limit=&after=&include_destroyed=
Client.runs
Client.runs(kind: str | None = None, status: str | None = None, limit: int = 50) -> dictThe 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).
| Parameter | Default | Meaning |
|---|---|---|
| kindstr | None | None | evaluation or scripted_test. |
| statusstr | None | None | A run status (queued, running, completed, failed, cancelled, ...). |
| limitint | 50 | 1 to 200. |
dict:{items, next_cursor}- GET /v1/runs
Client.create_run
Client.create_run(spec: dict, idempotency_key: str | None = None) -> dictQueue 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.
| Parameter | Default | Meaning |
|---|---|---|
| specdict | required | The RunSpec (see the HTTP reference for its fields). |
| idempotency_keystr | None | None | Sent as Idempotency-Key: the same key with the same spec returns the same run instead of a new one. |
dict:{run_id, operation_id, status, created}DrumsError(403quota_exceeded/budget_exceeded) when the execution or budget exceeds the workspace limitsConflict(409wrong_state) when the Idempotency-Key was used with a different body
- POST /v1/runs
Client.run
Client.run(run_id: str) -> dictOne run (GET /v1/runs/{id}): its row, the attempts summary and the last events.
| Parameter | Default | Meaning |
|---|---|---|
| run_idstr | required | A run id of your workspace; another workspace's is NotFound. |
dict: the run,attemptsandevents- GET /v1/runs/{id}
Client.api_keys
Client.api_keys() -> dictKey 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, ...}]}.
dict:{items: [{id, name, prefix, scopes, ...}]}- GET /v1/api-keys
Client.create_api_key
Client.create_api_key(
name: str,
scopes: list,
expires_in_days: int | None = None,
) -> dictMint 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.
| Parameter | Default | Meaning |
|---|---|---|
| namestr | required | 1 to 80 characters. |
| scopeslist | required | At least one scope. |
| expires_in_daysint | None | None | 1 to 365; None for no expiry. |
dict:{id, prefix, key, expires_at};keyis the whole key, shown onceDrumsError(403forbidden) from a member's key, or for a scope this client lacks
- POST /v1/api-keys
Client.revoke_api_key
Client.revoke_api_key(key_id: str) -> dictRevoke 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.
| Parameter | Default | Meaning |
|---|---|---|
| key_idstr | required | The key's id (from api_keys()), not its prefix. |
dict:{id, revoked_at, already}- DELETE /v1/api-keys/{id}
Client.audit
Client.audit(
action: str | None = None,
limit: int = 50,
cursor: str | None = None,
) -> dictThe 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").
| Parameter | Default | Meaning |
|---|---|---|
| actionstr | None | None | An exact action (computer.create) or a prefix (mcp). |
| limitint | 50 | 1 to 200. |
| cursorstr | None | None | next_cursor of the previous page. |
dict:{items, next_cursor}- 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,
) -> EnvA 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.
| Parameter | Default | Meaning |
|---|---|---|
| imagestr | 'osworld-ubuntu' | Base image name (osworld-ubuntu). See client.images(). |
| task | None | An 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_state | None | A saved state (State or id) to start from instead of image. |
| setupbool | None | None | Run the task's setup after every restore (default true for a task given here). |
| idle_timeoutfloat | None | None | Seconds without a request before the computer is closed (default 1800, at most the lifetime). |
| ttlfloat | None | None | Lifetime 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). |
Envin stateready(owned by this client).RestoreFailed,GateFailed,TaskSetupFailed(subclasses ofEnvFailed) if it did not come up ready; the computer exists on the server until closedTaskUnsupported(422) for a task that needs rootOverloaded(429) when the host is fullDrumsErrorwith status 403 forquota_exceededorbudget_exceededNotFoundfor an unknown image or saved state
- POST /v1/slots
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,
) -> listn envs created concurrently. All ready -> the list; otherwise BatchError(ok=[ready envs], failed=[(i, error)]).
| Parameter | Default | Meaning |
|---|---|---|
| nint | required | Number of computers. |
| imagestr | 'osworld-ubuntu' | Base image name. |
| task | None | Task id, path or JSON (fetched once). |
| from_state | None | Saved state to start from. |
| workersint | 8 | Parallel create requests. |
| idle_timeoutfloat | None | None | As in create. |
| ttlfloat | None | None | As in create. |
list[Env]when all came up.BatchError(ok=[...], failed=[(i, error)])if any failed: closeokyourself or let the client close them
- POST /v1/slots (n times)
Client.map
Client.map(fn, items, workers: int = 8) -> listfn over items in threads; each result is the return value or the exception it raised (never dropped).
| Parameter | Default | Meaning |
|---|---|---|
| fn | required | Function of one item. |
| items | required | Iterable of items. |
| workersint | 8 | Threads. |
list: each result, or the exception it raised.
drums.Env
class EnvOne 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_forkDeprecated: 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.idThe computer's UUID (the id the console shows).
Env.episodeproperty
Env.episodeEpisode number; increases with every reset.
Env.stateproperty
Env.stateready, 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.taskThe task JSON the computer runs, or None.
Env.from_stateproperty
Env.from_stateThe image or saved state this computer restores to on reset.
Env.refresh
Env.refresh() -> dictdict: the current record (also stored asenv.record).- GET /v1/slots/{id}
Env.reset
Env.reset() -> EnvNew episode from the image or saved state this env was made from (and the task's setup, for an env created with a task).
Env(self) with a new episode.Conflicton a quarantined computerEnvGoneEnvFailedsubclasses if the new episode did not come up
- POST /v1/slots/{id}/reset
Env.step
Env.step(action, screenshot: bool = False, check: bool = True) -> StepResultpyautogui 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.
| Parameter | Default | Meaning |
|---|---|---|
| action | required | pyautogui code as a string, or an action dict: {"type": "pyautogui", "code"}, {"type": "execute", "command", "shell"}, {"type": "launch", "command", "shell"}, {"type": "sleep", "seconds"}. |
| screenshotbool | False | Also return a screenshot (result.observation). |
| checkbool | True | Raise ActionFailed when the action exits non-zero. |
StepResultActionFailed(withcheck=True)GuestError(502) when the desktop does not answerConflicton a quarantined computerEnvGone
- POST /v1/slots/{id}/step
r = env.step("pyautogui.click(960, 540)")
r = env.step({"type": "execute", "command": ["ls", "/home/user"]})
print(r.result["output"])Env.observe
Env.observe() -> ObservationObservation: the PNG plusmeta(episode, state, verdict, task id, instruction, steps, time).- GET /v1/slots/{id}/screenshot, then GET /v1/slots/{id}
Env.screenshot
Env.screenshot() -> bytesbytes: PNG, 1920 x 1080.- GET /v1/slots/{id}/screenshot
Env.gates
Env.gates() -> dictdict: the episode's gate report and verdict.- GET /v1/slots/{id}/gates
Env.valid
Env.valid(timeout: float = 30.0) -> boolWait until the episode's verdict is no longer pending; True only when every gate passed.
| Parameter | Default | Meaning |
|---|---|---|
| timeoutfloat | 30.0 | Seconds to wait for the verdict. |
Env.wait_valid
Env.wait_valid(timeout: float = 60.0) -> EnvBlock 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.
| Parameter | Default | Meaning |
|---|---|---|
| timeoutfloat | 60.0 | Seconds to wait. |
Env(self)GateFailedif the episode is invalidDrumsErrorif still pending aftertimeout
- GET /v1/slots/{id}/gates (polled)
Env.verify
Env.verify(strict: bool = True) -> RewardRun 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.
| Parameter | Default | Meaning |
|---|---|---|
| strictbool | True | Raise instead of returning an unusable reward. |
RewardInvalidEpisode(strict) when the episode is not valid; the reward is attachedCheckerError(strict) when the checker failed
- POST /v1/slots/{id}/verify
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) -> EnvLoad an OSWorld task into this ready env (setup runs now and after every reset).
| Parameter | Default | Meaning |
|---|---|---|
| task | required | Task id, path or JSON. |
Env(self)- POST /v1/slots/{id}/task
Env.metrics
Env.metrics() -> dictThis 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).
Env.wait_quiescent
Env.wait_quiescent(timeout: float | None = None, window: float | None = None) -> dictBlock 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).
| Parameter | Default | Meaning |
|---|---|---|
| timeoutfloat | None | None | Seconds (server default 30). |
| windowfloat | None | None | Quiet window in seconds (server default 1). |
dict: the wait record.NotQuiescent
- 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,
) -> StateSave 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.
| Parameter | Default | Meaning |
|---|---|---|
| namestr | None | None | Display name for the snapshot. |
| quiesce_timeoutfloat | None | None | Seconds to wait for a quiet process tree. |
| idle_timeoutfloat | None | None | The saved state's idle lease (deleted after this long with no computer made from it). |
| ttlfloat | None | None | The saved state's lifetime once unused. |
State: pass it asclient.create(from_state=state).NotQuiescentDrumsError403quota_exceededat the host's saved-state limit
- 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,
) -> dictPrepare 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.
| Parameter | Default | Meaning |
|---|---|---|
| nint | required | The number of copies the coming fork(n) will ask for. |
| quiesce_timeoutfloat | None | None | As for fork(). |
| keep_statebool | True | Must match the coming fork() call. |
| idle_timeoutfloat | None | None | Children's idle timeout. |
| ttlfloat | None | None | Children's lifetime. |
| state_idle_timeoutfloat | None | None | The saved state's idle lease. |
| state_ttlfloat | None | None | The saved state's lifetime once unused. |
dict(the runtime's acknowledgement); the children are handed out by the nextfork(n), or discarded after any input or 120 s- 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,
) -> ForkResultSave 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).
| Parameter | Default | Meaning |
|---|---|---|
| nint | required | Number of copies (at most the host's fork limit; they count against your live-computer limit). |
| strictbool | True | Raise ForkError if any child did not come up; with False the failed ones are in .failed. |
| quiesce_timeoutfloat | None | None | Seconds to wait for a quiet process tree before saving. |
| keep_statebool | True | False: the saved state is deleted when the last child closes (recommended in loops). |
| idle_timeoutfloat | None | None | Children's idle timeout. |
| ttlfloat | None | None | Children's lifetime. |
| state_idle_timeoutfloat | None | None | The saved state's idle lease. |
| state_ttlfloat | None | None | The saved state's lifetime once unused. |
ForkResult: a list of childEnvs with.state,.checkpoint,.failed,.seconds.ForkError(strict)NotQuiescentOverloadedDrumsError403quota_exceeded
- POST /v1/slots/{id}/branch
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,
) -> ForkResultAlias 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).
| Parameter | Default | Meaning |
|---|---|---|
| strictbool | True | |
| quiesce_timeoutfloat | None | None | |
| keep_statebool | True | |
| idle_timeoutfloat | None | None | |
| ttlfloat | None | None | |
| state_idle_timeoutfloat | None | None | |
| state_ttlfloat | None | None |
Env.quarantine
Env.quarantine(reason: str = 'client') -> dict| Parameter | Default | Meaning |
|---|---|---|
| reasonstr | 'client' | Shown in invalid_reasons. |
dict: the record.- POST /v1/slots/{id}/quarantine
Env.release
Env.release() -> dictKill a quarantined env's processes; it becomes down and a reset restores it.
dict: the record (down); callreset()next.- POST /v1/slots/{id}/release
Env.close
Env.close() -> dictDestroy 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).
dict: the destroyed record.- DELETE /v1/slots/{id}
Env.guest
Env.guest() -> GuestClientAn OSWorld guest-server-like transport (execute, launch, screenshot, screen_size, platform) over the API.
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() -> NoneClose every child (already closed ones are skipped).
ForkResult.delete_state
ForkResult.delete_state() -> dict | NoneClose the children, then delete the saved state now (NotFound/EnvGone-safe).
dict | None- 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.resultThe guest's reply: status, returncode, output, error.
StepResult.secondsproperty
StepResult.secondsSeconds the step took inside the runtime.
StepResult.observationproperty
StepResult.observationAn Observation when the step asked for a screenshot.
drums.Observation
class ObservationA screenshot with metadata: .png (bytes), .width, .height, .meta.
Attributes: png, meta, width, height
drums.Reward
class RewardA 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 GuestClientAn 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| Parameter | Default | Meaning |
|---|---|---|
| command | required | Command (list, or string with shell=True). |
| shellbool | False | Run through sh -c. |
| allow_rc | (0,) | Exit codes that do not raise. |
| timeout | None | Unused (kept for the protocol). |
GuestClient.launch
GuestClient.launch(command, shell: bool = False) -> None| Parameter | Default | Meaning |
|---|---|---|
| command | required | Command to start. |
| shellbool | False | Run through sh -c. |
GuestClient.screenshot
GuestClient.screenshot() -> bytesGuestClient.screen_size
GuestClient.screen_size()GuestClient.platform
GuestClient.platform(timeout=10) -> str| Parameter | Default | Meaning |
|---|---|---|
| timeout | 10 | Unused (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.
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| Parameter | Default | Meaning |
|---|---|---|
| url | None | As drums.connect. |
| token | None | As drums.connect. |
| max_threadsint | 1024 | Worker threads for this client's calls. |
| **kw | required | Passed to drums.connect. |
AsyncClient
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 AsyncClientAsync wrapper of Client; .sync is the underlying client.
Attributes: sync, pool
AsyncClient.create
async AsyncClient.create(image: str = 'osworld-ubuntu', task=None, **kw) -> AsyncEnv| Parameter | Default | Meaning |
|---|---|---|
| imagestr | 'osworld-ubuntu' | As Client.create. |
| task | None | As Client.create. |
| **kw | required | from_state, setup, idle_timeout, ttl. |
AsyncEnv
AsyncClient.health
async AsyncClient.health()AsyncClient.metrics
async AsyncClient.metrics()AsyncClient.task
async AsyncClient.task(task_id)| Parameter | Default | Meaning |
|---|---|---|
| task_id | required | Task id or prefix. |
AsyncClient.delete_state
async AsyncClient.delete_state(state)| Parameter | Default | Meaning |
|---|---|---|
| state | required | Saved state. |
AsyncClient.close
async AsyncClient.close()Close every env this client created and has not closed (see Client.close).
drums.aio.AsyncEnv
class AsyncEnvAsync wrapper of Env; .sync is the underlying env.
Attributes: sync
AsyncEnv.idproperty
AsyncEnv.idAsyncEnv.episodeproperty
AsyncEnv.episodeAsyncEnv.step
async AsyncEnv.step(action, screenshot: bool = False)| Parameter | Default | Meaning |
|---|---|---|
| action | required | As Env.step. |
| screenshotbool | False | As Env.step. |
AsyncEnv.observe
async AsyncEnv.observe()AsyncEnv.reset
async AsyncEnv.reset()AsyncEnv.verify
async AsyncEnv.verify(strict: bool = True)| Parameter | Default | Meaning |
|---|---|---|
| strictbool | True | As 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.
| Parameter | Default | Meaning |
|---|---|---|
| nint | required | As Env.fork. |
| strictbool | True | As Env.fork. |
| quiesce_timeout | None | As Env.fork. |
| **kw | required | keep_state, idle_timeout, ttl, state_idle_timeout, state_ttl. |
ForkResultofAsyncEnv
AsyncEnv.save
async AsyncEnv.save(name=None, quiesce_timeout=None, **kw)| Parameter | Default | Meaning |
|---|---|---|
| name | None | As Env.save. |
| quiesce_timeout | None | As Env.save. |
| **kw | required | idle_timeout, ttl. |
AsyncEnv.wait_quiescent
async AsyncEnv.wait_quiescent(timeout=None, window=None)| Parameter | Default | Meaning |
|---|---|---|
| timeout | None | As Env.wait_quiescent. |
| window | None | As 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:
DrumsError
AuthError
BadRequest
TaskUnsupported
BatchError
CheckerError
Conflict
CheckpointStale
NotQuiescent
DaemonUnavailable
EnvFailed
GateFailed
RestoreFailed
TaskSetupFailed
ForkError
GuestError
ActionFailed
InsecureTransport
InvalidEpisode
NotFound
EnvGone
Overloaded
RedirectRefusedDrumsError
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.