Skip to main content

pe_core/
internal_tool.rs

1//! Internal tools -- cognitive-only tools for agent self-management.
2//!
3//! Internal tools operate inside the cognitive graph, reading from
4//! [`CognitiveState`] to help the agent manage its working memory,
5//! constraints, plans, and signals. They are NOT exposed to the outer
6//! execution graph.
7//!
8//! Tools receive `&CognitiveState` (read-only) and return JSON describing
9//! what should be applied. Actual state mutation happens via
10//! [`CognitiveStateUpdate`](crate::CognitiveStateUpdate) at the node level,
11//! following Pregel snapshot isolation.
12
13use crate::cognitive::CognitiveState;
14use crate::error::PeError;
15use std::collections::HashMap;
16use std::sync::Arc;
17
18/// A tool available only inside the cognitive graph.
19///
20/// Internal tools operate on [`CognitiveState`] for self-management:
21/// writing notes, recalling constraints, recording failures, etc.
22/// They are NOT exposed to the outer execution graph.
23///
24/// # Example
25///
26/// ```
27/// use pe_core::internal_tool::InternalTool;
28/// use pe_core::cognitive::CognitiveState;
29/// use pe_core::error::PeError;
30///
31/// struct MyTool;
32///
33/// #[async_trait::async_trait]
34/// impl InternalTool for MyTool {
35///     fn name(&self) -> &str { "my_tool" }
36///     fn description(&self) -> &str { "A custom internal tool" }
37///     async fn execute(
38///         &self,
39///         _state: &CognitiveState,
40///         _args: serde_json::Value,
41///     ) -> Result<serde_json::Value, PeError> {
42///         Ok(serde_json::json!({"status": "ok"}))
43///     }
44/// }
45/// ```
46#[async_trait::async_trait]
47pub trait InternalTool: Send + Sync {
48    /// Unique tool name.
49    fn name(&self) -> &str;
50
51    /// Human-readable description for LLM tool-use prompts.
52    fn description(&self) -> &str;
53
54    /// Execute the tool with the current cognitive state and arguments.
55    async fn execute(
56        &self,
57        state: &CognitiveState,
58        args: serde_json::Value,
59    ) -> Result<serde_json::Value, PeError>;
60}
61
62/// Registry of internal tools, keyed by name.
63///
64/// Stores tools as `Arc<dyn InternalTool>` for cheap cloning and
65/// shared ownership across cognitive graph nodes.
66///
67/// # Example
68///
69/// ```
70/// use pe_core::internal_tool::InternalToolRegistry;
71///
72/// let registry = InternalToolRegistry::new();
73/// assert!(registry.is_empty());
74/// assert_eq!(registry.len(), 0);
75/// ```
76pub struct InternalToolRegistry {
77    tools: HashMap<String, Arc<dyn InternalTool>>,
78}
79
80impl InternalToolRegistry {
81    /// Create an empty registry.
82    pub fn new() -> Self {
83        Self {
84            tools: HashMap::new(),
85        }
86    }
87
88    /// Register an internal tool. Overwrites any existing tool with the same name.
89    pub fn register(&mut self, tool: Arc<dyn InternalTool>) -> &mut Self {
90        self.tools.insert(tool.name().to_string(), tool);
91        self
92    }
93
94    /// Remove a tool by name. Returns the removed tool if it existed.
95    pub fn unregister(&mut self, name: &str) -> Option<Arc<dyn InternalTool>> {
96        self.tools.remove(name)
97    }
98
99    /// Look up a tool by name.
100    pub fn get(&self, name: &str) -> Option<&Arc<dyn InternalTool>> {
101        self.tools.get(name)
102    }
103
104    /// List all registered tool names.
105    pub fn list(&self) -> Vec<&str> {
106        self.tools.keys().map(|k| k.as_str()).collect()
107    }
108
109    /// Number of registered tools.
110    pub fn len(&self) -> usize {
111        self.tools.len()
112    }
113
114    /// Whether the registry is empty.
115    pub fn is_empty(&self) -> bool {
116        self.tools.is_empty()
117    }
118}
119
120impl Default for InternalToolRegistry {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    struct DummyTool {
131        tool_name: &'static str,
132    }
133
134    #[async_trait::async_trait]
135    impl InternalTool for DummyTool {
136        fn name(&self) -> &str {
137            self.tool_name
138        }
139        fn description(&self) -> &str {
140            "A dummy tool for testing"
141        }
142        async fn execute(
143            &self,
144            _state: &CognitiveState,
145            _args: serde_json::Value,
146        ) -> Result<serde_json::Value, PeError> {
147            Ok(serde_json::json!({"status": "ok"}))
148        }
149    }
150
151    #[test]
152    fn test_registry_new_is_empty() {
153        let registry = InternalToolRegistry::new();
154        assert!(registry.is_empty());
155        assert_eq!(registry.len(), 0);
156        assert!(registry.list().is_empty());
157    }
158
159    #[test]
160    fn test_registry_register_and_get() {
161        let mut registry = InternalToolRegistry::new();
162        registry.register(Arc::new(DummyTool { tool_name: "note" }));
163        assert_eq!(registry.len(), 1);
164        assert!(!registry.is_empty());
165
166        let tool = registry.get("note").expect("tool should exist");
167        assert_eq!(tool.name(), "note");
168        assert_eq!(tool.description(), "A dummy tool for testing");
169    }
170
171    #[test]
172    fn test_registry_get_missing_returns_none() {
173        let registry = InternalToolRegistry::new();
174        assert!(registry.get("nonexistent").is_none());
175    }
176
177    #[test]
178    fn test_registry_overwrite_same_name() {
179        let mut registry = InternalToolRegistry::new();
180        registry.register(Arc::new(DummyTool { tool_name: "note" }));
181        registry.register(Arc::new(DummyTool { tool_name: "note" }));
182        assert_eq!(registry.len(), 1);
183    }
184
185    #[test]
186    fn test_registry_list_returns_all_names() {
187        let mut registry = InternalToolRegistry::new();
188        registry.register(Arc::new(DummyTool { tool_name: "note" }));
189        registry.register(Arc::new(DummyTool {
190            tool_name: "read_notes",
191        }));
192        registry.register(Arc::new(DummyTool {
193            tool_name: "signal",
194        }));
195        let mut names = registry.list();
196        names.sort();
197        assert_eq!(names, vec!["note", "read_notes", "signal"]);
198    }
199
200    #[test]
201    fn test_registry_chained_register() {
202        let mut registry = InternalToolRegistry::new();
203        registry
204            .register(Arc::new(DummyTool { tool_name: "a" }))
205            .register(Arc::new(DummyTool { tool_name: "b" }));
206        assert_eq!(registry.len(), 2);
207    }
208
209    #[tokio::test]
210    async fn test_tool_execute() {
211        let tool = DummyTool { tool_name: "test" };
212        let state = CognitiveState::default();
213        let result = tool
214            .execute(&state, serde_json::json!({}))
215            .await
216            .expect("execute should succeed");
217        assert_eq!(result, serde_json::json!({"status": "ok"}));
218    }
219}