Skip to main content

machi_tools/
tool.rs

1//! Tool definition and dynamic execution surface.
2
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::Arc;
6
7use async_trait::async_trait;
8use serde_json::Value;
9
10use crate::context::ToolCallContext;
11use crate::error::ToolError;
12use crate::metadata::ToolMetadata;
13use crate::stream::{ToolStream, terminal_only};
14
15/// JSON-schema facing tool definition for model APIs.
16#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
17#[allow(
18    clippy::derive_partial_eq_without_eq,
19    reason = "JSON Schema Value is not Eq"
20)]
21pub struct ToolDefinition {
22    /// Tool name.
23    pub name: String,
24    /// Description.
25    pub description: String,
26    /// JSON Schema for parameters.
27    pub parameters: Value,
28}
29
30/// Successful tool output returned to the model.
31#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
32#[allow(
33    clippy::derive_partial_eq_without_eq,
34    reason = "optional JSON Value is not Eq"
35)]
36pub struct ToolResult {
37    /// Text content for the tool message.
38    pub content: String,
39    /// Optional structured payload.
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub structured: Option<Value>,
42    /// Whether the tool reported a logical failure (still a completed call).
43    #[serde(default)]
44    pub is_error: bool,
45}
46
47impl ToolResult {
48    /// Successful text result.
49    #[must_use]
50    pub fn text(content: impl Into<String>) -> Self {
51        Self {
52            content: content.into(),
53            structured: None,
54            is_error: false,
55        }
56    }
57
58    /// Error-shaped tool result (for model consumption).
59    #[must_use]
60    pub fn error(content: impl Into<String>) -> Self {
61        Self {
62            content: content.into(),
63            structured: None,
64            is_error: true,
65        }
66    }
67}
68
69/// Object-safe tool.
70#[async_trait]
71pub trait DynTool: Send + Sync {
72    /// Tool name.
73    fn name(&self) -> &str;
74    /// Description.
75    fn description(&self) -> &str;
76    /// JSON schema parameters.
77    fn parameters(&self) -> Value;
78    /// Metadata.
79    fn metadata(&self) -> ToolMetadata {
80        ToolMetadata::default()
81    }
82    /// Model-facing definition.
83    fn definition(&self) -> ToolDefinition {
84        ToolDefinition {
85            name: self.name().to_owned(),
86            description: self.description().to_owned(),
87            parameters: self.parameters(),
88        }
89    }
90    /// Execute with JSON arguments (blocking convenience).
91    ///
92    /// Prefer overriding [`DynTool::execute`] when the tool emits progress.
93    async fn call(&self, ctx: ToolCallContext, arguments: Value) -> Result<ToolResult, ToolError>;
94
95    /// Streaming entry point. Default wraps [`DynTool::call`] as a single terminal.
96    async fn execute(&self, ctx: ToolCallContext, arguments: Value) -> ToolStream {
97        let result = self.call(ctx, arguments).await;
98        terminal_only(result)
99    }
100}
101
102/// Shared tool handle.
103pub type SharedTool = Arc<dyn DynTool>;
104
105/// Boxed async future for dispatch internals.
106pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;