Lignes directrices de développement

Ce document fournit des lignes directrices complètes pour les développeurs contribuant à adk-rust. Le respect de ces normes garantit la qualité du code, sa cohérence et sa maintenabilité sur l'ensemble du projet.

Table des matières

Démarrage

Prérequis

  • Rust: 1.75 ou supérieur (vérifier avec rustc --version)
  • Cargo: Dernière version stable
  • Git: Pour le contrôle de version

Configuration de votre environnement

# Cloner le dépôt
git clone https://github.com/zavora-ai/adk-rust.git
cd adk-rust

# Construire le projet
cargo build

# Exécuter tous les tests
cargo test --all

# Vérifier les lints
cargo clippy --all-targets --all-features

# Formater le code
cargo fmt --all

Variables d'environnement

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

# Gemini (fournisseur par défaut)
export GOOGLE_API_KEY="your-api-key"

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

# Anthropic (facultatif)
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/       # Traits et types fondamentaux (Agent, Tool, Llm, Event)
├── adk-telemetry/  # Intégration OpenTelemetry
├── adk-model/      # Fournisseurs LLM (Gemini, OpenAI, Anthropic)
├── adk-tool/       # Système de Tool (FunctionTool, MCP, AgentTool)
├── adk-session/    # Gestion des Sessions (en mémoire, SQLite)
├── adk-artifact/   # Stockage des artefacts binaires
├── adk-memory/     # Mémoire à long terme avec recherche
├── adk-agent/      # Implémentations d'Agent (LlmAgent, Agents de workflow)
├── adk-runner/     # Environnement d'exécution
├── adk-server/     # API REST et protocole A2A
├── adk-cli/        # Lanceur en ligne de commande
├── adk-realtime/   # Agents de streaming vocal/audio
├── adk-graph/      # Workflows de style LangGraph
├── adk-browser/    # Outils d'automatisation de navigateur
├── adk-eval/       # Cadre d'évaluation d'Agent
├── adk-rust/       # Crate parapluie (ré-exporte tout)
└── examples/       # Exemples fonctionnels

Dépendances des Crates

Les crates doivent être publiés dans l'ordre de dépendance :

  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 (parapluie)

Style de code

Principes généraux

  1. Clarté plutôt que ruse : Écrivez du code facile à lire et à comprendre.
  2. Explicite plutôt qu'implicite : Préférez les types explicites et la gestion des erreurs.
  3. Petites fonctions : Gardez les fonctions ciblées et, si possible, sous 50 lignes.
  4. Noms significatifs : Utilisez 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
Modulessnake_casellm_agent, function_tool
Types/TraitsPascalCaseLlmAgent, ToolContext
Functionssnake_caseexecute_tool, run_agent
ConstantsSCREAMING_SNAKE_CASEKEY_PREFIX_APP
Type parametersUne majuscule ou PascalCaseT, State

Imports

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 avertissement :

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

Traitez les avertissements clippy plutôt que de les supprimer. Si la 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

Utiliser adk_core::AdkError

Toutes les erreurs doivent utiliser le type d'erreur centralisé :

use adk_core::{AdkError, Result};

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

    // Create errors with appropriate variants
    if data.is_empty() {
        return Err(AdkError::Tool("No data found".into()));
    }

    Ok(data)
}

Variants d'erreur

Utilisez la variante d'erreur appropriée :

VarianteCas d'utilisation
AdkError::Agent(String)Erreurs d'exécution d'Agent
AdkError::Model(String)Erreurs du fournisseur LLM
AdkError::Tool(String)Erreurs d'exécution de Tool
AdkError::Session(String)Erreurs de gestion de Session
AdkError::Artifact(String)Erreurs de stockage d'Artifact
AdkError::Config(String)Erreurs de configuration
AdkError::Network(String)Erreurs HTTP/réseau

Messages d'erreur

Rédigez des messages d'erreur clairs et exploitables :

// Good: Specific and actionable
Err(AdkError::Config("API key not found. Set GOOGLE_API_KEY environment variable.".into()))

// Bad: Vague
Err(AdkError::Config("Invalid config".into()))

Modèles asynchrones

Utiliser Tokio

Tout le code async utilise le runtime Tokio :

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

// Préférer RwLock pour les données à forte lecture
let state: Arc<RwLock<State>> = Arc::new(RwLock::new(State::default()));

// Utiliser Mutex pour les cas à forte écriture ou les cas simples
let counter: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));

Traits asynchrones

Utiliser async_trait pour les méthodes de trait async :

use async_trait::async_trait;

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

Streaming

Utiliser 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 :

// Bon : thread-safe
pub struct MyAgent {
    name: String,
    tools: Vec<Arc<dyn Tool>>,  // Arc pour la propriété partagée
}

// Vérifier avec des contrôles à la compilation
fn assert_send_sync<T: Send + Sync>() {}
fn _check() {
    assert_send_sync::<MyAgent>();
}

Tests

Organisation des tests

crate/
├── src/
│   ├── lib.rs          # Tests unitaires en bas du fichier
│   └── module.rs       # Tests spécifiques au module
└── tests/
    └── integration.rs  # Tests d'intégration

Tests unitaires

Placer 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

Placer dans le répertoire tests/ :

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

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

    // Exécution
    let session = service.create(request).await.unwrap();

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

Tests avec simulation (Mock)

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

    // Tester le comportement de l'agent
}

Commandes de test

# Exécuter tous les tests
cargo test --all

# Exécuter les tests d'un crate spécifique
cargo test --package adk-core

# Exécuter avec la sortie
cargo test --all -- --nocapture

# Exécuter les tests ignorés (nécessitent des clés API)
cargo test --all -- --ignored

Documentation

Commentaires de documentation

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

/// Crée un nouvel agent LLM avec la configuration spécifiée.
///
/// # Arguments
///
/// * `name` - Un identifiant unique pour cet agent
/// * `model` - Le fournisseur LLM à utiliser pour le raisonnement
///
/// # Exemples
///
/// ```rust
/// use adk_agent::LlmAgentBuilder;
///
/// let agent = LlmAgentBuilder::new("assistant")
///     .model(Arc::new(model))
///     .build()?;
/// ```
///
/// # Erreurs
///
/// Retourne `AdkError::Agent` si le modèle n'est pas défini.
pub fn new(name: impl Into<String>) -> Self {
    // ...
}

Documentation de module

Ajouter la documentation de module en haut de lib.rs :

//! # adk-core
//!
//! Types et traits fondamentaux pour ADK-Rust.
//!
//! ## Aperçu
//!
//! Ce crate fournit les types fondamentaux...

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

S'assurer que les exemples de documentation compilent :

cargo test --doc --all

Processus de Pull Request

Avant de soumettre

  1. Exécuter la suite de tests complète :

    cargo test --all
  2. Exécuter clippy :

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

    cargo fmt --all
  4. Mettre à jour la documentation si vous ajoutez/modifiez l'API publique

  5. Ajouter des tests pour les nouvelles fonctionnalités

Directives de PR

  • Titre : Description claire et concise du changement
  • Description : Expliquer quoi et pourquoi (pas comment)
  • Taille : Garder les PRs ciblées ; diviser les gros changements
  • Tests : Inclure des tests pour les nouvelles fonctionnalités
  • Breaking changes : Documenter clairement dans la description

Messages de Commit

Suivre 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

Tâches Courantes

Ajout d'un Nouveau Tool

  1. Créer 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. Ajouter à l'Agent :
let agent = LlmAgentBuilder::new("agent")
    .model(model)
    .tool(Arc::new(MyTool::new()))
    .build()?;

Ajout d'un Nouveau Model Provider

  1. Créer le module dans adk-model/src/ :
// adk-model/src/mymodel/mod.rs
mod client;
pub use client::MyModelClient;
  1. Implémenter le Llm trait :
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. Ajouter le feature flag dans adk-model/Cargo.toml :
[features]
mymodel = ["dep:mymodel-sdk"]
  1. Exporter conditionnellement :
#[cfg(feature = "mymodel")]
pub mod mymodel;
#[cfg(feature = "mymodel")]
pub use mymodel::MyModelClient;

Ajout d'un Nouveau Type d'Agent

  1. Créer 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. Exporter dans adk-agent/src/lib.rs :
mod my_agent;
pub use my_agent::MyAgent;

Conseils de Débogage

  1. Activer le tracing :

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

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

    RUST_LOG=debug cargo run --example myexample

Précédent : ← Access Control

Des questions ? Ouvrez une issue sur GitHub.