1use serde::{Deserialize, Serialize};
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
7pub enum AgentHarness {
8 #[serde(rename = "cdx")]
10 Cdx,
11 #[serde(rename = "claude-code")]
13 ClaudeCode,
14 #[serde(rename = "codex")]
16 Codex,
17 #[serde(rename = "gemini-cli")]
19 GeminiCli,
20 #[serde(rename = "opencode")]
22 Opencode,
23 #[serde(rename = "pi")]
25 Pi,
26}
27
28#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(deny_unknown_fields)]
31pub struct AgentSpec {
32 pub harness: AgentHarness,
34 pub purpose: String,
36 pub service: Option<String>,
38}
39
40#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(
43 tag = "event",
44 content = "detail",
45 rename_all = "snake_case",
46 deny_unknown_fields
47)]
48pub enum AgentEvent {
49 Create(AgentSpec),
51 Start,
53 Stop,
55 Present {
57 connection: u64,
59 },
60 Gone {
62 connection: u64,
64 reason: String,
66 },
67 Session {
69 session: String,
70 protocol_version: u8,
71 load_session: bool,
72 },
73 SessionClosed {
75 exit_status: i32,
76 session_state: String,
77 },
78 Retire,
80}
81
82#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
84#[serde(deny_unknown_fields)]
85pub struct AgentTransition {
86 pub holder: String,
88 pub at_ms: u64,
90 pub change: AgentEvent,
92}
93
94#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(deny_unknown_fields)]
97pub struct AgentRecord {
98 pub name: String,
100 pub harness: AgentHarness,
102 pub purpose: String,
104 pub owner: String,
106 pub service: Option<String>,
108 pub created_at_ms: u64,
110 pub transitions: Vec<AgentTransition>,
112}
113
114impl AgentRecord {
115 #[must_use]
117 pub fn active(&self) -> bool {
118 self.transitions
119 .last()
120 .is_none_or(|transition| !matches!(transition.change, AgentEvent::Retire))
121 }
122
123 #[must_use]
125 pub fn connection(&self) -> Option<u64> {
126 self.seat().map(|(connection, _)| connection)
127 }
128
129 #[must_use]
131 pub fn seat(&self) -> Option<(u64, u64)> {
132 self.transitions
133 .iter()
134 .rev()
135 .find_map(|transition| match transition.change {
136 AgentEvent::Present { connection } => Some(Some((connection, transition.at_ms))),
137 AgentEvent::Gone { .. } | AgentEvent::Retire => Some(None),
138 _ => None,
139 })
140 .flatten()
141 }
142}
143
144impl std::fmt::Debug for AgentRecord {
145 fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 out.debug_struct("AgentRecord")
147 .field("harness", &self.harness)
148 .field("transitions", &self.transitions.len())
149 .finish_non_exhaustive()
150 }
151}
152
153#[derive(Clone, Serialize, Deserialize)]
155#[serde(deny_unknown_fields)]
156pub struct AgentAct {
157 pub name: String,
159 pub holder: String,
161 pub owner: String,
163 pub at_ms: u64,
165 pub change: AgentEvent,
167}
168
169impl AgentAct {
170 pub fn prepare(
177 name: String,
178 change: AgentEvent,
179 pass: &lys_pass::VerifiedPass,
180 at_ms: u64,
181 ) -> Result<Self, super::error::AgentError> {
182 let holder = &pass.claims().holder;
183 let owner = match holder.kind.as_str() {
184 "person" => holder.id.as_str(),
185 "agent" => holder
186 .responsible
187 .as_deref()
188 .ok_or(super::error::AgentError::Invalid(
189 "agent responsible person",
190 ))?,
191 _ => {
192 return Err(super::error::AgentError::Invalid(
193 "agent creation holder kind",
194 ));
195 }
196 };
197 if owner.is_empty() {
198 return Err(super::error::AgentError::Invalid(
199 "agent responsible person",
200 ));
201 }
202 Ok(Self {
203 name,
204 holder: holder.id.clone(),
205 owner: owner.into(),
206 at_ms,
207 change,
208 })
209 }
210}