1use serde::{Deserialize, Serialize};
19use serde_json::Value;
20
21use rpi_agent::events::AgentEvent;
22use rpi_agent::types::AgentToolResult;
23
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
29#[serde(tag = "type", rename_all = "snake_case")]
30pub enum RemoteEvent {
31 AgentStart,
32 AgentEnd {
33 #[serde(rename = "messageCount")]
34 message_count: usize,
35 messages: Value,
36 },
37 RetryScheduled {
38 attempt: u32,
39 #[serde(rename = "maxRetries")]
40 max_retries: u32,
41 #[serde(rename = "delayMs")]
42 delay_ms: u64,
43 error: String,
44 },
45 TurnStart,
46 TurnEnd {
47 message: Value,
48 #[serde(rename = "toolResultCount")]
49 tool_result_count: usize,
50 #[serde(rename = "toolResults")]
51 tool_results: Value,
52 },
53 MessageStart {
54 #[serde(default)]
55 message: Value,
56 },
57 MessageEnd {
58 #[serde(default)]
59 message: Value,
60 },
61 MessageUpdate {
62 #[serde(default)]
63 message: Value,
64 #[serde(rename = "assistantMessageEvent", default)]
65 assistant_message_event: Value,
66 #[serde(rename = "eventType", default)]
67 event_type: String,
68 #[serde(
72 rename = "contentIndex",
73 default,
74 skip_serializing_if = "Option::is_none"
75 )]
76 content_index: Option<u64>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
79 delta: Option<String>,
80 },
81 ToolExecutionStart {
82 #[serde(rename = "toolCallId")]
83 tool_call_id: String,
84 #[serde(rename = "toolName")]
85 tool_name: String,
86 #[serde(default)]
87 args: Value,
88 },
89 ToolExecutionUpdate {
90 #[serde(rename = "toolCallId")]
91 tool_call_id: String,
92 #[serde(rename = "toolName")]
93 tool_name: String,
94 #[serde(default)]
95 args: Value,
96 #[serde(rename = "partialResult")]
97 partial_result: Value,
98 },
99 ToolExecutionEnd {
100 #[serde(rename = "toolCallId")]
101 tool_call_id: String,
102 #[serde(rename = "toolName")]
103 tool_name: String,
104 #[serde(rename = "isError")]
105 is_error: bool,
106 result: Value,
107 },
108}
109
110impl RemoteEvent {
111 pub fn type_tag(&self) -> &'static str {
113 match self {
114 RemoteEvent::AgentStart => "agent_start",
115 RemoteEvent::AgentEnd { .. } => "agent_end",
116 RemoteEvent::RetryScheduled { .. } => "retry_scheduled",
117 RemoteEvent::TurnStart => "turn_start",
118 RemoteEvent::TurnEnd { .. } => "turn_end",
119 RemoteEvent::MessageStart { .. } => "message_start",
120 RemoteEvent::MessageUpdate { .. } => "message_update",
121 RemoteEvent::MessageEnd { .. } => "message_end",
122 RemoteEvent::ToolExecutionStart { .. } => "tool_execution_start",
123 RemoteEvent::ToolExecutionUpdate { .. } => "tool_execution_update",
124 RemoteEvent::ToolExecutionEnd { .. } => "tool_execution_end",
125 }
126 }
127
128 pub fn is_terminal(&self) -> bool {
130 matches!(self, RemoteEvent::AgentEnd { .. })
131 }
132
133 pub fn from_agent_event(event: &AgentEvent) -> Self {
135 use rpi_ai::types::AssistantMessageEvent;
136 match event {
137 AgentEvent::AgentStart => RemoteEvent::AgentStart,
138 AgentEvent::AgentEnd { messages } => RemoteEvent::AgentEnd {
139 message_count: messages.len(),
140 messages: serde_json::to_value(messages).unwrap_or(Value::Null),
141 },
142 AgentEvent::RetryScheduled {
143 attempt,
144 max_retries,
145 delay_ms,
146 error,
147 } => RemoteEvent::RetryScheduled {
148 attempt: *attempt,
149 max_retries: *max_retries,
150 delay_ms: *delay_ms,
151 error: error.clone(),
152 },
153 AgentEvent::TurnStart => RemoteEvent::TurnStart,
154 AgentEvent::TurnEnd {
155 message,
156 tool_results,
157 } => RemoteEvent::TurnEnd {
158 message: serde_json::to_value(message).unwrap_or(Value::Null),
159 tool_result_count: tool_results.len(),
160 tool_results: serde_json::to_value(tool_results).unwrap_or(Value::Null),
161 },
162 AgentEvent::MessageStart { message } => RemoteEvent::MessageStart {
163 message: serde_json::to_value(message).unwrap_or(Value::Null),
164 },
165 AgentEvent::MessageEnd { message } => RemoteEvent::MessageEnd {
166 message: serde_json::to_value(message).unwrap_or(Value::Null),
167 },
168 AgentEvent::MessageUpdate {
169 message,
170 assistant_message_event,
171 } => {
172 let (content_index, delta) = match assistant_message_event {
173 AssistantMessageEvent::TextDelta {
174 content_index,
175 delta,
176 ..
177 }
178 | AssistantMessageEvent::ThinkingDelta {
179 content_index,
180 delta,
181 ..
182 }
183 | AssistantMessageEvent::ToolCallDelta {
184 content_index,
185 delta,
186 ..
187 } => (Some(*content_index as u64), Some(delta.clone())),
188 _ => (None, None),
189 };
190 RemoteEvent::MessageUpdate {
191 message: rpi_agent::message::assistant_json(message),
194 assistant_message_event: serde_json::to_value(assistant_message_event)
195 .unwrap_or(Value::Null),
196 event_type: assistant_message_event.type_tag().to_string(),
197 content_index,
198 delta,
199 }
200 }
201 AgentEvent::ToolExecutionStart {
202 tool_call_id,
203 tool_name,
204 args,
205 } => RemoteEvent::ToolExecutionStart {
206 tool_call_id: tool_call_id.clone(),
207 tool_name: tool_name.clone(),
208 args: args.clone(),
209 },
210 AgentEvent::ToolExecutionUpdate {
211 tool_call_id,
212 tool_name,
213 args,
214 partial_result,
215 } => RemoteEvent::ToolExecutionUpdate {
216 tool_call_id: tool_call_id.clone(),
217 tool_name: tool_name.clone(),
218 args: args.clone(),
219 partial_result: tool_result_json(partial_result),
220 },
221 AgentEvent::ToolExecutionEnd {
222 tool_call_id,
223 tool_name,
224 result,
225 is_error,
226 } => RemoteEvent::ToolExecutionEnd {
227 tool_call_id: tool_call_id.clone(),
228 tool_name: tool_name.clone(),
229 is_error: *is_error,
230 result: tool_result_json(result),
231 },
232 }
233 }
234}
235
236pub fn tool_result_json(result: &AgentToolResult) -> Value {
239 let content: Vec<Value> = result
240 .content
241 .iter()
242 .map(|item| match item {
243 rpi_agent::types::TextContentOrImage::Text(text) => serde_json::json!({
244 "type": "text",
245 "text": text.text,
246 }),
247 rpi_agent::types::TextContentOrImage::Image(image) => {
248 serde_json::to_value(image).unwrap_or(Value::Null)
249 }
250 })
251 .collect();
252 serde_json::json!({
253 "content": content,
254 "details": result.details,
255 "usage": result.usage.as_ref().and_then(|usage| serde_json::to_value(usage).ok()),
256 "addedToolNames": result.added_tool_names,
257 "terminate": result.terminate,
258 })
259}
260
261#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
266#[serde(tag = "type", rename_all = "snake_case")]
267pub enum RemoteCommand {
268 Prompt { content: String },
270 Abort,
272 GetState,
274 SetModel { model: String },
276 SetThinkingLevel { level: String },
278 SetActiveTools { tools: Vec<String> },
280 Ping,
282 Stop,
284}
285
286impl RemoteCommand {
287 pub fn with_id(&self, id: impl Into<Value>) -> Value {
289 let mut value = serde_json::to_value(self).unwrap_or(Value::Null);
290 if let Some(object) = value.as_object_mut() {
291 object.insert("id".to_string(), id.into());
292 }
293 value
294 }
295}
296
297#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
299#[serde(rename_all = "lowercase")]
300pub enum ResponseStatus {
301 Ok,
302 Error,
303}
304
305#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
307pub struct RemoteResponse {
308 #[serde(default)]
309 pub id: Value,
310 pub status: ResponseStatus,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
312 pub result: Option<Value>,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub error: Option<String>,
315}
316
317impl RemoteResponse {
318 pub fn ok(id: Value, result: Value) -> Self {
320 Self {
321 id,
322 status: ResponseStatus::Ok,
323 result: Some(result),
324 error: None,
325 }
326 }
327
328 pub fn error(id: Value, error: impl Into<String>) -> Self {
330 Self {
331 id,
332 status: ResponseStatus::Error,
333 result: None,
334 error: Some(error.into()),
335 }
336 }
337
338 pub fn is_ok(&self) -> bool {
340 matches!(self.status, ResponseStatus::Ok)
341 }
342
343 pub fn to_json(&self) -> Value {
345 let mut value = serde_json::to_value(self).unwrap_or(Value::Null);
346 if let Some(object) = value.as_object_mut() {
347 object.insert("type".to_string(), Value::String("response".to_string()));
348 }
349 value
350 }
351}
352
353#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
355pub struct SessionState {
356 #[serde(default)]
357 pub model: Option<String>,
358 #[serde(rename = "thinkingLevel", default)]
359 pub thinking_level: Option<Value>,
360 #[serde(rename = "activeTools", default)]
361 pub active_tools: Vec<String>,
362 #[serde(rename = "leafId", default)]
363 pub leaf_id: Option<String>,
364 #[serde(rename = "sessionId", default)]
365 pub session_id: Option<String>,
366}
367
368#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
370pub struct PromptOutcome {
371 #[serde(default)]
372 pub outcome: Option<String>,
373 #[serde(rename = "finalText", default)]
374 pub final_text: Option<String>,
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use rpi_agent::types::AgentToolResult;
381
382 #[test]
383 fn event_projection_matches_wire_shape() {
384 let end = serde_json::to_value(RemoteEvent::from_agent_event(&AgentEvent::AgentEnd {
385 messages: vec![],
386 }))
387 .unwrap();
388 assert_eq!(end["type"], "agent_end");
389 assert_eq!(end["messages"], serde_json::json!([]));
390 assert_eq!(end["messageCount"], 0);
391
392 let tool = serde_json::to_value(RemoteEvent::from_agent_event(
393 &AgentEvent::ToolExecutionEnd {
394 tool_call_id: "call-1".into(),
395 tool_name: "read".into(),
396 result: AgentToolResult::text("hello"),
397 is_error: false,
398 },
399 ))
400 .unwrap();
401 assert_eq!(tool["type"], "tool_execution_end");
402 assert_eq!(tool["result"]["content"][0]["text"], "hello");
403 assert_eq!(tool["result"]["terminate"], false);
404
405 let retry =
406 serde_json::to_value(RemoteEvent::from_agent_event(&AgentEvent::RetryScheduled {
407 attempt: 3,
408 max_retries: 10,
409 delay_ms: 8_000,
410 error: "503 service unavailable".into(),
411 }))
412 .unwrap();
413 assert_eq!(retry["type"], "retry_scheduled");
414 assert_eq!(retry["attempt"], 3);
415 assert_eq!(retry["maxRetries"], 10);
416 assert_eq!(retry["delayMs"], 8_000);
417 }
418
419 #[test]
420 fn events_round_trip_over_the_wire() {
421 for event in [
422 RemoteEvent::AgentStart,
423 RemoteEvent::MessageUpdate {
424 message: serde_json::json!({"role": "assistant"}),
425 assistant_message_event: serde_json::json!({"type": "text_delta"}),
426 event_type: "text_delta".into(),
427 content_index: Some(0),
428 delta: Some("hi".into()),
429 },
430 RemoteEvent::ToolExecutionEnd {
431 tool_call_id: "c1".into(),
432 tool_name: "read".into(),
433 is_error: true,
434 result: tool_result_json(&AgentToolResult::text("nope")),
435 },
436 ] {
437 let json = serde_json::to_value(&event).unwrap();
438 let back: RemoteEvent = serde_json::from_value(json).unwrap();
439 assert_eq!(event, back);
440 }
441 }
442
443 #[test]
444 fn command_and_response_round_trip() {
445 let cmd = RemoteCommand::Prompt {
446 content: "hi".into(),
447 };
448 let value = cmd.with_id("r1");
449 assert_eq!(value["type"], "prompt");
450 assert_eq!(value["content"], "hi");
451 assert_eq!(value["id"], "r1");
452
453 let response =
454 RemoteResponse::ok(serde_json::json!("r1"), serde_json::json!({"pong": true}));
455 let json = response.to_json();
456 assert_eq!(json["type"], "response");
457 assert_eq!(json["id"], "r1");
458 assert_eq!(json["status"], "ok");
459 let parsed: RemoteResponse = serde_json::from_value(json).unwrap();
460 assert!(parsed.is_ok());
461
462 let err = RemoteResponse::error(Value::Null, "boom").to_json();
463 assert_eq!(err["status"], "error");
464 assert_eq!(err["error"], "boom");
465 }
466}