1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7#[serde(rename_all = "snake_case")]
8pub enum LifecycleMode {
9 Manual,
11 AutoWorkspace,
13 DailyRollup,
15}
16
17impl Default for LifecycleMode {
18 fn default() -> Self {
19 Self::AutoWorkspace
20 }
21}
22
23#[derive(Debug, Clone, Default, Serialize, Deserialize)]
25pub struct Participants {
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub root_agent_instance_id: Option<String>,
29
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub final_output_agent_instance_id: Option<String>,
33
34 #[serde(default)]
36 pub total_agents: u32,
37
38 #[serde(default)]
40 pub spawned_subagents: u32,
41
42 #[serde(default)]
44 pub handoffs: u32,
45
46 #[serde(default)]
48 pub max_depth: u32,
49
50 #[serde(default)]
52 pub hosts: u32,
53
54 #[serde(default)]
56 pub tool_runtimes: u32,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct HostInfo {
62 pub host_id: String,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub hostname: Option<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub os: Option<String>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub arch: Option<String>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct ToolInfo {
74 pub tool_id: String,
75 pub tool_name: String,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub tool_runtime_id: Option<String>,
78 #[serde(default)]
79 pub invocation_count: u32,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
84#[serde(rename_all = "snake_case")]
85pub enum SessionStatus {
86 Active,
87 Completed,
88 Failed,
89 Abandoned,
90}
91
92impl Default for SessionStatus {
93 fn default() -> Self {
94 Self::Active
95 }
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct SessionManifest {
105 pub session_id: String,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub name: Option<String>,
109
110 pub actor: String,
111
112 pub started_at: String,
113
114 #[serde(default)]
115 pub started_at_ms: u64,
116
117 #[serde(default)]
118 pub artifact_count: u64,
119
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub root_artifact_id: Option<String>,
122
123 #[serde(default)]
126 pub mode: LifecycleMode,
127
128 #[serde(default)]
129 pub status: SessionStatus,
130
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub workspace_id: Option<String>,
133
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub mission_id: Option<String>,
136
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub closed_at: Option<String>,
139
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub close_artifact_id: Option<String>,
142
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub summary: Option<String>,
145
146 #[serde(default)]
147 pub participants: Participants,
148
149 #[serde(default, skip_serializing_if = "Vec::is_empty")]
150 pub hosts: Vec<HostInfo>,
151
152 #[serde(default, skip_serializing_if = "Vec::is_empty")]
153 pub tools: Vec<ToolInfo>,
154
155 #[serde(default, skip_serializing_if = "Vec::is_empty")]
157 pub authorized_tools: Vec<String>,
158
159 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub start_commit_sha: Option<String>,
166}
167
168impl SessionManifest {
169 pub fn new(session_id: String, actor: String, started_at: String, started_at_ms: u64) -> Self {
171 Self {
172 session_id,
173 name: None,
174 actor,
175 started_at,
176 started_at_ms,
177 artifact_count: 0,
178 root_artifact_id: None,
179 mode: LifecycleMode::default(),
180 status: SessionStatus::Active,
181 workspace_id: None,
182 mission_id: None,
183 closed_at: None,
184 close_artifact_id: None,
185 summary: None,
186 participants: Participants::default(),
187 hosts: Vec::new(),
188 tools: Vec::new(),
189 authorized_tools: Vec::new(),
190 start_commit_sha: None,
191 }
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn deserialize_legacy_manifest() {
201 let json = r#"{
203 "session_id": "ssn_abc123",
204 "name": "test",
205 "actor": "ship://local",
206 "started_at": "2026-04-05T08:00:00Z",
207 "started_at_ms": 1743843600000,
208 "artifact_count": 5,
209 "root_artifact_id": "art_deadbeef"
210 }"#;
211 let m: SessionManifest = serde_json::from_str(json).unwrap();
212 assert_eq!(m.session_id, "ssn_abc123");
213 assert_eq!(m.mode, LifecycleMode::AutoWorkspace);
214 assert_eq!(m.status, SessionStatus::Active);
215 assert_eq!(m.participants.total_agents, 0);
216 }
217
218 #[test]
219 fn roundtrip_full_manifest() {
220 let m = SessionManifest {
221 session_id: "ssn_001".into(),
222 name: Some("daily dev".into()),
223 actor: "agent://claude".into(),
224 started_at: "2026-04-05T08:00:00Z".into(),
225 started_at_ms: 1743843600000,
226 artifact_count: 12,
227 root_artifact_id: Some("art_root".into()),
228 mode: LifecycleMode::Manual,
229 status: SessionStatus::Completed,
230 workspace_id: Some("ws_abc".into()),
231 mission_id: None,
232 closed_at: Some("2026-04-05T12:00:00Z".into()),
233 close_artifact_id: Some("art_close".into()),
234 summary: Some("Fixed auth bug".into()),
235 participants: Participants {
236 root_agent_instance_id: Some("ai_root_1".into()),
237 final_output_agent_instance_id: Some("ai_review_2".into()),
238 total_agents: 6,
239 spawned_subagents: 4,
240 handoffs: 7,
241 max_depth: 3,
242 hosts: 2,
243 tool_runtimes: 5,
244 },
245 hosts: vec![HostInfo {
246 host_id: "host_1".into(),
247 hostname: Some("macbook".into()),
248 os: Some("darwin".into()),
249 arch: Some("arm64".into()),
250 }],
251 tools: vec![ToolInfo {
252 tool_id: "tool_1".into(),
253 tool_name: "claude-code".into(),
254 tool_runtime_id: Some("rt_cc1".into()),
255 invocation_count: 42,
256 }],
257 authorized_tools: vec!["read_file".into(), "write_file".into()],
258 start_commit_sha: Some("abc1234567890abcdef1234567890abcdef12345".into()),
259 };
260 let json = serde_json::to_string_pretty(&m).unwrap();
261 let m2: SessionManifest = serde_json::from_str(&json).unwrap();
262 assert_eq!(m2.session_id, "ssn_001");
263 assert_eq!(m2.participants.total_agents, 6);
264 assert_eq!(m2.hosts.len(), 1);
265 }
266}