Skip to main content

wcode_protocol/
backend.rs

1//! A session handle that is local *or* remote.
2//!
3//! [`Backend`] is the seam the client code targets: the REPL, the TUI, and (S4)
4//! a peer session call `send`/`ask`/`subscribe` and never branch on whether the
5//! session runs in this process ([`SessionHandle`]) or across a socket
6//! ([`Client`]). Swapping the transport is swapping the variant.
7
8use std::time::Duration;
9
10use tokio::sync::broadcast;
11use wcode_harness::actor::SessionHandle;
12use wcode_harness::event::AgentEvent;
13use wcode_harness::protocol::{Request, SessionId};
14
15#[cfg(unix)]
16use crate::client::Client;
17
18/// The session is gone — locally (the actor shut down) or remotely (the
19/// connection dropped).
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct Closed;
22
23impl std::fmt::Display for Closed {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        f.write_str("session is closed")
26    }
27}
28
29impl std::error::Error for Closed {}
30
31/// Why a bounded [`Backend::ask_within`] did not get a reply. `ask` keeps
32/// returning the narrower [`Closed`], so its existing call sites don't churn.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum AskError {
35    /// The session shut down (locally) or the connection dropped (remotely).
36    Closed,
37    /// No reply arrived before the deadline. The waiter is removed, so a reply
38    /// that arrives later finds no one and is silently dropped.
39    Timeout,
40}
41
42impl std::fmt::Display for AskError {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        f.write_str(match self {
45            AskError::Closed => "session is closed",
46            AskError::Timeout => "the ask timed out",
47        })
48    }
49}
50
51impl std::error::Error for AskError {}
52
53/// A handle to a session, local or remote.
54#[derive(Clone)]
55pub enum Backend {
56    /// In-process: the kernel actor.
57    Local(SessionHandle),
58    /// Across a socket: a remote client.
59    #[cfg(unix)]
60    Remote(Client),
61}
62
63impl Backend {
64    /// Fire-and-forget.
65    pub fn send(&self, request: Request) -> Result<(), Closed> {
66        match self {
67            Backend::Local(handle) => handle.send(request).map_err(|_| Closed),
68            #[cfg(unix)]
69            Backend::Remote(client) => client.send(request),
70        }
71    }
72
73    /// Fire-and-forget, attributed to `from` (A2A): the sender reaches the
74    /// target's `before_inbound` and sender tag, local or over a socket.
75    pub fn send_from(&self, from: SessionId, request: Request) -> Result<(), Closed> {
76        match self {
77            Backend::Local(handle) => handle.send_from(from, request).map_err(|_| Closed),
78            #[cfg(unix)]
79            Backend::Remote(client) => client.send_from(from, request),
80        }
81    }
82
83    /// Like [`Backend::ask`], but **bounded** (item-41 residual). The single
84    /// choke point for every bounded `ask`: a hung in-process
85    /// `SessionHandle::ask` and a hung-but-open `Client::ask` both time out to
86    /// [`AskError::Timeout`]. `Submit` stays unbounded (a long turn is not a
87    /// hang); callers pass `dur` only for short command/side queries.
88    pub async fn ask_within(
89        &self,
90        request: Request,
91        dur: Duration,
92    ) -> Result<AgentEvent, AskError> {
93        match self {
94            Backend::Local(handle) => tokio::time::timeout(dur, handle.ask(request))
95                .await
96                .map_err(|_| AskError::Timeout)?
97                .map_err(|_| AskError::Closed),
98            #[cfg(unix)]
99            Backend::Remote(client) => client.ask_within(request, dur).await,
100        }
101    }
102    /// Send and await the correlated reply.
103    pub async fn ask(&self, request: Request) -> Result<AgentEvent, Closed> {
104        match self {
105            Backend::Local(handle) => handle.ask(request).await.map_err(|_| Closed),
106            #[cfg(unix)]
107            Backend::Remote(client) => client.ask(request).await,
108        }
109    }
110
111    /// Stream the session's events.
112    pub fn subscribe(&self) -> broadcast::Receiver<AgentEvent> {
113        match self {
114            Backend::Local(handle) => handle.subscribe(),
115            #[cfg(unix)]
116            Backend::Remote(client) => client.subscribe(),
117        }
118    }
119}
120
121impl From<SessionHandle> for Backend {
122    fn from(handle: SessionHandle) -> Self {
123        Backend::Local(handle)
124    }
125}
126
127#[cfg(unix)]
128impl From<Client> for Backend {
129    fn from(client: Client) -> Self {
130        Backend::Remote(client)
131    }
132}