CodeActAgent (CodeAct)

CodeActAgent ist ein Gegenstück zu LlmAgent, das durch das Schreiben und Ausführen von Code handelt, anstatt jeweils einen einzelnen Tool-Aufruf auszugeben. In jeder Runde erzeugt das Modell ein einzelnes Skript; Tools werden als aufrufbare Funktionen bereitgestellt, die das Skript kombinieren kann; und das Skript kommuniziert sein Ergebnis, indem es einen markierten Wert zurückgibt.

Dies ist das CodeAct-Muster: Statt call tool A → observe → call tool B schreibt das Modell b(a(x)) in einem einzigen Skript, sodass mehrstufige Aufgaben in einer einzigen Runde ausgeführt werden. Es wird durch die Funktion codeact in adk-agent aktiviert.

Wann sollte es verwendet werden?

  • Aufgaben, bei denen mehrere Tools pro Runde verkettet oder kombiniert werden (Datenverarbeitung, Batch- Operationen, Verbindungslogik).
  • Modelle, die für die Codegenerierung nachtrainiert wurden.
  • Workflows, bei denen ein echter Interpreter (z. B. Python) als Aktionssubstrat verfügbar ist.

Für natives Tool-Calling sollte LlmAgent bevorzugt werden. Für eine abgeschottete Datei-/Shell-Coding-Umgebung siehe den Coding-Agenten.

Funktionsweise der Schleife

In jeder Runde:

  1. Das Modell gibt einen einzigen eingefassten Codeblock aus (ein Skript).
  2. Das Skript wird auf einem [CodeRuntime] ausgeführt; Tool-Aufrufe werden an den Host weitergeleitet, der das Tool ausführt und das Skript mit dem Ergebnis fortsetzt.
  3. Das Skript gibt ein markiertes ScriptOutput zurück:
    • observation — wird an das Modell zurückgegeben; die Schleife wird fortgesetzt.
    • error — wird als Nachricht zurückgegeben; die Schleife wird fortgesetzt.
    • final_result — wird an den Aufrufer zurückgegeben; die Schleife endet.
    • transfer_to_agent — übergibt die Kontrolle an einen anderen Agenten; die Schleife endet.

Das Framework ist sprachunabhängig: Das Merkmal CodeRuntime bildet die schrittweise Interpreter-Schnittstelle, und es teilt dem Modell seine eigene Sprache/Umgebung über einen frei formulierten Prompt mit. Der vorgesehene Produktionsadapter kapselt Monty, einen nativen Rust-Python-Interpreter.

Beständigkeit: Anhalten und Fortsetzen

CodeActAgent ist über Aufrufe hinweg zustandslos – der dauerhafte Zustand liegt in der Sitzung, genau wie bei LlmAgent. Zwei Situationen unterbrechen die Ausführung:

  • ein bestätigungsgebundenes Tool, für das noch keine Entscheidung vorliegt (HITL), und
  • ein lang laufendes Tool, dessen Ergebnis außerhalb des Bandes eintrifft.

Bei einer Unterbrechung wird die Fortsetzung des aktiven Interpreters in ein CodeActCheckpoint serialisiert und im Sitzungszustand gespeichert; das nächste run() liest sie zurück und setzt die Ausführung fort – die Bestätigungsentscheidung trifft über RunConfig::tool_confirmation_decisions ein, und ein lang laufendes Ergebnis trifft als FunctionResponse in der nächsten Nachricht ein. Inline-Toolaufrufe werden von Write-ahead-(SAVE-BEFORE-) und SAVE-AFTER-Prüfpunkten eingerahmt: Sobald der SAVE-AFTER-Prüfpunkt dauerhaft gespeichert wurde, wird die Wiederherstellung mit dem gespeicherten Ergebnis fortgesetzt und das Tool niemals erneut ausgeführt. Ein Absturz in dem kurzen Zeitfenster nach dem Seiteneffekt eines Tools, aber bevor dessen SAVE-AFTER-Prüfpunkt gespeichert wurde, führt dazu, dass das Tool bei der Wiederherstellung erneut ausgeführt wird. Daher sollten Tools, die nicht idempotent sind, sich dagegen absichern (dieselbe At-least-once-Grenze wie bei LlmAgent).

Dies erfordert eine Laufzeitumgebung, die einen pausierten Aufruf als Snapshot speichern kann. Eine Laufzeitumgebung, die dies nicht kann, führt lang laufende Tools inline aus und lehnt Bestätigungspausen ab.

Erstellen eines CodeActAgent

use adk_agent::codeact::CodeActAgent;
use std::sync::Arc;

// `model` implements `adk_core::Llm`; `runtime` implements `CodeRuntime`.
let agent = CodeActAgent::builder()
    .name("analyst")
    .model(model)
    .runtime(runtime)
    .instruction("Prefer concise, composable steps.")
    .tool(Arc::new(load_csv_tool))
    .output_key("report")
    .build()?;

model und runtime sind erforderlich; für alles andere gibt es einen Standardwert.

Parität mit LlmAgent

Der Builder spiegelt LlmAgentBuilder wider:

  • Modell: generate_content_config sowie die Kurzformen temperature/top_p/top_k/ max_output_tokens.
  • Anweisungen: instruction/instruction_provider, global_instruction/global_instruction_provider, mit {state.key}-Template-Injektion; außerdem Skills (Funktion skills).
  • Verlauf: include_contents.
  • Tools: statische tool und aufrufbezogene toolset; tool_timeout, default_retry_budget/tool_retry_budget, circuit_breaker_threshold und on_tool_error-Fallbacks.
  • Autorisierung: ToolConfirmationPolicy (require_tool_confirmation/require_tool_confirmation_for_all).
  • Übertragung: sub_agent und disallow_transfer_to_parent/ disallow_transfer_to_peers.
  • Ausgabe: output_key, output_schema/output_type mit einer Korrektur-Wiederholungsschleife (output_max_retries).
  • Callbacks: before_callback/after_callback, before_model_callback/after_model_callback und before_tool_callback/after_tool_callback/after_tool_callback_full. Callbacks nach der Tool-Ausführung können strukturierte Ausführungsmetadaten über CallbackContext::tool_outcome() untersuchen.
  • Feature-gesteuert: Guardrails für Ein- und Ausgaben (guardrails) sowie die EnhancedPlugin-Pipeline (enhanced-plugins).

Jeder Tool-Aufruf erhält ein neues ToolContext, das die Aufruf-ID des Interpreters überträgt und Artefakte, Speicher, gemeinsam genutzten Zustand, Benutzerbereiche und Geheimnisse an den laufenden Aufruf delegiert — dadurch verhält sich ein Tool unter CodeActAgent oder LlmAgent identisch.

Bewusste Unterschiede

  • Das Sandboxing der Codeausführung liegt in der Verantwortung von CodeRuntime und ist kein nachträglich hinzugefügtes Zusatzmodul.
  • Die Tool-Verteilung erfolgt absichtlich sequenziell (eine einzelne Fortsetzung wird an einer Aufrufgrenze als Snapshot gespeichert), daher gibt es keine parallelen tool_execution_strategy.
  • Es gibt keine Builder-Option skip_summarization — das Modell beendet die Schleife selbst über final_result — allerdings beendet ein Tool, das skip_summarization für seine Aktionen setzt, weiterhin die Ausführung.

Beispiel

Eine ausführbare, von Abhängigkeiten freie End-to-End-Demo — ein eigenständiges CodeRuntime sowie ein deterministisches Modell — befindet sich unter examples/codeact_agent:

cargo run --manifest-path examples/codeact_agent/Cargo.toml

Implementieren eines CodeRuntime

Ein CodeRuntime analysiert ein Skript und führt es schrittweise aus, wobei jeweils ein externer Aufruf sichtbar gemacht wird:

pub trait CodeRuntime: Send + Sync {
    fn start(&self, script: &str, script_name: &str) -> Result<RunStep, RuntimeError>;
    fn resume(&self, snapshot: &[u8], with: ResumeWith) -> Result<RunStep, RuntimeError>;
    fn capabilities(&self) -> RuntimeCapabilities { /* default */ }
    fn render_tools(&self, tools: &[Arc<dyn Tool>]) -> String { /* default */ }
}
  • RunStep ist eine Menge von Struct-Varianten — Call { call, stdout }, Complete { value, stdout } und Raised { message, stdout }. Erzeuge sie mit den Hilfsfunktionen RunStep::call / RunStep::complete / RunStep::raised und füge erfasste Ausgaben mit .with_stdout(..) hinzu. RunStep::Call macht genau einen ausstehenden Aufruf sichtbar; setze ihn mit einem Wert oder einem Fehler fort oder dump() seine Fortsetzung, um ihn auszusetzen. Das stdout, das eine Laufzeitumgebung anhängt, wird wieder an das Modell übergeben und in Checkpoints gespeichert, sodass es das Aussetzen und Fortsetzen übersteht.
  • Ein PendingCall meldet seine Argumente so, wie der Interpreter sie erzeugt hat — positional_args() und keyword_args() getrennt. Ordne Positionsargumente nicht selbst Namen zu: Der Treiber bindet sie zentral über adk_agent::codeact::bind_call_args an die Parameter des Tools, sodass eine Laufzeitumgebung am Aufrufübergang kein Tool-Schema benötigt und render_tools eine reine Funktion des Tool-Slices sein kann.
  • Skript- vs. Host-Fehler. Alles, was das Modell durch das Schreiben eines anderen Codes beheben könnte — ein Syntax-/Analysefehler, eine nicht abgefangene Ausnahme oder eine Abbruch wegen eines Ressourcenlimits — ist ein RunStep::Raised (eine undurchsichtige Zeichenkette, die unverändert an das Modell zurückgegeben wird). RuntimeError ist echten Host-Fehlern vorbehalten (Snapshot- (De-)Serialisierung, interne Interpreterfehler) und bricht die Ausführung ab.
  • RuntimeCapabilities::supports_suspension muss true sein, um HITL und langfristiges Zurückstellen zu ermöglichen; prompt beschreibt die Sprache und Umgebung für das Modell.

Eine vollständige, minimale Implementierung, die das Aussetzen und Fortsetzen unterstützt, findest du unter examples/codeact_agent/src/runtime.rs.

Python über Monty

Der für den Produktionseinsatz vorgesehene Adapter ist adk-codeact-monty, ein von Pydantic Monty unterstütztes CodeRuntime. Er ermöglicht es dem Modell, durch das Schreiben von Python zu handeln, wird ohne Container oder Unterprozess innerhalb desselben Prozesses ausgeführt und erstellt einen Snapshot eines pausierten Laufs als Bytes – genau das, was zum Anhalten und Fortsetzen benötigt wird. Der Monty-Interpreter wird über den embedded-python-Kernel von adk-code verwendet, der die monty-Crates an einer Stelle festlegt; rustc 1.95+ ist erforderlich.

[dependencies]
adk-agent = { version = "2.1.0", features = ["codeact"] }
adk-codeact-monty = "2.1.0"

Oder über das übergeordnete Crate (re-exportiert als adk_rust::codeact_monty):

[dependencies]
adk-rust = { version = "2.1.0", features = ["minimal", "codeact-monty"] }
use adk_codeact_monty::MontyRuntime;

// Conservative default resource limits (per-advance time + memory caps) make
// `new()` safe for untrusted, LLM-generated code.
let runtime = Arc::new(MontyRuntime::new());

// Tighten or relax with the builder; `unlimited()` removes the caps for
// trusted scripts only.
let runtime = Arc::new(
    MontyRuntime::builder()
        .max_duration(std::time::Duration::from_secs(2))
        .max_memory(64 * 1024 * 1024)
        .build(),
);

Betriebssystemzugriff

Betriebssystemeffekte, die ein Skript versucht auszuführen – Lese-/Schreibzugriffe auf das Dateisystem, os.getenv/os.environ und date.today()/datetime.now() – werden direkt gemäß einer vom Host kontrollierten Richtlinie verarbeitet. Sie sind keine Tools und halten die Agentenschleife niemals an. Standardmäßig ist eine Laufzeit vollständig sandboxed (kein Dateisystemzugriff, eine leere Umgebung, Host-Uhr aktiviert). Mit dem Builder kann gezielter Zugriff gewährt werden:

use adk_codeact_monty::{MontyRuntime, PathAccess};

let runtime = Arc::new(
    MontyRuntime::builder()
        // Mount host directories at virtual paths; Monty enforces the boundary
        // (canonicalization + symlink-escape detection) so a script can never
        // escape a mount. Reads/writes outside every mount raise PermissionError.
        .allow_path("/data", "/srv/agent/data", PathAccess::ReadOnly)
        .allow_path("/out", "/srv/agent/out", PathAccess::ReadWrite)
        // Expose an explicit environment map to os.getenv / os.environ. Empty by
        // default — the host process environment is never exposed implicitly.
        .environ_var("PROJECT", "acme")
        // date.today() / datetime.now() read the host clock (enabled by default).
        .system_clock(true)
        .build(),
);

Netzwerk- und Unterprozesszugriff verfügen über keine Monty-Schnittstelle für Betriebssystemaufrufe und bleiben unabhängig von der Richtlinie nicht verfügbar. Der gewährte Zugriff wird dem Modell in der Systemaufforderung beschrieben, damit es weiß, welche Pfade es lesen oder schreiben darf und welche Umgebungsvariablen vorhanden sind.

Monty implementiert nur eine Teilmenge von pathlib.Path. Wenn Pfade eingebunden sind, listet die Aufforderung daher die exakt unterstützten Methoden auf (jede andere löst AttributeError aus):

  • Lesen/Abfragen (beliebiger Mount): exists(), is_file(), is_dir(), is_symlink(), read_text(), read_bytes(), stat(), iterdir(), resolve(), absolute(), open("r").
  • Schreiben (nur Read-Write-Mounts): write_text(), write_bytes(), append_text(), append_bytes(), mkdir(), unlink(), rmdir(), rename(), open("w")/open("a").
  • Reine Pfadoperationen (keine I/O): der Operator / und joinpath(), is_absolute(), with_name(), with_stem(), with_suffix(), as_posix() sowie die Eigenschaften .name, .parent, .stem, .suffix, .suffixes, .parts.

Werkzeuge werden über eine einzige integrierte Funktion aufgerufen, call_tool("name", {"arg": value, ...}) — die einzige Möglichkeit, ein Werkzeug aufzurufen; sie befinden sich niemals als ungebundene aufrufbare Objekte im Gültigkeitsbereich. Der Werkzeugname ist ein Zeichenfolgenliteral und jedes Argument ist ein Eintrag mit Zeichenfolgenschlüssel in einem einzelnen Dict, sodass der tatsächliche Name innerhalb der serialisierten Fortsetzung übertragen wird (und Suspend/Resume ohne hostseitige Namenstabelle übersteht), ein Werkzeug und jedes Argument beliebige Namen tragen kann (keine gültigen Python-Bezeichner wie "fetch-cart", kein Python-Schlüsselwort und nicht einmal "call_tool"), und der Treiber die Einträge des Dicts exakt nach Namen bindet — ohne Positionsinferenz. Jedes Werkzeug erscheint in der Eingabeaufforderung als eine call_tool("name", {...})-Verwendungszeile mit seinen Parametern und seiner Beschreibung. Jede andere Form — ein ungebundenes fetch_cart(...), Schlüsselwortargumente, ein Argument, das kein Dict ist, oder ein Schlüssel, der keine Zeichenfolge ist — wird mit einem korrigierenden Fehler abgewiesen, statt stillschweigend weitergeleitet zu werden, sodass das Modell genau eine Aufrufform erlernen muss.

Das ausführbare examples/codeact_monty_agent führt ein CodeActAgent vollständig offline gegen echtes Python aus.

CodeActAgent (CodeAct) - ADK-Rust Dokumentation | ADK-Rust