use std::fmt;
use std::sync::Arc;
use crate::{CancelToken, ToolPermissionDecision, ToolUseRequest};
type ToolPermissionCallback<Context> =
dyn Fn(&ToolUseRequest, &Context, &CancelToken) -> ToolPermissionDecision + Send + Sync;
type ToolExecutionCallback<Context, Output> = dyn Fn(&ToolUseRequest, &Context, &CancelToken) -> Output + Send + Sync;
#[derive(Clone)]
pub struct ToolPermissionHook<Context>(Arc<ToolPermissionCallback<Context>>);
impl<Context> ToolPermissionHook<Context> {
pub fn new(
callback: impl Fn(&ToolUseRequest, &Context, &CancelToken) -> ToolPermissionDecision + Send + Sync + 'static,
) -> Self {
Self(Arc::new(callback))
}
pub fn decide(&self, request: &ToolUseRequest, context: &Context, cancel: &CancelToken) -> ToolPermissionDecision {
(self.0)(request, context, cancel)
}
}
impl<Context> fmt::Debug for ToolPermissionHook<Context> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("ToolPermissionHook(..)")
}
}
#[derive(Clone)]
pub struct ToolExecutionHook<Context, Output>(Arc<ToolExecutionCallback<Context, Output>>);
impl<Context, Output> ToolExecutionHook<Context, Output> {
pub fn new(callback: impl Fn(&ToolUseRequest, &Context, &CancelToken) -> Output + Send + Sync + 'static) -> Self {
Self(Arc::new(callback))
}
pub fn execute(&self, request: &ToolUseRequest, context: &Context, cancel: &CancelToken) -> Output {
(self.0)(request, context, cancel)
}
}
impl<Context, Output> fmt::Debug for ToolExecutionHook<Context, Output> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("ToolExecutionHook(..)")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn permission_hook_receives_only_typed_application_context() {
let hook = ToolPermissionHook::new(|request, prefix: &String, _| {
if request.name.starts_with(prefix) {
ToolPermissionDecision::Allow
} else {
ToolPermissionDecision::Reject
}
});
let request = ToolUseRequest::new("read_file", "{}", "call_1");
assert_eq!(
hook.decide(&request, &"read".to_string(), &CancelToken::new()),
ToolPermissionDecision::Allow
);
}
#[test]
fn execution_hook_preserves_application_defined_output() {
let hook = ToolExecutionHook::new(|request, suffix: &String, _| format!("{}-{suffix}", request.name));
let request = ToolUseRequest::new("search", "{}", "call_1");
assert_eq!(
hook.execute(&request, &"local".to_string(), &CancelToken::new()),
"search-local"
);
}
}