Skip to main content

holon/tool/
spec.rs

1//! Tool schema specifications
2//!
3//! This module defines the public types used to describe tools and their input/output schemas.
4
5use anyhow::Result;
6
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10use super::error::ToolError;
11
12/// A tool specification describing name, description, and input schema.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ToolSpec {
15    pub name: String,
16    pub description: String,
17    pub input_schema: Value,
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub freeform_grammar: Option<ToolFreeformGrammar>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
23pub struct ToolFreeformGrammar {
24    pub syntax: String,
25    pub definition: String,
26}
27
28/// A tool call from the model.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ToolCall {
31    pub id: String,
32    pub name: String,
33    pub input: Value,
34}
35
36/// Result from executing a tool.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ToolResult {
39    pub envelope: ToolResultEnvelope,
40    pub should_sleep: bool,
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub sleep_duration_ms: Option<u64>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
46#[serde(rename_all = "snake_case")]
47pub enum ToolResultStatus {
48    Success,
49    Error,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct ToolResultEnvelope {
54    pub tool_name: String,
55    pub status: ToolResultStatus,
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub summary_text: Option<String>,
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub result: Option<Value>,
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub error: Option<ToolError>,
62}
63
64impl ToolResult {
65    pub fn success(
66        tool_name: impl Into<String>,
67        result: Value,
68        summary_text: Option<String>,
69    ) -> Self {
70        let envelope = ToolResultEnvelope {
71            tool_name: tool_name.into(),
72            status: ToolResultStatus::Success,
73            summary_text,
74            result: Some(result),
75            error: None,
76        };
77        Self {
78            envelope,
79            should_sleep: false,
80            sleep_duration_ms: None,
81        }
82    }
83
84    pub fn sleep(
85        tool_name: impl Into<String>,
86        result: Value,
87        summary_text: Option<String>,
88        sleep_duration_ms: Option<u64>,
89    ) -> Self {
90        let envelope = ToolResultEnvelope {
91            tool_name: tool_name.into(),
92            status: ToolResultStatus::Success,
93            summary_text,
94            result: Some(result),
95            error: None,
96        };
97        Self {
98            envelope,
99            should_sleep: true,
100            sleep_duration_ms,
101        }
102    }
103
104    pub fn error(tool_name: impl Into<String>, error: ToolError) -> Self {
105        let envelope = ToolResultEnvelope {
106            tool_name: tool_name.into(),
107            status: ToolResultStatus::Error,
108            summary_text: Some(error.message.clone()),
109            result: None,
110            error: Some(error.clone()),
111        };
112        Self {
113            envelope,
114            should_sleep: false,
115            sleep_duration_ms: None,
116        }
117    }
118
119    pub fn content_text(&self) -> Result<String> {
120        serde_json::to_string(&self.envelope).map_err(Into::into)
121    }
122
123    pub fn is_error(&self) -> bool {
124        matches!(self.envelope.status, ToolResultStatus::Error)
125    }
126
127    pub fn tool_error(&self) -> Option<&ToolError> {
128        self.envelope.error.as_ref()
129    }
130
131    pub fn summary_text(&self) -> Option<&str> {
132        self.envelope.summary_text.as_deref()
133    }
134}
135
136/// Helper to build a ToolSpec.
137pub(crate) fn spec(name: &str, description: &str, input_schema: Value) -> ToolSpec {
138    ToolSpec {
139        name: name.to_string(),
140        description: description.to_string(),
141        input_schema,
142        freeform_grammar: None,
143    }
144}
145
146/// Helper to build a ToolSpec from a typed input schema.
147pub(crate) fn typed_spec<T: schemars::JsonSchema + 'static>(
148    name: &str,
149    description: &str,
150) -> Result<ToolSpec> {
151    Ok(spec(
152        name,
153        description,
154        crate::tool::schema::tool_input_schema::<T>()?,
155    ))
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_spec_builder() {
164        let tool_spec = spec(
165            "TestTool",
166            "A test tool",
167            serde_json::json!({"type": "object"}),
168        );
169
170        assert_eq!(tool_spec.name, "TestTool");
171        assert_eq!(tool_spec.description, "A test tool");
172    }
173}