Skip to main content

protocol_droid/adapters/secondary/acp/
peer.rs

1//! `AcpStdioPeer` — the stdio transport for ACP.
2//!
3//! # The wire
4//!
5//! ACP over stdio is newline-delimited JSON-RPC 2.0.
6//! Every message is one JSON object, terminated by
7//! `\n`. The agent's stdout carries the same shape.
8//! There is no Content-Length framing (that is the
9//! HTTP+SSE transport, which the v1.25.0 spec
10//! does not define for stdio).
11//!
12//! # The peer
13//!
14//! A peer is two halves:
15//!
16//! - A **reader task**, spawned at construction,
17//!   that owns the child's stdout and routes
18//!   incoming envelopes to the pending reply
19//!   channels. The reader is the only place that
20//!   reads from the pipe; the application layer
21//!   does not touch stdout directly.
22//!
23//! - A **writer task**, also spawned at
24//!   construction, that owns the child's stdin.
25//!   The application layer hands a serialised
26//!   envelope to the peer; the peer hands the
27//!   bytes to the writer via a bounded channel.
28//!   One writer, one mpsc — the channel is the
29//!   back-pressure point.
30//!
31//! Splitting the two halves means the application
32//! layer can hold a `&AcpStdioPeer` and call
33//! `send_request` from any task without
34//! re-entering the same lock that the reader is
35//! holding.
36//!
37//! # Why `kill_on_drop`
38//!
39//! A child process whose parent has gone away is an
40//! orphan; an ACP agent is, by design, a long-
41//! lived daemon that holds the conversation. A
42//! `AcpStdioPeer` that drops without killing its
43//! child leaves the agent answering into a void.
44//! `tokio::process::Child::kill_on_drop(true)` is
45//! the flag that takes the child with the peer.
46//!
47//! # The pending-reply table
48//!
49//! The peer tracks every in-flight request in a
50//! `HashMap<RequestId, oneshot::Sender<...>>`. The
51//! reader task looks up the id on every reply and
52//! hands the value to the oneshot; the application
53//! layer awaits the oneshot from `send_request`.
54//! A `HashMap` is the right shape: the in-flight
55//! set is small, lookups are O(1), and the
56//! concurrent-access ergonomics are exactly what
57//! `tokio::sync::Mutex` provides.
58//!
59//! # Timeouts
60//!
61//! The default request timeout is 30s. A peer
62//! that has to talk to a slow agent can be
63//! rebuilt with a longer timeout via
64//! [`AcpStdioPeerBuilder`]. The timeout fires
65//! from the application layer's task, not
66//! from the reader — the reader just sees an
67//! `Err` and discards the channel.
68//!
69//! # `HashMap::get_disjoint_mut` and the in-flight set
70//!
71//! The reader sometimes has to look up multiple
72//! ids in one pass (a batched reply). The
73//! `HashMap::get_disjoint_mut` method
74//! (stabilized in 1.86) is exactly that
75//! pattern, and the peer uses it. The rule
76//! the project carries is "before hand-rolling
77//! a helper, check whether it was stabilized
78//! between 1.86 and 1.98"; this is the case
79//! the rule was written for.
80
81use std::collections::HashMap;
82use std::collections::HashSet;
83use std::process::Stdio;
84use std::sync::Arc;
85use std::sync::Mutex as StdMutex;
86use std::sync::atomic::{AtomicU64, Ordering};
87use std::time::Duration;
88
89use super::AgentRequest;
90
91use serde::Serialize;
92use serde::de::DeserializeOwned;
93use serde_json::Value;
94use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
95use tokio::process::{Child, ChildStdin, ChildStdout, Command};
96use tokio::sync::{Mutex, mpsc, oneshot};
97
98use super::error::AcpPeerError;
99use super::reply::AcpReply;
100use super::stream::{AgentRequestStream, NotificationStream};
101use crate::domain::acp::method::AcpMethod;
102use crate::domain::acp::notification::SessionNotification;
103
104/// A `SpawnSpec` is the configuration for
105/// `AcpStdioPeer::spawn`. The `program` and
106/// `args` are the only required fields; the
107/// rest take the obvious defaults.
108#[derive(Debug, Clone)]
109pub struct SpawnSpec {
110    /// The program to run (e.g. `"node"`).
111    pub program: String,
112    /// The arguments (e.g. `["reference-agent.js"]`).
113    pub args: Vec<String>,
114    /// The default request timeout. `None` is
115    /// 30s.
116    pub timeout: Option<Duration>,
117}
118
119impl SpawnSpec {
120    /// A `SpawnSpec` from a `program` and a list
121    /// of `args`. The timeout takes the default.
122    pub fn new(program: impl Into<String>, args: Vec<String>) -> Self {
123        Self {
124            program: program.into(),
125            args,
126            timeout: None,
127        }
128    }
129
130    /// Override the default 30s timeout.
131    #[must_use]
132    pub const fn with_timeout(mut self, timeout: Duration) -> Self {
133        self.timeout = Some(timeout);
134        self
135    }
136}
137
138/// A pending reply channel: the oneshot the
139/// reader will hand the value to. The
140/// `method` field is recorded for diagnostics
141/// (a future increment that wants to log
142/// "request foo/bar timed out" rather than
143/// "request 7 timed out" uses the method).
144struct Pending {
145    /// The oneshot sender the application
146    /// task is awaiting.
147    tx: oneshot::Sender<Result<AcpReply, AcpPeerError>>,
148    /// The method the request carried. The
149    /// reader keeps the method so a reply
150    /// without a matching id can be matched
151    /// by the consumer after the fact (it
152    /// cannot today, but the field is the
153    /// shape a future feature would need).
154    method: AcpMethod,
155}
156
157impl std::fmt::Debug for Pending {
158    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        // The `tx` field is `Debug` but the
160        // `oneshot::Sender` does not
161        // implement it; the `method` field
162        // is the only useful diagnostic.
163        // Format by hand rather than
164        // deriving `Debug`, which would
165        // require a custom `Debug` impl
166        // for `oneshot::Sender` that
167        // doesn't exist.
168        formatter
169            .debug_struct("Pending")
170            .field("method", &self.method)
171            .finish_non_exhaustive()
172    }
173}
174
175/// The stdio peer.
176///
177/// Constructed via [`AcpStdioPeer::spawn`]. The
178/// peer owns the child process and the two
179/// tasks; dropping the peer kills the child
180/// and stops the tasks.
181pub struct AcpStdioPeer {
182    /// The pending-reply table. The reader
183    /// task holds a clone of the `Arc`; the
184    /// application layer holds this one.
185    pending: Arc<Mutex<HashMap<u64, Pending>>>,
186    /// The next request id. The peer is the
187    /// only writer; an `AtomicU64` is enough.
188    next_id: AtomicU64,
189    /// The writer task's input. The application
190    /// hands a fully-serialised envelope to the
191    /// channel; the writer writes the bytes.
192    write_tx: mpsc::Sender<Vec<u8>>,
193    /// The notification stream's consumer end.
194    /// The reader task pushes
195    /// [`SessionNotification`]s into the
196    /// matching `notifications_tx`; the
197    /// application layer awaits the next
198    /// notification with
199    /// [`recv_notification`][Self::recv_notification].
200    ///
201    /// The receiver is behind a
202    /// `tokio::sync::Mutex` so the
203    /// `recv_notification` method can take
204    /// `&self` and the consumer can race
205    /// the typed reply against the next
206    /// notification in a
207    /// `tokio::select!`. The lock is held
208    /// across the inner `recv().await`,
209    /// which is safe for the consumer's
210    /// main loop: only one task owns the
211    /// peer at a time, and the reader
212    /// task pushes to the matching
213    /// `notifications_tx` (not the
214    /// receiver) so the consumer never
215    /// blocks the agent.
216    notifications_rx: Arc<tokio::sync::Mutex<mpsc::Receiver<SessionNotification>>>,
217    /// The set of request ids the peer has
218    /// sent. The reader task uses this set
219    /// to differentiate client-originating
220    /// replies (the `id` is in the set) from
221    /// agent-originating requests (the
222    /// `id` is NOT in the set, because the
223    /// agent allocated it). The set is
224    /// pruned on every reply: the reader
225    /// removes the id once the reply is
226    /// dispatched to the pending oneshot.
227    ///
228    /// `std::sync::Mutex` is the right
229    /// choice: the lock is held for a
230    /// few microseconds (a `HashSet`
231    /// insert or remove), not across
232    /// an await, and a tokio mutex
233    /// would be a heavier allocation
234    /// for the same critical section.
235    sent_ids: Arc<StdMutex<HashSet<u64>>>,
236    /// The agent-originating request
237    /// stream. The agent sends requests
238    /// like `fs/read_text_file` and
239    /// `terminal/create` to the client;
240    /// the consumer reads them with
241    /// [`recv_agent_request`][Self::recv_agent_request]
242    /// and replies with
243    /// [`send_reply`][Self::send_reply].
244    ///
245    /// The receiver is behind a
246    /// `tokio::sync::Mutex` for the
247    /// same reason as `notifications_rx`:
248    /// the consumer can race the typed
249    /// reply against the next
250    /// agent-originating request in a
251    /// `tokio::select!`.
252    agent_requests_rx: Arc<tokio::sync::Mutex<mpsc::Receiver<AgentRequest>>>,
253    /// The default request timeout.
254    timeout: Duration,
255    /// The child process. The `kill_on_drop`
256    /// flag (set on the `Command` before
257    /// `spawn`) is what takes the child
258    /// when the peer is dropped. The peer
259    /// does not call `wait()` on it; the
260    /// reader and writer tasks own the
261    /// pipes, and the process is reaped
262    /// by tokio's machinery when the
263    /// `Child` value is dropped with the
264    /// peer.
265    #[expect(
266        dead_code,
267        reason = "the field is held to keep the `Child` alive; the `kill_on_drop` machinery reads it on drop"
268    )]
269    child: Child,
270}
271
272impl std::fmt::Debug for AcpStdioPeer {
273    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274        // The `Child` and the
275        // `mpsc::Receiver<SessionNotification>`
276        // do not implement `Debug`; format
277        // the peer by hand, omitting the
278        // child and the receiver.
279        formatter
280            .debug_struct("AcpStdioPeer")
281            .field("next_id", &self.next_id)
282            .field("timeout", &self.timeout)
283            .finish_non_exhaustive()
284    }
285}
286
287impl AcpStdioPeer {
288    /// Spawn the child, wire the reader and
289    /// writer tasks, and return the peer.
290    ///
291    /// The function is synchronous on purpose:
292    /// `tokio::process::Command::spawn` does
293    /// not await, and the reader / writer
294    /// tasks it spawns are the ones that
295    /// actually park. Returning a sync
296    /// `Result` keeps the future `Send` and
297    /// avoids a needless state-machine frame
298    /// at the seam.
299    ///
300    /// The spec is taken by reference because
301    /// the consumer is the owner — a `Read`
302    /// `SpawnSpec` lets the caller reuse it
303    /// for diagnostics or a retry without
304    /// cloning.
305    pub fn spawn(spec: &SpawnSpec) -> Result<Self, AcpPeerError> {
306        let mut command = Command::new(&spec.program);
307        command
308            .args(&spec.args)
309            .stdin(Stdio::piped())
310            .stdout(Stdio::piped())
311            .stderr(Stdio::piped())
312            .kill_on_drop(true);
313        let mut child: Child = command.spawn().map_err(AcpPeerError::Spawn)?;
314        let stdin: ChildStdin = child
315            .stdin
316            .take()
317            .ok_or_else(|| AcpPeerError::Spawn(io_other("stdin was not piped")))?;
318        let stdout: ChildStdout = child
319            .stdout
320            .take()
321            .ok_or_else(|| AcpPeerError::Spawn(io_other("stdout was not piped")))?;
322        let pending: Arc<Mutex<HashMap<u64, Pending>>> = Arc::new(Mutex::new(HashMap::new()));
323        let (write_tx, write_rx) = mpsc::channel::<Vec<u8>>(32);
324        // The notification stream. The reader
325        // pushes [`SessionNotification`]s;
326        // the application layer pulls them with
327        // `recv_notification`. A bounded
328        // channel (32) gives the consumer a
329        // moment to catch up before the agent
330        // starts to back-pressure. The receiver
331        // is wrapped in a `tokio::sync::Mutex`
332        // so `recv_notification` can take
333        // `&self` and race against the typed
334        // reply in a `tokio::select!`.
335        let (notifications_tx, notifications_rx) = mpsc::channel::<SessionNotification>(32);
336        let notifications_rx = Arc::new(tokio::sync::Mutex::new(notifications_rx));
337
338        // The agent-originating request
339        // stream. The agent sends requests
340        // like `fs/read_text_file` and
341        // `terminal/create` to the client;
342        // the consumer reads them with
343        // `recv_agent_request` and replies
344        // with `send_reply`. A bounded
345        // channel (8) is enough: the
346        // consumer's handler is synchronous
347        // with the request's arrival, and
348        // agent-originating requests are
349        // rare compared to the consumer's
350        // own request rate.
351        let (agent_requests_tx, agent_requests_rx) = mpsc::channel::<AgentRequest>(8);
352        let agent_requests_rx = Arc::new(tokio::sync::Mutex::new(agent_requests_rx));
353
354        // The set of request ids the peer
355        // has sent. Empty at start; the
356        // peer inserts on every
357        // `send_request` and the reader
358        // removes on every reply.
359        let sent_ids: Arc<StdMutex<HashSet<u64>>> = Arc::new(StdMutex::new(HashSet::new()));
360
361        // The reader task. Owns the child's
362        // stdout. Reads one line at a time
363        // (newline-delimited JSON-RPC), parses,
364        // and dispatches to the matching
365        // pending reply (for client-originating
366        // requests) or to the
367        // `agent_requests_tx` (for
368        // agent-originating requests).
369        // Notifications (no `id` field) go
370        // to the `notifications_tx`.
371        spawn_reader(
372            BufReader::new(stdout),
373            Arc::clone(&pending),
374            notifications_tx,
375            agent_requests_tx,
376            Arc::clone(&sent_ids),
377        );
378
379        // The writer task. Owns the child's
380        // stdin. Receives pre-serialised bytes
381        // and writes them with a trailing `\n`.
382        spawn_writer(stdin, write_rx);
383
384        // The child is now under the writer's and reader's care. We
385        // keep the `Child` handle in the peer so the `kill_on_drop`
386        // machinery takes the process when the peer drops. The peer
387        // does not call `wait()` on it; the reader and writer tasks
388        // own the pipes, and the OS process is reaped when the
389        // `Child` value is dropped with the peer.
390        //
391        // (A future increment that wants the exit status on the
392        // error path can store the `Child` in an
393        // `Arc<Mutex<Option<Child>>>` and `try_wait()` on the
394        // timeout path. For this increment the timeout is the only
395        // failure mode and the `Timeout` variant is enough.)
396
397        Ok(Self {
398            pending,
399            next_id: AtomicU64::new(1),
400            write_tx,
401            notifications_rx,
402            sent_ids,
403            agent_requests_rx,
404            timeout: spec.timeout.unwrap_or(Duration::from_secs(30)),
405            child,
406        })
407    }
408
409    /// Send a request and await the reply. The
410    /// `params` are serialised with the same
411    /// `serde_json` the consumer uses
412    /// everywhere else, so the application
413    /// layer can pass a typed struct
414    /// directly.
415    ///
416    /// `params` is taken by value (not by
417    /// reference) so the future the function
418    /// returns is unconditionally `Send`: a
419    /// borrow of `P` would only be `Send` if
420    /// `P: Sync`, which is a sharper
421    /// constraint than the consumer wants.
422    /// Serialising the value moves it into the
423    /// envelope, so the application layer
424    /// hands a fresh value per request.
425    pub async fn send_request<P>(
426        &self,
427        method: AcpMethod,
428        params: P,
429    ) -> Result<AcpReply, AcpPeerError>
430    where
431        P: Serialize,
432    {
433        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
434        // Register the id synchronously
435        // before the envelope hits the
436        // wire. The reader checks
437        // `sent_ids` to differentiate
438        // client-originating replies
439        // from agent-originating
440        // requests; if the id is not
441        // in the set when the reply
442        // arrives, the reader will
443        // route it to the
444        // agent-originating request
445        // channel by mistake. The
446        // insert is a few-nanosecond
447        // critical section. A poisoned
448        // mutex is a logic error: the
449        // peer has corrupted its own
450        // state, and the conservative
451        // response is a `Decode`
452        // error (the caller is the
453        // only one holding the
454        // poison; nobody else can
455        // recover).
456        self.sent_ids
457            .lock()
458            .map_err(|error| {
459                AcpPeerError::Decode(serde_json::Error::io(io_other(&format!(
460                    "sent_ids lock poisoned: {error}"
461                ))))
462            })?
463            .insert(id);
464        let params_value: Value = serde_json::to_value(&params).map_err(AcpPeerError::Decode)?;
465        let envelope = serde_json::json!({
466            "jsonrpc": "2.0",
467            "id": id,
468            "method": method.as_wire_name(),
469            "params": params_value,
470        });
471        let bytes = serde_json::to_vec(&envelope).map_err(AcpPeerError::Decode)?;
472        let mut framed = bytes;
473        framed.push(b'\n');
474        self.write_tx
475            .send(framed)
476            .await
477            .map_err(|_| AcpPeerError::Write(io_other("the writer task is gone")))?;
478        let (tx, rx) = oneshot::channel();
479        self.pending.lock().await.insert(id, Pending { tx, method });
480        match tokio::time::timeout(self.timeout, rx).await {
481            Ok(Ok(result)) => {
482                // The reply arrived. Remove
483                // the id from `sent_ids` so
484                // the set does not grow
485                // unboundedly over the
486                // peer's lifetime.
487                if let Ok(mut sent_ids) = self.sent_ids.lock() {
488                    sent_ids.remove(&id);
489                }
490                result
491            }
492            Ok(Err(_oneshot_dropped)) => {
493                // The reader dropped the oneshot without sending: the
494                // child exited. The exit status is not available on
495                // this path (we do not hold the `Child` handle here);
496                // a future increment that wants the status can store
497                // the handle in an `Arc<Mutex<_>>` and read it.
498                if let Ok(mut sent_ids) = self.sent_ids.lock() {
499                    sent_ids.remove(&id);
500                }
501                Err(AcpPeerError::Timeout(self.timeout))
502            }
503            Err(_elapsed) => {
504                // The reply never arrived. Drop the channel: the
505                // reader task will see a closed sender when (if) the
506                // reply does come.
507                self.pending.lock().await.remove(&id);
508                if let Ok(mut sent_ids) = self.sent_ids.lock() {
509                    sent_ids.remove(&id);
510                }
511                Err(AcpPeerError::Timeout(self.timeout))
512            }
513        }
514    }
515
516    /// Send a typed request and decode the typed
517    /// response.
518    ///
519    /// This is the typed wrapper over
520    /// [`send_request`][Self::send_request]. The
521    /// raw `Result<AcpReply, _>` it returns
522    /// carries the wire's `result` as a
523    /// `serde_json::Value`; the consumer has to
524    /// call `serde_json::from_value` themselves.
525    /// This method does that step.
526    ///
527    /// The two type parameters are the
528    /// consumer's call to the agent: the
529    /// `Req` is what the consumer's handler
530    /// sends, the `Resp` is what the agent's
531    /// handler is expected to return. The
532    /// typed `NewSessionRequest` /
533    /// `NewSessionResponse` pair is the
534    /// canonical example.
535    pub async fn send_typed_request<Req, Resp>(
536        &self,
537        method: AcpMethod,
538        params: Req,
539    ) -> Result<Resp, AcpPeerError>
540    where
541        Req: Serialize,
542        Resp: DeserializeOwned,
543    {
544        let reply = self.send_request(method, params).await?;
545        serde_json::from_value(reply.result).map_err(AcpPeerError::Decode)
546    }
547
548    /// Send a typed notification. A notification
549    /// has no `id` and no reply; the agent's
550    /// handler may respond, but the consumer
551    /// does not wait.
552    ///
553    /// The wire form is a JSON-RPC 2.0
554    /// notification: `{ "jsonrpc": "2.0",
555    /// "method": ..., "params": ... }` with no
556    /// `id` field. The schema's `session/cancel`
557    /// is the canonical example.
558    pub async fn send_notification<Params>(
559        &self,
560        method: AcpMethod,
561        params: Params,
562    ) -> Result<(), AcpPeerError>
563    where
564        Params: Serialize,
565    {
566        let params_value: Value = serde_json::to_value(&params).map_err(AcpPeerError::Decode)?;
567        let envelope = serde_json::json!({
568            "jsonrpc": "2.0",
569            "method": method.as_wire_name(),
570            "params": params_value,
571        });
572        let bytes = serde_json::to_vec(&envelope).map_err(AcpPeerError::Decode)?;
573        let mut framed = bytes;
574        framed.push(b'\n');
575        self.write_tx
576            .send(framed)
577            .await
578            .map_err(|_| AcpPeerError::Write(io_other("the writer task is gone")))?;
579        Ok(())
580    }
581
582    /// Await the next [`SessionNotification`]
583    /// the agent streams.
584    ///
585    /// `&self` (not `&mut self`) so the
586    /// consumer can race the typed reply
587    /// against the next notification in a
588    /// `tokio::select!`:
589    ///
590    /// ```ignore
591    /// tokio::select! {
592    ///     biased;
593    ///     Some(notification) = peer.recv_notification() => {
594    ///         render(notification);
595    ///     }
596    ///     response = peer.send_typed_request(...) => {
597    ///         return response;
598    ///     }
599    /// }
600    /// ```
601    ///
602    /// The receiver is behind a
603    /// `tokio::sync::Mutex`; the lock is
604    /// held across the inner
605    /// `recv().await`. That is safe for
606    /// the consumer's main loop because
607    /// only the consumer's task holds
608    /// the lock — the agent's reader
609    /// task pushes to the matching
610    /// `notifications_tx` (not the
611    /// receiver) and never contends for
612    /// the receiver lock.
613    ///
614    /// Returns `None` when the reader task
615    /// is gone (the child has exited and
616    /// the channel is closed). The
617    /// consumer is expected to drop the
618    /// peer at that point.
619    pub async fn recv_notification(&self) -> Option<SessionNotification> {
620        let mut guard = self.notifications_rx.lock().await;
621        guard.recv().await
622    }
623
624    /// Await the next agent-originating
625    /// request the agent sends to the
626    /// client. The agent's
627    /// `fs/read_text_file`,
628    /// `fs/write_text_file`,
629    /// `terminal/create`,
630    /// `terminal/output`,
631    /// `terminal/release`,
632    /// `terminal/wait_for_exit`,
633    /// `terminal/kill`, and
634    /// `session/request_permission` are
635    /// the canonical examples.
636    ///
637    /// `&self` (not `&mut self`) so the
638    /// consumer can race the typed
639    /// reply against the next
640    /// agent-originating request in a
641    /// `tokio::select!`, the same shape
642    /// `recv_notification` supports.
643    /// The receiver is behind a
644    /// `tokio::sync::Mutex`; the lock
645    /// is held across the inner
646    /// `recv().await`, which is safe
647    /// for the consumer's main loop
648    /// because only the consumer's
649    /// task holds the lock — the
650    /// reader task pushes to the
651    /// matching `agent_requests_tx`
652    /// (not the receiver) and never
653    /// contends for the receiver
654    /// lock.
655    ///
656    /// Returns `None` when the reader
657    /// task is gone (the child has
658    /// exited and the channel is
659    /// closed). The consumer is
660    /// expected to drop the peer at
661    /// that point.
662    pub async fn recv_agent_request(&self) -> Option<AgentRequest> {
663        let mut guard = self.agent_requests_rx.lock().await;
664        guard.recv().await
665    }
666
667    /// A `Stream` of the [`SessionNotification`]s the agent streams
668    /// to the client.
669    ///
670    /// The canonical alternative to [`recv_notification`](Self::recv_notification)
671    /// for a consumer whose main loop is a `tokio::select!` of
672    /// "next notification" against "next typed reply" against
673    /// "agent-originating request", and for a consumer who wants
674    /// `Stream` combinators (`StreamExt::map`, `filter`,
675    /// `take_while`, `for_each`, …) over the notification stream.
676    ///
677    /// The returned stream holds a clone of the peer's notification
678    /// mutex; [`recv_notification`](Self::recv_notification) and the
679    /// stream are mutually exclusive inside one task. Across tasks
680    /// the lock is the same lock, so a `select!` in one task and a
681    /// `next().await` in another race for the same channel and the
682    /// lock serialises them.
683    ///
684    /// `&self` (not `&mut self`) so the consumer can hold the peer
685    /// by `Arc` and call `notifications()` from any task — the same
686    /// shape `recv_notification` supports, but as a `Stream`. The
687    /// returned stream borrows the lock only (the `Arc` is
688    /// `Clone`), so the borrow is `&self` rather than `&mut self`.
689    ///
690    /// # Examples
691    ///
692    /// ```ignore
693    /// use futures_util::StreamExt;
694    ///
695    /// let mut notifications = peer.notifications();
696    /// while let Some(notification) = notifications.next().await {
697    ///     render(notification);
698    /// }
699    /// ```
700    pub fn notifications(&self) -> NotificationStream<'_> {
701        NotificationStream::new(Arc::clone(&self.notifications_rx))
702    }
703
704    /// A `Stream` of the [`AgentRequest`]s the agent sends to the
705    /// client.
706    ///
707    /// The agent-originating request counterpart of
708    /// [`notifications`](Self::notifications). The agent's
709    /// `fs/read_text_file`, `fs/write_text_file`,
710    /// `terminal/create`, `terminal/output`, `terminal/release`,
711    /// `terminal/wait_for_exit`, `terminal/kill`, and
712    /// `session/request_permission` requests flow through this
713    /// stream.
714    ///
715    /// Mutually exclusive with
716    /// [`recv_agent_request`](Self::recv_agent_request) inside one
717    /// task; across tasks the lock serialises them.
718    pub fn agent_requests(&self) -> AgentRequestStream<'_> {
719        AgentRequestStream::new(Arc::clone(&self.agent_requests_rx))
720    }
721
722    /// Reply to an agent-originating
723    /// request. The peer hands a
724    /// well-formed JSON-RPC 2.0 success
725    /// reply to the writer task; the
726    /// agent correlates the reply with
727    /// the request by the `id` (passed
728    /// through unchanged from the
729    /// [`AgentRequest`][crate::adapters::secondary::acp::AgentRequest]
730    /// the agent sent).
731    ///
732    /// The result is the typed value
733    /// the agent expects for the
734    /// method. The peer's role is
735    /// transport: it serialises the
736    /// value with `serde_json` and
737    /// hands the bytes to the writer;
738    /// the consumer's handler is what
739    /// constructs the typed value.
740    ///
741    /// `&self` because the writer
742    /// task owns the child's stdin
743    /// and the application layer
744    /// only hands a serialised
745    /// envelope to the channel.
746    pub async fn send_reply(&self, id: Value, result: Value) -> Result<(), AcpPeerError> {
747        let envelope = serde_json::json!({
748            "jsonrpc": "2.0",
749            "id": id,
750            "result": result,
751        });
752        let bytes = serde_json::to_vec(&envelope).map_err(AcpPeerError::Decode)?;
753        let mut framed = bytes;
754        framed.push(b'\n');
755        self.write_tx
756            .send(framed)
757            .await
758            .map_err(|_| AcpPeerError::Write(io_other("the writer task is gone")))?;
759        Ok(())
760    }
761}
762
763fn spawn_reader(
764    mut reader: BufReader<ChildStdout>,
765    pending: Arc<Mutex<HashMap<u64, Pending>>>,
766    notifications_tx: mpsc::Sender<SessionNotification>,
767    agent_requests_tx: mpsc::Sender<AgentRequest>,
768    sent_ids: Arc<StdMutex<HashSet<u64>>>,
769) {
770    tokio::spawn(async move {
771        let mut line = String::new();
772        loop {
773            line.clear();
774            match reader.read_line(&mut line).await {
775                Ok(0) | Err(_) => return,
776                Ok(_) => {}
777            }
778            // Trim the trailing newline; the
779            // JSON is the rest.
780            let trimmed = line.trim_end_matches('\n');
781            if trimmed.is_empty() {
782                continue;
783            }
784            // A JSON-RPC 2.0 envelope is a
785            // reply (carries an `id`) or a
786            // notification (no `id`). The
787            // shape detector is a peek at
788            // the `id` field; the dispatch
789            // path is the same.
790            let value: Value = match serde_json::from_str(trimmed) {
791                Ok(value) => value,
792                Err(_) => continue,
793            };
794            if value.get("id").is_some() {
795                // Reply-or-agent-request:
796                // the `id` is present in
797                // both. The wire form does
798                // not distinguish them;
799                // the peer's
800                // `sent_ids` set is the
801                // differentiator. If the
802                // peer allocated the id,
803                // the reply is dispatched
804                // to the pending oneshot;
805                // if the agent allocated
806                // the id, the request is
807                // pushed to the
808                // `agent_requests_tx` for
809                // the consumer's handler.
810                let (id, result) = parse_and_dispatch_value(value.clone());
811                let is_client_reply = id != 0 && sent_ids.lock().is_ok_and(|set| set.contains(&id));
812                if is_client_reply {
813                    let entry = pending.lock().await.remove(&id);
814                    if let Some(entry) = entry {
815                        let _ = entry.tx.send(result);
816                    }
817                } else if id != 0
818                    && value.get("method").is_some()
819                    && let Some(request) = parse_agent_request(&value)
820                {
821                    let _ = agent_requests_tx.send(request).await;
822                }
823                // Envelopes with `id == 0`
824                // (the sentinel for an
825                // unparseable id) and
826                // envelopes with an id that
827                // the client did not send
828                // AND no `method` field
829                // (a malformed reply) are
830                // dropped: the wire is
831                // ambiguous and the peer
832                // cannot dispatch.
833            } else {
834                // Notification: decode the
835                // `params` as a typed
836                // [`SessionNotification`] and
837                // push to the consumer's
838                // stream. The schema's
839                // `session/update` is the
840                // canonical example; a
841                // future notification method
842                // (e.g. `auth/required`) gets
843                // its own decoder.
844                if value.get("method").and_then(Value::as_str) == Some("session/update")
845                    && let Some(params) = value.get("params")
846                    && let Ok(notification) =
847                        serde_json::from_value::<SessionNotification>(params.clone())
848                {
849                    let _ = notifications_tx.send(notification).await;
850                }
851            }
852        }
853    });
854}
855
856/// Parse a JSON-RPC 2.0 envelope as an
857/// agent-originating request. The function
858/// never fails to produce a struct: a
859/// missing `method` or `id` field returns
860/// `None` and the caller drops the
861/// envelope. The wire form is the same as
862/// the client side: `{ jsonrpc, id, method,
863/// params }`.
864fn parse_agent_request(value: &Value) -> Option<AgentRequest> {
865    let id = value.get("id")?.clone();
866    let method = value.get("method").and_then(Value::as_str)?.to_owned();
867    let params = value.get("params").cloned().unwrap_or(Value::Null);
868    Some(AgentRequest { id, method, params })
869}
870
871/// Parse a single JSON-RPC 2.0 reply envelope
872/// into a `(id, Result)` pair.
873///
874/// The function never fails to produce a pair:
875/// a JSON-RPC error is a `(id,
876/// Err(JsonRpc))`, a success is a `(id,
877/// Ok(reply))`. The reader treats an
878/// id of `0` as "do not dispatch" — that is
879/// the sentinel for an envelope that cannot be
880/// correlated with a request. The caller is
881/// responsible for parsing the raw line; this
882/// function takes the already-parsed `Value`
883/// so the same parse can drive the
884/// notification path.
885fn parse_and_dispatch_value(value: Value) -> (u64, Result<AcpReply, AcpPeerError>) {
886    let id = value.get("id").and_then(Value::as_u64).unwrap_or(0);
887    if value.get("error").is_some() {
888        let code = value
889            .get("error")
890            .and_then(|error| error.get("code"))
891            .and_then(Value::as_i64)
892            .unwrap_or(0);
893        let message = value
894            .get("error")
895            .and_then(|error| error.get("message"))
896            .and_then(Value::as_str)
897            .unwrap_or("")
898            .to_owned();
899        return (id, Err(AcpPeerError::JsonRpc { code, message }));
900    }
901    match serde_json::from_value::<AcpReply>(value) {
902        Ok(reply) => (id, Ok(reply)),
903        Err(error) => (id, Err(AcpPeerError::Decode(error))),
904    }
905}
906
907fn spawn_writer(stdin: ChildStdin, mut write_rx: mpsc::Receiver<Vec<u8>>) {
908    tokio::spawn(async move {
909        let mut stdin = stdin;
910        while let Some(bytes) = write_rx.recv().await {
911            if stdin.write_all(&bytes).await.is_err() {
912                return;
913            }
914            if stdin.flush().await.is_err() {
915                return;
916            }
917        }
918    });
919}
920
921fn io_other(message: &str) -> std::io::Error {
922    std::io::Error::other(message)
923}
924
925#[cfg(test)]
926mod tests {
927    use super::*;
928
929    /// Errors a test can surface back to the
930    /// runner. Each variant is for a single
931    /// failure mode; the test function returns
932    /// `Result<(), Failure>` and uses `?` to
933    /// propagate rather than `.unwrap()` /
934    /// `.expect()`.
935    #[derive(Debug)]
936    enum Failure {
937        /// A `Result` from a tested call was
938        /// `Ok` when the test expected `Err`,
939        /// or vice versa. The string is the
940        /// expected variant the test was
941        /// looking for.
942        WrongResult(&'static str),
943    }
944
945    impl std::fmt::Display for Failure {
946        fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
947            match self {
948                Self::WrongResult(expected) => write!(formatter, "{expected}"),
949            }
950        }
951    }
952
953    impl std::error::Error for Failure {}
954
955    impl From<serde_json::Error> for Failure {
956        fn from(_error: serde_json::Error) -> Self {
957            Self::WrongResult("a JSON parse failed")
958        }
959    }
960
961    /// The reader parses a `result` reply into a
962    /// typed `AcpReply` and routes it to the
963    /// matching id. A reply with no id is
964    /// silently dropped (the consumer's oneshot
965    /// would never fire).
966    ///
967    /// The test only exercises
968    /// `parse_and_dispatch_value` in isolation;
969    /// the `oneshot` is fed by the reader task
970    /// in the real path. The `assert_eq!` on
971    /// the parsed pair is the claim the
972    /// function is asked to defend.
973    #[test]
974    fn parse_and_dispatch_routes_a_result_by_id() -> Result<(), Failure> {
975        let value: Value =
976            serde_json::from_str(r#"{"jsonrpc":"2.0","id":7,"result":{"answer":42}}"#)
977                .map_err(|_| Failure::WrongResult("valid JSON"))?;
978        let (id, result) = parse_and_dispatch_value(value);
979        assert_eq!(id, 7);
980        let Ok(reply) = result else {
981            return Err(Failure::WrongResult("Ok reply"));
982        };
983        assert_eq!(reply.id, serde_json::json!(7));
984        assert_eq!(reply.result, serde_json::json!({"answer": 42}));
985        Ok(())
986    }
987
988    /// The reader parses a JSON-RPC error reply
989    /// into a typed `AcpPeerError::JsonRpc`. The
990    /// error carries the agent's code and
991    /// message; the consumer decides what to
992    /// do.
993    #[test]
994    fn parse_and_dispatch_routes_an_error_by_id() -> Result<(), Failure> {
995        let value: Value = serde_json::from_str(
996            r#"{"jsonrpc":"2.0","id":7,"error":{"code":-32601,"message":"Method not found"}}"#,
997        )
998        .map_err(|_| Failure::WrongResult("valid JSON"))?;
999        let (id, result) = parse_and_dispatch_value(value);
1000        assert_eq!(id, 7);
1001        match result {
1002            Err(AcpPeerError::JsonRpc { code, message }) => {
1003                assert_eq!(code, -32601);
1004                assert_eq!(message, "Method not found");
1005                Ok(())
1006            }
1007            Err(_) => Err(Failure::WrongResult("JsonRpc variant")),
1008            Ok(_) => Err(Failure::WrongResult("Err variant")),
1009        }
1010    }
1011
1012    /// A JSON-RPC error with no `id` is
1013    /// dispatched to the `0` sentinel — the
1014    /// reader treats `id == 0` as "do not
1015    /// dispatch" so a misbehaving agent
1016    /// cannot crash the consumer's pending
1017    /// oneshot.
1018    #[test]
1019    fn parse_and_dispatch_treats_missing_id_as_zero() -> Result<(), Failure> {
1020        let value: Value = serde_json::from_str(
1021            r#"{"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request"}}"#,
1022        )
1023        .map_err(|_| Failure::WrongResult("valid JSON"))?;
1024        let (id, _result) = parse_and_dispatch_value(value);
1025        assert_eq!(id, 0);
1026        Ok(())
1027    }
1028
1029    /// The peer can be spawned, sent a request,
1030    /// and shut down. The test points at the
1031    /// `cat` command (always present on Unix)
1032    /// as a stand-in for a real agent. `cat`
1033    /// reads its stdin and writes it to stdout;
1034    /// a `cat` peer that receives a JSON-RPC
1035    /// envelope writes the envelope back, and
1036    /// the reader sees the request shape (which
1037    /// is well-formed JSON-RPC but is not a
1038    /// reply, so the dispatcher's id lookup
1039    /// fails and the result is dropped).
1040    ///
1041    /// The test's job is to prove the spawn /
1042    /// reader / writer machinery works; the
1043    /// fact that `cat` is not an ACP agent
1044    /// makes the inner dispatch silent, not
1045    /// a failure. The end-to-end test
1046    /// against a real agent lives in
1047    /// `tests/acp_stdio_interop.rs`.
1048    #[cfg(unix)]
1049    #[tokio::test]
1050    async fn the_peer_spawns_and_drops_cleanly() -> Result<(), Failure> {
1051        let spec = SpawnSpec::new("cat", Vec::new());
1052        let Ok(peer) = AcpStdioPeer::spawn(&spec) else {
1053            return Err(Failure::WrongResult("spawn succeeds on Unix"));
1054        };
1055        drop(peer);
1056        Ok(())
1057    }
1058
1059    /// A typed `SpawnSpec` carries the program
1060    /// and the args the spawner needs. The
1061    /// builder is a thin convenience over
1062    /// struct literal construction.
1063    #[test]
1064    fn a_spawn_spec_carries_the_program_and_args() {
1065        let spec = SpawnSpec::new("node", vec!["agent.js".to_owned()])
1066            .with_timeout(Duration::from_secs(5));
1067        assert_eq!(spec.program, "node");
1068        assert_eq!(spec.args, vec!["agent.js".to_owned()]);
1069        assert_eq!(spec.timeout, Some(Duration::from_secs(5)));
1070    }
1071}