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 title_after_turns: wcode_harness::title::DEFAULT_TITLE_AFTER_TURNS,
496 }));
497 ((SessionId::new("test"), handle), Registry::new())
498 }
499
500 fn driver(server: tokio::io::DuplexStream) -> tokio::task::JoinHandle<()> {
502 let (root, registry) = test_root();
503 let roster: Roster = tokio::sync::watch::channel(Vec::new()).1;
504 tokio::spawn(serve_stream(registry, roster, root, None, server))
505 }
506
507 #[tokio::test]
508 async fn stdio_duplex_round_trips_submit_to_events() {
509 let (server, client) = tokio::io::duplex(64 * 1024);
510 let driver = driver(server);
511 let (cr, mut cw) = tokio::io::split(client);
512
513 let submit = Frame::new(1, SessionId::new("test"), Request::Submit { text: "hi".into() });
514 write_frame(&mut cw, &submit).await.unwrap();
515
516 let mut reader = BufReader::new(cr);
518 let mut line = String::new();
519 let mut saw_message_end = false;
520 let mut saw_stopped = false;
521 for _ in 0..64 {
522 line.clear();
523 let n = tokio::time::timeout(Duration::from_secs(5), reader.read_line(&mut line))
524 .await
525 .expect("a frame in time")
526 .expect("read");
527 assert!(n > 0, "EOF before the run finished");
528 let value: serde_json::Value =
529 serde_json::from_str(line.trim()).expect("each line is one JSON object");
530 assert!(value.is_object(), "a frame is a flat JSON object");
531 match value["type"].as_str() {
532 Some("message_end") => saw_message_end = true,
533 Some("stopped") => {
534 saw_stopped = true;
535 break;
536 }
537 _ => {}
538 }
539 }
540 assert!(saw_message_end, "the Submit's streamed events arrived");
541 assert!(saw_stopped, "the run's correlated reply arrived");
542
543 drop(cw);
547 drop(reader);
548 let joined = tokio::time::timeout(Duration::from_secs(5), driver)
549 .await
550 .expect("driver ends on EOF");
551 assert!(joined.is_ok());
552 }
553
554 #[tokio::test]
555 async fn a_malformed_line_ends_the_connection() {
556 let (server, client) = tokio::io::duplex(64 * 1024);
557 let driver = driver(server);
558 let (_cr, mut cw) = tokio::io::split(client);
559
560 cw.write_all(b"not json\n").await.unwrap();
561 cw.flush().await.unwrap();
562
563 let joined = tokio::time::timeout(Duration::from_secs(5), driver)
565 .await
566 .expect("driver stops, no hang");
567 assert!(joined.is_ok());
568 }
569
570 async fn read_line<R>(reader: &mut BufReader<R>, line: &mut String)
572 where
573 R: AsyncRead + Unpin,
574 {
575 let n = tokio::time::timeout(Duration::from_secs(5), reader.read_line(line))
576 .await
577 .expect("a frame in time")
578 .expect("read");
579 assert!(n > 0, "a non-empty frame");
580 }
581
582 fn json(line: &str) -> serde_json::Value {
584 serde_json::from_str(line.trim()).unwrap()
585 }
586
587 #[tokio::test]
588 async fn list_models_replies_with_the_registry_catalog() {
589 let (server, client) = tokio::io::duplex(64 * 1024);
590 let (root, registry) = test_root();
591 registry.set_models(vec!["m1".into(), "m2".into()]);
592 let roster: Roster = tokio::sync::watch::channel(Vec::new()).1;
593 let _driver = tokio::spawn(serve_stream(registry, roster, root, None, server));
594 let (cr, mut cw) = tokio::io::split(client);
595
596 let req = Frame::new(1, SessionId::new("test"), Request::ListModels);
597 write_frame(&mut cw, &req).await.unwrap();
598
599 let mut reader = BufReader::new(cr);
601 let mut line = String::new();
602 read_line(&mut reader, &mut line).await;
603 assert_eq!(json(&line)["type"], "sessions");
604 line.clear();
605 read_line(&mut reader, &mut line).await;
606 let value = json(&line);
607 assert_eq!(value["type"], "models");
608 assert_eq!(value["reply_to"], 1);
609 assert_eq!(value["models"], serde_json::json!(["m1", "m2"]));
610 }
611
612 #[tokio::test]
613 async fn roster_carries_effort_and_window() {
614 let (server, client) = tokio::io::duplex(64 * 1024);
615 let (root, registry) = test_root();
616 let root_id = root.0.clone();
617 registry.set_model(root_id.clone(), "m1");
618 registry.set_effort(root_id.clone(), Some("high".into()));
619 registry.set_context_window(root_id.clone(), 200_000);
620 let roster: Roster = tokio::sync::watch::channel(Vec::new()).1;
621 let _driver = tokio::spawn(serve_stream(registry, roster, root, None, server));
622 let (cr, _cw) = tokio::io::split(client);
623
624 let mut reader = BufReader::new(cr);
626 let mut line = String::new();
627 read_line(&mut reader, &mut line).await;
628 let value = json(&line);
629 assert_eq!(value["type"], "sessions");
630 let row = &value["sessions"][0];
631 assert_eq!(row["id"], "test");
632 assert_eq!(row["model"], "m1");
633 assert_eq!(row["effort"], "high");
634 assert_eq!(row["context_window"], 200_000);
635 }
636
637 #[tokio::test]
638 async fn a_clean_eof_shuts_the_driver_down() {
639 let (server, client) = tokio::io::duplex(64 * 1024);
640 let driver = driver(server);
641 let (cr, mut cw) = tokio::io::split(client);
642
643 let frame = Frame::new(1, SessionId::new("test"), Request::ListSessions);
645 write_frame(&mut cw, &frame).await.unwrap();
646
647 let mut reader = BufReader::new(cr);
648 let mut line = String::new();
649 let n = tokio::time::timeout(Duration::from_secs(5), reader.read_line(&mut line))
650 .await
651 .expect("a frame in time")
652 .expect("read");
653 assert!(n > 0);
654 let value: serde_json::Value = serde_json::from_str(line.trim()).unwrap();
655 assert_eq!(value["type"], "sessions");
656
657 drop(cw);
659 drop(reader);
660 let joined = tokio::time::timeout(Duration::from_secs(5), driver)
661 .await
662 .expect("driver ends on EOF");
663 assert!(joined.is_ok());
664 }
665}