إرشادات التطوير

يوفر هذا المستند إرشادات شاملة للمطورين الذين يساهمون في ADK-Rust. يضمن اتباع هذه المعايير جودة الشيفرة، والاتساق، وقابلية الصيانة عبر المشروع.

جدول المحتويات

البدء

المتطلبات الأساسية

  • Rust: ‏1.95.0 أو أحدث (الإصدار 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 منظَّم كمساحة عمل Cargo مع عدة crates:

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

تبعيات crates

يجب نشر crates بترتيب التبعيات:

  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 (شامل)

نمط الشيفرة

المبادئ العامة

  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 هو نوع خطأ مهيكل يتضمن المكوّن (أين)، والفئة (ما النوع)، والرمز (مفتاح آلي)، والرسالة (نص بشري)، وتلميح إعادة المحاولة، وتفاصيل اختيارية:

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

اختيار المكوّن والفئة

ErrorComponent يحدد أين حدث الفشل (النظام الفرعي الأصلي، وليس حد الواجهة الذي يظهر من خلاله):

المكوّنمتى يُستخدم
Agentتنسيق agent، إرسال sub-agent
Modelاستدعاءات مزوّد LLM، تحليل الاستجابة
Toolتنفيذ الأداة، التحقق من صحة المعلمات
Sessionاستمرارية الجلسة، إدارة الحالة
Memoryعمليات الذاكرة/RAG
Graphتنفيذ سير عمل الرسم البياني
Authالمصادقة، التفويض
ServerHTTP الخادم، الإعداد

ErrorCategory يُصنِّف ما الذي حدث بشكل خاطئ:

الفئةHTTPيُستخدم عند
InvalidInput400معاملات غير صحيحة، config، نص الطلب
Unauthorized401بيانات اعتماد مفقودة أو غير صالحة
Forbidden403بيانات اعتماد صالحة، لكن الأذونات غير كافية
NotFound404المورد غير موجود
RateLimited429حد المعدل في المصدر العلوي (يمكن إعادة المحاولة)
Timeout408تجاوزت العملية المهلة الزمنية (يمكن إعادة المحاولة)
Unavailable503تعطل الخدمة upstream (قابل لإعادة المحاولة)
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", ... } }

أخطاء محلية في الـ crate مع تطبيقات From

الـ crates ذات الأخطاء الخاصة بالمجال تطبّق 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 شاملة

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

لا توجد حالات Panic في شيفرة المكتبة

يجب ألا تقوم crates الخاصة بالمكتبة بعمل panic أبدًا عند الأخطاء القابلة للاسترداد. تجنّب unwrap() وexpect() وpanic!() في شيفرة src/ (شيفرة الاختبار مستثناة).

RwLock / Mutex: استخدم تدهورًا سلسًا بدلًا من unwrap(). القفل المسموم يعني أن خيطًا آخر قام بعمل 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 })
}

طرائق المنشئ: استخدم if let Some بدلًا من expect() لـ 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
}

أنماط Async

استخدم 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

يجب أن تعلن crates الخاصة بالمكتبة (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"] في crate خاص بمكتبة. هذا يفرض على جميع المستهلكين downstream أن يجمّعوا كل نظام فرعي من tokio سواء احتاجوا إليه أم لا.

السمات Async

استخدم async_trait لطرائق السمات async:

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 كل ملف ثنائي للاختبار في عملية منفصلة مع جدولة متوازية، ما يمنح تسريعًا يقارب 10x مقارنةً بـ cargo test في مساحة العمل هذه.

# 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 — التطوير المحلي (إيقاف عند أول خطأ، بلا إعادة محاولات)
  • 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");
}

اختبار المحاكاة

استخدم MockLlm للاختبار دون استدعاءات API:

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

  • العنوان: وصف واضح ومختصر للتغيير
  • الوصف: اشرح ماذا ولماذا (وليس كيف)
  • الحجم: حافظ على تركيز PRs؛ قسّم التغييرات الكبيرة
  • الاختبارات: أدرج اختبارات للوظائف الجديدة
  • التغييرات الهدّامة: وثّقها بوضوح في الوصف

رسائل الالتزام

اتبع 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

هيكلة المشروع الأولية

استخدم cargo adk new مع Composable Template System لتهيئة مشاريع جديدة. يوفّر السجل المدمج 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. فعّل التتبّع:

    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

السابق: ← التحكم في الوصول

أسئلة؟ افتح issue على GitHub.

إرشادات التطوير - وثائق ADK-Rust | ADK-Rust