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 // Push on ANY roster change, not just growth: a state-only
174 // transition (a worker goes Running) leaves `locals()` identical,
175 // so a `grew` test would miss it — but `set_state` bumped the
176 // watch, so this task ran. The payload is tiny (one `SessionInfo`
177 // per member). `fan()` is still gated on `seen.insert`, so no
178 // event is fanned twice.
179 for (id, handle) in ¤t {
180 if seen.insert(id.clone()) {
181 fan(&out, id, handle);
182 }
183 }
184 let sessions = roster_infos(®istry, &root_id, ¤t);
185 let _ = out.send(Frame {
186 v: PROTOCOL_VERSION,
187 id: 0,
188 reply_to: None,
189 session: root_id.clone(),
190 sender: None,
191 body: AgentEvent::Sessions { sessions },
192 });
193 }
194 });
195 }
196
197 // Answer each request (in its own task, so a long run never blocks reads).
198 let mut read = BufReader::new(read);
199 while let Ok(Some(frame)) = read_frame::<_, Request>(&mut read).await {
200 let Frame {
201 id, session, sender, body, ..
202 } = frame;
203
204 // The served set: the root first, then the live roster (deduplicated).
205 let live: Vec<(SessionId, SessionHandle)> = read_roster
206 .borrow()
207 .iter()
208 .filter(|(sid, _)| sid != &root_id)
209 .cloned()
210 .collect();
211
212 // `ListSessions` names no session: the server answers it directly with
213 // the roster (root first), before any demux.
214 if matches!(body, Request::ListSessions) {
215 let sessions = roster_infos(®istry, &root_id, &live);
216 let _ = out.send(Frame {
217 v: PROTOCOL_VERSION,
218 id,
219 reply_to: Some(id),
220 session,
221 sender: None,
222 body: AgentEvent::Sessions { sessions },
223 });
224 continue;
225 }
226
227 // `Define` names no session either: the server answers it directly, via
228 // the injected handler (or a clean error when none is installed). The
229 // handler registers into the same registry the roster watches, so the
230 // new worker is pushed to every client with no extra work.
231 if let Request::Define {
232 name,
233 model,
234 role,
235 tools,
236 base_url,
237 api_key,
238 read_only,
239 effort,
240 } = &body
241 {
242 let reply = match &define {
243 Some(handler) => match handler(DefineArgs {
244 name: name.clone(),
245 model: model.clone(),
246 role: role.clone(),
247 tools: tools.clone(),
248 base_url: base_url.clone(),
249 api_key: api_key.clone(),
250 read_only: *read_only,
251 effort: effort.clone(),
252 }) {
253 Ok(id) => AgentEvent::Spawned { worker: id },
254 Err(message) => AgentEvent::Error { message },
255 },
256 None => AgentEvent::Error {
257 message: "agent definition is not enabled on this server (serve --agents)"
258 .into(),
259 },
260 };
261 let _ = out.send(Frame {
262 v: PROTOCOL_VERSION,
263 id,
264 reply_to: Some(id),
265 session,
266 sender: None,
267 body: reply,
268 });
269 continue;
270 }
271
272 // Demux on `frame.session`: the root; a roster session of that name; the
273 // sole session of a single-session server (any id — the legacy default);
274 // or, on a multi-session server, a correlated error echoing the request.
275 let handle = if session == root_id {
276 Some(root_handle.clone())
277 } else if let Some((_, handle)) = live.iter().find(|(sid, _)| sid == &session) {
278 Some(handle.clone())
279 } else if live.is_empty() {
280 Some(root_handle.clone())
281 } else {
282 None
283 };
284 let Some(handle) = handle else {
285 let message = format!("unknown session {session}");
286 let _ = out.send(Frame {
287 v: PROTOCOL_VERSION,
288 id,
289 reply_to: Some(id),
290 session,
291 sender: None,
292 body: AgentEvent::Error { message },
293 });
294 continue;
295 };
296
297 let out = out.clone();
298 tokio::spawn(async move {
299 // A peer's frame carries its address in `from`; feed it to the
300 // target so `before_inbound` and the sender tag see it (A2A).
301 let reply = match sender {
302 Some(from) => handle.ask_from(from, body).await,
303 None => handle.ask(body).await,
304 };
305 if let Ok(reply) = reply {
306 let frame = Frame {
307 v: PROTOCOL_VERSION,
308 id,
309 reply_to: Some(id),
310 session,
311 sender: None,
312 body: reply,
313 };
314 let _ = out.send(frame);
315 }
316 });
317 }
318}
319
320/// The served sessions in order: the root first, then each roster session (minus
321/// the root, if a roster ever names it) — each joined with the model the registry
322/// knows for it (`None` when unset). The push payload of `AgentEvent::Sessions`.
323fn roster_infos(
324 registry: &Registry,
325 root_id: &SessionId,
326 roster: &[(SessionId, SessionHandle)],
327) -> Vec<SessionInfo> {
328 let mut ids = vec![root_id.clone()];
329 for (id, _) in roster {
330 if id != root_id {
331 ids.push(id.clone());
332 }
333 }
334 ids.into_iter()
335 .map(|id| {
336 let model = registry.model_of(&id);
337 let state = registry.state_of(&id);
338 SessionInfo { id, model, state }
339 })
340 .collect()
341}
342
343/// Forward one session's events to a connection, each frame stamped with its
344/// origin session — the routing key the client demuxes on. Ends when the
345/// subscriber closes or the connection's queue is gone.
346fn fan(out: &mpsc::UnboundedSender<Frame<AgentEvent>>, session: &SessionId, handle: &SessionHandle) {
347 let mut events = handle.subscribe();
348 let out = out.clone();
349 let session = session.clone();
350 tokio::spawn(async move {
351 loop {
352 match events.recv().await {
353 Ok(event) => {
354 let frame = Frame {
355 v: PROTOCOL_VERSION,
356 id: 0,
357 reply_to: None,
358 session: session.clone(),
359 sender: None,
360 body: event,
361 };
362 if out.send(frame).is_err() {
363 break;
364 }
365 }
366 Err(broadcast::error::RecvError::Lagged(_)) => continue,
367 Err(broadcast::error::RecvError::Closed) => break,
368 }
369 }
370 });
371}
372
373async fn write_loop<W>(write: W, mut rx: mpsc::UnboundedReceiver<Frame<AgentEvent>>)
374where
375 W: AsyncWrite + Unpin,
376{
377 let mut write = BufWriter::new(write);
378 while let Some(frame) = rx.recv().await {
379 if write_frame(&mut write, &frame).await.is_err() {
380 break;
381 }
382 }
383}