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
- Structure du projet
- Style de code
- Gestion des erreurs
- ModĂšles asynchrones
- Tests
- Documentation
- Processus de pull request
- TĂąches courantes
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 :
adk-core(aucune dépendance interne)adk-telemetryadk-modeladk-tooladk-sessionadk-artifactadk-memoryadk-agentadk-runneradk-serveradk-cliadk-realtimeadk-graphadk-browseradk-evaladk-rust(ombrelle)
Style de code
Principes généraux
- La clartĂ© avant lâingĂ©niositĂ© : Ă©crire un code facile Ă lire et Ă comprendre
- Lâexplicite avant lâimplicite : privilĂ©gier les types explicites et la gestion des erreurs
- Fonctions courtes : garder des fonctions ciblées et, si possible, en dessous de 50 lignes
- 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
| Type | Convention | Exemple |
|---|---|---|
| Crates | adk-* (kebab-case) | adk-core, adk-agent |
| Modules du projet | snake_case | llm_agent, function_tool |
| Types/traits | PascalCase | LlmAgent, ToolContext |
| Fonctions | snake_case | execute_tool, run_agent |
| Constantes | SCREAMING_SNAKE_CASE | KEY_PREFIX_APP |
| ParamĂštres de type | Une seule lettre majuscule ou PascalCase | T, 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 |
|---|---|
Agent | Orchestration dâagent, dispatch de sous-agent |
Model | Appels du fournisseur LLM, analyse des réponses |
Tool | ExĂ©cution dâoutil, validation des paramĂštres |
Session | Persistance de session, gestion de lâĂ©tat |
Memory | Opérations de mémoire/RAG |
Graph | Exécution du workflow de graphe |
Auth | Authentification, autorisation |
Server | HTTP serveur, configuration |
ErrorCategory classe ce qui a mal tourné :
| Catégorie | HTTP | à utiliser lorsque |
|---|---|---|
InvalidInput | 400 | ParamĂštres, configuration ou corps de requĂȘte incorrects |
Unauthorized | 401 | Identifiants manquants ou invalides |
Forbidden | 403 | Identifiants valides, permissions insuffisantes |
NotFound | 404 | La ressource nâexiste pas |
RateLimited | 429 | Limite de débit en amont (réessayable) |
Timeout | 408 | LâopĂ©ration a dĂ©passĂ© la limite de temps (rĂ©essayable) |
Unavailable | 503 | Service en amont indisponible (retriable) |
Cancelled | 499 | Annulé par l'appelant ou le systÚme |
Internal | 500 | Bugs, violations d'invariants |
Unsupported | 501 | Fonctionnalité 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 :
- BrĂšve description
- Instructions dâinstallation
- Exemple rapide
- 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
-
Exécutez toute la suite de tests :
cargo nextest run --workspace -
Exécutez clippy :
cargo clippy --all-targets --all-features -
Formatez le code :
cargo fmt --all -
Mettez Ă jour la documentation si vous ajoutez/modifiez un API public
-
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
- 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 }))
}
}
- Ajoutez-le Ă lâagent :
let agent = LlmAgentBuilder::new("agent")
.model(model)
.tool(Arc::new(MyTool::new()))
.build()?;
Ajouter un nouveau fournisseur de modĂšle
- Créez le module dans
adk-model/src/:
// adk-model/src/mymodel/mod.rs
mod client;
pub use client::MyModelClient;
- 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
}
}
- Ajoutez un flag de fonctionnalité dans
adk-model/Cargo.toml:
[features]
mymodel = ["dep:mymodel-sdk"]
- Exportez conditionnellement :
#[cfg(feature = "mymodel")]
pub mod mymodel;
#[cfg(feature = "mymodel")]
pub use mymodel::MyModelClient;
Ajouter un nouveau type dâagent
- 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
}
}
- Exportez dans
adk-agent/src/lib.rs:
mod my_agent;
pub use my_agent::MyAgent;
Conseils de débogage
-
Activez le tracing :
adk_telemetry::init_telemetry(); -
Inspectez les événements :
while let Some(event) = stream.next().await { eprintln!("Event: {:?}", event); } -
Utilisez RUST_LOG :
RUST_LOG=debug cargo run --example myexample
PrĂ©cĂ©dent : â ContrĂŽle dâaccĂšs
Questions ? Ouvrez une issue sur GitHub.