Directives de développement

Ce document fournit des directives complĂštes pour les dĂ©veloppeurs contribuant Ă  ADK-Rust. Le respect de ces standards garantit la qualitĂ© du code, la cohĂ©rence et la maintenabilitĂ© dans l’ensemble du projet.

Table des matiĂšres

Bien démarrer

Prérequis

  • Rust : 1.95.0 ou version supĂ©rieure (Ă©dition 2024, vĂ©rifiez avec rustc --version)
  • Cargo : derniĂšre version stable
  • Git : pour le contrĂŽle de version
  • sccache (recommandĂ©) : cache de compilation qui rĂ©duit les temps de recompilation d’environ 70 %

Configuration de votre environnement

# Clone the repository
git clone https://github.com/zavora-ai/adk-rust.git
cd adk-rust

# Option A: Nix/devenv (reproducible — identical on Linux, macOS, CI)
devenv shell

# Option B: Setup script (installs sccache, cmake, etc.)
./scripts/setup-dev.sh

# Option C: Manual
cargo build

# Install cargo-nextest (parallel test runner, ~10x faster)
curl -LsSf https://get.nexte.st/latest/mac | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin

# Run all tests
cargo nextest run --workspace

# Check for lints
cargo clippy --all-targets --all-features

# Format code
cargo fmt --all

Variables d’environnement

Pour exécuter les exemples et les tests qui nécessitent des clés API :

# Gemini (default provider)
export GOOGLE_API_KEY="your-api-key"

# OpenAI (optional)
export OPENAI_API_KEY="your-api-key"

# Anthropic (optional)
export ANTHROPIC_API_KEY="your-api-key"

Structure du projet

ADK-Rust est organisé comme un espace de travail Cargo avec plusieurs crates :

adk-rust/
├── adk-core/       # Foundational traits and types (Agent, Tool, Llm, Event)
├── adk-telemetry/  # OpenTelemetry integration
├── adk-model/      # LLM providers (Gemini, OpenAI, Anthropic)
├── adk-tool/       # Tool system (FunctionTool, MCP, AgentTool)
├── adk-session/    # Session management (in-memory, SQLite)
├── adk-artifact/   # Binary artifact storage
├── adk-memory/     # Long-term memory with search
├── adk-agent/      # Agent implementations (LlmAgent, workflow agents)
├── adk-runner/     # Execution runtime
├── adk-server/     # REST API and A2A protocol
├── adk-cli/        # Command-line launcher
├── adk-realtime/   # Voice/audio streaming agents
├── adk-graph/      # LangGraph-style workflows
├── adk-browser/    # Browser automation tools
├── adk-eval/       # Agent evaluation framework
├── adk-rust/       # Umbrella crate (re-exports all)
└── examples/       # Working examples

Dépendances des crates

Les crates doivent ĂȘtre publiĂ©es dans l’ordre des dĂ©pendances :

  1. adk-core (aucune dépendance interne)
  2. adk-telemetry
  3. adk-model
  4. adk-tool
  5. adk-session
  6. adk-artifact
  7. adk-memory
  8. adk-agent
  9. adk-runner
  10. adk-server
  11. adk-cli
  12. adk-realtime
  13. adk-graph
  14. adk-browser
  15. adk-eval
  16. adk-rust (ombrelle)

Style de code

Principes généraux

  1. La clartĂ© avant l’ingĂ©niositĂ© : Ă©crire un code facile Ă  lire et Ă  comprendre
  2. L’explicite avant l’implicite : privilĂ©gier les types explicites et la gestion des erreurs
  3. Fonctions courtes : garder des fonctions ciblées et, si possible, en dessous de 50 lignes
  4. Noms significatifs : utiliser des noms de variables et de fonctions descriptifs

Formatage

Utilisez rustfmt avec les paramÚtres par défaut :

cargo fmt --all

Le pipeline CI impose le formatage. Exécutez toujours cargo fmt avant de valider.

Conventions de nommage

TypeConventionExemple
Cratesadk-* (kebab-case)adk-core, adk-agent
Modules du projetsnake_casellm_agent, function_tool
Types/traitsPascalCaseLlmAgent, ToolContext
Fonctionssnake_caseexecute_tool, run_agent
ConstantesSCREAMING_SNAKE_CASEKEY_PREFIX_APP
ParamĂštres de typeUne seule lettre majuscule ou PascalCaseT, State

Importations

Organisez les imports dans cet ordre :

// 1. Standard library
use std::collections::HashMap;
use std::sync::Arc;

// 2. External crates
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;

// 3. Internal crates (adk-*)
use adk_core::{Agent, Event, Result};

// 4. Local modules
use crate::config::Config;
use super::utils;

Clippy

Tout le code doit passer clippy sans avertissements :

cargo clippy --all-targets --all-features

Corrigez les avertissements clippy plutÎt que de les supprimer. Si une suppression est nécessaire, documentez pourquoi :

#[allow(clippy::too_many_arguments)]
// Builder pattern requires many parameters; refactoring would hurt usability
fn complex_builder(...) { }

Gestion des erreurs

Enveloppe d'erreur structurée

AdkError est un type d’erreur structurĂ© avec composant (oĂč), catĂ©gorie (quel type), code (clĂ© machine), message (texte humain), indice de rĂ©essai et dĂ©tails facultatifs :

use adk_core::{AdkError, ErrorComponent, ErrorCategory, Result};

// Return Result<T> (aliased to Result<T, AdkError>)
pub async fn my_function() -> Result<String> {
    let data = fetch_data().await?;

    if data.is_empty() {
        return Err(AdkError::new(
            ErrorComponent::Tool,
            ErrorCategory::NotFound,
            "tool.data.not_found",
            "No data found for the given query",
        ));
    }

    Ok(data)
}

Choisir le composant et la catégorie

ErrorComponent identifie oĂč l’échec s’est produit (le sous-systĂšme d’origine, et non la frontiĂšre de trait Ă  travers laquelle il est exposĂ©) :

ComposantÀ utiliser quand
AgentOrchestration d’agent, dispatch de sous-agent
ModelAppels du fournisseur LLM, analyse des réponses
ToolExĂ©cution d’outil, validation des paramĂštres
SessionPersistance de session, gestion de l’état
MemoryOpérations de mémoire/RAG
GraphExécution du workflow de graphe
AuthAuthentification, autorisation
ServerHTTP serveur, configuration

ErrorCategory classe ce qui a mal tourné :

CatĂ©gorieHTTPÀ utiliser lorsque
InvalidInput400ParamĂštres, configuration ou corps de requĂȘte incorrects
Unauthorized401Identifiants manquants ou invalides
Forbidden403Identifiants valides, permissions insuffisantes
NotFound404La ressource n’existe pas
RateLimited429Limite de débit en amont (réessayable)
Timeout408L’opĂ©ration a dĂ©passĂ© la limite de temps (rĂ©essayable)
Unavailable503Service en amont indisponible (retriable)
Cancelled499Annulé par l'appelant ou le systÚme
Internal500Bugs, violations d'invariants
Unsupported501Fonctionnalité non prise en charge

Constructeurs pratiques

Pour les schémas courants :

// Structured (preferred for new code)
AdkError::not_found(ErrorComponent::Session, "session.not_found", "Session xyz not found")
AdkError::rate_limited(ErrorComponent::Model, "model.openai.rate_limited", "Too many requests")
AdkError::unauthorized(ErrorComponent::Auth, "auth.token_expired", "Bearer token expired")
AdkError::timeout(ErrorComponent::Tool, "tool.execution_timeout", "Tool timed out after 30s")

// Backward-compatible (for migration — produces .legacy codes)
AdkError::tool("No data found")
AdkError::model("Provider returned 500")
AdkError::session("Session not found")

API du Builder

Attache des mĂ©tadonnĂ©es structurĂ©es pour un contexte d’erreur plus riche :

let err = AdkError::new(
    ErrorComponent::Model,
    ErrorCategory::RateLimited,
    "model.openai.rate_limited",
    "OpenAI rate limit exceeded",
)
.with_provider("openai")
.with_upstream_status(429)
.with_request_id("req-abc123")
.with_retry(RetryHint {
    should_retry: true,
    retry_after_ms: Some(5000),
    max_attempts: Some(3),
});

Indices de nouvelle tentative

Les catégories réessayables (RateLimited, Unavailable, Timeout) définissent automatiquement should_retry: true. Vérifiez la possibilité de réessayer avec err.is_retryable(), qui lit retry.should_retry comme source de vérité unique :

if err.is_retryable() {
    if let Some(delay) = err.retry.retry_after() {
        tokio::time::sleep(delay).await;
    }
    // retry the operation
}

Vérifications de catégorie

err.is_retryable()    // retry.should_retry (RateLimited, Unavailable, Timeout by default)
err.is_not_found()    // category == NotFound
err.is_unauthorized() // category == Unauthorized
err.is_rate_limited() // category == RateLimited
err.is_timeout()      // category == Timeout

Vérifications de composant (compatibilité ascendante)

err.is_model()   // component == Model
err.is_tool()    // component == Tool
err.is_session() // component == Session
err.is_config()  // code == "config.legacy" (temporary bridge)

Statut HTTP et problĂšme JSON

AdkError se mappe directement aux réponses HTTP :

let status = err.http_status_code(); // u16 based on category
let body = err.to_problem_json();    // structured JSON error body
// body: { "error": { "code", "message", "component", "category", "requestId", "retryAfter", ... } }

Erreurs locales au crate avec les impls From

Les crates avec des erreurs spécifiques au domaine implémentent From<CrateLocalError> for AdkError :

// In your crate
#[derive(Debug, thiserror::Error)]
pub enum MyToolError {
    #[error("connection failed: {0}")]
    ConnectionFailed(String),
    #[error("timeout after {0}ms")]
    Timeout(u64),
}

impl From<MyToolError> for AdkError {
    fn from(err: MyToolError) -> Self {
        let (category, code) = match &err {
            MyToolError::ConnectionFailed(_) => (ErrorCategory::Unavailable, "mytool.connection"),
            MyToolError::Timeout(_) => (ErrorCategory::Timeout, "mytool.timeout"),
        };
        AdkError::new(ErrorComponent::Tool, category, code, err.to_string())
            .with_source(err)
    }
}

Pas d’impls From globaux

std::io::Error et serde_json::Error traversent trop de frontiĂšres de sous-systĂšmes pour une conversion globale unique. Utilisez map_err explicites avec le bon composant :

// Good: explicit component and category
let data = std::fs::read_to_string(path)
    .map_err(|e| AdkError::new(
        ErrorComponent::Session,
        ErrorCategory::Internal,
        "session.io_read",
        format!("failed to read session file: {e}"),
    ).with_source(e))?;

// Good: for quick migration
let data = serde_json::from_str(&raw)
    .map_err(|e| AdkError::session(format!("JSON parse failed: {e}")))?;

Messages d’erreur

RĂ©digez des messages d’erreur clairs et exploitables :

// Good: specific and actionable
AdkError::new(
    ErrorComponent::Model,
    ErrorCategory::InvalidInput,
    "model.missing_api_key",
    "API key not found. Set GOOGLE_API_KEY environment variable.",
)

// Bad: vague
AdkError::model("Invalid config")

Pas de panic dans le code de bibliothĂšque

Les crates de bibliothĂšque ne doivent jamais paniquer sur des erreurs rĂ©cupĂ©rables. Évitez unwrap(), expect() et panic!() dans le code src/ (le code de test est exemptĂ©).

RwLock / Mutex : Utilisez une dĂ©gradation gracieuse au lieu de unwrap(). Un verrou empoisonnĂ© signifie qu’un autre thread a paniquĂ© — faire paniquer aussi le thread courant aggrave les choses.

// Bad: panics if lock is poisoned
let state = self.state.write().unwrap();

// Good: log and return a safe default
let Ok(state) = self.state.write() else {
    tracing::error!("state lock poisoned — returning default");
    return Default::default();
};

// Good: recover through the poison (data may be stale but won't crash)
let state = self.state.read().unwrap_or_else(|e| e.into_inner());

Constructeurs : Retournez Result lorsque l’initialisation peut Ă©chouer (par exemple, lors de la connexion Ă  des services externes).

// Bad: panics if Docker is not running
pub fn new(config: Config) -> Self {
    let client = connect().expect("connection failed");
    Self { client }
}

// Good: caller decides how to handle the failure
pub fn new(config: Config) -> Result<Self, MyError> {
    let client = connect().map_err(|e| MyError::Init(e.to_string()))?;
    Ok(Self { client })
}

Méthodes du Builder : Utilisez if let Some au lieu de expect() pour Arc::get_mut() :

// Bad: panics if Arc is shared
pub fn add_callback(mut self, cb: Callback) -> Self {
    Arc::get_mut(&mut self.callbacks).expect("not shared").push(cb);
    self
}

// Good: silent no-op (builder pattern guarantees single ownership)
pub fn add_callback(mut self, cb: Callback) -> Self {
    if let Some(callbacks) = Arc::get_mut(&mut self.callbacks) {
        callbacks.push(cb);
    }
    self
}

ModĂšles asynchrones

Utiliser Tokio

Tout le code async utilise le runtime Tokio :

use tokio::sync::{Mutex, RwLock};

// Prefer RwLock for read-heavy data
let state: Arc<RwLock<State>> = Arc::new(RwLock::new(State::default()));

// Use Mutex for write-heavy or simple cases
let counter: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));

Convention des fonctionnalités Tokio

Les crates de bibliothĂšque (adk-*) doivent dĂ©clarer uniquement les fonctionnalitĂ©s tokio minimales qu’elles utilisent rĂ©ellement :

# Library crates — minimal features
tokio = { workspace = true, features = ["rt", "sync", "time"] }

# Binary crates only (adk-cli, examples) — full is acceptable
tokio = { workspace = true, features = ["full"] }

N’utilisez jamais features = ["full"] dans une crate de bibliothùque. Cela force tous les consommateurs en aval à compiler chaque sous-systùme tokio, qu’ils en aient besoin ou non.

Traits async

Utilisez async_trait pour les méthodes de traits async :

use async_trait::async_trait;

#[async_trait]
pub trait MyTrait: Send + Sync {
    async fn do_work(&self) -> Result<()>;
}

Flux

Utilisez EventStream pour les réponses en streaming :

use adk_core::EventStream;
use async_stream::stream;
use futures::Stream;

fn create_stream() -> EventStream {
    let s = stream! {
        yield Ok(Event::new("inv-1"));
        yield Ok(Event::new("inv-2"));
    };
    Box::pin(s)
}

Sécurité des threads

Tous les types publics doivent ĂȘtre Send + Sync :

// Good: Thread-safe
pub struct MyAgent {
    name: String,
    tools: Vec<Arc<dyn Tool>>,  // Arc for shared ownership
}

// Verify with compile-time checks
fn assert_send_sync<T: Send + Sync>() {}
fn _check() {
    assert_send_sync::<MyAgent>();
}

Tests

Exécuteur de tests

ADK-Rust utilise cargo-nextest pour l’exĂ©cution des tests. Nextest exĂ©cute chaque binaire de test dans un processus sĂ©parĂ© avec une planification parallĂšle, offrant un gain d’environ 10× par rapport Ă  cargo test dans cet espace de travail.

# Install (one-time)
curl -LsSf https://get.nexte.st/latest/mac | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin

# Or via devenv (included automatically)
devenv shell

La configuration se trouve dans .config/nextest.toml avec deux profils :

  • default — dĂ©veloppement local (arrĂȘt au premier Ă©chec, aucune nouvelle tentative)
  • ci — exĂ©cutions CI (nouvelles tentatives pour les tests instables, avertissements sur les tests lents)

Organisation des tests

crate/
├── src/
│   ├── lib.rs          # Unit tests at bottom of file
│   └── module.rs       # Module-specific tests
└── tests/
    └── integration.rs  # Integration tests

Tests unitaires

Placez les tests unitaires dans le mĂȘme fichier que le code :

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }

    #[tokio::test]
    async fn test_async_function() {
        let result = async_function().await;
        assert!(result.is_ok());
    }
}

Tests d’intĂ©gration

Placez-les dans le répertoire tests/ :

// tests/integration_test.rs
use adk_core::*;

#[tokio::test]
async fn test_full_workflow() {
    // Setup
    let service = InMemorySessionService::new();

    // Execute
    let session = service.create(request).await.unwrap();

    // Assert
    assert_eq!(session.id(), "test-session");
}

Tests avec mocks

Utilisez MockLlm pour tester sans appels API :

use adk_model::MockLlm;

#[tokio::test]
async fn test_agent_with_mock() {
    let mock = MockLlm::new(vec![
        "First response".to_string(),
        "Second response".to_string(),
    ]);

    let agent = LlmAgentBuilder::new("test")
        .model(Arc::new(mock))
        .build()
        .unwrap();

    // Test agent behavior
}

Commandes de test

# Run all tests (nextest — parallel, fast)
cargo nextest run --workspace

# Run specific crate tests
cargo nextest run -p adk-core

# Run with CI profile (retries flaky tests)
cargo nextest run --workspace --profile ci

# Run doctests (nextest doesn't run these — use cargo test)
cargo test --workspace --doc

# Run ignored tests (require API keys)
cargo nextest run --workspace -- --run-ignored

# Run with output (nextest shows output for failing tests by default)
cargo nextest run --workspace --no-capture

# Devenv shortcuts
devenv shell ws-test          # nextest, default profile
devenv shell ws-test-ci       # nextest, CI profile
devenv shell ws-test-slow     # cargo test fallback (includes doctests)

Documentation

Commentaires de documentation

Utilisez /// pour les éléments publics :

/// Creates a new LLM agent with the specified configuration.
///
/// # Arguments
///
/// * `name` - A unique identifier for this agent
/// * `model` - The LLM provider to use for reasoning
///
/// # Examples
///
/// ```rust
/// use adk_agent::LlmAgentBuilder;
///
/// let agent = LlmAgentBuilder::new("assistant")
///     .model(Arc::new(model))
///     .build()?;
/// ```
///
/// # Errors
///
/// Returns an error with component `Agent` if the model is not set.
pub fn new(name: impl Into<String>) -> Self {
    // ...
}

Documentation de module

Ajoutez la documentation au niveau du module en haut de lib.rs :

//! # adk-core
//!
//! Core types and traits for ADK-Rust.
//!
//! ## Overview
//!
//! This crate provides the foundational types...

Fichiers README

Chaque crate doit avoir un README.md avec :

  1. BrĂšve description
  2. Instructions d’installation
  3. Exemple rapide
  4. Lien vers la documentation complĂšte

Tests de documentation

Assurez-vous que les exemples de doc compilent :

cargo test --doc --all

Processus de pull request

Avant de soumettre

  1. Exécutez toute la suite de tests :

    cargo nextest run --workspace
  2. Exécutez clippy :

    cargo clippy --all-targets --all-features
  3. Formatez le code :

    cargo fmt --all
  4. Mettez Ă  jour la documentation si vous ajoutez/modifiez un API public

  5. Ajoutez des tests pour les nouvelles fonctionnalités

Directives pour les PR

  • Titre : description claire et concise de la modification
  • Description : expliquez quoi et pourquoi (pas comment)
  • Taille : gardez les PR ciblĂ©es ; dĂ©coupez les gros changements
  • Tests : incluez des tests pour les nouvelles fonctionnalitĂ©s
  • Modifications cassantes : documentez-les clairement dans la description

Messages de commit

Suivez les conventional commits :

feat: add OpenAI streaming support
fix: correct tool parameter validation
docs: update quickstart guide
refactor: simplify session state management
test: add integration tests for A2A protocol

Mise en place du projet

Utilisez cargo adk new avec le Composable Template System pour amorcer de nouveaux projets. Le registre intégré fournit 12 templates, 9 add-ons et 5 modÚles enterprise :

# Basic agent (default)
cargo adk new my-agent

# Agent with tools and Docker support
cargo adk new my-agent --template tools --addon docker

# A2A protocol agent with CI and telemetry
cargo adk new my-agent --template a2a --addon ci --addon telemetry

# Graph workflow with enterprise observability
cargo adk new my-agent --template graph --addon telemetry --addon docker --addon ci

Le flag --addon est composable — combinez n’importe quel template de base avec n’importe quel nombre d’add-ons. Consultez la documentation Composable Templates pour la liste complùte des templates, add-ons et modùles enterprise.

TĂąches courantes

Ajouter un nouveau tool

  1. Créez le tool :
use adk_core::{Tool, ToolContext, Result};
use async_trait::async_trait;
use serde_json::Value;

pub struct MyTool {
    // fields
}

#[async_trait]
impl Tool for MyTool {
    fn name(&self) -> &str {
        "my_tool"
    }

    fn description(&self) -> &str {
        "Does something useful"
    }

    fn parameters_schema(&self) -> Option<Value> {
        Some(serde_json::json!({
            "type": "object",
            "properties": {
                "input": { "type": "string" }
            },
            "required": ["input"]
        }))
    }

    async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
        let input = args["input"].as_str().unwrap_or_default();
        Ok(serde_json::json!({ "result": input }))
    }
}
  1. Ajoutez-le à l’agent :
let agent = LlmAgentBuilder::new("agent")
    .model(model)
    .tool(Arc::new(MyTool::new()))
    .build()?;

Ajouter un nouveau fournisseur de modĂšle

  1. Créez le module dans adk-model/src/ :
// adk-model/src/mymodel/mod.rs
mod client;
pub use client::MyModelClient;
  1. Implémentez le trait Llm :
use adk_core::{Llm, LlmRequest, LlmResponse, LlmResponseStream, Result};

pub struct MyModelClient {
    api_key: String,
}

#[async_trait]
impl Llm for MyModelClient {
    fn name(&self) -> &str {
        "my-model"
    }

    async fn generate_content(
        &self,
        request: LlmRequest,
        stream: bool,
    ) -> Result<LlmResponseStream> {
        // Implementation
    }
}
  1. Ajoutez un flag de fonctionnalité dans adk-model/Cargo.toml :
[features]
mymodel = ["dep:mymodel-sdk"]
  1. Exportez conditionnellement :
#[cfg(feature = "mymodel")]
pub mod mymodel;
#[cfg(feature = "mymodel")]
pub use mymodel::MyModelClient;

Ajouter un nouveau type d’agent

  1. Créez le module dans adk-agent/src/ :
// adk-agent/src/my_agent.rs
use adk_core::{Agent, EventStream, InvocationContext, Result};
use async_trait::async_trait;

pub struct MyAgent {
    name: String,
}

#[async_trait]
impl Agent for MyAgent {
    fn name(&self) -> &str {
        &self.name
    }

    fn description(&self) -> &str {
        "My custom agent"
    }

    async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<EventStream> {
        // Implementation
    }
}
  1. Exportez dans adk-agent/src/lib.rs :
mod my_agent;
pub use my_agent::MyAgent;

Conseils de débogage

  1. Activez le tracing :

    adk_telemetry::init_telemetry();
  2. Inspectez les événements :

    while let Some(event) = stream.next().await {
        eprintln!("Event: {:?}", event);
    }
  3. Utilisez RUST_LOG :

    RUST_LOG=debug cargo run --example myexample

PrĂ©cĂ©dent : ← ContrĂŽle d’accĂšs

Questions ? Ouvrez une issue sur GitHub.

Directives de développement - Documentation ADK-Rust | ADK-Rust