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(®istry, &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 ¤t {
175 if seen.insert(id.clone()) {
176 fan(&out, id, handle);
177 grew = true;
178 }
179 }
180 if grew {
181 let sessions = roster_infos(®istry, &root_id, ¤t);
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(®istry, &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}