use crate::agentic::tools::shed::{
shed_definition, ShedResult, ToolDiscovery, ToolSummary, DEFAULT_SHED_LIMIT, SHED_TOOL_NAME,
};
use crate::types::{
Args, ExecutionHint, TextContent, Tool, ToolArguments, ToolDeclarations, UserModelContent,
};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::Duration;
use crate::types::ToolDefinition;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ToolError {
UnknownTool(String),
InvalidArguments { tool: String, reason: String },
Execution { tool: String, reason: String },
Timeout { tool: String },
Cancelled(String),
NotActive(String),
}
impl std::fmt::Display for ToolError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ToolError::UnknownTool(name) => write!(f, "unknown tool: {name}"),
ToolError::InvalidArguments { tool, reason } => {
write!(f, "invalid arguments for {tool}: {reason}")
}
ToolError::Execution { tool, reason } => {
write!(f, "execution error in {tool}: {reason}")
}
ToolError::Timeout { tool } => write!(f, "timeout in {tool}"),
ToolError::Cancelled(tool) => write!(f, "cancelled: {tool}"),
ToolError::NotActive(tool) => write!(
f,
"tool '{tool}' is not active yet: call `{SHED_TOOL_NAME}` with a description \
of what you need to discover it first"
),
}
}
}
#[derive(Debug, Clone)]
pub struct ToolCallResult {
pub content: crate::types::UserModelContent,
pub error_detail: Option<String>,
}
impl ToolCallResult {
#[must_use]
pub fn text(content: impl Into<String>) -> Self {
Self {
content: UserModelContent::Text(TextContent {
content: content.into(),
signature: None,
}),
error_detail: None,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ToolArgs<'a> {
tool: &'a str,
args: &'a ToolArguments,
}
impl<'a> ToolArgs<'a> {
#[must_use]
pub fn new(tool: &'a str, args: &'a ToolArguments) -> Self {
Self { tool, args }
}
#[must_use]
pub fn tool(&self) -> &'a str {
self.tool
}
#[must_use]
pub fn contains(&self, key: &str) -> bool {
self.args.contains_key(key)
}
fn invalid(&self, reason: String) -> ToolError {
ToolError::InvalidArguments {
tool: self.tool.to_string(),
reason,
}
}
fn missing(&self, key: &str) -> ToolError {
self.invalid(format!("missing required argument `{key}`"))
}
fn wrong_type(&self, key: &str, expected: &str) -> ToolError {
self.invalid(format!("`{key}` must be {expected}"))
}
fn required<T>(&self, key: &str, value: Result<Option<T>, ToolError>) -> Result<T, ToolError> {
value?.ok_or_else(|| self.missing(key))
}
pub fn str(&self, key: &str) -> Result<&'a str, ToolError> {
self.required(key, self.opt_str(key))
}
pub fn opt_str(&self, key: &str) -> Result<Option<&'a str>, ToolError> {
match self.args.get(key) {
None => Ok(None),
Some(serde_json::Value::String(s)) => Ok(Some(s.as_str())),
Some(_) => Err(self.wrong_type(key, "a string")),
}
}
pub fn i64(&self, key: &str) -> Result<i64, ToolError> {
self.required(key, self.opt_i64(key))
}
pub fn opt_i64(&self, key: &str) -> Result<Option<i64>, ToolError> {
let Some(value) = self.args.get(key) else {
return Ok(None);
};
let n = match value {
serde_json::Value::Number(n) => n.as_i64(),
serde_json::Value::String(s) => s.trim().parse().ok(),
_ => None,
};
n.map(Some)
.ok_or_else(|| self.wrong_type(key, "an integer"))
}
pub fn usize(&self, key: &str) -> Result<usize, ToolError> {
self.required(key, self.opt_usize(key))
}
pub fn opt_usize(&self, key: &str) -> Result<Option<usize>, ToolError> {
match self.opt_i64(key)? {
None => Ok(None),
Some(n) => usize::try_from(n)
.map(Some)
.map_err(|_| self.wrong_type(key, "a non-negative integer")),
}
}
pub fn f64(&self, key: &str) -> Result<f64, ToolError> {
self.required(key, self.opt_f64(key))
}
pub fn opt_f64(&self, key: &str) -> Result<Option<f64>, ToolError> {
let Some(value) = self.args.get(key) else {
return Ok(None);
};
let n = match value {
serde_json::Value::Number(n) => n.as_f64(),
serde_json::Value::String(s) => s.trim().parse().ok(),
_ => None,
};
n.map(Some).ok_or_else(|| self.wrong_type(key, "a number"))
}
pub fn bool(&self, key: &str) -> Result<bool, ToolError> {
self.required(key, self.opt_bool(key))
}
pub fn opt_bool(&self, key: &str) -> Result<Option<bool>, ToolError> {
match self.args.get(key) {
None => Ok(None),
Some(serde_json::Value::Bool(b)) => Ok(Some(*b)),
Some(serde_json::Value::String(s)) => match s.trim() {
"true" => Ok(Some(true)),
"false" => Ok(Some(false)),
_ => Err(self.wrong_type(key, "a boolean")),
},
Some(_) => Err(self.wrong_type(key, "a boolean")),
}
}
#[must_use]
pub fn opt_value(&self, key: &str) -> Option<serde_json::Value> {
self.args.get(key).cloned()
}
pub fn parse<T: serde::de::DeserializeOwned>(&self) -> Result<T, ToolError> {
serde_json::from_value(serde_json::Value::Object(self.args.clone()))
.map_err(|e| self.invalid(e.to_string()))
}
}
pub fn validate_arguments(
definition: &Tool,
tool_name: &str,
arguments: &ToolArguments,
) -> Result<(), ToolError> {
let invalid = |reason: String| ToolError::InvalidArguments {
tool: tool_name.to_string(),
reason,
};
let (schema, instance) = match definition {
Tool::SingleCommand(def) => (&def.arguments.schema, arguments.clone()),
Tool::MultiCommands(_, commands) => {
let command = match arguments.get("command") {
Some(serde_json::Value::String(command)) => command.as_str(),
_ => return Err(invalid("missing 'command' argument".into())),
};
let Some(def) = commands.iter().find(|def| def.name == command) else {
let known: Vec<&str> = commands.iter().map(|def| def.name.as_str()).collect();
return Err(invalid(format!(
"unknown command '{command}' (expected one of: {})",
known.join(", ")
)));
};
let mut rest = arguments.clone();
rest.remove("command");
(&def.arguments.schema, rest)
}
};
let instance = serde_json::Value::Object(instance);
foundation_jsonschema::validate(schema, &instance).map_err(|err| invalid(err.to_string()))
}
#[async_trait]
pub trait ToolImpl: Send + Sync {
fn definition(&self) -> Tool;
async fn execute(&self, arguments: ToolArguments) -> Result<ToolCallResult, ToolError>;
}
type FnToolFuture = core::pin::Pin<
Box<dyn core::future::Future<Output = Result<ToolCallResult, ToolError>> + Send>,
>;
type FnToolRun = dyn for<'a> Fn(ToolArgs<'a>) -> FnToolFuture + Send + Sync;
pub struct FnTool {
name: String,
description: String,
category: String,
arguments: Args,
run: Box<FnToolRun>,
}
impl FnTool {
pub fn new<F, Fut>(
name: impl Into<String>,
description: impl Into<String>,
arguments: Args,
run: F,
) -> Self
where
F: for<'a> Fn(ToolArgs<'a>) -> Fut + Send + Sync + 'static,
Fut: core::future::Future<Output = Result<ToolCallResult, ToolError>> + Send + 'static,
{
Self {
name: name.into(),
description: description.into(),
category: "custom".into(),
arguments,
run: Box::new(move |args| Box::pin(run(args))),
}
}
#[must_use]
pub fn with_category(mut self, category: impl Into<String>) -> Self {
self.category = category.into();
self
}
}
impl std::fmt::Debug for FnTool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FnTool")
.field("name", &self.name)
.field("category", &self.category)
.finish_non_exhaustive()
}
}
#[async_trait]
impl ToolImpl for FnTool {
fn definition(&self) -> Tool {
Tool::SingleCommand(ToolDefinition {
name: self.name.clone(),
category: self.category.clone(),
description: self.description.clone(),
arguments: self.arguments.clone(),
returns: None,
})
}
async fn execute(&self, arguments: ToolArguments) -> Result<ToolCallResult, ToolError> {
(self.run)(ToolArgs::new(&self.name, &arguments)).await
}
}
#[derive(Debug, Clone)]
pub struct ToolCallRequest {
pub id: String,
pub name: String,
pub arguments: ToolArguments,
pub depends_on: Vec<String>,
pub execution_hint: ExecutionHint,
}
#[derive(Debug, Clone)]
pub struct ToolCallWorkflow {
pub stages: Vec<ToolCallStage>,
}
#[derive(Debug, Clone)]
pub enum ToolCallStage {
Parallel {
calls: Vec<ToolCallRequest>,
fail_mode: FailMode,
},
Sequential {
calls: Vec<ToolCallRequest>,
fail_fast: bool,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum FailMode {
#[default]
CollectAll,
CancelOnFailure,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ToolErrorKind {
Timeout,
Network,
Execution,
InvalidArguments,
}
impl ToolError {
#[must_use]
pub fn kind(&self) -> ToolErrorKind {
match self {
ToolError::Timeout { .. } => ToolErrorKind::Timeout,
ToolError::Execution { .. } | ToolError::Cancelled(_) => ToolErrorKind::Execution,
ToolError::InvalidArguments { .. }
| ToolError::UnknownTool(_)
| ToolError::NotActive(_) => ToolErrorKind::InvalidArguments,
}
}
}
#[derive(Debug, Clone)]
pub struct ToolRetryConfig {
pub max_retries: u32,
pub initial_backoff: Duration,
pub backoff_multiplier: f64,
pub max_backoff: Duration,
pub retry_on: Vec<ToolErrorKind>,
}
impl Default for ToolRetryConfig {
fn default() -> Self {
Self {
max_retries: 3,
initial_backoff: Duration::from_secs(1),
backoff_multiplier: 2.0,
max_backoff: Duration::from_secs(30),
retry_on: vec![ToolErrorKind::Timeout, ToolErrorKind::Network],
}
}
}
impl ToolRetryConfig {
#[must_use]
pub fn should_retry(&self, err: &ToolError, attempt: u32) -> bool {
attempt < self.max_retries && self.retry_on.contains(&err.kind())
}
#[must_use]
pub fn backoff_for(&self, attempt: u32) -> Duration {
#[allow(clippy::cast_precision_loss)]
let millis = self.initial_backoff.as_millis() as f64
* self.backoff_multiplier.powi(attempt.cast_signed());
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
Duration::from_millis(millis as u64).min(self.max_backoff)
}
}
#[derive(Debug)]
pub struct WorkflowResult {
pub results: Vec<(ToolCallRequest, Result<ToolCallResult, ToolError>)>,
pub interrupted: bool,
}
pub struct ToolCallManager {
inner: Arc<ToolCallManagerInner>,
}
impl Clone for ToolCallManager {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
struct ToolCallManagerInner {
tools: std::sync::RwLock<HashMap<String, Arc<dyn ToolImpl>>>,
defs: std::sync::RwLock<HashMap<String, Tool>>,
retry_configs: std::sync::RwLock<HashMap<String, ToolRetryConfig>>,
active: std::sync::RwLock<HashSet<String>>,
discovery: std::sync::RwLock<Option<Arc<ToolDiscovery>>>,
session_id: crate::types::SessionId,
}
impl ToolCallManager {
#[must_use]
pub fn new(session_id: crate::types::SessionId) -> Self {
Self {
inner: Arc::new(ToolCallManagerInner {
tools: std::sync::RwLock::new(HashMap::new()),
defs: std::sync::RwLock::new(HashMap::new()),
retry_configs: std::sync::RwLock::new(HashMap::new()),
active: std::sync::RwLock::new(HashSet::new()),
discovery: std::sync::RwLock::new(None),
session_id,
}),
}
}
pub fn register(&self, tool: Arc<dyn ToolImpl>) {
let def = tool.definition();
let tool_name = def.name().to_string();
if tool_name == SHED_TOOL_NAME {
tracing::warn!(
"a tool named `{SHED_TOOL_NAME}` was registered; the name is reserved for the \
built-in discovery tool, so it will never be offered or run"
);
}
let discovery = self.inner.discovery.read().unwrap().clone();
if let Some(discovery) = discovery {
if let Err(err) = discovery.index_tool(&def) {
tracing::warn!(
tool = %tool_name,
"indexing the tool for `{SHED_TOOL_NAME}` failed ({err}); it stays \
findable by name and description"
);
}
}
let mut tools = self.inner.tools.write().unwrap();
let mut defs = self.inner.defs.write().unwrap();
defs.insert(tool_name.clone(), def);
tools.insert(tool_name, tool);
}
pub fn deregister(&self, name: &str) {
self.inner.tools.write().unwrap().remove(name);
self.inner.defs.write().unwrap().remove(name);
self.inner.active.write().unwrap().remove(name);
let discovery = self.inner.discovery.read().unwrap().clone();
if let Some(discovery) = discovery {
if let Err(err) = discovery.remove(name) {
tracing::warn!(
tool = %name,
"removing the tool from the discovery index failed: {err}"
);
}
}
}
#[must_use]
pub fn shed_definition(&self) -> Tool {
shed_definition()
}
#[must_use]
pub fn active_definitions(&self) -> Vec<Tool> {
let active = self.inner.active.read().unwrap();
let defs = self.inner.defs.read().unwrap();
let mut tools: Vec<Tool> = active.iter().filter_map(|n| defs.get(n).cloned()).collect();
tools.sort_by(|a, b| a.name().cmp(b.name()));
tools
}
pub fn activate(&self, names: &[String]) {
let defs = self.inner.defs.read().unwrap();
let mut active = self.inner.active.write().unwrap();
for name in names {
if name != SHED_TOOL_NAME && defs.contains_key(name) {
active.insert(name.clone());
}
}
}
#[must_use]
pub fn is_active(&self, name: &str) -> bool {
self.inner.active.read().unwrap().contains(name)
}
#[must_use]
pub fn offered_tools(&self) -> ToolDeclarations {
if self.inner.tools.read().unwrap().is_empty() {
return ToolDeclarations {
shed: None,
tools: Vec::new(),
};
}
ToolDeclarations {
shed: Some(self.shed_definition()),
tools: self.active_definitions(),
}
}
pub fn check_offered(&self, name: &str) -> Result<(), ToolError> {
if name == SHED_TOOL_NAME || self.is_active(name) {
return Ok(());
}
if self.inner.tools.read().unwrap().contains_key(name) {
Err(ToolError::NotActive(name.to_string()))
} else {
Err(ToolError::UnknownTool(name.to_string()))
}
}
pub fn search_tools(
&self,
description: &str,
limit: usize,
) -> Result<Vec<ToolSummary>, ToolError> {
let limit = limit.max(1);
let mut hits: Vec<ToolSummary> = Vec::new();
let discovery = self.inner.discovery.read().unwrap().clone();
if let Some(discovery) = discovery {
let found = discovery.search(description, limit)?;
let registered = self.inner.defs.read().unwrap();
hits.extend(
found
.into_iter()
.filter(|hit| registered.contains_key(&hit.name)),
);
}
if hits.len() < limit {
let words: Vec<String> = description
.split(|c: char| !c.is_alphanumeric() && c != '_')
.filter(|w| !w.is_empty())
.map(str::to_lowercase)
.collect();
let defs = self.inner.defs.read().unwrap();
let mut scored: Vec<(usize, ToolSummary)> = defs
.iter()
.filter(|(name, _)| name.as_str() != SHED_TOOL_NAME)
.map(|(_, def)| ToolSummary::of(def))
.filter(|s| !hits.iter().any(|h| h.name == s.name))
.filter_map(|s| {
let haystack =
format!("{} {} {}", s.name, s.description, s.category).to_lowercase();
let score = words
.iter()
.filter(|w| haystack.contains(w.as_str()))
.count();
(words.is_empty() || score > 0).then_some((score, s))
})
.collect();
scored.sort_by(|(sa, a), (sb, b)| sb.cmp(sa).then_with(|| a.name.cmp(&b.name)));
let room = limit - hits.len();
hits.extend(scored.into_iter().map(|(_, s)| s).take(room));
}
hits.truncate(limit);
Ok(hits)
}
pub fn enable_discovery(&self, discovery: Arc<ToolDiscovery>) -> Result<(), ToolError> {
let defs: Vec<Tool> = self
.inner
.defs
.read()
.unwrap()
.iter()
.filter(|(name, _)| name.as_str() != SHED_TOOL_NAME)
.map(|(_, def)| def.clone())
.collect();
for def in &defs {
discovery.index_tool(def)?;
}
*self.inner.discovery.write().unwrap() = Some(discovery);
Ok(())
}
fn run_shed(&self, arguments: &ToolArguments) -> Result<ToolCallResult, ToolError> {
validate_arguments(&shed_definition(), SHED_TOOL_NAME, arguments)?;
let args = ToolArgs::new(SHED_TOOL_NAME, arguments);
let description = args.str("description")?;
let limit = args.opt_usize("limit")?.unwrap_or(DEFAULT_SHED_LIMIT);
let hits = self.search_tools(description, limit)?;
let names: Vec<String> = hits.iter().map(|h| h.name.clone()).collect();
self.activate(&names);
let json = serde_json::to_string(&ShedResult { tools: hits }).map_err(|e| {
ToolError::Execution {
tool: SHED_TOOL_NAME.into(),
reason: format!("serialization failed: {e}"),
}
})?;
Ok(ToolCallResult::text(json))
}
#[must_use]
pub fn get(&self, name: &str) -> Option<Arc<dyn ToolImpl>> {
self.inner.tools.read().unwrap().get(name).cloned()
}
#[must_use]
pub fn get_def(&self, name: &str) -> Option<Tool> {
self.inner.defs.read().unwrap().get(name).cloned()
}
#[must_use]
pub fn names(&self) -> Vec<String> {
self.inner.tools.read().unwrap().keys().cloned().collect()
}
fn collected_tools(&self) -> Vec<Tool> {
let defs = self.inner.defs.read().unwrap();
let mut tools: Vec<Tool> = defs
.iter()
.filter(|(name, _)| name.as_str() != SHED_TOOL_NAME)
.map(|(_, def)| def.clone())
.collect();
tools.sort_by(|a, b| a.name().cmp(b.name()));
tools
}
pub async fn execute_one(
&self,
request: &ToolCallRequest,
) -> Result<ToolCallResult, ToolError> {
if request.name == SHED_TOOL_NAME {
return self.run_shed(&request.arguments);
}
let tool = self
.get(&request.name)
.ok_or_else(|| ToolError::UnknownTool(request.name.clone()))?;
validate_arguments(&tool.definition(), &request.name, &request.arguments)?;
use futures_lite::FutureExt;
let name = request.name.clone();
match std::panic::AssertUnwindSafe(tool.execute(request.arguments.clone()))
.catch_unwind()
.await
{
Ok(result) => result,
Err(_panic) => Err(ToolError::Execution {
tool: name,
reason: "tool panicked during execution".into(),
}),
}
}
#[must_use]
pub fn all_declarations(&self) -> ToolDeclarations {
let tools = self.collected_tools();
let shed = (!tools.is_empty()).then(shed_definition);
ToolDeclarations { shed, tools }
}
#[deprecated(note = "use all_declarations(); a request declares offered_tools()")]
#[must_use]
pub fn build_toolshed(&self) -> ToolDeclarations {
self.all_declarations()
}
#[must_use]
pub fn with_defaults(
session_id: crate::types::SessionId,
discovery: Arc<ToolDiscovery>,
) -> Self {
let mgr = Self::new(session_id);
*mgr.inner.discovery.write().unwrap() = Some(discovery);
mgr
}
#[must_use]
pub fn session_id(&self) -> &crate::types::SessionId {
&self.inner.session_id
}
pub fn build_workflow(&self, calls: &[ToolCallRequest]) -> Result<ToolCallWorkflow, ToolError> {
fn resolve_depth(
idx: usize,
calls: &[ToolCallRequest],
ids: &HashMap<&str, usize>,
depths: &mut [Option<u32>],
stack: &mut Vec<usize>,
) -> Result<u32, ToolError> {
if let Some(d) = depths[idx] {
return Ok(d);
}
if stack.contains(&idx) {
return Err(ToolError::InvalidArguments {
tool: calls[idx].name.clone(),
reason: format!("cyclic dependency involving '{}'", calls[idx].id),
});
}
stack.push(idx);
let mut max_dep = 0u32;
for dep_id in &calls[idx].depends_on {
let dep_idx =
ids.get(dep_id.as_str())
.ok_or_else(|| ToolError::InvalidArguments {
tool: calls[idx].name.clone(),
reason: format!("depends on unknown call '{dep_id}'"),
})?;
let dep_depth = resolve_depth(*dep_idx, calls, ids, depths, stack)?;
max_dep = max_dep.max(dep_depth + 1);
}
stack.pop();
depths[idx] = Some(max_dep);
Ok(max_dep)
}
if calls.is_empty() {
return Ok(ToolCallWorkflow { stages: vec![] });
}
let ids: HashMap<&str, usize> = calls
.iter()
.enumerate()
.map(|(i, c)| (c.id.as_str(), i))
.collect();
let mut depths: Vec<Option<u32>> = vec![None; calls.len()];
let mut stack: Vec<usize> = Vec::new();
for i in 0..calls.len() {
resolve_depth(i, calls, &ids, &mut depths, &mut stack)?;
}
let max_depth = depths.iter().filter_map(|d| *d).max().unwrap_or(0);
let mut stages = Vec::with_capacity((max_depth + 1) as usize);
for depth in 0..=max_depth {
let stage_calls: Vec<ToolCallRequest> = calls
.iter()
.enumerate()
.filter(|(i, _)| depths[*i] == Some(depth))
.map(|(_, c)| c.clone())
.collect();
if stage_calls.is_empty() {
continue;
}
let any_sequential = stage_calls
.iter()
.any(|c| c.execution_hint == ExecutionHint::Sequential);
if any_sequential || stage_calls.len() == 1 {
stages.push(ToolCallStage::Sequential {
calls: stage_calls,
fail_fast: true,
});
} else {
stages.push(ToolCallStage::Parallel {
calls: stage_calls,
fail_mode: FailMode::CollectAll,
});
}
}
Ok(ToolCallWorkflow { stages })
}
pub async fn execute_with_retry(
&self,
request: &ToolCallRequest,
config: &ToolRetryConfig,
) -> Result<ToolCallResult, ToolError> {
let mut attempt = 0u32;
loop {
match self.execute_one(request).await {
Ok(result) => return Ok(result),
Err(e) if config.should_retry(&e, attempt) => {
attempt += 1;
}
Err(e) => return Err(e),
}
}
}
pub fn set_retry_config(&self, tool_name: &str, config: ToolRetryConfig) {
self.inner
.retry_configs
.write()
.unwrap()
.insert(tool_name.to_string(), config);
}
#[must_use]
pub fn retry_config(&self, tool_name: &str) -> ToolRetryConfig {
self.inner
.retry_configs
.read()
.unwrap()
.get(tool_name)
.cloned()
.unwrap_or_default()
}
}