Skip to main content

symbi_runtime/reasoning/
knowledge_executor.rs

1//! Knowledge-aware action executor wrapper.
2//!
3//! `KnowledgeAwareExecutor` intercepts `recall_knowledge` and `store_knowledge`
4//! tool calls, handling them locally via the `KnowledgeBridge`, and delegates
5//! all other tool calls to an inner `ActionExecutor`.
6
7use std::sync::Arc;
8
9use async_trait::async_trait;
10
11use crate::reasoning::circuit_breaker::CircuitBreakerRegistry;
12use crate::reasoning::executor::ActionExecutor;
13use crate::reasoning::knowledge_bridge::KnowledgeBridge;
14use crate::reasoning::loop_types::{LoopConfig, Observation, ProposedAction};
15use crate::types::AgentId;
16
17/// An `ActionExecutor` wrapper that intercepts knowledge tool calls
18/// and delegates all others to an inner executor.
19pub struct KnowledgeAwareExecutor {
20    inner: Arc<dyn ActionExecutor>,
21    bridge: Arc<KnowledgeBridge>,
22    agent_id: AgentId,
23}
24
25impl KnowledgeAwareExecutor {
26    pub fn new(
27        inner: Arc<dyn ActionExecutor>,
28        bridge: Arc<KnowledgeBridge>,
29        agent_id: AgentId,
30    ) -> Self {
31        Self {
32            inner,
33            bridge,
34            agent_id,
35        }
36    }
37}
38
39#[async_trait]
40impl ActionExecutor for KnowledgeAwareExecutor {
41    async fn execute_actions(
42        &self,
43        actions: &[ProposedAction],
44        config: &LoopConfig,
45        circuit_breakers: &CircuitBreakerRegistry,
46    ) -> Vec<Observation> {
47        // Partition actions into knowledge tools vs regular tools
48        let mut knowledge_actions = Vec::new();
49        let mut regular_actions = Vec::new();
50
51        for action in actions {
52            if let ProposedAction::ToolCall {
53                name,
54                call_id,
55                arguments,
56                ..
57            } = action
58            {
59                if KnowledgeBridge::is_knowledge_tool(name) {
60                    knowledge_actions.push((call_id.clone(), name.clone(), arguments.clone()));
61                } else {
62                    regular_actions.push(action.clone());
63                }
64            } else {
65                regular_actions.push(action.clone());
66            }
67        }
68
69        let mut observations = Vec::new();
70
71        // Handle knowledge tools via the bridge
72        for (call_id, name, arguments) in &knowledge_actions {
73            let result = self
74                .bridge
75                .handle_tool_call(&self.agent_id, name, arguments)
76                .await;
77
78            match result {
79                Ok(content) => {
80                    observations.push(Observation::tool_result(call_id, content));
81                }
82                Err(err) => {
83                    observations.push(Observation::tool_error(call_id, err));
84                }
85            }
86        }
87
88        // Delegate regular tools to the inner executor
89        if !regular_actions.is_empty() {
90            let inner_obs = self
91                .inner
92                .execute_actions(&regular_actions, config, circuit_breakers)
93                .await;
94            observations.extend(inner_obs);
95        }
96
97        observations
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crate::reasoning::executor::DefaultActionExecutor;
105    use crate::reasoning::loop_types::LoopConfig;
106
107    /// A mock bridge needs a mock context manager. For unit tests we just
108    /// verify the partitioning logic with the real executor for non-knowledge tools.
109    #[tokio::test]
110    async fn test_regular_actions_delegated() {
111        // We can't easily construct a KnowledgeBridge without a real ContextManager,
112        // so this test focuses on verifying that regular tool calls pass through.
113        let inner = Arc::new(DefaultActionExecutor::default());
114        let config = LoopConfig::default();
115        let circuit_breakers = CircuitBreakerRegistry::default();
116
117        // Regular tool calls should be delegated
118        let actions = vec![ProposedAction::ToolCall {
119            call_id: "c1".into(),
120            name: "web_search".into(),
121            arguments: r#"{"q":"test"}"#.into(),
122        }];
123
124        let obs = inner
125            .execute_actions(&actions, &config, &circuit_breakers)
126            .await;
127        assert_eq!(obs.len(), 1);
128        assert!(!obs[0].is_error);
129    }
130
131    #[test]
132    fn test_knowledge_tool_detection() {
133        assert!(KnowledgeBridge::is_knowledge_tool("recall_knowledge"));
134        assert!(KnowledgeBridge::is_knowledge_tool("store_knowledge"));
135        assert!(!KnowledgeBridge::is_knowledge_tool("web_search"));
136    }
137}