Skip to main content

alwith_api/
client.rs

1//! Rust client for `alwith-runtime-v1`. The host (Desktop) and module subprocesses use it to talk
2//! to Runtime; the frames are identical to the TS
3//! `FrameHubPort`: requests `{id, method, params}`, responses `{id, result | error}`, events carry
4//! `type`.
5//!
6//! Two ways to connect: [`RuntimeClient::spawn`] starts an `alwith-runtime --listen stdio://` child
7//! process and takes over its pipes;
8//! [`RuntimeClient::over`] attaches to any pair of read/write streams (tests, unix socket). Events
9//! are broadcast via [`RuntimeClient::events`],
10//! and a slow consumer receives `Lagged`, equivalent to the stdio entry's `lagged` event: refill
11//! with `session_events`.
12
13use std::collections::HashMap;
14use std::process::Stdio;
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::sync::{Arc, Mutex};
17
18use crate::{AttachInfo, ResponseError, RuntimeEvent, SessionEventFrame, SessionLifecycleFrame, SessionRunState};
19use serde::{Deserialize, Serialize};
20use serde_json::Value;
21use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
22use tokio::process::{Child, Command};
23use tokio::sync::{mpsc, oneshot};
24use tokio::task::JoinHandle;
25
26/// An event frame coming from Runtime. Protocol events are decoded as [`RuntimeEvent`]; host
27/// extension events go to `Other`.
28///
29/// Delivery promise: every journaled frame of a session (outbound / acpNotification /
30/// acpAgentRequest carrying `session_id` + `sequence`)
31/// is delivered to subscribers in journal order, exactly once. Runtime drops events and sends
32/// `lagged` when the stdio host cannot keep up,
33/// and a slow ws connection is disconnected; here gaps are detected by sequence number, refilled
34/// first via `sessionEvents`, and only then are later frames delivered.
35/// A span that is gone from the journal too is reported as `Gap`, never pretending to be
36/// continuous.
37#[derive(Clone, Debug)]
38pub enum ClientEvent {
39    Runtime(RuntimeEvent),
40    /// A span that cannot be refilled: the journal has lost `from..=to` and nothing of it was
41    /// delivered; the app should reload this session.
42    Gap {
43        session_id: String,
44        from: u64,
45        to: u64,
46    },
47    Other(Value),
48    /// Read side ended: the peer exited or stdin hit EOF. Afterwards every `request` errors; a
49    /// module receiving this should wrap up.
50    Closed {
51        reason: String,
52    },
53}
54
55#[derive(Debug, Serialize)]
56struct RequestEnvelope<'a> {
57    id: u64,
58    method: &'a str,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    params: Option<Value>,
61}
62
63#[derive(Debug, Deserialize)]
64struct ResponseEnvelope {
65    id: u64,
66    #[serde(default)]
67    result: Value,
68    #[serde(default)]
69    error: Option<ResponseError>,
70}
71
72/// The answer to `cliStatus`.
73/// How to answer requests coming from the peer (Runtime): a module subprocess uses it to serve
74/// calls forwarded from `modules/<name>/<method>`.
75pub type RequestHandler = Arc<
76    dyn Fn(String, Value) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Value, ResponseError>> + Send>>
77        + Send
78        + Sync,
79>;
80
81/// Frames per page when refilling; Runtime returns at most 2000 per page.
82const REFILL_PAGE: usize = 2_000;
83/// Maximum extra refill rounds when frames slip in during a refill, after which Gap is reported.
84const REFILL_ROUNDS: u32 = 2;
85
86/// Where a session has got to in the journal.
87struct SessionCursor {
88    agent_id: String,
89    /// Last delivered sequence number; None before the first sequenced frame.
90    last: Option<u64>,
91    refilling: bool,
92    /// Live frames that arrived during a refill, delivered in order once it finishes.
93    parked: Vec<(u64, RuntimeEvent)>,
94}
95
96enum Verdict {
97    Deliver,
98    Duplicate,
99    Gap,
100}
101
102/// This frame relative to the last delivered one: the next number (or first sighting) is delivered;
103/// a 0 after a higher number means the engine replayed history
104/// (a resume with `replayFrom` clears the journal) and is also delivered; a number not above last
105/// is a duplicate; a later one means something in between was lost.
106fn verdict(last: Option<u64>, sequence: u64) -> Verdict {
107    match last {
108        None => Verdict::Deliver,
109        Some(last) if sequence == last + 1 => Verdict::Deliver,
110        Some(last) if sequence == 0 && last > 0 => Verdict::Deliver,
111        Some(last) if sequence <= last => Verdict::Duplicate,
112        Some(_) => Verdict::Gap,
113    }
114}
115
116/// Journal position on a live event: only the three journaled event kinds have one.
117fn journal_position(event: &RuntimeEvent) -> Option<(String, u64, String)> {
118    match event {
119        RuntimeEvent::Outbound { agent_id, session_id: Some(session_id), sequence: Some(sequence), .. }
120        | RuntimeEvent::AcpNotification { agent_id, session_id: Some(session_id), sequence: Some(sequence), .. }
121        | RuntimeEvent::AcpAgentRequest { agent_id, session_id: Some(session_id), sequence: Some(sequence), .. } => {
122            Some((session_id.clone(), *sequence, agent_id.clone()))
123        }
124        _ => None,
125    }
126}
127
128/// A frame in the journal, restored as it looked when it was a live event. Response frames (answers
129/// to our own requests) are not replayable.
130fn journaled_event(agent_id: &str, session_id: &str, frame: &SessionEventFrame) -> Option<RuntimeEvent> {
131    let position = (Some(session_id.to_string()), Some(frame.sequence));
132    if frame.direction == crate::FrameDirection::Out {
133        return Some(RuntimeEvent::Outbound {
134            agent_id: agent_id.to_string(),
135            line: frame.line.clone(),
136            session_id: position.0,
137            sequence: position.1,
138        });
139    }
140    let value: Value = serde_json::from_str(&frame.line).ok()?;
141    let method = value.get("method").and_then(Value::as_str)?.to_string();
142    let params = value.get("params").cloned().unwrap_or(Value::Null);
143    Some(match value.get("id") {
144        Some(id) if !id.is_null() => RuntimeEvent::AcpAgentRequest {
145            agent_id: agent_id.to_string(),
146            id: id.clone(),
147            method,
148            params,
149            session_id: position.0,
150            sequence: position.1,
151        },
152        _ => RuntimeEvent::AcpNotification {
153            agent_id: agent_id.to_string(),
154            method,
155            params,
156            session_id: position.0,
157            sequence: position.1,
158        },
159    })
160}
161
162struct Shared {
163    pending: Mutex<HashMap<u64, oneshot::Sender<Result<Value, ResponseError>>>>,
164    /// One unbounded queue per subscriber: nothing is lost in-process, a slow consumer just uses a
165    /// bit more memory, so there is no second drop point.
166    subscribers: Mutex<Vec<mpsc::UnboundedSender<ClientEvent>>>,
167    cursors: Mutex<HashMap<String, SessionCursor>>,
168    next_id: AtomicU64,
169    closed: Mutex<Option<String>>,
170    closed_notify: tokio::sync::Notify,
171    handler: Option<RequestHandler>,
172    /// Answering peer requests, sending events, and refill requests all go through this write end
173    /// (the same stream as `RuntimeClient::writer`).
174    outbound: mpsc::UnboundedSender<String>,
175}
176
177impl Shared {
178    fn emit(&self, event: ClientEvent) {
179        self.subscribers.lock().unwrap().retain(|subscriber| subscriber.send(event.clone()).is_ok());
180    }
181
182    /// Requests issued inside the read loop (for refill): written out via `outbound`, with
183    /// responses still paired by the read loop.
184    async fn request(self: &Arc<Self>, method: &str, params: Value) -> Result<Value, ResponseError> {
185        if let Some(reason) = self.closed.lock().unwrap().clone() {
186            return Err(ResponseError::connection_lost(format!("{method}: {reason}")));
187        }
188        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
189        let (sender, receiver) = oneshot::channel();
190        self.pending.lock().unwrap().insert(id, sender);
191        let mut line =
192            serde_json::to_string(&RequestEnvelope { id, method, params: Some(params) }).expect("serialize request");
193        line.push('\n');
194        if self.outbound.send(line).is_err() {
195            self.pending.lock().unwrap().remove(&id);
196            return Err(ResponseError::connection_lost(format!("{method}: connection closed")));
197        }
198        match receiver.await {
199            Ok(result) => result,
200            Err(_) => Err(ResponseError::connection_lost(format!("{method}: connection closed"))),
201        }
202    }
203
204    /// A live event with a journal position: deliver, queue, drop a duplicate, or start a refill
205    /// round for a hole before it.
206    fn route(self: &Arc<Self>, event: RuntimeEvent) {
207        let Some((session_id, sequence, agent_id)) = journal_position(&event) else {
208            self.emit(ClientEvent::Runtime(event));
209            return;
210        };
211        let mut cursors = self.cursors.lock().unwrap();
212        let cursor = cursors.entry(session_id.clone()).or_insert_with(|| SessionCursor {
213            agent_id: agent_id.clone(),
214            last: None,
215            refilling: false,
216            parked: Vec::new(),
217        });
218        cursor.agent_id = agent_id;
219        if cursor.refilling {
220            cursor.parked.push((sequence, event));
221            return;
222        }
223        match verdict(cursor.last, sequence) {
224            Verdict::Deliver => {
225                cursor.last = Some(sequence);
226                drop(cursors);
227                self.emit(ClientEvent::Runtime(event));
228            }
229            Verdict::Duplicate => {}
230            Verdict::Gap => {
231                cursor.refilling = true;
232                cursor.parked.push((sequence, event));
233                drop(cursors);
234                tokio::spawn(Arc::clone(self).refill(session_id, 0));
235            }
236        }
237    }
238
239    /// After Runtime says it dropped events (`lagged`) or after a reconnect: every followed session
240    /// refills from the last sequence seen.
241    fn refill_all(self: &Arc<Self>) {
242        let mut cursors = self.cursors.lock().unwrap();
243        for (session_id, cursor) in cursors.iter_mut() {
244            if cursor.last.is_some() && !cursor.refilling {
245                cursor.refilling = true;
246                tokio::spawn(Arc::clone(self).refill(session_id.clone(), 0));
247            }
248        }
249    }
250
251    /// Pull `sessionEvents` from the last delivered sequence onward, deliver in order, then deliver
252    /// the live frames accumulated during the refill.
253    /// A span missing from the journal too is reported once as a Gap.
254    async fn refill(self: Arc<Self>, session_id: String, round: u32) {
255        let (mut after, agent_id) = {
256            let cursors = self.cursors.lock().unwrap();
257            let cursor = &cursors[&session_id];
258            (cursor.last.unwrap_or(0), cursor.agent_id.clone())
259        };
260        let mut first = true;
261        loop {
262            let params = serde_json::json!({ "sessionId": session_id, "afterSequence": after, "limit": REFILL_PAGE });
263            let frames: Vec<SessionEventFrame> = match self.request("sessionEvents", params).await {
264                Ok(value) => match serde_json::from_value(value) {
265                    Ok(frames) => frames,
266                    Err(error) => {
267                        log::error!("[alwith-api] sessionEvents for {session_id}: unreadable page: {error}");
268                        break;
269                    }
270                },
271                // Connection gone: the reconnect path will refill again; deliver what has
272                // accumulated first.
273                Err(_) => break,
274            };
275            if frames.is_empty() {
276                break;
277            }
278            if first && frames[0].sequence > after + 1 {
279                self.emit(ClientEvent::Gap {
280                    session_id: session_id.clone(),
281                    from: after + 1,
282                    to: frames[0].sequence - 1,
283                });
284            }
285            first = false;
286            let page = frames.len();
287            for frame in frames {
288                if frame.sequence <= after {
289                    continue;
290                }
291                if let Some(event) = journaled_event(&agent_id, &session_id, &frame) {
292                    self.emit(ClientEvent::Runtime(event));
293                }
294                after = frame.sequence;
295                self.cursors.lock().unwrap().get_mut(&session_id).expect("cursor").last = Some(after);
296            }
297            if page < REFILL_PAGE {
298                break;
299            }
300        }
301        // Deliver the accumulated live frames in order; if holes remain after refilling, refill
302        // another round, reporting Gap only once rounds run out. Deliver while holding the lock:
303        // emit only puts into a queue and waits for no one.
304        // The lock's scope must end before the awaits below (block expression), otherwise this
305        // future is not Send.
306        let another_round = {
307            let mut cursors = self.cursors.lock().unwrap();
308            let cursor = cursors.get_mut(&session_id).expect("cursor");
309            let mut parked = std::mem::take(&mut cursor.parked);
310            parked.sort_by_key(|(sequence, _)| *sequence);
311            let mut still_missing = Vec::new();
312            for (sequence, event) in parked {
313                match verdict(cursor.last, sequence) {
314                    Verdict::Duplicate => continue,
315                    Verdict::Gap if round < REFILL_ROUNDS => {
316                        still_missing.push((sequence, event));
317                        continue;
318                    }
319                    Verdict::Gap => self.emit(ClientEvent::Gap {
320                        session_id: session_id.clone(),
321                        from: cursor.last.map_or(0, |last| last + 1),
322                        to: sequence - 1,
323                    }),
324                    Verdict::Deliver => {}
325                }
326                cursor.last = Some(sequence);
327                self.emit(ClientEvent::Runtime(event));
328            }
329            if still_missing.is_empty() {
330                cursor.refilling = false;
331                false
332            } else {
333                cursor.parked = still_missing;
334                true
335            }
336        };
337        if another_round {
338            Box::pin(self.refill(session_id, round + 1)).await;
339        }
340    }
341}
342
343pub struct RuntimeClient {
344    writer: Arc<tokio::sync::Mutex<Box<dyn AsyncWrite + Send + Unpin>>>,
345    shared: Arc<Shared>,
346    reader_task: JoinHandle<()>,
347    child: Mutex<Option<Child>>,
348}
349
350impl RuntimeClient {
351    /// Spawn the Runtime child process (stdin / stdout taken over, stderr inherited to the host
352    /// log) and wait for its `ready` line.
353    pub async fn spawn(mut command: Command) -> std::io::Result<Self> {
354        command.stdin(Stdio::piped()).stdout(Stdio::piped()).kill_on_drop(true);
355        let mut child = command.spawn()?;
356        let stdin = child.stdin.take().expect("piped stdin");
357        let stdout = child.stdout.take().expect("piped stdout");
358        let mut reader = BufReader::new(stdout);
359        let mut first = String::new();
360        loop {
361            first.clear();
362            if reader.read_line(&mut first).await? == 0 {
363                return Err(std::io::Error::other("alwith-runtime closed stdout before ready"));
364            }
365            let Ok(value) = serde_json::from_str::<Value>(first.trim()) else { continue };
366            if value.get("type").and_then(Value::as_str) == Some("ready") {
367                break;
368            }
369        }
370        let client = Self::over(reader, stdin);
371        *client.child.lock().unwrap() = Some(child);
372        Ok(client)
373    }
374
375    /// A connection over any pair of streams. One frame per line on `reader`; one line per frame on
376    /// `writer`.
377    pub fn over<R, W>(reader: R, writer: W) -> Self
378    where
379        R: AsyncRead + Send + Unpin + 'static,
380        W: AsyncWrite + Send + Unpin + 'static,
381    {
382        Self::over_with_handler(reader, writer, None)
383    }
384
385    /// Same as above, and also serves requests sent by the peer: a module subprocess is exactly
386    /// such a "client that also gets called".
387    pub fn over_with_handler<R, W>(reader: R, writer: W, handler: Option<RequestHandler>) -> Self
388    where
389        R: AsyncRead + Send + Unpin + 'static,
390        W: AsyncWrite + Send + Unpin + 'static,
391    {
392        let (outbound, outbound_receiver) = mpsc::unbounded_channel::<String>();
393        let shared = Arc::new(Shared {
394            pending: Mutex::new(HashMap::new()),
395            subscribers: Mutex::new(Vec::new()),
396            cursors: Mutex::new(HashMap::new()),
397            next_id: AtomicU64::new(1),
398            closed: Mutex::new(None),
399            closed_notify: tokio::sync::Notify::new(),
400            handler,
401            outbound,
402        });
403        let writer: Arc<tokio::sync::Mutex<Box<dyn AsyncWrite + Send + Unpin>>> =
404            Arc::new(tokio::sync::Mutex::new(Box::new(writer)));
405        tokio::spawn(write_loop(Arc::clone(&writer), outbound_receiver));
406        let reader_task = tokio::spawn(read_loop(BufReader::new(reader), Arc::clone(&shared)));
407        Self { writer, shared, reader_task, child: Mutex::new(None) }
408    }
409
410    /// Send an event frame (`{"type": ...}`). Events sent by a module are wrapped by Runtime as
411    /// `{"type":"module"}` and broadcast to apps.
412    pub fn emit(&self, event: Value) -> Result<(), ResponseError> {
413        let mut line = event.to_string();
414        line.push('\n');
415        self.shared.outbound.send(line).map_err(|_| ResponseError::connection_lost("connection closed"))
416    }
417
418    /// Subscribe to events. One unbounded queue per subscriber: nothing is lost in-process, `None`
419    /// means the client has been dropped.
420    /// Session events arrive in order, exactly once (see [`ClientEvent`]); a hole that cannot be
421    /// refilled is `ClientEvent::Gap`.
422    pub fn events(&self) -> mpsc::UnboundedReceiver<ClientEvent> {
423        let (sender, receiver) = mpsc::unbounded_channel();
424        self.shared.subscribers.lock().unwrap().push(sender);
425        receiver
426    }
427
428    /// Any method, including host extensions. An error is the `error` in the response frame (code /
429    /// message / data unchanged);
430    /// a lost connection is `ResponseError::CONNECTION_LOST`.
431    pub async fn request(&self, method: &str, params: Option<Value>) -> Result<Value, ResponseError> {
432        if let Some(reason) = self.shared.closed.lock().unwrap().clone() {
433            return Err(ResponseError::connection_lost(format!("{method}: {reason}")));
434        }
435        let id = self.shared.next_id.fetch_add(1, Ordering::Relaxed);
436        let (sender, receiver) = oneshot::channel();
437        self.shared.pending.lock().unwrap().insert(id, sender);
438        let mut line = serde_json::to_string(&RequestEnvelope { id, method, params }).expect("serialize request");
439        line.push('\n');
440        {
441            let mut writer = self.writer.lock().await;
442            if let Err(error) = writer.write_all(line.as_bytes()).await.and(writer.flush().await) {
443                self.shared.pending.lock().unwrap().remove(&id);
444                return Err(ResponseError::connection_lost(format!("{method}: write failed: {error}")));
445            }
446        }
447        match receiver.await {
448            Ok(result) => result,
449            Err(_) => Err(ResponseError::connection_lost(format!("{method}: connection closed"))),
450        }
451    }
452
453    async fn typed<T: serde::de::DeserializeOwned>(
454        &self, method: &str, params: Option<Value>,
455    ) -> Result<T, ResponseError> {
456        let value = self.request(method, params).await?;
457        serde_json::from_value(value).map_err(|error| ResponseError::runtime(format!("{method}: bad result: {error}")))
458    }
459
460    fn with_owner(mut params: Value, owner: Option<&str>) -> Value {
461        if let Some(owner) = owner {
462            params["owner"] = Value::String(owner.to_string());
463        }
464        params
465    }
466
467    /// `owner`: sub-owner under the connection (desktop window label); None means the connection
468    /// itself, which has write access to the agents of all sub-owners.
469    /// `launch` is the launch parameters on top of Runtime's engine table `{engine, cwd, args, env,
470    /// provider, model, connection}`.
471    /// Start an agent process. Errors if a process of the same name is still alive: switching
472    /// engine is "stop first, wait for the Exit event, then start".
473    pub async fn start(&self, agent_id: &str, launch: Value, owner: Option<&str>) -> Result<(), ResponseError> {
474        let params = Self::with_owner(serde_json::json!({ "agentId": agent_id, "launch": launch }), owner);
475        self.request("start", Some(params)).await.map(|_| ())
476    }
477
478    pub async fn send(&self, agent_id: &str, line: &str, owner: Option<&str>) -> Result<(), ResponseError> {
479        let params = Self::with_owner(serde_json::json!({ "agentId": agent_id, "line": line }), owner);
480        self.request("send", Some(params)).await.map(|_| ())
481    }
482
483    pub async fn stop(&self, agent_id: &str, owner: Option<&str>) -> Result<(), ResponseError> {
484        let params = Self::with_owner(serde_json::json!({ "agentId": agent_id }), owner);
485        self.request("stop", Some(params)).await.map(|_| ())
486    }
487
488    pub async fn attach_session(
489        &self, session_id: &str, owner: Option<&str>,
490    ) -> Result<Option<AttachInfo>, ResponseError> {
491        let params = Self::with_owner(serde_json::json!({ "sessionId": session_id }), owner);
492        self.typed("attachSession", Some(params)).await
493    }
494
495    pub async fn release_session(&self, session_id: &str, owner: Option<&str>) -> Result<(), ResponseError> {
496        let params = Self::with_owner(serde_json::json!({ "sessionId": session_id }), owner);
497        self.request("releaseSession", Some(params)).await.map(|_| ())
498    }
499
500    pub async fn stop_session(&self, session_id: &str, owner: Option<&str>) -> Result<(), ResponseError> {
501        let params = Self::with_owner(serde_json::json!({ "sessionId": session_id }), owner);
502        self.request("stopSession", Some(params)).await.map(|_| ())
503    }
504
505    pub async fn detach_owner(&self, owner: &str) -> Result<(), ResponseError> {
506        self.request("detachOwner", Some(serde_json::json!({ "owner": owner }))).await.map(|_| ())
507    }
508
509    pub async fn release_execution(&self, execution_id: &str) -> Result<bool, ResponseError> {
510        self.typed("releaseExecution", Some(serde_json::json!({ "executionId": execution_id }))).await
511    }
512
513    pub async fn agent_for_session(&self, session_id: &str) -> Result<Option<String>, ResponseError> {
514        self.typed("agentForSession", Some(serde_json::json!({ "sessionId": session_id }))).await
515    }
516
517    pub async fn session_replay(&self, session_id: &str) -> Result<Vec<String>, ResponseError> {
518        self.typed("sessionReplay", Some(serde_json::json!({ "sessionId": session_id }))).await
519    }
520
521    pub async fn set_slow_mode(&self, delay_ms: u64) -> Result<(), ResponseError> {
522        self.request("setSlowMode", Some(serde_json::json!({ "delayMs": delay_ms }))).await.map(|_| ())
523    }
524
525    /// `version`: the state the reader saw; an older confirmation does not clear a newer `done`.
526    /// `None` confirms whatever is current.
527    pub async fn mark_read(&self, session_id: &str, version: Option<u64>) -> Result<(), ResponseError> {
528        self.request("markRead", Some(serde_json::json!({ "sessionId": session_id, "version": version })))
529            .await
530            .map(|_| ())
531    }
532
533    pub async fn set_session_title(&self, session_id: &str, title: &str) -> Result<(), ResponseError> {
534        self.request("setSessionTitle", Some(serde_json::json!({ "sessionId": session_id, "title": title })))
535            .await
536            .map(|_| ())
537    }
538
539    pub async fn session_lifecycle(&self, session_id: &str) -> Result<Vec<SessionLifecycleFrame>, ResponseError> {
540        self.typed("sessionLifecycle", Some(serde_json::json!({ "sessionId": session_id }))).await
541    }
542
543    pub async fn session_events(
544        &self, session_id: &str, after_sequence: Option<u64>, limit: Option<usize>,
545    ) -> Result<Vec<SessionEventFrame>, ResponseError> {
546        self.typed(
547            "sessionEvents",
548            Some(serde_json::json!({ "sessionId": session_id, "afterSequence": after_sequence, "limit": limit })),
549        )
550        .await
551    }
552
553    pub async fn run_states(&self) -> Result<Vec<SessionRunState>, ResponseError> {
554        self.typed("runStates", None).await
555    }
556
557    /// Runtime's own version (the same number as `alwith-runtime --version`).
558    pub async fn version(&self) -> Result<String, ResponseError> {
559        self.typed("version", None).await
560    }
561
562    pub async fn agents(&self) -> Result<Vec<Value>, ResponseError> {
563        self.typed("agents", None).await
564    }
565
566    pub async fn ping(&self) -> Result<(), ResponseError> {
567        self.request("ping", None).await.map(|_| ())
568    }
569
570    // ---- ACP: Runtime speaks ACP v2 on your behalf. The raw send / inbound remain available. ----
571
572    /// Any ACP request; Runtime assigns the id and awaits the response. `request_id`: pick your own
573    /// id (needed only to retract with `$/cancel_request`).
574    pub async fn acp_request(
575        &self, agent_id: &str, method: &str, params: Value, request_id: Option<Value>, owner: Option<&str>,
576    ) -> Result<Value, ResponseError> {
577        let mut params = serde_json::json!({ "agentId": agent_id, "method": method, "params": params });
578        if let Some(id) = request_id {
579            params["id"] = id;
580        }
581        self.request("acpRequest", Some(Self::with_owner(params, owner))).await
582    }
583
584    pub async fn acp_notify(
585        &self, agent_id: &str, method: &str, params: Value, owner: Option<&str>,
586    ) -> Result<(), ResponseError> {
587        let params =
588            Self::with_owner(serde_json::json!({ "agentId": agent_id, "method": method, "params": params }), owner);
589        self.request("acpNotify", Some(params)).await.map(|_| ())
590    }
591
592    /// Answer an agent request (the `id` in `RuntimeEvent::AcpAgentRequest`).
593    pub async fn acp_respond(
594        &self, agent_id: &str, id: Value, result: Option<Value>, error: Option<Value>, owner: Option<&str>,
595    ) -> Result<(), ResponseError> {
596        let params = Self::with_owner(
597            serde_json::json!({ "agentId": agent_id, "id": id, "result": result, "error": error }),
598            owner,
599        );
600        self.request("acpRespond", Some(params)).await.map(|_| ())
601    }
602
603    /// ACP handshake, optionally with your own client info and capabilities; returns the cached
604    /// result if already done. Session methods handshake automatically with a default identity;
605    /// call this only to declare capabilities.
606    pub async fn initialize(
607        &self, agent_id: &str, info: Option<Value>, capabilities: Option<Value>, owner: Option<&str>,
608    ) -> Result<Value, ResponseError> {
609        let params = Self::with_owner(
610            serde_json::json!({ "agentId": agent_id, "info": info, "capabilities": capabilities }),
611            owner,
612        );
613        self.request("initialize", Some(params)).await
614    }
615
616    /// Create a session (handshaking first if needed). Returns the ACP `session/new` result,
617    /// containing `sessionId`.
618    pub async fn session_new(
619        &self, agent_id: &str, cwd: &str, mcp_servers: Vec<Value>, owner: Option<&str>,
620    ) -> Result<Value, ResponseError> {
621        let params =
622            Self::with_owner(serde_json::json!({ "agentId": agent_id, "cwd": cwd, "mcpServers": mcp_servers }), owner);
623        self.request("sessionNew", Some(params)).await
624    }
625
626    pub async fn session_load(
627        &self, agent_id: &str, session_id: &str, cwd: &str, mcp_servers: Vec<Value>, owner: Option<&str>,
628    ) -> Result<Value, ResponseError> {
629        let params = Self::with_owner(
630            serde_json::json!({ "agentId": agent_id, "sessionId": session_id, "cwd": cwd, "mcpServers": mcp_servers }),
631            owner,
632        );
633        self.request("sessionLoad", Some(params)).await
634    }
635
636    pub async fn session_resume(
637        &self, agent_id: &str, session_id: &str, cwd: &str, replay_from: Option<Value>, owner: Option<&str>,
638    ) -> Result<Value, ResponseError> {
639        let params = Self::with_owner(
640            serde_json::json!({ "agentId": agent_id, "sessionId": session_id, "cwd": cwd, "replayFrom": replay_from }),
641            owner,
642        );
643        self.request("sessionResume", Some(params)).await
644    }
645
646    pub async fn session_fork(
647        &self, agent_id: &str, session_id: &str, cwd: &str, mcp_servers: Vec<Value>, owner: Option<&str>,
648    ) -> Result<Value, ResponseError> {
649        let params = Self::with_owner(
650            serde_json::json!({ "agentId": agent_id, "sessionId": session_id, "cwd": cwd, "mcpServers": mcp_servers }),
651            owner,
652        );
653        self.request("sessionFork", Some(params)).await
654    }
655
656    /// One turn, returns only when the turn ends (the ACP result); progress arrives as
657    /// `AcpNotification` events.
658    pub async fn prompt(
659        &self, agent_id: &str, session_id: &str, prompt: Value, owner: Option<&str>,
660    ) -> Result<Value, ResponseError> {
661        let params = Self::with_owner(
662            serde_json::json!({ "agentId": agent_id, "sessionId": session_id, "prompt": prompt }),
663            owner,
664        );
665        self.request("prompt", Some(params)).await
666    }
667
668    pub async fn cancel(&self, agent_id: &str, session_id: &str, owner: Option<&str>) -> Result<(), ResponseError> {
669        let params = Self::with_owner(serde_json::json!({ "agentId": agent_id, "sessionId": session_id }), owner);
670        self.request("cancel", Some(params)).await.map(|_| ())
671    }
672
673    /// Kill the child process immediately (exit fallback; the normal path uses `shutdown`). No-op
674    /// if there is no child process.
675    pub fn kill(&self) {
676        if let Some(child) = self.child.lock().unwrap().as_mut() {
677            let _ = child.start_kill();
678        }
679    }
680
681    /// Whether the connection is already down (read side ended or child process exited).
682    pub fn closed(&self) -> Option<String> {
683        self.shared.closed.lock().unwrap().clone()
684    }
685
686    /// Exit chain: first ask Runtime to leave (the `shutdown` method; it gives modules and agents a
687    /// 2-second wind-down window and then kill_all), and wait for the connection to end;
688    /// if it has not ended within `grace`, force-kill the child process. Killing directly would
689    /// leave the agent's spawned process groups alive, so courtesy first, force second.
690    /// Difference from [`Self::shutdown`]: it does not consume self, so a host holding an
691    /// `Arc<RuntimeClient>` can call it too.
692    pub async fn close(&self, grace: std::time::Duration) {
693        let deadline = tokio::time::Instant::now() + grace;
694        // Register the wait first, then check whether it is already closed: `notify_waiters` only
695        // wakes registered waiters, otherwise a receipt arriving together with EOF would be missed.
696        let closed = self.shared.closed_notify.notified();
697        tokio::pin!(closed);
698        closed.as_mut().enable();
699        if self.closed().is_none() {
700            let _ = tokio::time::timeout_at(deadline, self.request("shutdown", None)).await;
701            if self.closed().is_none() {
702                let _ = tokio::time::timeout_at(deadline, closed).await;
703            }
704        }
705        if let Some(child) = self.child.lock().unwrap().as_mut() {
706            let _ = child.start_kill();
707        }
708    }
709
710    /// Close the write end (Runtime kill_all's and exits on EOF), wait for the read side to end;
711    /// reap the child process if there is one.
712    pub async fn shutdown(mut self) {
713        {
714            let mut writer = self.writer.lock().await;
715            let _ = writer.shutdown().await;
716        }
717        let _ = tokio::time::timeout(std::time::Duration::from_secs(5), &mut self.reader_task).await;
718        if let Some(mut child) = self.child.lock().unwrap().take() {
719            let _ = child.start_kill();
720        }
721    }
722}
723
724impl Drop for RuntimeClient {
725    fn drop(&mut self) {
726        self.reader_task.abort();
727    }
728}
729
730async fn write_loop(
731    writer: Arc<tokio::sync::Mutex<Box<dyn AsyncWrite + Send + Unpin>>>, mut receiver: mpsc::UnboundedReceiver<String>,
732) {
733    while let Some(line) = receiver.recv().await {
734        let mut writer = writer.lock().await;
735        if writer.write_all(line.as_bytes()).await.is_err() || writer.flush().await.is_err() {
736            break;
737        }
738    }
739}
740
741async fn read_loop<R: AsyncRead + Unpin>(mut reader: BufReader<R>, shared: Arc<Shared>) {
742    let mut line = String::new();
743    let mut handlers = tokio::task::JoinSet::<String>::new();
744    let mut active = std::collections::HashMap::<String, tokio::task::AbortHandle>::new();
745    let reason = loop {
746        while let Some(result) = handlers.try_join_next() {
747            if let Ok(id) = result {
748                active.remove(&id);
749            }
750        }
751        line.clear();
752        match reader.read_line(&mut line).await {
753            Ok(0) => break "connection closed".to_string(),
754            Ok(_) => {}
755            Err(error) => break format!("read failed: {error}"),
756        }
757        let text = line.trim();
758        if text.is_empty() {
759            continue;
760        }
761        let value: Value = match serde_json::from_str(text) {
762            Ok(value) => value,
763            Err(error) => {
764                log::warn!("[alwith-api] unparseable frame: {error}");
765                continue;
766            }
767        };
768        // Recognize events first: an event frame may also carry `method` (e.g. acpNotification) and
769        // must not be taken for a request from the peer.
770        if value.get("type").is_some() {
771            match value.get("type").and_then(Value::as_str) {
772                // Runtime dropped events on this connection: refill every followed session.
773                Some("lagged") => shared.refill_all(),
774                _ => match serde_json::from_value::<RuntimeEvent>(value.clone()) {
775                    Ok(event) => shared.route(event),
776                    Err(_) => shared.emit(ClientEvent::Other(value)),
777                },
778            }
779            continue;
780        }
781        if value.get("method").is_some() {
782            let id = value.get("id").cloned().unwrap_or(Value::Null);
783            let method = value.get("method").and_then(Value::as_str).unwrap_or_default().to_string();
784            let params = value.get("params").cloned().unwrap_or(Value::Null);
785            if method == "_runtime/cancel" {
786                if let Some(handle) = params.get("id").and_then(|id| active.remove(&id.to_string())) {
787                    handle.abort();
788                }
789                continue;
790            }
791            let key = id.to_string();
792            if !id.is_null() && active.get(&key).is_some_and(|handle| !handle.is_finished()) {
793                continue; // A duplicate active request must never execute twice.
794            }
795            let shared = Arc::clone(&shared);
796            let task_key = key.clone();
797            let task = handlers.spawn(async move {
798                let response = match &shared.handler {
799                    Some(handler) => match handler(method.clone(), params).await {
800                        Ok(result) => serde_json::json!({ "id": id, "result": result, "error": null }),
801                        Err(error) => serde_json::json!({ "id": id, "result": null, "error": error }),
802                    },
803                    None => serde_json::json!({ "id": id, "result": null, "error": ResponseError::method_not_found(&method) }),
804                };
805                if !id.is_null() {
806                    let mut line = response.to_string();
807                    line.push('\n');
808                    let _ = shared.outbound.send(line);
809                }
810                task_key
811            });
812            active.insert(key, task);
813            continue;
814        }
815        match serde_json::from_value::<ResponseEnvelope>(value) {
816            Ok(response) => {
817                if let Some(sender) = shared.pending.lock().unwrap().remove(&response.id) {
818                    let _ = sender.send(match response.error {
819                        Some(error) => Err(error),
820                        None => Ok(response.result),
821                    });
822                }
823            }
824            Err(error) => log::warn!("[alwith-api] frame is neither event nor response: {error}"),
825        }
826    };
827    handlers.abort_all();
828    while handlers.join_next().await.is_some() {}
829    *shared.closed.lock().unwrap() = Some(reason.clone());
830    let pending: Vec<_> = shared.pending.lock().unwrap().drain().collect();
831    for (_, sender) in pending {
832        let _ = sender.send(Err(ResponseError::connection_lost(reason.clone())));
833    }
834    shared.emit(ClientEvent::Closed { reason });
835    shared.closed_notify.notify_waiters();
836}
837
838#[cfg(test)]
839mod tests {
840    //! Runs against a fake peer (a small loop answering frames per the protocol), no real Runtime
841    //! needed; round-trip tests against a real Runtime live in the Runtime repo.
842    use super::*;
843    use tokio::io::{AsyncWriteExt, DuplexStream};
844
845    /// Fake peer: `ping` answers `"pong"`, `nope` answers an error, a `lines` request makes it
846    /// broadcast one lines event; event frames received are written back as is.
847    fn fake_peer() -> (RuntimeClient, JoinHandle<Vec<Value>>) {
848        let (client_reader, peer_writer) = tokio::io::duplex(64 * 1024);
849        let (peer_reader, client_writer) = tokio::io::duplex(64 * 1024);
850        let task = tokio::spawn(run_fake_peer(peer_reader, peer_writer));
851        (RuntimeClient::over(client_reader, client_writer), task)
852    }
853
854    async fn run_fake_peer(reader: DuplexStream, mut writer: DuplexStream) -> Vec<Value> {
855        let mut lines = BufReader::new(reader).lines();
856        let mut seen_events = Vec::new();
857        while let Ok(Some(line)) = lines.next_line().await {
858            let value: Value = serde_json::from_str(&line).unwrap();
859            let reply = match value.get("method").and_then(Value::as_str) {
860                Some("ping") => serde_json::json!({ "id": value["id"], "result": "pong", "error": null }),
861                Some("nope") => {
862                    serde_json::json!({ "id": value["id"], "result": null, "error": { "code": -32601, "message": "unknown method nope" } })
863                }
864                Some("lines") => {
865                    let event = serde_json::json!({ "type": "inbound", "agentId": "a", "lines": ["x", "y"] });
866                    writer.write_all(format!("{event}\n").as_bytes()).await.unwrap();
867                    serde_json::json!({ "id": value["id"], "result": null, "error": null })
868                }
869                Some("hang") => continue,
870                Some(_) => unreachable!("unexpected request {line}"),
871                None => {
872                    seen_events.push(value);
873                    continue;
874                }
875            };
876            writer.write_all(format!("{reply}\n").as_bytes()).await.unwrap();
877        }
878        seen_events
879    }
880
881    /// Fake peer with a journal: `sessionEvents` answers from the given frames by afterSequence;
882    /// after the `shutdown` receipt it closes its own write end.
883    /// The test uses `push` to inject event frames into the client.
884    fn fake_peer_with_journal(journal: Vec<Value>) -> (RuntimeClient, mpsc::UnboundedSender<Value>) {
885        let (client_reader, mut peer_writer) = tokio::io::duplex(64 * 1024);
886        let (peer_reader, client_writer) = tokio::io::duplex(64 * 1024);
887        let (push, mut pushed) = mpsc::unbounded_channel::<Value>();
888        tokio::spawn(async move {
889            let mut lines = BufReader::new(peer_reader).lines();
890            loop {
891                let reply = tokio::select! {
892                    Some(event) = pushed.recv() => event,
893                    line = lines.next_line() => {
894                        let Ok(Some(line)) = line else { break };
895                        let value: Value = serde_json::from_str(&line).unwrap();
896                        match value.get("method").and_then(Value::as_str) {
897                            Some("sessionEvents") => {
898                                let after = value["params"]["afterSequence"].as_u64();
899                                let frames: Vec<&Value> = journal
900                                    .iter()
901                                    .filter(|frame| after.is_none_or(|after| frame["sequence"].as_u64().unwrap() > after))
902                                    .collect();
903                                serde_json::json!({ "id": value["id"], "result": frames, "error": null })
904                            }
905                            Some("shutdown") => {
906                                let receipt = serde_json::json!({ "id": value["id"], "result": null, "error": null });
907                                peer_writer.write_all(format!("{receipt}\n").as_bytes()).await.unwrap();
908                                break;
909                            }
910                            _ => serde_json::json!({ "id": value["id"], "result": null, "error": null }),
911                        }
912                    }
913                };
914                peer_writer.write_all(format!("{reply}\n").as_bytes()).await.unwrap();
915            }
916        });
917        (RuntimeClient::over(client_reader, client_writer), push)
918    }
919
920    fn update(sequence: u64) -> Value {
921        serde_json::json!({
922            "type": "acpNotification", "agentId": "a", "method": "session/update",
923            "params": { "sessionId": "s-1", "update": { "sessionUpdate": "agent_message_chunk", "content": { "type": "text", "text": format!("t{sequence}") } } },
924            "sessionId": "s-1", "sequence": sequence
925        })
926    }
927
928    fn journaled(sequence: u64) -> Value {
929        let line = serde_json::json!({
930            "jsonrpc": "2.0", "method": "session/update",
931            "params": { "sessionId": "s-1", "update": { "sessionUpdate": "agent_message_chunk", "content": { "type": "text", "text": format!("t{sequence}") } } }
932        });
933        serde_json::json!({ "sequence": sequence, "recordedAt": 1, "direction": "in", "line": line.to_string() })
934    }
935
936    /// Collect `count` session events (or a Gap), returning the sequence of numbers; a Gap is
937    /// recorded as `None`.
938    async fn collect(events: &mut mpsc::UnboundedReceiver<ClientEvent>, count: usize) -> Vec<Option<u64>> {
939        let mut seen = Vec::new();
940        while seen.len() < count {
941            let event = tokio::time::timeout(std::time::Duration::from_secs(2), events.recv())
942                .await
943                .expect("event before deadline")
944                .expect("client alive");
945            match event {
946                ClientEvent::Runtime(RuntimeEvent::AcpNotification { sequence, .. }) => seen.push(sequence),
947                ClientEvent::Gap { from, to, .. } => {
948                    assert_eq!((from, to), (1, 3));
949                    seen.push(None);
950                }
951                _ => {}
952            }
953        }
954        seen
955    }
956
957    #[tokio::test]
958    async fn a_skipped_sequence_is_refilled_from_the_journal_before_later_frames() {
959        let (client, push) = fake_peer_with_journal(vec![journaled(0), journaled(1), journaled(2), journaled(3)]);
960        let mut events = client.events();
961        push.send(update(0)).unwrap();
962        push.send(update(1)).unwrap();
963        push.send(update(3)).unwrap();
964        push.send(update(4)).unwrap();
965        assert_eq!(collect(&mut events, 5).await, vec![Some(0), Some(1), Some(2), Some(3), Some(4)]);
966    }
967
968    #[tokio::test]
969    async fn what_the_journal_lost_is_reported_as_a_gap_not_hidden() {
970        let (client, push) = fake_peer_with_journal(vec![journaled(4), journaled(5)]);
971        let mut events = client.events();
972        push.send(update(0)).unwrap();
973        push.send(update(5)).unwrap();
974        assert_eq!(collect(&mut events, 4).await, vec![Some(0), None, Some(4), Some(5)]);
975    }
976
977    #[tokio::test]
978    async fn lagged_refills_every_followed_session_and_duplicates_are_dropped() {
979        let (client, push) = fake_peer_with_journal(vec![journaled(0), journaled(1), journaled(2)]);
980        let mut events = client.events();
981        push.send(update(0)).unwrap();
982        push.send(serde_json::json!({ "type": "lagged", "dropped": 2 })).unwrap();
983        push.send(update(2)).unwrap();
984        push.send(update(3)).unwrap();
985        // 0 live; after lagged refill 1, 2; live 2 is a duplicate, dropped; 3 joins on.
986        assert_eq!(collect(&mut events, 4).await, vec![Some(0), Some(1), Some(2), Some(3)]);
987    }
988
989    #[tokio::test]
990    async fn close_asks_the_runtime_to_shut_down_and_waits_for_the_connection_to_end() {
991        let (client, _push) = fake_peer_with_journal(Vec::new());
992        let mut events = client.events();
993        tokio::time::timeout(std::time::Duration::from_secs(2), client.close(std::time::Duration::from_secs(5)))
994            .await
995            .expect("close returns once the peer hangs up, not after the grace");
996        assert!(client.closed().is_some());
997        assert!(matches!(events.recv().await.unwrap(), ClientEvent::Closed { .. }));
998    }
999
1000    #[tokio::test]
1001    async fn requests_round_trip_and_errors_arrive_as_sent() {
1002        let (client, _peer) = fake_peer();
1003        assert_eq!(client.request("ping", None).await.unwrap(), Value::String("pong".into()));
1004        let error = client.request("nope", None).await.unwrap_err();
1005        assert_eq!(error, ResponseError::new(ResponseError::METHOD_NOT_FOUND, "unknown method nope"));
1006    }
1007
1008    #[tokio::test]
1009    async fn protocol_events_are_typed_and_others_pass_through() {
1010        let (client, _peer) = fake_peer();
1011        let mut events = client.events();
1012        client.request("lines", None).await.unwrap();
1013        match events.recv().await.unwrap() {
1014            ClientEvent::Runtime(RuntimeEvent::Inbound { agent_id, lines }) => {
1015                assert_eq!(agent_id, "a");
1016                assert_eq!(lines, vec!["x".to_string(), "y".to_string()]);
1017            }
1018            other => panic!("unexpected {other:?}"),
1019        }
1020    }
1021
1022    #[tokio::test]
1023    async fn peer_requests_reach_the_handler_and_emitted_events_reach_the_peer() {
1024        let (client_reader, mut peer_writer) = tokio::io::duplex(64 * 1024);
1025        let (peer_reader, client_writer) = tokio::io::duplex(64 * 1024);
1026        let handler: RequestHandler = Arc::new(|method, params| {
1027            Box::pin(async move {
1028                if method == "echo" { Ok(params) } else { Err(ResponseError::runtime(format!("unknown {method}"))) }
1029            })
1030        });
1031        let client = RuntimeClient::over_with_handler(client_reader, client_writer, Some(handler));
1032        let mut peer_lines = BufReader::new(peer_reader).lines();
1033
1034        peer_writer.write_all(b"{\"id\":5,\"method\":\"echo\",\"params\":{\"x\":1}}\n").await.unwrap();
1035        let reply: Value = serde_json::from_str(&peer_lines.next_line().await.unwrap().unwrap()).unwrap();
1036        assert_eq!(reply, serde_json::json!({ "id": 5, "result": { "x": 1 }, "error": null }));
1037
1038        peer_writer.write_all(b"{\"id\":6,\"method\":\"nope\"}\n").await.unwrap();
1039        let reply: Value = serde_json::from_str(&peer_lines.next_line().await.unwrap().unwrap()).unwrap();
1040        assert_eq!(
1041            reply["error"],
1042            serde_json::json!({ "code": ResponseError::RUNTIME, "message": "unknown nope", "data": null })
1043        );
1044
1045        client.emit(serde_json::json!({ "type": "statusChanged", "statuses": [] })).unwrap();
1046        let event: Value = serde_json::from_str(&peer_lines.next_line().await.unwrap().unwrap()).unwrap();
1047        assert_eq!(event["type"], "statusChanged");
1048    }
1049
1050    #[tokio::test]
1051    async fn closing_the_stream_fails_pending_requests_and_broadcasts_closed() {
1052        let (client_reader, peer_writer) = tokio::io::duplex(64 * 1024);
1053        let (_peer_reader, client_writer) = tokio::io::duplex(64 * 1024);
1054        let client = RuntimeClient::over(client_reader, client_writer);
1055        let mut events = client.events();
1056        let pending = tokio::spawn({
1057            let client = Arc::new(client);
1058            let client_for_task = Arc::clone(&client);
1059            async move { (client, client_for_task.request("hang", None).await) }
1060        });
1061        drop(peer_writer);
1062        let (client, result) = pending.await.unwrap();
1063        assert_eq!(result.unwrap_err(), ResponseError::connection_lost("hang: connection closed"));
1064        assert_eq!(client.closed().as_deref(), Some("connection closed"));
1065        assert!(matches!(events.recv().await.unwrap(), ClientEvent::Closed { .. }));
1066        assert_eq!(client.request("ping", None).await.unwrap_err().code, ResponseError::CONNECTION_LOST);
1067    }
1068    #[tokio::test]
1069    async fn peer_handler_can_request_on_same_connection_and_eof_cancels_it() {
1070        let (client_stream, peer_stream) = tokio::io::duplex(8192);
1071        let (reader, writer) = tokio::io::split(client_stream);
1072        let (peer_reader, mut peer_writer) = tokio::io::split(peer_stream);
1073        let slot = Arc::new(std::sync::OnceLock::<std::sync::Weak<RuntimeClient>>::new());
1074        let handler_slot = Arc::clone(&slot);
1075        let entered = Arc::new(tokio::sync::Notify::new());
1076        let dropped = Arc::new(tokio::sync::Notify::new());
1077        struct Signal(Arc<tokio::sync::Notify>);
1078        impl Drop for Signal {
1079            fn drop(&mut self) {
1080                self.0.notify_one();
1081            }
1082        }
1083        let handler_entered = Arc::clone(&entered);
1084        let handler_dropped = Arc::clone(&dropped);
1085        let handler: RequestHandler = Arc::new(move |method, _| {
1086            let client = handler_slot.get().unwrap().upgrade().unwrap();
1087            let entered = Arc::clone(&handler_entered);
1088            let dropped = Arc::clone(&handler_dropped);
1089            Box::pin(async move {
1090                if method == "nested" {
1091                    return client.request("ping", None).await;
1092                }
1093                let _signal = Signal(dropped);
1094                entered.notify_one();
1095                std::future::pending().await
1096            })
1097        });
1098        let client = Arc::new(RuntimeClient::over_with_handler(reader, writer, Some(handler)));
1099        assert!(slot.set(Arc::downgrade(&client)).is_ok());
1100        let mut lines = BufReader::new(peer_reader).lines();
1101        peer_writer.write_all(b"{\"id\":101,\"method\":\"nested\"}\n").await.unwrap();
1102        let nested: Value = serde_json::from_str(
1103            &tokio::time::timeout(std::time::Duration::from_secs(2), lines.next_line())
1104                .await
1105                .unwrap()
1106                .unwrap()
1107                .unwrap(),
1108        )
1109        .unwrap();
1110        assert_eq!(nested["method"], "ping");
1111        peer_writer
1112            .write_all(format!("{}\n", serde_json::json!({"id":nested["id"],"result":"pong"})).as_bytes())
1113            .await
1114            .unwrap();
1115        let response: Value = serde_json::from_str(
1116            &tokio::time::timeout(std::time::Duration::from_secs(2), lines.next_line())
1117                .await
1118                .unwrap()
1119                .unwrap()
1120                .unwrap(),
1121        )
1122        .unwrap();
1123        assert_eq!(response["id"], 101);
1124        assert_eq!(response["result"], "pong");
1125        peer_writer.write_all(b"{\"id\":102,\"method\":\"wait\"}\n").await.unwrap();
1126        tokio::time::timeout(std::time::Duration::from_secs(2), entered.notified()).await.unwrap();
1127        peer_writer.write_all(b"{\"method\":\"_runtime/cancel\",\"params\":{\"id\":102}}\n").await.unwrap();
1128        tokio::time::timeout(std::time::Duration::from_secs(2), dropped.notified()).await.unwrap();
1129        peer_writer.write_all(b"{\"id\":103,\"method\":\"wait\"}\n").await.unwrap();
1130        tokio::time::timeout(std::time::Duration::from_secs(2), entered.notified()).await.unwrap();
1131        peer_writer.shutdown().await.unwrap();
1132        tokio::time::timeout(std::time::Duration::from_secs(2), dropped.notified()).await.unwrap();
1133    }
1134}