1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4use vex_domain::{AgentId, AgentState, AgentType, ShellId, WorkstreamId};
5
6#[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#[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
52pub trait AgentStateDetector: Send + Sync {
54 fn detect_state(&self, agent: &Agent) -> Result<AgentState, AgentError>;
55}
56
57pub 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 let _s = format!("{evt:?}");
111
112 let evt2 = AgentEvent::BellDetected {
113 agent_id: AgentId("a1".into()),
114 };
115 let _s = format!("{evt2:?}");
116 }
117}