1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct AgentRecord {
12 pub agent_id: String,
13 pub role: String,
14 #[serde(default)]
15 pub labels: Vec<String>,
16 pub endpoint: String,
18 pub pid: u32,
19 #[serde(default)]
20 pub version: String,
21 pub started_at: DateTime<Utc>,
22 pub lease_expires_at: DateTime<Utc>,
24}
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct RunSpec {
29 pub assignment: String,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub reasoning_effort: Option<String>,
32 #[serde(default, skip_serializing_if = "Vec::is_empty")]
38 pub messages: Vec<serde_json::Value>,
39}
40
41#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43#[serde(tag = "kind", rename_all = "snake_case")]
44pub enum ParentFrame {
45 Run(RunSpec),
46 Cancel,
47 Message {
48 text: String,
49 },
50 ApprovalReply {
56 id: String,
57 approved: bool,
58 },
59}
60
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
63#[serde(tag = "kind", rename_all = "snake_case")]
64pub enum ChildFrame {
65 Event { event: serde_json::Value },
67 ApprovalRequest { id: String, body: serde_json::Value },
73 Terminal {
74 status: TerminalStatus,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 result: Option<String>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 error: Option<String>,
79 #[serde(default, skip_serializing_if = "Vec::is_empty")]
83 transcript: Vec<serde_json::Value>,
84 },
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "snake_case")]
89pub enum TerminalStatus {
90 Completed,
91 Error,
92 Cancelled,
93 Suspended,
97}
98
99impl ParentFrame {
100 pub fn to_text(&self) -> String {
101 serde_json::to_string(self).unwrap_or_else(|_| "{}".to_string())
102 }
103 pub fn from_text(s: &str) -> serde_json::Result<Self> {
104 serde_json::from_str(s)
105 }
106}
107
108impl ChildFrame {
109 pub fn to_text(&self) -> String {
110 serde_json::to_string(self).unwrap_or_else(|_| "{}".to_string())
111 }
112 pub fn from_text(s: &str) -> serde_json::Result<Self> {
113 serde_json::from_str(s)
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn parent_frames_round_trip() {
123 for f in [
124 ParentFrame::Run(RunSpec {
125 assignment: "do x".into(),
126 reasoning_effort: None,
127 messages: Vec::new(),
128 }),
129 ParentFrame::Cancel,
130 ParentFrame::Message { text: "hi".into() },
131 ] {
132 assert_eq!(ParentFrame::from_text(&f.to_text()).unwrap(), f);
133 }
134 }
135
136 #[test]
137 fn child_frames_round_trip() {
138 let e = ChildFrame::Event {
139 event: serde_json::json!({"type":"token","content":"hi"}),
140 };
141 assert_eq!(ChildFrame::from_text(&e.to_text()).unwrap(), e);
142 let t = ChildFrame::Terminal {
143 status: TerminalStatus::Completed,
144 result: Some("done".into()),
145 error: None,
146 transcript: Vec::new(),
147 };
148 assert_eq!(ChildFrame::from_text(&t.to_text()).unwrap(), t);
149
150 let s = ChildFrame::Terminal {
152 status: TerminalStatus::Suspended,
153 result: None,
154 error: None,
155 transcript: vec![serde_json::json!({"role":"assistant","content":"x"})],
156 };
157 assert_eq!(ChildFrame::from_text(&s.to_text()).unwrap(), s);
158
159 let areq = ChildFrame::ApprovalRequest {
161 id: "a1".into(),
162 body: serde_json::json!({
163 "tool_name": "Write",
164 "permission_type": "WriteFile",
165 "resource": "/tmp/x",
166 "question": "approve?",
167 }),
168 };
169 assert_eq!(ChildFrame::from_text(&areq.to_text()).unwrap(), areq);
170 let areply = ParentFrame::ApprovalReply {
171 id: "a1".into(),
172 approved: true,
173 };
174 assert_eq!(ParentFrame::from_text(&areply.to_text()).unwrap(), areply);
175 }
176
177 #[test]
178 fn run_frame_tag_is_stable() {
179 let f = ParentFrame::Run(RunSpec {
180 assignment: "a".into(),
181 reasoning_effort: Some("high".into()),
182 messages: Vec::new(),
183 });
184 let v: serde_json::Value = serde_json::from_str(&f.to_text()).unwrap();
185 assert_eq!(v["kind"], "run");
186 assert_eq!(v["assignment"], "a");
187 }
188
189 #[test]
190 fn run_frame_without_messages_parses_backward_compat() {
191 let parsed = ParentFrame::from_text(r#"{"kind":"run","assignment":"x"}"#).unwrap();
193 match parsed {
194 ParentFrame::Run(spec) => {
195 assert_eq!(spec.assignment, "x");
196 assert!(spec.messages.is_empty());
197 }
198 other => panic!("expected run frame, got {other:?}"),
199 }
200 }
201}