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