use std::sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
};
use crate::{
config::AgentConfig,
content::Content,
error::Error,
streaming::{ChatResponseHandle, ChatResponseSharedState},
types::{ConversationMessage, UsageMetadata},
};
#[cfg(test)]
pub(crate) mod mock;
pub type AgentId = u64;
pub trait Runtime: Send + Sync {
fn create_agent(
&self,
agent_id: u64,
config: AgentConfig,
) -> impl std::future::Future<
Output = Result<(AgentId, Vec<crate::tools::AvailableTool>), Error>,
> + Send;
fn chat(
&self,
agent_id: AgentId,
content: &Content,
) -> impl std::future::Future<Output = Result<ChatResponseHandle, Error>> + Send;
fn shutdown_agent(
&self,
agent_id: AgentId,
) -> impl std::future::Future<Output = Result<(), Error>> + Send;
fn cancel(
&self,
agent_id: AgentId,
) -> impl std::future::Future<Output = Result<(), Error>> + Send;
fn wait_for_idle(
&self,
agent_id: AgentId,
) -> impl std::future::Future<Output = Result<(), Error>> + Send;
fn send(
&self,
agent_id: AgentId,
content: &Content,
) -> impl std::future::Future<Output = Result<(), Error>> + Send;
fn signal_idle(
&self,
agent_id: AgentId,
) -> impl std::future::Future<Output = Result<(), Error>> + Send;
fn wait_for_wakeup(
&self,
agent_id: AgentId,
timeout: std::time::Duration,
) -> impl std::future::Future<Output = Result<bool, Error>> + Send;
fn history(
&self,
agent_id: AgentId,
) -> impl std::future::Future<Output = Result<Vec<ConversationMessage>, Error>> + Send;
fn turn_count(
&self,
agent_id: AgentId,
) -> impl std::future::Future<Output = Result<u32, Error>> + Send;
fn total_usage(
&self,
agent_id: AgentId,
) -> impl std::future::Future<Output = Result<UsageMetadata, Error>> + Send;
fn last_turn_usage(
&self,
agent_id: AgentId,
) -> impl std::future::Future<Output = Result<UsageMetadata, Error>> + Send;
fn clear_history(
&self,
agent_id: AgentId,
) -> impl std::future::Future<Output = Result<(), Error>> + Send;
fn last_response(
&self,
_agent_id: AgentId,
) -> impl std::future::Future<Output = Result<Option<String>, Error>> + Send {
async move { Ok(None) }
}
fn compaction_indices(
&self,
_agent_id: AgentId,
) -> impl std::future::Future<Output = Result<Vec<u32>, Error>> + Send {
async move { Ok(Vec::new()) }
}
fn delete(
&self,
_agent_id: AgentId,
) -> impl std::future::Future<Output = Result<(), Error>> + Send {
async move { Ok(()) }
}
fn disconnect(
&self,
_agent_id: AgentId,
) -> impl std::future::Future<Output = Result<(), Error>> + Send {
async move { Ok(()) }
}
fn is_idle(
&self,
_agent_id: AgentId,
) -> impl std::future::Future<Output = Result<bool, Error>> + Send {
async move { Ok(true) }
}
fn try_shutdown_agent(&self, _agent_id: AgentId) {}
}
pub struct AgentHandle<R: Runtime + 'static> {
id: AgentId,
runtime: Arc<R>,
config: AgentConfig,
_registry: Option<Arc<crate::tools::ToolRegistry>>,
policy_handler: Option<Arc<dyn crate::policies::AskUserHandler>>,
conversation_id: Arc<Mutex<Option<String>>>,
is_started: AtomicBool,
is_shutdown: AtomicBool,
available_tools: Vec<crate::tools::AvailableTool>,
last_shared_state: Mutex<Option<Arc<Mutex<ChatResponseSharedState>>>>,
}
struct InitializingHookGuard(u64);
impl Drop for InitializingHookGuard {
fn drop(&mut self) {
match crate::runtime::initializing_hook_runners().write() {
Ok(mut map) => {
map.remove(&self.0);
}
Err(e) => {
tracing::error!(
agent_id = self.0,
error = %e,
"initializing hook runners lock poisoned during cleanup — \
stale hook runner may persist"
);
}
}
}
}
impl<R: Runtime> AgentHandle<R> {
pub async fn new(
runtime: Arc<R>,
config: AgentConfig,
registry: Option<Arc<crate::tools::ToolRegistry>>,
hook_runner: Option<Arc<crate::hooks::Hooks>>,
policy_handler: Option<Arc<dyn crate::policies::AskUserHandler>>,
) -> Result<Self, Error> {
let agent_id_u64 = crate::runtime::next_agent_id();
let effective_hook_runner =
hook_runner.unwrap_or_else(|| Arc::new(crate::hooks::Hooks::new()));
match crate::runtime::initializing_hook_runners().write() {
Ok(mut map) => {
map.insert(agent_id_u64, Arc::clone(&effective_hook_runner));
}
Err(e) => {
return Err(Error::BackendError {
message: format!(
"initializing hook runners lock poisoned — hooks cannot be installed: {e}"
),
});
}
}
let _init_guard = InitializingHookGuard(agent_id_u64);
let conversation_id = Self::setup_bridge_state(
agent_id_u64,
&config,
registry.as_ref(),
effective_hook_runner,
policy_handler.as_ref(),
)?;
let create_result = runtime.create_agent(agent_id_u64, config.clone()).await;
let (agent_id, available_tools) = match create_result {
Ok(res) => res,
Err(e) => {
match crate::runtime::bridge_state().write() {
Ok(mut map) => {
map.remove(&agent_id_u64);
}
Err(lock_err) => {
tracing::warn!(error = %lock_err, "Poisoned bridge_state lock during agent creation error cleanup");
}
}
return Err(e);
}
};
debug_assert_eq!(
agent_id, agent_id_u64,
"runtime must echo the caller-provided agent ID"
);
tracing::info!(agent_id, "Agent created successfully");
Ok(Self {
id: agent_id,
runtime,
config,
_registry: registry,
policy_handler,
conversation_id,
is_started: AtomicBool::new(true),
is_shutdown: AtomicBool::new(false),
available_tools,
last_shared_state: Mutex::new(None),
})
}
fn setup_bridge_state(
id: AgentId,
config: &AgentConfig,
registry: Option<&Arc<crate::tools::ToolRegistry>>,
effective_hook_runner: Arc<crate::hooks::Hooks>,
policy_handler: Option<&Arc<dyn crate::policies::AskUserHandler>>,
) -> Result<Arc<Mutex<Option<String>>>, Error> {
#[cfg(feature = "python")]
let policies_set = crate::policies::PolicySet::validated_from(config.policies.clone())?;
let conversation_id = Arc::new(Mutex::new(config.conversation_id.clone()));
let bridge_entry = crate::runtime::AgentBridgeState {
registry: registry.map(Arc::clone),
hook_runner: Some(effective_hook_runner),
#[cfg(feature = "python")]
policies: policies_set,
policy_handler: policy_handler.map(Arc::clone),
tool_state: llm_tool::SharedState::new(),
conversation_id: Arc::clone(&conversation_id),
last_tool_error: std::sync::Mutex::new(None),
};
let bridge_insert_failed = match crate::runtime::bridge_state().write() {
Ok(mut map) => {
map.insert(id, bridge_entry);
false
}
Err(e) => {
tracing::error!(
agent_id = id,
error = %e,
"Failed to acquire write lock on BRIDGE_STATE — agent would be unusable"
);
true
}
};
if bridge_insert_failed {
return Err(Error::BackendError {
message: "BRIDGE_STATE RwLock poisoned during agent creation".to_string(),
});
}
Ok(conversation_id)
}
pub async fn chat(&self, content: impl Into<Content>) -> Result<ChatResponseHandle, Error> {
if !self.is_started() {
return Err(Error::AgentNotStarted);
}
self.chat_once(&content.into()).await
}
async fn chat_once(&self, content: &Content) -> Result<ChatResponseHandle, Error> {
let handle = self.runtime.chat(self.id, content).await?;
match self.last_shared_state.lock() {
Ok(mut guard) => {
*guard = Some(Arc::clone(&handle.shared_state));
}
Err(e) => {
tracing::error!(
agent_id = self.id,
error = %e,
"last_shared_state mutex poisoned — streaming metadata may be stale"
);
}
}
Ok(handle)
}
pub async fn chat_text(&self, message: impl Into<Content>) -> Result<String, Error> {
self.chat_text_once(&message.into()).await
}
async fn chat_text_once(&self, content: &Content) -> Result<String, Error> {
let response = self.chat_once(content).await?;
let text = response.text().await?;
Ok(text.into_string())
}
#[must_use]
pub fn conversation_id(&self) -> Option<String> {
let guard = self.conversation_id.lock().unwrap_or_else(|e| {
tracing::warn!(agent_id = self.id, error = %e, "conversation_id mutex was poisoned, recovering");
e.into_inner()
});
guard.clone()
}
#[must_use]
pub fn is_started(&self) -> bool {
self.is_started.load(Ordering::SeqCst) && !self.is_shutdown.load(Ordering::SeqCst)
}
#[must_use]
pub const fn id(&self) -> AgentId {
self.id
}
#[must_use]
pub const fn config(&self) -> &AgentConfig {
&self.config
}
#[must_use]
pub fn available_tools(&self) -> &[crate::tools::AvailableTool] {
&self.available_tools
}
#[must_use]
pub fn available_tool_names(&self) -> Vec<&str> {
self.available_tools
.iter()
.map(|t| t.name.as_str())
.collect()
}
pub async fn cancel(&self) -> Result<(), Error> {
self.runtime.cancel(self.id).await
}
pub async fn wait_for_idle(&self) -> Result<(), Error> {
self.runtime.wait_for_idle(self.id).await
}
pub async fn history(&self) -> Result<Vec<ConversationMessage>, Error> {
self.runtime.history(self.id).await
}
pub async fn turn_count(&self) -> Result<u32, Error> {
self.runtime.turn_count(self.id).await
}
pub async fn total_usage(&self) -> Result<UsageMetadata, Error> {
self.runtime.total_usage(self.id).await
}
pub async fn last_turn_usage(&self) -> Result<UsageMetadata, Error> {
self.runtime.last_turn_usage(self.id).await
}
pub async fn clear_history(&self) -> Result<(), Error> {
self.runtime.clear_history(self.id).await
}
pub async fn last_response(&self) -> Result<Option<String>, Error> {
self.runtime.last_response(self.id).await
}
pub async fn compaction_indices(&self) -> Result<Vec<u32>, Error> {
self.runtime.compaction_indices(self.id).await
}
pub async fn delete(&self) -> Result<(), Error> {
let result = self.runtime.delete(self.id).await;
self.is_shutdown.store(true, Ordering::SeqCst);
result
}
pub async fn disconnect(&self) -> Result<(), Error> {
let result = self.runtime.disconnect(self.id).await;
self.is_shutdown.store(true, Ordering::SeqCst);
result
}
pub async fn is_idle(&self) -> Result<bool, Error> {
self.runtime.is_idle(self.id).await
}
#[must_use]
pub fn get_last_structured_output(&self) -> Option<serde_json::Value> {
let guard = self.last_shared_state.lock().unwrap_or_else(|e| {
tracing::warn!(
agent_id = self.id,
error = %e,
"last_shared_state mutex was poisoned, recovering"
);
e.into_inner()
});
let state_arc = guard.as_ref()?;
let state = state_arc.lock().unwrap_or_else(|e| {
tracing::warn!(
agent_id = self.id,
error = %e,
"ChatResponseSharedState mutex was poisoned, recovering"
);
e.into_inner()
});
state.structured_output.clone()
}
pub fn get_last_structured_output_as<T: serde::de::DeserializeOwned>(
&self,
) -> Option<Result<T, serde_json::Error>> {
self.get_last_structured_output()
.map(serde_json::from_value)
}
#[must_use]
pub fn get_last_usage(&self) -> Option<UsageMetadata> {
let guard = self.last_shared_state.lock().unwrap_or_else(|e| {
tracing::warn!(
agent_id = self.id,
error = %e,
"last_shared_state mutex was poisoned, recovering"
);
e.into_inner()
});
let state_arc = guard.as_ref()?;
let state = state_arc.lock().unwrap_or_else(|e| {
tracing::warn!(
agent_id = self.id,
error = %e,
"ChatResponseSharedState mutex was poisoned, recovering"
);
e.into_inner()
});
state.usage.clone()
}
pub async fn send(&self, content: impl Into<Content>) -> Result<(), Error> {
if !self.is_started() {
return Err(Error::AgentNotStarted);
}
self.runtime.send(self.id, &content.into()).await
}
pub async fn signal_idle(&self) -> Result<(), Error> {
self.runtime.signal_idle(self.id).await
}
pub async fn wait_for_wakeup(&self, timeout: std::time::Duration) -> Result<bool, Error> {
self.runtime.wait_for_wakeup(self.id, timeout).await
}
pub async fn shutdown(&self) -> Result<(), Error> {
if self.is_shutdown.load(Ordering::SeqCst) {
tracing::debug!(agent_id = self.id, "Agent already shut down");
return Ok(());
}
tracing::info!(agent_id = self.id, "Shutting down agent");
let result = self.runtime.shutdown_agent(self.id).await;
self.is_shutdown.store(true, Ordering::SeqCst);
match crate::runtime::bridge_state().write() {
Ok(mut map) => {
map.remove(&self.id);
}
Err(e) => {
tracing::error!(
agent_id = self.id,
error = %e,
"BRIDGE_STATE RwLock poisoned during shutdown cleanup — \
bridge state entry may leak"
);
}
}
match result {
Ok(()) => {
tracing::info!(agent_id = self.id, "Agent shut down successfully");
}
Err(ref e) => {
tracing::error!(agent_id = self.id, error = ?e, "Agent shutdown failed");
}
}
result
}
pub async fn spawn_subagent(
&self,
mut config: AgentConfig,
registry: impl Into<Option<crate::tools::ToolRegistry>>,
) -> Result<Self, Error> {
let opt_registry = registry.into();
if let Some(disp) = &opt_registry
&& config.tools.is_empty()
{
config.tools = disp.definitions();
}
let arc_registry = opt_registry.map(Arc::new);
Self::new(
Arc::clone(&self.runtime),
config,
arc_registry,
None,
self.policy_handler.clone(),
)
.await
}
}
impl<R: Runtime> Drop for AgentHandle<R> {
fn drop(&mut self) {
if self.is_started.load(Ordering::SeqCst) && !self.is_shutdown.load(Ordering::SeqCst) {
tracing::debug!(
agent_id = self.id,
"AgentHandle dropped without explicit shutdown() — \
sending best-effort shutdown signal"
);
self.runtime.try_shutdown_agent(self.id);
} else if self.is_shutdown.load(Ordering::SeqCst) {
} else {
match crate::runtime::bridge_state().write() {
Ok(mut map) => {
map.remove(&self.id);
}
Err(e) => {
tracing::warn!(
agent_id = self.id,
error = %e,
"BRIDGE_STATE RwLock poisoned during Drop — \
bridge state entry for this agent may leak"
);
}
}
}
}
}