Expand description
Type-safe state management for Iron Cage runtime
Provides unified agent state storage with multiple backend options and
compile-time type safety via iron_types::AgentId.
§Quick Start
use iron_runtime_state::{StateManager, AgentState, AgentStatus};
use iron_types::AgentId;
// Create state manager
let manager = StateManager::new();
// Generate type-safe agent ID
let agent_id = AgentId::generate();
// Save agent state
manager.save_agent_state(AgentState {
agent_id: agent_id.clone(),
status: AgentStatus::Running,
budget_spent: 5.0,
pii_detections: 0,
});
// Retrieve agent state
let state = manager.get_agent_state(agent_id.as_str())
.expect("Agent state should exist");
assert_eq!(state.agent_id, agent_id);
assert!(matches!(state.status, AgentStatus::Running));§Type Safety (v0.3.0)
All agent state uses type-safe AgentId for compile-time validation:
// ✅ Type-safe: AgentId validated at creation
let agent_id = AgentId::parse("agent_550e8400-e29b-41d4-a716-446655440000")?;
let state = AgentState {
agent_id, // Guaranteed valid format
status: AgentStatus::Running,
budget_spent: 0.0,
pii_detections: 0,
};
// ❌ Compile error: can't use raw strings
// let state = AgentState {
// agent_id: "invalid-format", // Type mismatch
// ...
// };§Storage Backends
§In-Memory (Default)
Fast concurrent access using DashMap:
use iron_runtime_state::StateManager;
// Default: in-memory only
let manager = StateManager::new();
// Thread-safe: DashMap allows concurrent access
// Multiple threads can read/write simultaneouslyPerformance:
- Read: O(1) average, lock-free for reads
- Write: O(1) average, fine-grained locking
- Thread-safe: Yes (concurrent HashMap)
§SQLite (Optional)
Enable with sqlite feature for persistent audit logs:
[dependencies]
iron_runtime_state = { version = "0.3", features = ["sqlite"] }use iron_runtime_state::StateManager;
// With SQLite persistence
let manager = StateManager::with_sqlite("state.db").await?;§Redis (Optional)
Enable with redis feature for distributed state:
[dependencies]
iron_runtime_state = { version = "0.3", features = ["redis"] }§Agent State Management
§State Lifecycle
let manager = StateManager::new();
let agent_id = AgentId::generate();
// 1. Agent starts
manager.save_agent_state(AgentState {
agent_id: agent_id.clone(),
status: AgentStatus::Running,
budget_spent: 0.0,
pii_detections: 0,
});
// 2. Agent executes (update metrics)
if let Some(mut state) = manager.get_agent_state(agent_id.as_str()) {
state.budget_spent += 2.5;
state.pii_detections += 1;
manager.save_agent_state(state);
}
// 3. Agent stops
if let Some(mut state) = manager.get_agent_state(agent_id.as_str()) {
state.status = AgentStatus::Stopped;
manager.save_agent_state(state);
}§Listing Agents
let manager = StateManager::new();
// Save multiple agents
for i in 0..3 {
let agent_id = AgentId::generate();
manager.save_agent_state(AgentState {
agent_id,
status: AgentStatus::Running,
budget_spent: 0.0,
pii_detections: 0,
});
}
// List all agent IDs
let agent_ids = manager.list_agents();
assert_eq!(agent_ids.len(), 3);§Audit Logging
Track security and compliance events:
let manager = StateManager::new();
let agent_id = AgentId::generate();
// Log PII detection
manager.save_audit_log(AuditEvent {
agent_id: agent_id.clone(),
event_type: "pii_detected".to_string(),
timestamp: 1234567890,
details: "Email address found in output".to_string(),
});
// Log budget threshold exceeded
manager.save_audit_log(AuditEvent {
agent_id,
event_type: "budget_exceeded".to_string(),
timestamp: 1234567900,
details: "Budget limit $10 exceeded".to_string(),
});Current Implementation:
- Events logged via
tracing::debug! - SQLite persistence planned (see TODO in implementation)
§Design Rationale
§Why Type-Safe AgentId?
Before (v0.2.0):
// ❌ String-based: validation required everywhere
pub struct AgentState {
pub agent_id: String, // Could be invalid format
...
}
fn get_state(agent_id: &str) -> Option<AgentState> {
// Manual validation needed
if !agent_id.starts_with("agent_") { return None; }
...
}After (v0.3.0):
// ✅ Type-safe: validation at creation only
pub struct AgentState {
pub agent_id: AgentId, // Guaranteed valid
pub status: AgentStatus,
pub budget_spent: f64,
pub pii_detections: usize,
}
fn get_state(agent_id: &str) -> Option<AgentState> {
// No validation needed: AgentId in AgentState is always valid
None
}Benefits:
- Validate Once: ID format checked at parse/generate time
- Type Safety: Compiler prevents invalid IDs
- Refactoring Safety: Format changes propagate via compiler
- Self-Documenting: Function signatures clearly show ID requirements
- Security: Prevents injection attacks and format confusion
§Why DashMap for Storage?
- Lock-Free Reads: Multiple readers don’t block each other
- Fine-Grained Locking: Writers only lock specific shards
- Production-Ready: Battle-tested in high-concurrency environments
- API Simplicity: Drop-in replacement for RwLock
§Why String Keys in DashMap?
// Internal storage uses String for HashMap efficiency
memory: Arc<DashMap<String, AgentState>>Rationale:
- DashMap requires
Hash + Eqkeys (AgentId is not Copy) - String keys avoid cloning AgentId on every lookup
- Conversion happens only at storage boundary
- Public API still type-safe (accepts
&str, storesAgentId)
§Performance Characteristics
| Operation | Complexity | Concurrency | Notes |
|---|---|---|---|
get_agent_state | O(1) avg | Lock-free | No blocking for reads |
save_agent_state | O(1) avg | Shard-locked | Only locks one shard |
list_agents | O(n) | Snapshot | Concurrent-safe iteration |
save_audit_log | O(1) | Lock-free | Tracing overhead only |
Memory Usage:
- Per-agent overhead: ~200 bytes (AgentState + DashMap entry)
- 1M agents: ~200 MB memory
Scalability:
- DashMap sharding: 64 shards by default
- Concurrent readers: Unlimited (lock-free)
- Concurrent writers: Up to 64 (one per shard)
§Thread Safety
All operations are thread-safe and lock-free for reads:
use iron_runtime_state::{StateManager, AgentState, AgentStatus};
use iron_types::AgentId;
use std::sync::Arc;
use std::thread;
let manager = Arc::new(StateManager::new());
let agent_id = AgentId::generate();
// Initial state
manager.save_agent_state(AgentState {
agent_id: agent_id.clone(),
status: AgentStatus::Running,
budget_spent: 0.0,
pii_detections: 0,
});
// Multiple threads can read/write concurrently
let handles: Vec<_> = (0..10).map(|i| {
let manager = Arc::clone(&manager);
let agent_id = agent_id.clone();
thread::spawn(move || {
// Concurrent read
if let Some(mut state) = manager.get_agent_state(agent_id.as_str()) {
// Update metrics
state.budget_spent += i as f64;
manager.save_agent_state(state);
}
})
}).collect();
for handle in handles {
handle.join().unwrap();
}§Feature Flags
| Feature | Default | Description |
|---|---|---|
enabled | ✅ Yes | Full state management implementation |
sqlite | ❌ No | SQLite persistence for audit logs |
redis | ❌ No | Redis backend for distributed state |
full | ❌ No | Enables all features (enabled + sqlite + redis) |
Usage:
# Default: in-memory only
iron_runtime_state = "0.3"
# With SQLite persistence
iron_runtime_state = { version = "0.3", features = ["sqlite"] }
# With all backends
iron_runtime_state = { version = "0.3", features = ["full"] }§Migration from v0.2.0
v0.2.0 (String-based):
let state = AgentState {
agent_id: "agent_550e8400-e29b-41d4-a716-446655440000".to_string(),
...
};v0.3.0 (Type-safe):
let state = AgentState {
agent_id: AgentId::parse("agent_550e8400-e29b-41d4-a716-446655440000")?,
status: AgentStatus::Running,
budget_spent: 0.0,
pii_detections: 0,
};Compiler-Guided Migration:
- Type errors at every String → AgentId usage
- No runtime surprises
- Incremental migration possible
§Production Considerations
§Memory Management
StateManager keeps all agent states in memory. For long-running systems:
// Remove stopped agents periodically
for agent_id in manager.list_agents() {
if let Some(state) = manager.get_agent_state(&agent_id) {
if matches!(state.status, AgentStatus::Stopped) {
// TODO: Add remove_agent_state() method
// manager.remove_agent_state(&agent_id);
}
}
}§Error Handling
All operations are infallible for in-memory backend:
get_agent_state: ReturnsOption<AgentState>save_agent_state: Always succeedslist_agents: Always succeeds
SQLite/Redis backends may introduce Result types in future.
§Monitoring
Use list_agents() for metrics:
let total_agents = manager.list_agents().len();
let mut running = 0;
let mut stopped = 0;
for agent_id in manager.list_agents() {
if let Some(state) = manager.get_agent_state(&agent_id) {
match state.status {
AgentStatus::Running => running += 1,
AgentStatus::Stopped => stopped += 1,
AgentStatus::Failed => {},
}
}
}
println!("Agents: {} total, {} running, {} stopped", total_agents, running, stopped);§See Also
iron_types::AgentId- Type-safe agent identifiers- [
iron_runtime] - Agent lifecycle management - [
iron_telemetry] - Event logging and monitoring
Features #25: State Management
Structs§
- Agent
State - Agent state stored in memory
- Audit
Event - Audit log event
- State
Manager - State manager with multiple backends
Enums§
- Agent
Status - Agent execution status