Tipos Fundamentales

Tipos y traits fundamentales de adk-core que forman la base de adk-rust.

Content y Part

Cada mensaje en adk fluye a través de Content y Part. Comprender estos tipos es esencial para trabajar con agents, tools y callbacks.

Content

Content representa un único mensaje en una conversación. Tiene un role (quién lo envió) y una o más parts (el contenido real).

Roles:

  • "user" - Mensajes del usuario
  • "model" - Respuestas del model de IA
  • "tool" - Resultados de la ejecución de la tool
use adk_core::Content;

// Mensaje de texto simple del usuario
let user_msg = Content::new("user")
    .with_text("What's the weather like?");

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

// Múltiples partes de texto en un mensaje
let detailed_msg = Content::new("user")
    .with_text("Here's my question:")
    .with_text("What is the capital of France?");

Content Multimodal:

Content puede incluir imágenes, audio, PDFs y otros datos binarios junto con texto:

// Imagen desde bytes (ej., leída de un archivo)
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);

// Imagen desde URL (el model la obtiene)
let content = Content::new("user")
    .with_text("Describe this image")
    .with_file_uri("image/png", "https://example.com/chart.png");

// Documento PDF
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 es un enum que representa diferentes tipos de contenido dentro de un mensaje:

pub enum Part {
    // Texto plano
    Text { text: String },
    
    // Datos binarios incrustados en el mensaje
    InlineData { mime_type: String, data: Vec<u8> },
    
    // Referencia a archivo externo (URL o almacenamiento en la nube)
    FileData { mime_type: String, file_uri: String },
    
    // Model solicitando una llamada a tool
    FunctionCall { name: String, args: Value, id: Option<String> },
    
    // Resultado de la ejecución de una tool
    FunctionResponse { function_response: FunctionResponseData, id: Option<String> },
}

Creando Parts Directamente:

use adk_core::Part;

// Parte de texto
let text = Part::text_part("Hello, world!");

// Imagen desde bytes
let image = Part::inline_data("image/png", png_bytes);

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

Inspeccionando Parts:

// Obtiene el contenido de texto (devuelve None para partes que no son de texto)
if let Some(text) = part.text() {
    println!("Texto: {}", text);
}

// Obtiene el tipo MIME (para InlineData y FileData)
if let Some(mime) = part.mime_type() {
    println!("Tipo MIME: {}", mime);
}

// Obtiene el file URI (solo para FileData)
if let Some(uri) = part.file_uri() {
    println!("File URI: {}", uri);
}

// Comprueba si la parte contiene medios (imagen, audio, video, etc.)
if part.is_media() {
    println!("Esta parte contiene medios binarios");
}

Iterando Sobre Parts:

for part in &content.parts {
    match part {
        Part::Text { text } => println!("Texto: {}", text),
        Part::InlineData { mime_type, data } => {
            println!("Datos binarios: {} ({} bytes)", mime_type, data.len());
        }
        Part::FileData { mime_type, file_uri } => {
            println!("Archivo: {} en {}", mime_type, file_uri);
        }
        Part::FunctionCall { name, args, .. } => {
            println!("Llamada a tool: {}({})", name, args);
        }
        Part::FunctionResponse { function_response, .. } => {
            println!("Resultado de la tool: {} -> {}", 
                function_response.name, 
                function_response.response
            );
        }
    }
}

Rasgo Agent

El rasgo Agent es la abstracción central para todos los Agents en ADK. Cada tipo de Agent —LlmAgent, Agents de flujo de trabajo, Agents personalizados— implementa este rasgo.

#[async_trait]
pub trait Agent: Send + Sync {
    /// Identificador único para este Agent
    fn name(&self) -> &str;
    
    /// Descripción legible por humanos de lo que hace este Agent
    fn description(&self) -> &str;
    
    /// Agents hijo (para Agents de flujo de trabajo como SequentialAgent, ParallelAgent)
    fn sub_agents(&self) -> &[Arc<dyn Agent>];
    
    /// Ejecuta el Agent y devuelve un stream de eventos
    async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<EventStream>;
}

Puntos Clave:

  • name(): Se usa para logging, transferencias e identificación. Debe ser único dentro de un sistema multi-Agent.
  • description(): Se muestra a los LLMs cuando el Agent se usa como Tool o para decisiones de routing.
  • sub_agents(): Devuelve Agents hijo. Vacío para Agents hoja (LlmAgent), populado para contenedores (SequentialAgent, ParallelAgent).
  • run(): El método de ejecución principal. Recibe el contexto y devuelve un stream de eventos.

¿Por qué EventStream?

Los Agents devuelven EventStream (un stream de Result<Event>) en lugar de una única respuesta porque:

  1. Streaming: Las respuestas se pueden transmitir token a token para una mejor UX
  2. Tool calls: Múltiples tool calls y respuestas ocurren durante la ejecución
  3. Cambios de estado: Las actualizaciones de estado se emiten como eventos
  4. Transferencias: Las transferencias de Agent se señalan a través de eventos

Rasgo Tool

Las Tools extienden las capacidades del Agent más allá de la conversación. Permiten a los Agents interactuar con APIs, bases de datos, sistemas de archivos o realizar cálculos.

#[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>;
}

Puntos Clave:

  • name(): El nombre de la función que el modelo utiliza para llamar a esta Tool. Manténgalo corto y descriptivo (ej., get_weather, search_database).
  • description(): Fundamental para que el modelo entienda cuándo usar la Tool. Sea específico sobre lo que hace y cuándo usarla.
  • parameters_schema(): JSON Schema que le dice al modelo qué argumentos debe proporcionar. Sin esto, el modelo adivina.
  • execute(): Recibe los argumentos parseados como serde_json::Value. Devuelve el resultado como JSON.

Implementando una Tool Personalizada:

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

Tools de Larga Duración:

Para operaciones que requieren mucho tiempo (procesamiento de archivos, llamadas a API externas), marque la Tool como de larga duración:

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

Las Tools de larga duración devuelven un ID de tarea inmediatamente. Se le indica al modelo que no vuelva a llamar a la Tool mientras esté pendiente.


Rasgo Toolset

Toolset proporciona tools dinámicamente según el contexto. Úsalo cuando:

  • Las tools dependen de los permisos de usuario
  • Las tools se cargan desde fuentes externas (servidores MCP)
  • La disponibilidad de las tools cambia durante la ejecución
#[async_trait]
pub trait Toolset: Send + Sync {
    /// Identificador del Toolset
    fn name(&self) -> &str;
    
    /// Devuelve las tools disponibles para el contexto actual
    async fn tools(&self, ctx: Arc<dyn ReadonlyContext>) -> Result<Vec<Arc<dyn Tool>>>;
}

Ejemplo: Toolset Basado en Permisos:

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

Rasgos de Contexto

Los contextos proporcionan información y servicios a los agents y tools durante la ejecución. Existe una jerarquía de rasgos de contexto, cada uno añadiendo más capacidades.

ReadonlyContext

Información básica disponible en todas partes:

pub trait ReadonlyContext: Send + Sync {
    /// ID único para esta invocation
    fn invocation_id(&self) -> &str;
    
    /// Nombre del agent actualmente en ejecución
    fn agent_name(&self) -> &str;
    
    /// Identificador de usuario
    fn user_id(&self) -> &str;
    
    /// Nombre de la aplicación
    fn app_name(&self) -> &str;
    
    /// Identificador de Session
    fn session_id(&self) -> &str;
    
    /// La entrada del usuario que activó esta invocation
    fn user_content(&self) -> &Content;
}

CallbackContext

Añade acceso a los artefactos (extiende ReadonlyContext):

pub trait CallbackContext: ReadonlyContext {
    /// Acceso al almacenamiento de artefactos (si está configurado)
    fn artifacts(&self) -> Option<Arc<dyn Artifacts>>;
}

ToolContext

Para la ejecución de tools (extiende CallbackContext):

pub trait ToolContext: CallbackContext {
    /// ID de la llamada a función que activó esta tool
    fn function_call_id(&self) -> &str;
    
    /// Obtiene las acciones de evento actuales (transfer, escalate, etc.)
    fn actions(&self) -> EventActions;
    
    /// Establece las acciones de evento (p. ej., activar un transfer)
    fn set_actions(&self, actions: EventActions);
    
    /// Busca en la memoria a largo plazo
    async fn search_memory(&self, query: &str) -> Result<Vec<MemoryEntry>>;
}

InvocationContext

Contexto completo para la ejecución del agent (extiende CallbackContext):

pub trait InvocationContext: CallbackContext {
    /// El agent que se está ejecutando
    fn agent(&self) -> Arc<dyn Agent>;
    
    /// Servicio de Memory (si está configurado)
    fn memory(&self) -> Option<Arc<dyn Memory>>;
    
    /// Session actual con estado e historial
    fn session(&self) -> &dyn Session;
    
    /// Configuración de ejecución
    fn run_config(&self) -> &RunConfig;
    
    /// Indica que esta invocation debe terminar
    fn end_invocation(&self);
    
    /// Verifica si la invocation ha terminado
    fn ended(&self) -> bool;
}

Sesión y Estado

Las sesiones rastrean conversaciones. El estado almacena datos dentro de las sesiones.

Sesión

pub trait Session: Send + Sync {
    /// Identificador único de la sesión
    fn id(&self) -> &str;
    
    /// Aplicación a la que pertenece esta sesión
    fn app_name(&self) -> &str;
    
    /// Usuario propietario de esta sesión
    fn user_id(&self) -> &str;
    
    /// Almacenamiento de estado mutable
    fn state(&self) -> &dyn State;
    
    /// Mensajes anteriores en esta conversación
    fn conversation_history(&self) -> Vec<Content>;
}

Estado

Almacenamiento clave-valor con prefijos de alcance:

pub trait State: Send + Sync {
    /// Obtener un valor por clave
    fn get(&self, key: &str) -> Option<Value>;
    
    /// Establecer un valor
    fn set(&mut self, key: String, value: Value);
    
    /// Obtener todos los pares clave-valor
    fn all(&self) -> HashMap<String, Value>;
}

Prefijos de estado:

Las claves de State usan prefijos para controlar el alcance y la persistencia:

PrefijoAlcancePersistenciaCaso de uso
user:Nivel de usuarioEn todas las sesionesPreferencias de usuario, configuraciones
app:Nivel de aplicaciónEn toda la aplicaciónConfiguración compartida
temp:Nivel de turnoSe borra en cada turnoDatos de cómputo temporales
(none)Nivel de sesiónSolo esta sesiónContexto de conversación
// En un callback o Tool
let state = ctx.session().state();

// Preferencia de usuario (persiste en todas las sesiones)
state.set("user:theme".into(), json!("dark"));

// Datos específicos de la sesión
state.set("current_topic".into(), json!("weather"));

// Datos temporales (borrados después de este turno)
state.set("temp:step_count".into(), json!(1));

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

Manejo de errores

ADK utiliza un tipo de error unificado para todas las operaciones:

pub enum AdkError {
    Agent(String),      // Errores de ejecución de Agent
    Tool(String),       // Errores de ejecución de Tool
    Model(String),      // Errores de API de LLM
    Session(String),    // Errores de almacenamiento de Session
    Artifact(String),   // Errores de almacenamiento de Artifact
    Config(String),     // Errores de configuración
    Io(std::io::Error), // Errores de E/S de archivo/red
    Json(serde_json::Error), // Errores de análisis de JSON
}

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

Manejo de errores en 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" }))
}

EventStream

Los Agents devuelven un flujo de eventos en lugar de una única respuesta:

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

Procesando eventos:

use futures::StreamExt;

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

while let Some(result) = stream.next().await {
    match result {
        Ok(event) => {
            // Verificar contenido de texto
            if let Some(content) = event.content() {
                for part in &content.parts {
                    if let Some(text) = part.text() {
                        print!("{}", text);
                    }
                }
            }
            
            // Verificar cambios de estado
            for (key, value) in event.state_delta() {
                println!("State changed: {} = {}", key, value);
            }
            
            // Verificar si esta es la respuesta final
            if event.is_final_response() {
                println!("\n[Done]");
            }
        }
        Err(e) => {
            eprintln!("Error: {}", e);
            break;
        }
    }
}

Rasgo Llm

El rasgo que implementan todos los proveedores de LLM:

#[async_trait]
pub trait Llm: Send + Sync {
    /// Identificador del modelo (ej., "gemini-2.0-flash", "gpt-4o")
    fn name(&self) -> &str;
    
    /// Generar contenido (en streaming o no en streaming)
    async fn generate_content(
        &self,
        request: LlmRequest,
        stream: bool,
    ) -> Result<LlmResponseStream>;
}

LlmRequest:

pub struct LlmRequest {
    pub contents: Vec<Content>,           // Historial de conversación
    pub tools: Vec<ToolDeclaration>,      // Herramientas disponibles
    pub system_instruction: Option<String>, // Prompt del sistema
    pub config: GenerateContentConfig,    // Temperatura, max_tokens, etc.
}

LlmResponse:

pub struct LlmResponse {
    pub content: Option<Content>,         // Contenido generado
    pub finish_reason: Option<FinishReason>, // Razón por la que la generación se detuvo
    pub usage: Option<UsageMetadata>,     // Recuentos de tokens
    pub partial: bool,                    // ¿Es este un fragmento de streaming?
    pub turn_complete: bool,              // ¿Ha terminado el turno?
}

Todos los proveedores (Gemini, OpenAI, Anthropic, Ollama, etc.) implementan este rasgo, haciéndolos intercambiables:

// Cambiar de proveedor cambiando una línea
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()?;

Anterior: ← Introducción | Siguiente: Runner →