use std::sync::{
Arc,
atomic::{AtomicU64, Ordering},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct AgentId(pub(crate) u64);
impl std::fmt::Display for AgentId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "agent-{}", self.0)
}
}
static AGENT_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
pub(crate) fn next_agent_id() -> u64 {
AGENT_ID_COUNTER.fetch_add(1, Ordering::Relaxed)
}
pub(crate) struct AgentBridgeState {
pub(crate) registry: Option<Arc<crate::tools::ToolRegistry>>,
pub(crate) hook_runner: Option<Arc<crate::hooks::Hooks>>,
pub(crate) policies: crate::policies::PolicySet,
pub(crate) policy_handler: Option<Arc<dyn crate::policies::AskUserHandler>>,
pub(crate) tool_state: llm_tool::SharedState,
pub(crate) last_tool_error: std::sync::Mutex<Option<serde_json::Value>>,
}
pub(crate) fn record_last_tool_error(agent_id: u64, error: &llm_tool::ToolError) {
let value = match serde_json::to_value(error) {
Ok(value) => value,
Err(e) => {
tracing::error!(
agent_id,
error = %e,
"Failed to serialize ToolError for on_tool_error metadata — \
hook will receive no structured metadata"
);
return;
}
};
with_last_tool_error_slot(agent_id, "record", |slot| *slot = Some(value));
}
pub(crate) fn clear_last_tool_error(agent_id: u64) {
with_last_tool_error_slot(agent_id, "clear", |slot| *slot = None);
}
pub(crate) fn take_last_tool_error(agent_id: u64) -> Option<serde_json::Value> {
let mut taken = None;
with_last_tool_error_slot(agent_id, "take", |slot| taken = slot.take());
taken
}
fn with_last_tool_error_slot(
agent_id: u64,
op: &str,
f: impl FnOnce(&mut Option<serde_json::Value>),
) {
let map = match bridge_state().read() {
Ok(map) => map,
Err(e) => {
tracing::error!(
agent_id,
op,
error = %e,
"BRIDGE_STATE read lock poisoned — cannot access last_tool_error slot"
);
return;
}
};
let Some(entry) = map.get(&agent_id) else {
tracing::debug!(
agent_id,
op,
"No bridge state entry for last_tool_error access"
);
return;
};
match entry.last_tool_error.lock() {
Ok(mut slot) => f(&mut slot),
Err(e) => {
tracing::error!(
agent_id,
op,
error = %e,
"last_tool_error mutex poisoned — structured error metadata unavailable"
);
}
}
}
static BRIDGE_STATE: std::sync::OnceLock<
std::sync::RwLock<std::collections::HashMap<u64, AgentBridgeState>>,
> = std::sync::OnceLock::new();
pub(crate) fn bridge_state()
-> &'static std::sync::RwLock<std::collections::HashMap<u64, AgentBridgeState>> {
BRIDGE_STATE.get_or_init(|| std::sync::RwLock::new(std::collections::HashMap::new()))
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::*;
#[test]
fn next_agent_id_is_unique_sequentially() {
const N: usize = 1000;
let ids: Vec<u64> = std::iter::repeat_with(next_agent_id).take(N).collect();
let unique: HashSet<u64> = ids.iter().copied().collect();
assert_eq!(unique.len(), N, "sequential IDs must all be unique");
}
#[test]
fn next_agent_id_is_unique_under_concurrency() {
const THREADS: usize = 16;
const PER_THREAD: usize = 500;
let handles: Vec<_> = std::iter::repeat_with(|| {
std::thread::spawn(|| {
std::iter::repeat_with(next_agent_id)
.take(PER_THREAD)
.collect::<Vec<u64>>()
})
})
.take(THREADS)
.collect();
let mut all_ids = Vec::with_capacity(THREADS * PER_THREAD);
for handle in handles {
all_ids.extend(handle.join().expect("allocator thread must not panic"));
}
let unique: HashSet<u64> = all_ids.iter().copied().collect();
assert_eq!(
unique.len(),
all_ids.len(),
"concurrent allocations must be collision-free"
);
}
}