Websocket Client
A pool of remote monty workers reached over a WebSocket instead of local subprocesses — the intended peer is
monty-server.
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.
MontyDisconnectError means the connection closed mid-session.
It does not distinguish a worker crash from a server policy drop; check out a new session.
MontyShutdown means the server declined the next request because it is shutting down.
That request did not run.
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.
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.
Async context manager owning a pool of remote monty workers reached over a
WebSocket. The intended peer is monty-server (the production server: one
monty subprocess child per connection, plus capacity/timeout policy and
graceful drain), but any server that accepts the connection and bridges to
a worker fits — a relay pairing it with a child that dialed in from the
other end, or the dev relay scripts/websocket_relay.py.
Like AsyncMonty, but instead of spawning local subprocesses each checkout
dials the configured URL. There is no sync counterpart — remote turns are
network-bound. checkout() yields the same AsyncMontySession.
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) closes the connection and
raises MontyDisconnectError.
async with AsyncMontyWebsocket('ws://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,
connect_headers: Callable[[], Mapping[str, str]] | None = None,
feed_duration_limit_grace: float | None = 1.0,
turn_duration_limit_grace: float | None = 1.0,
) -> Self
Configure a remote worker pool; connections are made by async with and
each checkout (no workers are pre-warmed).
url : str
ws:///wss:// URL to dial — a relay, or any server that
bridges to a worker. Dialed verbatim; any session/rendezvous routing the URL
needs (e.g. a /<uuid>/parent path for a relay) must already be
in it.
Cap on concurrent connections (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. This also
bounds the wait when a relay accepts the connection but never
produces a worker. Pass None to wait indefinitely.
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 (or pass None) when installing
dependencies over the WebSocket transport.
Called once per session, as it is entered and before
any wait for pool capacity, to produce extra str to str
headers for that connection’s WebSocket upgrade request — 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; duplicate names are last-wins, also over the default
user-agent and the traceparent the Logfire integration
adds, and a malformed name or value raises RuntimeError as
the session is entered.
Seconds the parent waits past a feed’s
max_feed_duration_secs before killing the worker, giving the
sandbox time to raise TimeoutError itself rather than the
session dying with its worker. None disables this backstop.
The same, for max_turn_duration_secs.
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,
) -> AsyncMontySession
Prepare a REPL session served by a dedicated remote connection.
Identical to AsyncMonty.checkout; the connection is opened by
async with on the returned session.