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
- Estrutura do Projeto
- Estilo de Código
- Tratamento de Erros
- Padrões Assíncronos
- Testes
- Documentação
- Processo de Pull Request
- Tarefas Comuns
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:
adk-core(sem dependências internas)adk-telemetryadk-modeladk-tooladk-sessionadk-artifactadk-memoryadk-agentadk-runneradk-serveradk-cliadk-realtimeadk-graphadk-browseradk-evaladk-rust(guarda-chuva)
Estilo de Código
Princípios Gerais
- Clareza acima da inteligência: Escreva código que seja fácil de ler e entender
- Explícito acima do implícito: Prefira tipos explícitos e tratamento de erros
- Funções pequenas: Mantenha as funções focadas e com menos de 50 linhas, quando possível
- 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
| Tipo | Convenção | Exemplo |
|---|---|---|
| Crates | adk-* (kebab-case) | adk-core, adk-agent |
| Modules | snake_case | llm_agent, function_tool |
| Types/Traits | PascalCase | LlmAgent, ToolContext |
| Functions | snake_case | execute_tool, run_agent |
| Constants | SCREAMING_SNAKE_CASE | KEY_PREFIX_APP |
| Type parameters | Single uppercase or PascalCase | T, 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:
| Variante | Caso 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:
- Breve descrição
- Instruções de instalação
- Exemplo rápido
- 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
-
Execute o conjunto completo de testes:
cargo test --all -
Execute o clippy:
cargo clippy --all-targets --all-features -
Formate o código:
cargo fmt --all -
Atualize a documentação se estiver adicionando/alterando a API pública
-
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
- 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 }))
}
}
- Adicione ao Agent:
let agent = LlmAgentBuilder::new("agent")
.model(model)
.tool(Arc::new(MyTool::new()))
.build()?;
Adicionando um Novo Provedor de Modelo
- Crie o módulo em
adk-model/src/:
// adk-model/src/mymodel/mod.rs
mod client;
pub use client::MyModelClient;
- 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
}
}
- Adicione a feature flag em
adk-model/Cargo.toml:
[features]
mymodel = ["dep:mymodel-sdk"]
- Exporte condicionalmente:
#[cfg(feature = "mymodel")]
pub mod mymodel;
#[cfg(feature = "mymodel")]
pub use mymodel::MyModelClient;
Adicionando um Novo Tipo de Agent
- 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
}
}
- Exporte em
adk-agent/src/lib.rs:
mod my_agent;
pub use my_agent::MyAgent;
Dicas de Depuração
-
Habilite o rastreamento:
adk_telemetry::init_telemetry(); -
Inspecione os eventos:
while let Some(event) = stream.next().await { eprintln!("Event: {:?}", event); } -
Use RUST_LOG:
RUST_LOG=debug cargo run --example myexample
Anterior: ← Controle de Acesso
Perguntas? Abra uma issue no GitHub.