Directrices de desarrollo

Este documento proporciona directrices completas para los desarrolladores que contribuyen a ADK-Rust. Seguir estos estándares garantiza la calidad del código, la consistencia y la mantenibilidad en todo el proyecto.

Índice

Comenzando

Requisitos previos

  • Rust: 1.95.0 o superior (edición 2024, comprobar con rustc --version)
  • Cargo: última versión estable
  • Git: para control de versiones
  • sccache (recomendado): caché de compilación que reduce los tiempos de reconstrucción en ~70%

Configuración de tu entorno

# 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 de entorno

Para ejecutar ejemplos y pruebas que requieren claves de 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"

Estructura del proyecto

ADK-Rust está organizado como un espacio de trabajo de Cargo con múltiples 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

Dependencias de crates

Los crates deben publicarse en orden de dependencia:

  1. adk-core (sin dependencias internas)
  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 (paraguas)

Estilo de código

Principios generales

  1. Claridad sobre ingenio: escribe código que sea fácil de leer y entender
  2. Explícito sobre implícito: prioriza tipos explícitos y manejo de errores
  3. Funciones pequeñas: mantén las funciones enfocadas y, cuando sea posible, por debajo de 50 líneas
  4. Nombres significativos: usa nombres descriptivos para variables y funciones

Formato

Usa rustfmt con la configuración predeterminada:

cargo fmt --all

La canalización de CI aplica el formato de manera obligatoria. Ejecuta siempre cargo fmt antes de confirmar los cambios.

Convenciones de nombres

TipoConvenciónEjemplo
Cratesadk-* (kebab-case)adk-core, adk-agent
Módulossnake_casellm_agent, function_tool
Tipos/TraitsPascalCaseLlmAgent, ToolContext
Funcionessnake_caseexecute_tool, run_agent
ConstantesSCREAMING_SNAKE_CASEKEY_PREFIX_APP
Parámetros de tipoMayúscula única o PascalCaseT, State

Importaciones

Organiza las importaciones en este orden:

// 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

Todo el código debe pasar clippy sin advertencias:

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

Aborda las advertencias de clippy en lugar de suprimirlas. Si la supresión es necesaria, documenta por qué:

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

Manejo de errores

Envoltura de error estructurada

AdkError es un tipo de error estructurado con componente (dónde), categoría (qué tipo), código (clave de máquina), mensaje (texto humano), sugerencia de reintento y detalles opcionales:

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)
}

Elegir componente y categoría

ErrorComponent identifica dónde ocurrió la falla (el subsistema de origen, no la frontera del trait a través de la cual se expone):

ComponenteCuándo usar
AgentOrquestación de agentes, envío de subagentes
ModelLlamadas al proveedor LLM, análisis de respuestas
ToolEjecución de herramientas, validación de parámetros
SessionPersistencia de sesión, gestión de estado
MemoryOperaciones de memoria/RAG
GraphEjecución de flujo de trabajo de grafo
AuthAutenticación, autorización
ServerHTTP servidor, configuración

ErrorCategory clasifica lo que salió mal:

CategoríaHTTPCuándo usar
InvalidInput400Parámetros incorrectos, configuración, cuerpo de la solicitud
Unauthorized401Credenciales faltantes o inválidas
Forbidden403Credenciales válidas, permisos insuficientes
NotFound404El recurso no existe
RateLimited429Límite de tasa del upstream (reintentable)
Timeout408La operación excedió el límite de tiempo (reintentable)
Unavailable503Servicio upstream caído (reintentable)
Cancelled499Cancelado por el llamador o el sistema
Internal500Errores, violaciones de invariantes
Unsupported501Funcionalidad no compatible

Constructores de conveniencia

Para patrones comunes:

// 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 del builder

Adjunta metadatos estructurados para un contexto de error más rico:

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),
});

Pistas de reintento

Las categorías reintentables (RateLimited, Unavailable, Timeout) establecen automáticamente should_retry: true. Comprueba la capacidad de reintento con err.is_retryable(), que lee retry.should_retry como única fuente de verdad:

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

Comprobaciones de categoría

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

Comprobaciones de componente (compatibilidad hacia atrás)

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

Estado de HTTP y problema JSON

AdkError se mapea directamente a respuestas 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", ... } }

Errores locales al crate con From Impls

Los crates con errores específicos del dominio implementan 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)
    }
}

No a las impls From generales

std::io::Error y serde_json::Error cruzan demasiados límites de subsistemas para una sola conversión general. Usa map_err explícitos con el componente correcto:

// 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}")))?;

Mensajes de error

Escribe mensajes de error claros y accionables:

// 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")

Sin panics en el código de la librería

Los crates de librería nunca deben hacer panic en errores recuperables. Evita unwrap(), expect() y panic!() en código de src/ (el código de pruebas está exento).

RwLock / Mutex: Usa una degradación elegante en lugar de unwrap(). Un lock envenenado significa que otro hilo hizo panic; hacer que el hilo actual también falle solo empeora las cosas.

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

Constructores: Devuelve Result cuando la inicialización pueda fallar (por ejemplo, al conectarse a servicios externos).

// 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étodos del builder: Usa if let Some en lugar de expect() para 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
}

Patrones asíncronos

Usa Tokio

Todo el código async usa el runtime de 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));

Convención de características de Tokio

Los crates de librería (adk-*) deben declarar solo las características mínimas de tokio que realmente usan:

# 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"] }

Nunca uses features = ["full"] en un crate de librería. Esto obliga a todos los consumidores posteriores a compilar todos los subsistemas de tokio aunque no los necesiten.

Traits async

Usa async_trait para métodos async de traits:

use async_trait::async_trait;

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

Streaming

Usa EventStream para respuestas 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)
}

Seguridad de hilos

Todos los tipos públicos deben ser 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>();
}

Pruebas

Ejecutar pruebas

ADK-Rust usa cargo-nextest para la ejecución de pruebas. Nextest ejecuta cada binario de pruebas en un proceso separado con programación en paralelo, lo que da una aceleración de ~10x sobre cargo test en este workspace.

# 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 configuración vive en .config/nextest.toml con dos perfiles:

  • default — desarrollo local (fallo rápido, sin reintentos)
  • ci — ejecuciones de CI (reintentos para pruebas inestables, advertencias de pruebas lentas)

Organización de pruebas

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

Pruebas unitarias

Coloca las pruebas unitarias en el mismo archivo que el código:

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

Pruebas de integración

Coloca en el directorio 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");
}

Pruebas con mocks

Usa MockLlm para probar sin llamadas a 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
}

Comandos de prueba

# 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)

Documentación

Comentarios de documentación

Usa /// para elementos públicos:

/// 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 {
    // ...
}

Documentación del módulo

Añade documentación a nivel de módulo en la parte superior de lib.rs:

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

Archivos README

Cada crate debería tener un README.md con:

  1. Breve descripción
  2. Instrucciones de instalación
  3. Ejemplo rápido
  4. Enlace a la documentación completa

Pruebas de documentación

Asegúrate de que los ejemplos de la documentación compilen:

cargo test --doc --all

Proceso de Pull Request

Antes de enviar

  1. Ejecuta la suite completa de pruebas:

    cargo nextest run --workspace
  2. Ejecuta clippy:

    cargo clippy --all-targets --all-features
  3. Formatea el código:

    cargo fmt --all
  4. Actualiza la documentación si agregas/cambias API públicos

  5. Añade pruebas para la nueva funcionalidad

Guías para PR

  • Título: Descripción clara y concisa del cambio
  • Descripción: Explica qué y por qué (no cómo)
  • Tamaño: Mantén los PR enfocados; divide los cambios grandes
  • Pruebas: Incluye pruebas para la nueva funcionalidad
  • Cambios incompatibles: Documenta claramente en la descripción

Mensajes de commit

Sigue 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

Andamiaje del proyecto

Usa cargo adk new con el Composable Template System para crear nuevos proyectos. El registro integrado proporciona 12 plantillas, 9 complementos y 5 patrones empresariales:

# 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

La bandera --addon es componible: combina cualquier plantilla base con cualquier número de complementos. Consulta la documentación de Composable Templates para la lista completa de plantillas, complementos y patrones empresariales.

Tareas comunes

Añadir una nueva herramienta

  1. Crea la herramienta:
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. Añádela al agent:
let agent = LlmAgentBuilder::new("agent")
    .model(model)
    .tool(Arc::new(MyTool::new()))
    .build()?;

Añadir un nuevo proveedor de modelo

  1. Crea el módulo en adk-model/src/:
// adk-model/src/mymodel/mod.rs
mod client;
pub use client::MyModelClient;
  1. Implementa el 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. Añade la bandera de funcionalidad en adk-model/Cargo.toml:
[features]
mymodel = ["dep:mymodel-sdk"]
  1. Exporta condicionalmente:
#[cfg(feature = "mymodel")]
pub mod mymodel;
#[cfg(feature = "mymodel")]
pub use mymodel::MyModelClient;

Añadir un nuevo tipo de agent

  1. Crea el módulo en 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. Exporta en adk-agent/src/lib.rs:
mod my_agent;
pub use my_agent::MyAgent;

Consejos de depuración

  1. Activa tracing:

    adk_telemetry::init_telemetry();
  2. Inspecciona eventos:

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

    RUST_LOG=debug cargo run --example myexample

Anterior: ← Access Control

¿Preguntas? Abre un issue en GitHub.