개발 가이드라인

이 문서는 adk-rust에 기여하는 개발자를 위한 포괄적인 가이드라인을 제공합니다. 이 표준을 따르면 프로젝트 전반의 코드 품질, 일관성 및 유지 관리성을 보장할 수 있습니다.

목차

시작하기

사전 요구 사항

  • Rust: 1.75 이상 (rustc --version으로 확인)
  • Cargo: 최신 안정 버전
  • Git: 버전 관리를 위해

환경 설정

# 저장소 복제
git clone https://github.com/zavora-ai/adk-rust.git
cd adk-rust

# 프로젝트 빌드
cargo build

# 모든 테스트 실행
cargo test --all

# 린트 확인
cargo clippy --all-targets --all-features

# 코드 포맷팅
cargo fmt --all

환경 변수

API 키가 필요한 예제 및 테스트 실행을 위해:

# Gemini (기본 제공자)
export GOOGLE_API_KEY="your-api-key"

# OpenAI (선택 사항)
export OPENAI_API_KEY="your-api-key"

# Anthropic (선택 사항)
export ANTHROPIC_API_KEY="your-api-key"

프로젝트 구조

adk-rust는 여러 crate를 포함하는 Cargo 워크스페이스로 구성됩니다:

adk-rust/
├── adk-core/       # 기본 트레이트 및 타입 (Agent, Tool, Llm, Event)
├── adk-telemetry/  # OpenTelemetry 통합
├── adk-model/      # LLM 제공자 (Gemini, OpenAI, Anthropic)
├── adk-tool/       # Tool 시스템 (FunctionTool, MCP, AgentTool)
├── adk-session/    # 세션 관리 (인메모리, SQLite)
├── adk-artifact/   # 바이너리 아티팩트 저장소
├── adk-memory/     # 검색 기능이 있는 장기 메모리
├── adk-agent/      # Agent 구현 (LlmAgent, 워크플로 Agent)
├── adk-runner/     # 실행 런타임
├── adk-server/     # REST API 및 A2A 프로토콜
├── adk-cli/        # 명령줄 실행기
├── adk-realtime/   # 음성/오디오 스트리밍 Agent
├── adk-graph/      # LangGraph 스타일 워크플로
├── adk-browser/    # 브라우저 자동화 Tool
├── adk-eval/       # Agent 평가 프레임워크
├── adk-rust/       # Umbrella crate (모두 재익스포트)
└── 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를 실행하세요.

명명 규칙

TypeConventionExample
크레이트adk-* (kebab-case)adk-core, adk-agent
모듈snake_casellm_agent, function_tool
타입/트레잇PascalCaseLlmAgent, ToolContext
함수snake_caseexecute_tool, run_agent
상수SCREAMING_SNAKE_CASEKEY_PREFIX_APP
타입 매개변수Single uppercase or PascalCaseT, State

임포트

다음 순서로 임포트를 정리하세요:

// 1. 표준 라이브러리
use std::collections::HashMap;
use std::sync::Arc;

// 2. 외부 크레이트
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;

// 3. 내부 크레이트 (adk-*)
use adk_core::{Agent, Event, Result};

// 4. 로컬 모듈
use crate::config::Config;
use super::utils;

Clippy

모든 코드는 경고 없이 clippy를 통과해야 합니다:

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

clippy 경고를 억제하기보다는 해결하세요. 억제가 필요한 경우 이유를 문서화하세요:

#[allow(clippy::too_many_arguments)]
// 빌더 패턴에는 많은 매개변수가 필요하며, 리팩토링은 사용성을 저해할 수 있습니다.
fn complex_builder(...) { }

오류 처리

adk_core::AdkError 사용

모든 오류는 중앙 집중식 오류 타입을 사용해야 합니다:

use adk_core::{AdkError, Result};

// Result<T> (Result<T, AdkError>의 별칭) 반환
pub async fn my_function() -> Result<String> {
    // 전파를 위해 ? 사용
    let data = fetch_data().await?;

    // 적절한 유형으로 오류 생성
    if data.is_empty() {
        return Err(AdkError::Tool("No data found".into()));
    }

    Ok(data)
}

오류 유형

적절한 오류 유형을 사용하세요:

VariantUse Case
AdkError::Agent(String)Agent 실행 오류
AdkError::Model(String)LLM 공급자 오류
AdkError::Tool(String)Tool 실행 오류
AdkError::Session(String)세션 관리 오류
AdkError::Artifact(String)아티팩트 저장 오류
AdkError::Config(String)구성 오류
AdkError::Network(String)HTTP/네트워크 오류

오류 메시지

명확하고 실행 가능한 오류 메시지를 작성하세요:

// 좋음: 구체적이고 실행 가능함
Err(AdkError::Config("API key not found. Set GOOGLE_API_KEY environment variable.".into()))

// 나쁨: 모호함
Err(AdkError::Config("Invalid config".into()))

Async 패턴

Tokio 사용

모든 async 코드는 Tokio 런타임을 사용합니다:

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

// 읽기 작업이 많은 데이터에는 RwLock을 선호합니다
let state: Arc<RwLock<State>> = Arc::new(RwLock::new(State::default()));

// 쓰기 작업이 많거나 간단한 경우에는 Mutex를 사용합니다
let counter: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));

Async 트레이트

async 트레이트 메서드에는 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여야 합니다:

// 좋음: 스레드 안전
pub struct MyAgent {
    name: String,
    tools: Vec<Arc<dyn Tool>>,  // 공유 소유권을 위한 Arc
}

// 컴파일 시점 검사로 확인
fn assert_send_sync<T: Send + Sync>() {}
fn _check() {
    assert_send_sync::<MyAgent>();
}

테스트

테스트 구성

crate/
├── src/
│   ├── lib.rs          # 파일 하단에 유닛 테스트
│   └── module.rs       # 모듈별 테스트
└── tests/
    └── integration.rs  # 통합 테스트

유닛 테스트

유닛 테스트는 코드와 동일한 파일에 배치합니다:

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() {
    // 설정
    let service = InMemorySessionService::new();

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

    // 검증
    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();

    // Agent 동작 테스트
}

테스트 명령어

# 모든 테스트 실행
cargo test --all

# 특정 크레이트 테스트 실행
cargo test --package adk-core

# 출력과 함께 실행
cargo test --all -- --nocapture

# 무시된 테스트 실행 (API 키 필요)
cargo test --all -- --ignored

문서화

문서 주석

public 항목에는 ///를 사용합니다:

/// 지정된 구성으로 새 LLM agent를 생성합니다.
///
/// # 인자
///
/// * `name` - 이 agent의 고유 식별자
/// * `model` - 추론에 사용할 LLM provider
///
/// # 예시
///
/// ```rust
/// use adk_agent::LlmAgentBuilder;
///
/// let agent = LlmAgentBuilder::new("assistant")
///     .model(Arc::new(model))
///     .build()?;
/// ```
///
/// # 오류
///
/// model이 설정되지 않은 경우 `AdkError::Agent`를 반환합니다.
pub fn new(name: impl Into<String>) -> Self {
    // ...
}

모듈 문서

lib.rs 파일 상단에 모듈 수준 문서를 추가합니다:

//! # adk-core
//!
//! ADK-Rust를 위한 핵심 타입 및 트레이트.
//!
//! ## 개요
//!
//! 이 크레이트는 기본 타입들을 제공합니다...

README 파일

각 크레이트는 다음을 포함하는 README.md를 가져야 합니다:

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

문서 테스트

문서 예시가 컴파일되는지 확인합니다:

cargo test --doc --all

Pull Request 프로세스

제출 전

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

    cargo test --all
  2. clippy 실행:

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

    cargo fmt --all
  4. 퍼블릭 API를 추가하거나 변경하는 경우 문서 업데이트

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

PR 가이드라인

  • 제목: 변경 사항에 대한 명확하고 간결한 설명
  • 설명: 무엇을 왜 변경했는지 설명 (어떻게 변경했는지가 아님)
  • 크기: PR의 초점을 유지하고, 큰 변경 사항은 분할
  • 테스트: 새로운 기능에 대한 테스트 포함
  • 호환성 파괴 변경: 설명에 명확히 문서화

커밋 메시지

컨벤셔널 커밋 규칙을 따르세요:

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

일반적인 작업

새로운 Tool 추가

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

새로운 Model Provider 추가

  1. adk-model/src/모듈 생성:
// adk-model/src/mymodel/mod.rs
mod client;
pub use client::MyModelClient;
  1. 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. 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. 트레이싱 활성화:

    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

이전: ← Access Control

질문이 있으신가요? GitHub에 이슈를 열어주세요.