Skip to main content

ferrin_tool/
builder.rs

1//! [`ToolBuilder`] and the closure adapters behind `execute`.
2
3use std::marker::PhantomData;
4use std::sync::Arc;
5
6use ferrin_schema::JsonSchema;
7use ferrin_schema::Schema;
8use ferrin_spec::JsonObject;
9use ferrin_spec::JsonValue;
10use ferrin_spec::ProviderOptions;
11use ferrin_spec::language_model::prompt::ToolResultOutput;
12use futures_core::Stream;
13use futures_util::StreamExt;
14use serde::Serialize;
15use serde::de::DeserializeOwned;
16
17use crate::callers::ToolCallerDefinition;
18use crate::error::ToolError;
19use crate::execute::ToolContext;
20use crate::execute::ToolExecute;
21use crate::execute::ToolOutput;
22use crate::execute::ToolOutputStream;
23use crate::tool::Description;
24use crate::tool::DescriptionContext;
25use crate::tool::ModelOutputArgs;
26use crate::tool::NeedsApproval;
27use crate::tool::Tool;
28use crate::tool::ToolHooks;
29use crate::tool::ToolKind;
30
31/// Builds a [`Tool`]. `I` is the input type handed to the execute closure.
32pub struct ToolBuilder<I> {
33    tool: Tool,
34    _input: PhantomData<fn() -> I>,
35}
36
37impl<I> std::fmt::Debug for ToolBuilder<I> {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        f.debug_struct("ToolBuilder")
40            .field("tool", &self.tool)
41            .finish()
42    }
43}
44
45fn base(kind: ToolKind, input_schema: Schema<JsonValue>) -> Tool {
46    Tool {
47        kind,
48        description: None,
49        title: None,
50        input_schema,
51        output_schema: None,
52        context_schema: None,
53        execute: None,
54        needs_approval: NeedsApproval::Never,
55        strict: None,
56        input_examples: Vec::new(),
57        metadata: None,
58        provider_options: None,
59        hooks: ToolHooks::default(),
60        to_model_output: None,
61        caller_definition: None,
62    }
63}
64
65impl Tool {
66    /// Reopens this tool for configuration, preserving its definition and callbacks.
67    ///
68    /// The builder's execute closure receives JSON input; the existing input schema
69    /// and executor are retained until explicitly replaced.
70    ///
71    /// # Examples
72    ///
73    /// ```
74    /// use ferrin_tool::{Tool, ToolError};
75    /// use ferrin_spec::JsonObject;
76    /// let tool = Tool::provider_defined("provider.search", JsonObject::new()).build();
77    /// let executable = tool.into_builder()
78    ///     .execute(|input: serde_json::Value, _| async move {
79    ///         Ok::<_, ToolError>(input)
80    ///     })
81    ///     .build();
82    /// assert!(executable.is_executable());
83    /// ```
84    #[must_use]
85    pub fn into_builder(self) -> ToolBuilder<JsonValue> {
86        ToolBuilder {
87            tool: self,
88            _input: PhantomData,
89        }
90    }
91
92    /// A function tool whose input schema is derived from `I`
93    /// (draft-07, `additionalProperties: false` on objects).
94    #[must_use]
95    pub fn function<I: DeserializeOwned + JsonSchema + 'static>() -> ToolBuilder<I> {
96        ToolBuilder {
97            tool: base(ToolKind::Function, Schema::<I>::derived().erased()),
98            _input: PhantomData,
99        }
100    }
101
102    /// A function tool with an explicit JSON schema; the execute closure
103    /// receives the raw JSON input.
104    #[must_use]
105    pub fn function_with_schema(input_schema: Schema<JsonValue>) -> ToolBuilder<JsonValue> {
106        ToolBuilder {
107            tool: base(ToolKind::Function, input_schema),
108            _input: PhantomData,
109        }
110    }
111
112    /// A dynamic tool (runtime-defined schema, untyped input and output).
113    #[must_use]
114    pub fn dynamic(input_schema: Schema<JsonValue>) -> ToolBuilder<JsonValue> {
115        ToolBuilder {
116            tool: base(ToolKind::Dynamic, input_schema),
117            _input: PhantomData,
118        }
119    }
120
121    /// A provider-defined tool executed by the application. The input schema
122    /// defaults to "any"; provider crates set the real one.
123    #[must_use]
124    pub fn provider_defined(id: impl Into<String>, args: JsonObject) -> ToolBuilder<JsonValue> {
125        ToolBuilder {
126            tool: base(
127                ToolKind::ProviderDefined {
128                    id: id.into(),
129                    args,
130                },
131                Schema::any(),
132            ),
133            _input: PhantomData,
134        }
135    }
136
137    /// A provider-executed tool.
138    #[must_use]
139    pub fn provider_executed(id: impl Into<String>, args: JsonObject) -> ToolBuilder<JsonValue> {
140        ToolBuilder {
141            tool: base(
142                ToolKind::ProviderExecuted {
143                    id: id.into(),
144                    args,
145                    supports_deferred_results: false,
146                },
147                Schema::any(),
148            ),
149            _input: PhantomData,
150        }
151    }
152}
153
154impl<I> ToolBuilder<I> {
155    /// Fixed description.
156    #[must_use]
157    pub fn description(mut self, description: impl Into<String>) -> Self {
158        self.tool.description = Some(Description::Static(description.into()));
159        self
160    }
161
162    /// Description computed per call.
163    #[must_use]
164    pub fn description_fn<F, Fut>(mut self, function: F) -> Self
165    where
166        F: Fn(DescriptionContext) -> Fut + Send + Sync + 'static,
167        Fut: Future<Output = String> + Send + 'static,
168    {
169        self.tool.description = Some(Description::Dynamic(Arc::new(move |ctx| {
170            Box::pin(function(ctx))
171        })));
172        self
173    }
174
175    /// Title.
176    #[must_use]
177    pub fn title(mut self, title: impl Into<String>) -> Self {
178        self.tool.title = Some(title.into());
179        self
180    }
181
182    /// Replaces the input schema (keeps the closure input type).
183    #[must_use]
184    pub fn input_schema(mut self, schema: Schema<JsonValue>) -> Self {
185        self.tool.input_schema = schema;
186        self
187    }
188
189    /// Output schema.
190    #[must_use]
191    pub fn output_schema(mut self, schema: Schema<JsonValue>) -> Self {
192        self.tool.output_schema = Some(schema);
193        self
194    }
195
196    /// Context schema.
197    #[must_use]
198    pub fn context_schema(mut self, schema: Schema<JsonValue>) -> Self {
199        self.tool.context_schema = Some(schema);
200        self
201    }
202
203    /// Approval declaration.
204    #[must_use]
205    pub fn needs_approval(mut self, needs_approval: NeedsApproval) -> Self {
206        self.tool.needs_approval = needs_approval;
207        self
208    }
209
210    /// Approval decided per call from the validated input.
211    #[must_use]
212    pub fn needs_approval_if<F, Fut>(mut self, function: F) -> Self
213    where
214        F: Fn(JsonValue, ToolContext) -> Fut + Send + Sync + 'static,
215        Fut: Future<Output = bool> + Send + 'static,
216    {
217        self.tool.needs_approval =
218            NeedsApproval::Dynamic(Arc::new(move |input, ctx| Box::pin(function(input, ctx))));
219        self
220    }
221
222    /// Strict-mode request.
223    #[must_use]
224    pub fn strict(mut self, strict: bool) -> Self {
225        self.tool.strict = Some(strict);
226        self
227    }
228
229    /// Adds an example input.
230    #[must_use]
231    pub fn input_example(mut self, example: JsonObject) -> Self {
232        self.tool.input_examples.push(example);
233        self
234    }
235
236    /// Adds example inputs.
237    #[must_use]
238    pub fn input_examples(mut self, examples: impl IntoIterator<Item = JsonObject>) -> Self {
239        self.tool.input_examples.extend(examples);
240        self
241    }
242
243    /// Metadata propagated to tool calls.
244    #[must_use]
245    pub fn metadata(mut self, metadata: JsonObject) -> Self {
246        self.tool.metadata = Some(metadata);
247        self
248    }
249
250    /// Provider options sent with the definition.
251    #[must_use]
252    pub fn provider_options(mut self, options: ProviderOptions) -> Self {
253        self.tool.provider_options = Some(options);
254        self
255    }
256
257    /// Hook called when the model starts producing input.
258    #[must_use]
259    pub fn on_input_start<F, Fut>(mut self, function: F) -> Self
260    where
261        F: Fn(ToolContext) -> Fut + Send + Sync + 'static,
262        Fut: Future<Output = ()> + Send + 'static,
263    {
264        self.tool.hooks.on_input_start = Some(Arc::new(move |ctx| Box::pin(function(ctx))));
265        self
266    }
267
268    /// Hook called for each input delta.
269    #[must_use]
270    pub fn on_input_delta<F, Fut>(mut self, function: F) -> Self
271    where
272        F: Fn(String, ToolContext) -> Fut + Send + Sync + 'static,
273        Fut: Future<Output = ()> + Send + 'static,
274    {
275        self.tool.hooks.on_input_delta =
276            Some(Arc::new(move |delta, ctx| Box::pin(function(delta, ctx))));
277        self
278    }
279
280    /// Hook called once the input is complete and valid.
281    #[must_use]
282    pub fn on_input_available<F, Fut>(mut self, function: F) -> Self
283    where
284        F: Fn(JsonValue, ToolContext) -> Fut + Send + Sync + 'static,
285        Fut: Future<Output = ()> + Send + 'static,
286    {
287        self.tool.hooks.on_input_available =
288            Some(Arc::new(move |input, ctx| Box::pin(function(input, ctx))));
289        self
290    }
291
292    /// Custom conversion of the output into what the model receives.
293    #[must_use]
294    pub fn to_model_output<F>(mut self, function: F) -> Self
295    where
296        F: Fn(ModelOutputArgs<'_>) -> ToolResultOutput + Send + Sync + 'static,
297    {
298        self.tool.to_model_output = Some(Arc::new(function));
299        self
300    }
301
302    /// Declares how this tool calls other tools.
303    #[must_use]
304    pub fn caller(mut self, definition: ToolCallerDefinition) -> Self {
305        self.tool.caller_definition = Some(definition);
306        self
307    }
308
309    /// Marks a provider-executed tool as supporting deferred results. Ignored
310    /// for other kinds.
311    #[must_use]
312    pub fn supports_deferred_results(mut self, supported: bool) -> Self {
313        if let ToolKind::ProviderExecuted {
314            supports_deferred_results,
315            ..
316        } = &mut self.tool.kind
317        {
318            *supports_deferred_results = supported;
319        }
320        self
321    }
322
323    /// Uses a custom executor.
324    #[must_use]
325    pub fn execute_with(mut self, executor: Arc<dyn ToolExecute>) -> Self {
326        self.tool.execute = Some(executor);
327        self
328    }
329
330    /// Finishes the tool.
331    #[must_use]
332    pub fn build(self) -> Tool {
333        self.tool
334    }
335}
336
337impl<I: DeserializeOwned + Send + 'static> ToolBuilder<I> {
338    /// Executes with an async closure returning a single result.
339    #[must_use]
340    pub fn execute<F, Fut, O>(mut self, function: F) -> Self
341    where
342        F: Fn(I, ToolContext) -> Fut + Send + Sync + 'static,
343        Fut: Future<Output = Result<O, ToolError>> + Send + 'static,
344        O: Serialize + 'static,
345    {
346        self.tool.execute = Some(Arc::new(FnExecute {
347            function,
348            _marker: PhantomData::<fn() -> (I, O)>,
349        }));
350        self
351    }
352
353    /// Executes with a closure returning a stream; every item becomes a
354    /// preliminary output and the last one is repeated as the final output.
355    #[must_use]
356    pub fn execute_stream<F, S, O>(mut self, function: F) -> Self
357    where
358        F: Fn(I, ToolContext) -> S + Send + Sync + 'static,
359        S: Stream<Item = Result<O, ToolError>> + Send + 'static,
360        O: Serialize + 'static,
361    {
362        self.tool.execute = Some(Arc::new(StreamExecute {
363            function,
364            _marker: PhantomData::<fn() -> (I, O)>,
365        }));
366        self
367    }
368}
369
370fn decode<I: DeserializeOwned>(input: JsonValue) -> Result<I, ToolError> {
371    serde_json::from_value(input).map_err(|error| {
372        ToolError::message(format!("invalid tool input: {error}")).with_cause(error)
373    })
374}
375
376fn encode<O: Serialize>(output: &O) -> Result<JsonValue, ToolError> {
377    serde_json::to_value(output).map_err(|error| {
378        ToolError::message(format!("tool output is not serializable: {error}")).with_cause(error)
379    })
380}
381
382struct FnExecute<I, O, F> {
383    function: F,
384    _marker: PhantomData<fn() -> (I, O)>,
385}
386
387impl<I, O, F, Fut> ToolExecute for FnExecute<I, O, F>
388where
389    I: DeserializeOwned + Send + 'static,
390    O: Serialize + 'static,
391    F: Fn(I, ToolContext) -> Fut + Send + Sync + 'static,
392    Fut: Future<Output = Result<O, ToolError>> + Send + 'static,
393{
394    fn execute(&self, input: JsonValue, ctx: ToolContext) -> ToolOutputStream {
395        let future = decode::<I>(input).map(|input| (self.function)(input, ctx));
396        Box::pin(futures_util::stream::once(async move {
397            let output = future?.await?;
398            encode(&output).map(ToolOutput::Final)
399        }))
400    }
401}
402
403struct StreamExecute<I, O, F> {
404    function: F,
405    _marker: PhantomData<fn() -> (I, O)>,
406}
407
408struct StreamState<S> {
409    inner: S,
410    last: Option<JsonValue>,
411    done: bool,
412}
413
414impl<I, O, F, S> ToolExecute for StreamExecute<I, O, F>
415where
416    I: DeserializeOwned + Send + 'static,
417    O: Serialize + 'static,
418    F: Fn(I, ToolContext) -> S + Send + Sync + 'static,
419    S: Stream<Item = Result<O, ToolError>> + Send + 'static,
420{
421    fn execute(&self, input: JsonValue, ctx: ToolContext) -> ToolOutputStream {
422        let inner = match decode::<I>(input) {
423            Ok(input) => (self.function)(input, ctx),
424            Err(error) => {
425                return Box::pin(futures_util::stream::once(std::future::ready(Err(error))));
426            }
427        };
428        let state = StreamState {
429            inner: Box::pin(inner),
430            last: None,
431            done: false,
432        };
433        Box::pin(futures_util::stream::unfold(
434            state,
435            |mut state| async move {
436                if state.done {
437                    return None;
438                }
439                match state.inner.next().await {
440                    Some(Ok(output)) => match encode(&output) {
441                        Ok(value) => {
442                            state.last = Some(value.clone());
443                            Some((Ok(ToolOutput::Preliminary(value)), state))
444                        }
445                        Err(error) => {
446                            state.done = true;
447                            Some((Err(error), state))
448                        }
449                    },
450                    Some(Err(error)) => {
451                        state.done = true;
452                        Some((Err(error), state))
453                    }
454                    None => {
455                        state.done = true;
456                        let value = state.last.take().unwrap_or(JsonValue::Null);
457                        Some((Ok(ToolOutput::Final(value)), state))
458                    }
459                }
460            },
461        ))
462    }
463}