Crate iron_runtime_state

Crate iron_runtime_state 

Source
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 simultaneously

Performance:

  • 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:

  1. Validate Once: ID format checked at parse/generate time
  2. Type Safety: Compiler prevents invalid IDs
  3. Refactoring Safety: Format changes propagate via compiler
  4. Self-Documenting: Function signatures clearly show ID requirements
  5. Security: Prevents injection attacks and format confusion

§Why DashMap for Storage?

  1. Lock-Free Reads: Multiple readers don’t block each other
  2. Fine-Grained Locking: Writers only lock specific shards
  3. Production-Ready: Battle-tested in high-concurrency environments
  4. 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 + Eq keys (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, stores AgentId)

§Performance Characteristics

OperationComplexityConcurrencyNotes
get_agent_stateO(1) avgLock-freeNo blocking for reads
save_agent_stateO(1) avgShard-lockedOnly locks one shard
list_agentsO(n)SnapshotConcurrent-safe iteration
save_audit_logO(1)Lock-freeTracing 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

FeatureDefaultDescription
enabled✅ YesFull state management implementation
sqlite❌ NoSQLite persistence for audit logs
redis❌ NoRedis backend for distributed state
full❌ NoEnables 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: Returns Option<AgentState>
  • save_agent_state: Always succeeds
  • list_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§

AgentState
Agent state stored in memory
AuditEvent
Audit log event
StateManager
State manager with multiple backends

Enums§

AgentStatus
Agent execution status