use std::collections::HashSet;
use std::io;
use std::path::Path;
use tokio::io::{AsyncRead, AsyncWrite, BufReader, BufWriter};
#[cfg(unix)]
use tokio::net::UnixListener;
use tokio::sync::{broadcast, mpsc, watch};
use wcode_harness::actor::SessionHandle;
use wcode_harness::event::AgentEvent;
use wcode_harness::protocol::{Frame, PROTOCOL_VERSION, Request, SessionId, SessionInfo};
use crate::Registry;
use crate::frame::{read_frame, write_frame};
pub type Roster = watch::Receiver<Vec<(SessionId, SessionHandle)>>;
pub type Root = (SessionId, SessionHandle);
#[derive(Clone, Debug, Default)]
pub struct DefineArgs {
pub name: Option<String>,
pub model: Option<String>,
pub role: Option<String>,
pub tools: Option<Vec<String>>,
pub base_url: Option<String>,
pub api_key: Option<String>,
pub read_only: bool,
pub effort: Option<String>,
}
pub type DefineHandler =
std::sync::Arc<dyn Fn(DefineArgs) -> Result<SessionId, String> + Send + Sync>;
#[cfg(unix)]
pub async fn serve(
registry: Registry,
roster: Roster,
root: Root,
define: Option<DefineHandler>,
listener: UnixListener,
) -> io::Result<()> {
loop {
let (stream, _addr) = listener.accept().await?;
tokio::spawn(serve_stream(
registry.clone(),
roster.clone(),
root.clone(),
define.clone(),
stream,
));
}
}
#[cfg(unix)]
pub async fn serve_at(
registry: Registry,
roster: Roster,
root: Root,
define: Option<DefineHandler>,
path: &Path,
) -> io::Result<()> {
let listener = crate::socket::bind(path).await?;
serve(registry, roster, root, define, listener).await
}
pub async fn serve_stdio(
registry: Registry,
roster: Roster,
root: Root,
define: Option<DefineHandler>,
) -> io::Result<()> {
let stream = tokio::io::join(tokio::io::stdin(), tokio::io::stdout());
serve_stream(registry, roster, root, define, stream).await;
Ok(())
}
pub async fn serve_stream<S>(
registry: Registry,
mut roster: Roster,
root: Root,
define: Option<DefineHandler>,
stream: S,
) where
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
let (read, write) = tokio::io::split(stream);
let (out, out_rx) = mpsc::unbounded_channel::<Frame<AgentEvent>>();
tokio::spawn(write_loop(write, out_rx));
let (root_id, root_handle) = root;
let mut seen: HashSet<SessionId> = HashSet::new();
seen.insert(root_id.clone());
fan(&out, &root_id, &root_handle);
let initial = roster.borrow_and_update().clone();
for (id, handle) in &initial {
if seen.insert(id.clone()) {
fan(&out, id, handle);
}
}
let _ = out.send(Frame {
v: PROTOCOL_VERSION,
id: 0,
reply_to: None,
session: root_id.clone(),
sender: None,
body: AgentEvent::Sessions {
sessions: roster_infos(®istry, &root_id, &initial),
},
});
let read_roster = roster.clone();
{
let out = out.clone();
let root_id = root_id.clone();
let registry = registry.clone();
tokio::spawn(async move {
loop {
if roster.changed().await.is_err() {
break;
}
let current = roster.borrow_and_update().clone();
for (id, handle) in ¤t {
if seen.insert(id.clone()) {
fan(&out, id, handle);
}
}
let sessions = roster_infos(®istry, &root_id, ¤t);
let _ = out.send(Frame {
v: PROTOCOL_VERSION,
id: 0,
reply_to: None,
session: root_id.clone(),
sender: None,
body: AgentEvent::Sessions { sessions },
});
}
});
}
let mut read = BufReader::new(read);
while let Ok(Some(frame)) = read_frame::<_, Request>(&mut read).await {
let Frame {
id, session, sender, body, ..
} = frame;
let live: Vec<(SessionId, SessionHandle)> = read_roster
.borrow()
.iter()
.filter(|(sid, _)| sid != &root_id)
.cloned()
.collect();
if matches!(body, Request::ListSessions) {
let sessions = roster_infos(®istry, &root_id, &live);
let _ = out.send(Frame {
v: PROTOCOL_VERSION,
id,
reply_to: Some(id),
session,
sender: None,
body: AgentEvent::Sessions { sessions },
});
continue;
}
if matches!(body, Request::ListModels) {
let _ = out.send(Frame {
v: PROTOCOL_VERSION,
id,
reply_to: Some(id),
session,
sender: None,
body: AgentEvent::Models { models: registry.models() },
});
continue;
}
if let Request::Define {
name,
model,
role,
tools,
base_url,
api_key,
read_only,
effort,
} = &body
{
let reply = match &define {
Some(handler) => match handler(DefineArgs {
name: name.clone(),
model: model.clone(),
role: role.clone(),
tools: tools.clone(),
base_url: base_url.clone(),
api_key: api_key.clone(),
read_only: *read_only,
effort: effort.clone(),
}) {
Ok(id) => AgentEvent::Spawned { worker: id },
Err(message) => AgentEvent::Error { message },
},
None => AgentEvent::Error {
message: "agent definition is not enabled on this server (serve --agents)"
.into(),
},
};
let _ = out.send(Frame {
v: PROTOCOL_VERSION,
id,
reply_to: Some(id),
session,
sender: None,
body: reply,
});
continue;
}
let handle = if session == root_id {
Some(root_handle.clone())
} else if let Some((_, handle)) = live.iter().find(|(sid, _)| sid == &session) {
Some(handle.clone())
} else if live.is_empty() {
Some(root_handle.clone())
} else {
None
};
let Some(handle) = handle else {
let message = format!("unknown session {session}");
let _ = out.send(Frame {
v: PROTOCOL_VERSION,
id,
reply_to: Some(id),
session,
sender: None,
body: AgentEvent::Error { message },
});
continue;
};
let out = out.clone();
let registry = registry.clone();
let mirror: Option<(Option<String>, Option<String>)> = match &body {
Request::SetModel { model } => Some((Some(model.clone()), None)),
Request::SetEffort { effort } => Some((None, effort.clone())),
_ => None,
};
let session_for_reply = session.clone();
tokio::spawn(async move {
let reply = match sender {
Some(from) => handle.ask_from(from, body).await,
None => handle.ask(body).await,
};
if let Ok(reply) = reply {
match mirror {
Some((Some(model), _)) => registry.set_model(session.clone(), model),
Some((None, effort)) => registry.set_effort(session.clone(), effort),
None => {}
}
let frame = Frame {
v: PROTOCOL_VERSION,
id,
reply_to: Some(id),
session: session_for_reply,
sender: None,
body: reply,
};
let _ = out.send(frame);
}
});
}
}
fn roster_infos(
registry: &Registry,
root_id: &SessionId,
roster: &[(SessionId, SessionHandle)],
) -> Vec<SessionInfo> {
let mut ids = vec![root_id.clone()];
for (id, _) in roster {
if id != root_id {
ids.push(id.clone());
}
}
ids.into_iter()
.map(|id| {
let model = registry.model_of(&id);
let state = registry.state_of(&id);
let effort = registry.effort_of(&id);
let context_window = registry.window_of(&id);
SessionInfo { id, model, effort, context_window, state }
})
.collect()
}
fn fan(out: &mpsc::UnboundedSender<Frame<AgentEvent>>, session: &SessionId, handle: &SessionHandle) {
let mut events = handle.subscribe();
let out = out.clone();
let session = session.clone();
tokio::spawn(async move {
loop {
match events.recv().await {
Ok(event) => {
let frame = Frame {
v: PROTOCOL_VERSION,
id: 0,
reply_to: None,
session: session.clone(),
sender: None,
body: event,
};
if out.send(frame).is_err() {
break;
}
}
Err(broadcast::error::RecvError::Lagged(_)) => continue,
Err(broadcast::error::RecvError::Closed) => break,
}
}
});
}
async fn write_loop<W>(write: W, mut rx: mpsc::UnboundedReceiver<Frame<AgentEvent>>)
where
W: AsyncWrite + Unpin,
{
let mut write = BufWriter::new(write);
while let Some(frame) = rx.recv().await {
if write_frame(&mut write, &frame).await.is_err() {
break;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use wcode_harness::actor::SessionActor;
use wcode_harness::agent::{Agent, AgentConfig};
use wcode_harness::event::LlmStreamEvent;
use wcode_harness::message::StopReason;
use wcode_harness::streamfn::{LlmOpts, LlmStream, StreamFn};
fn test_root() -> (Root, Registry) {
let script = Arc::new(Mutex::new(VecDeque::from([vec![
LlmStreamEvent::TextDelta("hi".into()),
LlmStreamEvent::Done {
stop_reason: StopReason::Stop,
usage: None,
},
]])));
let stream_fn: StreamFn = Arc::new(move |_ctx, _sys, _tools, _opts| {
let events = script.lock().unwrap().pop_front().unwrap_or_default();
Box::pin(futures::stream::iter(events)) as LlmStream
});
let handle = SessionActor::spawn(Agent::new(AgentConfig {
system: "sys".into(),
tools: Vec::new(),
llm: LlmOpts {
model: "m".into(),
..LlmOpts::default()
},
stream_fn,
hooks: wcode_harness::hooks::HooksSet::default(),
session: None,
context: Vec::new(),
working_dir: std::path::PathBuf::new(),
max_turns: wcode_harness::loop_::DEFAULT_MAX_TURNS,
parallel_tools: true,
compaction: wcode_harness::compaction::CompactionPolicy::default(),
plan_mode: wcode_harness::hooks::PlanModeHandle::new(),
}));
((SessionId::new("test"), handle), Registry::new())
}
fn driver(server: tokio::io::DuplexStream) -> tokio::task::JoinHandle<()> {
let (root, registry) = test_root();
let roster: Roster = tokio::sync::watch::channel(Vec::new()).1;
tokio::spawn(serve_stream(registry, roster, root, None, server))
}
#[tokio::test]
async fn stdio_duplex_round_trips_submit_to_events() {
let (server, client) = tokio::io::duplex(64 * 1024);
let driver = driver(server);
let (cr, mut cw) = tokio::io::split(client);
let submit = Frame::new(1, SessionId::new("test"), Request::Submit { text: "hi".into() });
write_frame(&mut cw, &submit).await.unwrap();
let mut reader = BufReader::new(cr);
let mut line = String::new();
let mut saw_message_end = false;
let mut saw_stopped = false;
for _ in 0..64 {
line.clear();
let n = tokio::time::timeout(Duration::from_secs(5), reader.read_line(&mut line))
.await
.expect("a frame in time")
.expect("read");
assert!(n > 0, "EOF before the run finished");
let value: serde_json::Value =
serde_json::from_str(line.trim()).expect("each line is one JSON object");
assert!(value.is_object(), "a frame is a flat JSON object");
match value["type"].as_str() {
Some("message_end") => saw_message_end = true,
Some("stopped") => {
saw_stopped = true;
break;
}
_ => {}
}
}
assert!(saw_message_end, "the Submit's streamed events arrived");
assert!(saw_stopped, "the run's correlated reply arrived");
drop(cw);
drop(reader);
let joined = tokio::time::timeout(Duration::from_secs(5), driver)
.await
.expect("driver ends on EOF");
assert!(joined.is_ok());
}
#[tokio::test]
async fn a_malformed_line_ends_the_connection() {
let (server, client) = tokio::io::duplex(64 * 1024);
let driver = driver(server);
let (_cr, mut cw) = tokio::io::split(client);
cw.write_all(b"not json\n").await.unwrap();
cw.flush().await.unwrap();
let joined = tokio::time::timeout(Duration::from_secs(5), driver)
.await
.expect("driver stops, no hang");
assert!(joined.is_ok());
}
async fn read_line<R>(reader: &mut BufReader<R>, line: &mut String)
where
R: AsyncRead + Unpin,
{
let n = tokio::time::timeout(Duration::from_secs(5), reader.read_line(line))
.await
.expect("a frame in time")
.expect("read");
assert!(n > 0, "a non-empty frame");
}
fn json(line: &str) -> serde_json::Value {
serde_json::from_str(line.trim()).unwrap()
}
#[tokio::test]
async fn list_models_replies_with_the_registry_catalog() {
let (server, client) = tokio::io::duplex(64 * 1024);
let (root, registry) = test_root();
registry.set_models(vec!["m1".into(), "m2".into()]);
let roster: Roster = tokio::sync::watch::channel(Vec::new()).1;
let _driver = tokio::spawn(serve_stream(registry, roster, root, None, server));
let (cr, mut cw) = tokio::io::split(client);
let req = Frame::new(1, SessionId::new("test"), Request::ListModels);
write_frame(&mut cw, &req).await.unwrap();
let mut reader = BufReader::new(cr);
let mut line = String::new();
read_line(&mut reader, &mut line).await;
assert_eq!(json(&line)["type"], "sessions");
line.clear();
read_line(&mut reader, &mut line).await;
let value = json(&line);
assert_eq!(value["type"], "models");
assert_eq!(value["reply_to"], 1);
assert_eq!(value["models"], serde_json::json!(["m1", "m2"]));
}
#[tokio::test]
async fn roster_carries_effort_and_window() {
let (server, client) = tokio::io::duplex(64 * 1024);
let (root, registry) = test_root();
let root_id = root.0.clone();
registry.set_model(root_id.clone(), "m1");
registry.set_effort(root_id.clone(), Some("high".into()));
registry.set_context_window(root_id.clone(), 200_000);
let roster: Roster = tokio::sync::watch::channel(Vec::new()).1;
let _driver = tokio::spawn(serve_stream(registry, roster, root, None, server));
let (cr, _cw) = tokio::io::split(client);
let mut reader = BufReader::new(cr);
let mut line = String::new();
read_line(&mut reader, &mut line).await;
let value = json(&line);
assert_eq!(value["type"], "sessions");
let row = &value["sessions"][0];
assert_eq!(row["id"], "test");
assert_eq!(row["model"], "m1");
assert_eq!(row["effort"], "high");
assert_eq!(row["context_window"], 200_000);
}
#[tokio::test]
async fn a_clean_eof_shuts_the_driver_down() {
let (server, client) = tokio::io::duplex(64 * 1024);
let driver = driver(server);
let (cr, mut cw) = tokio::io::split(client);
let frame = Frame::new(1, SessionId::new("test"), Request::ListSessions);
write_frame(&mut cw, &frame).await.unwrap();
let mut reader = BufReader::new(cr);
let mut line = String::new();
let n = tokio::time::timeout(Duration::from_secs(5), reader.read_line(&mut line))
.await
.expect("a frame in time")
.expect("read");
assert!(n > 0);
let value: serde_json::Value = serde_json::from_str(line.trim()).unwrap();
assert_eq!(value["type"], "sessions");
drop(cw);
drop(reader);
let joined = tokio::time::timeout(Duration::from_secs(5), driver)
.await
.expect("driver ends on EOF");
assert!(joined.is_ok());
}
}