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:
- Das Modell gibt einen einzigen eingefassten Codeblock aus (ein Skript).
- 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. - Das Skript gibt ein markiertes
ScriptOutputzurü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_configsowie die Kurzformentemperature/top_p/top_k/max_output_tokens. - Anweisungen:
instruction/instruction_provider,global_instruction/global_instruction_provider, mit{state.key}-Template-Injektion; außerdem Skills (Funktionskills). - Verlauf:
include_contents. - Tools: statische
toolund aufrufbezogenetoolset;tool_timeout,default_retry_budget/tool_retry_budget,circuit_breaker_thresholdundon_tool_error-Fallbacks. - Autorisierung:
ToolConfirmationPolicy(require_tool_confirmation/require_tool_confirmation_for_all). - Übertragung:
sub_agentunddisallow_transfer_to_parent/disallow_transfer_to_peers. - Ausgabe:
output_key,output_schema/output_typemit einer Korrektur-Wiederholungsschleife (output_max_retries). - Callbacks:
before_callback/after_callback,before_model_callback/after_model_callbackundbefore_tool_callback/after_tool_callback/after_tool_callback_full. Callbacks nach der Tool-Ausführung können strukturierte Ausführungsmetadaten überCallbackContext::tool_outcome()untersuchen. - Feature-gesteuert: Guardrails für Ein- und Ausgaben (
guardrails) sowie dieEnhancedPlugin-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
CodeRuntimeund 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 überfinal_result— allerdings beendet ein Tool, dasskip_summarizationfü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 */ }
}
RunStepist eine Menge von Struct-Varianten —Call { call, stdout },Complete { value, stdout }undRaised { message, stdout }. Erzeuge sie mit den HilfsfunktionenRunStep::call/RunStep::complete/RunStep::raisedund füge erfasste Ausgaben mit.with_stdout(..)hinzu.RunStep::Callmacht genau einen ausstehenden Aufruf sichtbar; setze ihn mit einem Wert oder einem Fehler fort oderdump()seine Fortsetzung, um ihn auszusetzen. Dasstdout, das eine Laufzeitumgebung anhängt, wird wieder an das Modell übergeben und in Checkpoints gespeichert, sodass es das Aussetzen und Fortsetzen übersteht.- Ein
PendingCallmeldet seine Argumente so, wie der Interpreter sie erzeugt hat —positional_args()undkeyword_args()getrennt. Ordne Positionsargumente nicht selbst Namen zu: Der Treiber bindet sie zentral überadk_agent::codeact::bind_call_argsan die Parameter des Tools, sodass eine Laufzeitumgebung am Aufrufübergang kein Tool-Schema benötigt undrender_toolseine 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).RuntimeErrorist echten Host-Fehlern vorbehalten (Snapshot- (De-)Serialisierung, interne Interpreterfehler) und bricht die Ausführung ab. RuntimeCapabilities::supports_suspensionmusstruesein, um HITL und langfristiges Zurückstellen zu ermöglichen;promptbeschreibt 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
/undjoinpath(),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.