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(¶ms).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(¶ms).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}