Skip to main content

khive_types/
agent.rs

1//! Agent process lifecycle types (ADR-142 §1, "Persistent process record").
2
3extern crate alloc;
4use alloc::string::String;
5use alloc::vec::Vec;
6
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9
10/// One of the four lifecycle states an agent process record can occupy.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
13#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
14pub enum AgentState {
15    Spawned,
16    Running,
17    Suspended,
18    Terminal,
19}
20
21impl AgentState {
22    /// Canonical snake_case name, as stored and as serialized on the wire.
23    pub const fn as_str(self) -> &'static str {
24        match self {
25            Self::Spawned => "spawned",
26            Self::Running => "running",
27            Self::Suspended => "suspended",
28            Self::Terminal => "terminal",
29        }
30    }
31}
32
33/// Why a record reached `Terminal`. Set exactly once, at the transition into `Terminal`.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
36#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
37pub enum TerminalReason {
38    Completed,
39    Failed,
40    Killed,
41    Abandoned,
42    HostRestart,
43}
44
45impl TerminalReason {
46    /// Canonical snake_case name, as stored and as serialized on the wire.
47    pub const fn as_str(self) -> &'static str {
48        match self {
49            Self::Completed => "completed",
50            Self::Failed => "failed",
51            Self::Killed => "killed",
52            Self::Abandoned => "abandoned",
53            Self::HostRestart => "host_restart",
54        }
55    }
56}
57
58/// The runtime-owned agent process record (ADR-142 §1, "Persistent process record").
59#[derive(Debug, Clone)]
60#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
61pub struct AgentRecord {
62    pub agent_id: String,
63    pub state: AgentState,
64    pub terminal_reason: Option<TerminalReason>,
65    pub provider: String,
66    pub provider_session_id: Option<String>,
67    pub checkpoint_session_id: Option<String>,
68    pub checkpoint_cursor: Option<i64>,
69    pub owner_actor: String,
70    pub owner_peer_class: String,
71    pub owner_write_namespace: String,
72    pub owner_visible_namespaces: Vec<String>,
73    pub spawn_fingerprint: String,
74    pub spawned_at: i64,
75    pub state_changed_at: i64,
76    pub idempotency_key: Option<String>,
77}
78
79#[cfg(all(test, feature = "serde"))]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn agent_state_as_str_matches_serde_wire_spelling() {
85        let cases = [
86            (AgentState::Spawned, "spawned"),
87            (AgentState::Running, "running"),
88            (AgentState::Suspended, "suspended"),
89            (AgentState::Terminal, "terminal"),
90        ];
91        for (state, spelling) in cases {
92            let wire = serde_json::to_value(state).unwrap();
93            assert_eq!(wire.as_str(), Some(state.as_str()), "{state:?}");
94            assert_eq!(state.as_str(), spelling);
95        }
96    }
97
98    #[test]
99    fn terminal_reason_as_str_matches_serde_wire_spelling() {
100        let cases = [
101            (TerminalReason::Completed, "completed"),
102            (TerminalReason::Failed, "failed"),
103            (TerminalReason::Killed, "killed"),
104            (TerminalReason::Abandoned, "abandoned"),
105            (TerminalReason::HostRestart, "host_restart"),
106        ];
107        for (reason, spelling) in cases {
108            let wire = serde_json::to_value(reason).unwrap();
109            assert_eq!(wire.as_str(), Some(reason.as_str()), "{reason:?}");
110            assert_eq!(reason.as_str(), spelling);
111        }
112    }
113}