Skip to main content

oxios_kernel/
types.rs

1//! Core types for the Oxios kernel.
2//!
3//! Defines agent identity, status, and metadata.
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8/// Unique identifier for an agent instance.
9pub type AgentId = uuid::Uuid;
10
11/// Current status of an agent instance.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13pub enum AgentStatus {
14    /// Agent is being initialized.
15    Starting,
16    /// Agent is actively executing tasks.
17    Running,
18    /// Agent is alive but not currently working.
19    Idle,
20    /// Agent has been stopped.
21    Stopped,
22    /// Agent has encountered an error.
23    Failed,
24    /// Agent finished execution successfully.
25    Completed,
26}
27
28impl std::fmt::Display for AgentStatus {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        match self {
31            AgentStatus::Starting => write!(f, "starting"),
32            AgentStatus::Running => write!(f, "running"),
33            AgentStatus::Idle => write!(f, "idle"),
34            AgentStatus::Stopped => write!(f, "stopped"),
35            AgentStatus::Failed => write!(f, "failed"),
36            AgentStatus::Completed => write!(f, "completed"),
37        }
38    }
39}
40
41/// Metadata about an agent instance.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct AgentInfo {
44    /// Unique identifier for this agent.
45    pub id: AgentId,
46    /// Human-readable name of the agent.
47    pub name: String,
48    /// Current status of the agent.
49    pub status: AgentStatus,
50    /// Timestamp when the agent was created.
51    pub created_at: DateTime<Utc>,
52    /// The seed this agent was forked from, if any.
53    pub seed_id: Option<uuid::Uuid>,
54    /// Project ID detected by the orchestrator.
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub project_id: Option<uuid::Uuid>,
57    /// Timestamp when execution started.
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub started_at: Option<DateTime<Utc>>,
60    /// Timestamp when execution completed.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub completed_at: Option<DateTime<Utc>>,
63    /// Error message if the agent failed.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub error: Option<String>,
66    /// Number of tool call steps completed.
67    #[serde(default)]
68    pub steps_completed: usize,
69    /// Number of total steps (if known).
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub steps_total: Option<usize>,
72    /// Tool calls recorded during execution.
73    #[serde(default, skip_serializing_if = "Vec::is_empty")]
74    pub tool_calls: Vec<ToolCallRecord>,
75    /// Total input tokens consumed.
76    #[serde(default)]
77    pub tokens_input: u64,
78    /// Total output tokens generated.
79    #[serde(default)]
80    pub tokens_output: u64,
81    /// Estimated cost in USD.
82    #[serde(default)]
83    pub cost_usd: f64,
84    /// Model ID used for execution.
85    #[serde(default, skip_serializing_if = "String::is_empty")]
86    pub model_id: String,
87    /// Session ID that spawned this agent (if any).
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub session_id: Option<String>,
90}
91
92/// Record of a single tool call during agent execution.
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct ToolCallRecord {
95    /// Tool name (e.g. "read", "bash", "grep").
96    pub tool: String,
97    /// Input parameters or invocation summary.
98    pub input: String,
99    /// Output or result summary.
100    pub output: String,
101    /// Duration of the tool call in milliseconds.
102    pub duration_ms: u64,
103    /// Whether the tool call returned an error.
104    #[serde(default)]
105    pub is_error: bool,
106    /// Provider-specific tool call ID.
107    #[serde(default, skip_serializing_if = "String::is_empty")]
108    pub tool_call_id: String,
109    /// Timestamp when the tool call started (UTC).
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub timestamp: Option<DateTime<Utc>>,
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn test_agent_status_display_all_variants() {
120        assert_eq!(AgentStatus::Starting.to_string(), "starting");
121        assert_eq!(AgentStatus::Running.to_string(), "running");
122        assert_eq!(AgentStatus::Idle.to_string(), "idle");
123        assert_eq!(AgentStatus::Stopped.to_string(), "stopped");
124        assert_eq!(AgentStatus::Failed.to_string(), "failed");
125    }
126
127    #[test]
128    fn test_agent_status_equality() {
129        assert_eq!(AgentStatus::Running, AgentStatus::Running);
130        assert_ne!(AgentStatus::Running, AgentStatus::Idle);
131    }
132
133    #[test]
134    fn test_agent_status_serialization_roundtrip() {
135        for status in [
136            AgentStatus::Starting,
137            AgentStatus::Running,
138            AgentStatus::Idle,
139            AgentStatus::Stopped,
140            AgentStatus::Failed,
141        ] {
142            let json = serde_json::to_string(&status).unwrap();
143            let restored: AgentStatus = serde_json::from_str(&json).unwrap();
144            assert_eq!(status, restored);
145        }
146    }
147
148    #[test]
149    fn test_agent_info_construction() {
150        let id = AgentId::new_v4();
151        let seed_id = uuid::Uuid::new_v4();
152        let now = Utc::now();
153
154        let info = AgentInfo {
155            id,
156            name: "test-agent".to_string(),
157            status: AgentStatus::Running,
158            created_at: now,
159            seed_id: Some(seed_id),
160            project_id: None,
161            started_at: None,
162            completed_at: None,
163            error: None,
164            steps_completed: 0,
165            steps_total: None,
166            tool_calls: vec![],
167            tokens_input: 0,
168            tokens_output: 0,
169            cost_usd: 0.0,
170            model_id: String::new(),
171            session_id: None,
172        };
173
174        assert_eq!(info.id, id);
175        assert_eq!(info.name, "test-agent");
176        assert_eq!(info.status, AgentStatus::Running);
177        assert_eq!(info.created_at, now);
178        assert_eq!(info.seed_id, Some(seed_id));
179    }
180
181    #[test]
182    fn test_agent_info_serialization_roundtrip() {
183        let info = AgentInfo {
184            id: AgentId::new_v4(),
185            name: "serializer".to_string(),
186            status: AgentStatus::Idle,
187            created_at: Utc::now(),
188            seed_id: None,
189            project_id: None,
190            started_at: None,
191            completed_at: None,
192            error: None,
193            steps_completed: 3,
194            steps_total: Some(5),
195            tool_calls: vec![],
196            tokens_input: 100,
197            tokens_output: 50,
198            cost_usd: 0.002,
199            model_id: String::new(),
200            session_id: None,
201        };
202
203        let json = serde_json::to_string(&info).unwrap();
204        let restored: AgentInfo = serde_json::from_str(&json).unwrap();
205        assert_eq!(restored.id, info.id);
206        assert_eq!(restored.name, info.name);
207        assert_eq!(restored.status, info.status);
208        assert_eq!(restored.seed_id, None);
209        assert_eq!(restored.steps_completed, 3);
210    }
211
212    #[test]
213    fn test_agent_status_copy() {
214        let status = AgentStatus::Running;
215        let copied = status; // Copy semantics
216        assert_eq!(status, copied); // status is still valid because Copy
217    }
218}