Skip to main content

wcode_protocol/
server.rs

1//! Serve sessions over a socket: accept connections, bridge frames ↔ handles.
2//!
3//! Each connection gets a writer draining an outbound queue, one **fan** task per
4//! served session (forwarding that session's events, each frame stamped with its
5//! origin session), and the read loop answering inbound requests. A request is
6//! demuxed to the handle named by its `Frame::session`; a single-session server
7//! also accepts any id (the legacy default, so a `--socket` client's placeholder
8//! still reaches it). Requests are answered with the actor's reply (correlated by
9//! the envelope's `reply_to`), so a client's `ask` resolves even while streamed
10//! events flow on the same connection.
11//!
12//! The roster is **live**: beside an explicit root (served under its own session
13//! id, distinct from the registry's `agent:orchestrator` alias), the server
14//! follows a [`watch::Receiver`] of the extra sessions. A session registered
15//! after the server started — a worker a running orchestrator spawns — is fanned
16//! to every connection, and the grown roster is **pushed** as an unsolicited
17//! [`AgentEvent::Sessions`] frame, so a client learns new ids without polling.
18
19use std::collections::HashSet;
20use std::io;
21use std::path::Path;
22
23use tokio::io::{AsyncWrite, BufReader, BufWriter};
24use tokio::net::{UnixListener, UnixStream};
25use tokio::sync::{broadcast, mpsc, watch};
26use wcode_harness::actor::SessionHandle;
27use wcode_harness::event::AgentEvent;
28use wcode_harness::protocol::{Frame, PROTOCOL_VERSION, Request, SessionId, SessionInfo};
29
30use crate::Registry;
31use crate::frame::{read_frame, write_frame};
32
33/// The live roster of *extra* sessions served beside the root — a session is
34/// `(SessionId, SessionHandle)`. Empty means "root only"; the value is refreshed
35/// by a [`crate::Registry`] as sessions are registered.
36pub type Roster = watch::Receiver<Vec<(SessionId, SessionHandle)>>;
37
38/// A served session: the id every outbound frame carrying it is stamped with,
39/// and the handle the server reads events from / writes requests to.
40pub type Root = (SessionId, SessionHandle);
41
42/// The arguments of a [`wcode_harness::protocol::Request::Define`], passed to a
43/// [`DefineHandler`]. Mirrors the CLI's worker spec.
44#[derive(Clone, Debug, Default)]
45pub struct DefineArgs {
46    /// The worker's address; auto-assigned when absent.
47    pub name: Option<String>,
48    /// Model id override; `None` inherits the server's model.
49    pub model: Option<String>,
50    /// Role text appended to the worker's system prompt.
51    pub role: Option<String>,
52    /// Tool allow-list; `None` = the full default set.
53    pub tools: Option<Vec<String>>,
54    /// Provider base URL override; `None` inherits the server's.
55    pub base_url: Option<String>,
56    /// Provider API key override; `None` inherits the server's.
57    pub api_key: Option<String>,
58    /// Enforce read-only for the defined worker (D1/D2).
59    pub read_only: bool,
60    /// Reasoning-effort override (D3); `None` inherits, synonyms clear.
61    pub effort: Option<String>,
62}
63
64/// Builds a worker from [`DefineArgs`] and returns its address. A socket server
65/// uses it to answer [`wcode_harness::protocol::Request::Define`]; the
66/// composition root injects one, since this crate cannot name a session factory.
67pub type DefineHandler =
68    std::sync::Arc<dyn Fn(DefineArgs) -> Result<SessionId, String> + Send + Sync>;
69
70/// Bind `path` and serve the root session plus `roster` until the listener
71/// errors.
72///
73/// `root` is served first and labelled by its own `SessionId` (the agent's
74/// session id — *not* the registry's `agent:orchestrator` alias). Every session
75/// named by `roster` is served too, now and as it grows. A client addresses a
76/// session by `frame.session`; a server whose *current* served set is exactly
77/// one session also accepts any id, preserving the single-session flow (the
78/// `--socket` client's placeholder `"remote"` still reaches it). `define`, when
79/// installed, answers `Request::Define` — the composition root's worker factory.
80///
81/// `registry` is consulted (read-only) for each served session's model, so the
82/// roster this server pushes names models the server actually knows.
83pub async fn serve(
84    registry: Registry,
85    roster: Roster,
86    root: Root,
87    define: Option<DefineHandler>,
88    listener: UnixListener,
89) -> io::Result<()> {
90    loop {
91        let (stream, _addr) = listener.accept().await?;
92        tokio::spawn(connection(
93            registry.clone(),
94            roster.clone(),
95            root.clone(),
96            define.clone(),
97            stream,
98        ));
99    }
100}
101
102/// Bind `path`, then [`serve`].
103pub async fn serve_at(
104    registry: Registry,
105    roster: Roster,
106    root: Root,
107    define: Option<DefineHandler>,
108    path: &Path,
109) -> io::Result<()> {
110    let listener = crate::socket::bind(path).await?;
111    serve(registry, roster, root, define, listener).await
112}
113
114async fn connection(
115    registry: Registry,
116    mut roster: Roster,
117    root: Root,
118    define: Option<DefineHandler>,
119    stream: UnixStream,
120) {
121    let (read, write) = stream.into_split();
122    let (out, out_rx) = mpsc::unbounded_channel::<Frame<AgentEvent>>();
123    tokio::spawn(write_loop(write, out_rx));
124
125    let (root_id, root_handle) = root;
126    // Every id already fanned to this connection, so a session is fanned once.
127    let mut seen: HashSet<SessionId> = HashSet::new();
128
129    // Fan the root, then every session already in the roster. `borrow_and_update`
130    // marks the current value seen, so the watch task below only reacts to
131    // changes made *after* this snapshot — no id is fanned twice and none is
132    // missed.
133    seen.insert(root_id.clone());
134    fan(&out, &root_id, &root_handle);
135    let initial = roster.borrow_and_update().clone();
136    for (id, handle) in &initial {
137        if seen.insert(id.clone()) {
138            fan(&out, id, handle);
139        }
140    }
141    // Seed the connection with the roster it starts with, so a client learns the
142    // served ids on connect — and a session registered in the window before this
143    // task first ran (already folded into the snapshot above) is not lost to the
144    // watch's "seen" mark.
145    let _ = out.send(Frame {
146        v: PROTOCOL_VERSION,
147        id: 0,
148        reply_to: None,
149        session: root_id.clone(),
150        sender: None,
151        body: AgentEvent::Sessions {
152            sessions: roster_infos(&registry, &root_id, &initial),
153        },
154    });
155
156    // The read loop needs the live roster too, for routing and `ListSessions`.
157    // A clone is a read-only view (its own seen-pointer is irrelevant); the
158    // watch task owns the original, whose seen-pointer stays at the snapshot.
159    let read_roster = roster.clone();
160
161    // Follow the roster: fan each newly-seen session once and push the grown
162    // roster to this connection, so a client learns late ids without polling.
163    {
164        let out = out.clone();
165        let root_id = root_id.clone();
166        let registry = registry.clone();
167        tokio::spawn(async move {
168            loop {
169                if roster.changed().await.is_err() {
170                    break;
171                }
172                let current = roster.borrow_and_update().clone();
173                let mut grew = false;
174                for (id, handle) in &current {
175                    if seen.insert(id.clone()) {
176                        fan(&out, id, handle);
177                        grew = true;
178                    }
179                }
180                if grew {
181                    let sessions = roster_infos(&registry, &root_id, &current);
182                    let _ = out.send(Frame {
183                        v: PROTOCOL_VERSION,
184                        id: 0,
185                        reply_to: None,
186                        session: root_id.clone(),
187                        sender: None,
188                        body: AgentEvent::Sessions { sessions },
189                    });
190                }
191            }
192        });
193    }
194
195    // Answer each request (in its own task, so a long run never blocks reads).
196    let mut read = BufReader::new(read);
197    while let Ok(Some(frame)) = read_frame::<_, Request>(&mut read).await {
198        let Frame {
199            id, session, sender, body, ..
200        } = frame;
201
202        // The served set: the root first, then the live roster (deduplicated).
203        let live: Vec<(SessionId, SessionHandle)> = read_roster
204            .borrow()
205            .iter()
206            .filter(|(sid, _)| sid != &root_id)
207            .cloned()
208            .collect();
209
210        // `ListSessions` names no session: the server answers it directly with
211        // the roster (root first), before any demux.
212        if matches!(body, Request::ListSessions) {
213            let sessions = roster_infos(&registry, &root_id, &live);
214            let _ = out.send(Frame {
215                v: PROTOCOL_VERSION,
216                id,
217                reply_to: Some(id),
218                session,
219                sender: None,
220                body: AgentEvent::Sessions { sessions },
221            });
222            continue;
223        }
224
225        // `Define` names no session either: the server answers it directly, via
226        // the injected handler (or a clean error when none is installed). The
227        // handler registers into the same registry the roster watches, so the
228        // new worker is pushed to every client with no extra work.
229        if let Request::Define {
230            name,
231            model,
232            role,
233            tools,
234            base_url,
235            api_key,
236            read_only,
237            effort,
238        } = &body
239        {
240            let reply = match &define {
241                Some(handler) => match handler(DefineArgs {
242                    name: name.clone(),
243                    model: model.clone(),
244                    role: role.clone(),
245                    tools: tools.clone(),
246                    base_url: base_url.clone(),
247                    api_key: api_key.clone(),
248                    read_only: *read_only,
249                    effort: effort.clone(),
250                }) {
251                    Ok(id) => AgentEvent::Spawned { worker: id },
252                    Err(message) => AgentEvent::Error { message },
253                },
254                None => AgentEvent::Error {
255                    message: "agent definition is not enabled on this server (serve --agents)"
256                        .into(),
257                },
258            };
259            let _ = out.send(Frame {
260                v: PROTOCOL_VERSION,
261                id,
262                reply_to: Some(id),
263                session,
264                sender: None,
265                body: reply,
266            });
267            continue;
268        }
269
270        // Demux on `frame.session`: the root; a roster session of that name; the
271        // sole session of a single-session server (any id — the legacy default);
272        // or, on a multi-session server, a correlated error echoing the request.
273        let handle = if session == root_id {
274            Some(root_handle.clone())
275        } else if let Some((_, handle)) = live.iter().find(|(sid, _)| sid == &session) {
276            Some(handle.clone())
277        } else if live.is_empty() {
278            Some(root_handle.clone())
279        } else {
280            None
281        };
282        let Some(handle) = handle else {
283            let message = format!("unknown session {session}");
284            let _ = out.send(Frame {
285                v: PROTOCOL_VERSION,
286                id,
287                reply_to: Some(id),
288                session,
289                sender: None,
290                body: AgentEvent::Error { message },
291            });
292            continue;
293        };
294
295        let out = out.clone();
296        tokio::spawn(async move {
297            // A peer's frame carries its address in `from`; feed it to the
298            // target so `before_inbound` and the sender tag see it (A2A).
299            let reply = match sender {
300                Some(from) => handle.ask_from(from, body).await,
301                None => handle.ask(body).await,
302            };
303            if let Ok(reply) = reply {
304                let frame = Frame {
305                    v: PROTOCOL_VERSION,
306                    id,
307                    reply_to: Some(id),
308                    session,
309                    sender: None,
310                    body: reply,
311                };
312                let _ = out.send(frame);
313            }
314        });
315    }
316}
317
318/// The served sessions in order: the root first, then each roster session (minus
319/// the root, if a roster ever names it) — each joined with the model the registry
320/// knows for it (`None` when unset). The push payload of `AgentEvent::Sessions`.
321fn roster_infos(
322    registry: &Registry,
323    root_id: &SessionId,
324    roster: &[(SessionId, SessionHandle)],
325) -> Vec<SessionInfo> {
326    let mut ids = vec![root_id.clone()];
327    for (id, _) in roster {
328        if id != root_id {
329            ids.push(id.clone());
330        }
331    }
332    ids.into_iter()
333        .map(|id| {
334            let model = registry.model_of(&id);
335            SessionInfo { id, model }
336        })
337        .collect()
338}
339
340/// Forward one session's events to a connection, each frame stamped with its
341/// origin session — the routing key the client demuxes on. Ends when the
342/// subscriber closes or the connection's queue is gone.
343fn fan(out: &mpsc::UnboundedSender<Frame<AgentEvent>>, session: &SessionId, handle: &SessionHandle) {
344    let mut events = handle.subscribe();
345    let out = out.clone();
346    let session = session.clone();
347    tokio::spawn(async move {
348        loop {
349            match events.recv().await {
350                Ok(event) => {
351                    let frame = Frame {
352                        v: PROTOCOL_VERSION,
353                        id: 0,
354                        reply_to: None,
355                        session: session.clone(),
356                        sender: None,
357                        body: event,
358                    };
359                    if out.send(frame).is_err() {
360                        break;
361                    }
362                }
363                Err(broadcast::error::RecvError::Lagged(_)) => continue,
364                Err(broadcast::error::RecvError::Closed) => break,
365            }
366        }
367    });
368}
369
370async fn write_loop<W>(write: W, mut rx: mpsc::UnboundedReceiver<Frame<AgentEvent>>)
371where
372    W: AsyncWrite + Unpin,
373{
374    let mut write = BufWriter::new(write);
375    while let Some(frame) = rx.recv().await {
376        if write_frame(&mut write, &frame).await.is_err() {
377            break;
378        }
379    }
380}