use crate::types::{ArgType, Args, ExecutionHint, Tool, ToolShed};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
pub use crate::types::base_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),
}
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}"),
}
}
}
#[derive(Debug, Clone)]
pub struct ToolCallResult {
pub content: crate::types::UserModelContent,
pub error_detail: Option<String>,
}
#[async_trait]
pub trait ToolImpl: Send + Sync {
fn definition(&self) -> Tool;
async fn execute(
&self,
arguments: HashMap<String, ArgType>,
) -> Result<ToolCallResult, ToolError>;
}
#[derive(Debug, Clone)]
pub struct ToolCallRequest {
pub id: String,
pub name: String,
pub arguments: HashMap<String, ArgType>,
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(_) => {
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>>,
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()),
session_id,
}),
}
}
pub fn register(&self, tool: Arc<dyn ToolImpl>) {
let def = tool.definition();
let mut tools = self.inner.tools.write().unwrap();
let mut defs = self.inner.defs.write().unwrap();
let tool_name = def.name().to_string();
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);
}
#[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) -> (Option<Tool>, Vec<Tool>) {
let defs = self.inner.defs.read().unwrap();
let mut all: Vec<Tool> = defs.values().cloned().collect();
all.sort_by(|a, b| a.name().cmp(b.name()));
let mut shed = None;
let mut tools = Vec::new();
for tool in all {
if tool.name() == "shed" {
shed = Some(tool);
} else {
tools.push(tool);
}
}
(shed, tools)
}
pub async fn execute_one(
&self,
request: &ToolCallRequest,
) -> Result<ToolCallResult, ToolError> {
let tool = self
.get(&request.name)
.ok_or_else(|| ToolError::UnknownTool(request.name.clone()))?;
let _def = tool.definition();
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 build_toolshed(&self) -> ToolShed {
let (shed_tool, tools) = self.collected_tools();
let shed = if tools.is_empty() {
None
} else {
shed_tool.or_else(|| {
Some(Tool::SingleCommand(ToolDefinition {
name: "shed".into(),
category: "shed".into(),
description:
"Search the tool registry for available tools by category or free-text query."
.into(),
arguments: Args::empty(),
returns: None,
}))
})
};
ToolShed { shed, tools }
}
#[must_use]
pub fn with_defaults(
session_id: crate::types::SessionId,
discovery: Arc<crate::agentic::tools::shed::ToolDiscovery>,
) -> Self {
let mgr = Self::new(session_id);
mgr.register(Arc::new(crate::agentic::tools::shed::ShedTool::new(
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()
}
}