nomoreide_remote_protocol/
agent_event.rs1use serde::{Deserialize, Serialize};
23
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27pub struct AgentEvent {
28 pub run_id: String,
29 pub seq: u64,
31 pub event: AgentEventBody,
32}
33
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37#[serde(tag = "kind", content = "data", rename_all = "camelCase")]
38pub enum AgentEventBody {
39 Text(TextEvent),
41 ToolUse(ToolUseEvent),
44 ToolResult(ToolResultEvent),
46 ApprovalRequest(ApprovalRequestEvent),
48 ApprovalSettled(ApprovalSettledEvent),
50 Completed(NoData),
52 Cancelled(NoData),
54 Error(ErrorEvent),
56}
57
58#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(deny_unknown_fields)]
62pub struct NoData {}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(rename_all = "camelCase", deny_unknown_fields)]
66pub struct TextEvent {
67 pub text: String,
68}
69
70#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
71#[serde(rename_all = "camelCase", deny_unknown_fields)]
72pub struct ToolUseEvent {
73 pub tool_use_id: String,
74 pub name: String,
75 pub input: serde_json::Value,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase", deny_unknown_fields)]
82pub struct ToolResultEvent {
83 pub tool_use_id: String,
84 pub ok: bool,
85 pub summary: String,
86}
87
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase", deny_unknown_fields)]
94pub struct ApprovalRequestEvent {
95 pub approval_id: String,
96 pub provider: String,
97 pub tool_name: String,
98 pub input: serde_json::Value,
99 pub workspace: String,
102 pub expires_at: String,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase", deny_unknown_fields)]
108pub struct ApprovalSettledEvent {
109 pub approval_id: String,
110 pub verdict: super::device_bound::ApprovalVerdict,
111 pub decided_by: ApprovalDecider,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115#[serde(rename_all = "camelCase", deny_unknown_fields)]
116pub struct ErrorEvent {
117 pub message: String,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(rename_all = "camelCase")]
123pub enum ApprovalDecider {
124 User,
125 Expiry,
126 Shutdown,
127}
128
129impl AgentEventBody {
130 pub fn terminal(&self) -> bool {
133 matches!(
134 self,
135 Self::Completed(_) | Self::Cancelled(_) | Self::Error(_)
136 )
137 }
138
139 pub fn kind(&self) -> &'static str {
141 match self {
142 Self::Text(_) => "text",
143 Self::ToolUse(_) => "toolUse",
144 Self::ToolResult(_) => "toolResult",
145 Self::ApprovalRequest(_) => "approvalRequest",
146 Self::ApprovalSettled(_) => "approvalSettled",
147 Self::Completed(_) => "completed",
148 Self::Cancelled(_) => "cancelled",
149 Self::Error(_) => "error",
150 }
151 }
152
153 pub const KINDS: &'static [&'static str] = &[
155 "text",
156 "toolUse",
157 "toolResult",
158 "approvalRequest",
159 "approvalSettled",
160 "completed",
161 "cancelled",
162 "error",
163 ];
164}
165
166#[cfg(test)]
167mod tests {
168 use super::super::device_bound::ApprovalVerdict;
169 use super::*;
170
171 #[test]
172 fn an_event_carries_its_run_and_sequence_beside_the_body() {
173 let event = AgentEvent {
174 run_id: "run_1".into(),
175 seq: 7,
176 event: AgentEventBody::Text(TextEvent {
177 text: "hello".into(),
178 }),
179 };
180 let json = serde_json::to_value(&event).expect("serialise");
181 assert_eq!(json["runId"], "run_1");
182 assert_eq!(json["seq"], 7);
183 assert_eq!(json["event"]["kind"], "text");
184 assert_eq!(json["event"]["data"]["text"], "hello");
185 }
186
187 #[test]
188 fn an_approval_request_round_trips_with_its_full_input() {
189 let event = AgentEvent {
190 run_id: "run_1".into(),
191 seq: 0,
192 event: AgentEventBody::ApprovalRequest(ApprovalRequestEvent {
193 approval_id: "ap_1".into(),
194 provider: "claude".into(),
195 tool_name: "Bash".into(),
196 input: serde_json::json!({ "command": "rm -rf build" }),
197 workspace: "/w/project".into(),
198 expires_at: "2026-09-01T00:02:00Z".into(),
199 }),
200 };
201 let json = serde_json::to_string(&event).expect("serialise");
202 let back: AgentEvent = serde_json::from_str(&json).expect("parse");
203 assert_eq!(back, event);
204 }
205
206 #[test]
207 fn only_the_three_endings_are_terminal() {
208 assert!(AgentEventBody::Completed(NoData {}).terminal());
209 assert!(AgentEventBody::Cancelled(NoData {}).terminal());
210 assert!(AgentEventBody::Error(ErrorEvent {
211 message: "boom".into()
212 })
213 .terminal());
214 assert!(!AgentEventBody::Text(TextEvent { text: "x".into() }).terminal());
215 assert!(!AgentEventBody::ApprovalSettled(ApprovalSettledEvent {
216 approval_id: "ap_1".into(),
217 verdict: ApprovalVerdict::Deny,
218 decided_by: ApprovalDecider::Expiry,
219 })
220 .terminal());
221 }
222
223 #[test]
226 fn an_unknown_event_kind_is_refused() {
227 let refused = serde_json::from_str::<AgentEvent>(
228 r#"{"runId":"run_1","seq":0,"event":{"kind":"terminalOutput","data":{}}}"#,
229 );
230 assert!(refused.is_err());
231 }
232
233 #[test]
234 fn kinds_matches_the_union() {
235 let bodies = [
236 AgentEventBody::Text(TextEvent {
237 text: String::new(),
238 }),
239 AgentEventBody::ToolUse(ToolUseEvent {
240 tool_use_id: String::new(),
241 name: String::new(),
242 input: serde_json::Value::Null,
243 }),
244 AgentEventBody::ToolResult(ToolResultEvent {
245 tool_use_id: String::new(),
246 ok: true,
247 summary: String::new(),
248 }),
249 AgentEventBody::ApprovalRequest(ApprovalRequestEvent {
250 approval_id: String::new(),
251 provider: String::new(),
252 tool_name: String::new(),
253 input: serde_json::Value::Null,
254 workspace: String::new(),
255 expires_at: String::new(),
256 }),
257 AgentEventBody::ApprovalSettled(ApprovalSettledEvent {
258 approval_id: String::new(),
259 verdict: ApprovalVerdict::Allow,
260 decided_by: ApprovalDecider::User,
261 }),
262 AgentEventBody::Completed(NoData {}),
263 AgentEventBody::Cancelled(NoData {}),
264 AgentEventBody::Error(ErrorEvent {
265 message: String::new(),
266 }),
267 ];
268 let kinds: Vec<&str> = bodies.iter().map(AgentEventBody::kind).collect();
269 assert_eq!(kinds, AgentEventBody::KINDS);
270 }
271}