Remote Client
A pool of remote monty workers reached over gRPC instead of local subprocesses — the intended peer is
monty-server.
The pool holds one HTTP/2 connection to the server and each checkout opens one MontySession.Session stream on it;
a stream is never reused.
See running monty-server and the remote-worker trust boundary.
A remote peer may be CPython rather than a Monty sandbox; the transport does not provide isolation.
AsyncMontyWebsocket is the same class under its deprecated former name, and a ws:// or
wss:// URL is read as http:// or https://.
checkout(profile='...') names a server-side profile to run the session under; None takes the server’s default.
The client sends the name to the server unchecked; what a profile means is the server’s choice, and the monty worker
ignores it.
MontyRefusedError means the server refused to open the session’s stream: at
capacity, with no worker to serve it, or rejecting the caller; its status_code, status_message and cause say
why, no session existed, and retrying is safe.
It subclasses MontyError, not RuntimeError, so a retry loop around checkout() must catch it by name.
A server that cannot be reached, or that answers with any other status (such as UNIMPLEMENTED from a URL that is
no Monty server), raises RuntimeError.
MontyDisconnectError means the stream ended mid-session.
A server that ended it by policy says so in the error’s status and cause (see
disconnect diagnostics); a transport failure says nothing about the worker’s fate.
Either way the session is lost; check out a new one.
MontyShutdown means the server declined the next request because it is shutting down.
That request did not run.
Against a server that stores sessions the client resumes the session itself before raising, see
stored sessions; the exception is raised only when that fails or is disabled.
If the exception includes a dump, restore it into a fresh session using the appropriate
snapshot loader.
Restoring a suspended call re-announces it even if the host already executed the callback, so callback side effects
can happen twice.
Neither exception occurs on the local subprocess transport.
A server that stores sessions gives each one an opaque ID instead of sending its state back.
session_id holds it; it is None against a server that stores nothing.
monty-server run without a store also refuses dump(), load_session() and load_snapshot() with
MontyRuntimeError, and the session carries on; the development relay in scripts/grpc_relay.py instead passes
them to its worker, which dumps and loads bytes as a local session does.
Against a storing server dump() writes the session’s current state to a
record that never changes and returns that record’s ID; the session continues under its session_id.
The server also writes the session’s state under session_id whenever it parks the session: idle for its
--park-after, the client gone, or a drain.
Passing either ID to the loader matching the stored state, load_session()
for an idle session or load_snapshot() for one suspended mid-feed, on
a fresh session, from any process, starts a new session with its own ID: a session_id gives the state as of that
session’s last park, a dump() ID the state dumped.
The record is unchanged and the session that wrote it is never resumed in place, so loading one ID twice gives two
independent sessions, and a session still running elsewhere is unaffected.
Loading an ID restores a dump, so what restoring does not carry
applies: host objects sent before the dump, in particular, are unknown to the new session.
To branch a session at a chosen point, call dump(), then load_session() with its ID once per branch; the original
session can carry on as well.
checkout(ephemeral=True) asks the server not to store the session on its own, so it has no ID; whether it honours
dump() is the server’s choice.
When such a server drains a session, the client opens a new stream, loads the state the server named into a new session and
re-sends the request, so the caller sees the result rather than MontyShutdown; session_id then names the new
session, and the session’s suspension and sleep totals carry over.
MontyShutdown is still raised if the server named nothing to load, if the reload fails, if auto_resume=False is
passed to AsyncMontyRemote, or if the session has no ID.
The new stream uses the metadata returned when the session was entered, so an expired token makes
the resume fail.
MontyDisconnectError is never resumed, because the request may have run.
MontyDisconnectError exposes status_code, status_message, cause and
transport_error.
If the server ended the stream with a gRPC status, status_code holds its canonical name (DEADLINE_EXCEEDED,
RESOURCE_EXHAUSTED, UNAVAILABLE, …), or OK when the server simply finished the stream, and status_message its
message.
A monty-server names why in cause: idle_timeout, session_timeout, turn_timeout, out_of_memory,
server_shutdown, capacity, caller_cap, unauthenticated, permission_denied, worker_unavailable or
protocol_violation.
An out_of_memory cause is raised as the same MemoryError a local out-of-memory kill raises, and a
server_shutdown cause as MontyShutdown with no dump, so neither reaches this error.
A connection failure or protocol failure supplies transport_error text instead, with no status.
A later failure does not replace an earlier one.
Missing evidence remains None, and a status message is server-supplied text, not a verified explanation.
These fields do not establish whether the request ran, so the client does not replay it.
install_dependencies() is supported only by embedded-CPython workers.
It installs PEP 508 requirements using uv, within the pool’s request_timeout.
Those workers also install PEP 723 inline dependencies before running a feed.
A Monty sandbox worker rejects non-empty installation requests with MontyRuntimeError and ignores PEP 723 comments.
install_dependencies([]) is a no-op on either worker.
AsyncMontyRemote(max_decode_bytes=...) can lower the default 1 GiB allocation budget for each decoded protocol frame.
Values above 1 GiB are rejected when the client is constructed.
The budget excludes wire buffers and Python conversion, and does not change
sandbox memory limits. Exhaustion ends the session with a protocol error;
the client does not replay the request.
Separately, the transport refuses any message larger than the budget, clamped between 64 KiB and the 256 MiB frame limit, before buffering it.
Async context manager owning a pool of remote monty workers reached over
gRPC: one HTTP/2 connection to the configured URL, one stream per session.
The intended peer is monty-server (the production server: one
monty subprocess child per stream, plus capacity/timeout policy and
graceful drain), but any server that admits the stream and bridges it to
a worker fits — e.g. the dev relay scripts/grpc_relay.py.
Like AsyncMonty, but instead of spawning local subprocesses each checkout
opens a stream to the configured URL. There is no sync counterpart — remote
turns are network-bound. checkout() yields the same AsyncMontySession.
AsyncMontyWebsocket is the same class under its former name.
A monty-server enforces its own policy on top of the pool’s. On SIGTERM
drain it answers the session’s next request with MontyShutdown, whose
dump restores the session onto another server; every other server-side
drop (idle, session or turn timeout, capacity) ends the stream with a
status and raises MontyDisconnectError naming its cause; a stream it
refuses to open raises MontyRefusedError the same way.
A server that stores sessions gives each one an ID (session.session_id)
instead of handing state back: dump() writes the current state to a
record that never changes and returns its ID, and load_session /
load_snapshot start a new session from either ID, in any process.
checkout(ephemeral=True) opts a session out. When such a server drains,
the session resumes on another server without the caller noticing (see
auto_resume).
async with AsyncMontyRemote('http://127.0.0.1:8799') as pool:
async with pool.checkout() as session:
result = await session.feed_run('1 + 1')
def __new__(
cls,
url: str,
*,
max_processes: int | None = None,
checkout_timeout: float | None = None,
request_timeout: float | None = 10.0,
metadata: Callable[[], Mapping[str, str]] | None = None,
feed_duration_limit_grace: float | None = 1.0,
turn_duration_limit_grace: float | None = 1.0,
auto_resume: bool = True,
max_decode_bytes: int | None = None,
connect_headers: Callable[[], Mapping[str, str]] | None = None,
) -> Self
Configure a remote worker pool; the connection is made by the first checkout (no workers are pre-warmed).
url : str
http:///https:// URL of the server — a relay, or any
server that bridges a stream to a worker. ws:///wss://
are read as those two.
Cap on concurrent streams (defaults to the CPU count); checkouts beyond it wait.
Seconds checkout() waits for capacity before
raising TimeoutError. None waits forever.
Hard per-turn deadline in seconds (default 10.0) — a
worker that exceeds it has its connection killed and the call
raises MontyCrashedError with timed_out=True. It also bounds
opening the stream, including the server’s admission (30
seconds when None). CPU-mode servers require a finite value.
Pass None to wait indefinitely with wall-mode servers.
Under contention, this wall deadline can expire before a server
CPU budget. Choose it for the expected budgets and scheduling delays.
Note that install_dependencies is a turn too, so the default
10.0 is often too low for it — a real uv pip install can exceed
it. Raise request_timeout when installing dependencies over
the remote transport.
Per-frame host allocation budget in bytes. None
keeps the protocol’s 1 GiB ceiling. Values above 1 GiB are rejected
at construction. Excludes wire buffers and Python conversion.
Exhaustion ends the session with a protocol error without replay.
Also caps each message’s size, clamped between 64 KiB and the 256 MiB frame limit.
The former name of metadata, accepted for one
release with a DeprecationWarning; passing both raises
TypeError.
Called once per session, as it is entered and before
any wait for pool capacity, to produce extra str to str
request metadata for that session’s stream — e.g. a token for
infrastructure in front of the worker. It runs synchronously
on the checking-out task, so it sees that task’s contextvars
and must not block. Monty never interprets the values; names
are lowercase ASCII and values ASCII, duplicate names are
last-wins, also over the traceparent the Logfire integration
adds, a user-agent entry is put in front of the default one,
and a malformed name or value raises RuntimeError as the
session is entered.
Client wall-time grace beyond
max_feed_duration_secs when the server uses wall time.
CPU mode uses the server’s supervisor and grace instead.
None disables this client backstop. request_timeout stays independent.
The same, for max_turn_duration_secs.
auto_resume : bool Default: True
When a server that stores sessions drains one, open a
new stream, load the state it named into a new session and re-send the
request it did not run, instead of raising MontyShutdown; the
session’s suspension and sleep totals carry over. MontyShutdown
is still raised when the resume fails. An ended stream is
never resumed: the request may have run.
def checkout(
*,
script_name: str = 'main.py',
limits: ResourceLimits | None = None,
type_check: bool = False,
type_check_stubs: str | None = None,
type_check_format: TypeCheckFormat | None = None,
type_check_color: bool = False,
assert_message_annotations: bool | int = ...,
print_flush_interval: float | None = None,
os_policy: OSPolicy | None = None,
external_modules: Mapping[str, ExternalModule | Callable[[], Awaitable[ExternalModule]]] | None = None,
ephemeral: bool | None = None,
profile: str | None = None,
) -> AsyncMontySession
Prepare a REPL session served by a dedicated remote stream.
Identical to AsyncMonty.checkout, except for ephemeral and
profile; the stream is opened by async with on the returned
session.
Whether a server that stores sessions may store this one.
True means the server never stores it on its own and it gets
no session_id; False asks for it to be stored; None takes
the server’s default. Servers that store nothing ignore it.
The name of the server-side profile to run the session
under; None takes the server’s default. Sent to the server
as-is; the monty worker itself ignores it.