개발 지침

이 문서는 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는 의존성 순서대로 배포되어야 합니다:

  1. adk-core(내부 의존성 없음)
  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)

코드 스타일

일반 원칙

  1. 기발함보다 명확성: 읽고 이해하기 쉬운 코드를 작성하세요
  2. 암시보다 명시: 명시적인 타입과 오류 처리를 선호하세요
  3. 작은 함수: 가능하면 함수를 집중된 역할로 유지하고 50줄 미만으로 유지하세요
  4. 의미 있는 이름: 설명적인 변수 및 함수 이름을 사용하세요

형식 지정

기본 설정으로 rustfmt를 사용하세요:

cargo fmt --all

CI 파이프라인은 형식을 강제합니다. 커밋하기 전에 항상 cargo fmt를 실행하세요.

명명 규칙

유형규칙예시
크레이트adk-* (kebab-case)adk-core, adk-agent
모듈snake_casellm_agent, function_tool
타입/트레이트PascalCaseLlmAgent, ToolContext
함수snake_caseexecute_tool, run_agent
상수SCREAMING_SNAKE_CASEKEY_PREFIX_APP
타입 매개변수대문자 하나 또는 PascalCaseT, 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에이전트 오케스트레이션, 하위 에이전트 디스패치
ModelLLM provider 호출, 응답 파싱
Tool도구 실행, 매개변수 검증
Session세션 지속성, 상태 관리
Memory메모리/RAG 작업
Graph그래프 워크플로 실행
Auth인증, 권한 부여
ServerHTTP 서버, 설정

ErrorCategory은 무엇이 잘못되었는지 분류합니다:

범주HTTP사용하는 경우
InvalidInput400잘못된 매개변수, 구성, 요청 본문
Unauthorized401자격 증명이 없거나 유효하지 않음
Forbidden403유효한 자격 증명, 권한 부족
NotFound404리소스가 존재하지 않음
RateLimited429상위 제한 속도 초과(재시도 가능)
Timeout408작업이 시간 제한을 초과함(재시도 가능)
Unavailable503상위 서비스 다운(재시도 가능)
Cancelled499호출자 또는 시스템에 의해 취소됨
Internal500버그, 불변식 위반
Unsupported501지원되지 않는 기능

편의 생성자

일반적인 패턴의 경우:

// 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::Errorserde_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 SomeArc::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가 있어야 합니다:

  1. 간단한 설명
  2. 설치 지침
  3. 빠른 예시
  4. 전체 문서 링크

문서 테스트

문서 예제가 컴파일되는지 확인하세요:

cargo test --doc --all

풀 리퀘스트 절차

제출 전

  1. 전체 테스트 스위트 실행:

    cargo nextest run --workspace
  2. clippy 실행:

    cargo clippy --all-targets --all-features
  3. 코드 포맷:

    cargo fmt --all
  4. 공개 API를 추가/변경했다면 문서 업데이트

  5. 새 기능에 대한 테스트 추가

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 문서를 참조하세요.

일반적인 작업

새 도구 추가

  1. 도구 생성:
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. agent에 추가:
let agent = LlmAgentBuilder::new("agent")
    .model(model)
    .tool(Arc::new(MyTool::new()))
    .build()?;

새 모델 제공자 추가

  1. adk-model/src/모듈 생성:
// adk-model/src/mymodel/mod.rs
mod client;
pub use client::MyModelClient;
  1. 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. adk-model/Cargo.toml기능 플래그 추가:
[features]
mymodel = ["dep:mymodel-sdk"]
  1. 조건부로 내보내기:
#[cfg(feature = "mymodel")]
pub mod mymodel;
#[cfg(feature = "mymodel")]
pub use mymodel::MyModelClient;

새 agent 유형 추가

  1. 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. adk-agent/src/lib.rs내보내기:
mod my_agent;
pub use my_agent::MyAgent;

디버깅 팁

  1. tracing 활성화:

    adk_telemetry::init_telemetry();
  2. 이벤트 검사:

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

    RUST_LOG=debug cargo run --example myexample

이전: ← 액세스 제어

질문이 있으신가요? GitHub에 이슈를 여세요.