use std::collections::HashMap;
use futures_util::future::BoxFuture;
use crate::protocol::{
CallHookRequest, CallHookResponse, EmptyResult, PolicyDecisionRequest, PolicyDecisionResponse,
PolicyEvaluationOutcome, StepUpdate, ToolCall, ToolResponse, UserQuestionsRequest,
UserQuestionsResponse,
};
type ToolFn = Box<dyn Fn(ToolCall) -> BoxFuture<'static, ToolResponse> + Send + Sync>;
type HookFn = Box<dyn Fn(CallHookRequest) -> BoxFuture<'static, CallHookResponse> + Send + Sync>;
type PolicyFn =
Box<dyn Fn(PolicyDecisionRequest) -> BoxFuture<'static, PolicyDecisionResponse> + Send + Sync>;
type QuestionFn =
Box<dyn Fn(UserQuestionsRequest) -> BoxFuture<'static, UserQuestionsResponse> + Send + Sync>;
type ConfirmFn = Box<dyn Fn(StepUpdate) -> BoxFuture<'static, bool> + Send + Sync>;
#[derive(Default)]
pub struct Handlers {
tools: HashMap<String, ToolFn>,
hook: Option<HookFn>,
policy: Option<PolicyFn>,
questions: Option<QuestionFn>,
confirm: Option<ConfirmFn>,
}
impl std::fmt::Debug for Handlers {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Handlers")
.field("tools", &self.tools.keys().collect::<Vec<_>>())
.field("hook", &self.hook.is_some())
.field("policy", &self.policy.is_some())
.field("questions", &self.questions.is_some())
.field("confirm", &self.confirm.is_some())
.finish()
}
}
impl Handlers {
pub fn new() -> Self {
Self::default()
}
pub fn tool<F, Fut>(mut self, name: impl Into<String>, f: F) -> Self
where
F: Fn(ToolCall) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = ToolResponse> + Send + 'static,
{
self.tools
.insert(name.into(), Box::new(move |call| Box::pin(f(call))));
self
}
pub fn on_hook<F, Fut>(mut self, f: F) -> Self
where
F: Fn(CallHookRequest) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = CallHookResponse> + Send + 'static,
{
self.hook = Some(Box::new(move |request| Box::pin(f(request))));
self
}
pub fn on_policy<F, Fut>(mut self, f: F) -> Self
where
F: Fn(PolicyDecisionRequest) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = PolicyDecisionResponse> + Send + 'static,
{
self.policy = Some(Box::new(move |request| Box::pin(f(request))));
self
}
pub fn on_questions<F, Fut>(mut self, f: F) -> Self
where
F: Fn(UserQuestionsRequest) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = UserQuestionsResponse> + Send + 'static,
{
self.questions = Some(Box::new(move |request| Box::pin(f(request))));
self
}
pub fn on_tool_confirmation<F, Fut>(mut self, f: F) -> Self
where
F: Fn(StepUpdate) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = bool> + Send + 'static,
{
self.confirm = Some(Box::new(move |step| Box::pin(f(step))));
self
}
pub fn handles_tool(&self, name: &str) -> bool {
self.tools.contains_key(name)
}
pub(crate) async fn call_tool(&self, call: ToolCall) -> ToolResponse {
let id = call.id.clone().unwrap_or_default();
let name = call.name.clone().unwrap_or_default();
match self.tools.get(&name) {
Some(f) => f(call).await,
None => ToolResponse::error(id, format!("no handler registered for tool `{name}`")),
}
}
pub(crate) async fn call_hook(&self, request: CallHookRequest) -> CallHookResponse {
let request_id = request.request_id.clone();
match &self.hook {
Some(f) => f(request).await,
None => CallHookResponse {
request_id,
empty_result: Some(EmptyResult {}),
..Default::default()
},
}
}
pub(crate) async fn call_policy(
&self,
request: PolicyDecisionRequest,
) -> PolicyDecisionResponse {
let request_id = request.request_id.clone();
match &self.policy {
Some(f) => f(request).await,
None => PolicyDecisionResponse {
request_id,
outcome: Some(PolicyEvaluationOutcome::NoMatch),
..Default::default()
},
}
}
pub(crate) async fn call_questions(
&self,
request: UserQuestionsRequest,
) -> UserQuestionsResponse {
match &self.questions {
Some(f) => f(request).await,
None => UserQuestionsResponse {
cancelled: Some(true),
..Default::default()
},
}
}
pub(crate) async fn call_confirm(&self, step: StepUpdate) -> bool {
match &self.confirm {
Some(f) => f(step).await,
None => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn an_unregistered_tool_fails_that_call_only() {
let handlers = Handlers::new();
let response = handlers
.call_tool(ToolCall {
id: Some("call-1".into()),
name: Some("missing".into()),
..Default::default()
})
.await;
assert_eq!(response.id.as_deref(), Some("call-1"));
assert!(response
.error_message
.unwrap()
.contains("no handler registered"));
}
#[tokio::test]
async fn a_registered_tool_is_dispatched_by_name() {
let handlers = Handlers::new().tool("echo", |call| async move {
ToolResponse::ok(call.id.unwrap_or_default(), "42")
});
let response = handlers
.call_tool(ToolCall {
id: Some("c".into()),
name: Some("echo".into()),
..Default::default()
})
.await;
assert_eq!(response.response_json.as_deref(), Some("42"));
}
#[tokio::test]
async fn defaults_keep_the_turn_moving() {
let handlers = Handlers::new();
let hook = handlers
.call_hook(CallHookRequest {
request_id: Some("r".into()),
..Default::default()
})
.await;
assert_eq!(hook.request_id.as_deref(), Some("r"));
assert!(hook.empty_result.is_some());
let policy = handlers
.call_policy(PolicyDecisionRequest {
request_id: Some("p".into()),
..Default::default()
})
.await;
assert_eq!(policy.outcome, Some(PolicyEvaluationOutcome::NoMatch));
let questions = handlers
.call_questions(UserQuestionsRequest::default())
.await;
assert_eq!(questions.cancelled, Some(true));
}
}