
Un intérprete de Python minimalista y seguro escrito en Rust para uso por IA
Experimental - Este proyecto aún está en desarrollo y no está listo para producción.
Un intérprete de Python mínimo y seguro escrito en Rust para uso por parte de la IA.
Monty evita el costo, la latencia, la complejidad y la molestia general de usar un sandbox basado en contenedores completos para ejecutar código generado por LLM.
En cambio, te permite ejecutar de forma segura código Python escrito por un LLM integrado en tu agente, con tiempos de inicio medidos en microsegundos de un solo dígito, no en cientos de milisegundos.
Lo que Monty puede hacer:
sys, os, typing, asyncio, re, datetime, json, (pronto)Lo que Monty no puede hacer:
En resumen, Monty es extremadamente limitado y está diseñado para un caso de uso:
Ejecutar código escrito por agentes.
Para motivación sobre por qué podrías querer hacer esto, consulta:
En términos muy simples, la idea de todo lo anterior es que los LLM pueden trabajar más rápido, de forma más económica y más fiable si se les pide que escriban código Python (o Javascript), en lugar de depender de las llamadas a herramientas tradicionales. Monty hace eso posible sin la complejidad de un sandbox ni el riesgo de ejecutar código directamente en el host.
Nota: Monty se usará (pronto) para implementar codemode en Pydantic AI
Se puede llamar a Monty desde Python, JavaScript/TypeScript o Rust.
Para instalarlo:```bash uv add pydantic-monty
(O `pip install pydantic-monty` para los boomers)
`pydantic-monty` es un metapaquete que combina `pydantic-monty-client` (el módulo `pydantic_monty`) con `pydantic-monty-runtime` (el binario del worker `monty`). Instala `pydantic-monty-client` por sí solo si el binario ya viene de otro lugar.
Uso:```python
from typing import Any
import pydantic_monty
code = """
async def agent(prompt: str, messages: Messages):
while True:
print(f'messages so far: {messages}')
output = await call_llm(prompt, messages)
if isinstance(output, str):
return output
messages.extend(output)
await agent(prompt, [])
"""
type_definitions = """
from typing import Any
Messages = list[dict[str, Any]]
async def call_llm(prompt: str, messages: Messages) -> str | Messages:
raise NotImplementedError()
prompt: str = ''
"""
Messages = list[dict[str, Any]]
async def call_llm(prompt: str, messages: Messages) -> str | Messages:
if len(messages) < 2:
return [{'role': 'system', 'content': 'example response'}]
else:
return f'example output, message count {len(messages)}'
async def main():
async with pydantic_monty.AsyncMonty() as pool:
async with pool.checkout(
script_name='agent.py',
type_check=True,
type_check_stubs=type_definitions,
) as session:
output = await session.feed_run(
code,
inputs={'prompt': 'testing'},
external_lookup={'call_llm': call_llm},
)
print(output)
#> example output, message count 2
if __name__ == '__main__':
import asyncio
asyncio.run(main())
La ejecución se realiza en un grupo de subprocesos worker monty, por lo que incluso un error
de memoria provocado por código adversarial (desbordamiento de pila, aborto del asignador)
nunca puede hacer fallar tu proceso — el worker muere, lanza MontyCrashedError, y
es reemplazado. También hay una API totalmente síncrona:```python
import pydantic_monty
with pydantic_monty.Monty() as pool: with pool.checkout() as session: # session state persists between feed_run calls session.feed_run('x = 21') print(session.feed_run('x * 2')) #> 42
### JavaScript / TypeScript
Para instalar:```bash
npm install @pydantic/monty
El paquete JS es un binding nativo (napi) sobre el mismo pool de workers Rust que
usa el paquete Python — el binding y el binario worker monty se distribuyen mediante
paquetes npm específicos de cada plataforma:```ts
import { Monty } from '@pydantic/monty'
await using pool = await Monty.create() await using session = await pool.checkout()
// session state persists between feedRun calls await session.feedRun('x = 21') console.log(await session.feedRun('x * 2')) // 42
// external functions may be async const result = await session.feedRun('await fetch_data()', { externalLookup: { fetch_data: async () => 'data' }, })
Para navegadores (o en cualquier lugar donde los subprocesos sean imposibles), el mismo paquete
expone una compilación WebAssembly en proceso bajo la subruta `@pydantic/monty/wasm`
(sin aislamiento de fallos: un fallo del sandbox es un fallo del host allí).
### Rust
Para ejecutar código no confiable desde Rust, recomendamos la
crate [`monty-pool`](https://crates.io/crates/monty-pool) en lugar de la API en proceso que se muestra a continuación.
`monty-pool` solo ejecuta código en subprocesos de trabajo de `monty`, lo que ofrece protecciones adicionales:
un fallo provocado por código adversario (desbordamiento de pila, aborto del asignador) mata solo al worker —
el pool detecta la muerte y reemplaza al worker — y un watchdog en el lado del padre puede matar workers
que excedan un tiempo límite estricto. Es el mismo motor sobre el que se construyen los paquetes de Python y JavaScript
mencionados anteriormente. Consulta el [README de monty-pool](https://github.com/pydantic/monty/tree/main/crates/monty-pool)
para más información.
La propia crate `monty` proporciona el intérprete en proceso:```rust
use monty::MontyRun;
use monty_types::{CompileOptions, ResourceTracker, MontyObject, PrintWriter, ResourceLimits};
let code = r#"
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
fib(x)
"#;
let runner = MontyRun::new(code.to_owned(), "fib.py", vec!["x".to_owned()], CompileOptions::default()).unwrap();
let result = runner.run(vec![MontyObject::Int(10)], ResourceTracker::default(), PrintWriter::Stdout).unwrap();
assert_eq!(result, MontyObject::Int(55));
Una sesión de REPL puede serializarse con dump() y restaurarse con Dump::load(). El volcado transporta los metadatos de la sesión (nombre del script, stubs de verificación de tipos) junto con el estado del intérprete, bajo una versión que la compilación de carga comprueba:```rust
use monty::{Dump, MontyRepl, Session, SessionRef, dump};
use monty_types::{CompileOptions, MontyObject, PrintWriter, ResourceTracker};
// Snapshot a session between snippets let mut repl = MontyRepl::new("main.py", ResourceTracker::default(), CompileOptions::default()); repl.feed_run("x = 41", vec![], PrintWriter::Stdout).unwrap(); let bytes = dump("main.py", None, SessionRef::Idle(&repl)).unwrap();
// Later, restore and carry on feeding let Session::Idle(mut restored) = Dump::load(&bytes).unwrap().state else { panic!("dumped an idle session") }; let result = restored.feed_run("x + 1", vec![], PrintWriter::Stdout).unwrap(); assert_eq!(result, MontyObject::Int(42));
`MontyRun` y `RunProgress` no tienen un formato de volcado propio, pero ambos implementan `serde::Serialize`/`Deserialize`, por lo que un host puede serializar código analizado o una ejecución en pausa con el formato que ya utilice.
## Límites de memoria en los workers
El `max_memory` de una sesión lo mide el asignador del worker. El intérprete
informa de un `MemoryError` controlado al superar el límite blando; un límite
duro más alto mata y reemplaza al worker si una sola asignación salta demasiado entre puntos de control.
Consulta [`limitations/resource_limits.md`](https://github.com/pydantic/monty/blob/HEAD/limitations/resource_limits.md) para ver cómo
la superación de un límite se manifiesta a un host, y `monty-alloc` para el
asignador que usan tanto el subproceso como los workers de WebAssembly.
## Integración con PydanticAI
Monty impulsará el modo de código en
[Pydantic AI](https://github.com/pydantic/pydantic-ai). En lugar de realizar
llamadas secuenciales a herramientas, el LLM escribe código Python que llama a tus herramientas
como funciones y Monty lo ejecuta de forma segura.```python test="skip"
import asyncio
import json
import logfire
from httpx import AsyncClient
from pydantic_ai import Agent, RunContext
from pydantic_ai.toolsets.code_mode import CodeModeToolset
from pydantic_ai.toolsets.function import FunctionToolset
from typing_extensions import TypedDict
logfire.configure()
logfire.instrument_pydantic_ai()
class LatLng(TypedDict):
lat: float
lng: float
weather_toolset: FunctionToolset[AsyncClient] = FunctionToolset()
@weather_toolset.tool
async def get_lat_lng(
ctx: RunContext[AsyncClient], location_description: str
) -> LatLng:
"""Get the latitude and longitude of a location."""
# NOTE: the response here will be random, and is not related to the location description.
r = await ctx.deps.get(
'https://demo-endpoints.pydantic.workers.dev/latlng',
params={'location': location_description},
)
r.raise_for_status()
return json.loads(r.content)
@weather_toolset.tool
async def get_temp(ctx: RunContext[AsyncClient], lat: float, lng: float) -> float:
"""Get the temp at a location."""
# NOTE: the responses here will be random, and are not related to the lat and lng.
r = await ctx.deps.get(
'https://demo-endpoints.pydantic.workers.dev/number',
params={'min': 10, 'max': 30},
)
r.raise_for_status()
return float(r.text)
@weather_toolset.tool
async def get_weather_description(
ctx: RunContext[AsyncClient], lat: float, lng: float
) -> str:
"""Get the weather description at a location."""
# NOTE: the responses here will be random, and are not related to the lat and lng.
r = await ctx.deps.get(
'https://demo-endpoints.pydantic.workers.dev/weather',
params={'lat': lat, 'lng': lng},
)
r.raise_for_status()
return r.text
agent = Agent(
'gateway/anthropic:claude-sonnet-4-5',
# toolsets=[weather_toolset],
toolsets=[CodeModeToolset(weather_toolset)],
deps_type=AsyncClient,
)
async def main():
async with AsyncClient() as client:
await agent.run('Compare the weather of London, Paris, and Tokyo.', deps=client)
if __name__ == '__main__':
asyncio.run(main())
Generalmente hay dos respuestas cuando le muestras Monty a la gente:
Donde X es alguna tecnología alternativa. Curiosamente, estas respuestas suelen combinarse, lo que sugiere que la gente aún no ha encontrado una alternativa que les funcione, pero se muestran incrédulos de que realmente no haya una buena alternativa a crear una implementación completa de Python desde cero.
Intentaré repasar las alternativas más obvias y por qué no son adecuadas para lo que queríamos.
NOTA: todas estas tecnologías son impresionantes y tienen usos generalizados; este comentario sobre sus limitaciones para nuestro caso de uso no debe considerarse una crítica. La mayoría de estas soluciones no fueron concebidas con el objetivo de proporcionar un sandbox para LLM, por lo que no son necesariamente excelentes en ello.
Ver ./scripts/startup_performance.py para el script utilizado para calcular los números de rendimiento de inicio.
Detalles de cada fila a continuación:
pip install pydantic-monty o npm install @pydantic/monty, descarga de ~4.5MBdump() y load() hace que sea trivial pausar, reanudar y bifurcar la ejecuciónpython:3.14-alpine pesa 50MB - Docker no se puede instalar desde PyPIVer starlark-rust.
Ejecutar Python en WebAssembly mediante Wasmer.
python/python de wasmer no tiene readme, no tiene licencia, no tiene enlace al código fuente ni indicación de cómo está construido; las versiones subidas recientemente muestran un tamaño de "0B" aunque la descarga es de ~50MB - el proceso de compilación del binario de Python no es claro ni transparente. (Si me equivoco aquí, por favor crea un issue para corregirme)Servicios como Daytona, E2B, Modal.
Hay desafíos similares, más complejidad de configuración pero menor latencia de red para montar tu propio sandbox con k8s.
Ejecutar Python directamente mediante exec() (~0.1ms) o subprocess (~30ms).
exec(), ~30ms para subprocessEl Stack de Pydantic es todo lo que necesitas para lanzar agentes de IA de nivel de producción:
dataclasses| Tecnología | Completitud del lenguaje | Seguridad | Latencia de inicio | FOSS | Complejidad de configuración | Montaje de archivos | Instantáneas |
|---|
| Monty | parcial | estricto | 0.06ms | gratis / OSS | fácil | fácil | fácil |
| Docker | completo | bueno | 195ms | gratis / OSS | intermedio | fácil | intermedio |
| Pyodide | completo | deficiente | 2800ms | gratis / OSS | intermedio | fácil | difícil |
| starlark-rust | muy limitado | bueno | 1.7ms | gratis / OSS | fácil | ¿no disponible? | ¿imposible? |
| WASI / Wasmer | parcial, casi completo | estricto | 66ms | gratis * | intermedio | fácil | intermedio |
| sandboxing service | completo | estricto | 1033ms | de pago | intermedio | difícil | intermedio |
| YOLO Python | completo | inexistente | 0.1ms / 30ms | gratis / OSS | fácil | fácil / aterrador | difícil |