CodeActAgent (CodeAct)

CodeActAgent est l’équivalent de LlmAgent qui agit en écrivant et en exécutant du code au lieu d’émettre un appel d’outil à la fois. À chaque tour, le modèle produit un script unique ; les outils sont exposés sous forme de fonctions appelables que le script peut composer ; et le script communique son résultat en renvoyant une valeur marquée.

Il s’agit du modèle CodeAct : plutôt que call tool A → observe → call tool B, le modèle écrit b(a(x)) dans un seul script, de sorte que le travail en plusieurs étapes s’effectue en un seul tour. Il est activé par la fonctionnalité codeact de adk-agent.

Quand l’utiliser

  • Tâches qui enchaînent ou combinent plusieurs outils par tour (manipulation de données, opérations par lots, logique de raccordement).
  • Modèles ayant fait l’objet d’un post-entraînement pour la génération de code.
  • Flux de travail dans lesquels un véritable interpréteur (par ex. Python) est disponible comme substrat d’action.

Pour les appels d’outils natifs, préférez LlmAgent. Pour un environnement de codage avec fichiers et shell en bac à sable, consultez l’Agent de codage.

Fonctionnement de la boucle

À chaque tour :

  1. Le modèle émet un bloc de code délimité (un script).
  2. Le script s’exécute sur un [CodeRuntime] ; les appels d’outils remontent jusqu’à l’hôte, qui exécute l’outil et reprend le script avec le résultat.
  3. Le script renvoie un ScriptOutput marqué :
    • observation — renvoyé au modèle ; la boucle continue.
    • error — renvoyé sous forme de message ; la boucle continue.
    • final_result — renvoyé à l’appelant ; la boucle se termine.
    • transfer_to_agent — transmet le contrôle à un autre agent ; la boucle se termine.

Le framework est indépendant du langage : le trait CodeRuntime constitue l’interface de l’interpréteur pas à pas, et il indique lui-même son langage et son environnement au modèle via une invite en texte libre. L’adaptateur de production prévu encapsule Monty, un interpréteur Python natif en Rust.

Durabilité : suspendre et reprendre

CodeActAgent est sans état entre les invocations — l’état durable réside dans la session, exactement comme pour LlmAgent. Deux situations suspendent l’exécution :

  • un outil soumis à confirmation, pour lequel aucune décision n’a encore été prise (HITL), et
  • un outil de longue durée dont le résultat arrive hors bande.

Lors de la suspension, la continuation de l’interpréteur en cours est sérialisée dans un CodeActCheckpoint et écrite dans l’état de la session ; le run() suivant la relit et reprend l’exécution — la décision de confirmation arrive via RunConfig::tool_confirmation_decisions, et le résultat d’une opération de longue durée arrive sous la forme d’un FunctionResponse dans le message suivant. Les appels d’outils inline sont encadrés par des points de contrôle write-ahead (SAVE-BEFORE) et SAVE-AFTER : une fois le point de contrôle SAVE-AFTER persisté, la récupération reprend avec le résultat enregistré et n’exécute jamais à nouveau l’outil. Un plantage dans la courte fenêtre suivant l’effet secondaire d’un outil, mais avant l’enregistrement de son point de contrôle SAVE-AFTER, entraînera la réexécution de l’outil lors de la récupération ; les outils qui ne sont pas idempotents doivent donc s’en prémunir (la même limite « au moins une fois » que LlmAgent).

Cela nécessite un environnement d’exécution capable de prendre un instantané d’un appel en pause. Un environnement d’exécution qui ne le peut pas exécute les outils de longue durée en ligne et refuse les pauses de confirmation.

Création d’un 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 et runtime sont requis ; tout le reste possède une valeur par défaut.

Parité avec LlmAgent

Le générateur reflète LlmAgentBuilder :

  • Modèle : generate_content_config ainsi que les raccourcis temperature/top_p/top_k/ max_output_tokens.
  • Instructions : instruction/instruction_provider, global_instruction/global_instruction_provider, avec injection de modèle {state.key} ; ainsi que les compétences (fonctionnalité skills).
  • Historique : include_contents.
  • Outils : tools statiques et toolsets par invocation ; tool_timeout, default_retry_budget/tool_retry_budget, circuit_breaker_threshold, ainsi que les solutions de repli on_tool_error.
  • Autorisation : ToolConfirmationPolicy (require_tool_confirmation/require_tool_confirmation_for_all).
  • Transfert : sub_agents et disallow_transfer_to_parent/ disallow_transfer_to_peers.
  • Sortie : output_key, output_schema/output_type avec une boucle de correction et de nouvelle tentative (output_max_retries).
  • Rappels : before_callback/after_callback, before_model_callback/after_model_callback, et before_tool_callback/after_tool_callback/after_tool_callback_full. Les rappels après l’appel d’un outil peuvent inspecter les métadonnées d’exécution structurées via CallbackContext::tool_outcome().
  • Conditionné par fonctionnalité : garde-fous d’entrée/sortie (guardrails) et le pipeline EnhancedPlugin (enhanced-plugins).

Chaque appel d’outil reçoit un nouveau ToolContext qui transporte l’identifiant d’appel de l’interpréteur et délègue les artefacts, la mémoire, l’état partagé, les portées utilisateur et les secrets à l’invocation active — ainsi, un outil se comporte de manière identique sous CodeActAgent ou LlmAgent.

Différences délibérées

  • La mise en bac à sable de l’exécution de code relève de la responsabilité du CodeRuntime, et ne constitue pas un ajout rapporté.
  • La distribution des outils est séquentielle par conception (une continuation unique est capturée à une seule limite d’appel), il n’y a donc pas de tool_execution_strategy parallèle.
  • Il n’existe pas d’option de construction skip_summarization — le modèle termine lui-même la boucle via final_result — bien qu’un outil qui définisse skip_summarization sur ses actions termine tout de même l’exécution.

Exemple

Une démonstration de bout en bout exécutable et sans dépendances — un CodeRuntime autonome ainsi qu’un modèle déterministe — se trouve dans examples/codeact_agent:

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

Implémentation d’un CodeRuntime

Un CodeRuntime analyse et exécute un script par étapes, en exposant un appel externe à la fois :

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 est un ensemble de variantes de structure — Call { call, stdout }, Complete { value, stdout } et Raised { message, stdout }. Construisez-les avec les assistants RunStep::call / RunStep::complete / RunStep::raised et associez la sortie capturée avec .with_stdout(..). RunStep::Call expose exactement un appel en attente ; reprenez-le avec une valeur ou une erreur, ou dump() sa continuation pour le suspendre. Le stdout attaché par un environnement d’exécution est renvoyé au modèle et conservé dans les points de contrôle, de sorte qu’il survive à la suspension et à la reprise.
  • Un PendingCall transmet ses arguments tels que l’interpréteur les a produits — positional_args() et keyword_args() séparément. Ne faites pas correspondre vous-même les arguments positionnels aux noms : le pilote les associe aux paramètres de l’outil de manière centralisée via adk_agent::codeact::bind_call_args ; un environnement d’exécution n’a donc pas besoin de schéma d’outil à la limite de l’appel, et render_tools peut être une fonction pure de la tranche d’outils.
  • Erreurs du script et erreurs de l’hôte. Tout ce que le modèle pourrait corriger en écrivant un code différent — une erreur de syntaxe ou d’analyse, une exception non interceptée, une annulation due à une limite de ressources — est une RunStep::Raised (une chaîne opaque renvoyée au modèle textuellement). RuntimeError est réservé aux véritables défaillances de l’hôte (sérialisation ou désérialisation d’un instantané, erreurs internes de l’interpréteur) et interrompt l’exécution.
  • RuntimeCapabilities::supports_suspension doit être true pour permettre le HITL et le report des opérations de longue durée ; prompt décrit le langage et l’environnement au modèle.

Voir examples/codeact_agent/src/runtime.rs pour une implémentation complète et minimale prenant en charge la suspension et la reprise.

Python avec Monty

L’adaptateur de production prévu est adk-codeact-monty, un CodeRuntime reposant sur Pydantic Monty. Il permet au modèle d’agir en écrivant du Python, s’exécute dans le même processus, sans conteneur ni sous-processus, et capture une exécution en pause sous forme d’octets — exactement ce dont la suspension/reprise a besoin. L’interpréteur Monty est utilisé via le noyau embedded-python de adk-code, qui regroupe les crates monty en un seul endroit ; rustc 1.95+ est requis.

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

Ou via la crate parapluie (réexportée sous le nom de 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(),
);

Accès au système d’exploitation

Les effets sur le système d’exploitation qu’un script tente d’effectuer — lectures/écritures du système de fichiers, os.getenv/os.environ et date.today()/datetime.now() — sont traités sur place conformément à une politique contrôlée par l’hôte. Ce ne sont pas des outils et ils ne mettent jamais en pause la boucle de l’agent. Par défaut, un runtime est entièrement isolé (aucun accès au système de fichiers, environnement vide, horloge de l’hôte activée). Accordez des accès spécifiques avec le builder :

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

L’accès au réseau et aux sous-processus ne dispose d’aucune surface d’appel système Monty et reste indisponible quelle que soit la politique. Les accès accordés sont décrits au modèle dans le prompt système, afin qu’il sache quels chemins il peut lire ou écrire et quelles variables d’environnement existent.

Monty n’implémente qu’un sous-ensemble de pathlib.Path ; lorsque des chemins sont montés, le prompt répertorie les méthodes exactement prises en charge (toute autre méthode déclenche AttributeError) :

  • Lecture/requête (tout montage) : exists(), is_file(), is_dir(), is_symlink(), read_text(), read_bytes(), stat(), iterdir(), resolve(), absolute(), open("r").
  • Écriture (montages en lecture-écriture uniquement) : write_text(), write_bytes(), append_text(), append_bytes(), mkdir(), unlink(), rmdir(), rename(), open("w")/open("a").
  • Opérations pures sur les chemins (sans E/S) : l’opérateur / et joinpath(), is_absolute(), with_name(), with_stem(), with_suffix(), as_posix(), ainsi que les propriétés .name, .parent, .stem, .suffix, .suffixes, .parts.

Les outils sont invoqués au moyen d’une unique fonction intégrée, call_tool("name", {"arg": value, ...}) — le seul moyen d’appeler un outil ; ils ne sont jamais accessibles comme fonctions appelables isolées. Le nom de l’outil est une chaîne littérale et chaque argument est une entrée indexée par une chaîne dans un dictionnaire, de sorte que le véritable nom est transporté dans la continuation sérialisée (et conservé lors de la suspension et de la reprise, sans table de noms côté hôte), un outil ainsi que chaque argument pouvant porter n’importe quel nom (pas nécessairement un identifiant Python valide comme "fetch-cart", un mot-clé Python, ni même "call_tool"), et le pilote associe exactement les entrées du dictionnaire par leur nom — sans déduction positionnelle. Chaque outil apparaît dans l’invite sous la forme d’une ligne d’utilisation call_tool("name", {...}) avec ses paramètres et sa description. Toute autre forme que celle-ci — un fetch_cart(...) isolé, des arguments nommés, un argument qui n’est pas un dictionnaire ou une clé qui n’est pas une chaîne — est refusée avec une erreur corrective plutôt que distribuée silencieusement ; le modèle n’a donc qu’une seule forme d’appel à apprendre.

L’élément exécutable examples/codeact_monty_agent exécute un CodeActAgent avec du Python réel, entièrement hors ligne.