This guide is for developers who want to contribute to SimpleAgents or understand its internals.
SimpleAgents is a Rust workspace with multiple crates:
SimpleAgents/
├── crates/
│ ├── simple-agents-types/ # Core types and traits
│ │ ├── src/
│ │ │ ├── lib.rs # Crate root
│ │ │ ├── cache.rs # Cache trait
│ │ │ ├── coercion.rs # Response coercion
│ │ │ ├── config.rs # Configuration types
│ │ │ ├── error.rs # Error types
│ │ │ ├── message.rs # Message types
│ │ │ ├── provider.rs # Provider trait
│ │ │ ├── request.rs # Request types
│ │ │ ├── response.rs # Response types
│ │ │ ├── router.rs # Routing types
│ │ │ └── validation.rs # Validation (ApiKey, etc.)
│ │ └── Cargo.toml
│ │
│ ├── simple-agents-providers/ # Provider implementations
│ │ ├── src/
│ │ │ ├── lib.rs # Crate root
│ │ │ ├── openai/ # OpenAI provider
│ │ │ │ ├── mod.rs # Provider implementation
│ │ │ │ ├── models.rs # Request/response models
│ │ │ │ └── error.rs # Error mapping
│ │ │ ├── anthropic/ # Anthropic provider (stub)
│ │ │ ├── retry.rs # Retry logic
│ │ │ └── utils.rs # Shared utilities
│ │ └── Cargo.toml
│ │
│ └── simple-agents-cache/ # Cache implementations
│ ├── src/
│ │ ├── lib.rs # Crate root
│ │ ├── memory.rs # In-memory cache
│ │ └── noop.rs # No-op cache
│ └── Cargo.toml
│
├── docs/ # Documentation
├── OPTIMISATION.md # Performance tracking
├── Cargo.toml # Workspace manifest
└── README.md # Project README
simple-agents-types
simple-agents-providers
Provider trait for different APIssimple-agents-cache
Cache traitcargo build --all
cargo build --all --release
cargo build -p simple-agents-types
cargo build -p simple-agents-providers
cargo build -p simple-agents-cache
cargo check --all
cargo test --all
Current Test Count: 132+ tests
cargo test -p simple-agents-types
cargo test -p simple-agents-providers
cargo test -p simple-agents-cache
cargo test test_api_key_constant_time_comparison
cargo test -- --nocapture
Integration tests that require API keys or local servers are ignored by default:
cargo test -- --ignored
cargo test --doc
We use various test types:
#[cfg(test)] mod tests)tests/ directory#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_functionality() {
// Arrange
let input = "test";
// Act
let result = function_under_test(input);
// Assert
assert_eq!(result, expected);
}
#[tokio::test]
async fn test_async_functionality() {
let result = async_function().await;
assert!(result.is_ok());
}
}
git checkout -b feature/my-featurecargo test --allcargo clippy --allcargo fmt --allgit commit -m "Add my feature"git push origin feature/my-featureUse conventional commits:
feat: add streaming support for OpenAI
fix: correct cache key generation
docs: update usage guide
test: add tests for retry logic
refactor: simplify error handling
perf: optimize message cloning
cargo test --all)cargo clippy --all)cargo fmt --all)Use rustfmt with default settings:
cargo fmt --all
Use clippy with strict settings:
cargo clippy --all -- -D warnings
/// Calculate the factorial of a number.
///
/// # Arguments
///
/// * `n` - The number to calculate factorial for
///
/// # Returns
///
/// The factorial of `n`
///
/// # Examples
///
/// ```
/// let result = factorial(5);
/// assert_eq!(result, 120);
/// ```
///
/// # Panics
///
/// Panics if `n > 20` (would overflow)
pub fn factorial(n: u64) -> u64 {
// Implementation
}
PascalCasesnake_caseSCREAMING_SNAKE_CASEsnake_caseResult for recoverable errorspanic! only for programming errorsthiserror for error typesuse thiserror::Error;
#[derive(Debug, Error)]
pub enum MyError {
#[error("validation failed: {0}")]
Validation(String),
#[error("network error: {0}")]
Network(#[from] std::io::Error),
}
NEVER:
ALWAYS:
ApiKey type for all keys.expose() only when needed// Good
let key = ApiKey::new(env::var("API_KEY")?)?;
let header = format!("Bearer {}", key.expose());
// Bad
let key = env::var("API_KEY")?;
println!("Key: {}", key); // NEVER DO THIS
All user input must be validated:
// Validate before use
let request = CompletionRequest::builder()
.model("gpt-4")
.message(Message::user(user_input))
.build()?; // Validates automatically
Use constant-time operations for security-sensitive comparisons:
use subtle::ConstantTimeEq;
// Good: constant-time
self.0.as_bytes().ct_eq(other.0.as_bytes()).into()
// Bad: timing attack vulnerable
self.0 == other.0
Use cryptographically secure RNG:
use rand::Rng;
// Good
rand::thread_rng().gen()
// Bad
SystemTime::now() // Predictable!
Prefer borrowing over cloning:
// Good: borrows data
pub struct Request<'a> {
pub messages: &'a [Message],
}
// Bad: clones data
pub struct Request {
pub messages: Vec<Message>,
}
Use Cow for strings that are often static:
use std::borrow::Cow;
// Can be static (zero allocation) or owned
pub headers: Vec<(Cow<'static, str>, Cow<'static, str>)>
// Usage
headers.push((Cow::Borrowed("Content-Type"), Cow::Borrowed("application/json")));
Configure HTTP clients for connection pooling:
Client::builder()
.pool_max_idle_per_host(10)
.pool_idle_timeout(Duration::from_secs(90))
.http2_prior_knowledge()
.build()?
Implement caching for expensive operations:
// Cache key generation (fast)
let key = CacheKey::from_parts(provider, model, content);
// Check cache before API call
if let Some(cached) = cache.get(&key).await? {
return Ok(cached);
}
Use profiling tools to identify bottlenecks:
# CPU profiling with flamegraph
cargo install flamegraph
cargo flamegraph --bin your_binary
# Memory profiling with valgrind
cargo build --release
valgrind --tool=massif target/release/your_binary
mkdir -p crates/simple-agents-providers/src/newprovider
touch crates/simple-agents-providers/src/newprovider/mod.rs
touch crates/simple-agents-providers/src/newprovider/models.rs
touch crates/simple-agents-providers/src/newprovider/error.rs
// models.rs
use serde::{Deserialize, Serialize};
use simple_agents_types::message::Message;
#[derive(Debug, Serialize)]
pub struct NewProviderRequest<'a> {
pub model: &'a str,
pub messages: &'a [Message],
// ... provider-specific fields
}
#[derive(Debug, Deserialize)]
pub struct NewProviderResponse {
pub id: String,
pub choices: Vec<Choice>,
// ... provider-specific fields
}
// error.rs
use simple_agents_types::error::ProviderError;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum NewProviderError {
#[error("invalid API key")]
InvalidApiKey,
#[error("rate limit exceeded")]
RateLimit,
// ... other errors
}
impl From<NewProviderError> for ProviderError {
fn from(err: NewProviderError) -> Self {
match err {
NewProviderError::InvalidApiKey => {
ProviderError::Authentication("Invalid API key".to_string())
}
NewProviderError::RateLimit => {
ProviderError::RateLimit {
retry_after: None,
message: "Rate limit exceeded".to_string(),
}
}
// ... other mappings
}
}
}
// mod.rs
use async_trait::async_trait;
use simple_agents_types::prelude::*;
pub struct NewProvider {
api_key: ApiKey,
client: reqwest::Client,
}
impl NewProvider {
pub fn new(api_key: ApiKey) -> Result<Self> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.build()?;
Ok(Self { api_key, client })
}
}
#[async_trait]
impl Provider for NewProvider {
fn name(&self) -> &str {
"newprovider"
}
fn transform_request(&self, req: &CompletionRequest) -> Result<ProviderRequest> {
// Transform to provider format
let provider_req = NewProviderRequest {
model: &req.model,
messages: &req.messages,
};
let body = serde_json::to_value(&provider_req)?;
Ok(ProviderRequest {
url: "https://api.newprovider.com/v1/chat".to_string(),
headers: vec![
(Cow::Borrowed("Authorization"),
Cow::Owned(format!("Bearer {}", self.api_key.expose()))),
],
body,
timeout: None,
})
}
async fn execute(&self, req: ProviderRequest) -> Result<ProviderResponse> {
// Make HTTP request
let response = self.client
.post(&req.url)
.json(&req.body)
.send()
.await?;
// Handle errors
if !response.status().is_success() {
// Parse and map errors
}
// Parse response
let body = response.json().await?;
Ok(ProviderResponse {
status: 200,
body,
headers: None,
})
}
fn transform_response(&self, resp: ProviderResponse) -> Result<CompletionResponse> {
// Transform from provider format
let provider_resp: NewProviderResponse = serde_json::from_value(resp.body)?;
// Map to unified format
Ok(CompletionResponse {
id: provider_resp.id,
model: "model-name".to_string(),
choices: vec![/* ... */],
usage: Usage::new(0, 0),
created: None,
provider: Some(self.name().to_string()),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_provider_creation() {
let key = ApiKey::new("test-key-1234567890").unwrap();
let provider = NewProvider::new(key).unwrap();
assert_eq!(provider.name(), "newprovider");
}
#[tokio::test]
async fn test_request_transformation() {
// Test transform_request
}
// Add more tests...
}
simple-agents-providers/src/lib.rsdocs/USAGE.md with examplesuse tracing::info;
use tracing_subscriber;
// In your binary
tracing_subscriber::fmt::init();
// In library code
tracing::info!("Processing request for model: {}", model);
tracing::debug!("Request details: {:?}", request);
RUST_LOG=debug cargo run
RUST_LOG=simple_agents=trace cargo test
#[test]
fn test_something() {
let _ = env_logger::builder()
.is_test(true)
.try_init();
// Your test code
}
Cargo.toml filescargo test --allcargo build --all --releasegit tag v0.x.0git push --tagscargo publish -p simple-agents-types, etc.