# Callbacks Callbacks in ADK-Rust provide hooks to observe, customize, and control agent behavior at key execution points. They enable logging, guardrails, caching, response modification, and more. ## Overview ADK-Rust supports eight callback types that intercept different stages of agent execution: | Callback Type | When Executed | Use Cases | |--------------|---------------|-----------| | `before_agent` | Before agent starts processing | Input validation, logging, early termination | | `after_agent` | After agent completes | Response modification, logging, cleanup | | `before_model` | Before LLM call | Request modification, caching, rate limiting | | `after_model` | After LLM response | Response filtering, logging, caching | | `before_tool` | Before tool execution | Permission checks, parameter validation | | `after_tool` | After tool execution | Result modification, logging, `ToolOutcome` inspection | | `after_tool_full` | After tool execution (V2 rich) | Inspect/modify tool args and response directly | | `on_tool_error` | After tool failure (retries exhausted) | Fallback results, error recovery | ## Callback Types ### Agent Callbacks Agent callbacks wrap the entire agent execution cycle. ```rust use adk_rust::prelude::*; use std::sync::Arc; // BeforeAgentCallback type signature type BeforeAgentCallback = Box< dyn Fn(Arc) -> Pin>> + Send>> + Send + Sync >; // AfterAgentCallback type signature type AfterAgentCallback = Box< dyn Fn(Arc) -> Pin>> + Send>> + Send + Sync >; ``` ### Model Callbacks Model callbacks intercept LLM requests and responses. ```rust use adk_rust::prelude::*; use std::sync::Arc; // BeforeModelResult - controls what happens after the callback pub enum BeforeModelResult { Continue(LlmRequest), // Continue with (possibly modified) request Skip(LlmResponse), // Skip model call, use this response instead } // BeforeModelCallback - can modify request or skip model call type BeforeModelCallback = Box< dyn Fn(Arc, LlmRequest) -> Pin> + Send>> + Send + Sync >; // AfterModelCallback - can modify the response type AfterModelCallback = Box< dyn Fn(Arc, LlmResponse) -> Pin>> + Send>> + Send + Sync >; ``` ### Tool Callbacks Tool callbacks intercept tool execution. ```rust use adk_rust::prelude::*; use std::sync::Arc; // BeforeToolCallback - can skip tool by returning Some(Content) type BeforeToolCallback = Box< dyn Fn(Arc) -> Pin>> + Send>> + Send + Sync >; // AfterToolCallback - can modify tool result type AfterToolCallback = Box< dyn Fn(Arc) -> Pin>> + Send>> + Send + Sync >; // AfterToolCallbackFull (V2) - receives tool, args, and response type AfterToolCallbackFull = Box< dyn Fn( Arc, Arc, // the tool that was executed serde_json::Value, // args passed to the tool serde_json::Value, // tool response (success or error JSON) ) -> Pin>> + Send>> + Send + Sync >; ``` `AfterToolCallbackFull` is the V2 rich after-tool callback aligned with the Python/Go ADK model. Unlike `AfterToolCallback` (which only receives `CallbackContext`), it receives the tool reference, the arguments it was called with, and the response it produced. Return `Ok(None)` to keep the original response, or `Ok(Some(value))` to replace the function response sent to the LLM. ## Return Value Semantics Callbacks use different return values to control execution flow: ### Agent/Tool Callbacks | Return Value | Effect | |-------------|--------| | `Ok(None)` | Continue normal execution | | `Ok(Some(content))` | Override/skip with provided content | | `Err(e)` | Abort execution with error | ### Model Callbacks **BeforeModelCallback** uses `BeforeModelResult`: | Return Value | Effect | |-------------|--------| | `Ok(BeforeModelResult::Continue(request))` | Continue with the (possibly modified) request | | `Ok(BeforeModelResult::Skip(response))` | Skip model call, use this response instead | | `Err(e)` | Abort execution with error | **AfterModelCallback** uses `Option`: | Return Value | Effect | |-------------|--------| | `Ok(None)` | Keep the original response | | `Ok(Some(response))` | Replace with the modified response | | `Err(e)` | Abort execution with error | ### Summary - **Before agent/tool callbacks**: Return `None` to continue, `Some(content)` to skip - **Before model callback**: Return `Continue(request)` to proceed, `Skip(response)` to bypass the model - **After callbacks**: Return `None` to keep original, `Some(...)` to replace ## Adding Callbacks to Agents Callbacks are added to agents using the `LlmAgentBuilder`: ```rust use adk_rust::prelude::*; use std::sync::Arc; #[tokio::main] async fn main() -> std::result::Result<(), Box> { let api_key = std::env::var("GOOGLE_API_KEY")?; let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.5-flash")?); let agent = LlmAgentBuilder::new("my_agent") .model(model) .instruction("You are a helpful assistant.") // Add before_agent callback .before_callback(Box::new(|ctx| { Box::pin(async move { println!("Agent starting: {}", ctx.agent_name()); Ok(None) // Continue execution }) })) // Add after_agent callback .after_callback(Box::new(|ctx| { Box::pin(async move { println!("Agent completed: {}", ctx.agent_name()); Ok(None) // Keep original result }) })) .build()?; Ok(()) } ``` ## CallbackContext Interface The `CallbackContext` trait provides access to execution context: ```rust use adk_rust::prelude::*; #[async_trait] pub trait CallbackContext: ReadonlyContext { /// Access artifact storage (if configured) fn artifacts(&self) -> Option>; /// Structured metadata about the most recent tool execution. /// Available in after-tool callbacks. Returns `None` outside tool context. fn tool_outcome(&self) -> Option { None } /// Name of the tool being executed. /// Available in before-tool and after-tool callbacks via `ToolCallbackContext`. fn tool_name(&self) -> Option<&str> { None } /// Input arguments for the tool being executed. /// Available in before-tool and after-tool callbacks via `ToolCallbackContext`. fn tool_input(&self) -> Option<&serde_json::Value> { None } } // CallbackContext extends ReadonlyContext #[async_trait] pub trait ReadonlyContext: Send + Sync { /// Current invocation ID fn invocation_id(&self) -> &str; /// Name of the current agent fn agent_name(&self) -> &str; /// User ID from session fn user_id(&self) -> &str; /// Application name fn app_name(&self) -> &str; /// Session ID fn session_id(&self) -> &str; /// Current branch (for multi-agent) fn branch(&self) -> &str; /// The user's input content fn user_content(&self) -> &Content; } ``` ### ToolOutcome `ToolOutcome` carries structured metadata about a completed tool execution. It is available via `ctx.tool_outcome()` in after-tool callbacks: ```rust pub struct ToolOutcome { pub tool_name: String, pub tool_args: serde_json::Value, pub success: bool, pub duration: std::time::Duration, pub error_message: Option, pub attempt: u32, // 0-based retry attempt number } ``` The `success` field is derived from the Rust `Result` path, not from JSON content inspection. A tool that returns `Ok(json!({"error": "..."}))` will have `success: true`. ## Common Patterns ### Logging Callback Log all agent interactions: ```rust use adk_rust::prelude::*; use std::sync::Arc; let agent = LlmAgentBuilder::new("logged_agent") .model(model) .before_callback(Box::new(|ctx| { Box::pin(async move { println!("[LOG] Agent '{}' starting", ctx.agent_name()); println!("[LOG] Session: {}", ctx.session_id()); println!("[LOG] User: {}", ctx.user_id()); Ok(None) }) })) .after_callback(Box::new(|ctx| { Box::pin(async move { println!("[LOG] Agent '{}' completed", ctx.agent_name()); Ok(None) }) })) .build()?; ``` ### Input Guardrails Block inappropriate content before processing: ```rust use adk_rust::prelude::*; use std::sync::Arc; let agent = LlmAgentBuilder::new("guarded_agent") .model(model) .before_callback(Box::new(|ctx| { Box::pin(async move { // Check user input for blocked content let user_content = ctx.user_content(); for part in &user_content.parts { if let Part::Text { text } = part { if text.to_lowercase().contains("blocked_word") { // Return early with rejection message return Ok(Some(Content { role: "model".to_string(), parts: vec![Part::Text { text: "I cannot process that request.".to_string(), }], })); } } } Ok(None) // Continue normal execution }) })) .build()?; ``` ### Response Caching (Before Model) Cache LLM responses to reduce API calls: ```rust use adk_rust::prelude::*; use std::sync::Arc; use std::collections::HashMap; use std::sync::Mutex; // Simple in-memory cache let cache: Arc>> = Arc::new(Mutex::new(HashMap::new())); let cache_clone = cache.clone(); let agent = LlmAgentBuilder::new("cached_agent") .model(model) .before_model_callback(Box::new(move |ctx, request| { let cache = cache_clone.clone(); Box::pin(async move { // Create cache key from request contents let key = format!("{:?}", request.contents); // Check cache if let Some(cached) = cache.lock().unwrap().get(&key) { println!("[CACHE] Hit for request"); return Ok(BeforeModelResult::Skip(cached.clone())); } println!("[CACHE] Miss, calling model"); Ok(BeforeModelResult::Continue(request)) // Continue to model }) })) .build()?; ``` ### Injecting Multimodal Content (Before Model) Inject images or other binary content into LLM requests for multimodal analysis: ```rust use adk_rust::prelude::*; use adk_rust::artifact::{ArtifactService, LoadRequest}; use std::sync::Arc; // Artifact service with pre-loaded image let artifact_service: Arc = /* ... */; let callback_service = artifact_service.clone(); let agent = LlmAgentBuilder::new("image_analyst") .model(model) .instruction("Describe the image provided by the user.") .before_model_callback(Box::new(move |_ctx, mut request| { let service = callback_service.clone(); Box::pin(async move { // Load image from artifact storage if let Ok(response) = service.load(LoadRequest { app_name: "my_app".to_string(), user_id: "user".to_string(), session_id: "session".to_string(), file_name: "user:photo.png".to_string(), version: None, }).await { // Inject image into the user's message if let Some(last_content) = request.contents.last_mut() { if last_content.role == "user" { last_content.parts.push(response.part); } } } Ok(BeforeModelResult::Continue(request)) }) })) .build()?; ``` This pattern is essential for multimodal AI because tool responses are JSON text - the model can't "see" images returned by tools. By injecting the image directly into the request, the model receives actual image data. ### Response Modification (After Model) Modify or filter model responses: ```rust use adk_rust::prelude::*; use std::sync::Arc; let agent = LlmAgentBuilder::new("filtered_agent") .model(model) .after_model_callback(Box::new(|ctx, mut response| { Box::pin(async move { // Modify the response content if let Some(ref mut content) = response.content { for part in &mut content.parts { if let Part::Text { text } = part { // Add disclaimer to all responses *text = format!("{}\n\n[AI-generated response]", text); } } } Ok(Some(response)) }) })) .build()?; ``` ### Tool Permission Check (Before Tool) Validate tool execution permissions: ```rust use adk_rust::prelude::*; use std::sync::Arc; let agent = LlmAgentBuilder::new("permission_agent") .model(model) .tool(Arc::new(GoogleSearchTool::new())) .before_tool_callback(Box::new(|ctx| { Box::pin(async move { // Access tool name and input via ToolCallbackContext if let Some(name) = ctx.tool_name() { println!("About to execute tool: {}", name); } if let Some(input) = ctx.tool_input() { println!("Tool input: {}", input); } // Check if user has permission for tools let user_id = ctx.user_id(); // Example: block certain users from using tools if user_id == "restricted_user" { return Ok(Some(Content { role: "function".to_string(), parts: vec![Part::Text { text: "Tool access denied for this user.".to_string(), }], })); } Ok(None) // Allow tool execution }) })) .build()?; ``` ### Tool Result Logging (After Tool) Log all tool executions: ```rust use adk_rust::prelude::*; use std::sync::Arc; let agent = LlmAgentBuilder::new("tool_logged_agent") .model(model) .tool(Arc::new(GoogleSearchTool::new())) .after_tool_callback(Box::new(|ctx| { Box::pin(async move { println!("[TOOL LOG] Tool executed for agent: {}", ctx.agent_name()); println!("[TOOL LOG] Session: {}", ctx.session_id()); // Inspect structured outcome metadata if let Some(outcome) = ctx.tool_outcome() { println!( "[TOOL LOG] {} {} in {:?}", outcome.tool_name, if outcome.success { "OK" } else { "FAIL" }, outcome.duration, ); } Ok(None) // Keep original result }) })) .build()?; ``` ### Tool Error Fallback (On Tool Error) Provide a fallback result when a tool fails: ```rust use adk_rust::prelude::*; use serde_json::json; use std::sync::Arc; let agent = LlmAgentBuilder::new("fallback_agent") .model(model) .tool(Arc::new(my_tool)) .on_tool_error(Box::new(|_ctx, tool, _args, error| { Box::pin(async move { // Return a fallback result instead of the error if tool.name() == "weather_api" { Ok(Some(json!({ "error": "Weather service unavailable", "fallback": true }))) } else { Ok(None) // No fallback — propagate original error to LLM } }) })) .build()?; ``` The `on_tool_error` callback is invoked after retries are exhausted (if a retry budget is configured). Return `Some(value)` to substitute a fallback, or `None` to let the original error reach the LLM. ### Rich After-Tool Inspection (V2) Inspect and optionally modify tool results with full context: ```rust use adk_rust::prelude::*; use serde_json::json; use std::sync::Arc; let agent = LlmAgentBuilder::new("audited_agent") .model(model) .tool(Arc::new(my_tool)) .after_tool_callback_full(Box::new(|_ctx, tool, args, response| { Box::pin(async move { // Log the full tool execution details println!( "[AUDIT] Tool '{}' called with {} returned {}", tool.name(), args, response ); // Optionally modify the response if tool.name() == "sensitive_api" { // Redact sensitive fields before the LLM sees them let mut redacted = response.clone(); if let Some(obj) = redacted.as_object_mut() { obj.remove("secret_token"); } Ok(Some(redacted)) } else { Ok(None) // Keep original response } }) })) .build()?; ``` `after_tool_callback_full` runs after the legacy `after_tool_callback` chain. Both can coexist on the same agent. ## Multiple Callbacks You can add multiple callbacks of the same type. They execute in order: ```rust use adk_rust::prelude::*; use std::sync::Arc; let agent = LlmAgentBuilder::new("multi_callback_agent") .model(model) // First before callback - logging .before_callback(Box::new(|ctx| { Box::pin(async move { println!("[1] Logging callback"); Ok(None) }) })) // Second before callback - validation .before_callback(Box::new(|ctx| { Box::pin(async move { println!("[2] Validation callback"); Ok(None) }) })) .build()?; ``` When a callback returns `Some(content)`, subsequent callbacks of the same type are skipped. ## Error Handling Callbacks can return errors to abort execution: ```rust use adk_rust::prelude::*; use std::sync::Arc; let agent = LlmAgentBuilder::new("error_handling_agent") .model(model) .before_callback(Box::new(|ctx| { Box::pin(async move { // Validate something critical if ctx.user_id().is_empty() { return Err(AdkError::agent("User ID is required")); } Ok(None) }) })) .build()?; ``` ## Best Practices 1. **Keep callbacks lightweight**: Avoid heavy computation in callbacks 2. **Handle errors gracefully**: Return meaningful error messages 3. **Use logging sparingly**: Too much logging can impact performance 4. **Cache wisely**: Consider cache invalidation strategies 5. **Test callbacks independently**: Unit test callback logic separately ## Related - [LlmAgent](../agents/llm-agent.md) - Agent configuration - [Tools](../tools/function-tools.md) - Tool system - [Events](../events/events.md) - Event structure --- **Previous**: [← State Management](../sessions/state.md) | **Next**: [Artifacts →](../artifacts/artifacts.md)