Python-Codeausführung (Monty)

ADK-Rust führt vom Modell geschriebenes Python im Prozess über den Interpreter Pydantic Monty aus — kein Container, kein Unterprozess, Start in Mikrosekunden. Die Funktionalität wird in zwei Ebenen bereitgestellt:

  • adk-code (embedded-python-Funktion) — MontyExecutorBuilder sowie die beiden Executor-Produkte MontyOneShotExecutor und MontyReplExecutor, die beide CodeExecutor implementieren.
  • adk-tool (code-embedded-python-Funktion) — MontyPythonCodeTool (monty_python_code), das agentenseitige Tool für diese Executor.

Dies ergänzt das containerbasierte PythonCodeTool (python_code), das vollständiges CPython in Docker ausführt — verwenden Sie dieses, wenn Skripte das echte Python-Ökosystem benötigen (pip-Pakete, C-Erweiterungen, die vollständige Standardbibliothek). Monty implementiert eine Teilmenge von Python und bietet dafür Geschwindigkeit innerhalb des Prozesses, serialisierbaren Interpreter-Zustand sowie eine konstruktionsbedingte Garantie ohne Netzwerk und Unterprozesse.

[dependencies]
adk-tool = { version = "2.1.0", features = ["code-embedded-python"] }

Oder über den übergeordneten Crate:

[dependencies]
adk-rust = { version = "2.1.0", features = ["minimal", "code-embedded-python"] }

Einmalige Ausführung vs. REPL

Ein Builder erzeugt beide Produkte; der Modus ist im Typ und nicht in einem Flag codiert:

ModusBuildZustandNebenläufigkeit
Einmaligbuild_one_shot()Frischer Interpreter pro AufrufNebenläufigkeitssicher
REPLbuild_repl()Variablen, Funktionen und Importe bleiben über Aufrufe hinweg erhaltenAufrufe werden pro Sitzung serialisiert
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()?;

Der REPL-Executor speichert den serialisierten Interpreter zwischen den Aufrufen. Monty bewahrt die Sitzung über Ausnahmen auf Python-Ebene hinweg, sodass ein fehlgeschlagenes Snippet den angesammelten Zustand nicht zerstört. Die Lebenszyklusmethoden CodeExecutor verwalten die Sitzung: start() initialisiert sie, stop() verwirft sie, restart() setzt sie zurück, und execute() initialisiert sie vor start() bei Bedarf.

Sicherheitsmodell

Die Isolation kombiniert explizite Richtlinien mit einer Durchsetzung durch Weglassen:

  • Dateisystem. Nur mit allow_path gewährte Verzeichnisse sind erreichbar, jeweils schreibgeschützt oder mit Lese- und Schreibzugriff, über pathlib.Path gegenüber dem virtuellen Einhängepfad. Montys Einhängetabelle setzt die Grenze durch (Kanonisierung und Erkennung von Entkommen über symbolische Verknüpfungen). Jeder andere Pfad löst einen abfangbaren OSError aus (Existenzprüfungen geben False zurück).
  • Umgebung. os.getenv / os.environ lesen ausschließlich die bei der Erstellung gewährte explizite Zuordnung — die Prozessumgebung des Hosts wird niemals offengelegt.
  • Uhr. date.today() / datetime.now() funktionieren nur, wenn .system_clock() gewährt wurde; andernfalls lösen sie OSError aus.
  • Netzwerk und Unterprozesse. Monty verfügt über keine Schnittstelle für beides — unabhängig von der Konfiguration unmöglich.
  • Zeitüberschreitungen. SandboxPolicy::timeout wird auf Montys ResourceLimits::max_duration abgebildet (echte Preemption innerhalb der VM, pro Aufruf). Eine Speicherbegrenzung (standardmäßig 256 MiB) begrenzt den Heap; im REPL-Modus begrenzt sie den kumulativen Sitzungsspeicher.

Berechtigungen vs. Anfrage-Richtlinie. Die Berechtigungen des Builders stellen den maximalen Zugriff dar, den ein Skript haben kann. Die Anfrage SandboxPolicy darf diesen Umfang nur einschränken – eine Anfrage, die die Berechtigungen überschreitet, wird mit ExecutionError::UnsupportedPolicy, das den Überschuss benennt, ausfallsicher abgelehnt, bevor irgendein Code ausgeführt wird. Eine Berechtigung gilt für den gesamten Verzeichnis-Unterbaum: Das Anfordern eines berechtigten Mounts oder eines beliebigen Unterverzeichnisses davon ist erfolgreich, und der effektive Mount ist der angeforderte Pfad, der durch das entsprechende Host-Unterverzeichnis unterstützt wird. Verwende granted_policy(), um genau das anzufordern, was der Executor anbietet.

Die effektive Richtlinie einer REPL-Sitzung darf zwischen Aufrufen nicht variieren; ein Aufruf, dessen Richtlinie von der für die Sitzung festgelegten Richtlinie abweicht, wird abgelehnt, mit dem Hinweis, restart() zu verwenden.

Host-Funktionen

Registrierte Rust-Funktionen (synchron oder asynchron) werden zu aufrufbaren Python-Funktionen und sind für Skripte unter ihrem einfachen Namen sichtbar:

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()?;

Für die vollständige Trait-Form implementiere HostFunction (name, description, optional signature für die Eingabeaufforderung LLM sowie asynchron call mit in JSON umgewandelten Positions- und Schlüsselwortargumenten). Die Validierung der Registry erfolgt bei build_*(): Namen müssen gültige Python-Bezeichner sein, eindeutig sein und dürfen nicht mit integrierten Python-Namen kollidieren.

Innerhalb eines Skripts werden Host-Funktionen synchron aufgerufen – niemals mit await. Ein zurückgegebenes Err wird zu einer abfangbaren Python-Ausnahme, die die Nachricht enthält; der Aufruf eines nicht registrierten Namens löst eine korrigierende Ausnahme aus, in der die registrierten Namen aufgeführt sind. Die Ausführung von Host-Funktionen hat ein eigenes Zeitlimit (host_function_timeout, standardmäßig 30 s), sodass eine hängende Funktion execute() nicht blockieren kann.

Hinweis: Host-Funktionen werden als Host-Code ausgeführt. Sie bilden die eigene Vertrauensgrenze des Benutzers, nicht die von Monty – die Interpreter-Sandbox enthält ihre Nebeneffekte nicht.

Selbstdokumentierende Executoren

Beide Executors implementieren CodeExecutor::prompt_snippet() und stellen ihre aufgebauten Fähigkeiten dar: Modussemantik, Dateisystem-Stammverzeichnisse mit Zugriffsebenen, Namen von Umgebungsvariablen (Werte werden niemals dargestellt), Uhrverfügbarkeit, die Garantie ohne Netzwerkzugriff und ohne Subprozesse, den Ausgabevertrag sowie einen Python-Stub-Block für registrierte Hostfunktionen. MontyPythonCodeTool hängt das Snippet an seine an LLM gerichtete Beschreibung an, sodass Prompt und Verhalten im Interpreter aus derselben Konfiguration abgeleitet werden und nicht voneinander abweichen können.

MontyPythonCodeTool

Das agentenseitige Tool (monty_python_code, Geltungsbereich code:execute) spiegelt JavaScriptCodeTool wider: Fehler als Information JSON, camelCase-Ausgabeschlüssel und ein strukturierter "rejected"-Fallback, wenn das Feature deaktiviert ist.

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() erstellt ein vollständig sandboxed One-Shot-Tool; MontyPythonCodeTool::repl() ein vollständig sandboxed REPL-Tool.

Sitzungsgeltungsbereich

Im REPL-Modus werden Interpretersitzungen anhand der vollständigen Identität der ADK-Sitzung bestimmt – App-Name, Benutzer-ID und Sitzungs-ID –, sodass Zustand niemals zwischen Benutzern durchsickert, selbst wenn sich Sitzungs-ID-Zeichenfolgen bei verschiedenen Benutzern wiederholen. Alle Sitzungen teilen sich dieselben Berechtigungen und dieselbe Hostfunktionsregistrierung – nur der Interpreterzustand ist sitzungsspezifisch. Die Sitzungszuordnung ist durch ein LRU-Limit begrenzt (max_sessions, standardmäßig 100; 0 wird als 1 behandelt); beim nächsten Aufruf einer entfernten Sitzung wird transparent ein neuer Interpreter gestartet.

Toolargumente

ArgumentTypBeschreibung
codestring (erforderlich)Python-Quellcode zur Ausführung
inputanyOptionaler JSON-Wert, der an die Variable input gebunden ist
timeout_secsGanzzahlZeitbudget des Interpreters (Standardwert 30, begrenzt auf 1–300)
resetBoolescher WertNur im REPL-Modus: Persistente Sitzung vor der Ausführung verwerfen

Ausgabehülle

{ "status": "success", "stdout": "", "stderr": "", "output": {"n": 42},
  "stdoutTruncated": false, "stderrTruncated": false, "durationMs": 3 }

Es gibt kein exitCode — die Ausführung erfolgt innerhalb des Prozesses, es wird kein Prozess gestartet; status ist das Signal für Erfolg bzw. Fehlschlag. stdoutTruncated / stderrTruncated melden, wenn die erfasste Ausgabe am Byte-Limit der Sandbox-Richtlinie gekürzt wurde (standardmäßig jeweils 1 MB).

Der Wert des abschließenden Ausdrucks des Skripts wird als output zurückgegeben; die print()-Ausgabe wird als stdout erfasst. Fehlerstatus: "failed" (Python- Ausnahme — Traceback in stderr, einschließlich von Hostfunktionen ausgelöster Ausnahmen), "timeout" (Zeitbudget überschritten), "rejected" (ungültige Argumente oder Funktion deaktiviert). Niemals ein ToolError.

Die Hülle ist in beiden Modi identisch, daher deklariert das Tool sie über Tool::response_schema() — Anbieter, die Antwortschemas bereitstellen, erhalten sie in der Tooldeklaration zusammen mit parameters.

Beziehung zu CodeAct

Der Pfad CodeActAgent + adk-codeact-monty führt ebenfalls Python über Monty aus, jedoch mit ADK Tool-Dispatch aus Skripten heraus (call_tool(...)) und Aussetzen/Fortsetzen über mehrere Agentenrunden hinweg. MontyPythonCodeTool schließt beides bewusst aus — es ist ein eigenständiges Tool zur Codeausführung, dessen Erweiterungsschnittstelle die Hostfunktionsregistrierung ist. Siehe Coding Agent für CodeAct.

Beispiel

examples/monty_python_code_tool führt eine LlmAgent mit einem REPL-Modus- MontyPythonCodeTool aus, der mit einer Lese-/Schreibbereitstellung, einer Umgebungsvariablen und einer registrierten Hostfunktion konfiguriert ist — und demonstriert so die Persistenz von Variablen über mehrere Runden hinweg sowie Aufrufe von Hostfunktionen aus vom Modell geschriebenem Python.