Diretrizes de Desenvolvimento

Este documento fornece diretrizes abrangentes para desenvolvedores que contribuem para o adk-rust. Seguir esses padrões garante a qualidade, consistência e manutenibilidade do código em todo o projeto.

Índice

Introdução

Pré-requisitos

  • Rust: 1.75 ou superior (verifique com rustc --version)
  • Cargo: Mais recente estável
  • Git: Para controle de versão

Configurando Seu Ambiente

# Clone o repositório
git clone https://github.com/zavora-ai/adk-rust.git
cd adk-rust

# Construa o projeto
cargo build

# Execute todos os testes
cargo test --all

# Verifique por lints
cargo clippy --all-targets --all-features

# Formate o código
cargo fmt --all

Variáveis de Ambiente

Para executar exemplos e testes que exigem chaves de API:

# Gemini (provedor padrão)
export GOOGLE_API_KEY="your-api-key"

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

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

Estrutura do Projeto

adk-rust é organizado como um Cargo workspace com múltiplos crates:

adk-rust/
├── adk-core/       # Traits e tipos fundamentais (Agent, Tool, Llm, Event)
├── adk-telemetry/  # Integração OpenTelemetry
├── adk-model/      # Provedores de LLM (Gemini, OpenAI, Anthropic)
├── adk-tool/       # Sistema de ferramentas (FunctionTool, MCP, AgentTool)
├── adk-session/    # Gerenciamento de sessão (em memória, SQLite)
├── adk-artifact/   # Armazenamento de artefatos binários
├── adk-memory/     # Memória de longo prazo com pesquisa
├── adk-agent/      # Implementações de Agent (LlmAgent, agentes de fluxo de trabalho)
├── adk-runner/     # Tempo de execução
├── adk-server/     # API REST e protocolo A2A
├── adk-cli/        # Lançador de linha de comando
├── adk-realtime/   # Agentes de streaming de voz/áudio
├── adk-graph/      # Fluxos de trabalho estilo LangGraph
├── adk-browser/    # Ferramentas de automação de navegador
├── adk-eval/       # Estrutura de avaliação de Agent
├── adk-rust/       # Crate guarda-chuva (re-exporta tudo)
└── examples/       # Exemplos de trabalho

Dependências dos Crates

Os crates devem ser publicados na ordem de dependência:

  1. adk-core (sem dependências 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 (guarda-chuva)

Estilo de Código

Princípios Gerais

  1. Clareza acima da inteligência: Escreva código que seja fácil de ler e entender
  2. Explícito acima do implícito: Prefira tipos explícitos e tratamento de erros
  3. Funções pequenas: Mantenha as funções focadas e com menos de 50 linhas, quando possível
  4. Nomes significativos: Use nomes descritivos para variáveis e funções

Formatação

Use rustfmt com as configurações padrão:

cargo fmt --all

O pipeline de CI impõe a formatação. Sempre execute cargo fmt antes de fazer o commit.

Convenções de Nomenclatura

TipoConvençãoExemplo
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 parametersSingle uppercase or PascalCaseT, State

Importações

Organize as importações nesta ordem:

// 1. Biblioteca padrão
use std::collections::HashMap;
use std::sync::Arc;

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

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

// 4. Módulos locais
use crate::config::Config;
use super::utils;

Clippy

Todo o código deve passar pelo clippy sem avisos:

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

Resolva os avisos do clippy em vez de suprimi-los. Se a supressão for necessária, documente o motivo:

#[allow(clippy::too_many_arguments)]
// O padrão Builder requer muitos parâmetros; refatorar prejudicaria a usabilidade
fn complex_builder(...) { }

Tratamento de Erros

Use adk_core::AdkError

Todos os erros devem usar o tipo de erro centralizado:

use adk_core::{AdkError, Result};

// Retorne Result<T> (apelidado como Result<T, AdkError>)
pub async fn my_function() -> Result<String> {
    // Use ? para propagação
    let data = fetch_data().await?;

    // Crie erros com variantes apropriadas
    if data.is_empty() {
        return Err(AdkError::Tool("No data found".into()));
    }

    Ok(data)
}

Variantes de Erro

Use a variante de erro apropriada:

VarianteCaso de Uso
AdkError::Agent(String)Erros de execução de Agent
AdkError::Model(String)Erros do provedor de LLM
AdkError::Tool(String)Erros de execução de Tool
AdkError::Session(String)Erros de gerenciamento de Session
AdkError::Artifact(String)Erros de armazenamento de artefatos
AdkError::Config(String)Erros de configuração
AdkError::Network(String)Erros de HTTP/rede

Mensagens de Erro

Escreva mensagens de erro claras e acionáveis:

// Bom: Específico e acionável
Err(AdkError::Config("API key not found. Set GOOGLE_API_KEY environment variable.".into()))

// Ruim: Vago
Err(AdkError::Config("Invalid config".into()))

Padrões Assíncronos

Usar Tokio

Todo o código async utiliza o runtime Tokio:

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

// Preferir RwLock para dados com muitas leituras
let state: Arc<RwLock<State>> = Arc::new(RwLock::new(State::default()));

// Usar Mutex para casos simples ou com muitas escritas
let counter: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));

Traits Assíncronos

Utilize 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

Utilize EventStream para respostas em 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)
}

Segurança de Thread

Todos os tipos públicos devem ser Send + Sync:

// Bom: Thread-safe
pub struct MyAgent {
    name: String,
    tools: Vec<Arc<dyn Tool>>,  // Arc para propriedade compartilhada
}

// Verificar com verificações em tempo de compilação
fn assert_send_sync<T: Send + Sync>() {}
fn _check() {
    assert_send_sync::<MyAgent>();
}

Testes

Organização dos Testes

crate/
├── src/
│   ├── lib.rs          # Testes de unidade no final do arquivo
│   └── module.rs       # Testes específicos do módulo
└── tests/
    └── integration.rs  # Testes de integração

Testes de Unidade

Coloque os testes de unidade no mesmo arquivo que o 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());
    }
}

Testes de Integração

Coloque no diretório tests/:

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

#[tokio::test]
async fn test_full_workflow() {
    // Configuração
    let service = InMemorySessionService::new();

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

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

Testes com Mock

Utilize MockLlm para testar sem chamadas de 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();

    // Testar o comportamento do agent
}

Comandos de Teste

# Executar todos os testes
cargo test --all

# Executar testes de um crate específico
cargo test --package adk-core

# Executar com saída
cargo test --all -- --nocapture

# Executar testes ignorados (exigem chaves de API)
cargo test --all -- --ignored

Documentação

Comentários de Documentação

Utilize /// para itens públicos:

/// Cria um novo LLM agent com a configuração especificada.
///
/// # Arguments
///
/// * `name` - Um identificador único para este agent
/// * `model` - O provedor LLM a ser usado para o raciocínio
///
/// # Examples
///
/// ```rust
/// use adk_agent::LlmAgentBuilder;
///
/// let agent = LlmAgentBuilder::new("assistant")
///     .model(Arc::new(model))
///     .build()?;
/// ```
///
/// # Errors
///
/// Retorna `AdkError::Agent` se o model não estiver configurado.
pub fn new(name: impl Into<String>) -> Self {
    // ...
}

Documentação de Módulo

Adicione docs em nível de módulo no topo de lib.rs:

//! # adk-core
//!
//! Tipos e traits essenciais para o ADK-Rust.
//!
//! ## Visão Geral
//!
//! Este crate fornece os tipos fundamentais...

Arquivos README

Cada crate deve ter um README.md com:

  1. Breve descrição
  2. Instruções de instalação
  3. Exemplo rápido
  4. Link para a documentação completa

Testes de Documentação

Garanta que os exemplos de doc compilam:

cargo test --doc --all

Processo de Pull Request

Antes de Enviar

  1. Execute o conjunto completo de testes:

    cargo test --all
  2. Execute o clippy:

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

    cargo fmt --all
  4. Atualize a documentação se estiver adicionando/alterando a API pública

  5. Adicione testes para novas funcionalidades

Diretrizes de PR

  • Título: Descrição clara e concisa da alteração
  • Descrição: Explique o que e por que (não como)
  • Tamanho: Mantenha os PRs focados; divida alterações grandes
  • Testes: Inclua testes para novas funcionalidades
  • Alterações que quebram a compatibilidade: Documente claramente na descrição

Mensagens de Commit

Siga as convenções de 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

Tarefas Comuns

Adicionando uma Nova Tool

  1. Crie a 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. Adicione ao Agent:
let agent = LlmAgentBuilder::new("agent")
    .model(model)
    .tool(Arc::new(MyTool::new()))
    .build()?;

Adicionando um Novo Provedor de Modelo

  1. Crie o módulo em adk-model/src/:
// adk-model/src/mymodel/mod.rs
mod client;
pub use client::MyModelClient;
  1. Implemente a 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. Adicione a feature flag em adk-model/Cargo.toml:
[features]
mymodel = ["dep:mymodel-sdk"]
  1. Exporte condicionalmente:
#[cfg(feature = "mymodel")]
pub mod mymodel;
#[cfg(feature = "mymodel")]
pub use mymodel::MyModelClient;

Adicionando um Novo Tipo de Agent

  1. Crie o módulo em 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. Exporte em adk-agent/src/lib.rs:
mod my_agent;
pub use my_agent::MyAgent;

Dicas de Depuração

  1. Habilite o rastreamento:

    adk_telemetry::init_telemetry();
  2. Inspecione os eventos:

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

    RUST_LOG=debug cargo run --example myexample

Anterior: ← Controle de Acesso

Perguntas? Abra uma issue no GitHub.