Skip to main content

synapto_interface/
tool.rs

1#![doc = include_str!("tool.md")]
2
3use crate::context::ContextRequest;
4use crate::llm::LLMSafe;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8#[async_trait::async_trait]
9pub trait Tool: Send + Sync + 'static {
10    type Arguments: schemars::JsonSchema
11        + serde::de::DeserializeOwned
12        + LLMSafe
13        + Send
14        + Sync
15        + 'static;
16    const NAME: &'static str;
17    const DESCRIPTION: &'static str;
18    #[doc = " Evaluated dynamically every turn AFTER the ContextProviders have compiled the World State."]
19    #[doc = " `compiled_context` is the JSON value generated by all ContextProviders that the LLM is about to see."]
20    async fn is_available(
21        &self,
22        _ctx_request: &ContextRequest,
23        _compiled_context: &serde_json::Value,
24    ) -> Result<bool, String> {
25        Ok(true)
26    }
27    #[doc = " Executes the tool. The result is serialized and fed back to the LLM."]
28    async fn execute(
29        &self,
30        ctx_request: &ContextRequest,
31        args: Self::Arguments,
32    ) -> Result<serde_json::Value, String>;
33}
34
35#[doc = " Type-erased trait for tools registered dynamically at runtime."]
36#[async_trait::async_trait]
37pub trait ErasedTool: Send + Sync + 'static {
38    #[doc = " Unique identifier name of the tool."]
39    fn name(&self) -> &'static str;
40    #[doc = " Human/LLM-readable description explaining the tool's capability."]
41    fn description(&self) -> &'static str;
42    #[doc = " JSON Schema describing expected tool call arguments."]
43    fn schema(&self) -> schemars::Schema;
44    #[doc = " Evaluated per turn to determine if tool is currently active/available."]
45    async fn erased_is_available(
46        &self,
47        _ctx_request: &ContextRequest,
48        _compiled_context: &serde_json::Value,
49    ) -> Result<bool, String> {
50        Ok(true)
51    }
52    #[doc = " Executes the tool with untyped JSON arguments and returns structured JSON output."]
53    async fn erased_execute(
54        &self,
55        ctx_request: &ContextRequest,
56        args: serde_json::Value,
57    ) -> Result<serde_json::Value, String>;
58}
59
60#[async_trait::async_trait]
61impl<T> ErasedTool for T
62where
63    T: Tool,
64{
65    fn name(&self) -> &'static str {
66        <T as Tool>::NAME
67    }
68    fn description(&self) -> &'static str {
69        <T as Tool>::DESCRIPTION
70    }
71    fn schema(&self) -> schemars::Schema {
72        schemars::schema_for!(<T as Tool>::Arguments)
73    }
74    async fn erased_is_available(
75        &self,
76        ctx_request: &ContextRequest,
77        compiled_context: &serde_json::Value,
78    ) -> Result<bool, String> {
79        <T as Tool>::is_available(self, ctx_request, compiled_context).await
80    }
81    async fn erased_execute(
82        &self,
83        ctx_request: &ContextRequest,
84        args: serde_json::Value,
85    ) -> Result<serde_json::Value, String> {
86        let parsed_args = serde_json::from_value(args).map_err(|e| e.to_string())?;
87        <T as Tool>::execute(self, ctx_request, parsed_args).await
88    }
89}
90
91/// Opaque handle wrapping a type-erased tool for dynamic registration.
92#[derive(Clone)]
93pub struct ToolHandle(std::sync::Arc<dyn ErasedTool>);
94
95impl std::fmt::Debug for ToolHandle {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        f.debug_struct("ToolHandle")
98            .field("name", &self.0.name())
99            .finish()
100    }
101}
102
103impl ToolHandle {
104    pub fn new<T: ErasedTool + 'static>(tool: T) -> Self {
105        Self(std::sync::Arc::new(tool))
106    }
107
108    pub fn from_arc(tool: std::sync::Arc<dyn ErasedTool>) -> Self {
109        Self(tool)
110    }
111
112    pub fn into_inner(self) -> std::sync::Arc<dyn ErasedTool> {
113        self.0
114    }
115
116    pub fn inner(&self) -> &std::sync::Arc<dyn ErasedTool> {
117        &self.0
118    }
119
120    pub fn name(&self) -> &'static str {
121        self.0.name()
122    }
123
124    pub fn description(&self) -> &'static str {
125        self.0.description()
126    }
127
128    pub fn schema(&self) -> schemars::Schema {
129        self.0.schema()
130    }
131
132    pub async fn erased_is_available(
133        &self,
134        ctx_request: &ContextRequest,
135        compiled_context: &serde_json::Value,
136    ) -> Result<bool, String> {
137        self.0
138            .erased_is_available(ctx_request, compiled_context)
139            .await
140    }
141
142    pub async fn erased_execute(
143        &self,
144        ctx_request: &ContextRequest,
145        args: serde_json::Value,
146    ) -> Result<serde_json::Value, String> {
147        self.0.erased_execute(ctx_request, args).await
148    }
149}
150
151#[async_trait::async_trait]
152impl ErasedTool for ToolHandle {
153    fn name(&self) -> &'static str {
154        self.0.name()
155    }
156    fn description(&self) -> &'static str {
157        self.0.description()
158    }
159    fn schema(&self) -> schemars::Schema {
160        self.0.schema()
161    }
162    async fn erased_is_available(
163        &self,
164        ctx_request: &ContextRequest,
165        compiled_context: &serde_json::Value,
166    ) -> Result<bool, String> {
167        self.0
168            .erased_is_available(ctx_request, compiled_context)
169            .await
170    }
171    async fn erased_execute(
172        &self,
173        ctx_request: &ContextRequest,
174        args: serde_json::Value,
175    ) -> Result<serde_json::Value, String> {
176        self.0.erased_execute(ctx_request, args).await
177    }
178}
179
180#[derive(Default)]
181pub struct ToolRegistryBuilder {
182    pub tools: std::sync::RwLock<std::collections::HashMap<String, std::sync::Arc<dyn ErasedTool>>>,
183}
184
185impl ToolRegistryBuilder {
186    pub fn register<T>(&self, tool: T)
187    where
188        T: ErasedTool + 'static,
189    {
190        let tool_arc: std::sync::Arc<dyn ErasedTool> = std::sync::Arc::new(tool);
191        self.register_erased(tool_arc);
192    }
193    pub fn register_erased(&self, tool: std::sync::Arc<dyn ErasedTool>) {
194        self.tools
195            .write()
196            .unwrap_or_else(|e| panic!("Failed to acquire write lock on tools: {:?}", e))
197            .insert(tool.name().to_string(), tool);
198    }
199    pub fn get(&self, name: &str) -> Option<std::sync::Arc<dyn ErasedTool>> {
200        self.tools
201            .read()
202            .unwrap_or_else(|e| panic!("Failed to acquire read lock on tools: {:?}", e))
203            .get(name)
204            .cloned()
205    }
206    pub fn get_all(&self) -> Vec<std::sync::Arc<dyn ErasedTool>> {
207        self.tools
208            .read()
209            .unwrap_or_else(|e| panic!("Failed to acquire read lock on tools: {:?}", e))
210            .values()
211            .cloned()
212            .collect()
213    }
214}
215
216#[derive(
217    Serialize,
218    Deserialize,
219    JsonSchema,
220    PartialEq,
221    Eq,
222    Debug,
223    Clone,
224    derive_more :: Display,
225    derive_more :: From,
226    derive_more :: Deref,
227)]
228pub struct ToolCallId(pub String);