Vapora/crates/vapora-agents/src/runtime/state_machine.rs
Jesús Pérez 765841b18f
Some checks failed
Documentation Lint & Validation / Markdown Linting (push) Has been cancelled
Documentation Lint & Validation / Validate mdBook Configuration (push) Has been cancelled
Documentation Lint & Validation / Content & Structure Validation (push) Has been cancelled
Documentation Lint & Validation / Lint & Validation Summary (push) Has been cancelled
mdBook Build & Deploy / Build mdBook (push) Has been cancelled
mdBook Build & Deploy / Documentation Quality Check (push) Has been cancelled
mdBook Build & Deploy / Deploy to GitHub Pages (push) Has been cancelled
mdBook Build & Deploy / Notification (push) Has been cancelled
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
feat(capabilities): add vapora-capabilities crate with in-process executor dispatch
- New vapora-capabilities crate: CapabilitySpec, Capability trait, CapabilityRegistry
     (parking_lot RwLock), CapabilityLoader (TOML overrides), 3 built-ins
     (code-reviewer, doc-generator, pr-monitor), 22 tests
   - Move AgentDefinition to vapora-shared to break capabilities↔agents circular dep
   - Wire system_prompt into AgentExecutor via LLMRouter.complete_with_budget
   - AgentCoordinator: in-process task dispatch via DashMap<String, Sender<TaskAssignment>>
   - server.rs: bootstrap CapabilityRegistry + LLMRouter from env, spawn executors per capability
   - Landing page: 620 tests, 21 crates, Capability Packages feature box
   - docs: capability-packages feature guide, ADR-0037, CHANGELOG, SUMMARY
   EOF
2026-02-26 16:43:28 +00:00

242 lines
6.4 KiB
Rust

// Type-state machine for agent lifecycle
// Ensures safe state transitions at compile time
use std::marker::PhantomData;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::messages::TaskAssignment;
use crate::registry::AgentMetadata;
/// Agent states - compile-time enforced state machine
/// Initial state: Agent is idle
pub struct Idle;
/// Task assigned state
pub struct Assigned {
pub task: TaskAssignment,
}
/// Executing state
pub struct Executing {
pub task: TaskAssignment,
pub started_at: DateTime<Utc>,
}
/// Completed state
pub struct Completed;
/// Failed state
pub struct Failed {
pub error: String,
}
/// Execution result containing outcome data
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ExecutionResult {
pub output: String,
pub input_tokens: u64,
pub output_tokens: u64,
pub duration_ms: u64,
}
/// Agent with compile-time state tracking
pub struct Agent<S> {
pub metadata: AgentMetadata,
state: PhantomData<S>,
result: Option<ExecutionResult>,
}
/// Transitions from Idle state
impl Agent<Idle> {
/// Create new idle agent
pub fn new(metadata: AgentMetadata) -> Self {
Self {
metadata,
state: PhantomData,
result: None,
}
}
/// Transition to Assigned state
pub fn assign_task(self, _task: TaskAssignment) -> Agent<Assigned> {
Agent {
metadata: self.metadata,
state: PhantomData,
result: None,
}
}
}
/// Transitions from Assigned state
impl Agent<Assigned> {
/// Transition to Executing state
pub fn start_execution(self) -> Agent<Executing> {
Agent {
metadata: self.metadata,
state: PhantomData,
result: None,
}
}
}
/// Transitions from Executing state
impl Agent<Executing> {
/// Complete execution successfully
pub fn complete(self, result: ExecutionResult) -> Agent<Completed> {
Agent {
metadata: self.metadata,
state: PhantomData,
result: Some(result),
}
}
/// Fail execution
pub fn fail(self, _error: String) -> Agent<Failed> {
Agent {
metadata: self.metadata,
state: PhantomData,
result: None,
}
}
}
/// Transitions from Completed state
impl Agent<Completed> {
/// Get execution result
pub fn result(&self) -> Option<&ExecutionResult> {
self.result.as_ref()
}
/// Transition back to Idle
pub fn reset(self) -> Agent<Idle> {
Agent {
metadata: self.metadata,
state: PhantomData,
result: None,
}
}
}
/// Transitions from Failed state
impl Agent<Failed> {
/// Get error message
pub fn error(&self) -> String {
match &self.result {
Some(result) => format!("Error: {}", result.output),
None => "Unknown error".to_string(),
}
}
/// Transition back to Idle
pub fn reset(self) -> Agent<Idle> {
Agent {
metadata: self.metadata,
state: PhantomData,
result: None,
}
}
}
#[cfg(test)]
mod tests {
use chrono::Utc;
use super::*;
#[test]
fn test_type_state_transitions() {
// Create metadata for testing
let metadata = AgentMetadata {
id: "test-agent".to_string(),
role: "developer".to_string(),
name: "Test Developer".to_string(),
version: "0.1.0".to_string(),
status: crate::registry::AgentStatus::Active,
capabilities: vec!["coding".to_string()],
llm_provider: "claude".to_string(),
llm_model: "claude-sonnet-4".to_string(),
max_concurrent_tasks: 5,
current_tasks: 0,
created_at: Utc::now(),
last_heartbeat: Utc::now(),
uptime_percentage: 100.0,
total_tasks_completed: 0,
system_prompt: None,
};
// Type-state chain: Idle → Assigned → Executing → Completed → Idle
let agent = Agent::new(metadata.clone());
let task = TaskAssignment {
id: "task-1".to_string(),
agent_id: "test-agent".to_string(),
required_role: "developer".to_string(),
title: "Test task".to_string(),
description: "Test description".to_string(),
context: "{}".to_string(),
priority: 1,
deadline: None,
assigned_at: Utc::now(),
};
let agent = agent.assign_task(task);
let agent = agent.start_execution();
let result = ExecutionResult {
output: "Success".to_string(),
input_tokens: 100,
output_tokens: 50,
duration_ms: 1000,
};
let agent = agent.complete(result);
assert!(agent.result().is_some());
let _agent = agent.reset();
// agent is now back to Idle state - type system ensures this
}
#[test]
fn test_failed_state_transition() {
let metadata = AgentMetadata {
id: "test-agent".to_string(),
role: "developer".to_string(),
name: "Test Developer".to_string(),
version: "0.1.0".to_string(),
status: crate::registry::AgentStatus::Active,
capabilities: vec!["coding".to_string()],
llm_provider: "claude".to_string(),
llm_model: "claude-sonnet-4".to_string(),
max_concurrent_tasks: 5,
current_tasks: 0,
created_at: Utc::now(),
last_heartbeat: Utc::now(),
uptime_percentage: 100.0,
total_tasks_completed: 0,
system_prompt: None,
};
let agent = Agent::new(metadata);
let task = TaskAssignment {
id: "task-1".to_string(),
agent_id: "test-agent".to_string(),
required_role: "developer".to_string(),
title: "Test task".to_string(),
description: "Test description".to_string(),
context: "{}".to_string(),
priority: 1,
deadline: None,
assigned_at: Utc::now(),
};
let agent = agent.assign_task(task);
let agent = agent.start_execution();
let agent = agent.fail("API timeout".to_string());
let _error = agent.error();
let _agent = agent.reset();
// agent is now back to Idle state
}
}