LlmAgent

Le LlmAgent est le type d’agent principal de ADK-Rust qui utilise un grand modèle de langage pour le raisonnement et la prise de décision.

Démarrage rapide

Créez un nouveau projet :

cargo new llm_agent
cd llm_agent

Ajoutez les dépendances à Cargo.toml :

[dependencies]
adk-rust = "2.1.0"
tokio = { version = "1.40", features = ["full"] }
dotenvy = "0.15"
serde_json = "1.0"

Créez .env avec votre clé API :

echo 'GOOGLE_API_KEY=your-api-key' > .env

Remplacez src/main.rs :

use adk_rust::prelude::*;
use adk_rust::{SessionId, UserId};
use adk_rust::Launcher;
use std::sync::Arc;

#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    dotenvy::dotenv().ok();
    
    let api_key = std::env::var("GOOGLE_API_KEY")?;
    let model = GeminiModel::new(&api_key, "gemini-3.7-flash")?;

    let agent = LlmAgentBuilder::new("my_agent")
        .instruction("You are a helpful assistant.")
        .model(Arc::new(model))
        .build()?;

    Launcher::new(Arc::new(agent)).run().await?;
    Ok(())
}

Exécutez-le :

cargo run

Interagir avec votre agent

Une invite interactive s’affichera :

🤖 Agent ready! Type your questions (or 'exit' to quit).

You: Hello! What can you help me with?
Assistant: Hello! I'm a helpful assistant. I can help you with:
- Answering questions on various topics
- Explaining concepts clearly
- Having a conversation

What would you like to know?

You: exit
👋 Goodbye!

Façonner le comportement de l’agent avec des instructions

La méthode instruction() définit la personnalité et le comportement de votre agent. Il s’agit du prompt système qui guide chaque réponse :

// A formal business assistant
let formal_agent = LlmAgentBuilder::new("formal_assistant")
    .instruction("You are a professional business consultant. \
                 Use formal language. Be concise and data-driven.")
    .model(Arc::new(model))
    .build()?;

// A friendly coding tutor
let tutor_agent = LlmAgentBuilder::new("code_tutor")
    .instruction("You are a friendly coding tutor for beginners. \
                 Explain concepts simply. Use examples. \
                 Encourage questions. Never make the user feel bad for not knowing.")
    .model(Arc::new(model))
    .build()?;

// A creative storyteller
let story_agent = LlmAgentBuilder::new("storyteller")
    .instruction("You are a creative storyteller. \
                 Craft engaging narratives with vivid descriptions. \
                 Use plot twists and memorable characters.")
    .model(Arc::new(model))
    .build()?;

Exemples de sorties

Prompt utilisateur : « Qu’est-ce que Rust ? »

Assistant professionnel :

Rust is a systems programming language focused on performance, memory safety, 
and concurrency. Key metrics: 0 runtime overhead, 0 garbage collection. 
Adoption growing 30% YoY in enterprise. Recommended for performance-critical 
applications where reliability is paramount.

Tuteur de programmation convivial :

Great question! 🎉 Rust is a programming language that helps you write fast 
and safe code. Think of it like a helpful friend who stops you from making 
mistakes before they happen!

Here's a simple example:
  let name = "Alice";  // This creates a variable

What makes Rust special is that it catches bugs at compile time, not when 
your program is running. Pretty cool, right? Want me to explain more?

Conteur créatif :

In the twilight of 2010, in a secret Mozilla laboratory, a lone developer 
named Graydon Hoare breathed life into something extraordinary—a language 
that would challenge the old gods of C and C++.

They called it Rust, named for a fungus that survives against all odds. 
Like a phoenix rising from the ashes of segmentation faults and memory 
leaks, Rust promised a new era: speed without sacrifice, power without peril.

And so, the revolution began...

Modèles d’instructions

Les instructions prennent en charge l’injection de variables à l’aide de la syntaxe {var}. Les variables sont résolues à partir de l’état de la session lors de l’exécution :

let agent = LlmAgentBuilder::new("personalized")
    .instruction("You are helping {user_name}. Their role is {user_role}. \
                 Tailor your responses to their expertise level.")
    .model(Arc::new(model))
    .build()?;

Guide étape par étape pour utiliser les modèles :

  1. Créez l’agent avec des variables de modèle dans l’instruction
  2. Configurez Runner et SessionService pour gérer l’état
  3. Créez une session avec des variables d’état correspondant à votre modèle
  4. Exécutez l’agent — les modèles sont automatiquement remplacés

Voici un exemple complet fonctionnel :

use adk_rust::prelude::*;
use adk_rust::{SessionId, UserId};
use adk_rust::runner::{Runner, RunnerConfig};
use adk_rust::session::{CreateRequest, InMemorySessionService, SessionService};
use adk_rust::futures::StreamExt;
use serde_json::json;
use std::collections::HashMap;
use std::sync::Arc;

#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    dotenvy::dotenv().ok();
    let api_key = std::env::var("GOOGLE_API_KEY")?;
    let model = GeminiModel::new(&api_key, "gemini-3.7-flash")?;

    // 1. Agent with templated instruction
    let agent = LlmAgentBuilder::new("personalized")
        .instruction("You are helping {user_name}. Their role is {user_role}. \
                     Tailor your responses to their expertise level.")
        .model(Arc::new(model))
        .build()?;

    // 2. Create session service and runner
    let session_service = Arc::new(InMemorySessionService::new());
    let runner = Runner::new(RunnerConfig {
        app_name: "templating_demo".to_string(),
        agent: Arc::new(agent),
        session_service: session_service.clone(),
        artifact_service: None,
        memory_service: None,
        run_config: None,
    })?;

    // 3. Create session with state variables
    let mut state = HashMap::new();
    state.insert("user_name".to_string(), json!("Alice"));
    state.insert("user_role".to_string(), json!("Senior Developer"));

    let session = session_service.create(CreateRequest {
        app_name: "templating_demo".to_string(),
        user_id: "user123".to_string(),
        session_id: None,
        state,
    }).await?;

    // 4. Run the agent - instruction becomes:
    // "You are helping Alice. Their role is Senior Developer..."
    let mut response_stream = runner.run(
        UserId::new("user123")?,
        SessionId::new(session.id())?,
        Content::new("user").with_text("Explain async/await in Rust"),
    ).await?;

    // Print the response
    while let Some(event) = response_stream.next().await {
        let event = event?;
        if let Some(content) = event.content() {
            for part in &content.parts {
                if let Part::Text { text } = part {
                    print!("{}", text);
                }
            }
        }
    }

    Ok(())
}

Types de variables de modèle :

MotifExempleSource
{var}{user_name}État de session
{prefix:var}{user:name}, {app:config}État préfixé
{var?}{user_name?}Facultatif (vide si absent)
{artifact.file}{artifact.resume.pdf}Contenu de l’artefact

Exemple de sortie :

Modèle : "You are helping {user_name}. Their role is {user_role}."
Devient : "You are helping Alice. Their role is Senior Developer."

L’agent répondra alors avec un contenu personnalisé en fonction du nom et du niveau d’expertise de l’utilisateur !


Ajout d’outils

Les outils donnent à votre agent des capacités qui vont au-delà de la conversation : ils peuvent récupérer des données, effectuer des calculs, effectuer des recherches sur le Web ou appeler des APIs externes. Le LLM détermine quand utiliser un outil en fonction de la demande de l’utilisateur.

Fonctionnement des outils

  1. L’agent reçoit le message de l’utilisateur → « Quel temps fait-il à Tokyo ? »
  2. LLM décide d’appeler l’outil → Sélectionne get_weather avec {"city": "Tokyo"}
  3. L’outil s’exécute → Renvoie {"temperature": "22°C", "condition": "sunny"}
  4. LLM formate la réponse → « À Tokyo, le temps est ensoleillé et il fait 22 °C. »

Création d’un outil avec FunctionTool

FunctionTool est la manière la plus simple de créer un outil : encapsulez n’importe quelle fonction Rust asynchrone, et le LLM pourra l’appeler. Vous fournissez un nom, une description et une fonction gestionnaire qui reçoit des arguments JSON et renvoie un résultat JSON.

let weather_tool = FunctionTool::new(
    "get_weather",                              // Tool name (used by LLM)
    "Get the current weather for a city",       // Description (helps LLM decide when to use it)
    |_ctx, args| async move {                   // Handler function
        let city = args.get("city")             // Extract arguments from JSON
            .and_then(|v| v.as_str())
            .unwrap_or("unknown");
        Ok(json!({ "city": city, "temperature": "22°C" }))  // Return JSON result
    },
);

Les outils intégrés natifs du fournisseur peuvent désormais être combinés avec des instances FunctionTool au sein du même agent. ADK transmet les déclarations des outils natifs au fournisseur tout en continuant à exécuter localement les outils fonctionnels ordinaires.

Création d’un agent à plusieurs outils

Créez un nouveau projet :

cargo new tool_agent
cd tool_agent

Ajoutez les dépendances à Cargo.toml :

[dependencies]
adk-rust = { version = "2.1.0", features = ["tools"] }
tokio = { version = "1.40", features = ["full"] }
dotenvy = "0.15"
serde_json = "1.0"

Créez .env :

echo 'GOOGLE_API_KEY=your-api-key' > .env

Remplacez src/main.rs par un agent doté de trois outils :

use adk_rust::prelude::*;
use adk_rust::Launcher;
use serde_json::json;
use std::sync::Arc;

#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    dotenvy::dotenv().ok();
    let api_key = std::env::var("GOOGLE_API_KEY")?;
    let model = GeminiModel::new(&api_key, "gemini-3.7-flash")?;

    // Tool 1: Weather lookup
    let weather_tool = FunctionTool::new(
        "get_weather",
        "Get the current weather for a city. Parameters: city (string)",
        |_ctx, args| async move {
            let city = args.get("city").and_then(|v| v.as_str()).unwrap_or("unknown");
            Ok(json!({ "city": city, "temperature": "22°C", "condition": "sunny" }))
        },
    );

    // Tool 2: Calculator
    let calculator = FunctionTool::new(
        "calculate",
        "Perform arithmetic. Parameters: a (number), b (number), operation (add/subtract/multiply/divide)",
        |_ctx, args| async move {
            let a = args.get("a").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let b = args.get("b").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let op = args.get("operation").and_then(|v| v.as_str()).unwrap_or("add");
            let result = match op {
                "add" => a + b,
                "subtract" => a - b,
                "multiply" => a * b,
                "divide" => if b != 0.0 { a / b } else { 0.0 },
                _ => 0.0,
            };
            Ok(json!({ "result": result }))
        },
    );

    // Tool 3: Built-in Google Search (Note: Currently unsupported in ADK-Rust)
    // let search_tool = GoogleSearchTool::new();

    // Build agent with weather and calculator tools
    let agent = LlmAgentBuilder::new("multi_tool_agent")
        .instruction("You are a helpful assistant. Use tools when needed: \
                     - get_weather for weather questions \
                     - calculate for math")
        .model(Arc::new(model))
        .tool(Arc::new(weather_tool))
        .tool(Arc::new(calculator))
        // .tool(Arc::new(search_tool))  // Currently unsupported
        .build()?;

    Launcher::new(Arc::new(agent)).run().await?;
    Ok(())
}

Exécutez votre agent :

cargo run

Exemple d’interaction

You: What's 15% of 250?
Assistant: [Using calculate tool with a=250, b=0.15, operation=multiply]
15% of 250 is 37.5.

You: What's the weather in Tokyo?
Assistant: [Using get_weather tool with city=Tokyo]
The weather in Tokyo is sunny with a temperature of 22°C.

You: Search for latest Rust features
Assistant: I don't have access to search functionality at the moment, but I can help with other questions about Rust or perform calculations!

Sortie structurée avec le schéma JSON

Pour les applications qui nécessitent des données structurées, utilisez output_schema() :

use adk_rust::prelude::*;
use serde_json::json;
use std::sync::Arc;

#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    dotenvy::dotenv().ok();
    let api_key = std::env::var("GOOGLE_API_KEY")?;
    let model = GeminiModel::new(&api_key, "gemini-3.7-flash")?;

    let extractor = LlmAgentBuilder::new("entity_extractor")
        .instruction("Extract entities from the given text.")
        .model(Arc::new(model))
        .output_schema(json!({
            "type": "object",
            "properties": {
                "people": {
                    "type": "array",
                    "items": { "type": "string" }
                },
                "locations": {
                    "type": "array",
                    "items": { "type": "string" }
                },
                "dates": {
                    "type": "array",
                    "items": { "type": "string" }
                }
            },
            "required": ["people", "locations", "dates"]
        }))
        .build()?;

    println!("Entity extractor ready!");
    Ok(())
}

Application du schéma par les fournisseurs

output_schema parvient au fournisseur sous la forme de GenerateContentConfig::response_schema.
La manière dont le fournisseur le traite varie, et l’agent valide le résultat dans tous les cas :

FournisseurApplication native
GeminiSchéma complet, envoyé en tant que schéma de réponse
OpenAI et compatible avec OpenAISchéma complet, envoyé en tant que format de réponse strict json_schema
OpenRouterSchéma complet
DeepSeekJSON syntaxe uniquement — le mode de sortie de DeepSeek JSON ne possède aucune variante json_schema, le schéma est donc appliqué par la validation de l’agent

Lorsque le fournisseur applique uniquement la syntaxe, ou aucune contrainte, l’agent injecte tout de même le schéma en tant qu’instruction et valide la réponse. Ainsi, une réponse non conforme entraîne une nouvelle tentative au lieu de renvoyer des données incorrectes.

Remarque : DeepSeek exige que le mot « json » apparaisse dans l’invite lorsque JSON Output est activé, sinon API peut renvoyer un contenu vide. L’adaptateur ajoute lui-même cette mention lorsque votre invite ne la contient pas déjà.

Exemple de sortie JSON

Entrée : « John a rencontré Sarah à Paris le 25 décembre »

Sortie :

{
  "people": ["John", "Sarah"],
  "locations": ["Paris"],
  "dates": ["December 25th"]
}

Fonctionnalités avancées

Inclure le contenu

Contrôler la visibilité de l’historique de la conversation :

// Full history (default)
.include_contents(IncludeContents::Default)

// Stateless - sees only injected instructions plus the current user turn
.include_contents(IncludeContents::None)

Clé de sortie

Enregistrer les réponses de l’agent dans l’état de la session :

.output_key("summary")  // Response saved to state["summary"]

Instructions dynamiques

Calculer les instructions au moment de l’exécution :

.instruction_provider(|ctx| {
    Box::pin(async move {
        let user_id = ctx.user_id();
        Ok(format!("You are assisting user {}.", user_id))
    })
})

Rappels

Intercepter le comportement de l’agent :

.before_model_callback(|ctx, request| {
    Box::pin(async move {
        println!("About to call LLM with {} messages", request.contents.len());
        Ok(BeforeModelResult::Continue)
    })
})

Référence du générateur

MéthodeDescription
new(name)Crée un constructeur avec le nom de l’agent
model(Arc<dyn Llm>)Définit le LLM (obligatoire)
description(text)Description de l’agent
instruction(text)Invite système
tool(Arc<dyn Tool>)Ajoute un outil statique
toolset(Arc<dyn Toolset>)Ajoute un ensemble d’outils dynamiques résolu à chaque invocation
output_schema(json)Schéma JSON pour une sortie structurée
output_key(key)Enregistre la réponse dans l’état
include_contents(mode)Visibilité de l’historique
max_iterations(n)Nombre maximal d’échanges aller-retour LLM (par défaut : 100)
tool_execution_strategy(strategy)Mode de distribution des outils : Sequential, Parallel ou Auto
default_retry_budget(RetryBudget)Réessayer les outils ayant échoué jusqu’à N fois avec un délai
tool_retry_budget(name, RetryBudget)Remplacement des tentatives pour chaque outil
circuit_breaker_threshold(u32)Désactiver l’outil après N échecs consécutifs
on_tool_error(callback)Enregistrer un gestionnaire de secours pour les échecs des outils
after_tool_callback_full(callback)Rappel enrichi V2 après l’outil avec l’outil, les arguments et la réponse
build()Crée l’agent

Contrôle des itérations

La méthode max_iterations() limite le nombre d'allers-retours LLM qu'un agent peut effectuer avant de s'arrêter. Cela est utile pour :

  • Empêcher les boucles d'appels d'outils incontrôlées
  • Contrôler les coûts en production
  • Définir des limites raisonnables pour les tâches complexes
let agent = LlmAgentBuilder::new("bounded_agent")
    .model(Arc::new(model))
    .instruction("You are a helpful assistant.")
    .tool(Arc::new(my_tool))
    .max_iterations(10)  // Stop after 10 LLM calls
    .build()?;

La valeur par défaut est de 100 itérations, ce qui convient à la plupart des cas d'utilisation. Des valeurs plus faibles (5 à 20) sont recommandées pour les agents simples de questions-réponses, tandis que des valeurs plus élevées peuvent être nécessaires pour les tâches complexes de raisonnement en plusieurs étapes.


Ensembles d'outils dynamiques

Pour les outils qui dépendent du contexte d'invocation (par exemple, des sessions de navigateur propres à chaque utilisateur), utilisez .toolset() au lieu de .tool(). Les ensembles d'outils sont résolus au début de chaque appel run() :

use adk_browser::{BrowserSessionPool, BrowserToolset, BrowserConfig};
use adk_agent::LlmAgentBuilder;
use std::sync::Arc;

let pool = Arc::new(BrowserSessionPool::new(BrowserConfig::new(), 10));
let toolset = Arc::new(BrowserToolset::with_pool(pool));

let agent = LlmAgentBuilder::new("web_agent")
    .model(model)
    .instruction("You are a web automation assistant.")
    .toolset(toolset)  // Resolved per-user at runtime
    .build()?;

Vous pouvez mélanger .tool() statiques et .toolset() dynamiques sur le même agent. Les noms d'outils en double entre les outils statiques et les ensembles d'outils produisent une erreur déterministe.

RealtimeAgentBuilder prend également en charge .toolset() avec la même sémantique ; les agents vocaux en temps réel bénéficient donc eux aussi d'une résolution dynamique des outils.

Composition d'ensembles d'outils

Utilisez FilteredToolset, MergedToolset et PrefixedToolset depuis adk-tool pour composer des configurations complexes d'ensembles d'outils :

use adk_tool::{BasicToolset, FilteredToolset, MergedToolset, PrefixedToolset, string_predicate};

// Prefix weather tools to avoid name collisions
let weather = Arc::new(PrefixedToolset::new(weather_toolset, "wx"));

// Filter utility tools to only expose search and calculate
let utils = Arc::new(FilteredToolset::new(
    utility_toolset,
    string_predicate(vec!["search".into(), "calculate".into()]),
));

// Merge into a single toolset
let composed = MergedToolset::new("all", vec![weather, utils]);

let agent = LlmAgentBuilder::new("agent")
    .model(model)
    .toolset(Arc::new(composed))
    .build()?;

Tous les utilitaires de composition fonctionnent avec toute implémentation de Toolset, notamment McpToolset et BrowserToolset.

Exécution parallèle des outils

Lorsqu'un LLM renvoie plusieurs appels d'outils dans une seule réponse, vous pouvez contrôler leur distribution :

use adk_core::ToolExecutionStrategy;

let agent = LlmAgentBuilder::new("fast_agent")
    .model(Arc::new(model))
    .instruction("You are a research assistant. Use multiple tools in parallel.")
    // Auto requires both safety signals for concurrent inclusion
    .tool_execution_strategy(ToolExecutionStrategy::Auto)
    .tool(Arc::new(
        search_tool
            .with_read_only(true)
            .with_concurrency_safe(true),
    ))
    .tool(Arc::new(
        lookup_tool
            .with_read_only(true)
            .with_concurrency_safe(true),
    ))
    .tool(Arc::new(save_tool)) // runs after the concurrent safe subset
    .build()?;

Trois stratégies sont disponibles :

  • Sequential (par défaut) — les outils sont exécutés un à la fois, dans l'ordre LLM
  • Parallel — tous les outils sont exécutés simultanément ; cette substitution explicite contourne les métadonnées de sécurité, le demandeur assume donc la responsabilité de la sécurité
  • Auto — les appels dont les outils sont à la fois en lecture seule et compatibles avec l'exécution concurrente sont exécutés simultanément en premier ; tous les appels restants sont ensuite exécutés séquentiellement

Les résultats sont toujours renvoyés dans l’ordre LLM d’origine, quelle que soit la stratégie. Les outils en échec produisent une réponse d’erreur JSON sans interrompre le lot.

La stratégie est définie par agent via LlmAgentBuilder::tool_execution_strategy(). Si elle n’est pas définie, la valeur par défaut est Sequential.

Résilience des outils

Configurez les budgets de nouvelle tentative et les disjoncteurs pour les agents en production :

use adk_core::RetryBudget;
use std::time::Duration;

let agent = LlmAgentBuilder::new("resilient_agent")
    .model(model)
    .tool(Arc::new(my_tool))
    // Retry all tools up to 2 times with 500ms delay
    .default_retry_budget(RetryBudget::new(2, Duration::from_millis(500)))
    // Override for a specific tool
    .tool_retry_budget("flaky_api", RetryBudget::new(4, Duration::from_secs(1)))
    // Disable a tool after 3 consecutive failures in one invocation
    .circuit_breaker_threshold(3)
    // Provide a fallback when a tool fails
    .on_tool_error(Box::new(|_ctx, tool, _args, error| {
        Box::pin(async move {
            tracing::warn!(tool = tool.name(), %error, "tool failed");
            Ok(None) // None = propagate error; Some(value) = use as fallback
        })
    }))
    .build()?;

Les rappels après l’exécution d’un outil peuvent inspecter les métadonnées structurées ToolOutcome via CallbackContext::tool_outcome() :

.after_tool_callback(Box::new(|ctx| {
    Box::pin(async move {
        if let Some(outcome) = ctx.tool_outcome() {
            println!(
                "Tool '{}' {} in {:?} (attempt {})",
                outcome.tool_name,
                if outcome.success { "succeeded" } else { "failed" },
                outcome.duration,
                outcome.attempt,
            );
        }
        Ok(None)
    })
}))

Exemple complet

Un agent prêt pour la production avec plusieurs outils (météo, calculatrice, recherche) et une sortie enregistrée dans l’état de la session :

use adk_rust::prelude::*;
use adk_rust::Launcher;
use serde_json::json;
use std::sync::Arc;

#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    dotenvy::dotenv().ok();
    let api_key = std::env::var("GOOGLE_API_KEY")?;
    let model = GeminiModel::new(&api_key, "gemini-3.7-flash")?;

    // Weather tool
    let weather = FunctionTool::new(
        "get_weather",
        "Get weather for a city. Parameters: city (string)",
        |_ctx, args| async move {
            let city = args.get("city").and_then(|v| v.as_str()).unwrap_or("unknown");
            Ok(json!({
                "city": city,
                "temperature": "22°C",
                "humidity": "65%",
                "condition": "partly cloudy"
            }))
        },
    );

    // Calculator tool
    let calc = FunctionTool::new(
        "calculate",
        "Math operations. Parameters: expression (string like '2 + 2')",
        |_ctx, args| async move {
            let expr = args.get("expression").and_then(|v| v.as_str()).unwrap_or("0");
            Ok(json!({ "expression": expr, "result": "computed" }))
        },
    );

    // Build the full agent
    let agent = LlmAgentBuilder::new("assistant")
        .description("A helpful assistant with weather and calculation abilities")
        .instruction("You are a helpful assistant. \
                     Use the weather tool for weather questions. \
                     Use the calculator for math. \
                     Be concise and friendly.")
        .model(Arc::new(model))
        .tool(Arc::new(weather))
        .tool(Arc::new(calc))
        // .tool(Arc::new(GoogleSearchTool::new()))  // Provider-native tools can be mixed with FunctionTool
        .output_key("last_response")
        .build()?;

    println!("✅ Agent '{}' ready!", agent.name());
    Launcher::new(Arc::new(agent)).run().await?;
    Ok(())
}

Essayez ces invites :

You: What's 25 times 4?
Assistant: It's 100.

You: How's the weather in New York?
Assistant: The weather in New York is partly cloudy with a temperature of 22°C and 65% humidity.

You: Calculate 15% tip on $85
Assistant: A 15% tip on $85 is $12.75, making the total $97.75.


Précédent : Démarrage rapide | Suivant : Agents de workflow →