Getting Started with Radkit¶
This guide will help you get up and running with Radkit, the A2A-native agent development kit for Rust.
Prerequisites¶
- Rust: Version 1.70 or higher
- Cargo: Rust's package manager
- API Keys: For LLM providers (Anthropic Claude, Google Gemini)
Installation¶
Add these dependencies to your Cargo.toml
:
[dependencies]
radkit = "0.0.1"
futures = "0.3.31"
tokio = "1.47.1"
uuid = "1.18.0"
dotenvy = "0.15.7"
Setting Up Your First Agent¶
Step 1: Environment Configuration¶
Create a .env
file in your project root:
# For Anthropic Claude
ANTHROPIC_API_KEY=sk-ant-your-key-here
# For Google Gemini
GEMINI_API_KEY=your-gemini-key-here
Step 2: Basic Agent Creation¶
use radkit::a2a::{Message, MessageRole, MessageSendParams, Part, SendMessageResult};
use radkit::agents::Agent;
use radkit::models::AnthropicLlm;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load environment variables
dotenvy::dotenv().ok();
// Create an LLM provider
let llm = AnthropicLlm::new(
"claude-3-5-sonnet-20241022".to_string(),
std::env::var("ANTHROPIC_API_KEY")?,
);
// Create an agent (services are created automatically)
let agent = Agent::builder(
"You are a knowledgeable and friendly assistant.",
llm,
)
.with_card(|c| c
.with_name("MyFirstAgent")
.with_description("A helpful AI assistant")
)
.build();
println!("✅ Agent created successfully!");
// Your agent is ready to use!
Ok(())
}
Step 3: Sending Your First Message¶
// Helper function to create a user message
fn create_user_message(text: &str) -> Message {
Message {
kind: "message".to_string(),
message_id: uuid::Uuid::new_v4().to_string(),
role: MessageRole::User,
parts: vec![Part::Text {
text: text.to_string(),
metadata: None,
}],
context_id: None, // Will create a new session
task_id: None, // Will create a new task
reference_task_ids: Vec::new(),
extensions: Vec::new(),
metadata: None,
}
}
// Create message parameters
let params = MessageSendParams {
message: create_user_message("Hello! What can you help me with today?"),
configuration: None,
metadata: None,
};
// Send the message (non-streaming)
let response = agent.send_message(
"my_app".to_string(), // Application name
"user123".to_string(), // User ID
params,
).await?;
// Process the response
match response.result {
SendMessageResult::Task(task) => {
println!("✅ Task created: {}", task.id);
println!("Context ID: {}", task.context_id);
println!("Status: {:?}", task.status.state);
// Get the agent's response
for message in &task.history {
if message.role == MessageRole::Agent {
for part in &message.parts {
if let Part::Text { text, .. } = part {
println!("Agent: {}", text);
}
}
}
}
}
SendMessageResult::Message(msg) => {
println!("Received direct message response");
}
}
Understanding Core Concepts¶
1. Multi-Tenancy¶
Radkit provides built-in multi-tenant isolation:
// Each request requires app_name and user_id
agent.send_message(
"app_name".to_string(), // Isolates different applications
"user_id".to_string(), // Isolates different users
params,
).await?;
This ensures complete data isolation between different applications and users.
2. Tasks and Sessions¶
Every agent interaction creates or continues: - Task: A unit of work with history and artifacts - Session: A conversation context (maps to A2A contextId)
// Continue an existing conversation in the same session
let mut follow_up_message = create_user_message("Do you remember what we were talking about?");
follow_up_message.context_id = Some("existing_context_id".to_string());
// task_id remains None to create a new task in the same session
let params = MessageSendParams {
message: follow_up_message,
configuration: None,
metadata: None,
};
let response = agent.send_message("my_app".to_string(), "user123".to_string(), params).await?;
How Conversations are Built from Session Events¶
Key Architecture: Radkit builds conversations dynamically from session.events
rather than storing static message history. This enables:
- Cross-Task Memory: Agents remember conversations across multiple tasks in the same session
- Event-Driven Conversation: All interactions (messages, tool calls, state changes) are stored as events
- A2A Context Mapping: Session ID directly maps to A2A
contextId
for protocol compliance
// Example: How the agent reconstructs conversation context
async fn create_llm_request(&self, context: &ExecutionContext) -> LlmRequest {
// Get conversation from EventProcessor which reconstructs from session events
let content_messages = context.get_llm_conversation().await?;
// The LLM sees the ENTIRE conversation history reconstructed from events
LlmRequest {
messages: content_messages, // All content from session events
current_task_id: context.task_id.clone(),
context_id: context.context_id.clone(), // Maps to A2A contextId
system_instruction: Some(self.agent.instruction().to_string()),
config: GenerateContentConfig::default(),
toolset: self.agent.toolset().cloned(),
metadata: context.current_params.metadata.clone().unwrap_or_default(),
}
}
// SessionEvent to Content conversion preserves:
// - User messages (SessionEventType::UserMessage)
// - Agent responses (SessionEventType::AgentMessage)
// - Function calls and responses (ContentPart within messages)
// - All metadata and context information
This means when you continue a conversation:
1. Same context_id
= Agent remembers everything from previous tasks
2. New context_id
= Fresh conversation with no memory
3. Events are the source of truth = No separate message storage needed
3. Built-in Tools¶
Enable task management capabilities:
use radkit::agents::AgentConfig;
let config = AgentConfig::default().with_max_iterations(10);
let agent = Agent::builder(
"You can update task status and save artifacts using built-in tools.",
llm,
)
.with_card(|c| c
.with_name("builtin_agent")
.with_description("Agent with built-in tools")
)
.with_config(config)
.with_builtin_task_tools(); // Adds update_status and save_artifact tools
.build();
// The agent can now use:
// - update_status: Update task status (submitted, working, completed, failed, etc.)
// - save_artifact: Save analysis results, files, or any generated content
// These tools automatically emit A2A-compliant events
4. Custom Function Tools¶
Create your own tools using FunctionTool
:
use radkit::tools::{FunctionTool, ToolResult};
use serde_json::json;
// Create a weather tool
fn create_weather_tool() -> FunctionTool {
FunctionTool::new(
"get_weather".to_string(),
"Get the current weather for a location".to_string(),
|args, _context| Box::pin(async move {
let location = args
.get("location")
.and_then(|v| v.as_str())
.unwrap_or("Unknown");
let weather_data = json!({
"location": location,
"temperature": "72°F",
"conditions": "Partly cloudy",
"humidity": "45%"
});
ToolResult::success(weather_data)
}),
)
.with_parameters_schema(json!({
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA"
}
},
"required": ["location"]
}))
}
// Example tool that uses ToolContext for state management
fn create_preference_tool() -> FunctionTool {
FunctionTool::new(
"set_preference".to_string(),
"Set user preferences".to_string(),
|args, context| Box::pin(async move {
let key = args
.get("key")
.and_then(|v| v.as_str())
.unwrap_or_default();
let value = args
.get("value")
.cloned()
.unwrap_or(json!(null));
// Use the ToolContext API for state management
match context.set_user_state(key.to_string(), value.clone()).await {
Ok(()) => ToolResult::success(json!({
"message": format!("Set preference '{}' to: {}", key, value)
})),
Err(e) => ToolResult::error(format!("Failed to set preference: {}", e))
}
}),
)
.with_parameters_schema(json!({
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The preference key to set"
},
"value": {
"type": "string",
"description": "The preference value to set"
}
},
"required": ["key", "value"]
}))
}
// Create a calculator tool
fn create_calculator_tool() -> FunctionTool {
FunctionTool::new(
"calculate".to_string(),
"Perform basic mathematical calculations".to_string(),
|args, _context| Box::pin(async move {
let expression = args
.get("expression")
.and_then(|v| v.as_str())
.unwrap_or("");
let result = match expression {
"2+2" => 4,
"10*5" => 50,
"100/4" => 25,
"15-3" => 12,
_ => {
return ToolResult::error(format!("Cannot calculate: {}", expression));
}
};
ToolResult::success(json!({"result": result, "expression": expression}))
}),
)
.with_parameters_schema(json!({
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Mathematical expression to evaluate (e.g., '2+2', '10*5')"
}
},
"required": ["expression"]
}))
}
// Add tools to your agent using a toolset
use radkit::tools::SimpleToolset;
let toolset = SimpleToolset::new()
.add_tool(create_weather_tool())
.add_tool(create_calculator_tool());
let agent = Agent::builder(
"You are a helpful assistant. Use the available tools when requested by the user.",
llm,
)
.with_card(|c| c
.with_name("tool_agent")
.with_description("Agent with custom tools")
)
.with_toolset(toolset)
.build();
5. Monitoring Tool Calls and Responses¶
Tool calls and responses are captured in session events and can be monitored in real-time via streaming:
use futures::StreamExt;
use radkit::a2a::SendStreamingMessageResult;
// Send a message that will trigger tool usage
let message = create_user_message(
"What's the weather like in San Francisco? Please use the get_weather tool."
);
let params = MessageSendParams {
message,
configuration: None,
metadata: None,
};
// Use streaming to monitor tool calls in real-time
let mut execution = agent.send_streaming_message(
"test_app".to_string(),
"test_user".to_string(),
params
).await?;
let mut final_task = None;
// Process A2A events and monitor tool responses
while let Some(event) = execution.a2a_stream.next().await {
match event {
SendStreamingMessageResult::Message(msg) => {
// Agent's response includes tool results
println!("💬 Agent (role={:?}):", msg.role);
for part in &msg.parts {
if let Part::Text { text, .. } = part {
println!(" {}", text);
}
}
}
SendStreamingMessageResult::Task(task) => {
final_task = Some(task);
break;
}
_ => {}
}
}
// Tool calls are persisted in session events with detailed information
if let Some(task) = final_task {
let session_service = agent.session_service();
let session = session_service
.get_session("test_app", "test_user", &task.context_id)
.await?
.expect("Session should exist");
println!("\n📋 Session Events Summary:");
println!(" Total events: {}", session.events.len());
// Count different types of content in session events
let mut tool_calls = 0;
let mut tool_responses = 0;
for event in &session.events {
if let radkit::sessions::SessionEventType::UserMessage { content }
| radkit::sessions::SessionEventType::AgentMessage { content } = &event.event_type {
for part in &content.parts {
match part {
radkit::models::content::ContentPart::FunctionCall { name, .. } => {
tool_calls += 1;
println!(" 🔧 Tool Call: {}", name);
}
radkit::models::content::ContentPart::FunctionResponse { name, success, .. } => {
tool_responses += 1;
println!(" ⚙️ Tool Response: {} (success: {})", name, success);
}
_ => {}
}
}
}
}
println!(" Tool calls: {}, Tool responses: {}", tool_calls, tool_responses);
}
6. Tool Error Handling¶
Handle tool failures gracefully:
// Create a tool that sometimes fails for demonstration
let error_tool = FunctionTool::new(
"risky_operation".to_string(),
"An operation that might fail".to_string(),
|args, _context| Box::pin(async move {
let input = args.get("input").and_then(|v| v.as_str()).unwrap_or("");
if input == "fail" {
ToolResult::error("Operation failed as requested".to_string())
} else {
ToolResult::success(json!({ "result": "Operation succeeded" }))
}
}),
)
.with_parameters_schema(json!({
"type": "object",
"properties": {
"input": {
"type": "string",
"description": "Input parameter - use 'fail' to trigger error"
}
},
"required": ["input"]
}));
// Monitor tool failures through session events after completion
let response = agent.send_message(app, user, params).await?;
if let SendMessageResult::Task(task) = response.result {
// Check session events for tool failures
let session_service = agent.session_service();
let session = session_service
.get_session("test_app", "test_user", &task.context_id)
.await?
.expect("Session should exist");
for event in &session.events {
if let radkit::sessions::SessionEventType::UserMessage { content }
| radkit::sessions::SessionEventType::AgentMessage { content } = &event.event_type {
for part in &content.parts {
if let radkit::models::content::ContentPart::FunctionResponse {
name, success, error_message, ..
} = part {
if !success {
println!("❌ Tool '{}' failed: {}", name,
error_message.as_deref().unwrap_or("Unknown error"));
}
}
}
}
}
}
Streaming vs Non-Streaming¶
Non-Streaming (Complete Response)¶
Best for: Simple queries, batch processing, testing
let execution = agent.send_message(app, user, params).await?;
// Access the complete response
if let SendMessageResult::Task(task) = execution.result {
println!("Task completed: {}", task.id);
}
// Access ALL events including function calls/responses
println!("Total events captured: {}", execution.all_events.len());
for event in &execution.all_events {
match &event.event_type {
radkit::sessions::SessionEventType::UserMessage { content } |
radkit::sessions::SessionEventType::AgentMessage { content } => {
// Check for function calls and responses
for part in &content.parts {
match part {
radkit::models::content::ContentPart::FunctionCall { name, arguments, .. } => {
println!("🔧 Function called: {} with args: {:?}", name, arguments);
}
radkit::models::content::ContentPart::FunctionResponse {
name, success, result, error_message, duration_ms, ..
} => {
println!("⚙️ Function {} returned (success: {})", name, success);
if *success {
println!(" Result: {:?}", result);
} else if let Some(err) = error_message {
println!(" Error: {}", err);
}
if let Some(ms) = duration_ms {
println!(" Duration: {}ms", ms);
}
}
radkit::models::content::ContentPart::Text { text, .. } => {
if content.role == MessageRole::Agent {
println!("💬 Agent: {}", text);
}
}
_ => {}
}
}
}
radkit::sessions::SessionEventType::TaskStatusChanged { new_state, .. } => {
println!("📊 Task status: {:?}", new_state);
}
radkit::sessions::SessionEventType::ArtifactSaved { artifact } => {
println!("💾 Artifact saved: {:?}", artifact.name);
}
_ => {}
}
}
// Access A2A protocol events only (filtered subset)
println!("A2A events: {}", execution.a2a_events.len());
Streaming (Real-Time Response)¶
Best for: Interactive chat, progress updates, long-running tasks
use futures::StreamExt;
use radkit::a2a::{SendStreamingMessageResult, TaskState};
// Create a message that will trigger tool usage
let message = create_user_message(
"Please update the task status to 'working' and save a config file as an artifact."
);
let params = MessageSendParams {
message,
configuration: None,
metadata: None,
};
// Use streaming to capture events in real-time
let mut execution = agent.send_streaming_message(
"my_app".to_string(),
"user123".to_string(),
params
).await?;
// Option 1: Monitor A2A protocol events (for UI updates)
let mut status_updates = 0;
let mut artifacts = 0;
let mut final_task = None;
// Process A2A events as they arrive
while let Some(event) = execution.a2a_stream.next().await {
match event {
SendStreamingMessageResult::Message(msg) => {
// Real-time message content
for part in &msg.parts {
if let Part::Text { text, .. } = part {
print!("{}", text);
}
}
}
SendStreamingMessageResult::TaskStatusUpdate(update) => {
status_updates += 1;
println!("✅ Status update: {:?} (final: {})",
update.status.state, update.is_final);
}
SendStreamingMessageResult::TaskArtifactUpdate(update) => {
artifacts += 1;
println!("✅ Artifact saved: {:?}", update.artifact.name);
}
SendStreamingMessageResult::Task(task) => {
// Final complete task
final_task = Some(task);
break;
}
}
}
println!("Streaming completed: {} status updates, {} artifacts", status_updates, artifacts);
// Option 2: Also monitor ALL events stream for detailed debugging
// You can spawn a task to monitor all_events_stream in parallel
tokio::spawn(async move {
let mut all_events_stream = execution.all_events_stream;
while let Some(event) = all_events_stream.next().await {
match &event.event_type {
radkit::sessions::SessionEventType::UserMessage { content } |
radkit::sessions::SessionEventType::AgentMessage { content } => {
// Monitor function calls and responses in real-time
for part in &content.parts {
match part {
radkit::models::content::ContentPart::FunctionCall { name, arguments, .. } => {
println!("🔧 [DEBUG] Function called: {} with {:?}", name, arguments);
}
radkit::models::content::ContentPart::FunctionResponse {
name, success, result, duration_ms, ..
} => {
println!("⚙️ [DEBUG] Function {} returned in {:?}ms (success: {})",
name, duration_ms, success);
if *success {
println!(" [DEBUG] Result: {:?}", result);
}
}
_ => {}
}
}
}
radkit::sessions::SessionEventType::TaskCreated { .. } => {
println!("📋 [DEBUG] Task created");
}
radkit::sessions::SessionEventType::StateChanged { key, new_value, .. } => {
println!("📝 [DEBUG] State changed: {} = {}", key, new_value);
}
_ => {}
}
}
});
// Access session events for debugging after completion
if let Some(task) = &final_task {
let session_service = agent.session_service();
let session = session_service
.get_session("my_app", "user123", &task.context_id)
.await?
.expect("Session should exist");
println!("Session has {} total events", session.events.len());
}
Event Monitoring¶
Both streaming and non-streaming modes capture events in the session:
use radkit::models::content::ContentPart;
// Access session events after completion
let session_service = agent.session_service();
let session = session_service
.get_session("test_app", "test_user", &task.context_id)
.await?
.expect("Session should exist");
// Monitor all activity in the session
println!("📋 Session Event Summary:");
println!(" Total events: {}", session.events.len());
for event in &session.events {
match &event.event_type {
radkit::sessions::SessionEventType::UserMessage { content } => {
println!("👤 User message: {}",
content.parts.iter()
.filter_map(|p| match p {
ContentPart::Text { text, .. } => Some(text.chars().take(50).collect::<String>()),
_ => None
})
.collect::<Vec<_>>()
.join(", ")
);
}
radkit::sessions::SessionEventType::AgentMessage { content } => {
println!("🤖 Agent response");
// Monitor tool calls and responses
for part in &content.parts {
match part {
ContentPart::FunctionCall { name, arguments, .. } => {
println!(" 🔧 Function Call: {} with args: {:?}", name, arguments);
}
ContentPart::FunctionResponse { name, success, result, error_message, duration_ms, .. } => {
println!(" ⚙️ Function Response: {} (success: {})", name, success);
if let Some(error) = error_message {
println!(" Error: {}", error);
}
if let Some(duration) = duration_ms {
println!(" Duration: {}ms", duration);
}
}
ContentPart::Text { text, .. } => {
println!(" 💬 Text: {}", text.chars().take(50).collect::<String>());
}
_ => {}
}
}
}
radkit::sessions::SessionEventType::TaskStatusChanged { new_state, .. } => {
println!("📊 Task status changed to: {:?}", new_state);
}
radkit::sessions::SessionEventType::ArtifactSaved { artifact } => {
println!("💾 Artifact saved: {:?}", artifact.name);
}
radkit::sessions::SessionEventType::StateChanged { key, new_value, scope, .. } => {
println!("📝 State changed: {} = {} (scope: {:?})", key, new_value, scope);
}
_ => {}
}
}
Complete Example: Math Tutor Agent¶
use radkit::a2a::{Message, MessageRole, MessageSendParams, Part, SendMessageResult};
use radkit::agents::{Agent, AgentConfig};
use radkit::models::AnthropicLlm;
use radkit::sessions::SessionEventType;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
dotenvy::dotenv().ok();
// Create a specialized math tutor agent
let llm = AnthropicLlm::new(
"claude-3-5-sonnet-20241022".to_string(),
std::env::var("ANTHROPIC_API_KEY")?,
);
let agent = Agent::builder(
r#"You are a patient math tutor. When solving problems:
1. Break down the problem into steps
2. Explain each step clearly
3. Use the save_artifact tool to save the solution
4. Update your status as you work through the problem"#,
llm,
)
.with_card(|c| c
.with_name("MathTutor")
.with_description("An AI math tutor that explains concepts step-by-step")
)
.with_config(AgentConfig::default().with_max_iterations(10))
.with_builtin_task_tools()
.build();
// Student asks a question
let question = "A train travels 120 miles in 2 hours. What is its average speed?";
let params = MessageSendParams {
message: Message {
kind: "message".to_string(),
message_id: uuid::Uuid::new_v4().to_string(),
role: MessageRole::User,
parts: vec![Part::Text {
text: question.to_string(),
metadata: None,
}],
context_id: None,
task_id: None,
reference_task_ids: Vec::new(),
extensions: Vec::new(),
metadata: None,
},
configuration: None,
metadata: None,
};
println!("📚 Math Tutor Agent");
println!("Student: {}\n", question);
// Get the solution
let response = agent.send_message(
"math_tutor_app".to_string(),
"student_001".to_string(),
params,
).await?;
// Display the response
if let SendMessageResult::Task(task) = response.result {
// Find the tutor's explanation
for message in &task.history {
if message.role == MessageRole::Agent {
println!("Tutor:");
for part in &message.parts {
if let Part::Text { text, .. } = part {
println!("{}", text);
}
}
}
}
// Check for saved solutions
if !task.artifacts.is_empty() {
println!("\n📝 Saved Solutions:");
for artifact in &task.artifacts {
if let Some(name) = &artifact.name {
println!("- {}", name);
}
}
}
println!("\n✅ Task Status: {:?}", task.status.state);
}
// Analyze session events
let session_service = agent.session_service();
let session = session_service
.get_session("math_tutor_app", "student_001", &task.context_id)
.await?
.expect("Session should exist");
println!("📊 Session Analysis:");
println!(" Total events: {}", session.events.len());
let mut message_events = 0;
let mut status_changes = 0;
let mut artifacts = 0;
for event in &session.events {
match &event.event_type {
radkit::sessions::SessionEventType::UserMessage { .. } |
radkit::sessions::SessionEventType::AgentMessage { .. } => {
message_events += 1;
}
radkit::sessions::SessionEventType::TaskStatusChanged { .. } => {
status_changes += 1;
}
radkit::sessions::SessionEventType::ArtifactSaved { .. } => {
artifacts += 1;
}
_ => {}
}
}
println!(" Messages: {}, Status changes: {}, Artifacts: {}",
message_events, status_changes, artifacts);
Ok(())
}
Error Handling¶
Always handle potential errors gracefully:
use radkit::errors::AgentError;
match agent.send_message(app, user, params).await {
Ok(response) => {
// Process successful execution
}
Err(AgentError::LlmRateLimit { provider }) => {
println!("Rate limited by {}. Retrying in 60 seconds...", provider);
tokio::time::sleep(Duration::from_secs(60)).await;
// Retry logic
}
Err(AgentError::SessionNotFound { session_id, .. }) => {
println!("Session {} not found. Creating new session...", session_id);
// Create new session
}
Err(e) => {
eprintln!("Error: {}", e);
// Generic error handling
}
}
Best Practices¶
- Always Use Multi-Tenancy: Provide app_name and user_id for proper isolation
- Enable Built-in Tools: Use
with_builtin_task_tools()
for task management - Monitor Events: Use captured events for debugging and observability
- Handle Errors: Implement proper error handling and retry logic
- Use Streaming for Interactive Apps: Better user experience with real-time responses
- Set Appropriate Timeouts: Configure max_iterations to prevent infinite loops
- Secure API Keys: Never hardcode API keys; use environment variables
Next Steps¶
- Tasks Guide - Understand task lifecycle
- Sessions Guide - Learn about state management