Skip to main content

pulsehive_core/
tool.rs

1//! Tool trait and execution context.
2//!
3//! Products implement [`Tool`] to give agents domain-specific capabilities.
4//! The framework calls tools during the Act phase of the agentic loop.
5//!
6//! # Example
7//! ```rust,ignore
8//! struct FileReader;
9//!
10//! #[async_trait]
11//! impl Tool for FileReader {
12//!     fn name(&self) -> &str { "read_file" }
13//!     fn description(&self) -> &str { "Read file contents at a path" }
14//!     fn parameters(&self) -> Value {
15//!         json!({"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]})
16//!     }
17//!     async fn execute(&self, params: Value, _ctx: &ToolContext) -> Result<ToolResult> {
18//!         let path = params["path"].as_str().ok_or(PulseHiveError::validation("path required"))?;
19//!         Ok(ToolResult::text(tokio::fs::read_to_string(path).await.unwrap()))
20//!     }
21//! }
22//! ```
23
24use std::sync::Arc;
25
26use async_trait::async_trait;
27use pulsedb::{CollectiveId, SubstrateProvider};
28use serde_json::Value;
29
30use crate::error::Result;
31use crate::event::EventEmitter;
32
33/// Trait for domain-specific tool implementations.
34///
35/// Tools are the capabilities that agents can invoke during task execution.
36/// The LLM decides which tool to call based on the `name()`, `description()`,
37/// and `parameters()` (JSON Schema) exposed to it.
38///
39/// Must be `Send + Sync` for concurrent execution across Tokio tasks.
40#[async_trait]
41pub trait Tool: Send + Sync {
42    /// Tool name shown to the LLM for selection.
43    fn name(&self) -> &str;
44
45    /// Description the LLM uses to decide when to invoke this tool.
46    fn description(&self) -> &str;
47
48    /// JSON Schema describing the tool's parameters.
49    fn parameters(&self) -> Value;
50
51    /// Execute the tool with the given parameters.
52    async fn execute(&self, params: Value, context: &ToolContext) -> Result<ToolResult>;
53
54    /// Whether this tool requires human approval before execution.
55    ///
56    /// When `true`, the framework calls the [`ApprovalHandler`](crate::approval::ApprovalHandler)
57    /// before executing. Default: `false`.
58    fn requires_approval(&self) -> bool {
59        false
60    }
61}
62
63/// Runtime context available to tools during execution.
64///
65/// Provides access to the agent's identity, the shared substrate, and the event
66/// emitter for tools that need to read/write experiences or emit custom events.
67pub struct ToolContext {
68    /// ID of the agent executing this tool.
69    pub agent_id: String,
70    /// Collective (namespace) the agent belongs to.
71    pub collective_id: CollectiveId,
72    /// Shared substrate for reading/writing experiences during tool execution.
73    pub substrate: Arc<dyn SubstrateProvider>,
74    /// Event emitter for tools that need to emit custom events.
75    pub event_emitter: EventEmitter,
76}
77
78/// Result of a tool execution.
79#[derive(Debug, Clone)]
80pub enum ToolResult {
81    /// Plain text result.
82    Text(String),
83    /// Structured JSON result.
84    Json(Value),
85    /// Error message (tool failed but execution continues — LLM is informed).
86    Error(String),
87}
88
89impl ToolResult {
90    /// Creates a text result.
91    pub fn text(s: impl Into<String>) -> Self {
92        Self::Text(s.into())
93    }
94
95    /// Creates a JSON result.
96    pub fn json(v: Value) -> Self {
97        Self::Json(v)
98    }
99
100    /// Creates an error result.
101    pub fn error(s: impl Into<String>) -> Self {
102        Self::Error(s.into())
103    }
104
105    /// Returns the result as a string (for sending back to the LLM).
106    pub fn to_content(&self) -> String {
107        match self {
108            Self::Text(s) => s.clone(),
109            Self::Json(v) => v.to_string(),
110            Self::Error(s) => format!("Error: {s}"),
111        }
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::error::PulseHiveError;
119
120    #[test]
121    fn test_tool_is_object_safe() {
122        fn _assert_object_safe(_: &dyn Tool) {}
123        fn _assert_boxable(_: Box<dyn Tool>) {}
124        fn _assert_arcable(_: Arc<dyn Tool>) {}
125    }
126
127    // Mock tool for testing
128    struct EchoTool;
129
130    #[async_trait]
131    impl Tool for EchoTool {
132        fn name(&self) -> &str {
133            "echo"
134        }
135        fn description(&self) -> &str {
136            "Echoes input back"
137        }
138        fn parameters(&self) -> Value {
139            serde_json::json!({"type": "object", "properties": {"text": {"type": "string"}}})
140        }
141        async fn execute(&self, params: Value, _ctx: &ToolContext) -> Result<ToolResult> {
142            let text = params["text"]
143                .as_str()
144                .ok_or_else(|| PulseHiveError::validation("text required"))?;
145            Ok(ToolResult::text(text))
146        }
147    }
148
149    #[test]
150    fn test_mock_tool_metadata() {
151        let tool = EchoTool;
152        assert_eq!(tool.name(), "echo");
153        assert_eq!(tool.description(), "Echoes input back");
154        assert!(!tool.requires_approval());
155    }
156
157    #[test]
158    fn test_tool_result_constructors() {
159        let text = ToolResult::text("hello");
160        assert!(matches!(text, ToolResult::Text(s) if s == "hello"));
161
162        let json = ToolResult::json(serde_json::json!({"key": "value"}));
163        assert!(matches!(json, ToolResult::Json(_)));
164
165        let err = ToolResult::error("not found");
166        assert!(matches!(err, ToolResult::Error(s) if s == "not found"));
167    }
168
169    #[test]
170    fn test_tool_result_to_content() {
171        assert_eq!(ToolResult::text("hello").to_content(), "hello");
172        assert_eq!(
173            ToolResult::json(serde_json::json!({"a": 1})).to_content(),
174            r#"{"a":1}"#
175        );
176        assert_eq!(ToolResult::error("oops").to_content(), "Error: oops");
177    }
178}