Skip to main content

vex_agent/
lib.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4use vex_domain::{AgentId, AgentState, AgentType, ShellId, WorkstreamId};
5
6/// An AI agent running in a shell.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Agent {
9    pub id: AgentId,
10    pub workstream_id: WorkstreamId,
11    pub agent_type: AgentType,
12    pub state: AgentState,
13    pub shell_id: ShellId,
14    pub spawned_at: DateTime<Utc>,
15    pub last_activity: DateTime<Utc>,
16}
17
18/// Events emitted by the agent monitoring loop.
19#[derive(Debug, Clone)]
20pub enum AgentEvent {
21    StateChanged {
22        agent_id: AgentId,
23        old: AgentState,
24        new: AgentState,
25    },
26    TaskCompleted {
27        agent_id: AgentId,
28    },
29    BellDetected {
30        agent_id: AgentId,
31    },
32}
33
34#[derive(Debug, Error)]
35pub enum AgentError {
36    #[error("agent not found: {0}")]
37    NotFound(String),
38    #[error("spawn failed: {0}")]
39    SpawnFailed(String),
40    #[error("shell error: {0}")]
41    ShellError(String),
42    #[error("invalid state transition: {from:?} -> {to:?}")]
43    InvalidTransition { from: AgentState, to: AgentState },
44    #[error("JSONL parse error: {0}")]
45    JsonlError(String),
46    #[error("session file not found: {0}")]
47    SessionNotFound(String),
48    #[error("IO error: {0}")]
49    Io(#[from] std::io::Error),
50}
51
52/// Port trait for detecting agent state from external signals (JSONL, bell, process liveness).
53pub trait AgentStateDetector: Send + Sync {
54    fn detect_state(&self, agent: &Agent) -> Result<AgentState, AgentError>;
55}
56
57/// Validates agent state transitions. Returns Ok(()) if valid.
58pub fn validate_agent_transition(from: AgentState, to: AgentState) -> Result<(), AgentError> {
59    let valid = matches!(
60        (from, to),
61        (AgentState::Idle, AgentState::Running)
62            | (AgentState::Running, AgentState::Idle)
63            | (AgentState::Running, AgentState::NeedsIntervention)
64            | (AgentState::NeedsIntervention, AgentState::Running)
65            | (AgentState::NeedsIntervention, AgentState::Idle)
66    );
67    if valid {
68        Ok(())
69    } else {
70        Err(AgentError::InvalidTransition { from, to })
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn valid_agent_transitions() {
80        use AgentState::*;
81        assert!(validate_agent_transition(Idle, Running).is_ok());
82        assert!(validate_agent_transition(Running, Idle).is_ok());
83        assert!(validate_agent_transition(Running, NeedsIntervention).is_ok());
84        assert!(validate_agent_transition(NeedsIntervention, Running).is_ok());
85        assert!(validate_agent_transition(NeedsIntervention, Idle).is_ok());
86    }
87
88    #[test]
89    fn invalid_agent_transitions() {
90        use AgentState::*;
91        assert!(validate_agent_transition(Idle, NeedsIntervention).is_err());
92        assert!(validate_agent_transition(Idle, Idle).is_err());
93        assert!(validate_agent_transition(Running, Running).is_err());
94    }
95
96    #[test]
97    fn agent_error_display() {
98        let err = AgentError::NotFound("agent-1".into());
99        assert_eq!(err.to_string(), "agent not found: agent-1");
100    }
101
102    #[test]
103    fn agent_event_variants() {
104        let evt = AgentEvent::StateChanged {
105            agent_id: AgentId("a1".into()),
106            old: AgentState::Idle,
107            new: AgentState::Running,
108        };
109        // Just ensure it compiles and Debug works
110        let _s = format!("{evt:?}");
111
112        let evt2 = AgentEvent::BellDetected {
113            agent_id: AgentId("a1".into()),
114        };
115        let _s = format!("{evt2:?}");
116    }
117}