Execução de código Python (Monty)
ADK-Rust executa Python escrito pelo modelo no processo por meio do interpretador Pydantic Monty — sem contêiner, sem subprocesso e com inicialização em microssegundos. O recurso é distribuído em duas camadas:
adk-code(recursoembedded-python) —MontyExecutorBuildere os dois produtos executores,MontyOneShotExecutoreMontyReplExecutor, ambos implementandoCodeExecutor.adk-tool(recursocode-embedded-python) —MontyPythonCodeTool(monty_python_code), a ferramenta voltada ao agente sobre esses executores.
Isso complementa PythonCodeTool baseado em contêiner (python_code),
que executa o CPython completo no Docker — use-o quando os scripts precisarem do ecossistema Python real
(pacotes do pip, extensões C e a biblioteca padrão completa). Monty implementa um subconjunto do Python, em troca da velocidade no processo, do estado serializável do interpretador e de uma garantia de ausência de rede e subprocessos que é assegurada por construção.
[dependencies]
adk-tool = { version = "2.1.0", features = ["code-embedded-python"] }
Ou por meio do crate abrangente:
[dependencies]
adk-rust = { version = "2.1.0", features = ["minimal", "code-embedded-python"] }
Execução única vs. REPL
Um builder produz ambos os produtos; o modo é codificado no tipo, não em uma flag:
| Modo | Compilação | Estado | Concorrência |
|---|---|---|---|
| Execução única | build_one_shot() | Interpretador novo a cada chamada | Seguro para concorrência |
| REPL | build_repl() | Variáveis, funções e importações persistem entre chamadas | As chamadas são serializadas por sessão |
use adk_code::{MontyExecutorBuilder, PathAccess};
let builder = MontyExecutorBuilder::new()
.allow_path("/data", "/srv/agent/data", PathAccess::ReadOnly)
.allow_path("/out", "/srv/agent/out", PathAccess::ReadWrite)
.environ_var("PROJECT", "acme")
.system_clock();
let one_shot = builder.clone().build_one_shot()?;
let repl = builder.build_repl()?;
O executor REPL armazena o interpretador serializado entre as chamadas. Monty
preserva a sessão por meio de exceções no nível do Python, portanto um trecho
com falha não destrói o estado acumulado. Os métodos de ciclo de vida CodeExecutor
gerenciam a sessão: start() a inicializa, stop() a descarta, restart() a
redefine e execute() a inicializa de forma tardia antes de start().
Modelo de segurança
O isolamento combina uma política explícita com a aplicação por omissão:
- Sistema de arquivos. Somente os diretórios concedidos com
allow_pathsão acessíveis, cada um como somente leitura ou leitura e gravação, por meio depathlib.Pathcontra o caminho de montagem virtual. A tabela de montagem do Monty aplica o limite (canonicalização + detecção de escape por link simbólico). Qualquer outro caminho gera umOSErrorcapturável (as verificações de existência retornamFalse). - Ambiente.
os.getenv/os.environleem somente o mapa explícito concedido na construção — o ambiente do processo host nunca é exposto. - Relógio.
date.today()/datetime.now()funcionam somente quando.system_clock()foi concedido; caso contrário, geramOSError. - Rede e subprocessos. Monty não oferece uma superfície para nenhum dos dois — são impossíveis independentemente da configuração.
- Tempos limite.
SandboxPolicy::timeouté mapeado para oResourceLimits::max_durationdo Monty (preempção real na VM, por chamada). Um limite de memória (padrão de 256 MiB) restringe o heap; no modo REPL, ele restringe o heap cumulativo da sessão.
Concessões vs. política de solicitação. As concessões do construtor são o acesso máximo que qualquer script pode ter. O SandboxPolicy por solicitação pode apenas restringir dentro delas — uma solicitação que exceda as concessões é rejeitada de forma segura com falha, com ExecutionError::UnsupportedPolicy identificando o excesso, antes que qualquer código seja executado.
Uma concessão abrange toda a subárvore do diretório: solicitar um ponto de montagem concedido ou qualquer subdiretório dele é bem-sucedido, e o ponto de montagem efetivo é o caminho solicitado, respaldado pelo subdiretório correspondente do host. Use granted_policy() para solicitar exatamente o que o executor oferece.
A política efetiva de uma sessão REPL não deve variar entre chamadas; uma chamada cuja política seja diferente da política estabelecida para a sessão é rejeitada com orientação para restart().
Funções do host
Funções Rust registradas (síncronas ou assíncronas) tornam-se funções Python chamáveis, visíveis aos scripts pelo nome simples:
use adk_code::MontyExecutorBuilder;
use serde_json::json;
let executor = MontyExecutorBuilder::new()
.function_fn("row_count", "Count rows in the loaded dataset.", |args, _kwargs| async move {
Ok(json!(args.len()))
})
.build_one_shot()?;
Para a forma completa do trait, implemente HostFunction (name, description, signature opcional para o prompt LLM e call assíncrono com argumentos posicionais e nomeados convertidos por JSON). A validação do registro ocorre em build_*(): os nomes devem ser identificadores Python válidos, exclusivos e não podem entrar em conflito com os elementos integrados do Python.
Dentro de um script, as funções do host são chamadas de forma síncrona — nunca com await. Um Err retornado torna-se uma exceção Python que pode ser capturada e contém a mensagem; uma chamada para um nome não registrado gera uma exceção corretiva listando os nomes registrados. A execução de funções do host tem seu próprio limite de tempo de parede (host_function_timeout, 30 s por padrão), para que uma função travada não possa bloquear execute().
Observação: as funções do host são executadas como código do host. Elas são o próprio limite de confiança do usuário, não o do Monty — o sandbox do interpretador não contém seus efeitos colaterais.
Executores autodescritivos
Ambos os executores implementam CodeExecutor::prompt_snippet(), exibindo suas capacidades
compiladas: semântica dos modos, raízes do sistema de arquivos com níveis de
acesso, nomes de variáveis de ambiente (os valores nunca são exibidos),
disponibilidade do relógio, a garantia de ausência de rede e subprocessos, o
contrato de saída e um bloco de stub Python para funções de host registradas.
MontyPythonCodeTool acrescenta o trecho à sua descrição voltada para LLM, portanto o
prompt e o comportamento no interpretador derivam da mesma configuração e não
podem divergir.
MontyPythonCodeTool
A ferramenta voltada para o agente (monty_python_code, escopo code:execute) espelha
JavaScriptCodeTool: JSON de erro como informação, chaves de saída em camelCase e um
fallback estruturado de "rejected" quando o recurso está desabilitado.
use adk_code::PathAccess;
use adk_tool::MontyPythonCodeTool;
use serde_json::json;
use std::sync::Arc;
let tool = MontyPythonCodeTool::builder()
.allow_path("/out", "/srv/agent/out", PathAccess::ReadWrite)
.environ_var("PROJECT", "acme")
.system_clock()
.function_fn("get_weather", "Current weather for a city.", |args, _kwargs| async move {
Ok(json!({ "temp_c": 21 }))
})
.build_repl()?;
let agent = LlmAgentBuilder::new("data_agent")
.instruction("Use monty_python_code for calculations and data work.")
.model(model)
.tool(Arc::new(tool))
.build()?;
MontyPythonCodeTool::new() cria uma ferramenta totalmente isolada de execução única;
MontyPythonCodeTool::repl() cria uma ferramenta REPL totalmente isolada.
Escopo da sessão
No modo REPL, as sessões do interpretador são identificadas pela identidade
completa da sessão ADK — nome do aplicativo, ID do usuário e ID da sessão —
para que o estado nunca vaze entre usuários, mesmo quando as strings de ID da
sessão se repetem entre usuários. Todas as sessões compartilham as mesmas
concessões e o mesmo registro de funções de host — apenas o estado do
interpretador é específico de cada sessão. O mapa de sessões é limitado por um
limite LRU (max_sessions, 100 por padrão; 0 é tratado como 1); a próxima chamada de
uma sessão removida inicia de forma transparente um novo interpretador.
Argumentos da ferramenta
| Argumento | Tipo | Descrição |
|---|---|---|
code | string (obrigatório) | Código-fonte Python a ser executado |
input | qualquer | Valor opcional de JSON vinculado à variável input |
timeout_secs | inteiro | Orçamento de tempo do interpretador (padrão 30, limitado entre 1 e 300) |
reset | booleano | Somente no modo REPL: descarta a sessão persistente antes da execução |
Invólucro de saída
{ "status": "success", "stdout": "", "stderr": "", "output": {"n": 42},
"stdoutTruncated": false, "stderrTruncated": false, "durationMs": 3 }
Não há exitCode — a execução ocorre no processo atual, nenhum processo é iniciado;
status é o sinal de sucesso/falha. stdoutTruncated / stderrTruncated
informam quando a saída capturada foi truncada no limite de bytes da política do sandbox (1 MB
por padrão para cada uma).
O valor da expressão final do script é retornado como output; a saída de
print() é capturada como stdout. Status de falha: "failed" (exceção do Python
— traceback em stderr, incluindo exceções geradas por funções do host), "timeout" (orçamento de tempo excedido), "rejected" (argumentos inválidos ou recurso desativado). Nunca um ToolError.
O invólucro é fixo nos dois modos, portanto a ferramenta o declara por meio de
Tool::response_schema() — provedores que expõem esquemas de resposta o recebem
na declaração da ferramenta junto com parameters.
Relação com CodeAct
O caminho CodeActAgent + adk-codeact-monty também executa Python via Monty,
mas com o despacho de ADK Tool a partir dos scripts (call_tool(...)) e
suspensão/retomada entre turnos do agente. MontyPythonCodeTool exclui deliberadamente
ambos — é uma ferramenta autocontida de execução de código cuja interface de extensibilidade é
o registro de funções do host. Consulte Agente de codificação para
CodeAct.
Exemplo
examples/monty_python_code_tool executa um LlmAgent com um
MontyPythonCodeTool no modo REPL configurado com uma montagem de leitura e gravação, uma variável
de ambiente e uma função do host registrada — demonstrando a persistência de variáveis entre
múltiplos turnos e chamadas de funções do host a partir de Python escrito pelo modelo.