1use std::collections::HashSet;
20use std::io;
21use std::path::Path;
22
23use tokio::io::{AsyncRead, AsyncWrite, BufReader, BufWriter};
24#[cfg(unix)]
25use tokio::net::UnixListener;
26use tokio::sync::{broadcast, mpsc, watch};
27use wcode_harness::actor::SessionHandle;
28use wcode_harness::event::AgentEvent;
29use wcode_harness::protocol::{Frame, PROTOCOL_VERSION, Request, SessionId, SessionInfo};
30
31use crate::Registry;
32use crate::frame::{read_frame, write_frame};
33
34pub type Roster = watch::Receiver<Vec<(SessionId, SessionHandle)>>;
38
39pub type Root = (SessionId, SessionHandle);
42
43#[derive(Clone, Debug, Default)]
46pub struct DefineArgs {
47 pub name: Option<String>,
49 pub model: Option<String>,
51 pub role: Option<String>,
53 pub tools: Option<Vec<String>>,
55 pub base_url: Option<String>,
57 pub api_key: Option<String>,
59 pub read_only: bool,
61 pub effort: Option<String>,
63}
64
65pub type DefineHandler =
69 std::sync::Arc<dyn Fn(DefineArgs) -> Result<SessionId, String> + Send + Sync>;
70
71#[cfg(unix)]
85pub async fn serve(
86 registry: Registry,
87 roster: Roster,
88 root: Root,
89 define: Option<DefineHandler>,
90 listener: UnixListener,
91) -> io::Result<()> {
92 loop {
93 let (stream, _addr) = listener.accept().await?;
94 tokio::spawn(serve_stream(
95 registry.clone(),
96 roster.clone(),
97 root.clone(),
98 define.clone(),
99 stream,
100 ));
101 }
102}
103
104#[cfg(unix)]
106pub async fn serve_at(
107 registry: Registry,
108 roster: Roster,
109 root: Root,
110 define: Option<DefineHandler>,
111 path: &Path,
112) -> io::Result<()> {
113 let listener = crate::socket::bind(path).await?;
114 serve(registry, roster, root, define, listener).await
115}
116
117pub async fn serve_stdio(
123 registry: Registry,
124 roster: Roster,
125 root: Root,
126 define: Option<DefineHandler>,
127) -> io::Result<()> {
128 let stream = tokio::io::join(tokio::io::stdin(), tokio::io::stdout());
129 serve_stream(registry, roster, root, define, stream).await;
130 Ok(())
131}
132
133pub async fn serve_stream<S>(
142 registry: Registry,
143 mut roster: Roster,
144 root: Root,
145 define: Option<DefineHandler>,
146 stream: S,
147) where
148 S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
149{
150 let (read, write) = tokio::io::split(stream);
151 let (out, out_rx) = mpsc::unbounded_channel::<Frame<AgentEvent>>();
152 tokio::spawn(write_loop(write, out_rx));
153
154 let (root_id, root_handle) = root;
155 let mut seen: HashSet<SessionId> = HashSet::new();
157
158 seen.insert(root_id.clone());
163 fan(&out, &root_id, &root_handle);
164 let initial = roster.borrow_and_update().clone();
165 for (id, handle) in &initial {
166 if seen.insert(id.clone()) {
167 fan(&out, id, handle);
168 }
169 }
170 let _ = out.send(Frame {
175 v: PROTOCOL_VERSION,
176 id: 0,
177 reply_to: None,
178 session: root_id.clone(),
179 sender: None,
180 body: AgentEvent::Sessions {
181 sessions: roster_infos(®istry, &root_id, &initial),
182 },
183 });
184
185 let read_roster = roster.clone();
189
190 {
193 let out = out.clone();
194 let root_id = root_id.clone();
195 let registry = registry.clone();
196 tokio::spawn(async move {
197 loop {
198 if roster.changed().await.is_err() {
199 break;
200 }
201 let current = roster.borrow_and_update().clone();
202 for (id, handle) in ¤t {
209 if seen.insert(id.clone()) {
210 fan(&out, id, handle);
211 }
212 }
213 let sessions = roster_infos(®istry, &root_id, ¤t);
214 let _ = out.send(Frame {
215 v: PROTOCOL_VERSION,
216 id: 0,
217 reply_to: None,
218 session: root_id.clone(),
219 sender: None,
220 body: AgentEvent::Sessions { sessions },
221 });
222 }
223 });
224 }
225
226 let mut read = BufReader::new(read);
228 while let Ok(Some(frame)) = read_frame::<_, Request>(&mut read).await {
229 let Frame {
230 id, session, sender, body, ..
231 } = frame;
232
233 let live: Vec<(SessionId, SessionHandle)> = read_roster
235 .borrow()
236 .iter()
237 .filter(|(sid, _)| sid != &root_id)
238 .cloned()
239 .collect();
240
241 if matches!(body, Request::ListSessions) {
244 let sessions = roster_infos(®istry, &root_id, &live);
245 let _ = out.send(Frame {
246 v: PROTOCOL_VERSION,
247 id,
248 reply_to: Some(id),
249 session,
250 sender: None,
251 body: AgentEvent::Sessions { sessions },
252 });
253 continue;
254 }
255
256 if matches!(body, Request::ListModels) {
259 let _ = out.send(Frame {
260 v: PROTOCOL_VERSION,
261 id,
262 reply_to: Some(id),
263 session,
264 sender: None,
265 body: AgentEvent::Models { models: registry.models() },
266 });
267 continue;
268 }
269
270 if let Request::Define {
275 name,
276 model,
277 role,
278 tools,
279 base_url,
280 api_key,
281 read_only,
282 effort,
283 } = &body
284 {
285 let reply = match &define {
286 Some(handler) => match handler(DefineArgs {
287 name: name.clone(),
288 model: model.clone(),
289 role: role.clone(),
290 tools: tools.clone(),
291 base_url: base_url.clone(),
292 api_key: api_key.clone(),
293 read_only: *read_only,
294 effort: effort.clone(),
295 }) {
296 Ok(id) => AgentEvent::Spawned { worker: id },
297 Err(message) => AgentEvent::Error { message },
298 },
299 None => AgentEvent::Error {
300 message: "agent definition is not enabled on this server (serve --agents)"
301 .into(),
302 },
303 };
304 let _ = out.send(Frame {
305 v: PROTOCOL_VERSION,
306 id,
307 reply_to: Some(id),
308 session,
309 sender: None,
310 body: reply,
311 });
312 continue;
313 }
314
315 let handle = if session == root_id {
319 Some(root_handle.clone())
320 } else if let Some((_, handle)) = live.iter().find(|(sid, _)| sid == &session) {
321 Some(handle.clone())
322 } else if live.is_empty() {
323 Some(root_handle.clone())
324 } else {
325 None
326 };
327 let Some(handle) = handle else {
328 let message = format!("unknown session {session}");
329 let _ = out.send(Frame {
330 v: PROTOCOL_VERSION,
331 id,
332 reply_to: Some(id),
333 session,
334 sender: None,
335 body: AgentEvent::Error { message },
336 });
337 continue;
338 };
339
340 let out = out.clone();
341 let registry = registry.clone();
342 let mirror: Option<(Option<String>, Option<String>)> = match &body {
346 Request::SetModel { model } => Some((Some(model.clone()), None)),
347 Request::SetEffort { effort } => Some((None, effort.clone())),
348 _ => None,
349 };
350 let session_for_reply = session.clone();
351 tokio::spawn(async move {
352 let reply = match sender {
355 Some(from) => handle.ask_from(from, body).await,
356 None => handle.ask(body).await,
357 };
358 if let Ok(reply) = reply {
359 match mirror {
362 Some((Some(model), _)) => registry.set_model(session.clone(), model),
363 Some((None, effort)) => registry.set_effort(session.clone(), effort),
364 None => {}
365 }
366 let frame = Frame {
367 v: PROTOCOL_VERSION,
368 id,
369 reply_to: Some(id),
370 session: session_for_reply,
371 sender: None,
372 body: reply,
373 };
374 let _ = out.send(frame);
375 }
376 });
377 }
378}
379
380fn roster_infos(
384 registry: &Registry,
385 root_id: &SessionId,
386 roster: &[(SessionId, SessionHandle)],
387) -> Vec<SessionInfo> {
388 let mut ids = vec![root_id.clone()];
389 for (id, _) in roster {
390 if id != root_id {
391 ids.push(id.clone());
392 }
393 }
394 ids.into_iter()
395 .map(|id| {
396 let model = registry.model_of(&id);
397 let state = registry.state_of(&id);
398 let effort = registry.effort_of(&id);
399 let context_window = registry.window_of(&id);
400 SessionInfo { id, model, effort, context_window, state }
401 })
402 .collect()
403}
404
405fn fan(out: &mpsc::UnboundedSender<Frame<AgentEvent>>, session: &SessionId, handle: &SessionHandle) {
409 let mut events = handle.subscribe();
410 let out = out.clone();
411 let session = session.clone();
412 tokio::spawn(async move {
413 loop {
414 match events.recv().await {
415 Ok(event) => {
416 let frame = Frame {
417 v: PROTOCOL_VERSION,
418 id: 0,
419 reply_to: None,
420 session: session.clone(),
421 sender: None,
422 body: event,
423 };
424 if out.send(frame).is_err() {
425 break;
426 }
427 }
428 Err(broadcast::error::RecvError::Lagged(_)) => continue,
429 Err(broadcast::error::RecvError::Closed) => break,
430 }
431 }
432 });
433}
434
435async fn write_loop<W>(write: W, mut rx: mpsc::UnboundedReceiver<Frame<AgentEvent>>)
436where
437 W: AsyncWrite + Unpin,
438{
439 let mut write = BufWriter::new(write);
440 while let Some(frame) = rx.recv().await {
441 if write_frame(&mut write, &frame).await.is_err() {
442 break;
443 }
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450 use std::collections::VecDeque;
451 use std::sync::{Arc, Mutex};
452 use std::time::Duration;
453
454 use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
455 use wcode_harness::actor::SessionActor;
456 use wcode_harness::agent::{Agent, AgentConfig};
457 use wcode_harness::event::LlmStreamEvent;
458 use wcode_harness::message::StopReason;
459 use wcode_harness::streamfn::{LlmOpts, LlmStream, StreamFn};
460
461 fn test_root() -> (Root, Registry) {
467 let script = Arc::new(Mutex::new(VecDeque::from([vec![
468 LlmStreamEvent::TextDelta("hi".into()),
469 LlmStreamEvent::Done {
470 stop_reason: StopReason::Stop,
471 usage: None,
472 },
473 ]])));
474 let stream_fn: StreamFn = Arc::new(move |_ctx, _sys, _tools, _opts| {
475 let events = script.lock().unwrap().pop_front().unwrap_or_default();
476 Box::pin(futures::stream::iter(events)) as LlmStream
477 });
478 let handle = SessionActor::spawn(Agent::new(AgentConfig {
479 system: "sys".into(),
480 tools: Vec::new(),
481 llm: LlmOpts {
482 model: "m".into(),
483 ..LlmOpts::default()
484 },
485 stream_fn,
486 hooks: wcode_harness::hooks::HooksSet::default(),
487 session: None,
488 context: Vec::new(),
489 working_dir: std::path::PathBuf::new(),
490 max_turns: wcode_harness::loop_::DEFAULT_MAX_TURNS,
491 parallel_tools: true,
492 compaction: wcode_harness::compaction::CompactionPolicy::default(),
493 plan_mode: wcode_harness::hooks::PlanModeHandle::new(),
494 }));
495 ((SessionId::new("test"), handle), Registry::new())
496 }
497
498 fn driver(server: tokio::io::DuplexStream) -> tokio::task::JoinHandle<()> {
500 let (root, registry) = test_root();
501 let roster: Roster = tokio::sync::watch::channel(Vec::new()).1;
502 tokio::spawn(serve_stream(registry, roster, root, None, server))
503 }
504
505 #[tokio::test]
506 async fn stdio_duplex_round_trips_submit_to_events() {
507 let (server, client) = tokio::io::duplex(64 * 1024);
508 let driver = driver(server);
509 let (cr, mut cw) = tokio::io::split(client);
510
511 let submit = Frame::new(1, SessionId::new("test"), Request::Submit { text: "hi".into() });
512 write_frame(&mut cw, &submit).await.unwrap();
513
514 let mut reader = BufReader::new(cr);
516 let mut line = String::new();
517 let mut saw_message_end = false;
518 let mut saw_stopped = false;
519 for _ in 0..64 {
520 line.clear();
521 let n = tokio::time::timeout(Duration::from_secs(5), reader.read_line(&mut line))
522 .await
523 .expect("a frame in time")
524 .expect("read");
525 assert!(n > 0, "EOF before the run finished");
526 let value: serde_json::Value =
527 serde_json::from_str(line.trim()).expect("each line is one JSON object");
528 assert!(value.is_object(), "a frame is a flat JSON object");
529 match value["type"].as_str() {
530 Some("message_end") => saw_message_end = true,
531 Some("stopped") => {
532 saw_stopped = true;
533 break;
534 }
535 _ => {}
536 }
537 }
538 assert!(saw_message_end, "the Submit's streamed events arrived");
539 assert!(saw_stopped, "the run's correlated reply arrived");
540
541 drop(cw);
545 drop(reader);
546 let joined = tokio::time::timeout(Duration::from_secs(5), driver)
547 .await
548 .expect("driver ends on EOF");
549 assert!(joined.is_ok());
550 }
551
552 #[tokio::test]
553 async fn a_malformed_line_ends_the_connection() {
554 let (server, client) = tokio::io::duplex(64 * 1024);
555 let driver = driver(server);
556 let (_cr, mut cw) = tokio::io::split(client);
557
558 cw.write_all(b"not json\n").await.unwrap();
559 cw.flush().await.unwrap();
560
561 let joined = tokio::time::timeout(Duration::from_secs(5), driver)
563 .await
564 .expect("driver stops, no hang");
565 assert!(joined.is_ok());
566 }
567
568 async fn read_line<R>(reader: &mut BufReader<R>, line: &mut String)
570 where
571 R: AsyncRead + Unpin,
572 {
573 let n = tokio::time::timeout(Duration::from_secs(5), reader.read_line(line))
574 .await
575 .expect("a frame in time")
576 .expect("read");
577 assert!(n > 0, "a non-empty frame");
578 }
579
580 fn json(line: &str) -> serde_json::Value {
582 serde_json::from_str(line.trim()).unwrap()
583 }
584
585 #[tokio::test]
586 async fn list_models_replies_with_the_registry_catalog() {
587 let (server, client) = tokio::io::duplex(64 * 1024);
588 let (root, registry) = test_root();
589 registry.set_models(vec!["m1".into(), "m2".into()]);
590 let roster: Roster = tokio::sync::watch::channel(Vec::new()).1;
591 let _driver = tokio::spawn(serve_stream(registry, roster, root, None, server));
592 let (cr, mut cw) = tokio::io::split(client);
593
594 let req = Frame::new(1, SessionId::new("test"), Request::ListModels);
595 write_frame(&mut cw, &req).await.unwrap();
596
597 let mut reader = BufReader::new(cr);
599 let mut line = String::new();
600 read_line(&mut reader, &mut line).await;
601 assert_eq!(json(&line)["type"], "sessions");
602 line.clear();
603 read_line(&mut reader, &mut line).await;
604 let value = json(&line);
605 assert_eq!(value["type"], "models");
606 assert_eq!(value["reply_to"], 1);
607 assert_eq!(value["models"], serde_json::json!(["m1", "m2"]));
608 }
609
610 #[tokio::test]
611 async fn roster_carries_effort_and_window() {
612 let (server, client) = tokio::io::duplex(64 * 1024);
613 let (root, registry) = test_root();
614 let root_id = root.0.clone();
615 registry.set_model(root_id.clone(), "m1");
616 registry.set_effort(root_id.clone(), Some("high".into()));
617 registry.set_context_window(root_id.clone(), 200_000);
618 let roster: Roster = tokio::sync::watch::channel(Vec::new()).1;
619 let _driver = tokio::spawn(serve_stream(registry, roster, root, None, server));
620 let (cr, _cw) = tokio::io::split(client);
621
622 let mut reader = BufReader::new(cr);
624 let mut line = String::new();
625 read_line(&mut reader, &mut line).await;
626 let value = json(&line);
627 assert_eq!(value["type"], "sessions");
628 let row = &value["sessions"][0];
629 assert_eq!(row["id"], "test");
630 assert_eq!(row["model"], "m1");
631 assert_eq!(row["effort"], "high");
632 assert_eq!(row["context_window"], 200_000);
633 }
634
635 #[tokio::test]
636 async fn a_clean_eof_shuts_the_driver_down() {
637 let (server, client) = tokio::io::duplex(64 * 1024);
638 let driver = driver(server);
639 let (cr, mut cw) = tokio::io::split(client);
640
641 let frame = Frame::new(1, SessionId::new("test"), Request::ListSessions);
643 write_frame(&mut cw, &frame).await.unwrap();
644
645 let mut reader = BufReader::new(cr);
646 let mut line = String::new();
647 let n = tokio::time::timeout(Duration::from_secs(5), reader.read_line(&mut line))
648 .await
649 .expect("a frame in time")
650 .expect("read");
651 assert!(n > 0);
652 let value: serde_json::Value = serde_json::from_str(line.trim()).unwrap();
653 assert_eq!(value["type"], "sessions");
654
655 drop(cw);
657 drop(reader);
658 let joined = tokio::time::timeout(Duration::from_secs(5), driver)
659 .await
660 .expect("driver ends on EOF");
661 assert!(joined.is_ok());
662 }
663}