use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio::sync::watch;
use wcode_harness::actor::SessionHandle;
use wcode_harness::protocol::{MemberState, Request, SessionId};
use crate::backend::Backend;
#[cfg(unix)]
use crate::client::Client;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RouteError {
Unknown(SessionId),
NotPermitted { from: SessionId, to: SessionId },
Closed,
}
impl std::fmt::Display for RouteError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RouteError::Unknown(id) => write!(f, "no peer at {id}"),
RouteError::NotPermitted { from, to } => {
write!(f, "{from} may not address {to}")
}
RouteError::Closed => f.write_str("the target session is closed"),
}
}
}
impl std::error::Error for RouteError {}
#[derive(Clone, Default)]
pub struct Registry {
inner: Arc<Inner>,
}
struct Inner {
peers: Mutex<HashMap<SessionId, Backend>>,
owners: Mutex<HashMap<SessionId, SessionId>>,
models: Mutex<HashMap<SessionId, String>>,
states: Mutex<HashMap<SessionId, MemberState>>,
roster: watch::Sender<Vec<(SessionId, SessionHandle)>>,
}
impl Default for Inner {
fn default() -> Self {
Self {
peers: Mutex::new(HashMap::new()),
owners: Mutex::new(HashMap::new()),
models: Mutex::new(HashMap::new()),
states: Mutex::new(HashMap::new()),
roster: watch::channel(Vec::new()).0,
}
}
}
impl Registry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&self, id: SessionId, handle: SessionHandle) {
let mut state_rx = handle.state_rx();
self.inner
.peers
.lock()
.unwrap()
.insert(id.clone(), Backend::Local(handle));
let registry = self.clone();
tokio::spawn(async move {
while state_rx.changed().await.is_ok() {
registry.set_state(id.clone(), *state_rx.borrow_and_update());
}
});
self.refresh_roster();
}
#[cfg(unix)]
pub fn register_remote(&self, id: SessionId, client: Client) {
self.inner
.peers
.lock()
.unwrap()
.insert(id, Backend::Remote(client.clone()));
let registry = self.clone();
let mut roster = client.subscribe_roster();
tokio::spawn(async move {
while roster.changed().await.is_ok() {
let sessions = roster.borrow_and_update().clone();
for info in &sessions {
registry.set_state(info.id.clone(), info.state);
}
}
});
self.refresh_roster();
}
pub fn locals(&self) -> Vec<(SessionId, SessionHandle)> {
let mut locals: Vec<(SessionId, SessionHandle)> = self
.inner
.peers
.lock()
.unwrap()
.iter()
.filter_map(|(id, backend)| match backend {
Backend::Local(handle) => Some((id.clone(), handle.clone())),
#[cfg(unix)]
Backend::Remote(_) => None,
})
.collect();
locals.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str()));
locals
}
pub fn subscribe(&self) -> watch::Receiver<Vec<(SessionId, SessionHandle)>> {
self.inner.roster.subscribe()
}
fn refresh_roster(&self) {
let _ = self.inner.roster.send_replace(self.locals());
}
pub fn contains(&self, id: &SessionId) -> bool {
self.inner.peers.lock().unwrap().contains_key(id)
}
pub fn set_owner(&self, worker: SessionId, owner: SessionId) {
self.inner.owners.lock().unwrap().insert(worker, owner);
}
pub fn set_model(&self, id: SessionId, model: impl Into<String>) {
self.inner.models.lock().unwrap().insert(id, model.into());
}
pub fn set_state(&self, id: SessionId, state: MemberState) {
self.inner.states.lock().unwrap().insert(id, state);
self.refresh_roster();
}
pub fn state_of(&self, id: &SessionId) -> MemberState {
*self
.inner
.states
.lock()
.unwrap()
.get(id)
.unwrap_or(&MemberState::Idle)
}
pub fn model_of(&self, id: &SessionId) -> Option<String> {
self.inner.models.lock().unwrap().get(id).cloned()
}
pub fn permitted(&self, from: &SessionId, to: &SessionId) -> bool {
let owners = self.inner.owners.lock().unwrap();
owners.get(from) == Some(to) || owners.get(to) == Some(from)
}
pub fn resolve(&self, from: &SessionId, to: &SessionId) -> Result<Backend, RouteError> {
let endpoint = self
.inner
.peers
.lock()
.unwrap()
.get(to)
.cloned()
.ok_or_else(|| RouteError::Unknown(to.clone()))?;
if !self.permitted(from, to) {
return Err(RouteError::NotPermitted {
from: from.clone(),
to: to.clone(),
});
}
Ok(endpoint)
}
pub fn deliver(
&self,
from: &SessionId,
to: &SessionId,
request: Request,
) -> Result<(), RouteError> {
let endpoint = self.resolve(from, to)?;
endpoint
.send_from(from.clone(), request)
.map_err(|_| RouteError::Closed)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use wcode_harness::actor::SessionActor;
use wcode_harness::agent::{Agent, AgentConfig};
use wcode_harness::compaction::CompactionPolicy;
use wcode_harness::event::{AgentEvent, LlmStreamEvent};
use wcode_harness::hooks::HooksSet;
use wcode_harness::loop_::DEFAULT_MAX_TURNS;
use wcode_harness::streamfn::{LlmOpts, LlmStream, StreamFn};
use futures::StreamExt as _;
fn session() -> SessionHandle {
session_with(Arc::new(|_ctx, _sys, _tools, _opts| {
Box::pin(futures::stream::empty()) as LlmStream
}))
}
fn session_with(stream_fn: StreamFn) -> SessionHandle {
SessionActor::spawn(Agent::new(AgentConfig {
system: "sys".into(),
tools: Vec::new(),
llm: LlmOpts {
model: "m".into(),
..LlmOpts::default()
},
stream_fn,
hooks: HooksSet::default(),
session: None,
context: Vec::new(),
working_dir: std::path::PathBuf::new(),
max_turns: DEFAULT_MAX_TURNS,
parallel_tools: true,
compaction: CompactionPolicy::default(),
plan_mode: wcode_harness::hooks::PlanModeHandle::new(),
}))
}
async fn next_message(
rx: &mut tokio::sync::broadcast::Receiver<AgentEvent>,
) -> AgentEvent {
tokio::time::timeout(Duration::from_secs(2), rx.recv())
.await
.expect("an event")
.expect("open")
}
#[tokio::test]
async fn orchestrator_and_worker_reach_each_other() {
let reg = Registry::new();
let orch = SessionId::agent("orch");
let worker = SessionId::agent("w1");
let h_orch = session();
let h_worker = session();
reg.register(orch.clone(), h_orch.clone());
reg.register(worker.clone(), h_worker.clone());
reg.set_owner(worker.clone(), orch.clone());
let mut rx = h_worker.subscribe();
reg.deliver(
&orch,
&worker,
Request::Notify {
content: "do the thing".into(),
},
)
.unwrap();
match next_message(&mut rx).await {
AgentEvent::MessageReceived { from, content } => {
assert_eq!(from, orch);
assert_eq!(content, "do the thing");
}
other => panic!("expected MessageReceived, got {other:?}"),
}
let mut rx = h_orch.subscribe();
reg.deliver(
&worker,
&orch,
Request::Notify {
content: "done".into(),
},
)
.unwrap();
match next_message(&mut rx).await {
AgentEvent::MessageReceived { from, content } => {
assert_eq!(from, worker);
assert_eq!(content, "done");
}
other => panic!("expected MessageReceived, got {other:?}"),
}
}
#[tokio::test]
async fn lateral_message_between_workers_is_refused() {
let reg = Registry::new();
let orch = SessionId::agent("orch");
let w1 = SessionId::agent("w1");
let w2 = SessionId::agent("w2");
for id in [&orch, &w1, &w2] {
reg.register(id.clone(), session());
}
reg.set_owner(w1.clone(), orch.clone());
reg.set_owner(w2.clone(), orch.clone());
assert_eq!(
reg.deliver(
&w1,
&w2,
Request::Notify {
content: "psst".into()
}
),
Err(RouteError::NotPermitted {
from: w1.clone(),
to: w2.clone()
})
);
assert!(!reg.permitted(&w1, &w2));
}
#[tokio::test]
async fn unknown_target_is_reported() {
let reg = Registry::new();
let orch = SessionId::agent("orch");
reg.register(orch.clone(), session());
let ghost = SessionId::agent("ghost");
assert_eq!(
reg.deliver(&orch, &ghost, Request::Notify { content: "?".into() }),
Err(RouteError::Unknown(ghost))
);
}
#[tokio::test]
async fn locals_lists_only_local_peers_sorted_by_id() {
let reg = Registry::new();
let b = SessionId::agent("b");
let a = SessionId::agent("a");
reg.register(b.clone(), session());
reg.register(a.clone(), session());
#[cfg(unix)]
reg.register_remote(
SessionId::agent("remote"),
Client::lazy(std::path::Path::new("/nonexistent-t2.sock")),
);
let ids: Vec<SessionId> = reg.locals().into_iter().map(|(id, _)| id).collect();
assert_eq!(ids, vec![a, b]);
}
#[tokio::test]
async fn set_model_is_read_back_per_id() {
let reg = Registry::new();
let w1 = SessionId::agent("w1");
let ghost = SessionId::agent("ghost");
assert_eq!(reg.model_of(&w1), None, "unset id has no model");
reg.set_model(w1.clone(), "m1");
assert_eq!(reg.model_of(&w1).as_deref(), Some("m1"));
assert_eq!(reg.model_of(&ghost), None);
}
#[tokio::test]
async fn set_state_is_read_back_and_republishes() {
let reg = Registry::new();
let w1 = SessionId::agent("w1");
assert_eq!(reg.state_of(&w1), MemberState::Idle, "unset id reads Idle");
let mut rx = reg.subscribe();
reg.set_state(w1.clone(), MemberState::Running);
assert_eq!(reg.state_of(&w1), MemberState::Running);
tokio::time::timeout(Duration::from_secs(2), rx.changed())
.await
.expect("a roster change")
.expect("open");
}
#[tokio::test]
async fn register_mirrors_the_actor_state_into_the_roster() {
let stream_fn: StreamFn = Arc::new(|_ctx, _sys, _tools, _opts| {
let head =
futures::stream::iter(vec![LlmStreamEvent::TextDelta("part".into())]);
Box::pin(head.chain(futures::stream::pending())) as LlmStream
});
let handle = session_with(stream_fn);
let reg = Registry::new();
let w1 = SessionId::agent("w1");
reg.register(w1.clone(), handle.clone());
assert_eq!(reg.state_of(&w1), MemberState::Idle, "a virgin session is idle");
handle.send(Request::Submit { text: "hi".into() }).unwrap();
let mut running = false;
for _ in 0..500 {
if reg.state_of(&w1) == MemberState::Running {
running = true;
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
assert!(running, "the registry mirrors the actor's Running");
handle.send(Request::Cancel).unwrap();
let mut done = false;
for _ in 0..500 {
if reg.state_of(&w1) == MemberState::Done {
done = true;
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
assert!(done, "the registry mirrors the actor's Done");
}
#[tokio::test]
async fn subscribe_yields_the_new_roster_after_register() {
let reg = Registry::new();
let mut rx = reg.subscribe();
let w1 = SessionId::agent("w1");
reg.register(w1.clone(), session());
rx.changed().await.unwrap();
let ids: Vec<SessionId> = rx.borrow().iter().map(|(id, _)| id.clone()).collect();
assert_eq!(ids, vec![w1]);
}
#[tokio::test]
async fn subscribe_seeds_a_session_registered_before_it() {
let reg = Registry::new();
let w1 = SessionId::agent("w1");
reg.register(w1.clone(), session());
let rx = reg.subscribe();
let ids: Vec<SessionId> = rx.borrow().iter().map(|(id, _)| id.clone()).collect();
assert_eq!(ids, vec![w1]);
}
#[tokio::test]
async fn an_unowned_pair_is_not_permitted() {
let reg = Registry::new();
let a = SessionId::agent("a");
let b = SessionId::agent("b");
reg.register(a.clone(), session());
reg.register(b.clone(), session());
assert_eq!(
reg.deliver(&a, &b, Request::Notify { content: "?".into() }),
Err(RouteError::NotPermitted {
from: a.clone(),
to: b.clone()
})
);
}
}