use std::collections::HashSet;
use std::sync::Arc;
use crate::types::{AgentTool, ToolContext, ToolError, ToolResult};
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
pub trait ToolSource: crate::rt::MaybeSend + crate::rt::MaybeSync {
async fn tools(&self) -> Vec<Arc<dyn AgentTool>>;
fn generation(&self) -> u64 {
0
}
}
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl<T: ToolSource + ?Sized> ToolSource for Arc<T> {
async fn tools(&self) -> Vec<Arc<dyn AgentTool>> {
(**self).tools().await
}
fn generation(&self) -> u64 {
(**self).generation()
}
}
pub(crate) struct ArcTool(pub(crate) Arc<dyn AgentTool>);
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl AgentTool for ArcTool {
fn name(&self) -> &str {
self.0.name()
}
fn label(&self) -> &str {
self.0.label()
}
fn description(&self) -> &str {
self.0.description()
}
fn parameters_schema(&self) -> serde_json::Value {
self.0.parameters_schema()
}
async fn execute(
&self,
params: serde_json::Value,
ctx: ToolContext,
) -> Result<ToolResult, ToolError> {
self.0.execute(params, ctx).await
}
}
#[derive(Clone)]
struct SourceState {
source: Arc<dyn ToolSource>,
generation: u64,
tools: Vec<Arc<dyn AgentTool>>,
generation_panicked: bool,
}
#[derive(Default)]
pub(crate) struct Sourced {
states: Vec<SourceState>,
static_len: usize,
}
impl Sourced {
fn all(&self) -> Vec<Arc<dyn AgentTool>> {
self.states
.iter()
.flat_map(|s| s.tools.iter().cloned())
.collect()
}
pub(crate) fn merge_into(&mut self, tools: &mut Vec<Box<dyn AgentTool>>) {
self.static_len = tools.len();
merge(tools, self.all());
}
async fn refresh(&mut self, cancel: &tokio_util::sync::CancellationToken) -> bool {
let mut changed = false;
for (index, state) in self.states.iter_mut().enumerate() {
let Some(now) = generation_of(&state.source, index, &mut state.generation_panicked)
else {
continue;
};
if now == state.generation {
continue;
}
let asked = tokio::select! {
tools = ask(&state.source, index) => tools,
_ = cancel.cancelled() => return changed,
};
tracing::info!(
source = index,
from = state.generation,
to = now,
tools = asked.len(),
"tool source changed; its tools are offered again from this request"
);
state.generation = now;
state.tools = asked;
changed = true;
}
changed
}
}
fn generation_of(source: &Arc<dyn ToolSource>, index: usize, panicked: &mut bool) -> Option<u64> {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| source.generation())) {
Ok(generation) => Some(generation),
Err(payload) => {
if !*panicked {
*panicked = true;
tracing::warn!(
source = index,
panic = %panic_message(payload.as_ref()),
"tool source's generation() panicked; counted as unchanged \
(logged once per run)"
);
}
None
}
}
}
async fn ask(source: &Arc<dyn ToolSource>, index: usize) -> Vec<Arc<dyn AgentTool>> {
use futures::FutureExt;
let guarded = std::panic::AssertUnwindSafe(async move { (**source).tools().await });
match guarded.catch_unwind().await {
Ok(tools) => tools,
Err(payload) => {
tracing::warn!(
source = index,
panic = %panic_message(payload.as_ref()),
"tool source panicked; it contributes no tools"
);
Vec::new()
}
}
}
pub(crate) async fn collect(sources: &[Arc<dyn ToolSource>]) -> Sourced {
let mut sourced = Sourced::default();
for (index, source) in sources.iter().enumerate() {
let mut generation_panicked = false;
let generation = generation_of(source, index, &mut generation_panicked).unwrap_or(0);
let tools = ask(source, index).await;
sourced.states.push(SourceState {
source: source.clone(),
generation,
tools,
generation_panicked,
});
}
sourced
}
pub(crate) struct RunTools {
sourced: Sourced,
extension_tools: Vec<Arc<dyn AgentTool>>,
base_len: usize,
}
impl RunTools {
pub(crate) fn start(
tools: &mut Vec<Box<dyn AgentTool>>,
sourced: Sourced,
extension_tools: Vec<Arc<dyn AgentTool>>,
) -> Self {
debug_assert!(sourced.static_len <= tools.len());
let base_len = tools.len();
if !extension_tools.is_empty() {
merge(tools, extension_tools.clone());
}
Self {
sourced,
extension_tools,
base_len,
}
}
pub(crate) fn base_len(&self) -> usize {
self.base_len
}
pub(crate) async fn refresh(
&mut self,
tools: &mut Vec<Box<dyn AgentTool>>,
cancel: &tokio_util::sync::CancellationToken,
) {
if self.sourced.states.is_empty() || !self.sourced.refresh(cancel).await {
return;
}
tools.truncate(self.sourced.static_len);
merge(tools, self.sourced.all());
self.base_len = tools.len();
if !self.extension_tools.is_empty() {
merge(tools, self.extension_tools.clone());
}
}
}
pub(crate) fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
if let Some(s) = payload.downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"<non-string panic payload>".to_string()
}
}
pub(crate) fn merge(tools: &mut Vec<Box<dyn AgentTool>>, sourced: Vec<Arc<dyn AgentTool>>) {
let static_names: HashSet<String> = tools.iter().map(|t| t.name().to_string()).collect();
let mut taken = static_names.clone();
let mut kept: Vec<Arc<dyn AgentTool>> = Vec::new();
for tool in sourced {
let name = tool.name().to_string();
if taken.contains(&name) {
if static_names.contains(&name) {
tracing::warn!(
tool = %name,
"a tool source offered a tool whose name a static tool already uses; \
the static tool is kept"
);
} else {
tracing::warn!(
tool = %name,
"two tool sources offered tools with the same name; \
the first one is kept"
);
}
continue;
}
taken.insert(name);
kept.push(tool);
}
kept.sort_by(|a, b| a.name().cmp(b.name()));
tools.extend(
kept.into_iter()
.map(|t| Box::new(ArcTool(t)) as Box<dyn AgentTool>),
);
}