1use std::future::Future;
2use std::marker::PhantomData;
3use std::time::Duration;
4
5use async_trait::async_trait;
6use schemars::JsonSchema;
7use serde::de::DeserializeOwned;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use sha2::{Digest, Sha256};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum Effect {
16 Read,
18 Write,
20 Irreversible,
22}
23
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub struct ToolSpec {
26 pub name: String,
27 pub description: String,
28 pub input_schema: Value,
29 pub effect: Effect,
30}
31
32#[derive(Debug, Clone)]
34pub struct ToolCtx {
35 pub run_id: String,
36 pub call_id: String,
37 pub idempotency_key: String,
40}
41
42impl ToolCtx {
43 pub fn new(run_id: &str, call_id: &str) -> Self {
44 let digest = Sha256::digest(format!("{run_id}:{call_id}").as_bytes());
45 Self { run_id: run_id.to_owned(), call_id: call_id.to_owned(), idempotency_key: hex::encode(&digest[..16]) }
46 }
47}
48
49#[derive(Debug, Clone, thiserror::Error)]
50#[error("{0}")]
51pub struct ToolError(pub String);
52
53impl ToolError {
54 pub fn new(msg: impl Into<String>) -> Self {
55 Self(msg.into())
56 }
57}
58
59#[async_trait]
60pub trait Tool: Send + Sync {
61 fn spec(&self) -> &ToolSpec;
62 async fn call(&self, input: Value, ctx: &ToolCtx) -> Result<Value, ToolError>;
63
64 fn timeout(&self) -> Option<Duration> {
66 None
67 }
68}
69
70pub fn tool<I, O, F, Fut>(name: &str, description: &str, effect: Effect, f: F) -> FnTool<I, O, F>
81where
82 I: DeserializeOwned + JsonSchema + Send + 'static,
83 O: Serialize + Send + 'static,
84 F: Fn(I, ToolCtx) -> Fut + Send + Sync + 'static,
85 Fut: Future<Output = Result<O, ToolError>> + Send + 'static,
86{
87 let mut schema = serde_json::to_value(schemars::schema_for!(I)).unwrap_or(Value::Null);
88 if let Value::Object(map) = &mut schema {
89 map.remove("$schema");
90 map.remove("title");
91 }
92 FnTool {
93 spec: ToolSpec { name: name.to_owned(), description: description.to_owned(), input_schema: schema, effect },
94 f,
95 timeout: None,
96 _marker: PhantomData,
97 }
98}
99
100pub struct FnTool<I, O, F> {
101 spec: ToolSpec,
102 f: F,
103 timeout: Option<Duration>,
104 _marker: PhantomData<fn(I) -> O>,
105}
106
107impl<I, O, F> FnTool<I, O, F> {
108 pub fn with_timeout(mut self, limit: Duration) -> Self {
111 self.timeout = Some(limit);
112 self
113 }
114}
115
116#[async_trait]
117impl<I, O, F, Fut> Tool for FnTool<I, O, F>
118where
119 I: DeserializeOwned + JsonSchema + Send + 'static,
120 O: Serialize + Send + 'static,
121 F: Fn(I, ToolCtx) -> Fut + Send + Sync + 'static,
122 Fut: Future<Output = Result<O, ToolError>> + Send + 'static,
123{
124 fn spec(&self) -> &ToolSpec {
125 &self.spec
126 }
127
128 fn timeout(&self) -> Option<Duration> {
129 self.timeout
130 }
131
132 async fn call(&self, input: Value, ctx: &ToolCtx) -> Result<Value, ToolError> {
133 let typed: I = serde_json::from_value(input)
134 .map_err(|e| ToolError(format!("invalid input for `{}`: {e}", self.spec.name)))?;
135 let out = (self.f)(typed, ctx.clone()).await?;
136 serde_json::to_value(out).map_err(|e| ToolError(format!("could not serialize output: {e}")))
137 }
138}