1use crate::io::{Model, PiMessage};
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub struct ImageContent {
16 #[serde(rename = "type")]
18 pub content_type: String,
19 pub data: String,
21 #[serde(rename = "mimeType")]
22 pub mime_type: String,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27pub enum StreamingBehavior {
28 #[serde(rename = "steer")]
31 Steer,
32 #[serde(rename = "followUp")]
34 FollowUp,
35}
36
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40#[serde(tag = "type", rename_all = "snake_case")]
41pub enum RpcCommand {
42 Prompt {
43 #[serde(skip_serializing_if = "Option::is_none")]
44 id: Option<String>,
45 message: String,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 images: Option<Vec<ImageContent>>,
48 #[serde(rename = "streamingBehavior", skip_serializing_if = "Option::is_none")]
49 streaming_behavior: Option<StreamingBehavior>,
50 },
51 Steer {
52 #[serde(skip_serializing_if = "Option::is_none")]
53 id: Option<String>,
54 message: String,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 images: Option<Vec<ImageContent>>,
57 },
58 FollowUp {
59 #[serde(skip_serializing_if = "Option::is_none")]
60 id: Option<String>,
61 message: String,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 images: Option<Vec<ImageContent>>,
64 },
65 Abort {
66 #[serde(skip_serializing_if = "Option::is_none")]
67 id: Option<String>,
68 },
69 ClearQueue {
70 #[serde(skip_serializing_if = "Option::is_none")]
71 id: Option<String>,
72 },
73 NewSession {
74 #[serde(skip_serializing_if = "Option::is_none")]
75 id: Option<String>,
76 },
77 GetState {
78 #[serde(skip_serializing_if = "Option::is_none")]
79 id: Option<String>,
80 },
81 GetMessages {
82 #[serde(skip_serializing_if = "Option::is_none")]
83 id: Option<String>,
84 },
85 SetModel {
86 #[serde(skip_serializing_if = "Option::is_none")]
87 id: Option<String>,
88 provider: String,
89 #[serde(rename = "modelId")]
90 model_id: String,
91 },
92 CycleModel {
93 #[serde(skip_serializing_if = "Option::is_none")]
94 id: Option<String>,
95 },
96 GetAvailableModels {
97 #[serde(skip_serializing_if = "Option::is_none")]
98 id: Option<String>,
99 },
100 SetThinkingLevel {
101 #[serde(skip_serializing_if = "Option::is_none")]
102 id: Option<String>,
103 level: String,
104 },
105 CycleThinkingLevel {
106 #[serde(skip_serializing_if = "Option::is_none")]
107 id: Option<String>,
108 },
109 GetAvailableThinkingLevels {
110 #[serde(skip_serializing_if = "Option::is_none")]
111 id: Option<String>,
112 },
113 SetSteeringMode {
114 #[serde(skip_serializing_if = "Option::is_none")]
115 id: Option<String>,
116 mode: String,
117 },
118 SetFollowUpMode {
119 #[serde(skip_serializing_if = "Option::is_none")]
120 id: Option<String>,
121 mode: String,
122 },
123 Compact {
124 #[serde(skip_serializing_if = "Option::is_none")]
125 id: Option<String>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 instructions: Option<String>,
128 },
129 SetAutoCompaction {
130 #[serde(skip_serializing_if = "Option::is_none")]
131 id: Option<String>,
132 enabled: bool,
133 },
134 SetAutoRetry {
135 #[serde(skip_serializing_if = "Option::is_none")]
136 id: Option<String>,
137 enabled: bool,
138 },
139 AbortRetry {
140 #[serde(skip_serializing_if = "Option::is_none")]
141 id: Option<String>,
142 },
143 Bash {
147 #[serde(skip_serializing_if = "Option::is_none")]
148 id: Option<String>,
149 command: String,
150 },
151 AbortBash {
152 #[serde(skip_serializing_if = "Option::is_none")]
153 id: Option<String>,
154 },
155 GetSessionStats {
156 #[serde(skip_serializing_if = "Option::is_none")]
157 id: Option<String>,
158 },
159 ExportHtml {
160 #[serde(skip_serializing_if = "Option::is_none")]
161 id: Option<String>,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 path: Option<String>,
164 },
165 SetSessionName {
166 #[serde(skip_serializing_if = "Option::is_none")]
167 id: Option<String>,
168 name: String,
169 },
170 #[serde(untagged)]
173 Raw(Value),
174}
175
176impl RpcCommand {
177 pub fn to_line(&self) -> Result<String, serde_json::Error> {
179 Ok(format!("{}\n", serde_json::to_string(self)?))
180 }
181}
182
183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
185pub struct RpcResponse {
186 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub id: Option<String>,
188 #[serde(default)]
190 pub command: String,
191 #[serde(default)]
192 pub success: bool,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub data: Option<Value>,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub error: Option<String>,
197}
198
199impl RpcResponse {
200 pub fn disposition(&self) -> Option<InputDisposition> {
204 self.data
205 .as_ref()?
206 .get("disposition")?
207 .as_str()
208 .map(InputDisposition::from)
209 }
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Hash)]
217pub enum InputDisposition {
218 Handled,
221 Queued,
224 Started,
226 Unknown(String),
228}
229
230impl InputDisposition {
231 pub fn as_str(&self) -> &str {
232 match self {
233 Self::Handled => "handled",
234 Self::Queued => "queued",
235 Self::Started => "started",
236 Self::Unknown(s) => s.as_str(),
237 }
238 }
239}
240
241impl std::fmt::Display for InputDisposition {
242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 f.write_str(self.as_str())
244 }
245}
246
247impl From<&str> for InputDisposition {
248 fn from(s: &str) -> Self {
249 match s {
250 "handled" => Self::Handled,
251 "queued" => Self::Queued,
252 "started" => Self::Started,
253 other => Self::Unknown(other.to_string()),
254 }
255 }
256}
257
258impl Serialize for InputDisposition {
259 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
260 serializer.serialize_str(self.as_str())
261 }
262}
263
264impl<'de> Deserialize<'de> for InputDisposition {
265 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
266 let s = String::deserialize(deserializer)?;
267 Ok(Self::from(s.as_str()))
268 }
269}
270
271#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
273pub struct AgentState {
274 #[serde(default, skip_serializing_if = "Option::is_none")]
277 pub model: Option<Model>,
278 #[serde(rename = "thinkingLevel", default)]
279 pub thinking_level: String,
280 #[serde(rename = "isStreaming", default)]
281 pub is_streaming: bool,
282 #[serde(rename = "isCompacting", default)]
283 pub is_compacting: bool,
284 #[serde(rename = "steeringMode", default)]
285 pub steering_mode: String,
286 #[serde(rename = "followUpMode", default)]
287 pub follow_up_mode: String,
288 #[serde(
289 rename = "sessionFile",
290 default,
291 skip_serializing_if = "Option::is_none"
292 )]
293 pub session_file: Option<String>,
294 #[serde(rename = "sessionId", default)]
295 pub session_id: String,
296 #[serde(
297 rename = "sessionName",
298 default,
299 skip_serializing_if = "Option::is_none"
300 )]
301 pub session_name: Option<String>,
302 #[serde(rename = "autoCompactionEnabled", default)]
303 pub auto_compaction_enabled: bool,
304 #[serde(rename = "messageCount", default)]
305 pub message_count: i64,
306 #[serde(rename = "pendingMessageCount", default)]
307 pub pending_message_count: i64,
308 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
309 pub extra: serde_json::Map<String, Value>,
310}
311
312#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
314pub struct BashResult {
315 #[serde(default)]
316 pub output: String,
317 #[serde(rename = "exitCode", default, skip_serializing_if = "Option::is_none")]
318 pub exit_code: Option<i64>,
319 #[serde(default)]
320 pub cancelled: bool,
321 #[serde(default)]
322 pub truncated: bool,
323 #[serde(
324 rename = "fullOutputPath",
325 default,
326 skip_serializing_if = "Option::is_none"
327 )]
328 pub full_output_path: Option<String>,
329}
330
331#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
333pub struct Messages {
334 #[serde(default)]
335 pub messages: Vec<PiMessage>,
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn commands_serialize_with_snake_case_type_tags() {
344 let c = RpcCommand::GetState {
345 id: Some("r1".into()),
346 };
347 let v = serde_json::to_value(&c).unwrap();
348 assert_eq!(v["type"], "get_state");
349 assert_eq!(v["id"], "r1");
350
351 let c = RpcCommand::Prompt {
352 id: None,
353 message: "hi".into(),
354 images: None,
355 streaming_behavior: Some(StreamingBehavior::Steer),
356 };
357 let v = serde_json::to_value(&c).unwrap();
358 assert_eq!(v["type"], "prompt");
359 assert_eq!(v["streamingBehavior"], "steer");
360 assert!(v.get("images").is_none());
361 }
362
363 #[test]
364 fn response_envelope_parses_success_and_error() {
365 let r: RpcResponse = serde_json::from_str(
366 r#"{"id":"r1","type":"response","command":"get_state","success":true,"data":{}}"#,
367 )
368 .unwrap();
369 assert!(r.success);
370 assert_eq!(r.command, "get_state");
371
372 let r: RpcResponse = serde_json::from_str(
373 r#"{"type":"response","command":"set_model","success":false,"error":"Model not found: invalid/model"}"#,
374 )
375 .unwrap();
376 assert!(!r.success);
377 assert!(r.error.unwrap().contains("Model not found"));
378 }
379
380 #[test]
381 fn input_disposition_reads_from_response_data() {
382 for (command, wire, expected) in [
383 ("prompt", "started", InputDisposition::Started),
384 ("prompt", "handled", InputDisposition::Handled),
385 ("steer", "queued", InputDisposition::Queued),
386 (
387 "follow_up",
388 "deferred",
389 InputDisposition::Unknown("deferred".into()),
390 ),
391 ] {
392 let r: RpcResponse = serde_json::from_value(serde_json::json!({
393 "id": "r1", "type": "response", "command": command,
394 "success": true, "data": {"disposition": wire}
395 }))
396 .unwrap();
397 let got = r.disposition().expect("disposition present");
398 assert_eq!(got, expected);
399 assert_eq!(got.to_string(), wire);
400 assert_eq!(serde_json::to_value(&got).unwrap(), wire);
401 }
402
403 let legacy: RpcResponse = serde_json::from_str(
405 r#"{"id":"r1","type":"response","command":"prompt","success":true}"#,
406 )
407 .unwrap();
408 assert_eq!(legacy.disposition(), None);
409 }
410
411 #[test]
412 fn agent_state_parses_credential_free_placeholder() {
413 let s: AgentState = serde_json::from_value(serde_json::json!({
414 "model": {"id":"unknown","name":"unknown","api":"unknown","provider":"unknown",
415 "baseUrl":"","reasoning":false,"input":[],
416 "cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0},
417 "contextWindow":0,"maxTokens":0},
418 "thinkingLevel":"off","isStreaming":false,"isCompacting":false,
419 "steeringMode":"one-at-a-time","followUpMode":"one-at-a-time",
420 "sessionId":"01a05ec0-4e0e-7dd4-a225-2e78cccd0059",
421 "autoCompactionEnabled":true,"messageCount":0,"pendingMessageCount":0
422 }))
423 .unwrap();
424 assert_eq!(s.model.unwrap().context_window, 0);
425 assert_eq!(s.message_count, 0);
426 }
427
428 #[test]
429 fn raw_command_passes_through_unmodeled_types() {
430 let c = RpcCommand::Raw(serde_json::json!({"type": "get_tree", "id": "r9"}));
431 let line = c.to_line().unwrap();
432 assert!(line.ends_with('\n'));
433 assert!(line.contains("\"get_tree\""));
434 }
435}