An educational Rust framework for building autonomous AI agents with memory, planning, tool execution, and safety guardrails.
This framework demonstrates how to build production-quality AI agents in Rust, with clear architectural boundaries and incremental complexity. It's designed as a learning resource with a modular workspace structure where each component can be understood independently.
Building AI agents requires orchestrating multiple complex systems: language models, memory management, tool execution, and safety constraints. This framework provides a clear, well-structured implementation that:
- Teaches by Example: Each crate demonstrates a specific architectural pattern (traits, async execution, error handling)
- Production-Ready Patterns: Uses industry-standard libraries and follows Rust best practices
- Incremental Learning: Components can be studied independently, building from simple to complex
- Safety First: Demonstrates how to build guardrails and validation into AI systems
- Extensible Design: Easy to add new LLM providers, tools, and safety rules
This project serves as both a functional framework and a comprehensive tutorial on building AI systems in Rust.
The framework is organized into three layers, with clear separation of concerns and minimal coupling:
┌─────────────────────────────────────────────────────────────────┐
│ CLI & Examples │
│ (User Interface Layer) │
└────────────────────────────┬────────────────────────────────────┘
│
┌────────────────────────────┴────────────────────────────────────┐
│ Intelligence Layer │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐ │
│ │ Planner │ │ Executor │ │Guardrails │ │ Rules │ │
│ └──────────┘ └──────────┘ └───────────┘ └──────────┘ │
│ │ │ │ │ │
└────--───┼──────────────┼───────────────┼──────────────┼─────────┘
│ │ │ │
┌──────--─┴──────────────┴───────────────┴──────────────┴─────────┐
│ Capability Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ LLM │ │ Memory │ │ Tools │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │
└───────┼──────────────┼───────────────┼──────────────────────────┘
│ │ │
┌───────┴──────────────┴───────────────┴──────────────────────────┐
│ Foundation Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Core │ │ Config │ │Communication │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
- core - Fundamental types (Message, Role, AgentError) and error handling
- config - Configuration management from files and environment variables
- communication - HTTP client utilities with retry logic and timeout handling
- llm - LLM provider interfaces (OpenAI, Anthropic) with unified API
- memory - Conversation storage with token-aware context management
- tools - Tool system with registry and example implementations (Calculator, FileReader, WebSearch)
- planner - Task decomposition using LLM reasoning (ReAct pattern)
- executor - Step-by-step execution of plans with tool invocation
- guardrails - Safety validation before execution (file paths, rate limits)
- rules - Behavior customization through prompt modification
- cli - Command-line interface (REPL and single-turn modes)
- examples - Example agents demonstrating framework capabilities
ai-agent-framework/
├── Cargo.toml # Workspace manifest
├── README.md # This file
├── core/ # Fundamental traits and types
├── config/ # Configuration management
├── communication/ # HTTP client and API utilities
├── llm/ # LLM provider implementations
├── memory/ # Conversation storage
├── tools/ # Tool trait and implementations
├── planner/ # Task decomposition
├── executor/ # Step execution
├── guardrails/ # Safety validation
├── rules/ # Behavior customization
├── cli/ # Command-line interface
└── examples/ # Example agents
- Rust: Version 1.70 or later (Install Rust)
- LLM Provider (choose one):
- Ollama (Recommended for local development) - Install Ollama
- OpenAI - API key from OpenAI API Keys
- Anthropic - API key from Anthropic API Keys
With Docker (Recommended):
git clone https://github.com/netologist/samurai ai-agent-framework
cd ai-agent-framework
# One-command setup (starts container + pulls model)
make ollama-setup
# Or: ./scripts/setup-ollama.sh
# Run the chatbot
make run-ollamaNative Installation:
# Install Ollama from https://ollama.ai
ollama pull llama2
# Clone and run
git clone https://github.com/netologist/samurai ai-agent-framework
cd ai-agent-framework
cargo build --workspace
cargo run --example ollama_chatbot- Clone the repository:
git clone https://github.com/netologist/samurai ai-agent-framework
cd ai-agent-framework- Set up your API key:
# For OpenAI (GPT-3.5, GPT-4)
export OPENAI_API_KEY="sk-..."
# OR for Anthropic (Claude)
export ANTHROPIC_API_KEY="sk-ant-..."- Build the project:
cargo build --workspace- Run your first agent:
cargo run --example chatbotYou should see an interactive prompt where you can chat with the AI agent!
The framework includes four example agents that demonstrate different capabilities:
Conversational agent using local open-source models via Ollama.
cargo run --example ollama_chatbotWhat it demonstrates:
- Running agents completely locally
- Using open-source models (llama2, mistral, etc.)
- No API costs or internet dependency
Prerequisites: Ollama installed with a model pulled (e.g., ollama pull llama2)
Basic conversational agent with memory but no tools.
cargo run --example chatbotWhat it demonstrates:
- LLM integration (OpenAI/Anthropic/Ollama)
- Conversation memory
- Multi-turn interactions
Agent with web search and file reading capabilities.
cargo run --example researchWhat it demonstrates:
- Tool integration and execution
- Multi-step planning
- Tool result handling
Agent with file operations and safety guardrails.
cargo run --example file_managerWhat it demonstrates:
- Guardrail validation
- File path restrictions
- Safe tool execution
Here's a minimal example of creating an agent:
use ai_agent_framework::*;
#[tokio::main]
async fn main() -> Result<()> {
// Load configuration
let config = config::load_from_file("config.yaml")?;
// Initialize components
let llm = llm::create_provider(&config.llm)?;
let memory = memory::InMemoryStore::new();
let tools = tools::ToolRegistry::new();
// Create planner and executor
let planner = planner::Planner::new(llm.clone(), memory.clone());
let executor = executor::Executor::new(tools, memory);
// Process a query
let plan = planner.create_plan("What is 2 + 2?", &tools.list_tools()).await?;
let result = executor.execute_plan(plan).await?;
println!("Result: {}", result.final_response);
Ok(())
}Create a config.yaml file:
llm:
provider: openai # or "anthropic"
model: gpt-4
api_key: ${OPENAI_API_KEY}
temperature: 0.7
max_tokens: 2000
memory:
max_messages: 100
token_budget: 4000
tools:
- calculator
- file_reader
guardrails:
- file_path
- rate_limit# Run all tests
cargo test --workspace
# Run tests for a specific crate
cargo test -p core
# Run integration tests
cargo test --test agent_flow
# Run with output
cargo test -- --nocaptureEach crate has a specific responsibility and can be understood independently:
Purpose: Fundamental types and error handling used throughout the framework.
Key Types:
Message- Represents conversation turns with role, content, and timestampRole- Enum for System, User, and Assistant rolesAgentError- Common error type with structured error information using thiserrorResult<T>- Type alias forstd::result::Result<T, AgentError>
Dependencies: serde, thiserror, chrono
When to use: Import core types when building any framework component.
Purpose: Configuration management from files and environment variables.
Key Functions:
load_from_file(path)- Parse YAML/TOML configuration filesfrom_env()- Build configuration from environment variablesmerge(file, env)- Combine file and environment configs (env takes precedence)
Configuration Structure:
AgentConfig- Top-level configurationLLMConfig- Provider settings (provider, model, api_key, temperature, max_tokens)MemoryConfig- Memory settings (max_messages, token_budget)
Dependencies: serde, serde_yaml, core
When to use: Load configuration at application startup before initializing other components.
Purpose: HTTP client utilities with retry logic and error handling.
Key Types:
ApiClient- Wrapper around reqwest with timeout and retry supportwith_retry()- Exponential backoff retry function (max 3 attempts)
Features:
- 30-second default timeout
- Automatic retry on network errors and 5xx responses
- JSON serialization/deserialization
- Structured error conversion
Dependencies: reqwest, tokio, serde_json, core
When to use: Use for all HTTP API calls to LLM providers.
Purpose: Unified interface for multiple LLM providers.
Key Trait:
LLMProvider- Async trait withsend_message(&self, messages: &[Message]) -> Result<String>
Implementations:
OpenAIProvider- OpenAI API (GPT-3.5, GPT-4)AnthropicProvider- Anthropic API (Claude models)OllamaProvider- Local Ollama server (llama2, mistral, phi, etc.)
Factory:
create_provider(config)- Creates provider instance from configuration
Supported Providers:
- OpenAI: Cloud-based, requires API key, supports GPT models
- Anthropic: Cloud-based, requires API key, supports Claude models
- Ollama: Local execution, no API key needed, supports open-source models
Dependencies: async-trait, communication, config, core
When to use: Initialize at startup and use for all LLM interactions.
Purpose: Conversation storage with token-aware context management.
Key Trait:
MemoryStore- Trait for different storage backends
Implementations:
InMemoryStore- Vec-based storage for MVPConversationHistory- Wrapper with helper methods
Key Methods:
add_message(message)- Append to conversation historyget_recent(limit)- Retrieve last N messagesget_within_budget(tokens)- Token-aware retrievalclear()- Reset conversation
Dependencies: tiktoken-rs, core
When to use: Store all conversation turns and retrieve context for LLM calls.
Purpose: Extensible tool system for agent capabilities.
Key Trait:
Tool- Async trait withname(),description(),parameters_schema(),execute(params)
Registry:
ToolRegistry- HashMap-based tool storage and lookup
Built-in Tools:
Calculator- Arithmetic operations (add, subtract, multiply, divide)FileReader- Read file contents with error handlingWebSearchStub- Mock web search for demonstration
Dependencies: async-trait, serde_json, core
When to use: Register tools at startup; executor invokes them during plan execution.
Purpose: LLM-based task decomposition using ReAct pattern.
Key Types:
Plan- Sequence of steps with reasoningStep- Enum: ToolCall, Reasoning, ResponseToolCall- Structured tool invocation (name + parameters)Planner- Orchestrates plan generation
Key Methods:
create_plan(goal, tools)- Generate plan from user goalvalidate_plan(plan, registry)- Ensure all tools exist
Dependencies: llm, tools, memory, core
When to use: Convert user queries into executable plans before execution.
Purpose: Sequential execution of plans with tool invocation.
Key Types:
Executor- Stateful executor with tool registry and memoryExecutionResult- Outcome with success status and final responseStepResult- Individual step execution result
Key Methods:
execute_plan(plan)- Run all steps sequentiallyexecute_step(step)- Run single stephandle_tool_call(tool_call)- Invoke tool with parameters
Dependencies: planner, tools, memory, core
When to use: Execute validated plans after guardrail checks.
Purpose: Safety validation before plan execution.
Key Trait:
Guardrail- Trait withvalidate(plan) -> Result<()>
Registry:
GuardrailRegistry- Collection of active guardrails
Built-in Guardrails:
FilePathGuardrail- Restrict file operations to allowed directoriesRateLimitGuardrail- Enforce API call limits per minute
Dependencies: planner, core
When to use: Validate plans before execution to prevent unauthorized actions.
Purpose: Customize agent behavior through prompt modification.
Key Trait:
Rule- Trait withapply(context)andpriority()
Engine:
RuleEngine- Ordered collection of rules
Built-in Rules:
ResponseLengthRule- Limit response word countToneRule- Guide response style (Formal, Casual, Technical)
Dependencies: core
When to use: Apply rules before planning to modify LLM behavior.
Purpose: Command-line interface for agent interaction.
Modes:
- REPL Mode - Interactive conversation with history
- Single-Turn Mode - One query, one response
Features:
- Colored output (errors in red, success in green)
- Line editing with rustyline
- Conversation history display
- Verbose logging option
Dependencies: clap, rustyline, colored, all framework crates
When to use: Run as binary for testing and demonstration.
Purpose: Demonstrate framework usage patterns.
Examples:
- chatbot.rs - Basic conversation (LLM + memory only)
- research.rs - Tool-enabled agent (web search, file reading)
- file_manager.rs - Guardrail demonstration (safe file operations)
Configuration: Each example has a corresponding YAML file in examples/configs/
When to use: Study examples to learn framework patterns and best practices.
# Build all crates
cargo build --workspace
# Build in release mode
cargo build --release --workspace# Run all tests
cargo test --workspace
# Run tests for a specific crate
cargo test -p core
# Run integration tests
cargo test --test agent_flow# Generate and open documentation
cargo doc --open --no-deps
# Check documentation
cargo doc --workspace --no-deps# Run clippy for linting
cargo clippy --workspace -- -D warnings
# Format code
cargo fmt --all
# Check formatting
cargo fmt --all -- --checkContributions are welcome! Please see CONTRIBUTING.md for guidelines on:
- Adding new LLM providers
- Creating custom tools
- Implementing guardrails and rules
- Code style and testing requirements
This project is licensed under the MIT License - see the LICENSE file for details.
- Create a new module in
llm/src/your_provider/ - Define request/response types matching the provider's API
- Implement the
LLMProvidertrait - Add to the factory in
llm/src/factory.rs
Example:
// llm/src/my_provider/mod.rs
use async_trait::async_trait;
use crate::provider::LLMProvider;
pub struct MyProvider {
api_key: String,
client: ApiClient,
}
#[async_trait]
impl LLMProvider for MyProvider {
async fn send_message(&self, messages: &[Message]) -> Result<String> {
// Implementation
}
}- Create a struct for your tool
- Implement the
Tooltrait - Define parameter schema using JSON Schema
- Register with
ToolRegistry
Example:
use async_trait::async_trait;
use tools::Tool;
pub struct WeatherTool;
#[async_trait]
impl Tool for WeatherTool {
fn name(&self) -> &str { "weather" }
fn description(&self) -> &str {
"Get current weather for a location"
}
fn parameters_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"location": {"type": "string"}
},
"required": ["location"]
})
}
async fn execute(&self, params: Value) -> Result<Value> {
let location = params["location"].as_str().unwrap();
// Call weather API
Ok(json!({"temperature": 72, "condition": "sunny"}))
}
}- Create a struct for your guardrail
- Implement the
Guardrailtrait - Add validation logic in the
validatemethod - Register with
GuardrailRegistry
Example:
use guardrails::Guardrail;
pub struct TokenLimitGuardrail {
max_tokens: usize,
}
impl Guardrail for TokenLimitGuardrail {
fn name(&self) -> &str { "token_limit" }
fn validate(&self, plan: &Plan) -> Result<()> {
let total_tokens = estimate_plan_tokens(plan);
if total_tokens > self.max_tokens {
return Err(AgentError::GuardrailViolation(
format!("Plan exceeds token limit: {} > {}",
total_tokens, self.max_tokens)
));
}
Ok(())
}
}This framework was developed as part of a comprehensive blog series on building AI agents in Rust. Each day of development corresponds to a blog post that explains the concepts, design decisions, and implementation details.
Blog Series: Building AI Agents in Rust - A 14-Day Journey
Topics covered:
- Day 1-3: Foundation (workspace setup, configuration, HTTP communication)
- Day 4-7: Capabilities (LLM integration, memory, tools)
- Day 8-11: Intelligence (planning, execution, guardrails, rules)
- Day 12-14: Interface (CLI, examples, testing, documentation)
- The Rust Book - Essential Rust fundamentals
- Async Rust Book - Understanding async/await
- Rust API Guidelines - Best practices for API design
- Rust Design Patterns - Common patterns used in this framework
- ReAct Paper - Reasoning and Acting in Language Models (used in planner)
- Chain-of-Thought Paper - Prompting strategies for better reasoning
- OpenAI Function Calling - Tool integration patterns
- Anthropic Claude Docs - Claude API reference
- thiserror - Error handling patterns
- async-trait - Async trait support
- reqwest - HTTP client
- tiktoken-rs - Token counting
- clap - CLI argument parsing
# Verify your API key is set
echo $OPENAI_API_KEY
# If empty, set it
export OPENAI_API_KEY="sk-..."# Clean and rebuild
cargo clean
cargo build --workspace
# Update dependencies
cargo update# Run tests with output
cargo test -- --nocapture
# Run specific test
cargo test test_name -- --nocaptureIntegration tests that call real LLM APIs are marked with #[ignore]. Run them explicitly:
cargo test --test openai_integration -- --ignoredThis framework prioritizes clarity and learning over performance. For production use, consider:
- Async Runtime: Uses tokio with
fullfeatures; minimize features for smaller binaries - Token Counting: tiktoken-rs is OpenAI-specific; implement provider-specific counting
- Memory Storage: InMemoryStore is not persistent; implement file or database backends
- Error Handling: Detailed errors are helpful for debugging but may expose sensitive info
- Retry Logic: Fixed exponential backoff; consider adaptive strategies for production
This framework was built as an educational resource to demonstrate production-quality AI agent architecture in Rust. It prioritizes clarity and learning over performance optimization.
Special thanks to:
- The Rust community for excellent documentation and libraries
- OpenAI and Anthropic for accessible LLM APIs
- Contributors and learners who provide feedback and improvements