開発ガイドライン

このドキュメントは、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

clippy の警告は抑制するのではなく、対処してください。抑制が必要な場合は、その理由を記述してください:

#[allow(clippy::too_many_arguments)]
// Builder pattern requires many parameters; refactoring would hurt usability
fn complex_builder(...) { }

エラーハンドリング

構造化エンベロープのエラー

AdkError は、component (where)、category (what kind)、code (machine key)、message (human text)、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 の境界ではなく、実際の起点となるサブシステム):

コンポーネント使用する場合
Agentエージェントのオーケストレーション、サブエージェントのディスパッチ
ModelLLM プロバイダー呼び出し、レスポンスのパース
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),
});

リトライのヒント

再試行可能なカテゴリ(RateLimitedUnavailableTimeout)は自動的に 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 Impl を使った crate ローカルのエラー

ドメイン固有のエラーを持つ crate は 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 Impl は不可

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")

ライブラリコードで panic しない

ライブラリ crate は、回復可能なエラーで決して panic してはいけません。unwrap()expect()panic!()src/ コードで避けてください(テストコードは例外です)。

RwLock / Mutex: unwrap() の代わりに段階的な劣化を使ってください。poisoned なロックは別スレッドが panic したことを意味します。現在のスレッドまでクラッシュさせると、さらに悪化します。

// 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 })
}

ビルダーメソッド: Arc::get_mut() に対しては expect() ではなく if let Some を使用してください:

// 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 を使う

すべての async コードは 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 の機能に関する規約

ライブラリ crate(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"] }

ライブラリ crate で features = ["full"] を決して使わないでください。これにより、下流の利用者は必要かどうかに関係なく、すべての tokio サブシステムをコンパイルすることになります。

非同期トレイト

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)
}

スレッドセーフティ

すべての公開型は 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 にあり、2つのプロファイルがあります:

  • 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 ファイル

各 crate には次の内容を含む 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. エージェントに追加する:
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 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.tomlfeature flag を追加する:
[features]
mymodel = ["dep:mymodel-sdk"]
  1. 条件付きでエクスポートする:
#[cfg(feature = "mymodel")]
pub mod mymodel;
#[cfg(feature = "mymodel")]
pub use mymodel::MyModelClient;

新しいエージェント型の追加

  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

前へ: ← アクセス制御

質問がありますか? GitHub で issue を開いてください。

開発ガイドライン - ADK-Rust ドキュメント | ADK-Rust