agents-core 0.0.10

Core traits, data models, and prompt primitives for building deep agents.
Documentation

Rust Deep Agents SDK

Crates.io Documentation License

High-performance Rust framework for composing reusable "deep" AI agents with custom tools, sub-agents, and prompts. This repository contains the SDK workspace, AWS integration helpers, documentation, and deployment scaffolding.

Workspace Layout

  • crates/agents-core – Domain traits, message structures, prompt packs, and state models.
  • crates/agents-runtime – Tokio-powered runtime glue between planners, tools, and state stores.
  • crates/agents-toolkit – Built-in tools (mock filesystem, todo management) and utilities.
  • crates/agents-aws – AWS adapters (Secrets Manager, DynamoDB, CloudWatch) behind feature flags.
  • examples/ – Reference agents; getting-started provides the echo smoke test.
    • agents-example-cli provides a local CLI harness using OpenAI.
  • deploy/ – Terraform modules and IaC assets for AWS environments.
  • docs/ – Roadmap, ADRs, playbooks, and reference material.

Installation

Add the unified SDK to your Cargo.toml:

# Simple installation (includes toolkit by default)
[dependencies]
agents-sdk = "0.0.1"

# Or choose specific features:
# agents-sdk = { version = "0.0.1", default-features = false }  # Core only
# agents-sdk = { version = "0.0.1", features = ["aws"] }       # With AWS
# agents-sdk = { version = "0.0.1", features = ["redis"] }     # With Redis persistence
# agents-sdk = { version = "0.0.1", features = ["postgres"] }  # With PostgreSQL persistence
# agents-sdk = { version = "0.0.1", features = ["dynamodb"] }  # With DynamoDB persistence
# agents-sdk = { version = "0.0.1", features = ["full"] }      # Everything

Individual Crates (Advanced)

If you prefer granular control, you can also use individual crates:

[dependencies]
agents-core = "0.0.1"      # Core traits and types
agents-runtime = "0.0.1"   # Agent runtime and builders
agents-toolkit = "0.0.1"   # Built-in tools (optional)
agents-aws = "0.0.1"       # AWS integrations (optional)

Quick Start

Basic Agent with Tools

use agents_sdk::{ConfigurableAgentBuilder, OpenAiConfig};
use agents_macros::tool;
use std::sync::Arc;

// Define a tool using the #[tool] macro - it's that simple!
#[tool("Adds two numbers together")]
fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Configure OpenAI
    let config = OpenAiConfig::new(
        std::env::var("OPENAI_API_KEY")?,
        "gpt-4o-mini"
    );

    // Build an agent with tools
    let agent = ConfigurableAgentBuilder::new("You are a helpful math assistant.")
        .with_openai_chat(config)?
        .with_tool(AddTool::as_tool())  // Tool name is auto-generated
        .build()?;

    // Use the agent
    use agents_sdk::state::AgentStateSnapshot;
    
    let response = agent.handle_message(
        "What is 5 + 3?",
        Arc::new(AgentStateSnapshot::default())
    ).await?;
    
    println!("{}", response.content.as_text().unwrap_or("No response"));

    Ok(())
}

Defining Tools

The #[tool] macro automatically generates the schema and wrapper code:

use agents_macros::tool;

// Simple tool
#[tool("Multiplies two numbers")]
fn multiply(a: f64, b: f64) -> f64 {
    a * b
}

// Async tool
#[tool("Fetches user data from API")]
async fn get_user(user_id: String) -> String {
    // Make API call...
    format!("User {}", user_id)
}

// Tool with optional parameters
#[tool("Searches with optional filters")]
fn search(query: String, max_results: Option<u32>) -> Vec<String> {
    let limit = max_results.unwrap_or(10);
    // Perform search...
    vec![]
}

// Use the tools:
let tools = vec![
    MultiplyTool::as_tool(),
    GetUserTool::as_tool(),
    SearchTool::as_tool(),
];

Using Persistence Backends

Choose the persistence layer that fits your infrastructure:

use agents_sdk::{ConfigurableAgentBuilder, InMemoryCheckpointer};
use std::sync::Arc;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // InMemory (default, no external dependencies)
    let checkpointer = Arc::new(InMemoryCheckpointer::new());
    
    // Redis (requires redis feature)
    #[cfg(feature = "redis")]
    let checkpointer = Arc::new(
        agents_sdk::RedisCheckpointer::new("redis://127.0.0.1:6379").await?
    );
    
    // PostgreSQL (requires postgres feature)
    #[cfg(feature = "postgres")]
    let checkpointer = Arc::new(
        agents_sdk::PostgresCheckpointer::new("postgresql://user:pass@localhost/agents").await?
    );
    
    // DynamoDB (requires dynamodb feature)
    #[cfg(feature = "dynamodb")]
    let checkpointer = Arc::new(
        agents_sdk::DynamoDbCheckpointer::new("agent-checkpoints").await?
    );

    let agent = ConfigurableAgentBuilder::new("You are a helpful assistant")
        .with_checkpointer(checkpointer)
        .build()?;

    // Save and load state across sessions
    let thread_id = "user-123";
    agent.save_state(thread_id).await?;
    agent.load_state(thread_id).await?;
    
    Ok(())
}

See examples/checkpointer-demo for a complete working example.

Human-in-the-Loop (HITL) Tool Approval

The HITL middleware allows you to require human approval before executing specific tools. This is essential for:

  • Critical Operations: Database modifications, file deletions, API calls with side effects
  • Security Review: Operations that access sensitive data or external systems
  • Cost Control: Expensive API calls or resource-intensive operations
  • Compliance: Operations requiring audit trails or manual oversight

Basic HITL Configuration

use agents_sdk::{ConfigurableAgentBuilder, HitlPolicy};
use std::collections::HashMap;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Define which tools require approval
    let mut tool_policies = HashMap::new();
    
    // Require approval for dangerous operations
    tool_policies.insert(
        "delete_file".to_string(),
        HitlPolicy {
            allow_auto: false,  // Requires human approval
            note: Some("File deletion requires security review".to_string()),
        }
    );
    
    // Allow automatic execution for safe operations
    tool_policies.insert(
        "read_file".to_string(),
        HitlPolicy {
            allow_auto: true,  // No approval needed
            note: None,
        }
    );

    let agent = ConfigurableAgentBuilder::new("You are a helpful assistant")
        .with_tool_interrupts(tool_policies)
        .with_checkpointer(checkpointer)  // Required for HITL!
        .build()?;

    Ok(())
}

Important: HITL requires a checkpointer to persist interrupt state. If no checkpointer is configured, HITL will be automatically disabled with a warning.

HITL Policy Structure

The HitlPolicy struct controls tool execution behavior:

pub struct HitlPolicy {
    /// If true, tool executes automatically without approval
    /// If false, execution pauses and waits for human response
    pub allow_auto: bool,
    
    /// Optional note explaining why approval is needed
    /// Shown to humans when reviewing the interrupt
    pub note: Option<String>,
}

Handling Interrupts

When a tool requires approval, the agent execution pauses and creates an interrupt:

use agents_sdk::{AgentMessage, MessageContent, MessageRole};
use std::sync::Arc;

// Agent encounters a tool requiring approval
let result = agent.handle_message(
    "Delete the old_data.txt file",
    Arc::new(AgentStateSnapshot::default())
).await;

// Execution pauses with an interrupt error
match result {
    Err(e) if e.to_string().contains("HITL interrupt") => {
        println!("Tool execution requires approval!");
        
        // Check the current interrupt
        if let Some(interrupt) = agent.current_interrupt().await? {
            match interrupt {
                AgentInterrupt::HumanInLoop(hitl) => {
                    println!("Tool: {}", hitl.tool_name);
                    println!("Args: {}", hitl.tool_args);
                    println!("Note: {:?}", hitl.policy_note);
                    println!("Call ID: {}", hitl.call_id);
                }
            }
        }
    }
    _ => {}
}

Responding to Interrupts

Use HitlAction to respond to interrupts:

use agents_sdk::HitlAction;

// 1. Accept - Execute with original arguments
agent.resume_with_approval(HitlAction::Accept).await?;

// 2. Edit - Execute with modified arguments
agent.resume_with_approval(HitlAction::Edit {
    tool_name: "delete_file".to_string(),
    tool_args: json!({"path": "/safe/path/file.txt"}),  // Modified path
}).await?;

// 3. Reject - Cancel execution with optional reason
agent.resume_with_approval(HitlAction::Reject {
    reason: Some("Operation not authorized".to_string()),
}).await?;

// 4. Respond - Provide custom message instead of executing
agent.resume_with_approval(HitlAction::Respond {
    message: AgentMessage {
        role: MessageRole::Agent,
        content: MessageContent::Text(
            "I cannot delete that file. Please use the archive tool instead.".to_string()
        ),
        metadata: None,
    },
}).await?;

Complete HITL Example

use agents_sdk::{
    ConfigurableAgentBuilder, HitlPolicy, HitlAction, AgentInterrupt,
    InMemoryCheckpointer, AgentMessage, MessageContent, MessageRole,
};
use std::collections::HashMap;
use std::sync::Arc;
use serde_json::json;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Configure HITL policies
    let mut policies = HashMap::new();
    policies.insert(
        "execute_command".to_string(),
        HitlPolicy {
            allow_auto: false,
            note: Some("Shell commands require security review".to_string()),
        }
    );

    // Build agent with HITL and checkpointer
    let checkpointer = Arc::new(InMemoryCheckpointer::new());
    let agent = ConfigurableAgentBuilder::new(
        "You are a system administrator assistant."
    )
        .with_tool_interrupts(policies)
        .with_checkpointer(checkpointer)
        .build()?;

    // User request triggers a tool requiring approval
    let result = agent.handle_message(
        "Run 'rm -rf /tmp/cache' to clear the cache",
        Arc::new(AgentStateSnapshot::default())
    ).await;

    // Handle the interrupt
    if result.is_err() {
        if let Some(interrupt) = agent.current_interrupt().await? {
            match interrupt {
                AgentInterrupt::HumanInLoop(hitl) => {
                    println!("⚠️  Approval Required");
                    println!("Tool: {}", hitl.tool_name);
                    println!("Command: {}", hitl.tool_args);
                    
                    // Human reviews and decides
                    let user_decision = get_user_approval(); // Your UI logic
                    
                    match user_decision {
                        "approve" => {
                            agent.resume_with_approval(HitlAction::Accept).await?;
                            println!("✅ Command executed");
                        }
                        "modify" => {
                            // Safer alternative
                            agent.resume_with_approval(HitlAction::Edit {
                                tool_name: "execute_command".to_string(),
                                tool_args: json!({"command": "rm -rf /tmp/cache/*.tmp"}),
                            }).await?;
                            println!("✅ Modified command executed");
                        }
                        "reject" => {
                            agent.resume_with_approval(HitlAction::Reject {
                                reason: Some("Too dangerous".to_string()),
                            }).await?;
                            println!("❌ Command rejected");
                        }
                        _ => {}
                    }
                }
            }
        }
    }

    Ok(())
}

fn get_user_approval() -> &'static str {
    // Your approval UI logic here
    "approve"
}

See examples/hitl-demo for a complete working example with OpenAI integration.

HITL Best Practices

  1. Always use a checkpointer: HITL requires state persistence to work correctly
  2. Provide clear policy notes: Help humans understand why approval is needed
  3. Handle all action types: Support Accept, Edit, Reject, and Respond in your UI
  4. Log interrupt decisions: Maintain audit trails for compliance
  5. Test interrupt scenarios: Verify your approval workflow handles edge cases
  6. Consider timeout policies: Decide how long to wait for human response
  7. Use appropriate granularity: Not every tool needs approval - focus on critical operations

Development Setup (From Source)

git clone https://github.com/yafatek/rust-deep-agents-sdk.git
cd rust-deep-agents-sdk

# Format, lint, and test
cargo fmt
cargo clippy --all-targets --all-features
cargo test --all

# Run examples
cargo run --example simple-agent
cargo run --example deep-research-agent
cargo run --example checkpointer-demo

Features

✅ Core Features (Python Parity Achieved)

Agent Builder API

  • ConfigurableAgentBuilder with fluent interface matching Python's API
  • .with_model() method supporting OpenAI, Anthropic, and Gemini models
  • .get_default_model() function returning pre-configured Claude Sonnet 4
  • Async and sync agent creation: create_deep_agent() and create_async_deep_agent()

Middleware Stack

  • Planning Middleware: Todo list management with comprehensive tool descriptions
  • Filesystem Middleware: Mock filesystem with ls, read_file, write_file, edit_file tools
  • SubAgent Middleware: Task delegation to specialized sub-agents
  • HITL (Human-in-the-Loop): Tool execution interrupts with approval policies
    • Configurable per-tool approval requirements
    • Support for Accept, Edit, Reject, and Respond actions
    • Automatic state persistence with checkpointer integration
    • Policy notes for human reviewers
  • Summarization Middleware: Context window management
  • AnthropicPromptCaching: Automatic prompt caching for efficiency

State Management

  • State Reducers: Smart merging functions matching Python's file_reducer behavior
  • Persistence: Checkpointer trait with multiple backend implementations
  • Thread Management: Save/load/delete agent conversation threads

Persistence Backends

  • InMemory: Built-in, zero-config persistence (development)
  • Redis: High-performance in-memory data store with optional durability
  • PostgreSQL: ACID-compliant relational database with full SQL support
  • DynamoDB: AWS-managed NoSQL database with auto-scaling

Provider Support

  • Anthropic: Claude models with prompt caching support
  • OpenAI: GPT models integration
  • Gemini: Google's Gemini Chat models

Built-in Tools

  • Todo Management: write_todos with detailed usage examples
  • File Operations: Full CRUD operations on mock filesystem
  • Task Delegation: task tool for spawning ephemeral sub-agents

🚧 Future Features (Planned)

Custom SubAgent Support

Enable users to define completely custom execution graphs beyond simple prompt/tool configurations:

// Future API design
let custom_subagent = CustomSubAgent {
    name: "data-processor".to_string(),
    description: "Processes complex data with custom logic".to_string(),
    graph: Box::new(MyCustomGraph::new()), // Custom execution graph
};

let agent = ConfigurableAgentBuilder::new("main instructions")
    .with_custom_subagent(custom_subagent)
    .build()?;

Benefits:

  • Full control over sub-agent execution flow
  • Custom state management within sub-agents
  • Complex branching and conditional logic
  • Integration with external systems and APIs

Dict-based Model Configuration

Allow models to be configured via dictionary/struct configs in addition to instances:

// Future API design
let agent = ConfigurableAgentBuilder::new("main instructions")
    .with_model_config(ModelConfig {
        provider: "anthropic".to_string(),
        model: "claude-sonnet-4".to_string(),
        max_tokens: 64000,
        temperature: 0.1,
        // ... other provider-specific options
    })
    .build()?;

Benefits:

  • Simplified configuration management
  • Easy serialization/deserialization of agent configs
  • Runtime model switching without code changes
  • Better integration with configuration management systems

Advanced State Features

  • Distributed State Stores: Redis, DynamoDB backends for multi-agent systems
  • State Migrations: Schema evolution support for long-running agents
  • State Encryption: Automatic encryption for sensitive data
  • Custom Reducers: User-defined state merging logic beyond built-in reducers

Enhanced Tool System

  • Dynamic Tool Registration: Runtime tool addition/removal
  • Tool Composition: Combining multiple tools into workflows
  • Tool Validation: Schema-based input/output validation
  • Tool Metrics: Performance and usage analytics

Support the Project

If you find this project helpful, consider supporting its development:

PayPal

Your support helps maintain and improve this open-source project. Thank you! ❤️

Next Steps

Follow the roadmap to implement planners, runtime orchestration, AWS integrations, and customer-ready templates.