simbiot-harness/venv/lib/python3.12/site-packages/pydantic_monty/_monty.pyi
Simbiot Harness b60439152b Simbiot Harness v0.1: тактовая лестница на Strands + CQRS/ES-спина
- harness/es.py: append-only event store + проекции (образец — кооп-портал)
- harness/levels.py: уровни 1с/1м/1ч/1д, контекст ∝ масштабу
- harness/scheduler.py: уровневые потоки, идемпотентные тики, absorbed-стабы
- harness/isolated_runner.py + _llm_worker.py: LLM-тик в сабпроцессе
  (глобальный asyncio-рантайм Strands не переживает 2+ create_harness)
- cli.py: init/task/tasks/status/events + requeue (написан самим минутным агентом)
- 9router: отдельный ключ sk-simbiot, модель auto (glm-5.3 первым)
2026-09-29 15:28:50 +00:00

1274 lines
52 KiB
Python

import uuid
from collections.abc import Mapping
from pathlib import Path
from typing import Any, Callable, Literal, NoReturn, final
from typing_extensions import Self
from . import (
AsyncSnapshot,
ExternalResult,
ExternalSettledResult,
OsHandler,
PrintCallback,
ResourceLimits,
SyncSnapshot,
TypeCheckFormat,
)
from .os_access import AbstractOS, OsFunction
__all__ = [
'__version__',
'_install_telemetry',
'NOT_HANDLED',
'AsyncMonty',
'AsyncMontySession',
'AsyncMontyWebsocket',
'CollectStreams',
'CollectString',
'Frame',
'Monty',
'MontyClassProxy',
'MontyClassTypeProxy',
'MontyConversionError',
'MontyCrashedError',
'MontyDisconnectError',
'MontyError',
'MontyFileHandle',
'MontySession',
'MontyShutdown',
'MontySyntaxError',
'MontyRuntimeError',
'MontyTypingError',
'MountDir',
'MontyComplete',
'FunctionSnapshot',
'NameLookupSnapshot',
'FutureSnapshot',
'AsyncFunctionSnapshot',
'AsyncNameLookupSnapshot',
'AsyncFutureSnapshot',
]
__version__: str
# Private native hook behind pydantic_monty.instrument_telemetry.
def _install_telemetry(tracer: Any | None, meter: Any | None, logger: Any | None) -> None: ...
NOT_HANDLED = object()
@final
class CollectStreams:
"""Collect printed output as `(stream, text)` tuples.
Defaults to a 10 MiB cap. Pass `max_bytes=None` to disable (trusted hosts).
Exceeding the cap fails the feed with `MontyRuntimeError` wrapping a
`MemoryError`. Not covered by `ResourceLimits.max_memory`.
The cap includes a fixed per-entry overhead (many tiny fragments).
"""
def __new__(cls, max_bytes: int | None = 10 * 1024 * 1024) -> CollectStreams: ...
@property
def output(self) -> list[tuple[Literal['stdout', 'stderr'], str]]:
"""Collected output so far."""
@final
class CollectString:
"""Collect printed output as one concatenated string.
Defaults to a 10 MiB cap. Pass `max_bytes=None` to disable (trusted hosts).
Exceeding the cap fails the feed with `MontyRuntimeError` wrapping a
`MemoryError`. Not covered by `ResourceLimits.max_memory`.
"""
def __new__(cls, max_bytes: int | None = 10 * 1024 * 1024) -> CollectString: ...
@property
def output(self) -> str:
"""Collected output so far."""
@final
class MountDir:
"""A mount point mapping a virtual path to a host directory.
The directory is opened here, and every feed this mount is passed to serves
that same directory — so build one and reuse it. `'overlay'` writes live in
each feed's own table and are discarded when the feed ends.
**Warning: `mode='read-write'` writes files from untrusted code to your
real filesystem.**
Those files are untrusted input; do not execute them. Importing counts as
executing, and the import can be indirect: with a directory on `sys.path`
mounted, sandboxed code can write `json.py`, or any module not yet
imported, and the next `import` runs it. That includes imports made by
`pydantic_monty` itself. `sys.path[0]` is the script's directory, or the
cwd for `python -m`, `python -c` and the REPL.
Tools also read files without an explicit import: `conftest.py`,
`sitecustomize.py`, `.git/hooks/*`, `Makefile`, `.env`, `__pycache__`.
The `'overlay'` default keeps writes in memory, so nothing reaches the host
filesystem. Use `'read-write'` only with a directory that contains no code
or config and is not on `sys.path` or any other execution path.
```python
from pathlib import Path
from pydantic_monty import Monty, MountDir
with Monty() as pool, MountDir(host_path=Path('host-dir'), virtual_path='/data') as mount:
with pool.checkout() as session:
contents = session.feed_run("open('/data/notes.txt').read()", mount=mount)
```
The directory is held open from construction until the mount is closed, not
for the duration of a feed — so reusing one across feeds is free, and works
the same inside a `with` block or outside it. `with` (or `close()`) is what
hands the directory back; feeds passed a closed mount raise `ValueError`.
Without it the directory stays open until the object is collected: a file
descriptor on Unix, but on Windows that blocks renaming or deleting it.
"""
host_path: str
virtual_path: str
mode: Literal['read-only', 'read-write', 'overlay']
write_bytes_limit: int | None
memory_usage_limit: int
def __new__(
cls,
*,
host_path: str | Path,
virtual_path: str,
mode: Literal['read-only', 'read-write', 'overlay'] = 'overlay',
write_bytes_limit: int | None = None,
memory_usage_limit: int = 100_000_000,
) -> MountDir:
"""Configure a mount point; validation happens here, not at feed time.
All arguments are keyword-only: mount tools disagree on host-first
(docker `-v`) vs virtual-first (nginx `alias`) ordering, so requiring
names removes the ambiguity.
Arguments:
host_path: Real host directory to expose. Opened at construction;
raises if it doesn't exist, isn't a directory, or cannot be
opened — on macOS/BSD a search-only (`0o111`) directory is not
mountable, though Linux accepts one. Sandbox code can never see
this path or reach outside it. The mount tracks the directory
itself rather than its name, so renaming it on the host does
not detach the mount; on Windows the open handle prevents the
host renaming or deleting it at all while the mount lives.
Symlinks inside it are followed only if their targets are
relative — an absolute target raises `PermissionError` in the
sandbox even when it points back into the same mount.
virtual_path: Absolute POSIX-style path prefix inside the sandbox
(e.g. `'/data'`), regardless of host OS. Raises `TypeError`
if not absolute.
mode: `'read-only'` — reads only, writes raise `PermissionError`;
`'read-write'` — writes through to the host directory, where
the files persist after the feed (see the warning above);
`'overlay'` (default) — reads fall through to the host, writes
are captured in memory per feed and discarded when it ends.
write_bytes_limit: Cap on cumulative bytes written through the
mount within one feed; exceeding it raises `OSError` in the
sandbox. `None` (default) means unlimited.
memory_usage_limit: Per-mount budget in bytes (default 100 MB,
matches DEFAULT_MEMORY_USAGE_LIMIT in rust) shared by retained
overlay data and transient filesystem results; an operation that
would exceed it raises `MemoryError` in the sandbox.
"""
def close(self) -> None:
"""Release the open host directory. Idempotent.
Feeds passed this mount afterwards raise `ValueError`; the attributes
above keep answering. Only Windows needs this — it refuses to rename or
delete a directory while a handle to it is open — but `MountDir` also
works as a context manager, which closes on exit.
"""
def __enter__(self) -> MountDir: ...
def __exit__(self, *args: object) -> bool: ...
class MontyError(Exception):
"""Base exception for all Monty interpreter errors.
Catching `MontyError` will catch syntax, runtime, and typing errors from Monty.
This exception is raised internally by Monty and cannot be constructed directly.
"""
def exception(self) -> BaseException:
"""Returns the inner exception as a Python exception object."""
def __str__(self) -> str:
"""Returns the exception message."""
@final
class MontySyntaxError(MontyError):
"""Raised when Python code has syntax errors or cannot be parsed by Monty.
Inherits exception(), __str__() from MontyError.
"""
def traceback(self) -> list[Frame]:
"""Returns the Monty traceback as a list of Frame objects."""
def display(self, format: Literal['traceback', 'type-msg', 'msg'] = 'traceback') -> str:
"""Returns formatted exception string.
Args:
format: 'traceback' - full traceback with exception
'type-msg' - 'ExceptionType: message' format
'msg' - just the message
"""
@final
class MontyTypingError(MontyError):
"""Raised when type checking rejects a fed snippet.
Type checking runs inside the worker subprocess; the diagnostics arrive
pre-rendered as text, in the `type_check_format` chosen at checkout.
Inherits exception(), __str__() from MontyError.
Cannot be constructed directly from Python.
"""
def display(self) -> str:
"""Returns the rendered type-check diagnostics."""
@final
class MontyRuntimeError(MontyError):
"""Raised when Monty code fails during execution.
Inherits exception(), __str__() from MontyError.
Additionally provides traceback() and display() methods.
"""
def traceback(self) -> list[Frame]:
"""Returns the Monty traceback as a list of Frame objects."""
def display(self, format: Literal['traceback', 'type-msg', 'msg'] = 'traceback') -> str:
"""Returns formatted exception string.
Args:
format: 'traceback' - full traceback with exception
'type-msg' - 'ExceptionType: message' format
'msg' - just the message
"""
@final
class MontyConversionError(MontyError):
"""Raised when a host value cannot be converted across the Monty/host boundary.
A value Monty cannot represent — an `external_lookup` entry or an `inputs`
value of an unsupported type — rejects the feed with this error rather than
crossing into the sandbox. Inherits `exception()` (a native `TypeError`) and
`__str__()` (the conversion message) from `MontyError`.
"""
@final
class Frame:
"""A single frame in a Monty traceback."""
@property
def filename(self) -> str:
"""The filename where the code is located."""
@property
def line(self) -> int:
"""Line number (1-based)."""
@property
def column(self) -> int:
"""Column number (1-based)."""
@property
def end_line(self) -> int:
"""End line number (1-based)."""
@property
def end_column(self) -> int:
"""End column number (1-based)."""
@property
def function_name(self) -> str | None:
"""The name of the function, or None for module-level code."""
@property
def source_line(self) -> str | None:
"""The source code line for preview in the traceback."""
def dict(self) -> dict[str, int | str | None]:
"""dict of attributes."""
@final
class MontyFileHandle:
"""Host-side handle to a file opened inside a Monty sandbox.
Plain data holder — Monty never gives the host a live OS file descriptor.
Exposed to callbacks (e.g. as the first argument of an `open` result or
a `read`/`write` request) so they can route on `path` and branch on
`mode`/`binary`/`readable`/`writable` without re-parsing the mode string.
Construct one from a Python `open` OS handler to return a handle back to
Monty: `MontyFileHandle('/data/foo.txt', 'r')`. The `mode` is canonicalized
at construction (`'rt'` → `'r'`, `'r+b'` → `'rb+'`).
"""
def __new__(cls, path: str, mode: str, *, position: int = 0) -> MontyFileHandle:
"""Construct a `MontyFileHandle` to return from an `open` OS callback.
Arguments:
path: Virtual sandbox path of the opened file (POSIX-style).
mode: Python `open()` mode string. Parsed and canonicalized at
construction, so `'rt'` becomes `'r'` and `'r+b'` becomes
`'rb+'`. Raises `ValueError` for malformed or unsupported
modes (e.g. `'x'`).
position: Initial position for sized/line/seek operations (char
index in text mode, byte index in binary mode). Almost always
`0` for a freshly opened file.
"""
@property
def path(self) -> str:
"""Virtual sandbox path of the open file (always POSIX-style, never a host path)."""
@property
def mode(self) -> str:
"""Canonical Python `open()` mode string for this file (e.g. `'r'`, `'rb+'`, `'w'`)."""
@property
def position(self) -> int:
"""Current position for sized/line/seek operations.
Char index in text mode, byte index in binary mode. `0` for a freshly
opened file.
"""
@property
def binary(self) -> bool:
"""`True` if the mode opens the file in binary form (`'rb'`, `'wb'`, …)."""
@property
def readable(self) -> bool:
"""`True` if the mode permits `read()` (`'r'`, `'r+'`, `'w+'`, `'a+'`, and binary variants)."""
@property
def writable(self) -> bool:
"""`True` if the mode permits `write()` (`'w'`, `'a'`, `'r+'`, `'w+'`, `'a+'`, and binary variants)."""
@final
class MontyClassProxy:
"""Read-only proxy for a class instance the host has no original object for:
a sandbox-defined instance, or a host-sent one after a session restore (the
instance store never survives `load_session` / `load_snapshot`). It keeps the
instance's `id`, so passed back in it resolves to the live sandbox object
(`attributes` not applied; a freed one raises) or, after a restore, re-enters
as a host-backed copy built from `attributes`.
"""
@property
def name(self) -> str:
"""Class name of the instance (e.g. `'Point'`)."""
@property
def id(self) -> uuid.UUID:
"""Identity of the instance, the id the sandbox resolves it by when passed back."""
@property
def is_dataclass(self) -> bool:
"""Whether the instance was a dataclass on the side that produced it."""
@property
def attributes(self) -> dict[str, Any]:
"""The instance's attributes as a plain dict."""
def __repr__(self) -> str: ...
def __eq__(self, value: object, /) -> bool: ...
@final
class MontyClassTypeProxy:
"""Read-only proxy for a host class the session has no class object for: a
`ClassType`, or `type(x)` of a `ClassInstance`, returned after a session
restore. It keeps the class `id`, so passed back in it is the same sandbox
type object.
"""
@property
def name(self) -> str:
"""Name of the class (e.g. `'Point'`)."""
@property
def id(self) -> uuid.UUID:
"""Identity of the class, the id the sandbox resolves it by when passed back."""
@property
def is_dataclass(self) -> bool:
"""Whether the class is a dataclass on the side that produced it."""
@property
def attributes(self) -> dict[str, Any]:
"""The eager class attrs that crossed with the class, as a plain dict."""
def __repr__(self) -> str: ...
def __eq__(self, value: object, /) -> bool: ...
@final
class MontyCrashedError(MontyError):
"""Raised when the sandbox is gone and the session with it.
This is the failure mode subprocess pools exist to contain: the worker is
gone — segfault, allocator abort, external kill, `request_timeout`
watchdog, or a fatal error it announced before exiting — but the host
process is unharmed and the pool replaces it. A remote server also reports
its own failure to start a worker this way. Catch this error to retry or
report; the message says which happened.
`exit_status` is `None` whenever the process could not be reaped, which
includes every remote worker.
Cannot be constructed directly from Python.
"""
@property
def timed_out(self) -> bool:
"""`True` when the pool's `request_timeout` watchdog killed the worker."""
@property
def exit_status(self) -> int | None:
"""Exit code of the dead worker when the OS reported one (signal deaths report `None`)."""
@final
class MontyDisconnectError(MontyError):
"""Raised when a remote worker's connection closed mid-session (WebSocket transport only).
The local analogue is `MontyCrashedError`. The sandbox may have died, or
the server may have dropped the session by policy — an idle, session, or
turn timeout, or being over capacity. A client that only sees the
connection go away cannot tell those apart, so this error claims no more
than that. Retry on a fresh session.
Cannot be constructed directly from Python.
"""
@final
class MontyShutdown(MontyError):
"""Raised when the remote server is shutting down (WebSocket transport only).
Not an error in your code, which is why it is the one exception here
without an `Error` suffix; it still subclasses `MontyError`. The request
that raised it **did not run**, so re-running it on a fresh session is
safe.
`dump` carries the session state captured just before shutdown — restore
it on a new session to carry the session across a server restart, with
`session.load_session` (idle, between feeds) or `session.load_snapshot`
(suspended mid-feed).
One caveat: if the interrupted request was answering a suspension (an
external function or `os` callback), the host already ran that call and
the restored session re-announces it, so it runs again. Make such
callbacks idempotent if you intend to restore across a shutdown.
Cannot be constructed directly from Python.
"""
@property
def dump(self) -> bytes | None:
"""Restorable session dump, or `None` when nothing had run yet or the server's dump failed."""
@final
class Monty:
"""
Sync context manager owning a pool of `monty` subprocess workers.
Monty processes can never be made fully crash-proof against memory errors
(stack overflow, allocator aborts), so execution always happens in worker
subprocesses: a crashed worker raises `MontyCrashedError` and is replaced
transparently — the host Python process is never at risk.
```python
with Monty() as pool:
with pool.checkout() as session:
result = session.feed_run('1 + 1')
```
"""
def __new__(
cls,
*,
binary_path: str | Path | None = None,
min_processes: int = 1,
max_processes: int | None = None,
checkout_timeout: float | None = None,
request_timeout: float | None = None,
max_checkouts_per_worker: int | None = None,
) -> Self:
"""
Configure a worker pool; the workers are spawned by `with`.
Arguments:
binary_path: Path to the `monty` CLI binary. When omitted it is
resolved from the `MONTY_BIN` environment variable, the
environment's scripts directory (where the `pydantic-monty-runtime`
dependency installs it), or `PATH`.
min_processes: Workers spawned eagerly and kept warm.
max_processes: Cap on live workers (defaults to the CPU count);
checkouts beyond it wait for a worker to be returned.
checkout_timeout: Seconds `checkout()` waits for a free worker
before raising `TimeoutError`. `None` waits forever.
request_timeout: Per-turn parent-side deadline in seconds — a worker
that exceeds it is killed and the call raises `MontyCrashedError`
with `timed_out=True`. Trusted synchronous span and log callbacks
delay enforcement while they run. Backstops sandbox `limits`.
max_checkouts_per_worker: Recycle a worker after this many sessions.
"""
def __enter__(self) -> Self: ...
def __exit__(self, *args: Any) -> None: ...
def checkout(
self,
*,
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,
) -> MontySession:
"""
Prepare a REPL session served by a dedicated worker.
The worker is checked out of the pool by `with` on the returned
session and returned to the pool when the `with` block exits.
Arguments:
script_name: Name used in tracebacks and error messages.
limits: Resource limits enforced inside the worker, plus `max_suspensions`,
which the pool enforces itself.
type_check: Type-check each fed snippet before executing it; each
successfully executed snippet is appended to the accumulated
context used for type-checking subsequent snippets.
type_check_stubs: Stub declarations made available to type checking.
type_check_format: How `MontyTypingError` diagnostics are rendered;
`None` (the default) means `'full'`. Chosen here rather than on
the error because the checker's structured diagnostics never
leave the worker.
type_check_color: Render diagnostics with ANSI colour escapes; only
`'full'` and `'concise'` carry colour.
assert_message_annotations: Give failed `assert` statements
pytest-style introspected messages, e.g.
`AssertionError: assert 2 == 5` — a deliberate divergence from
CPython's empty `AssertionError`. On by default; set to `False`
to restore CPython's behavior, or to an int >= 1 to customize
the per-operand repr truncation length (default 120 bytes).
print_flush_interval: How long, in seconds, the worker may hold
buffered `print()` output before sending it, so that a burst
of prints costs one callback rather than one each. `None` (the
default) means 0.005; `0` restores line buffering, delivering
each completed line on its own. Output is always flushed
before a host call and before a run ends, so this only sets
how far live output may lag — never what arrives, or in what
order.
"""
@final
class MontySession:
"""
A REPL session running in a dedicated `monty` subprocess worker.
Obtained from `Monty.checkout()` and used as a context manager. Session
state (globals, functions) persists across `feed_run` calls within the
session.
"""
def __enter__(self) -> Self: ...
def __exit__(self, *args: Any) -> None: ...
def feed_run(
self,
code: str,
*,
inputs: dict[str, Any] | None = None,
external_lookup: dict[str, Any] | None = None,
print_callback: Callable[[Literal['stdout', 'stderr'], str], None]
| CollectStreams
| CollectString
| None = None,
mount: MountDir | list[MountDir] | None = None,
os: Callable[[OsFunction, tuple[Any, ...], dict[str, Any]], Any] | AbstractOS | None = None,
skip_type_check: bool = False,
) -> Any:
"""
Execute one snippet in the worker and return its result.
Blocks the calling thread (with the GIL released) while the worker
runs; external functions, the `os` fallback, and print callbacks are
invoked in this process. Async external functions are not supported
here — use `AsyncMonty`.
Arguments:
code: The Python snippet to execute; its trailing expression value
(if any) is converted to a Python object and returned.
inputs: Values eagerly bound as globals before the snippet runs —
every entry is converted and bound once, whether or not it is
referenced.
external_lookup: Host values resolving names the snippet leaves
undefined, lazily and on demand: a callable entry becomes a host
function the sandbox can call, any other value is converted and
returned directly when the name is read, and an absent name
raises `NameError`. The lazy counterpart to `inputs`; a name
present in both is served by the eager `inputs` binding.
print_callback: Receives the sandbox's `print()` output as
`(stream, text)`, or a `CollectStreams` / `CollectString`
collector. Defaults to the host process stdout/stderr.
mount: Host directories mounted into the sandbox for this feed.
Serviced by the pool on the host side — `'overlay'` writes
live in the pool's per-feed mount table and are discarded when
the feed ends.
os: Fallback handler for OS calls (e.g. filesystem access) not
covered by a mount, invoked as `(function_name, args, kwargs)`,
or an `AbstractOS` instance.
skip_type_check: Skip type checking for this feed even when the
session was checked out with `type_check=True`.
Raises:
MontyRuntimeError: The code raised an exception (session survives).
MontyTypingError: Type checking rejected the snippet (session survives).
MontyCrashedError: The worker process died or hit `request_timeout`;
the session is lost but the pool replaces the worker.
"""
def feed_start(
self,
code: str,
*,
inputs: dict[str, Any] | None = None,
external_lookup: dict[str, Any] | None = None,
print_callback: PrintCallback | None = None,
mount: MountDir | list[MountDir] | None = None,
os: OsHandler | None = None,
skip_type_check: bool = False,
) -> SyncSnapshot:
"""
Start a snippet and return a snapshot at each external call, OS call,
name lookup, or future resolution instead of driving to completion.
Answer the snapshot with `snapshot.resume(...)`, which returns the next
snapshot or a `MontyComplete`. Alternatively, supply `external_lookup`
(and/or `os`) and drive the whole snippet with `snapshot.resume_auto()`,
which answers each suspension from them automatically:
```python
snapshot = session.feed_start(code, external_lookup={'fetch': fetch})
while not isinstance(snapshot, MontyComplete):
snapshot = snapshot.resume_auto()
```
Unlike `feed_run`, `external_lookup` is *not* consulted during this
initial drive — external calls and name lookups are still surfaced as
snapshots; it is only captured for later `resume_auto()` calls.
Use `snapshot.dump()` to checkpoint the worker mid-execution and
`load_snapshot` to restore it.
Arguments:
code: The Python snippet to execute; its trailing expression value
(if any) is the `MontyComplete.output` when the feed completes.
inputs: Values eagerly bound as globals before the snippet runs —
every entry is converted and bound once, whether or not it is
referenced.
external_lookup: Host functions and values, by name, that
`resume_auto()` resolves external calls and undefined names
against (as in `feed_run`). Captured for `resume_auto()`; not
used by a plain `resume(...)`.
print_callback: Receives the sandbox's `print()` output as
`(stream, text)`, or a `CollectStreams` / `CollectString`
collector. Defaults to the host process stdout/stderr.
mount: Host directories mounted into the sandbox for the whole feed
(there is no `mount=` on `resume`). `'overlay'` writes live in
the pool's per-feed mount table and are discarded when the feed
ends.
os: Fallback handler for OS calls not covered by a mount, invoked
as `(function_name, args, kwargs)`, or an `AbstractOS` instance.
Consulted only by `resume_auto()` — `feed_start` always surfaces
OS calls as snapshots.
skip_type_check: Skip type checking for this feed even when the
session was checked out with `type_check=True`.
"""
def load_session(self, state: bytes) -> None:
"""
Restore a session between feeds.
This method should take data from `session.dump()` taken when no block of
code is running (i.e. between feeds).
Use `load_snapshot` for a dump taken mid-execution.
The dump restores its own `script_name` /
limits / type-check state (the `checkout()` config for those is not
applied). The class-instance store starts empty — it is host state and
never part of a dump, so restored `ClassInstance` values fall back to
`MontyClassProxy` stand-ins and method calls on them fail. Raises if
the dump is actually a suspended snapshot.
"""
def load_snapshot(
self,
state: bytes,
*,
mount: MountDir | list[MountDir] | None = None,
print_callback: PrintCallback | None = None,
external_lookup: dict[str, Any] | None = None,
os: OsHandler | None = None,
) -> SyncSnapshot:
"""
Restore a snapshot generated while a block of code is running (e.g.
after `feed_start`) and return the re-announced snapshot to resume.
Use `load_session` for a dump taken between feeds.
Valid only on a fresh session, before any feed or load; raises
`RuntimeError` otherwise. The dump restores its own `script_name` /
limits / type-check state (the `checkout()` config for those is not
applied). The class-instance store starts empty — it is host state and
never part of a dump, so restored `ClassInstance` values fall back to
`MontyClassProxy` stand-ins and method calls on them fail. `mount`
re-establishes the suspended feed's mounts, which are never part of the
dump — pass the same mounts the original feed used, or its filesystem
calls degrade into unhandled OS calls. `'overlay'` writes made before
the dump are not preserved (the restored overlay starts empty). Raises
if the dump is actually an idle session.
`external_lookup` / `os` are captured for `resume_auto()`, exactly as on
`feed_start`. One caveat applies to a *restored* snapshot: a restored
`FutureSnapshot`'s pending coroutines are gone (they lived in the
previous process), so `resume_auto()` on it raises — resolve it manually
with `resume({call_id: ...})`.
"""
def dump(self) -> bytes:
"""
Serialize the worker's session state (idle or suspended) to opaque
bytes using monty's existing dump format. The session stays usable.
"""
def install_dependencies(self, requirements: list[str]) -> None:
"""
Install third-party Python packages into the session, making them
importable by subsequent `feed_run` calls. Session-scoped and
repeatable; an empty list is a no-op.
Only supported by an embedded-CPython worker.
Against the pure-Monty sandbox worker, or on a `uv` install failure
(the error carries uv's stderr), raises `MontyRuntimeError`; the
session stays usable. Bounded by the pool's `request_timeout`, so raise
it for large dependency sets.
Requirements are PEP 508 strings, e.g. `["httpx>=0.27", "numpy"]`.
Dependencies a script declares inline via PEP 723 (`# /// script`) are
installed automatically on `feed_run` and need no call here.
"""
@property
def worker_pid(self) -> int | None:
"""OS process id of this session's worker (diagnostics/tests).
`None` when no worker is attached or a turn is currently in flight
on another thread (the getter never blocks on a running turn).
"""
@final
class AsyncMonty:
"""
Async context manager owning a pool of `monty` subprocess workers.
The async counterpart of `Monty`: worker I/O runs off the event loop, and
external functions may be coroutines.
```python
async with AsyncMonty() as pool:
async with pool.checkout() as session:
result = await session.feed_run('1 + 1')
```
"""
def __new__(
cls,
*,
binary_path: str | Path | None = None,
min_processes: int = 1,
max_processes: int | None = None,
checkout_timeout: float | None = None,
request_timeout: float | None = None,
max_checkouts_per_worker: int | None = None,
) -> Self:
"""
Configure a worker pool; the workers are spawned by `async with`.
Arguments are identical to `Monty`.
"""
async def __aenter__(self) -> Self: ...
async def __aexit__(self, *args: Any) -> None: ...
def checkout(
self,
*,
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,
) -> AsyncMontySession:
"""
Prepare a REPL session served by a dedicated worker.
The worker is checked out of the pool by `async with` on the returned
session and returned to the pool when the `async with` block exits.
Arguments are identical to `Monty.checkout`.
"""
@final
class AsyncMontyWebsocket:
"""
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`.
```python
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,
) -> Self:
"""
Configure a remote worker pool; connections are made by `async with` and
each checkout (no workers are pre-warmed).
Arguments:
url: `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.
max_processes: Cap on concurrent connections (defaults to the CPU
count); checkouts beyond it wait.
checkout_timeout: Seconds `checkout()` waits for capacity before
raising `TimeoutError`. `None` waits forever.
request_timeout: 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.
connect_headers: 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.
"""
async def __aenter__(self) -> Self: ...
async def __aexit__(self, *args: Any) -> None: ...
def checkout(
self,
*,
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,
) -> 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.
"""
@final
class AsyncMontySession:
"""
A REPL session running in a dedicated `monty` subprocess worker.
Obtained from `AsyncMonty.checkout()` and used as an async context
manager. Session state (globals, functions) persists across
`feed_run` calls within the session.
"""
async def __aenter__(self) -> Self: ...
async def __aexit__(self, *args: Any) -> None: ...
async def feed_run(
self,
code: str,
*,
inputs: dict[str, Any] | None = None,
external_lookup: dict[str, Any] | None = None,
print_callback: Callable[[Literal['stdout', 'stderr'], str], None]
| CollectStreams
| CollectString
| None = None,
mount: MountDir | list[MountDir] | None = None,
os: Callable[[OsFunction, tuple[Any, ...], dict[str, Any]], Any] | AbstractOS | None = None,
skip_type_check: bool = False,
) -> Any:
"""
Execute one snippet in the worker and return its result.
Worker I/O runs off the event loop; external functions (the callable
entries in `external_lookup`) may be coroutines, awaited concurrently.
See `MontySession.feed_run` for the shared error types.
Arguments:
code: The Python snippet to execute; its trailing expression value
(if any) is converted to a Python object and returned.
inputs: Values eagerly bound as globals before the snippet runs —
every entry is converted and bound once, whether or not it is
referenced.
external_lookup: Host values resolving names the snippet leaves
undefined, lazily and on demand: a callable entry (sync or a
coroutine function) becomes a host function the sandbox can call,
any other value is converted and returned directly when the name
is read, and an absent name raises `NameError`. The lazy
counterpart to `inputs`; a name present in both is served by the
eager `inputs` binding.
print_callback: Receives the sandbox's `print()` output as
`(stream, text)`, or a `CollectStreams` / `CollectString`
collector. Defaults to the host process stdout/stderr.
mount: Host directories mounted into the sandbox for this feed.
Serviced by the pool on the host side — `'overlay'` writes
live in the pool's per-feed mount table and are discarded when
the feed ends.
os: Fallback handler for OS calls (e.g. filesystem access) not
covered by a mount, invoked as `(function_name, args, kwargs)`,
or an `AbstractOS` instance.
skip_type_check: Skip type checking for this feed even when the
session was checked out with `type_check=True`.
"""
async def feed_start(
self,
code: str,
*,
inputs: dict[str, Any] | None = None,
external_lookup: dict[str, Any] | None = None,
print_callback: PrintCallback | None = None,
mount: MountDir | list[MountDir] | None = None,
os: OsHandler | None = None,
skip_type_check: bool = False,
) -> AsyncSnapshot:
"""
Async counterpart of `MontySession.feed_start`: resolves to a snapshot
(whose `resume(...)` / `resume_auto()` is awaitable) or a
`MontyComplete`.
As in the sync version, `external_lookup` (and `os`) are captured for
`await snapshot.resume_auto()` rather than consulted during this initial
drive. A coroutine external answered by `resume_auto()` is awaited
concurrently: it yields an `AsyncFutureSnapshot` whose `resume_auto()`
settles the pending coroutines.
Arguments:
code: The Python snippet to execute; its trailing expression value
(if any) is the `MontyComplete.output` when the feed completes.
inputs: Values eagerly bound as globals before the snippet runs —
every entry is converted and bound once, whether or not it is
referenced.
external_lookup: Host functions and values, by name, that
`resume_auto()` resolves external calls and undefined names
against (as in `feed_run`). Callables may be coroutine
functions. Captured for `resume_auto()`; not used by a plain
`resume(...)`.
print_callback: Receives the sandbox's `print()` output as
`(stream, text)`, or a `CollectStreams` / `CollectString`
collector. Defaults to the host process stdout/stderr.
mount: Host directories mounted into the sandbox for the whole feed
(there is no `mount=` on `resume`). `'overlay'` writes live in
the pool's per-feed mount table and are discarded when the feed
ends.
os: Fallback handler for OS calls not covered by a mount, invoked
as `(function_name, args, kwargs)`, or an `AbstractOS` instance.
Consulted only by `resume_auto()` — `feed_start` always surfaces
OS calls as snapshots.
skip_type_check: Skip type checking for this feed even when the
session was checked out with `type_check=True`.
"""
async def load_session(self, state: bytes) -> None:
"""Async counterpart of `MontySession.load_session`: restore a session between feeds."""
async def load_snapshot(
self,
state: bytes,
*,
mount: MountDir | list[MountDir] | None = None,
print_callback: PrintCallback | None = None,
external_lookup: dict[str, Any] | None = None,
os: OsHandler | None = None,
) -> AsyncSnapshot:
"""
Async counterpart of `MontySession.load_snapshot`.
Restore a snapshot generated while a block of code is running (e.g.
after `feed_start`) and return the re-announced snapshot to resume.
`external_lookup` / `os` are captured for `resume_auto()`, with the same
restored-snapshot caveats as the sync method (a restored `FutureSnapshot`
cannot be driven with `resume_auto()` — its pending coroutines are gone).
"""
async def dump(self) -> bytes:
"""
Serialize the worker's session state (idle or suspended) to opaque
bytes using monty's existing dump format. The session stays usable.
"""
async def install_dependencies(self, requirements: list[str]) -> None:
"""
Async counterpart of `MontySession.install_dependencies`: install
third-party packages into the session (off the event loop) so later
`feed_run` calls can import them. Session-scoped and repeatable; an
empty list is a no-op.
Only supported by an embedded-CPython worker. Against the pure-Monty
sandbox worker, or on a `uv` install failure, raises
`MontyRuntimeError`; the session stays usable. PEP 723 inline
dependencies are installed automatically on `feed_run`.
"""
@property
def worker_pid(self) -> int | None:
"""OS process id of this session's worker (diagnostics/tests).
`None` when no worker is attached or a turn is currently in flight
on another thread (the getter never blocks on a running turn).
"""
@final
class MontyComplete:
"""The result of a completed `feed_start` execution."""
@property
def output(self) -> Any:
"""The final value, converted to a Python object on each access."""
def __repr__(self) -> str: ...
@final
class FunctionSnapshot:
"""A paused execution waiting for an external function or OS call result.
For OS calls `is_os_function` is `True` and `function_name` is the
`OsFunction` name; resume with a value, an exception, or
`resume_not_handled()`.
"""
@property
def script_name(self) -> str: ...
@property
def is_os_function(self) -> bool: ...
@property
def object_id(self) -> uuid.UUID | None:
"""Session uuid of the routed receiver — a host class instance sent
via `ClassInstance`, or a host class sent via `ClassType` (a
classmethod call, or construction, which arrives as `__call__`);
`None` for plain external functions and OS calls. The receiver is not
included in `args`."""
@property
def function_name(self) -> str | OsFunction: ...
@property
def call_id(self) -> int: ...
@property
def args(self) -> tuple[Any, ...]: ...
@property
def kwargs(self) -> dict[str, Any]: ...
def resume(self, result: ExternalResult) -> SyncSnapshot:
"""Resume with the call's result; resumes at most once.
Answers only this call: the result is passed straight through, and
neither the feed's mounts nor the captured `os=` are consulted. Use
`resume_auto()` for those.
"""
def resume_not_handled(self) -> SyncSnapshot:
"""Resume an OS-call snapshot with monty's default unhandled behaviour."""
def resume_auto(self) -> SyncSnapshot:
"""Answer this call automatically, then return the next snapshot (or
`MontyComplete`). Resumes at most once.
An OS call is offered to the feed's mounts first, falling back to the
`os=` captured at `feed_start` / `load_snapshot` and then to monty's
unhandled default. An external call is resolved through
`external_lookup=`; a name absent from it makes the sandbox raise
`NameError` (as in `feed_run`). A coroutine external raises
`RuntimeError` — use `AsyncMonty` for async externals."""
def dump(self) -> bytes:
"""Serialize the suspended worker; restore via `MontySession.load_snapshot`."""
def __repr__(self) -> str: ...
@final
class NameLookupSnapshot:
"""A paused execution waiting for the value of an undefined name, or —
when `object_id` is set — a lazy attribute lookup on a host-backed object
(a `ClassInstance` instance, or a `ClassType` class)."""
@property
def script_name(self) -> str: ...
@property
def variable_name(self) -> str: ...
@property
def object_id(self) -> uuid.UUID | None:
"""Session uuid of the receiver for a lazy attribute lookup; `None`
for a plain undefined-name lookup. An omitted-`value` resume raises
`AttributeError` (not `NameError`) for attribute lookups."""
def resume(self, *, value: Any = ...) -> SyncSnapshot:
"""Resume by binding the name to `value` (any value, including `None`), or
omit `value` to leave the name undefined — the sandbox then raises
`NameError`, or `AttributeError` when `object_id` is set (a lazy
attribute lookup on a host-backed object)."""
def resume_auto(self) -> SyncSnapshot:
"""Answer this name lookup automatically, then return the next snapshot
(or `MontyComplete`). A plain lookup resolves from the captured
`external_lookup=` (an absent name raises `NameError` in the sandbox);
an `object_id` lookup resolves through the sending wrapper's
`lazy_attrs` policy (a denied or absent attribute raises
`AttributeError`; any other exception raised while serving it, or a
value that cannot be converted, is raised inside the sandbox)."""
def dump(self) -> bytes:
"""Serialize the suspended worker; restore via `MontySession.load_snapshot`."""
def __repr__(self) -> str: ...
@final
class FutureSnapshot:
"""A paused execution where every sandbox task is blocked on external futures."""
@property
def script_name(self) -> str: ...
@property
def pending_call_ids(self) -> list[int]: ...
def resume(self, results: dict[int, ExternalSettledResult]) -> SyncSnapshot:
"""Resume with settled results for one or more pending futures (by
`call_id`); a future cannot resolve to another `future`."""
def resume_auto(self) -> NoReturn:
"""Always raises `RuntimeError`: a sync session cannot drive coroutine
externals. Resolve the pending futures manually with `resume({...})`, or
use `AsyncMonty`. Does not consume the snapshot."""
def dump(self) -> bytes:
"""Serialize the suspended worker; restore via `MontySession.load_snapshot`."""
def __repr__(self) -> str: ...
@final
class AsyncFunctionSnapshot:
"""Async sibling of `FunctionSnapshot`; `resume`/`resume_not_handled` are awaitable."""
@property
def script_name(self) -> str: ...
@property
def is_os_function(self) -> bool: ...
@property
def object_id(self) -> uuid.UUID | None:
"""As `FunctionSnapshot.object_id`: the routed receiver's session uuid."""
@property
def function_name(self) -> str | OsFunction: ...
@property
def call_id(self) -> int: ...
@property
def args(self) -> tuple[Any, ...]: ...
@property
def kwargs(self) -> dict[str, Any]: ...
async def resume(self, result: ExternalResult) -> AsyncSnapshot: ...
async def resume_not_handled(self) -> AsyncSnapshot: ...
async def resume_auto(self) -> AsyncSnapshot:
"""Async sibling of `FunctionSnapshot.resume_auto`. A coroutine external
is spawned and answered with a pending future, so other sandbox tasks
keep running; it is later settled by `AsyncFutureSnapshot.resume_auto`."""
def dump(self) -> bytes: ...
def __repr__(self) -> str: ...
@final
class AsyncNameLookupSnapshot:
"""Async sibling of `NameLookupSnapshot`."""
@property
def script_name(self) -> str: ...
@property
def variable_name(self) -> str: ...
@property
def object_id(self) -> uuid.UUID | None:
"""As `NameLookupSnapshot.object_id`: the host object a lazy attribute is read from."""
async def resume(self, *, value: Any = ...) -> AsyncSnapshot: ...
async def resume_auto(self) -> AsyncSnapshot:
"""Async sibling of `NameLookupSnapshot.resume_auto`."""
def dump(self) -> bytes: ...
def __repr__(self) -> str: ...
@final
class AsyncFutureSnapshot:
"""Async sibling of `FutureSnapshot`."""
@property
def script_name(self) -> str: ...
@property
def pending_call_ids(self) -> list[int]: ...
async def resume(self, results: dict[int, ExternalSettledResult]) -> AsyncSnapshot: ...
async def resume_auto(self) -> AsyncSnapshot:
"""Wait for one or more coroutine externals spawned by earlier
`resume_auto` calls to settle, deliver them, and return the next
snapshot. Raises if there are no pending coroutines to await (e.g. a
snapshot restored via `load_snapshot`)."""
def dump(self) -> bytes: ...
def __repr__(self) -> str: ...