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 (recurso embedded-python) — MontyExecutorBuilder e os dois produtos executores, MontyOneShotExecutor e MontyReplExecutor, ambos implementando CodeExecutor.
  • adk-tool (recurso code-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:

ModoCompilaçãoEstadoConcorrência
Execução únicabuild_one_shot()Interpretador novo a cada chamadaSeguro para concorrência
REPLbuild_repl()Variáveis, funções e importações persistem entre chamadasAs 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_path são acessíveis, cada um como somente leitura ou leitura e gravação, por meio de pathlib.Path contra 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 um OSError capturável (as verificações de existência retornam False).
  • Ambiente. os.getenv / os.environ leem 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, geram OSError.
  • 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 o ResourceLimits::max_duration do 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

ArgumentoTipoDescrição
codestring (obrigatório)Código-fonte Python a ser executado
inputqualquerValor opcional de JSON vinculado à variável input
timeout_secsinteiroOrçamento de tempo do interpretador (padrão 30, limitado entre 1 e 300)
resetbooleanoSomente 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.