use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc, Mutex,
};
use rho_sdk::SessionId;
use tokio::sync::mpsc;
pub(crate) const NOTICE_QUEUE_CAPACITY: usize = 32;
pub(crate) const MAX_MESSAGE_BYTES: usize = 8 * 1024;
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ValidatedMessage(String);
impl ValidatedMessage {
pub(crate) fn parse(text: &str) -> Result<Self, MessageValidationError> {
let trimmed = text.trim();
if trimmed.is_empty() {
return Err(MessageValidationError::Empty);
}
let bytes = trimmed.len();
if bytes > MAX_MESSAGE_BYTES {
return Err(MessageValidationError::TooLarge {
bytes,
max_bytes: MAX_MESSAGE_BYTES,
});
}
Ok(Self(trimmed.to_string()))
}
pub(crate) fn as_str(&self) -> &str {
&self.0
}
pub(crate) fn into_string(self) -> String {
self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub(crate) enum MessageValidationError {
#[error("message text must not be empty")]
Empty,
#[error("message text is {bytes} bytes; limit is {max_bytes} bytes")]
TooLarge { bytes: usize, max_bytes: usize },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct SubagentNotice {
pub(crate) run_id: String,
pub(crate) agent_id: String,
pub(crate) parent_session_id: SessionId,
pub(crate) message: String,
}
#[derive(Clone)]
pub(crate) struct NoticePermits {
outstanding: Arc<AtomicUsize>,
}
impl NoticePermits {
fn new() -> Self {
Self {
outstanding: Arc::new(AtomicUsize::new(0)),
}
}
pub(crate) fn release(&self, count: usize) {
if count == 0 {
return;
}
self.outstanding
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
current.checked_sub(count)
})
.expect("notice permit release exceeds outstanding reservations");
}
fn try_reserve(&self, capacity: usize) -> bool {
let mut current = self.outstanding.load(Ordering::Acquire);
while current < capacity {
match self.outstanding.compare_exchange_weak(
current,
current + 1,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => return true,
Err(observed) => current = observed,
}
}
false
}
#[cfg(test)]
pub(crate) fn outstanding(&self) -> usize {
self.outstanding.load(Ordering::Acquire)
}
}
struct NoticeBinding {
sender: mpsc::Sender<SubagentNotice>,
permits: NoticePermits,
}
#[derive(Clone)]
pub(crate) struct SubagentNoticeBridge {
binding: Arc<Mutex<Option<NoticeBinding>>>,
capacity: usize,
}
impl Default for SubagentNoticeBridge {
fn default() -> Self {
Self::new()
}
}
pub(crate) struct NoticeRebind {
pub(crate) receiver: mpsc::Receiver<SubagentNotice>,
pub(crate) permits: NoticePermits,
pub(crate) retained: Vec<SubagentNotice>,
pub(crate) retired_permits: Option<NoticePermits>,
}
impl SubagentNoticeBridge {
pub(crate) fn new() -> Self {
Self {
binding: Arc::new(Mutex::new(None)),
capacity: NOTICE_QUEUE_CAPACITY,
}
}
#[cfg(test)]
pub(crate) fn bind_parent(&self) -> (mpsc::Receiver<SubagentNotice>, NoticePermits) {
let rebind = self.rebind_parent(None);
(rebind.receiver, rebind.permits)
}
pub(crate) fn rebind_parent(
&self,
mut old_receiver: Option<mpsc::Receiver<SubagentNotice>>,
) -> NoticeRebind {
let mut guard = self.binding_slot();
let retired_permits = guard.as_ref().map(|binding| binding.permits.clone());
let mut retained = Vec::new();
if let Some(receiver) = old_receiver.as_mut() {
while let Ok(notice) = receiver.try_recv() {
retained.push(notice);
}
}
drop(old_receiver);
let (sender, receiver) = mpsc::channel(self.capacity);
let permits = NoticePermits::new();
*guard = Some(NoticeBinding {
sender,
permits: permits.clone(),
});
NoticeRebind {
receiver,
permits,
retained,
retired_permits,
}
}
pub(crate) fn unbind_parent(&self) {
*self.binding_slot() = None;
}
pub(crate) fn is_bound(&self) -> bool {
self.binding_slot().is_some()
}
pub(crate) fn post(&self, notice: SubagentNotice) -> Result<(), NoticePostError> {
self.post_with_enqueue_gap(notice, &|| {})
}
pub(crate) fn post_with_enqueue_gap(
&self,
notice: SubagentNotice,
gap: &dyn Fn(),
) -> Result<(), NoticePostError> {
let capacity = self.capacity;
let guard = self.binding_slot();
let binding = guard.as_ref().ok_or(NoticePostError::Unbound)?;
if !binding.permits.try_reserve(capacity) {
return Err(NoticePostError::QueueFull { capacity });
}
gap();
binding.sender.try_send(notice).map_err(|error| {
binding.permits.release(1);
match error {
mpsc::error::TrySendError::Full(_) => NoticePostError::QueueFull { capacity },
mpsc::error::TrySendError::Closed(_) => NoticePostError::Unbound,
}
})
}
fn binding_slot(&self) -> std::sync::MutexGuard<'_, Option<NoticeBinding>> {
self.binding
.lock()
.expect("subagent notice bridge binding lock")
}
#[cfg(test)]
fn binding_lock_held(&self) -> bool {
match self.binding.try_lock() {
Ok(_) => false,
Err(std::sync::TryLockError::WouldBlock) => true,
Err(std::sync::TryLockError::Poisoned(poisoned)) => {
panic!("subagent notice bridge binding lock poisoned: {poisoned}")
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub(crate) enum NoticePostError {
#[error("delegated agent notices require an interactive parent session listening for them")]
Unbound,
#[error(
"parent notice queue is full ({capacity} waiting); deliver pending notices before sending more"
)]
QueueFull { capacity: usize },
}
pub(crate) trait NoticePoster: Send + Sync {
fn post(&self, message: ValidatedMessage) -> Result<(), NoticePostError>;
}
#[derive(Clone, Debug, Default)]
pub(crate) struct SteeringSlot {
handle: Arc<Mutex<Option<rho_sdk::SteeringHandle>>>,
}
impl SteeringSlot {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn publish(&self, handle: rho_sdk::SteeringHandle) {
*self.slot() = Some(handle);
}
pub(crate) fn clear(&self) {
*self.slot() = None;
}
pub(crate) fn handle(&self) -> Option<rho_sdk::SteeringHandle> {
self.slot().clone()
}
fn slot(&self) -> std::sync::MutexGuard<'_, Option<rho_sdk::SteeringHandle>> {
self.handle.lock().expect("delegated steering slot lock")
}
}
pub(crate) fn parent_message_prompt(message: &ValidatedMessage) -> String {
format!(
"Message from the parent session (not a new task - incorporate this into your current work):\n\n{}",
message.as_str()
)
}
pub(crate) fn notice_prompts(notices: &[SubagentNotice]) -> (String, String) {
let model = notices
.iter()
.map(|notice| {
format!(
"Message from delegated agent {} ({}):\n{}",
notice.run_id, notice.agent_id, notice.message
)
})
.collect::<Vec<_>>()
.join("\n\n");
let display = notices
.iter()
.map(|notice| format!("agent {} ({}) notice", notice.run_id, notice.agent_id))
.collect::<Vec<_>>()
.join("\n");
(model, display)
}
#[cfg(test)]
#[path = "subagent_messaging_tests.rs"]
mod tests;