Skip to main content

synaptic_middleware/
human_in_the_loop.rs

1use 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/// A callback that decides whether a tool call should proceed.
11///
12/// Return `Ok(true)` to approve, `Ok(false)` to reject (returns an
13/// error message to the model), or `Err(...)` to abort.
14#[async_trait]
15pub trait ApprovalCallback: Send + Sync {
16    async fn approve(&self, tool_name: &str, arguments: &Value) -> Result<bool, SynapticError>;
17}
18
19/// Pauses tool execution to request human approval.
20///
21/// When a tool call targets one of the configured tool names (or all
22/// tools if the set is empty), the middleware invokes the
23/// `ApprovalCallback`. If the callback returns `false`, the tool call
24/// is replaced with an error message fed back to the model.
25pub struct HumanInTheLoopMiddleware {
26    callback: Arc<dyn ApprovalCallback>,
27    /// Tool names that require approval. Empty means all tools.
28    tools: HashSet<String>,
29}
30
31impl HumanInTheLoopMiddleware {
32    /// Create middleware that requires approval for all tool calls.
33    pub fn new(callback: Arc<dyn ApprovalCallback>) -> Self {
34        Self {
35            callback,
36            tools: HashSet::new(),
37        }
38    }
39
40    /// Create middleware that requires approval only for specific tools.
41    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}