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