Guías de Desarrollo

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

Tabla de Contenidos

Primeros Pasos

Prerrequisitos

  • Rust: 1.75 o superior (verificar con rustc --version)
  • Cargo: Última versión estable
  • Git: Para control de versiones

Configuración de tu Entorno

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

# Build the project
cargo build

# Run all tests
cargo test --all

# 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 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 Cargo workspace 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 Crate

Los crates deben publicarse en orden de dependencia:

  1. adk-core (no internal deps)
  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 (umbrella)

Estilo de Código

Principios Generales

  1. Claridad sobre la inteligencia: Escribe código que sea fácil de leer y entender
  2. Explícito sobre implícito: Prefiere tipos y manejo de errores explícitos
  3. Funciones pequeñas: Mantén las funciones enfocadas y, si es posible, por debajo de las 50 líneas
  4. Nombres significativos: Usa nombres de variables y funciones descriptivos

Formato

Usa rustfmt con la configuración predeterminada:

cargo fmt --all

La pipeline de CI exige el formato. Siempre ejecuta cargo fmt antes de hacer commit.

Convenciones de Nomenclatura

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 tipoLetra mayúscula única o PascalCaseT, State

Imports

Organiza los imports 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

Usar adk_core::AdkError

Todos los errores deben usar el tipo de error centralizado:

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

Variantes de Error

Usa la variante de error apropiada:

VarianteCaso de Uso
AdkError::Agent(String)Errores de ejecución de Agent
AdkError::Model(String)Errores del proveedor de LLM
AdkError::Tool(String)Errores de ejecución de Tool
AdkError::Session(String)Errores de gestión de Session
AdkError::Artifact(String)Errores de almacenamiento de artefactos
AdkError::Config(String)Errores de configuración
AdkError::Network(String)Errores de red/HTTP

Mensajes de Error

Escribe mensajes de error claros y accionables:

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

Patrones Async

Uso de Tokio

Todo el código async utiliza el runtime de Tokio:

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

// Preferir RwLock para datos con muchas lecturas
let state: Arc<RwLock<State>> = Arc::new(RwLock::new(State::default()));

// Usar Mutex para casos con muchas escrituras o simples
let counter: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));

Traits Async

Utilice async_trait para métodos de trait async:

use async_trait::async_trait;

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

Streaming

Utilice EventStream para respuestas de 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:

// Bueno: Seguro para hilos
pub struct MyAgent {
    name: String,
    tools: Vec<Arc<dyn Tool>>,  // Arc para propiedad compartida
}

// Verificar con comprobaciones en tiempo de compilación
fn assert_send_sync<T: Send + Sync>() {}
fn _check() {
    assert_send_sync::<MyAgent>();
}

Pruebas

Organización de las Pruebas

crate/
├── src/
│   ├── lib.rs          # Pruebas unitarias al final del archivo
│   └── module.rs       # Pruebas específicas del módulo
└── tests/
    └── integration.rs  # Pruebas de integración

Pruebas Unitarias

Coloque 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

Coloque en el directorio tests/:

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

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

    // Ejecución
    let session = service.create(request).await.unwrap();

    // Afirmación
    assert_eq!(session.id(), "test-session");
}

Pruebas con Mock

Utilice MockLlm para realizar pruebas sin llamadas a la 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();

    // Probar el comportamiento del agent
}

Comandos de Prueba

# Ejecutar todas las pruebas
cargo test --all

# Ejecutar pruebas de crate específico
cargo test --package adk-core

# Ejecutar con salida
cargo test --all -- --nocapture

# Ejecutar pruebas ignoradas (requieren claves API)
cargo test --all -- --ignored

Documentación

Comentarios de Documentación

Utilice /// para elementos públicos:

/// Crea un nuevo LLM agent con la configuración especificada.
///
/// # Argumentos
///
/// * `name` - Un identificador único para este agent
/// * `model` - El proveedor LLM a utilizar para el razonamiento
///
/// # Ejemplos
///
/// ```rust
/// use adk_agent::LlmAgentBuilder;
///
/// let agent = LlmAgentBuilder::new("assistant")
///     .model(Arc::new(model))
///     .build()?;
/// ```
///
/// # Errores
///
/// Devuelve `AdkError::Agent` si el modelo no está configurado.
pub fn new(name: impl Into<String>) -> Self {
    // ...
}

Documentación del Módulo

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

//! # adk-core
//!
//! Tipos y traits principales para adk-rust.
//!
//! ## Resumen
//!
//! Este crate proporciona los tipos fundamentales...

Archivos README

Cada crate debe 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úrese de que los ejemplos de la documentación compilan:

cargo test --doc --all

Proceso de Pull Request

Antes de Enviar

  1. Ejecutar el conjunto completo de pruebas:

    cargo test --all
  2. Ejecutar clippy:

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

    cargo fmt --all
  4. Actualizar la documentación si se añade o cambia la API pública

  5. Añadir pruebas para la nueva funcionalidad

Pautas para PRs

  • Título: Descripción clara y concisa del cambio
  • Descripción: Explicar qué y por qué (no cómo)
  • Tamaño: Mantener los PRs enfocados; dividir los cambios grandes
  • Pruebas: Incluir pruebas para la nueva funcionalidad
  • Cambios importantes: Documentar claramente en la descripción

Mensajes de Commit

Siga los 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

Tareas Comunes

Añadir una nueva Tool

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

Añadir un nuevo Proveedor de Modelo

  1. Crear módulo en adk-model/src/:
// adk-model/src/mymodel/mod.rs
mod client;
pub use client::MyModelClient;
  1. Implementar el 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. Añadir feature flag en adk-model/Cargo.toml:
[features]
mymodel = ["dep:mymodel-sdk"]
  1. Exportar condicionalmente:
#[cfg(feature = "mymodel")]
pub mod mymodel;
#[cfg(feature = "mymodel")]
pub use mymodel::MyModelClient;

Añadir un nuevo Tipo de Agent

  1. Crear 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. Exportar en adk-agent/src/lib.rs:
mod my_agent;
pub use my_agent::MyAgent;

Consejos de Depuración

  1. Habilitar tracing:

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

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

    RUST_LOG=debug cargo run --example myexample

Anterior: ← Control de Acceso

¿Preguntas? Abra un issue en GitHub.