1use std::fmt;
4use std::sync::Arc;
5
6use ferrin_schema::Schema;
7use ferrin_spec::BoxFuture;
8use ferrin_spec::JsonObject;
9use ferrin_spec::JsonValue;
10use ferrin_spec::ProviderOptions;
11use ferrin_spec::ToolCallId;
12use ferrin_spec::ToolDefinition;
13use ferrin_spec::ToolName;
14use ferrin_spec::error::TypeValidationContext;
15use ferrin_spec::error::TypeValidationError;
16use ferrin_spec::language_model::prompt::ToolResultOutput;
17
18use crate::callers::ToolCallerDefinition;
19use crate::execute::ToolContext;
20use crate::execute::ToolExecute;
21use crate::execute::ToolOutputStream;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum ToolKind {
27 Function,
30 Dynamic,
32 ProviderDefined {
34 id: String,
36 args: JsonObject,
38 },
39 ProviderExecuted {
41 id: String,
43 args: JsonObject,
45 supports_deferred_results: bool,
47 },
48}
49
50impl ToolKind {
51 #[must_use]
53 pub fn is_provider(&self) -> bool {
54 matches!(
55 self,
56 Self::ProviderDefined { .. } | Self::ProviderExecuted { .. }
57 )
58 }
59
60 #[must_use]
62 pub fn is_provider_executed(&self) -> bool {
63 matches!(self, Self::ProviderExecuted { .. })
64 }
65
66 #[must_use]
68 pub fn is_dynamic(&self) -> bool {
69 matches!(self, Self::Dynamic)
70 }
71
72 #[must_use]
74 pub fn provider_id(&self) -> Option<&str> {
75 match self {
76 Self::ProviderDefined { id, .. } | Self::ProviderExecuted { id, .. } => Some(id),
77 _ => None,
78 }
79 }
80}
81
82#[derive(Clone, Default)]
84pub struct DescriptionContext {
85 pub tool_context: Option<JsonValue>,
87 #[cfg(feature = "sandbox")]
89 pub sandbox: Option<Arc<dyn crate::sandbox::Sandbox>>,
90}
91
92impl DescriptionContext {
93 #[must_use]
95 pub fn with_tool_context(tool_context: JsonValue) -> Self {
96 Self {
97 tool_context: Some(tool_context),
98 #[cfg(feature = "sandbox")]
99 sandbox: None,
100 }
101 }
102}
103
104impl fmt::Debug for DescriptionContext {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 let mut debug = f.debug_struct("DescriptionContext");
107 debug.field("tool_context", &self.tool_context);
108 #[cfg(feature = "sandbox")]
109 debug.field(
110 "sandbox",
111 &self.sandbox.as_ref().map(|sandbox| sandbox.description()),
112 );
113 debug.finish()
114 }
115}
116
117pub type DescriptionFn =
119 Arc<dyn Fn(DescriptionContext) -> BoxFuture<'static, String> + Send + Sync>;
120
121#[derive(Clone)]
123#[non_exhaustive]
124pub enum Description {
125 Static(String),
127 Dynamic(DescriptionFn),
129}
130
131impl fmt::Debug for Description {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 match self {
134 Self::Static(text) => f.debug_tuple("Static").field(text).finish(),
135 Self::Dynamic(_) => f.write_str("Dynamic(..)"),
136 }
137 }
138}
139
140impl Description {
141 pub async fn resolve(&self, ctx: DescriptionContext) -> String {
143 match self {
144 Self::Static(text) => text.clone(),
145 Self::Dynamic(function) => function(ctx).await,
146 }
147 }
148
149 #[must_use]
151 pub fn as_static(&self) -> Option<&str> {
152 match self {
153 Self::Static(text) => Some(text),
154 Self::Dynamic(_) => None,
155 }
156 }
157}
158
159pub type ApprovalFn = Arc<dyn Fn(JsonValue, ToolContext) -> BoxFuture<'static, bool> + Send + Sync>;
161
162#[derive(Clone, Default)]
165#[non_exhaustive]
166pub enum NeedsApproval {
167 #[default]
169 Never,
170 Always,
172 Dynamic(ApprovalFn),
174}
175
176impl fmt::Debug for NeedsApproval {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 match self {
179 Self::Never => f.write_str("Never"),
180 Self::Always => f.write_str("Always"),
181 Self::Dynamic(_) => f.write_str("Dynamic(..)"),
182 }
183 }
184}
185
186impl NeedsApproval {
187 pub async fn resolve(&self, input: JsonValue, ctx: ToolContext) -> bool {
189 match self {
190 Self::Never => false,
191 Self::Always => true,
192 Self::Dynamic(function) => function(input, ctx).await,
193 }
194 }
195
196 #[must_use]
198 pub fn is_declared(&self) -> bool {
199 !matches!(self, Self::Never)
200 }
201}
202
203pub type InputStartHook = Arc<dyn Fn(ToolContext) -> BoxFuture<'static, ()> + Send + Sync>;
205pub type InputDeltaHook = Arc<dyn Fn(String, ToolContext) -> BoxFuture<'static, ()> + Send + Sync>;
207pub type InputAvailableHook =
209 Arc<dyn Fn(JsonValue, ToolContext) -> BoxFuture<'static, ()> + Send + Sync>;
210
211#[derive(Clone, Default)]
213pub struct ToolHooks {
214 pub on_input_start: Option<InputStartHook>,
216 pub on_input_delta: Option<InputDeltaHook>,
218 pub on_input_available: Option<InputAvailableHook>,
220}
221
222impl fmt::Debug for ToolHooks {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 f.debug_struct("ToolHooks")
225 .field("on_input_start", &self.on_input_start.is_some())
226 .field("on_input_delta", &self.on_input_delta.is_some())
227 .field("on_input_available", &self.on_input_available.is_some())
228 .finish()
229 }
230}
231
232#[derive(Debug, Clone, Copy)]
234pub struct ModelOutputArgs<'a> {
235 pub tool_call_id: &'a ToolCallId,
237 pub input: &'a JsonValue,
239 pub output: &'a JsonValue,
241}
242
243pub type ToModelOutputFn = Arc<dyn Fn(ModelOutputArgs<'_>) -> ToolResultOutput + Send + Sync>;
245
246#[derive(Clone)]
249pub struct Tool {
250 pub(crate) kind: ToolKind,
251 pub(crate) description: Option<Description>,
252 pub(crate) title: Option<String>,
253 pub(crate) input_schema: Schema<JsonValue>,
254 pub(crate) output_schema: Option<Schema<JsonValue>>,
255 pub(crate) context_schema: Option<Schema<JsonValue>>,
256 pub(crate) execute: Option<Arc<dyn ToolExecute>>,
257 pub(crate) needs_approval: NeedsApproval,
258 pub(crate) strict: Option<bool>,
259 pub(crate) input_examples: Vec<JsonObject>,
260 pub(crate) metadata: Option<JsonObject>,
261 pub(crate) provider_options: Option<ProviderOptions>,
262 pub(crate) hooks: ToolHooks,
263 pub(crate) to_model_output: Option<ToModelOutputFn>,
264 pub(crate) caller_definition: Option<ToolCallerDefinition>,
265}
266
267impl fmt::Debug for Tool {
268 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
269 f.debug_struct("Tool")
270 .field("kind", &self.kind)
271 .field("description", &self.description)
272 .field("title", &self.title)
273 .field("input_schema", self.input_schema.json_schema())
274 .field("has_output_schema", &self.output_schema.is_some())
275 .field("has_context_schema", &self.context_schema.is_some())
276 .field("executable", &self.execute.is_some())
277 .field("needs_approval", &self.needs_approval)
278 .field("strict", &self.strict)
279 .field("input_examples", &self.input_examples)
280 .field("metadata", &self.metadata)
281 .field("provider_options", &self.provider_options)
282 .field("hooks", &self.hooks)
283 .field("has_to_model_output", &self.to_model_output.is_some())
284 .field("caller_definition", &self.caller_definition)
285 .finish()
286 }
287}
288
289impl Tool {
290 #[must_use]
292 pub fn kind(&self) -> &ToolKind {
293 &self.kind
294 }
295
296 #[must_use]
298 pub fn description(&self) -> Option<&Description> {
299 self.description.as_ref()
300 }
301
302 #[must_use]
304 pub fn title(&self) -> Option<&str> {
305 self.title.as_deref()
306 }
307
308 #[must_use]
310 pub fn input_schema(&self) -> &Schema<JsonValue> {
311 &self.input_schema
312 }
313
314 #[must_use]
316 pub fn output_schema(&self) -> Option<&Schema<JsonValue>> {
317 self.output_schema.as_ref()
318 }
319
320 #[must_use]
322 pub fn context_schema(&self) -> Option<&Schema<JsonValue>> {
323 self.context_schema.as_ref()
324 }
325
326 #[must_use]
328 pub fn executor(&self) -> Option<&Arc<dyn ToolExecute>> {
329 self.execute.as_ref()
330 }
331
332 #[must_use]
334 pub fn is_executable(&self) -> bool {
335 self.execute.is_some()
336 }
337
338 #[must_use]
340 pub fn needs_approval(&self) -> &NeedsApproval {
341 &self.needs_approval
342 }
343
344 #[must_use]
346 pub fn strict(&self) -> Option<bool> {
347 self.strict
348 }
349
350 #[must_use]
352 pub fn input_examples(&self) -> &[JsonObject] {
353 &self.input_examples
354 }
355
356 #[must_use]
358 pub fn metadata(&self) -> Option<&JsonObject> {
359 self.metadata.as_ref()
360 }
361
362 #[must_use]
364 pub fn provider_options(&self) -> Option<&ProviderOptions> {
365 self.provider_options.as_ref()
366 }
367
368 #[must_use]
370 pub fn hooks(&self) -> &ToolHooks {
371 &self.hooks
372 }
373
374 #[must_use]
376 pub fn to_model_output(&self) -> Option<&ToModelOutputFn> {
377 self.to_model_output.as_ref()
378 }
379
380 #[must_use]
382 pub fn caller_definition(&self) -> Option<&ToolCallerDefinition> {
383 self.caller_definition.as_ref()
384 }
385
386 #[must_use]
388 pub fn with_provider_options(mut self, provider_options: Option<ProviderOptions>) -> Self {
389 self.provider_options = provider_options;
390 self
391 }
392
393 pub async fn resolve_description(&self, ctx: DescriptionContext) -> Option<String> {
395 match &self.description {
396 Some(description) => Some(description.resolve(ctx).await),
397 None => None,
398 }
399 }
400
401 #[must_use]
403 pub fn definition(&self, name: ToolName, description: Option<String>) -> ToolDefinition {
404 match &self.kind {
405 ToolKind::Function | ToolKind::Dynamic => ToolDefinition::Function {
406 name,
407 description,
408 input_schema: self.input_schema.json_schema().clone(),
409 strict: self.strict,
410 input_examples: self.input_examples.clone(),
411 provider_options: self.provider_options.clone(),
412 },
413 ToolKind::ProviderDefined { id, args }
414 | ToolKind::ProviderExecuted { id, args, .. } => ToolDefinition::Provider {
415 id: id.clone(),
416 name,
417 args: args.clone(),
418 },
419 }
420 }
421
422 pub fn validate_input(
429 &self,
430 name: &ToolName,
431 input: JsonValue,
432 ) -> Result<JsonValue, TypeValidationError> {
433 self.input_schema.validate(input).map_err(|error| {
434 error.with_context(TypeValidationContext {
435 field: Some("tool input".to_owned()),
436 entity_name: Some(name.as_str().to_owned()),
437 entity_id: None,
438 })
439 })
440 }
441
442 pub fn validate_context(
449 &self,
450 name: &ToolName,
451 context: Option<JsonValue>,
452 ) -> Result<Option<JsonValue>, TypeValidationError> {
453 let Some(schema) = &self.context_schema else {
454 return Ok(None);
455 };
456 schema
457 .validate(context.unwrap_or(JsonValue::Null))
458 .map(Some)
459 .map_err(|error| {
460 error.with_context(TypeValidationContext {
461 field: Some("tool context".to_owned()),
462 entity_name: Some(name.as_str().to_owned()),
463 entity_id: None,
464 })
465 })
466 }
467
468 #[must_use]
470 pub fn execute(&self, input: JsonValue, ctx: ToolContext) -> Option<ToolOutputStream> {
471 self.execute
472 .as_ref()
473 .map(|executor| executor.execute(input, ctx))
474 }
475}