use std::collections::HashSet;
use std::io;
use std::path::Path;
use tokio::io::{AsyncWrite, BufReader, BufWriter};
use tokio::net::{UnixListener, UnixStream};
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>;
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(connection(
registry.clone(),
roster.clone(),
root.clone(),
define.clone(),
stream,
));
}
}
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
}
async fn connection(
registry: Registry,
mut roster: Roster,
root: Root,
define: Option<DefineHandler>,
stream: UnixStream,
) {
let (read, write) = stream.into_split();
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();
let mut grew = false;
for (id, handle) in ¤t {
if seen.insert(id.clone()) {
fan(&out, id, handle);
grew = true;
}
}
if grew {
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 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();
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 {
let frame = Frame {
v: PROTOCOL_VERSION,
id,
reply_to: Some(id),
session,
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);
SessionInfo { id, model }
})
.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;
}
}
}