개발 가이드라인
이 문서는 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는 의존성 순서대로 게시되어야 합니다:
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를 실행하세요.
명명 규칙
| Type | Convention | Example |
|---|---|---|
| 크레이트 | 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 |
| 타입 매개변수 | Single uppercase or PascalCase | T, 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)
}
오류 유형
적절한 오류 유형을 사용하세요:
| Variant | Use 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를 가져야 합니다:
- 간략한 설명
- 설치 지침
- 빠른 예시
- 전체 문서 링크
문서 테스트
문서 예시가 컴파일되는지 확인합니다:
cargo test --doc --all
Pull Request 프로세스
제출 전
-
전체 테스트 스위트 실행:
cargo test --all -
clippy 실행:
cargo clippy --all-targets --all-features -
코드 포맷팅:
cargo fmt --all -
퍼블릭 API를 추가하거나 변경하는 경우 문서 업데이트
-
새로운 기능에 대한 테스트 추가
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 추가
- 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 }))
}
}
- Agent에 추가:
let agent = LlmAgentBuilder::new("agent")
.model(model)
.tool(Arc::new(MyTool::new()))
.build()?;
새로운 Model Provider 추가
adk-model/src/에 모듈 생성:
// adk-model/src/mymodel/mod.rs
mod client;
pub use client::MyModelClient;
- 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
}
}
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;
디버깅 팁
-
트레이싱 활성화:
adk_telemetry::init_telemetry(); -
이벤트 검사:
while let Some(event) = stream.next().await { eprintln!("Event: {:?}", event); } -
RUST_LOG 사용:
RUST_LOG=debug cargo run --example myexample
이전: ← Access Control
질문이 있으신가요? GitHub에 이슈를 열어주세요.