-
-
Notifications
You must be signed in to change notification settings - Fork 88
core
Fundamental types and traits from adk-core that form the foundation of ADK-Rust.
Every message in ADK flows through Content and Part. Understanding these types is essential for working with agents, tools, and callbacks.
Content represents a single message in a conversation. It has a role (who sent it) and one or more parts (the actual content).
Roles:
-
"user"- Messages from the user -
"model"- Responses from the AI model -
"tool"- Results from tool execution
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?");Multimodal Content:
Content can include images, audio, PDFs, and other binary data alongside text:
// 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 is an enum representing different types of content within a message:
pub enum Part {
// Plain text
Text { text: String },
// Binary data embedded in the message
InlineData { mime_type: String, data: Vec<u8> },
// Reference to external file (URL or cloud storage)
FileData { mime_type: String, file_uri: String },
// Model requesting a tool call
FunctionCall { name: String, args: Value, id: Option<String> },
// Result of a tool execution
FunctionResponse { function_response: FunctionResponseData, id: Option<String> },
}Creating Parts Directly:
use adk_core::Part;
// Text part
let text = Part::text_part("Hello, world!");
// Image from bytes
let image = Part::inline_data("image/png", png_bytes);
// Image from URL
let remote_image = Part::file_data("image/jpeg", "https://example.com/photo.jpg");Inspecting Parts:
// Get text content (returns None for non-text parts)
if let Some(text) = part.text() {
println!("Text: {}", text);
}
// Get MIME type (for InlineData and FileData)
if let Some(mime) = part.mime_type() {
println!("MIME type: {}", mime);
}
// Get file URI (for FileData only)
if let Some(uri) = part.file_uri() {
println!("File URI: {}", uri);
}
// Check if part contains media (image, audio, video, etc.)
if part.is_media() {
println!("This part contains binary media");
}Iterating Over Parts:
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
);
}
}
}The Agent trait is the core abstraction for all agents in ADK. Every agent type—LLM agents, workflow agents, custom agents—implements this 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>;
}Key Points:
-
name(): Used for logging, transfers, and identification. Must be unique within a multi-agent system. -
description(): Shown to LLMs when the agent is used as a tool or for routing decisions. -
sub_agents(): Returns child agents. Empty for leaf agents (LlmAgent), populated for containers (SequentialAgent, ParallelAgent). -
run(): The main execution method. Receives context and returns a stream of events.
Why EventStream?
Agents return EventStream (a stream of Result<Event>) rather than a single response because:
- Streaming: Responses can be streamed token-by-token for better UX
- Tool calls: Multiple tool calls and responses happen during execution
- State changes: State updates are emitted as events
- Transfers: Agent transfers are signaled through events
Tools extend agent capabilities beyond conversation. They let agents interact with APIs, databases, file systems, or perform computations.
#[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>;
}Key Points:
-
name(): The function name the model uses to call this tool. Keep it short and descriptive (e.g.,get_weather,search_database). -
description(): Critical for the model to understand when to use the tool. Be specific about what it does and when to use it. -
parameters_schema(): JSON Schema that tells the model what arguments to provide. Without this, the model guesses. -
execute(): Receives parsed arguments asserde_json::Value. Return the result as JSON.
Implementing a Custom Tool:
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"
}))
}
}Long-Running Tools:
For operations that take a long time (file processing, external API calls), mark the tool as long-running:
fn is_long_running(&self) -> bool { true }Long-running tools return a task ID immediately. The model is instructed not to call the tool again while it's pending.
Toolset provides tools dynamically based on context. Use this when:
- Tools depend on user permissions
- Tools are loaded from external sources (MCP servers)
- Tool availability changes during execution
#[async_trait]
pub trait Toolset: Send + Sync {
/// Toolset identifier
fn name(&self) -> &str;
/// Return available tools for the current context
async fn tools(&self, ctx: Arc<dyn ReadonlyContext>) -> Result<Vec<Arc<dyn Tool>>>;
}Example: Permission-Based 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())
}
}
}Contexts provide information and services to agents and tools during execution. There's a hierarchy of context traits, each adding more capabilities.
Basic information available everywhere:
pub trait ReadonlyContext: Send + Sync {
/// Unique ID for this invocation
fn invocation_id(&self) -> &str;
/// Name of the currently executing agent
fn agent_name(&self) -> &str;
/// User identifier
fn user_id(&self) -> &str;
/// Application name
fn app_name(&self) -> &str;
/// Session identifier
fn session_id(&self) -> &str;
/// The user's input that triggered this invocation
fn user_content(&self) -> &Content;
}Adds artifact access (extends ReadonlyContext):
pub trait CallbackContext: ReadonlyContext {
/// Access to artifact storage (if configured)
fn artifacts(&self) -> Option<Arc<dyn Artifacts>>;
}For tool execution (extends CallbackContext):
pub trait ToolContext: CallbackContext {
/// ID of the function call that triggered this tool
fn function_call_id(&self) -> &str;
/// Get current event actions (transfer, escalate, etc.)
fn actions(&self) -> EventActions;
/// Set event actions (e.g., trigger a transfer)
fn set_actions(&self, actions: EventActions);
/// Search long-term memory
async fn search_memory(&self, query: &str) -> Result<Vec<MemoryEntry>>;
}Full context for agent execution (extends CallbackContext):
pub trait InvocationContext: CallbackContext {
/// The agent being executed
fn agent(&self) -> Arc<dyn Agent>;
/// Memory service (if configured)
fn memory(&self) -> Option<Arc<dyn Memory>>;
/// Current session with state and history
fn session(&self) -> &dyn Session;
/// Execution configuration
fn run_config(&self) -> &RunConfig;
/// Signal that this invocation should end
fn end_invocation(&self);
/// Check if invocation has been ended
fn ended(&self) -> bool;
}Sessions track conversations. State stores data within sessions.
pub trait Session: Send + Sync {
/// Unique session identifier
fn id(&self) -> &str;
/// Application this session belongs to
fn app_name(&self) -> &str;
/// User who owns this session
fn user_id(&self) -> &str;
/// Mutable state storage
fn state(&self) -> &dyn State;
/// Previous messages in this conversation
fn conversation_history(&self) -> Vec<Content>;
}Key-value storage with scoped prefixes:
pub trait State: Send + Sync {
/// Get a value by key
fn get(&self, key: &str) -> Option<Value>;
/// Set a value
fn set(&mut self, key: String, value: Value);
/// Get all key-value pairs
fn all(&self) -> HashMap<String, Value>;
}State Prefixes:
State keys use prefixes to control scope and persistence:
| Prefix | Scope | Persistence | Use Case |
|---|---|---|---|
user: |
User-level | Across all sessions | User preferences, settings |
app: |
Application-level | Application-wide | Shared configuration |
temp: |
Turn-level | Cleared each turn | Temporary computation data |
| (none) | Session-level | This session only | Conversation context |
// In a callback or tool
let state = ctx.session().state();
// User preference (persists across sessions)
state.set("user:theme".into(), json!("dark"));
// Session-specific data
state.set("current_topic".into(), json!("weather"));
// Temporary data (cleared after this turn)
state.set("temp:step_count".into(), json!(1));
// Read values
if let Some(theme) = state.get("user:theme") {
println!("Theme: {}", theme);
}ADK uses a unified structured error type for all operations:
use adk_core::{AdkError, ErrorComponent, ErrorCategory};
// AdkError is a struct with component, category, code, message, retry hint, and details
let err = AdkError::new(
ErrorComponent::Model,
ErrorCategory::RateLimited,
"model.openai.rate_limited",
"Too many requests",
);
// Backward-compatible shorthand (produces .legacy codes)
let err = AdkError::model("something went wrong");
// Check error properties
err.is_retryable(); // true for RateLimited, Unavailable, Timeout
err.is_not_found(); // category == NotFound
err.http_status_code(); // 429 for RateLimited
err.to_problem_json(); // structured JSON for HTTP responses
pub type Result<T> = std::result::Result<T, AdkError>;Error Handling 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"))?;
let response = reqwest::get(&format!("https://api.weather.com/{}", city))
.await
.map_err(|e| AdkError::tool(format!("API error: {e}")))?;
Ok(json!({ "weather": "sunny" }))
}See Development Guidelines for the full error handling guide.
ADK-Rust uses typed identity primitives to prevent accidental ID mixing and make session addressing explicit. There are three distinct identity layers, each serving a different purpose.
Auth identity represents who is authenticated and what they are allowed to do. It lives in RequestContext and is populated at HTTP or Studio ingress:
-
user_id— the authenticated user -
scopes— authorization scopes (e.g.["read", "write"]) -
metadata— additional request-level key-value pairs
Auth identity does not include session or invocation information. It answers: "Who is making this request?"
Session identity is the stable address of a conversation session. It is a composite of three typed fields:
use adk_core::{AdkIdentity, AppName, SessionId, UserId};
let identity = AdkIdentity::new(
AppName::try_from("weather-app")?,
UserId::try_from("tenant:alice@example.com")?,
SessionId::generate(),
);
// Each field is a typed wrapper — no accidental swaps
assert_eq!(identity.app_name.as_ref(), "weather-app");AdkIdentity replaces the pattern of passing (app_name, user_id, session_id) as three separate strings. It eliminates parameter ordering bugs and makes the addressing model explicit across all session backends.
Session identity answers: "Which conversation session is being addressed?"
Execution identity extends session identity with per-invocation runtime coordinates:
use adk_core::{ExecutionIdentity, InvocationId};
let exec = ExecutionIdentity {
adk: identity.clone(),
invocation_id: InvocationId::generate(),
branch: String::new(),
agent_name: "planner".to_string(),
};The runtime stores ExecutionIdentity internally so that event creation, agent transfers, and telemetry correlation work with validated IDs — no re-parsing of raw strings after the initial boundary parse.
Execution identity answers: "Which invocation of which session is currently running, and which agent is executing?"
The four leaf types — AppName, UserId, SessionId, InvocationId — all share the same validation rules:
- Must not be empty
- Must not contain null bytes
- Must not exceed 512 bytes
- Characters like
:,@,/are allowed (validation is not coupled to backend key encoding)
All four use #[serde(transparent)], so existing JSON and SSE payloads continue to encode identifiers as plain strings.
use adk_core::{AppName, SessionId, InvocationId};
// Parse at trust boundaries
let app = AppName::try_from("my-app")?;
// Generate internally
let session = SessionId::generate();
let invocation = InvocationId::generate();
// Use as &str anywhere
println!("app={}, session={}", app.as_ref(), session.as_ref());Both ReadonlyContext and Session provide typed helper methods that parse the underlying string values on demand:
// From a ReadonlyContext (agent or tool execution)
let identity = ctx.try_identity()?; // AdkIdentity
let exec = ctx.try_execution_identity()?; // ExecutionIdentity
let app = ctx.try_app_name()?; // AppName
// From a Session
let identity = session.try_identity()?; // AdkIdentity
let user = session.try_user_id()?; // UserIdThese methods return Result<T> — if the underlying string is invalid (e.g. empty), you get a descriptive error instead of a panic.
| Use Case | Identity Layer | Why |
|---|---|---|
| Multi-tenant isolation | AdkIdentity |
The (app, user, session) triple ensures sessions never cross tenant boundaries |
| Audit correlation | ExecutionIdentity |
Links every event to a specific invocation, agent, and session |
| Replay and resume |
AdkIdentity + InvocationId
|
Stable session address plus invocation ID enables deterministic replay |
| Safer plugin/tool contracts | Typed leaf IDs | Plugins receive AppName and UserId instead of raw strings — no accidental swaps |
| Boundary validation | TryFrom<&str> |
Parse once at HTTP ingress, reject bad input early with a 400 error |
RequestContext → Auth: who is authenticated, what scopes
↓ (user binding)
AdkIdentity → Session: which app + user + session
↓ (+ invocation)
ExecutionIdentity → Runtime: which invocation, branch, agent
These three layers are intentionally separate. RequestContext may share the same UserId type, but the structs are never merged. Auth concerns stay in auth, session addressing stays in session identity, and runtime coordinates stay in execution identity.
Agents return a stream of events rather than a single response:
pub type EventStream = Pin<Box<dyn Stream<Item = Result<Event>> + Send>>;Processing Events:
use futures::StreamExt;
let mut stream = agent.run(ctx).await?;
while let Some(result) = stream.next().await {
match result {
Ok(event) => {
// Check for text content
if let Some(content) = event.content() {
for part in &content.parts {
if let Some(text) = part.text() {
print!("{}", text);
}
}
}
// Check for state changes
for (key, value) in event.state_delta() {
println!("State changed: {} = {}", key, value);
}
// Check if this is the final response
if event.is_final_response() {
println!("\n[Done]");
}
}
Err(e) => {
eprintln!("Error: {}", e);
break;
}
}
}The trait that all LLM providers implement:
#[async_trait]
pub trait Llm: Send + Sync {
/// Model identifier (e.g., "gemini-2.5-flash", "gpt-5-mini")
fn name(&self) -> &str;
/// Generate content (streaming or non-streaming)
async fn generate_content(
&self,
request: LlmRequest,
stream: bool,
) -> Result<LlmResponseStream>;
}LlmRequest:
pub struct LlmRequest {
pub contents: Vec<Content>, // Conversation history
pub tools: Vec<ToolDeclaration>, // Available tools
pub system_instruction: Option<String>, // System prompt
pub config: GenerateContentConfig, // Temperature, max_tokens, etc.
}LlmRequest Builder Methods:
LlmRequest provides builder methods for convenient construction:
use adk_core::{LlmRequest, Content, GenerateContentConfig};
use serde_json::json;
// Basic request
let request = LlmRequest::new(vec![
Content::new("user").with_text("Hello!")
]);
// With system instruction
let request = LlmRequest::new(contents)
.with_system_instruction("You are a helpful assistant.");
// With response schema for structured output
let request = LlmRequest::new(contents)
.with_response_schema(json!({
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "number" }
},
"required": ["name", "age"]
}));
// With full config
let config = GenerateContentConfig {
temperature: Some(0.7),
max_output_tokens: Some(1024),
response_schema: Some(schema),
..Default::default()
};
let request = LlmRequest::new(contents).with_config(config);| Method | Description |
|---|---|
new(contents) |
Create request with conversation history |
with_system_instruction(text) |
Set the system prompt |
with_response_schema(schema) |
Set JSON schema for structured output |
with_config(config) |
Set full generation config |
with_tools(tools) |
Add tool declarations |
LlmResponse:
pub struct LlmResponse {
pub content: Option<Content>, // Generated content
pub finish_reason: Option<FinishReason>, // Why generation stopped
pub usage: Option<UsageMetadata>, // Token counts
pub partial: bool, // Is this a streaming chunk?
pub turn_complete: bool, // Is the turn finished?
}All providers (Gemini, OpenAI, Anthropic, Ollama, etc.) implement this trait, making them interchangeable:
// Switch providers by changing one line
let model: Arc<dyn Llm> = Arc::new(GeminiModel::new(&key, "gemini-2.5-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()?;Previous: ← Introduction | Next: Runner →