개발 지침
이 문서는 ADK-Rust에 기여하는 개발자를 위한 포괄적인 지침을 제공합니다. 이러한 표준을 따르면 프로젝트 전반에서 코드 품질, 일관성, 유지보수성을 보장할 수 있습니다.
목차
시작하기
사전 요구 사항
- Rust: 1.95.0 이상(edition 2024,
rustc --version로 확인) - Cargo: 최신 안정 버전
- Git: 버전 관리용
- sccache(권장): 빌드 재실행 시간을 약 70% 줄여 주는 컴파일 캐시
환경 설정
# 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
환경 변수
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"
프로젝트 구조
ADK-Rust는 여러 crate로 구성된 Cargo 작업 공간으로 조직되어 있습니다:
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
Crate 의존성
Crate는 의존성 순서대로 배포되어야 합니다:
adk-core(내부 의존성 없음)adk-telemetryadk-modeladk-tooladk-sessionadk-artifactadk-memoryadk-agentadk-runneradk-serveradk-cliadk-realtimeadk-graphadk-browseradk-evaladk-rust(umbrella)
코드 스타일
일반 원칙
- 기발함보다 명확성: 읽고 이해하기 쉬운 코드를 작성하세요
- 암시보다 명시: 명시적인 타입과 오류 처리를 선호하세요
- 작은 함수: 가능하면 함수를 집중된 역할로 유지하고 50줄 미만으로 유지하세요
- 의미 있는 이름: 설명적인 변수 및 함수 이름을 사용하세요
형식 지정
기본 설정으로 rustfmt를 사용하세요:
cargo fmt --all
CI 파이프라인은 형식을 강제합니다. 커밋하기 전에 항상 cargo fmt를 실행하세요.
명명 규칙
| 유형 | 규칙 | 예시 |
|---|---|---|
| 크레이트 | adk-* (kebab-case) | adk-core, adk-agent |
| 모듈 | snake_case | llm_agent, function_tool |
| 타입/트레이트 | PascalCase | LlmAgent, ToolContext |
| 함수 | snake_case | execute_tool, run_agent |
| 상수 | SCREAMING_SNAKE_CASE | KEY_PREFIX_APP |
| 타입 매개변수 | 대문자 하나 또는 PascalCase | T, State |
임포트
다음 순서로 임포트를 정리하세요:
// 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
모든 코드는 경고 없이 clippy를 통과해야 합니다:
cargo clippy --all-targets --all-features
억제(suppress)하기보다는 clippy 경고를 해결하세요. 억제가 필요하다면 그 이유를 문서화하세요:
#[allow(clippy::too_many_arguments)]
// Builder pattern requires many parameters; refactoring would hurt usability
fn complex_builder(...) { }
오류 처리
구조화된 오류 봉투
AdkError은 component(어디에서), category(어떤 종류인지), code(기계 키), message(사람이 읽는 텍스트), retry hint, 그리고 선택적 details로 구성된 구조화된 오류 유형입니다:
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)
}
Component와 Category 선택
ErrorComponent은 실패가 어디에서 발생했는지(표면화되는 trait boundary가 아니라 원본 하위 시스템)를 식별합니다:
| 구성 요소 | 사용하는 경우 |
|---|---|
Agent | 에이전트 오케스트레이션, 하위 에이전트 디스패치 |
Model | LLM provider 호출, 응답 파싱 |
Tool | 도구 실행, 매개변수 검증 |
Session | 세션 지속성, 상태 관리 |
Memory | 메모리/RAG 작업 |
Graph | 그래프 워크플로 실행 |
Auth | 인증, 권한 부여 |
Server | HTTP 서버, 설정 |
ErrorCategory은 무엇이 잘못되었는지 분류합니다:
| 범주 | HTTP | 사용하는 경우 |
|---|---|---|
InvalidInput | 400 | 잘못된 매개변수, 구성, 요청 본문 |
Unauthorized | 401 | 자격 증명이 없거나 유효하지 않음 |
Forbidden | 403 | 유효한 자격 증명, 권한 부족 |
NotFound | 404 | 리소스가 존재하지 않음 |
RateLimited | 429 | 상위 제한 속도 초과(재시도 가능) |
Timeout | 408 | 작업이 시간 제한을 초과함(재시도 가능) |
Unavailable | 503 | 상위 서비스 다운(재시도 가능) |
Cancelled | 499 | 호출자 또는 시스템에 의해 취소됨 |
Internal | 500 | 버그, 불변식 위반 |
Unsupported | 501 | 지원되지 않는 기능 |
편의 생성자
일반적인 패턴의 경우:
// 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
더 풍부한 오류 컨텍스트를 위해 구조화된 메타데이터를 첨부합니다:
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),
});
재시도 힌트
재시도 가능한 범주(RateLimited, Unavailable, Timeout)는 should_retry: true를 자동으로 설정합니다. 재시도 가능 여부는 err.is_retryable()로 확인하며, 이는 retry.should_retry를 단일 진실의 원천으로 읽습니다:
if err.is_retryable() {
if let Some(delay) = err.retry.retry_after() {
tokio::time::sleep(delay).await;
}
// retry the operation
}
범주 확인
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
컴포넌트 확인(하위 호환)
err.is_model() // component == Model
err.is_tool() // component == Tool
err.is_session() // component == Session
err.is_config() // code == "config.legacy" (temporary bridge)
HTTP 상태 및 문제 JSON
AdkError는 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", ... } }
From Impls를 사용한 크레이트 로컬 오류
도메인별 오류가 있는 크레이트는 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)
}
}
포괄적 From Impls 없음
std::io::Error와 serde_json::Error는 단일 포괄 변환에 비해 너무 많은 하위 시스템 경계를 가로지릅니다. 올바른 컴포넌트에 대해 명시적인 map_err를 사용하세요:
// 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}")))?;
오류 메시지
명확하고 실행 가능한 오류 메시지를 작성하세요:
// 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")
라이브러리 코드에서 패닉 금지
라이브러리 크레이트는 복구 가능한 오류에서 절대 패닉하면 안 됩니다. unwrap(), expect(), panic!()를 src/ 코드에서 피하세요(테스트 코드는 예외입니다).
RwLock / Mutex: unwrap() 대신 우아한 저하를 사용하세요. poisoned lock은 다른 스레드가 패닉했다는 뜻이며, 현재 스레드까지 크래시하면 상황만 더 악화됩니다.
// 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());
생성자: 초기화가 실패할 수 있을 때는 Result를 반환하세요(예: 외부 서비스 연결).
// 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 })
}
빌더 메서드: expect() 대신 if let Some를 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
}
비동기 패턴
Tokio 사용
모든 비동기 코드는 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));
Tokio 기능 규칙
라이브러리 크레이트(adk-*)는 실제로 사용하는 최소한의 tokio 기능만 선언해야 합니다:
# 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"] }
라이브러리 크레이트에서 절대 features = ["full"]를 사용하지 마세요. 이렇게 하면 하위 소비자가 필요 여부와 상관없이 모든 tokio 하위 시스템을 컴파일하게 됩니다.
비동기 트레이트
비동기 트레이트 메서드에는 async_trait를 사용하세요:
use async_trait::async_trait;
#[async_trait]
pub trait MyTrait: Send + Sync {
async fn do_work(&self) -> Result<()>;
}
스트리밍
스트리밍 응답에는 EventStream를 사용하세요:
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)
}
스레드 안전성
모든 public 타입은 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>();
}
테스트
테스트 실행기
ADK-Rust는 테스트 실행에 cargo-nextest를 사용합니다. Nextest는 각 테스트 바이너리를 별도 프로세스로 실행하고 병렬 스케줄링을 사용하여, 이 작업공간에서 cargo test보다 약 10배 빠른 속도를 제공합니다.
# 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
설정은 .config/nextest.toml에 있으며 두 개의 프로필이 있습니다:
default— 로컬 개발(fail-fast, 재시도 없음)ci— CI 실행(불안정한 테스트 재시도, 느린 테스트 경고)
테스트 조직
crate/
├── src/
│ ├── lib.rs # Unit tests at bottom of file
│ └── module.rs # Module-specific tests
└── tests/
└── integration.rs # Integration tests
단위 테스트
단위 테스트는 코드와 같은 파일에 배치하세요:
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/ 디렉터리에 배치하세요:
// 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");
}
모의 테스트
API 호출 없이 테스트하려면 MockLlm를 사용하세요:
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
}
테스트 명령
# 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)
문서화
문서 주석
공개 항목에는 ///를 사용하세요:
/// 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 {
// ...
}
모듈 문서화
lib.rs 맨 위에 모듈 수준 문서를 추가하세요:
//! # adk-core
//!
//! Core types and traits for ADK-Rust.
//!
//! ## Overview
//!
//! This crate provides the foundational types...
README 파일
각 크레이트에는 다음이 포함된 README.md가 있어야 합니다:
- 간단한 설명
- 설치 지침
- 빠른 예시
- 전체 문서 링크
문서 테스트
문서 예제가 컴파일되는지 확인하세요:
cargo test --doc --all
풀 리퀘스트 절차
제출 전
-
전체 테스트 스위트 실행:
cargo nextest run --workspace -
clippy 실행:
cargo clippy --all-targets --all-features -
코드 포맷:
cargo fmt --all -
공개 API를 추가/변경했다면 문서 업데이트
-
새 기능에 대한 테스트 추가
PR 가이드라인
- 제목: 변경 사항을 명확하고 간결하게 설명
- 설명: 무엇을 했는지와 왜 했는지 설명(어떻게는 아님)
- 크기: PR은 집중되게 유지; 큰 변경은 분리
- 테스트: 새 기능에 대한 테스트 포함
- 파괴적 변경: 설명에 명확히 문서화
커밋 메시지
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
프로젝트 스캐폴딩
새 프로젝트를 스캐폴딩하려면 Composable Template System과 함께 cargo adk new를 사용하세요. 내장 레지스트리는 12개의 템플릿, 9개의 애드온, 5개의 엔터프라이즈 패턴을 제공합니다:
# 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
--addon 플래그는 조합 가능하며, 어떤 기본 템플릿이든 원하는 수의 애드온과 결합할 수 있습니다. 템플릿, 애드온, 엔터프라이즈 패턴의 전체 목록은 Composable Templates 문서를 참조하세요.
일반적인 작업
새 도구 추가
- 도구 생성:
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 }))
}
}
- agent에 추가:
let agent = LlmAgentBuilder::new("agent")
.model(model)
.tool(Arc::new(MyTool::new()))
.build()?;
새 모델 제공자 추가
adk-model/src/에 모듈 생성:
// adk-model/src/mymodel/mod.rs
mod client;
pub use client::MyModelClient;
- 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
}
}
adk-model/Cargo.toml에 기능 플래그 추가:
[features]
mymodel = ["dep:mymodel-sdk"]
- 조건부로 내보내기:
#[cfg(feature = "mymodel")]
pub mod mymodel;
#[cfg(feature = "mymodel")]
pub use mymodel::MyModelClient;
새 agent 유형 추가
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
}
}
adk-agent/src/lib.rs에 내보내기:
mod my_agent;
pub use my_agent::MyAgent;
디버깅 팁
-
tracing 활성화:
adk_telemetry::init_telemetry(); -
이벤트 검사:
while let Some(event) = stream.next().await { eprintln!("Event: {:?}", event); } -
RUST_LOG 사용:
RUST_LOG=debug cargo run --example myexample
이전: ← 액세스 제어
질문이 있으신가요? GitHub에 이슈를 여세요.