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}