use std::sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc, Mutex,
};
use rho_sdk::SessionId;
use tokio::sync::mpsc;
pub(crate) const CHILD_COMMUNICATION_CONTRACT: &str = include_str!("child_contract.md");
pub(crate) const NOTICE_QUEUE_CAPACITY: usize = 32;
const ACTION_NOTICE_RESERVE: usize = 1;
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, Copy, Debug, PartialEq, Eq)]
pub(crate) enum NoticeDelivery {
NextTurn,
ParentActionRequired,
}
impl NoticeDelivery {
pub(crate) fn requires_parent_action(self) -> bool {
match self {
Self::NextTurn => false,
Self::ParentActionRequired => true,
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct SubagentNotice {
pub(crate) run_id: String,
pub(crate) agent_id: String,
pub(crate) parent_session_id: SessionId,
pub(crate) message: String,
pub(crate) delivery: NoticeDelivery,
pub(crate) acknowledged: Arc<AtomicBool>,
}
impl PartialEq for SubagentNotice {
fn eq(&self, other: &Self) -> bool {
(
&self.run_id,
&self.agent_id,
&self.parent_session_id,
&self.message,
self.delivery,
) == (
&other.run_id,
&other.agent_id,
&other.parent_session_id,
&other.message,
other.delivery,
)
}
}
impl Eq for SubagentNotice {}
impl SubagentNotice {
pub(crate) fn acknowledge(&self) {
self.acknowledged.store(true, Ordering::Release);
}
pub(crate) fn is_acknowledged(&self) -> bool {
self.acknowledged.load(Ordering::Acquire)
}
}
#[derive(Clone)]
pub(crate) struct NoticePermits {
outstanding: Arc<AtomicUsize>,
pending: Arc<Mutex<Vec<SubagentNotice>>>,
}
impl NoticePermits {
fn new() -> Self {
Self {
outstanding: Arc::new(AtomicUsize::new(0)),
pending: Arc::new(Mutex::new(Vec::new())),
}
}
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");
}
pub(crate) fn release_notice(&self, notice: &SubagentNotice) {
let mut pending = self.pending.lock().expect("pending notice receipts");
if let Some(index) = pending
.iter()
.position(|entry| Arc::ptr_eq(&entry.acknowledged, ¬ice.acknowledged))
{
pending.remove(index);
}
self.release(1);
}
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>>>,
pending: Arc<Mutex<Vec<SubagentNotice>>>,
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)),
pending: Arc::new(Mutex::new(Vec::new())),
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 + ACTION_NOTICE_RESERVE);
let mut permits = NoticePermits::new();
permits.pending = Arc::clone(&self.pending);
*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 = match notice.delivery {
NoticeDelivery::NextTurn => self.capacity,
NoticeDelivery::ParentActionRequired => self.capacity + ACTION_NOTICE_RESERVE,
};
let _delivery = super::notification_delivery::lock();
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();
self.pending
.lock()
.expect("pending notice receipts")
.push(notice.clone());
binding.sender.try_send(notice.clone()).map_err(|error| {
binding.permits.release_notice(¬ice);
match error {
mpsc::error::TrySendError::Full(_) => NoticePostError::QueueFull { capacity },
mpsc::error::TrySendError::Closed(_) => NoticePostError::Unbound,
}
})?;
Ok(())
}
pub(crate) fn pending_for_run(&self, run_id: &str) -> Vec<SubagentNotice> {
let _binding = self.binding_slot();
self.pending
.lock()
.expect("pending notice receipts")
.iter()
.filter(|notice| notice.run_id == run_id)
.cloned()
.collect()
}
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 admission limit reached ({capacity} outstanding allowed for this class); deliver pending notices before sending more"
)]
QueueFull { capacity: usize },
}
pub(crate) trait NoticePoster: Send + Sync {
fn post(
&self,
message: ValidatedMessage,
delivery: NoticeDelivery,
) -> Result<(), NoticePostError>;
}
pub(crate) use super::parent_steering::SteeringSlot;
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_prompt(notices: &[SubagentNotice]) -> String {
notices
.iter()
.map(|notice| {
let label = match notice.delivery {
NoticeDelivery::NextTurn => "Message from delegated agent",
NoticeDelivery::ParentActionRequired => "Parent action required by delegated agent",
};
format!(
"{label} {} ({}):\n{}",
notice.run_id, notice.agent_id, notice.message
)
})
.collect::<Vec<_>>()
.join("\n\n")
}
#[cfg(test)]
#[path = "subagent_messaging_tests.rs"]
mod tests;