Agents de workflow

Les agents de workflow orchestrent plusieurs agents selon des schémas prévisibles : pipelines séquentiels, exécution parallèle ou boucles itératives. Contrairement à LlmAgent, qui utilise le raisonnement IA, les agents de workflow suivent des chemins d’exécution déterministes.

Démarrage rapide

Créez un nouveau projet :

cargo new workflow_demo
cd workflow_demo

Ajoutez des dépendances à Cargo.toml :

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

Créez .env :

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

SequentialAgent

SequentialAgent exécute les sous-agents les uns après les autres. Chaque agent voit l’historique de conversation accumulé par les agents précédents.

Quand l’utiliser

  • Pipelines en plusieurs étapes où la sortie alimente l’étape suivante
  • Flux de travail Recherche → Analyse → Résumé
  • Chaînes de transformation de données

Exemple complet

Remplacez src/main.rs :

use adk_rust::prelude::*;
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 = Arc::new(GeminiModel::new(&api_key, "gemini-2.5-flash")?);

    // Step 1: Research agent gathers information
    let researcher = LlmAgentBuilder::new("researcher")
        .instruction("Research the given topic. List 3-5 key facts or points. \
                     Be factual and concise.")
        .model(model.clone())
        .output_key("research")  // Saves output to state
        .build()?;

    // Step 2: Analyzer agent identifies patterns
    let analyzer = LlmAgentBuilder::new("analyzer")
        .instruction("Based on the research above, identify 2-3 key insights \
                     or patterns. What's the bigger picture?")
        .model(model.clone())
        .output_key("analysis")
        .build()?;

    // Step 3: Summarizer creates final output
    let summarizer = LlmAgentBuilder::new("summarizer")
        .instruction("Create a brief executive summary combining the research \
                     and analysis. Keep it under 100 words.")
        .model(model.clone())
        .build()?;

    // Create the sequential pipeline
    let pipeline = SequentialAgent::new(
        "research_pipeline",
        vec![Arc::new(researcher), Arc::new(analyzer), Arc::new(summarizer)],
    ).with_description("Research → Analyze → Summarize");

    println!("📋 Sequential Pipeline: Research → Analyze → Summarize");
    println!();

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

Exécutez-le :

cargo run

Exemple d’interaction

You: Tell me about Rust programming language

🔄 [researcher] Researching...
Here are key facts about Rust:
1. Systems programming language created at Mozilla in 2010
2. Memory safety without garbage collection via ownership system
3. Zero-cost abstractions and minimal runtime
4. Voted "most loved language" on Stack Overflow for 7 years
5. Used by Firefox, Discord, Dropbox, and Linux kernel

🔄 [analyzer] Analyzing...
Key insights:
1. Rust solves the memory safety vs performance tradeoff
2. Strong developer satisfaction drives rapid adoption
3. Trust from major tech companies validates production-readiness

🔄 [summarizer] Summarizing...
Rust is a systems language that achieves memory safety without garbage 
collection through its ownership model. Created at Mozilla in 2010, it's 
been rated the most loved language for 7 consecutive years. Major companies 
like Discord and Linux kernel adopt it for its zero-cost abstractions 
and performance guarantees.

Comment ça fonctionne

┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  Researcher │ →  │   Analyzer  │ →  │  Summarizer │
│   (step 1)  │    │   (step 2)  │    │   (step 3)  │
└─────────────┘    └─────────────┘    └─────────────┘
       ↓                  ↓                  ↓
 "Key facts..."    "Insights..."    "Executive summary"
  1. Le message de l’utilisateur va au premier agent (Researcher)
  2. La réponse du Researcher est ajoutée à l’historique
  3. Analyzer voit : le message de l’utilisateur + la réponse du Researcher
  4. Summarizer voit : le message de l’utilisateur + les réponses du Researcher + de l’Analyzer
  5. Le pipeline se termine lorsque le dernier agent a fini

ParallelAgent

ParallelAgent exécute tous les sous-agents simultanément. Chaque agent reçoit la même entrée et travaille indépendamment.

L’indépendance est imposée, pas seulement voulue : chaque sous-agent s’exécute sur sa propre branche de conversation ({parent}.{parallel_agent}.{sub_agent}), et les lectures de l’historique sont limitées à cette branche. Un sous-agent voit la conversation qui a conduit au fan-out, mais pas ce que ses pairs ont produit, de sorte que les branches concurrentes ne peuvent pas contaminer le contexte des autres. Utilisez ParallelAgent::with_shared_state() lorsque vous voulez que les sous-agents se coordonnent.

Les branches sont interrogées ensemble, donc le coût en temps réel correspond approximativement à la branche la plus lente plutôt qu’à la somme de toutes, et une branche lente ne bloque pas les autres. Deux conséquences à prendre en compte :

  • Les événements s’entrelacent. Les événements arrivent dans l’ordre où les branches les produisent, et non regroupés par sous-agent. Utilisez event.author (ou event.branch) pour attribuer un fragment à sa branche. L’ordre entre les branches n’est pas garanti.
  • Une erreur met fin à l’exécution, choisie de manière déterministe. Chaque branche va tout de même jusqu’à son terme, puis une seule erreur terminale est signalée. Lorsque plusieurs branches échouent, l’erreur signalée est celle du sous-agent le plus tôt dans l’ordre de déclaration — et non celle qui a échoué en premier en temps réel, ce qui serait une course en contexte concurrent.

Fermer le flux d’événements trop tôt arrête les branches encore en cours d’exécution.

Quand l’utiliser

  • Plusieurs points de vue sur le même sujet
  • Traitement en fan-out (même entrée, analyses différentes)
  • Scénarios multitâches critiques pour la vitesse

Exemple complet

use adk_rust::prelude::*;
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 = Arc::new(GeminiModel::new(&api_key, "gemini-2.5-flash")?);

    // Three analysts with DISTINCT personas (important for parallel execution)
    let technical = LlmAgentBuilder::new("technical_analyst")
        .instruction("You are a senior software architect. \
                     FOCUS ONLY ON: code quality, system architecture, scalability, \
                     security vulnerabilities, and tech stack choices. \
                     Start your response with '🔧 TECHNICAL:' and give 2-3 bullet points.")
        .model(model.clone())
        .build()?;

    let business = LlmAgentBuilder::new("business_analyst")
        .instruction("You are a business strategist and MBA graduate. \
                     FOCUS ONLY ON: market opportunity, revenue model, competition, \
                     cost structure, and go-to-market strategy. \
                     Start your response with '💼 BUSINESS:' and give 2-3 bullet points.")
        .model(model.clone())
        .build()?;

    let user_exp = LlmAgentBuilder::new("ux_analyst")
        .instruction("You are a UX researcher and designer. \
                     FOCUS ONLY ON: user journey, accessibility, pain points, \
                     visual design, and user satisfaction metrics. \
                     Start your response with '🎨 UX:' and give 2-3 bullet points.")
        .model(model.clone())
        .build()?;

    // Create parallel agent
    let multi_analyst = ParallelAgent::new(
        "multi_perspective",
        vec![Arc::new(technical), Arc::new(business), Arc::new(user_exp)],
    ).with_description("Technical + Business + UX analysis in parallel");

    println!("⚡ Parallel Analysis: Technical | Business | UX");
    println!("   (All three run simultaneously!)");
    println!();

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

💡 Astuce : Rendez les instructions des agents parallèles très distinctes avec des personas, des domaines de concentration et des préfixes de réponse uniques. Cela garantit que chaque agent produit une sortie unique.

Exemple d’interaction

You: Evaluate a mobile banking app

🔧 TECHNICAL:
• Requires robust API security: OAuth 2.0, certificate pinning, encrypted storage
• Offline mode with sync requires complex state management and conflict resolution
• Biometric auth integration varies significantly across iOS/Android platforms

💼 BUSINESS:
• Highly competitive market - need unique differentiator (neobanks, traditional banks)
• Revenue model: interchange fees, premium tiers, or lending products cross-sell
• Regulatory compliance costs significant: PCI-DSS, regional banking laws, KYC/AML

🎨 UX:
• Critical: fast task completion - check balance must be < 3 seconds
• Accessibility essential: screen reader support, high contrast mode, large touch targets
• Trust indicators important: security badges, familiar banking patterns

Comment ça fonctionne

                    ┌─────────────────┐
                    │  User Message   │
                    └────────┬────────┘
         ┌───────────────────┼───────────────────┐
         ↓                   ↓                   ↓
  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐
  │  Technical  │    │  Business   │    │     UX      │
  │   Analyst   │    │   Analyst   │    │   Analyst   │
  └──────┬──────┘    └──────┬──────┘    └──────┬──────┘
         ↓                   ↓                   ↓
    (response 1)       (response 2)       (response 3)

Tous les agents démarrent en même temps et les résultats sont diffusés au fur et à mesure qu’ils se terminent.


LoopAgent

LoopAgent exécute les sous-agents de façon répétée jusqu’à ce qu’une condition de sortie soit satisfaite ou que le nombre maximal d’itérations soit atteint.

Quand l’utiliser

  • Raffinement itératif (brouillon → critique → amélioration → répétition)
  • Logique de relance avec amélioration
  • Garde-fous de qualité nécessitant plusieurs passes

ExitLoopTool

Pour quitter une boucle plus tôt, donnez à un agent la ExitLoopTool. Lorsqu’elle est appelée, elle signale à la boucle de s’arrêter.

Exemple complet

use adk_rust::prelude::*;
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 = Arc::new(GeminiModel::new(&api_key, "gemini-2.5-flash")?);

    // Critic agent evaluates content
    let critic = LlmAgentBuilder::new("critic")
        .instruction("Review the content for quality. Score it 1-10 and list \
                     specific improvements needed. Be constructive but critical.")
        .model(model.clone())
        .build()?;

    // Refiner agent improves based on critique
    let refiner = LlmAgentBuilder::new("refiner")
        .instruction("Apply the critique to improve the content. \
                     If the score is 8 or higher, call exit_loop to finish. \
                     Otherwise, provide an improved version.")
        .model(model.clone())
        .tool(Arc::new(ExitLoopTool::new()))  // Can exit the loop
        .build()?;

    // Create inner sequential: critic → refiner
    let critique_refine = SequentialAgent::new(
        "critique_refine_step",
        vec![Arc::new(critic), Arc::new(refiner)],
    );

    // Wrap in loop with max 3 iterations
    let iterative_improver = LoopAgent::new(
        "iterative_improver",
        vec![Arc::new(critique_refine)],
    ).with_max_iterations(3)
     .with_description("Critique-refine loop (max 3 passes)");

    println!("🔄 Iterative Improvement Loop");
    println!("   critic → refiner → repeat (max 3x or until quality >= 8)");
    println!();

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

Exemple d’interaction

You: Write a tagline for a coffee shop

🔄 Iteration 1
[critic] Score: 5/10. "Good coffee here" is too generic. Needs:
- Unique value proposition
- Emotional connection
- Memorable phrasing

[refiner] Improved: "Where every cup tells a story"

🔄 Iteration 2
[critic] Score: 7/10. Better! But could be stronger:
- More action-oriented
- Hint at the experience

[refiner] Improved: "Brew your perfect moment"

🔄 Iteration 3
[critic] Score: 8/10. Strong, action-oriented, experiential.
Minor: could be more distinctive.

[refiner] Score is 8+, quality threshold met!
[exit_loop called]

Final: "Brew your perfect moment"

Comment ça fonctionne

     ┌──────────────────────────────────────────┐
     │              LoopAgent                    │
     │  ┌────────────────────────────────────┐  │
     │  │        SequentialAgent              │  │
     │  │  ┌──────────┐    ┌──────────────┐  │  │
  →  │  │  │  Critic  │ →  │   Refiner    │  │  │  →
     │  │  │ (review) │    │ (improve or  │  │  │
     │  │  └──────────┘    │  exit_loop)  │  │  │
     │  │                  └──────────────┘  │  │
     │  └────────────────────────────────────┘  │
     │         ↑_____________↓                  │
     │         repeat until exit                │
     └──────────────────────────────────────────┘

ConditionalAgent (basé sur des règles)

ConditionalAgent branche l’exécution en fonction d’une condition synchrone, basée sur des règles. Utilisez cela pour un routage déterministe comme les tests A/B ou le routage basé sur l’environnement.

ConditionalAgent::new("router", |ctx| ctx.session().state().get("premium")..., premium_agent)
    .with_else(basic_agent)

Remarque : Pour un routage intelligent basé sur LLM, utilisez plutôt LlmConditionalAgent.


LlmConditionalAgent (basé sur LLM)

LlmConditionalAgent utilise un LLM pour classer l’entrée de l’utilisateur et l’acheminer vers le sous-agent approprié. C’est idéal pour un routage intelligent lorsque la décision de routage nécessite de comprendre le contenu.

Quand l’utiliser

  • Classification d’intention - Routage basé sur ce que demande l’utilisateur
  • Routage multi-voies - Plus de 2 destinations
  • Routage sensible au contexte - Nécessite une compréhension, pas seulement des mots-clés

Exemple complet

use adk_rust::prelude::*;
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 = Arc::new(GeminiModel::new(&api_key, "gemini-2.5-flash")?);

    // Create specialist agents
    let tech_agent: Arc<dyn Agent> = Arc::new(
        LlmAgentBuilder::new("tech_expert")
            .instruction("You are a senior software engineer. Be precise and technical.")
            .model(model.clone())
            .build()?
    );

    let general_agent: Arc<dyn Agent> = Arc::new(
        LlmAgentBuilder::new("general_helper")
            .instruction("You are a friendly assistant. Explain simply, use analogies.")
            .model(model.clone())
            .build()?
    );

    let creative_agent: Arc<dyn Agent> = Arc::new(
        LlmAgentBuilder::new("creative_writer")
            .instruction("You are a creative writer. Be imaginative and expressive.")
            .model(model.clone())
            .build()?
    );

    // LLM classifies the query and routes accordingly
    let router = LlmConditionalAgent::builder("smart_router", model.clone())
        .instruction("Classify the user's question as exactly ONE of: \
                     'technical' (coding, debugging, architecture), \
                     'general' (facts, knowledge, how-to), \
                     'creative' (writing, stories, brainstorming). \
                     Respond with ONLY the category name.")
        .route("technical", tech_agent)
        .route("general", general_agent.clone())
        .route("creative", creative_agent)
        .default_route(general_agent)
        .build()?;

    println!("🧠 LLM-Powered Intelligent Router");
    Launcher::new(Arc::new(router)).run().await?;
    Ok(())
}

Exemple d’interaction

You: How do I fix a borrow error in Rust?
[Routing to: technical]
[Agent: tech_expert]
A borrow error occurs when Rust's ownership rules are violated...

You: What's the capital of France?
[Routing to: general]
[Agent: general_helper]
The capital of France is Paris! It's a beautiful city...

You: Write me a haiku about the moon
[Routing to: creative]
[Agent: creative_writer]
Silver orb above,
Shadows dance on silent waves—
Night whispers secrets.

Comment ça fonctionne

┌─────────────────┐
│  User Message   │
└────────┬────────┘
         ↓
┌─────────────────┐
│   LLM Classifies│  "technical" / "general" / "creative"
│   (smart_router)│
└────────┬────────┘
         ↓
    ┌────┴────┬──────────┐
    ↓         ↓          ↓
┌───────┐ ┌───────┐ ┌─────────┐
│ tech  │ │general│ │creative │
│expert │ │helper │ │ writer  │
└───────┘ └───────┘ └─────────┘

Combiner des agents de workflow

Les agents de workflow peuvent être imbriqués pour des schémas complexes.

Séquentiel + parallèle + boucle

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

// 1. Parallel analysis from multiple perspectives
let parallel_analysis = ParallelAgent::new(
    "multi_analysis",
    vec![Arc::new(tech_analyst), Arc::new(biz_analyst)],
);

// 2. Synthesize the parallel results
let synthesizer = LlmAgentBuilder::new("synthesizer")
    .instruction("Combine all analyses into a unified recommendation.")
    .model(model.clone())
    .build()?;

// 3. Quality loop: critique and refine
let quality_loop = LoopAgent::new(
    "quality_check",
    vec![Arc::new(critic), Arc::new(refiner)],
).with_max_iterations(2);

// Final pipeline: parallel → synthesize → quality loop
let full_pipeline = SequentialAgent::new(
    "full_analysis_pipeline",
    vec![
        Arc::new(parallel_analysis),
        Arc::new(synthesizer),
        Arc::new(quality_loop),
    ],
);

Tracer l’exécution du workflow

Pour voir ce qui se passe à l’intérieur d’un workflow, সকտիվez le traçage :

use adk_rust::prelude::*;
use adk_rust::{SessionId, UserId};
use adk_rust::runner::{Runner, RunnerConfig};
use adk_rust::futures::StreamExt;
use std::sync::Arc;

// Create pipeline as before...

// Use Runner instead of Launcher for detailed control
let session_service = Arc::new(InMemorySessionService::new());
let runner = Runner::new(RunnerConfig {
    app_name: "workflow_trace".to_string(),
    agent: Arc::new(pipeline),
    session_service: session_service.clone(),
    artifact_service: None,
    memory_service: None,
    run_config: None,
})?;

let session = session_service.create(CreateRequest {
    app_name: "workflow_trace".to_string(),
    user_id: "user".to_string(),
    session_id: None,
    state: Default::default(),
}).await?;

let mut stream = runner.run(
    UserId::new("user")?,
    SessionId::new(session.id())?,
    Content::new("user").with_text("Analyze Rust"),
).await?;

// Process each event to see workflow execution
while let Some(event) = stream.next().await {
    let event = event?;
    
    // Show which agent is responding
    println!("📍 Agent: {}", event.author);
    
    // Show the response content
    if let Some(content) = event.content() {
        for part in &content.parts {
            if let Part::Text { text } = part {
                println!("   {}", text);
            }
        }
    }
    println!();
}

Référence de API

SequentialAgent

SequentialAgent::new("name", vec![agent1, agent2, agent3])
    .with_description("Optional description")
    .before_callback(callback)  // Called before execution
    .after_callback(callback)   // Called after execution

ParallelAgent

ParallelAgent::new("name", vec![agent1, agent2, agent3])
    .with_description("Optional description")
    .before_callback(callback)
    .after_callback(callback)

Si un sous-agent échoue, ParallelAgent vide toutes les futures restantes avant de propager la première erreur, évitant ainsi les fuites de ressources.

LoopAgent

LoopAgent::new("name", vec![agent1, agent2])
    .with_max_iterations(5)     // Safety limit (recommended, default: 1000)
    .with_description("Optional description")
    .before_callback(callback)
    .after_callback(callback)

ConditionalAgent

ConditionalAgent::new("name", |ctx| condition_fn, if_agent)
    .with_else(else_agent)      // Optional else branch
    .with_description("Optional description")

ExitLoopTool

// Add to an agent to let it exit a LoopAgent
.tool(Arc::new(ExitLoopTool::new()))

Précédent : LlmAgent | Suivant : Systèmes multi-agents →