synaptic_middleware/
human_in_the_loop.rs1use std::collections::HashSet;
2use std::sync::Arc;
3
4use async_trait::async_trait;
5use serde_json::Value;
6use synaptic_core::SynapticError;
7
8use crate::{AgentMiddleware, ToolCallRequest, ToolCaller};
9
10#[async_trait]
15pub trait ApprovalCallback: Send + Sync {
16 async fn approve(&self, tool_name: &str, arguments: &Value) -> Result<bool, SynapticError>;
17}
18
19pub struct HumanInTheLoopMiddleware {
26 callback: Arc<dyn ApprovalCallback>,
27 tools: HashSet<String>,
29}
30
31impl HumanInTheLoopMiddleware {
32 pub fn new(callback: Arc<dyn ApprovalCallback>) -> Self {
34 Self {
35 callback,
36 tools: HashSet::new(),
37 }
38 }
39
40 pub fn for_tools(callback: Arc<dyn ApprovalCallback>, tools: Vec<String>) -> Self {
42 Self {
43 callback,
44 tools: tools.into_iter().collect(),
45 }
46 }
47}
48
49#[async_trait]
50impl AgentMiddleware for HumanInTheLoopMiddleware {
51 async fn wrap_tool_call(
52 &self,
53 request: ToolCallRequest,
54 next: &dyn ToolCaller,
55 ) -> Result<Value, SynapticError> {
56 let needs_approval = self.tools.is_empty() || self.tools.contains(&request.call.name);
57
58 if needs_approval {
59 let approved = self
60 .callback
61 .approve(&request.call.name, &request.call.arguments)
62 .await?;
63
64 if !approved {
65 return Ok(Value::String(format!(
66 "Tool call '{}' was rejected by human review.",
67 request.call.name
68 )));
69 }
70 }
71
72 next.call(request).await
73 }
74}