
Um interpretador Python mínimo e seguro escrito em Rust para uso por IA.
Experimental - Este projeto ainda está em desenvolvimento e não está pronto para uso em produção.
Um interpretador Python mínimo e seguro escrito em Rust para uso por IA.
O Monty evita o custo, a latência, a complexidade e o incômodo geral de usar um sandbox baseado em contêiner completo para executar código gerado por LLM.
Em vez disso, ele permite que você execute com segurança código Python escrito por um LLM incorporado ao seu agente, com tempos de inicialização medidos em dígitos únicos de microssegundos, não em centenas de milissegundos.
O que o Monty consegue fazer:
sys, os, typing, asyncio, re, datetime, json, dataclasses (em breve)O que o Monty não pode fazer:
Em resumo, o Monty é extremamente limitado e projetado para um caso de uso:
Executar código escrito por agentes.
Para entender a motivação de por que você pode querer fazer isso, veja:
Em termos muito simples, a ideia de tudo isso é que LLMs podem trabalhar mais rápido, de forma mais barata e mais confiável se forem solicitados a escrever código Python (ou Javascript), em vez de depender do chamamento tradicional de ferramentas. O Monty torna isso possível sem a complexidade de um sandbox ou o risco de executar código diretamente no host.
Nota: o Monty será (em breve) usado para implementar o codemode no Pydantic AI
O Monty pode ser chamado a partir de Python, JavaScript/TypeScript ou Rust.
Para instalar:```bash uv add pydantic-monty
(Ou `pip install pydantic-monty` para os boomers)
`pydantic-monty` é um metapacote que combina `pydantic-monty-client` (o
módulo `pydantic_monty`) com `pydantic-monty-runtime` (o binário do worker
`monty`). Instale apenas `pydantic-monty-client` se o binário já vier de
outro 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())
A execução acontece em um pool de subprocessos workers monty, então até mesmo um erro de memória acionado por código adversarial (estouro de pilha, abort do alocador) nunca pode derrubar seu processo — o worker morre, levanta MontyCrashedError, e é substituído. Também existe uma 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
O pacote JS é um binding nativo (napi) sobre o mesmo pool de workers Rust que o
pacote Python usa — o binding e o binário do worker monty são distribuídos via
pacotes npm específicos de 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 (ou em qualquer lugar onde subprocessos sejam impossíveis), o mesmo pacote
expõe uma compilação WebAssembly em processo sob o subcaminho `@pydantic/monty/wasm`
(sem isolamento de falhas: uma falha da sandbox é uma falha do host nesse caso).
### Rust
Para executar código não confiável a partir do Rust, recomendamos o
crate [`monty-pool`](https://crates.io/crates/monty-pool) em vez da API em processo abaixo.
O `monty-pool` executa código apenas em subprocessos de trabalho `monty`, o que oferece proteções extras:
uma falha causada por código adversário (estouro de pilha, abort do alocador) mata apenas o worker —
o pool detecta a morte e substitui o worker — e um watchdog do lado do pai pode matar workers
que excedam um timeout rígido. É o mesmo motor no qual os pacotes Python e JavaScript acima são
construídos. Consulte o [README do monty-pool](https://github.com/pydantic/monty/tree/main/crates/monty-pool)
para instruções de uso.
O próprio crate `monty` fornece o interpretador em processo:```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));
Uma sessão de REPL pode ser serializada com dump() e restaurada com Dump::load(). O dump carrega os metadados da sessão (nome do script, stubs de verificação de tipo) juntamente com o estado do interpretador, sob uma versão que a build de carregamento verifica:```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` e `RunProgress` não têm formato de dump próprio, mas ambos implementam `serde::Serialize`/`Deserialize`, então um host pode serializar código analisado ou uma execução pausada com qualquer formato que já utilize.
## Limites de memória nos workers
O `max_memory` de uma sessão é medido pelo alocador do worker. O interpretador
relata um `MemoryError` elegante após ultrapassar o limite suave; um limite rígido
mais alto mata e substitui o worker se uma alocação saltar longe demais entre checkpoints.
Veja [`limitations/resource_limits.md`](https://github.com/pydantic/monty/blob/HEAD/limitations/resource_limits.md) para saber como
exceder um limite se manifesta para um host, e `monty-alloc` para o alocador
sob o qual os workers de subprocesso e WebAssembly são executados.
## Integração com PydanticAI
Monty será o motor do code-mode no
[Pydantic AI](https://github.com/pydantic/pydantic-ai). Em vez de fazer
chamadas sequenciais de ferramentas, o LLM escreve código Python que chama suas ferramentas
como funções, e Monty o executa com segurança.```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())
Geralmente há duas reações quando você mostra Monty às pessoas:
Onde X é alguma tecnologia alternativa. Curiosamente, essas respostas costumam vir combinadas, sugerindo que as pessoas ainda não encontraram uma alternativa que funcione para elas, mas ficam incrédulas de que realmente não existe uma boa alternativa para criar uma implementação Python inteira do zero.
Vou tentar percorrer as alternativas mais óbvias e explicar por que elas não são adequadas para o que queríamos.
NOTA: todas essas tecnologias são impressionantes e têm usos difundidos; este comentário sobre suas limitações para o nosso caso de uso não deve ser visto como uma crítica. A maioria dessas soluções não foi concebida com o objetivo de fornecer um sandbox para LLM, e é por isso que elas não são necessariamente ótimas nisso.
Consulte ./scripts/startup_performance.py para ver o script usado para calcular os números de desempenho de inicialização.
Detalhes sobre cada linha abaixo:
pip install pydantic-monty ou npm install @pydantic/monty, download de ~4.5MBdump() e load() torna trivial pausar, retomar e bifurcar a execuçãopython:3.14-alpine tem 50MB - o docker não pode ser instalado via PyPIVeja starlark-rust.
Executando Python em WebAssembly via Wasmer.
python/python não tem readme, nem licença, nem link para o código-fonte e nenhuma indicação de como é construído; as versões enviadas recentemente mostram o tamanho como "0B", embora o download seja de ~50MB - o processo de build do binário Python não é claro nem transparente. (Se eu estiver errado aqui, por favor crie uma issue para me corrigir)Serviços como Daytona, E2B, Modal.
Há desafios semelhantes, mais complexidade de configuração, porém menor latência de rede para montar sua própria configuração de sandbox com k8s.
Executando Python diretamente via exec() (~0.1ms) ou subprocess (~30ms).
exec(), ~30ms para subprocessO Pydantic Stack é tudo o que você precisa para entregar agentes de IA de nível de produção:
| Tecnologia | Completude da linguagem | Segurança | Latência de inicialização | FOSS | Complexidade de configuração | Montagem de arquivos | Snapshotting |
|---|
| Monty | parcial | estrita | 0.06ms | gratuito / OSS | fácil | fácil | fácil |
| Docker | completa | boa | 195ms | gratuito / OSS | intermediária | fácil | intermediário |
| Pyodide | completa | fraca | 2800ms | gratuito / OSS | intermediária | fácil | difícil |
| starlark-rust | muito limitada | boa | 1.7ms | gratuito / OSS | fácil | não disponível? | impossível? |
| WASI / Wasmer | parcial, quase completa | estrita | 66ms | gratuito * | intermediária | fácil | intermediário |
| serviço de sandboxing | completa | estrita | 1033ms | não gratuito | intermediária | difícil | intermediário |
| YOLO Python | completa | inexistente | 0.1ms / 30ms | gratuito / OSS | fácil | fácil / assustadora | difícil |