use std::collections::HashMap;
use async_trait::async_trait;
use locode_protocol::{ContentBlock, ResultChunk, ToolCallRecord, ToolInputFormat, ToolSpec};
use serde_json::Value;
use crate::ctx::ToolCtx;
use crate::error::ToolError;
use crate::tool::{Tool, ToolKind, ToolOutput};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolRunResult {
pub output: Value,
pub prompt_text: String,
}
#[async_trait]
pub trait DynTool: Send + Sync {
fn kind(&self) -> ToolKind;
fn description(&self) -> &str;
fn parameters_schema(&self) -> Value;
fn input_format(&self) -> ToolInputFormat {
ToolInputFormat::JsonSchema {
parameters: self.parameters_schema(),
}
}
async fn call(&self, ctx: &ToolCtx, raw_args: Value) -> Result<ToolRunResult, ToolError>;
}
struct TypedTool<T: Tool>(T);
#[async_trait]
impl<T: Tool> DynTool for TypedTool<T> {
fn kind(&self) -> ToolKind {
self.0.kind()
}
fn description(&self) -> &str {
self.0.description()
}
fn parameters_schema(&self) -> Value {
self.0.parameters_schema()
}
fn input_format(&self) -> ToolInputFormat {
self.0.input_format()
}
async fn call(&self, ctx: &ToolCtx, raw_args: Value) -> Result<ToolRunResult, ToolError> {
let args: T::Args = serde_json::from_value(raw_args)
.map_err(|e| ToolError::Respond(format!("invalid arguments: {e}")))?;
let output = self.0.run(ctx, args).await?;
let prompt_text = output.to_prompt_text();
let output = serde_json::to_value(&output)
.map_err(|e| ToolError::Fatal(format!("failed to serialize tool output: {e}")))?;
Ok(ToolRunResult {
output,
prompt_text,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Dispatched {
pub tool_result: ContentBlock,
pub record: ToolCallRecord,
pub fatal: Option<String>,
}
#[derive(Default)]
pub struct Registry {
tools: HashMap<String, Box<dyn DynTool>>,
}
impl Registry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register<T: Tool + 'static>(&mut self, name: impl Into<String>, tool: T) {
self.register_dyn(name, Box::new(TypedTool(tool)));
}
pub fn register_dyn(&mut self, name: impl Into<String>, tool: Box<dyn DynTool>) {
let name = name.into();
assert!(
!self.tools.contains_key(&name),
"duplicate tool registration for name `{name}`"
);
self.tools.insert(name, tool);
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.tools.contains_key(name)
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.tools.keys().map(String::as_str)
}
#[must_use]
pub fn kind_of(&self, name: &str) -> Option<ToolKind> {
self.tools.get(name).map(|tool| tool.kind())
}
#[must_use]
pub fn specs(&self) -> Vec<ToolSpec> {
self.tools
.iter()
.map(|(name, tool)| ToolSpec {
name: name.clone(),
description: tool.description().to_owned(),
input: tool.input_format(),
})
.collect()
}
pub async fn dispatch(&self, name: &str, raw_args: Value, ctx: &ToolCtx) -> Dispatched {
let id = ctx.call_id.clone();
let Some(tool) = self.tools.get(name) else {
let message = format!("unknown tool: {name}");
return Dispatched {
tool_result: error_result(&id, &message),
record: record(&id, name, ToolKind::Other, raw_args, false, Value::Null),
fatal: None,
};
};
let kind = tool.kind();
match tool.call(ctx, raw_args.clone()).await {
Ok(ToolRunResult {
output,
prompt_text,
}) => Dispatched {
tool_result: ok_result(&id, &prompt_text),
record: record(&id, name, kind, raw_args, true, output),
fatal: None,
},
Err(ToolError::Respond(message)) => Dispatched {
tool_result: error_result(&id, &message),
record: record(&id, name, kind, raw_args, false, Value::Null),
fatal: None,
},
Err(ToolError::Fatal(message)) => Dispatched {
tool_result: error_result(&id, &message),
record: record(&id, name, kind, raw_args, false, Value::Null),
fatal: Some(message),
},
}
}
}
fn ok_result(id: &str, text: &str) -> ContentBlock {
let (text, _) = crate::truncate_for_model(text, crate::MODEL_OUTPUT_BUDGET);
ContentBlock::ToolResult {
tool_use_id: id.to_owned(),
content: vec![ResultChunk::Text { text }],
is_error: false,
}
}
fn error_result(id: &str, message: &str) -> ContentBlock {
let (text, _) = crate::truncate_for_model(message, crate::MODEL_OUTPUT_BUDGET);
ContentBlock::ToolResult {
tool_use_id: id.to_owned(),
content: vec![ResultChunk::Text { text }],
is_error: true,
}
}
fn record(
id: &str,
name: &str,
kind: ToolKind,
args: Value,
ok: bool,
output: Value,
) -> ToolCallRecord {
ToolCallRecord {
id: id.to_owned(),
name: name.to_owned(),
kind: kind.as_str().to_owned(),
args,
ok,
denial_reason: None,
output,
}
}