Skip to main content

oxi/rpc_mode/
utils.rs

1//! Utility types: paste handling and RPC client.
2
3use anyhow::{Context, Result};
4use serde_json::Value;
5use std::collections::HashMap;
6use std::io::{BufRead, Write};
7use tokio::sync::oneshot;
8
9use super::protocol::*;
10
11// ============================================================================
12// Bracketed Paste Detection
13// ============================================================================
14
15/// Bracketed paste state
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum PasteState {
18    /// Normal mode
19    Normal,
20    /// Bracketed paste mode - collecting paste data
21    Pasting,
22}
23
24/// Bracketed paste handler
25pub struct PasteHandler {
26    /// Current paste state.
27    pub state: PasteState,
28    /// Buffer for collecting paste data.
29    pub buffer: Vec<u8>,
30    /// Bracketed paste start sequence: ESC [ 2 0 0 ~
31    start_sequence: Vec<u8>,
32    /// Bracketed paste end sequence: ESC [ 2 0 1 ~
33    end_sequence: Vec<u8>,
34}
35
36impl PasteHandler {
37    /// Create a new paste handler
38    pub fn new() -> Self {
39        Self {
40            state: PasteState::Normal,
41            buffer: Vec::new(),
42            start_sequence: vec![0x1B, 0x5B, 0x32, 0x30, 0x30, 0x7E], // \x1b[200~
43            end_sequence: vec![0x1B, 0x5B, 0x32, 0x30, 0x31, 0x7E],   // \x1b[201~
44        }
45    }
46
47    /// Reset to normal mode
48    pub fn reset(&mut self) {
49        self.state = PasteState::Normal;
50        self.buffer.clear();
51    }
52
53    /// Check current state
54    pub fn state(&self) -> PasteState {
55        self.state.clone()
56    }
57
58    /// Get collected paste buffer
59    pub fn buffer(&self) -> &[u8] {
60        &self.buffer
61    }
62
63    /// Process a byte, returning Some(byte) if it should be passed through
64    /// or None if it was part of a paste sequence
65    pub fn process_byte(&mut self, byte: u8) -> Option<u8> {
66        match self.state {
67            PasteState::Normal => {
68                if (self.buffer.is_empty() && byte == 0x1B)
69                    || (!self.buffer.is_empty() && self.buffer[0] == 0x1B && byte == 0x5B)
70                    || (self.buffer.len() >= 2
71                        && self.buffer[0] == 0x1B
72                        && self.buffer[1] == 0x5B
73                        && byte == 0x32)
74                    || (self.buffer.len() >= 3
75                        && self.buffer[0] == 0x1B
76                        && self.buffer[1] == 0x5B
77                        && self.buffer[2] == 0x32
78                        && byte == 0x30)
79                    || (self.buffer.len() >= 4
80                        && self.buffer[0] == 0x1B
81                        && self.buffer[1] == 0x5B
82                        && self.buffer[2] == 0x32
83                        && self.buffer[3] == 0x30
84                        && byte == 0x30)
85                {
86                    self.buffer.push(byte);
87                    None
88                } else if self.buffer.len() >= 5
89                    && self.buffer[0] == 0x1B
90                    && self.buffer[1] == 0x5B
91                    && self.buffer[2] == 0x32
92                    && self.buffer[3] == 0x30
93                    && self.buffer[4] == 0x30
94                    && byte == 0x7E
95                {
96                    // Paste start detected
97                    self.buffer.clear();
98                    self.state = PasteState::Pasting;
99                    None
100                } else {
101                    let first_byte = self.buffer.first().copied();
102                    self.buffer.clear();
103                    first_byte
104                }
105            }
106            PasteState::Pasting => {
107                if (self.buffer.is_empty() && byte == 0x1B)
108                    || (!self.buffer.is_empty() && self.buffer[0] == 0x1B && byte == 0x5B)
109                    || (self.buffer.len() >= 2
110                        && self.buffer[0] == 0x1B
111                        && self.buffer[1] == 0x5B
112                        && byte == 0x32)
113                    || (self.buffer.len() >= 3
114                        && self.buffer[0] == 0x1B
115                        && self.buffer[1] == 0x5B
116                        && self.buffer[2] == 0x32
117                        && byte == 0x30)
118                    || (self.buffer.len() >= 4
119                        && self.buffer[0] == 0x1B
120                        && self.buffer[1] == 0x5B
121                        && self.buffer[2] == 0x32
122                        && self.buffer[3] == 0x30
123                        && byte == 0x31)
124                {
125                    self.buffer.push(byte);
126                    None
127                } else if self.buffer.len() >= 5
128                    && self.buffer[0] == 0x1B
129                    && self.buffer[1] == 0x5B
130                    && self.buffer[2] == 0x32
131                    && self.buffer[3] == 0x30
132                    && self.buffer[4] == 0x31
133                    && byte == 0x7E
134                {
135                    // Paste end detected
136                    self.buffer.clear();
137                    self.state = PasteState::Normal;
138                    None
139                } else {
140                    self.buffer.push(byte);
141                    None
142                }
143            }
144        }
145    }
146
147    /// Check if buffer ends with a sequence
148    pub fn ends_with(&self, sequence: &[u8]) -> bool {
149        if self.buffer.len() < sequence.len() {
150            return false;
151        }
152        let end_pos = self.buffer.len() - sequence.len();
153        &self.buffer[end_pos..] == sequence
154    }
155
156    /// Extract image data from clipboard paste
157    pub fn extract_image_data(&self) -> Option<Vec<u8>> {
158        let buffer = self.buffer();
159        if buffer.len() < 8 {
160            return None;
161        }
162
163        // PNG magic bytes
164        if buffer.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
165            return Some(buffer.to_vec());
166        }
167
168        // JPEG magic bytes
169        if buffer.starts_with(&[0xFF, 0xD8, 0xFF]) {
170            return Some(buffer.to_vec());
171        }
172
173        // Binary data heuristic
174        if buffer.iter().take(100).filter(|&&b| b == 0).count() > 5 {
175            return Some(buffer.to_vec());
176        }
177
178        None
179    }
180}
181
182impl Default for PasteHandler {
183    fn default() -> Self {
184        Self::new()
185    }
186}
187
188// ============================================================================
189// RPC Client (for programmatic access to an oxi RPC server)
190// ============================================================================
191
192/// Configuration for the RPC client.
193#[derive(Debug, Clone)]
194pub struct RpcClientConfig {
195    /// Path to the oxi binary
196    pub binary_path: String,
197    /// Working directory for the agent
198    pub cwd: Option<String>,
199    /// Environment variables
200    pub env: Vec<(String, String)>,
201    /// Provider to use
202    pub provider: Option<String>,
203    /// Model ID to use
204    pub model: Option<String>,
205    /// Additional CLI arguments
206    pub args: Vec<String>,
207}
208
209impl Default for RpcClientConfig {
210    fn default() -> Self {
211        Self {
212            binary_path: "oxi".to_string(),
213            cwd: None,
214            env: Vec::new(),
215            provider: None,
216            model: None,
217            args: Vec::new(),
218        }
219    }
220}
221
222/// Event listener callback type
223pub type BoxedEventListener = Box<dyn Fn(RpcEvent) + Send>;
224
225/// RPC Client for programmatic access to an oxi agent.
226///
227/// Spawns the agent in RPC mode and provides a typed API for all operations.
228pub struct RpcClient {
229    config: RpcClientConfig,
230    child: Option<std::process::Child>,
231    line_reader: JsonlLineReader,
232    pending_requests: HashMap<String, oneshot::Sender<RpcResponse>>,
233    request_counter: u64,
234    event_listeners: Vec<BoxedEventListener>,
235    stderr_buffer: String,
236}
237
238impl RpcClient {
239    /// Create a new RPC client with the given configuration.
240    pub fn new(config: RpcClientConfig) -> Self {
241        Self {
242            config,
243            child: None,
244            line_reader: JsonlLineReader::new(),
245            pending_requests: HashMap::new(),
246            request_counter: 0,
247            event_listeners: Vec::new(),
248            stderr_buffer: String::new(),
249        }
250    }
251
252    /// Start the RPC agent process.
253    pub fn start(&mut self) -> Result<()> {
254        if self.child.is_some() {
255            anyhow::bail!("Client already started");
256        }
257
258        let mut args = vec!["--mode".to_string(), "rpc".to_string()];
259        if let Some(ref provider) = self.config.provider {
260            args.push("--provider".to_string());
261            args.push(provider.clone());
262        }
263        if let Some(ref model) = self.config.model {
264            args.push("--model".to_string());
265            args.push(model.clone());
266        }
267        args.extend(self.config.args.iter().cloned());
268
269        let mut cmd = std::process::Command::new(&self.config.binary_path);
270        cmd.args(&args)
271            .stdin(std::process::Stdio::piped())
272            .stdout(std::process::Stdio::piped())
273            .stderr(std::process::Stdio::piped());
274
275        if let Some(ref cwd) = self.config.cwd {
276            cmd.current_dir(cwd);
277        }
278
279        for (key, value) in &self.config.env {
280            cmd.env(key, value);
281        }
282
283        let child = cmd.spawn().context("Failed to spawn oxi RPC process")?;
284        self.child = Some(child);
285
286        Ok(())
287    }
288
289    /// Stop the RPC agent process.
290    pub fn stop(&mut self) -> Result<()> {
291        if let Some(mut child) = self.child.take() {
292            let _ = child.kill();
293            let _ = child.wait();
294        }
295        self.pending_requests.clear();
296        Ok(())
297    }
298
299    /// Subscribe to agent events.
300    pub fn on_event<F>(&mut self, listener: F)
301    where
302        F: Fn(RpcEvent) + Send + 'static,
303    {
304        self.event_listeners.push(Box::new(listener));
305    }
306
307    /// Get collected stderr output.
308    pub fn stderr(&self) -> &str {
309        &self.stderr_buffer
310    }
311
312    /// Send a prompt to the agent.
313    pub fn prompt(&mut self, message: &str) -> Result<()> {
314        Self::require_success(self.send_and_wait(serde_json::json!({
315            "type": "prompt",
316            "message": message
317        }))?)
318    }
319
320    /// Send a steer message.
321    pub fn steer(&mut self, message: &str) -> Result<()> {
322        Self::require_success(self.send_and_wait(serde_json::json!({
323            "type": "steer",
324            "message": message
325        }))?)
326    }
327
328    /// Send a follow-up message.
329    pub fn follow_up(&mut self, message: &str) -> Result<()> {
330        Self::require_success(self.send_and_wait(serde_json::json!({
331            "type": "follow_up",
332            "message": message
333        }))?)
334    }
335
336    /// Abort current operation.
337    pub fn abort(&mut self) -> Result<()> {
338        Self::require_success(self.send_and_wait(serde_json::json!({ "type": "abort" }))?)
339    }
340
341    /// Start a new session.
342    pub fn new_session(&mut self, parent_session: Option<&str>) -> Result<RpcResponse> {
343        let mut cmd = serde_json::json!({ "type": "new_session" });
344        if let Some(parent) = parent_session {
345            cmd["parent_session"] = Value::String(parent.to_string());
346        }
347        self.send_and_wait(cmd)
348    }
349
350    /// Get current session state.
351    pub fn get_state(&mut self) -> Result<RpcResponse> {
352        self.send_and_wait(serde_json::json!({ "type": "get_state" }))
353    }
354
355    /// Set model by provider and ID.
356    pub fn set_model(&mut self, provider: &str, model_id: &str) -> Result<RpcResponse> {
357        self.send_and_wait(serde_json::json!({
358            "type": "set_model",
359            "provider": provider,
360            "model_id": model_id
361        }))
362    }
363
364    /// Cycle to next model.
365    pub fn cycle_model(&mut self) -> Result<RpcResponse> {
366        self.send_and_wait(serde_json::json!({ "type": "cycle_model" }))
367    }
368
369    /// Get available models.
370    pub fn get_available_models(&mut self) -> Result<RpcResponse> {
371        self.send_and_wait(serde_json::json!({ "type": "get_available_models" }))
372    }
373
374    /// Set thinking level.
375    pub fn set_thinking_level(&mut self, level: &str) -> Result<()> {
376        Self::require_success(self.send_and_wait(serde_json::json!({
377            "type": "set_thinking_level",
378            "level": level
379        }))?)
380    }
381
382    /// Cycle thinking level.
383    pub fn cycle_thinking_level(&mut self) -> Result<RpcResponse> {
384        self.send_and_wait(serde_json::json!({ "type": "cycle_thinking_level" }))
385    }
386
387    /// Set steering mode.
388    pub fn set_steering_mode(&mut self, mode: &str) -> Result<()> {
389        Self::require_success(self.send_and_wait(serde_json::json!({
390            "type": "set_steering_mode",
391            "mode": mode
392        }))?)
393    }
394
395    /// Set follow-up mode.
396    pub fn set_follow_up_mode(&mut self, mode: &str) -> Result<()> {
397        Self::require_success(self.send_and_wait(serde_json::json!({
398            "type": "set_follow_up_mode",
399            "mode": mode
400        }))?)
401    }
402
403    /// Compact session context.
404    pub fn compact(&mut self, custom_instructions: Option<&str>) -> Result<RpcResponse> {
405        let mut cmd = serde_json::json!({ "type": "compact" });
406        if let Some(instructions) = custom_instructions {
407            cmd["custom_instructions"] = Value::String(instructions.to_string());
408        }
409        self.send_and_wait(cmd)
410    }
411
412    /// Set auto-compaction.
413    pub fn set_auto_compaction(&mut self, enabled: bool) -> Result<()> {
414        Self::require_success(self.send_and_wait(serde_json::json!({
415            "type": "set_auto_compaction",
416            "enabled": enabled
417        }))?)
418    }
419
420    /// Set auto-retry.
421    pub fn set_auto_retry(&mut self, enabled: bool) -> Result<()> {
422        Self::require_success(self.send_and_wait(serde_json::json!({
423            "type": "set_auto_retry",
424            "enabled": enabled
425        }))?)
426    }
427
428    /// Abort retry.
429    pub fn abort_retry(&mut self) -> Result<()> {
430        Self::require_success(self.send_and_wait(serde_json::json!({ "type": "abort_retry" }))?)
431    }
432
433    /// Send an arbitrary RPC command and require success. Exposed for
434    /// integration tests that need to verify error handling for unknown
435    /// command types (e.g. `rpc_client_surfaces_unsupported_command_errors`).
436    pub fn send_raw(&mut self, command: Value) -> Result<()> {
437        Self::require_success(self.send_and_wait(command)?)
438    }
439
440    /// Execute a bash command.
441    pub fn bash(&mut self, command: &str) -> Result<RpcResponse> {
442        self.send_and_wait(serde_json::json!({
443            "type": "bash",
444            "command": command
445        }))
446    }
447
448    /// Abort bash.
449    pub fn abort_bash(&mut self) -> Result<()> {
450        Self::require_success(self.send_and_wait(serde_json::json!({ "type": "abort_bash" }))?)
451    }
452
453    /// Get session statistics.
454    pub fn get_session_stats(&mut self) -> Result<RpcResponse> {
455        self.send_and_wait(serde_json::json!({ "type": "get_session_stats" }))
456    }
457
458    /// Export session to HTML.
459    pub fn export_html(&mut self, output_path: Option<&str>) -> Result<RpcResponse> {
460        let mut cmd = serde_json::json!({ "type": "export_html" });
461        if let Some(path) = output_path {
462            cmd["output_path"] = Value::String(path.to_string());
463        }
464        self.send_and_wait(cmd)
465    }
466
467    /// Switch to a different session.
468    pub fn switch_session(&mut self, session_path: &str) -> Result<RpcResponse> {
469        self.send_and_wait(serde_json::json!({
470            "type": "switch_session",
471            "session_path": session_path
472        }))
473    }
474
475    /// Fork from a specific message.
476    pub fn fork(&mut self, entry_id: &str) -> Result<RpcResponse> {
477        self.send_and_wait(serde_json::json!({
478            "type": "fork",
479            "entry_id": entry_id
480        }))
481    }
482
483    /// Clone the current branch.
484    pub fn clone_session(&mut self) -> Result<RpcResponse> {
485        self.send_and_wait(serde_json::json!({ "type": "clone" }))
486    }
487
488    /// Get messages available for forking.
489    pub fn get_fork_messages(&mut self) -> Result<RpcResponse> {
490        self.send_and_wait(serde_json::json!({ "type": "get_fork_messages" }))
491    }
492
493    /// Get text of last assistant message.
494    pub fn get_last_assistant_text(&mut self) -> Result<RpcResponse> {
495        self.send_and_wait(serde_json::json!({ "type": "get_last_assistant_text" }))
496    }
497
498    /// Set the session display name.
499    pub fn set_session_name(&mut self, name: &str) -> Result<()> {
500        Self::require_success(self.send_and_wait(serde_json::json!({
501            "type": "set_session_name",
502            "name": name
503        }))?)
504    }
505
506    /// Get all messages.
507    pub fn get_messages(&mut self) -> Result<RpcResponse> {
508        self.send_and_wait(serde_json::json!({ "type": "get_messages" }))
509    }
510
511    /// Get available commands.
512    pub fn get_commands(&mut self) -> Result<RpcResponse> {
513        self.send_and_wait(serde_json::json!({ "type": "get_commands" }))
514    }
515
516    // ── Internal helpers ─────────────────────────────────────────
517
518    fn require_success(response: RpcResponse) -> Result<()> {
519        match response {
520            RpcResponse::Response { success: true, .. } => Ok(()),
521            RpcResponse::Response { error, command, .. } => anyhow::bail!(
522                "RPC command {command} failed: {}",
523                error.unwrap_or_else(|| "unknown error".to_string())
524            ),
525            RpcResponse::ExtensionUiRequest(_) => {
526                anyhow::bail!("unexpected extension UI request response")
527            }
528        }
529    }
530
531    /// Generate the next request ID.
532    fn next_request_id(&mut self) -> String {
533        self.request_counter += 1;
534        format!("req_{}", self.request_counter)
535    }
536
537    /// Send a command and wait for the response.
538    fn send_and_wait(&mut self, mut command: Value) -> Result<RpcResponse> {
539        let id = self.next_request_id();
540        if let Some(obj) = command.as_object_mut() {
541            obj.insert("id".to_string(), Value::String(id.clone()));
542        }
543
544        let line = serialize_json_line(&command);
545
546        // Write command
547        {
548            let child = self.child.as_mut().context("Client not started")?;
549            if let Some(ref mut stdin) = child.stdin {
550                stdin
551                    .write_all(line.as_bytes())
552                    .context("Failed to write to stdin")?;
553                stdin.flush().context("Failed to flush stdin")?;
554            }
555        }
556
557        // Read responses until we find ours
558        let child = self.child.as_mut().context("Client not started")?;
559        if let Some(ref mut stdout) = child.stdout {
560            let mut buf_reader = std::io::BufReader::new(std::io::BufReader::new(stdout));
561            let mut buf = String::new();
562            loop {
563                buf.clear();
564                match buf_reader.read_line(&mut buf) {
565                    Ok(0) => anyhow::bail!("EOF while waiting for response"),
566                    Ok(_) => {
567                        let trimmed = buf.trim();
568                        if trimmed.is_empty() {
569                            continue;
570                        }
571                        match parse_json_line(trimmed) {
572                            Ok(value) => {
573                                if let Some(obj) = value.as_object() {
574                                    // Check if it's our response
575                                    if obj.get("type").and_then(|v| v.as_str()) == Some("response")
576                                        && obj.get("id").and_then(|v| v.as_str())
577                                            == Some(id.as_str())
578                                    {
579                                        let success = obj
580                                            .get("success")
581                                            .and_then(|v| v.as_bool())
582                                            .unwrap_or(false);
583                                        let cmd_name = obj
584                                            .get("command")
585                                            .and_then(|v| v.as_str())
586                                            .unwrap_or("")
587                                            .to_string();
588                                        let data = obj.get("data").cloned();
589                                        let error = obj
590                                            .get("error")
591                                            .and_then(|v| v.as_str())
592                                            .map(|s| s.to_string());
593                                        return Ok(RpcResponse::Response {
594                                            id: Some(id.clone()),
595                                            command: cmd_name,
596                                            success,
597                                            data,
598                                            error,
599                                        });
600                                    }
601
602                                    // Otherwise it's an event — parse and notify listeners
603                                    let event_type = obj
604                                        .get("type")
605                                        .and_then(|v: &Value| v.as_str())
606                                        .unwrap_or("");
607                                    let event = match event_type {
608                                        "agent_start" => Some(RpcEvent::AgentStart),
609                                        "agent_end" => Some(RpcEvent::AgentEnd),
610                                        "thinking" => Some(RpcEvent::Thinking),
611                                        "thinking_end" => Some(RpcEvent::ThinkingEnd),
612                                        "tool_call_delta" => {
613                                            let tool_call_id = obj
614                                                .get("tool_call_id")
615                                                .and_then(|v: &Value| v.as_str())
616                                                .unwrap_or("")
617                                                .to_string();
618                                            let args_delta = obj
619                                                .get("args_delta")
620                                                .and_then(|v: &Value| v.as_str())
621                                                .unwrap_or("")
622                                                .to_string();
623                                            Some(RpcEvent::ToolCallDelta {
624                                                tool_call_id,
625                                                args_delta,
626                                            })
627                                        }
628                                        "error" => {
629                                            let msg = obj
630                                                .get("message")
631                                                .and_then(|v: &Value| v.as_str())
632                                                .unwrap_or("")
633                                                .to_string();
634                                            Some(RpcEvent::Error { message: msg })
635                                        }
636                                        "text_chunk" => {
637                                            let text = obj
638                                                .get("text")
639                                                .and_then(|v: &Value| v.as_str())
640                                                .unwrap_or("")
641                                                .to_string();
642                                            Some(RpcEvent::TextChunk { text })
643                                        }
644                                        "tool_start" => {
645                                            let tool = obj
646                                                .get("tool")
647                                                .and_then(|v: &Value| v.as_str())
648                                                .unwrap_or("")
649                                                .to_string();
650                                            Some(RpcEvent::ToolStart { tool })
651                                        }
652                                        "tool_end" => {
653                                            let tool = obj
654                                                .get("tool")
655                                                .and_then(|v: &Value| v.as_str())
656                                                .unwrap_or("")
657                                                .to_string();
658                                            Some(RpcEvent::ToolEnd { tool })
659                                        }
660                                        _ => None,
661                                    };
662                                    if let Some(event) = event {
663                                        for listener in &self.event_listeners {
664                                            listener(event.clone());
665                                        }
666                                    }
667                                }
668                            }
669                            Err(_) => continue,
670                        }
671                    }
672                    Err(e) => anyhow::bail!("Error reading stdout: {}", e),
673                }
674            }
675        } else {
676            anyhow::bail!("No stdout available");
677        }
678    }
679
680    /// Handle an incoming JSON value during send_and_wait.
681    /// Returns Ok(response) if it matches our request ID, otherwise processes as event.
682    fn handle_incoming_value(&mut self, expected_id: &str, value: Value) -> Result<RpcResponse> {
683        if let Some(obj) = value.as_object() {
684            // Check if it's our response
685            if obj.get("type").and_then(|v| v.as_str()) == Some("response")
686                && obj.get("id").and_then(|v| v.as_str()) == Some(expected_id)
687            {
688                let success = obj
689                    .get("success")
690                    .and_then(|v| v.as_bool())
691                    .unwrap_or(false);
692                let command = obj
693                    .get("command")
694                    .and_then(|v| v.as_str())
695                    .unwrap_or("")
696                    .to_string();
697                let data = obj.get("data").cloned();
698                let error = obj
699                    .get("error")
700                    .and_then(|v| v.as_str())
701                    .map(|s| s.to_string());
702                return Ok(RpcResponse::Response {
703                    id: Some(expected_id.to_string()),
704                    command,
705                    success,
706                    data,
707                    error,
708                });
709            }
710
711            // Otherwise it's an event — try to parse and notify listeners
712            let event_type = obj
713                .get("type")
714                .and_then(|v: &Value| v.as_str())
715                .unwrap_or("");
716            let event = match event_type {
717                "agent_start" => Some(RpcEvent::AgentStart),
718                "agent_end" => Some(RpcEvent::AgentEnd),
719                "thinking" => Some(RpcEvent::Thinking),
720                "thinking_end" => Some(RpcEvent::ThinkingEnd),
721                "tool_call_delta" => {
722                    let tool_call_id = obj
723                        .get("tool_call_id")
724                        .and_then(|v: &Value| v.as_str())
725                        .unwrap_or("")
726                        .to_string();
727                    let args_delta = obj
728                        .get("args_delta")
729                        .and_then(|v: &Value| v.as_str())
730                        .unwrap_or("")
731                        .to_string();
732                    Some(RpcEvent::ToolCallDelta {
733                        tool_call_id,
734                        args_delta,
735                    })
736                }
737                "error" => {
738                    let msg = obj
739                        .get("message")
740                        .and_then(|v: &Value| v.as_str())
741                        .unwrap_or("")
742                        .to_string();
743                    Some(RpcEvent::Error { message: msg })
744                }
745                "text_chunk" => {
746                    let text = obj
747                        .get("text")
748                        .and_then(|v: &Value| v.as_str())
749                        .unwrap_or("")
750                        .to_string();
751                    Some(RpcEvent::TextChunk { text })
752                }
753                "tool_start" => {
754                    let tool = obj
755                        .get("tool")
756                        .and_then(|v: &Value| v.as_str())
757                        .unwrap_or("")
758                        .to_string();
759                    Some(RpcEvent::ToolStart { tool })
760                }
761                "tool_end" => {
762                    let tool = obj
763                        .get("tool")
764                        .and_then(|v: &Value| v.as_str())
765                        .unwrap_or("")
766                        .to_string();
767                    Some(RpcEvent::ToolEnd { tool })
768                }
769                _ => None,
770            };
771            if let Some(event) = event {
772                for listener in &self.event_listeners {
773                    listener(event.clone());
774                }
775            }
776        }
777
778        // Not our response — loop continues (caller will read next line)
779        // This is a hack: we return an error to signal "continue reading"
780        // Better approach: use a loop inside send_and_wait
781        anyhow::bail!("Internal: not our response, caller should retry")
782    }
783}
784
785impl Drop for RpcClient {
786    fn drop(&mut self) {
787        let _ = self.stop();
788    }
789}