Kerntypen

Grundlegende Typen und Traits aus adk-core, die das Fundament von ADK-Rust bilden.

Content und Part

Jede Nachricht in ADK fließt durch Content und Part. Das Verständnis dieser Typen ist essenziell für die Arbeit mit Agenten, Tools und Callbacks.

Content

Content repräsentiert eine einzelne Nachricht in einer Konversation. Es hat eine role (wer sie gesendet hat) und einen oder mehrere parts (den eigentlichen Inhalt).

Rollen:

  • "user" - Nachrichten vom Benutzer
  • "model" - Antworten vom AI model
  • "tool" - Ergebnisse der Tool-Ausführung
use adk_core::Content;

// Simple text message from user
let user_msg = Content::new("user")
    .with_text("What's the weather like?");

// Model response
let model_msg = Content::new("model")
    .with_text("The weather is sunny and 72°F.");

// Multiple text parts in one message
let detailed_msg = Content::new("user")
    .with_text("Here's my question:")
    .with_text("What is the capital of France?");

Multimodaler Content:

Content kann Bilder, Audio, PDFs und andere Binärdaten neben Text enthalten:

// Image from bytes (e.g., read from file)
let image_bytes = std::fs::read("photo.jpg")?;
let content = Content::new("user")
    .with_text("What's in this image?")
    .with_inline_data("image/jpeg", image_bytes);

// Image from URL (model fetches it)
let content = Content::new("user")
    .with_text("Describe this image")
    .with_file_uri("image/png", "https://example.com/chart.png");

// PDF document
let pdf_bytes = std::fs::read("report.pdf")?;
let content = Content::new("user")
    .with_text("Summarize this document")
    .with_inline_data("application/pdf", pdf_bytes);

Part

Part ist ein enum, das verschiedene Arten von Inhalt innerhalb einer Nachricht repräsentiert:

pub enum Part {
    // Klartext
    Text { text: String },
    
    // Binärdaten, die in die Nachricht eingebettet sind
    InlineData { mime_type: String, data: Vec<u8> },
    
    // Referenz auf externe Datei (URL oder Cloud-Speicher)
    FileData { mime_type: String, file_uri: String },
    
    // Model fordert einen Tool-Aufruf an
    FunctionCall { name: String, args: Value, id: Option<String> },
    
    // Ergebnis einer Tool-Ausführung
    FunctionResponse { function_response: FunctionResponseData, id: Option<String> },
}

Parts direkt erstellen:

use adk_core::Part;

// Text-Part
let text = Part::text_part("Hello, world!");

// Bild aus Bytes
let image = Part::inline_data("image/png", png_bytes);

// Bild von URL
let remote_image = Part::file_data("image/jpeg", "https://example.com/photo.jpg");

Parts inspizieren:

// Textinhalt abrufen (gibt None für Nicht-Text-Parts zurück)
if let Some(text) = part.text() {
    println!("Text: {}", text);
}

// MIME-Typ abrufen (für InlineData und FileData)
if let Some(mime) = part.mime_type() {
    println!("MIME type: {}", mime);
}

// file URI abrufen (nur für FileData)
if let Some(uri) = part.file_uri() {
    println!("File URI: {}", uri);
}

// Prüfen, ob der Part Medien enthält (Bild, Audio, Video, etc.)
if part.is_media() {
    println!("This part contains binary media");
}

Über Parts iterieren:

for part in &content.parts {
    match part {
        Part::Text { text } => println!("Text: {}", text),
        Part::InlineData { mime_type, data } => {
            println!("Binary data: {} ({} bytes)", mime_type, data.len());
        }
        Part::FileData { mime_type, file_uri } => {
            println!("File: {} at {}", mime_type, file_uri);
        }
        Part::FunctionCall { name, args, .. } => {
            println!("Tool call: {}({})", name, args);
        }
        Part::FunctionResponse { function_response, .. } => {
            println!("Tool result: {} -> {}", 
                function_response.name, 
                function_response.response
            );
        }
    }
}

Agent Trait

Das Agent trait ist die zentrale Abstraktion für alle Agents im ADK. Jeder Agent-Typ – LLM Agents, Workflow Agents, benutzerdefinierte Agents – implementiert dieses trait.

#[async_trait]
pub trait Agent: Send + Sync {
    /// Unique identifier for this agent
    fn name(&self) -> &str;
    
    /// Human-readable description of what this agent does
    fn description(&self) -> &str;
    
    /// Child agents (for workflow agents like Sequential, Parallel)
    fn sub_agents(&self) -> &[Arc<dyn Agent>];
    
    /// Execute the agent and return a stream of events
    async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<EventStream>;
}

Wichtige Punkte:

  • name(): Wird für Logging, Transfers und Identifikation verwendet. Muss innerhalb eines Multi-Agenten-Systems eindeutig sein.
  • description(): Wird LLMs gezeigt, wenn der Agent als Tool oder für Routing-Entscheidungen verwendet wird.
  • sub_agents(): Gibt untergeordnete Agents zurück. Leer für Leaf Agents (LlmAgent), befüllt für Container (SequentialAgent, ParallelAgent).
  • run(): Die Hauptausführungsmethode. Empfängt Kontext und gibt einen EventStream zurück.

Warum EventStream?

Agents geben einen EventStream (einen Stream von Result<Event>) zurück, anstatt einer einzelnen Antwort, weil:

  1. Streaming: Antworten können Token für Token gestreamt werden für eine bessere UX.
  2. Tool calls: Mehrere Tool calls und Antworten erfolgen während der Ausführung.
  3. Zustandsänderungen: Zustandsaktualisierungen werden als Events ausgegeben.
  4. Transfers: Agent transfers werden durch Events signalisiert.

Tool Trait

Tools erweitern die Fähigkeiten von Agenten über die Konversation hinaus. Sie ermöglichen es Agenten, mit APIs, Datenbanken, Dateisystemen zu interagieren oder Berechnungen durchzuführen.

#[async_trait]
pub trait Tool: Send + Sync {
    /// Tool name (used in function calls from the model)
    fn name(&self) -> &str;
    
    /// Description shown to the LLM to help it decide when to use this tool
    fn description(&self) -> &str;
    
    /// JSON Schema defining the expected parameters
    fn parameters_schema(&self) -> Option<Value> { None }
    
    /// JSON Schema defining the response format
    fn response_schema(&self) -> Option<Value> { None }
    
    /// Whether this tool runs asynchronously (returns task ID immediately)
    fn is_long_running(&self) -> bool { false }
    
    /// Execute the tool with given arguments
    async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value>;
}

Wichtige Punkte:

  • name(): Der Funktionsname, den das Modell verwendet, um dieses Tool aufzurufen. Halten Sie ihn kurz und deskriptiv (z.B. get_weather, search_database).
  • description(): Entscheidend, damit das Modell versteht, wann das Tool verwendet werden soll. Seien Sie spezifisch bezüglich dessen, was es tut und wann es verwendet werden soll.
  • parameters_schema(): JSON Schema, das dem Modell mitteilt, welche Argumente bereitzustellen sind. Ohne dies rät das Modell.
  • execute(): Empfängt geparste Argumente als serde_json::Value. Geben Sie das Ergebnis als JSON zurück.

Implementierung eines benutzerdefinierten Tools:

use adk_core::{Tool, ToolContext, Result};
use async_trait::async_trait;
use serde_json::{json, Value};
use std::sync::Arc;

struct WeatherTool {
    api_key: String,
}

#[async_trait]
impl Tool for WeatherTool {
    fn name(&self) -> &str { 
        "get_weather" 
    }
    
    fn description(&self) -> &str { 
        "Get current weather for a city. Use this when the user asks about weather conditions."
    }
    
    fn parameters_schema(&self) -> Option<Value> {
        Some(json!({
            "type": "object",
            "properties": {
                "city": { 
                    "type": "string",
                    "description": "City name (e.g., 'London', 'New York')"
                },
                "units": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Temperature units"
                }
            },
            "required": ["city"]
        }))
    }
    
    async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
        let city = args["city"].as_str().unwrap_or("Unknown");
        let units = args["units"].as_str().unwrap_or("celsius");
        
        // Call weather API here...
        
        Ok(json!({
            "city": city,
            "temperature": 22,
            "units": units,
            "condition": "sunny"
        }))
    }
}

Langlaufende Tools:

Für Operationen, die lange dauern (Dateiverarbeitung, externe API-Aufrufe), kennzeichnen Sie das Tool als langlaufend:

fn is_long_running(&self) -> bool { true }

Langlaufende Tools geben sofort eine Aufgaben-ID zurück. Dem Modell wird mitgeteilt, das Tool nicht erneut aufzurufen, solange es ausstehend ist.


Toolset Trait

Toolset stellt Tools dynamisch basierend auf dem Kontext bereit. Verwenden Sie dies, wenn:

  • Tools von Benutzerberechtigungen abhängen
  • Tools von externen Quellen (MCP servers) geladen werden
  • die Verfügbarkeit von Tools während der Ausführung variiert
#[async_trait]
pub trait Toolset: Send + Sync {
    /// Toolset-Bezeichner
    fn name(&self) -> &str;
    
    /// Gibt verfügbare Tools für den aktuellen Kontext zurück
    async fn tools(&self, ctx: Arc<dyn ReadonlyContext>) -> Result<Vec<Arc<dyn Tool>>>;
}

Beispiel: Berechtigungsbasiertes Toolset:

struct AdminToolset {
    admin_tools: Vec<Arc<dyn Tool>>,
    user_tools: Vec<Arc<dyn Tool>>,
}

#[async_trait]
impl Toolset for AdminToolset {
    fn name(&self) -> &str { "admin_toolset" }
    
    async fn tools(&self, ctx: Arc<dyn ReadonlyContext>) -> Result<Vec<Arc<dyn Tool>>> {
        if ctx.user_id().starts_with("admin_") {
            Ok(self.admin_tools.clone())
        } else {
            Ok(self.user_tools.clone())
        }
    }
}

Kontext-Traits

Kontexte stellen Informationen und Dienste für Agents und Tools während der Ausführung bereit. Es gibt eine Hierarchie von Kontext-Traits, wobei jeder weitere Fähigkeiten hinzufügt.

ReadonlyContext

Grundlegende Informationen, die überall verfügbar sind:

pub trait ReadonlyContext: Send + Sync {
    /// Eindeutige ID für diese Invocation
    fn invocation_id(&self) -> &str;
    
    /// Name des aktuell ausführenden Agent
    fn agent_name(&self) -> &str;
    
    /// Benutzer-Bezeichner
    fn user_id(&self) -> &str;
    
    /// Anwendungsname
    fn app_name(&self) -> &str;
    
    /// Session-Bezeichner
    fn session_id(&self) -> &str;
    
    /// Die Benutzereingabe, die diese Invocation ausgelöst hat
    fn user_content(&self) -> &Content;
}

CallbackContext

Fügt Zugriff auf Artefakte hinzu (erweitert ReadonlyContext):

pub trait CallbackContext: ReadonlyContext {
    /// Zugriff auf Artefakt-Speicher (falls konfiguriert)
    fn artifacts(&self) -> Option<Arc<dyn Artifacts>>;
}

ToolContext

Für die Tool-Ausführung (erweitert CallbackContext):

pub trait ToolContext: CallbackContext {
    /// ID des Funktionsaufrufs, der dieses Tool ausgelöst hat
    fn function_call_id(&self) -> &str;
    
    /// Holt aktuelle Ereignisaktionen (transfer, escalate, etc.)
    fn actions(&self) -> EventActions;
    
    /// Setzt Ereignisaktionen (z.B. Auslösen eines transfers)
    fn set_actions(&self, actions: EventActions);
    
    /// Langzeit-Memory durchsuchen
    async fn search_memory(&self, query: &str) -> Result<Vec<MemoryEntry>>;
}

InvocationContext

Vollständiger Kontext für die Agent-Ausführung (erweitert CallbackContext):

pub trait InvocationContext: CallbackContext {
    /// Der aktuell ausgeführte Agent
    fn agent(&self) -> Arc<dyn Agent>;
    
    /// Memory-Dienst (falls konfiguriert)
    fn memory(&self) -> Option<Arc<dyn Memory>>;
    
    /// Aktuelle Session mit Status und Historie
    fn session(&self) -> &dyn Session;
    
    /// Ausführungskonfiguration
    fn run_config(&self) -> &RunConfig;
    
    /// Signalisiert, dass diese Invocation beendet werden soll
    fn end_invocation(&self);
    
    /// Prüft, ob die Invocation beendet wurde
    fn ended(&self) -> bool;
}

Sitzung und Zustand

Sitzungen verfolgen Konversationen. Zustand speichert Daten innerhalb von Sitzungen.

Sitzung

pub trait Session: Send + Sync {
    /// Eindeutiger Sitzungsbezeichner
    fn id(&self) -> &str;
    
    /// Anwendung, zu der diese Sitzung gehört
    fn app_name(&self) -> &str;
    
    /// Benutzer, dem diese Sitzung gehört
    fn user_id(&self) -> &str;
    
    /// Veränderlicher Zustandspeicher
    fn state(&self) -> &dyn State;
    
    /// Vorherige Nachrichten in dieser Konversation
    fn conversation_history(&self) -> Vec<Content>;
}

Zustand

Schlüssel-Wert-Speicher mit bereichsbezogenen Präfixen:

pub trait State: Send + Sync {
    /// Wert nach Schlüssel abrufen
    fn get(&self, key: &str) -> Option<Value>;
    
    /// Wert setzen
    fn set(&mut self, key: String, value: Value);
    
    /// Alle Schlüssel-Wert-Paare abrufen
    fn all(&self) -> HashMap<String, Value>;
}

Zustandspräfixe:

Zustandsschlüssel verwenden Präfixe, um den Bereich und die Persistenz zu steuern:

PräfixBereichPersistenzAnwendungsfall
user:Benutzer-EbeneSitzungsübergreifendBenutzereinstellungen, Präferenzen
app:Anwendungs-EbeneAnwendungsweitGemeinsame Konfiguration
temp:Zug-EbeneWird bei jedem Zug gelöschtTemporäre Berechnungsdaten
(none)Sitzungs-EbeneNur diese SitzungKonversationskontext
// In einem Callback oder Tool
let state = ctx.session().state();

// Benutzereinstellung (bleibt sitzungsübergreifend bestehen)
state.set("user:theme".into(), json!("dark"));

// Sitzungsspezifische Daten
state.set("current_topic".into(), json!("weather"));

// Temporäre Daten (nach diesem Zug gelöscht)
state.set("temp:step_count".into(), json!(1));

// Werte lesen
if let Some(theme) = state.get("user:theme") {
    println!("Theme: {}", theme);
}

Fehlerbehandlung

ADK verwendet einen vereinheitlichten Fehlertyp für alle Operationen:

pub enum AdkError {
    Agent(String),      // Fehler bei der Agenten-Ausführung
    Tool(String),       // Fehler bei der Tool-Ausführung
    Model(String),      // LLM API-Fehler
    Session(String),    // Fehler bei der Sitzungsspeicherung
    Artifact(String),   // Fehler bei der Artefaktspeicherung
    Config(String),     // Konfigurationsfehler
    Io(std::io::Error), // Datei-/Netzwerk-I/O-Fehler
    Json(serde_json::Error), // JSON-Analysefehler
}

pub type Result<T> = std::result::Result<T, AdkError>;

Fehlerbehandlung in Tools:

async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
    let city = args["city"]
        .as_str()
        .ok_or_else(|| AdkError::Tool("Missing 'city' parameter".into()))?;
    
    let response = reqwest::get(&format!("https://api.weather.com/{}", city))
        .await
        .map_err(|e| AdkError::Tool(format!("API error: {}", e)))?;
    
    Ok(json!({ "weather": "sunny" }))
}

Ereignis-Stream

Agents geben einen Stream von Ereignissen zurück, anstatt einer einzelnen Antwort:

pub type EventStream = Pin<Box<dyn Stream<Item = Result<Event>> + Send>>;

Ereignisse verarbeiten:

use futures::StreamExt;

let mut stream = agent.run(ctx).await?;

while let Some(result) = stream.next().await {
    match result {
        Ok(event) => {
            // Nach Textinhalt suchen
            if let Some(content) = event.content() {
                for part in &content.parts {
                    if let Some(text) = part.text() {
                        print!("{}", text);
                    }
                }
            }
            
            // Nach Zustandsänderungen suchen
            for (key, value) in event.state_delta() {
                println!("State changed: {} = {}", key, value);
            }
            
            // Prüfen, ob dies die endgültige Antwort ist
            if event.is_final_response() {
                println!("\n[Done]");
            }
        }
        Err(e) => {
            eprintln!("Error: {}", e);
            break;
        }
    }
}

Llm Trait

Das Trait, das alle LLM-Anbieter implementieren:

#[async_trait]
pub trait Llm: Send + Sync {
    /// Modell-Identifikator (z.B. "gemini-2.0-flash", "gpt-4o")
    fn name(&self) -> &str;
    
    /// Inhalt generieren (Streaming oder Nicht-Streaming)
    async fn generate_content(
        &self,
        request: LlmRequest,
        stream: bool,
    ) -> Result<LlmResponseStream>;
}

LlmRequest:

pub struct LlmRequest {
    pub contents: Vec<Content>,           // Konversationsverlauf
    pub tools: Vec<ToolDeclaration>,      // Verfügbare Tools
    pub system_instruction: Option<String>, // System-Prompt
    pub config: GenerateContentConfig,    // Temperatur, max_tokens, etc.
}

LlmResponse:

pub struct LlmResponse {
    pub content: Option<Content>,         // Generierter Inhalt
    pub finish_reason: Option<FinishReason>, // Warum die Generierung gestoppt wurde
    pub usage: Option<UsageMetadata>,     // Token-Zählungen
    pub partial: bool,                    // Ist dies ein Streaming-Chunk?
    pub turn_complete: bool,              // Ist der Turn beendet?
}

Alle Anbieter (Gemini, OpenAI, Anthropic, Ollama, etc.) implementieren dieses Trait, wodurch sie austauschbar sind:

// Anbieter durch Ändern einer Zeile wechseln
let model: Arc<dyn Llm> = Arc::new(GeminiModel::new(&key, "gemini-2.0-flash")?);
// let model: Arc<dyn Llm> = Arc::new(OpenAIClient::new(config)?);
// let model: Arc<dyn Llm> = Arc::new(AnthropicClient::new(config)?);

let agent = LlmAgentBuilder::new("assistant")
    .model(model)
    .build()?;

Zurück: ← Einführung | Weiter: Runner →