1#[cfg(feature = "derive")]
208pub use aither_derive::tool;
209use alloc::borrow::Cow;
210use serde_json::Value;
211
212use crate::Result;
213use alloc::format;
214use alloc::string::{String, ToString};
215use alloc::vec::Vec;
216use alloc::{boxed::Box, collections::BTreeMap};
217use core::any::Any;
218use core::fmt::{Debug, Display};
219use core::{future::Future, pin::Pin};
220pub use mime::Mime;
221use schemars::{JsonSchema, Schema, schema_for};
222use serde::de::DeserializeOwned;
223
224mod command;
225mod context;
226
227pub use command::{CommandOutcome, CommandResult, CommandStream, StoredFile};
228pub use context::{Progress, ProgressError, ProgressSink, ToolContext};
229
230#[derive(Debug, Clone, PartialEq, Eq)]
249#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
250#[cfg_attr(feature = "serde", serde(tag = "kind", rename_all = "snake_case"))]
251pub enum ToolResult {
252 Done,
254
255 Text {
257 text: String,
259 },
260
261 Tsv {
263 text: String,
265 },
266
267 Binary {
269 mime: String,
271 content: Vec<u8>,
273 },
274
275 Error {
277 message: String,
279 },
280
281 Command(Box<CommandResult>),
287
288 Parts {
295 parts: Vec<ToolResultPart>,
297 },
298}
299
300#[derive(Debug, Clone, PartialEq, Eq)]
305#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
306#[cfg_attr(feature = "serde", serde(tag = "kind", rename_all = "snake_case"))]
307pub enum ToolResultPart {
308 Text {
310 text: String,
312 },
313
314 Tsv {
316 text: String,
318 },
319
320 Binary {
322 mime: String,
324 content: Vec<u8>,
326 },
327}
328
329impl ToolResult {
330 #[must_use]
332 pub fn text(s: impl Into<String>) -> Self {
333 Self::Text { text: s.into() }
334 }
335
336 #[must_use]
338 pub fn tsv(s: impl Into<String>) -> Self {
339 Self::Tsv { text: s.into() }
340 }
341
342 #[must_use]
344 pub fn image(data: Vec<u8>, media_type: &str) -> Self {
345 Self::Binary {
346 mime: parse_media_type_or_octet_stream(media_type),
347 content: data,
348 }
349 }
350
351 #[must_use]
353 pub fn binary(data: Vec<u8>) -> Self {
354 Self::Binary {
355 mime: mime::APPLICATION_OCTET_STREAM.essence_str().to_string(),
356 content: data,
357 }
358 }
359
360 #[must_use]
362 pub fn error(message: impl Into<String>) -> Self {
363 Self::Error {
364 message: message.into(),
365 }
366 }
367
368 #[must_use]
374 pub fn parts(parts: Vec<ToolResultPart>) -> Self {
375 match <[ToolResultPart; 1]>::try_from(parts) {
376 Ok([part]) => part.into_result(),
377 Err(parts) if parts.is_empty() => Self::Done,
378 Err(parts) => Self::Parts { parts },
379 }
380 }
381
382 #[must_use]
384 pub const fn is_done(&self) -> bool {
385 matches!(self, Self::Done)
386 }
387
388 #[must_use]
390 pub const fn is_error(&self) -> bool {
391 matches!(self, Self::Error { .. })
392 }
393
394 #[must_use]
396 pub fn as_text(&self) -> Option<&str> {
397 match self {
398 Self::Text { text } | Self::Tsv { text } => Some(text),
399 Self::Error { message } => Some(message),
400 Self::Done | Self::Binary { .. } | Self::Command(_) | Self::Parts { .. } => None,
401 }
402 }
403
404 #[must_use]
406 pub fn command(&self) -> Option<&CommandResult> {
407 match self {
408 Self::Command(command) => Some(command.as_ref()),
409 _ => None,
410 }
411 }
412
413 #[must_use]
415 pub fn error_message(&self) -> Option<&str> {
416 match self {
417 Self::Error { message } => Some(message),
418 Self::Done
419 | Self::Text { .. }
420 | Self::Tsv { .. }
421 | Self::Binary { .. }
422 | Self::Command(_)
423 | Self::Parts { .. } => None,
424 }
425 }
426
427 #[must_use]
429 pub fn render_for_model(&self) -> String {
430 match self {
431 Self::Done => String::new(),
432 Self::Text { text } | Self::Tsv { text } => text.clone(),
433 Self::Command(command) => command.render_for_model(),
434 Self::Binary { mime, content } => render_binary_placeholder(mime, content),
435 Self::Error { message } => message.clone(),
436 Self::Parts { parts } => join_part_renders(parts),
437 }
438 }
439
440 #[must_use]
445 pub fn mime(&self) -> Option<Mime> {
446 match self {
447 Self::Binary { mime, .. } => mime.parse().ok(),
448 Self::Parts { parts } => match parts.as_slice() {
449 [ToolResultPart::Binary { mime, .. }] => mime.parse().ok(),
450 _ => None,
451 },
452 Self::Done
453 | Self::Text { .. }
454 | Self::Tsv { .. }
455 | Self::Command(_)
456 | Self::Error { .. } => None,
457 }
458 }
459
460 #[must_use]
464 pub fn content(&self) -> Option<&[u8]> {
465 match self {
466 Self::Binary { content, .. } => Some(content),
467 Self::Parts { parts } => match parts.as_slice() {
468 [ToolResultPart::Binary { content, .. }] => Some(content),
469 _ => None,
470 },
471 Self::Done
472 | Self::Text { .. }
473 | Self::Tsv { .. }
474 | Self::Command(_)
475 | Self::Error { .. } => None,
476 }
477 }
478}
479
480impl ToolResultPart {
481 #[must_use]
483 pub fn text(s: impl Into<String>) -> Self {
484 Self::Text { text: s.into() }
485 }
486
487 #[must_use]
489 pub fn tsv(s: impl Into<String>) -> Self {
490 Self::Tsv { text: s.into() }
491 }
492
493 #[must_use]
495 pub fn image(data: Vec<u8>, media_type: &str) -> Self {
496 Self::Binary {
497 mime: parse_media_type_or_octet_stream(media_type),
498 content: data,
499 }
500 }
501
502 #[must_use]
504 pub fn binary(data: Vec<u8>) -> Self {
505 Self::Binary {
506 mime: mime::APPLICATION_OCTET_STREAM.essence_str().to_string(),
507 content: data,
508 }
509 }
510
511 #[must_use]
513 pub fn as_text(&self) -> Option<&str> {
514 match self {
515 Self::Text { text } | Self::Tsv { text } => Some(text),
516 Self::Binary { .. } => None,
517 }
518 }
519
520 #[must_use]
522 pub fn render_for_model(&self) -> String {
523 match self {
524 Self::Text { text } | Self::Tsv { text } => text.clone(),
525 Self::Binary { mime, content } => render_binary_placeholder(mime, content),
526 }
527 }
528
529 #[must_use]
531 pub fn mime(&self) -> Option<Mime> {
532 match self {
533 Self::Binary { mime, .. } => mime.parse().ok(),
534 Self::Text { .. } | Self::Tsv { .. } => None,
535 }
536 }
537
538 #[must_use]
540 pub fn content(&self) -> Option<&[u8]> {
541 match self {
542 Self::Binary { content, .. } => Some(content),
543 Self::Text { .. } | Self::Tsv { .. } => None,
544 }
545 }
546
547 #[must_use]
549 pub fn into_result(self) -> ToolResult {
550 match self {
551 Self::Text { text } | Self::Tsv { text } => ToolResult::Text { text },
552 Self::Binary { mime, content } => ToolResult::Binary { mime, content },
553 }
554 }
555}
556
557fn render_binary_placeholder(mime: &str, content: &[u8]) -> String {
558 let mut rendered = String::new();
559 rendered.push_str("[binary tool result: ");
560 rendered.push_str(mime);
561 rendered.push_str(", ");
562 rendered.push_str(content.len().to_string().as_str());
563 rendered.push_str(" bytes]");
564 rendered
565}
566
567fn join_part_renders(parts: &[ToolResultPart]) -> String {
568 let mut rendered = String::new();
569 for part in parts {
570 let text = part.render_for_model();
571 if text.is_empty() {
572 continue;
573 }
574 if !rendered.is_empty() {
575 rendered.push('\n');
576 }
577 rendered.push_str(&text);
578 }
579 rendered
580}
581
582pub trait IntoToolResult {
588 fn into_tool_result(self) -> Result<ToolResult>;
594}
595
596impl IntoToolResult for ToolResult {
597 fn into_tool_result(self) -> Result<ToolResult> {
598 Ok(self)
599 }
600}
601
602impl IntoToolResult for CommandResult {
603 fn into_tool_result(self) -> Result<ToolResult> {
604 Ok(ToolResult::Command(Box::new(self)))
605 }
606}
607
608impl IntoToolResult for () {
609 fn into_tool_result(self) -> Result<ToolResult> {
610 Ok(ToolResult::Done)
611 }
612}
613
614impl IntoToolResult for String {
615 fn into_tool_result(self) -> Result<ToolResult> {
616 Ok(ToolResult::text(self))
617 }
618}
619
620impl IntoToolResult for &str {
621 fn into_tool_result(self) -> Result<ToolResult> {
622 Ok(ToolResult::text(self))
623 }
624}
625
626impl IntoToolResult for Cow<'_, str> {
627 fn into_tool_result(self) -> Result<ToolResult> {
628 Ok(ToolResult::text(self.into_owned()))
629 }
630}
631
632impl<T> IntoToolResult for Option<T>
633where
634 T: IntoToolResult,
635{
636 fn into_tool_result(self) -> Result<ToolResult> {
637 self.map_or_else(|| Ok(ToolResult::Done), IntoToolResult::into_tool_result)
638 }
639}
640
641impl IntoToolResult for Vec<ToolResultPart> {
642 fn into_tool_result(self) -> Result<ToolResult> {
643 Ok(ToolResult::parts(self))
644 }
645}
646
647impl<T, E> IntoToolResult for core::result::Result<T, E>
648where
649 T: IntoToolResult,
650 E: Display,
651{
652 fn into_tool_result(self) -> Result<ToolResult> {
653 match self {
654 Ok(value) => value.into_tool_result(),
655 Err(error) => Ok(ToolResult::error(error.to_string())),
656 }
657 }
658}
659
660fn parse_media_type_or_octet_stream(media_type: &str) -> String {
661 media_type
662 .parse::<Mime>()
663 .unwrap_or(mime::APPLICATION_OCTET_STREAM)
664 .essence_str()
665 .to_string()
666}
667
668pub trait Tool: Send + Sync {
708 fn name(&self) -> Cow<'static, str>;
710
711 fn description(&self) -> Cow<'static, str> {
721 description_from_schema::<Self::Arguments>().unwrap_or_default()
722 }
723
724 type Arguments: Send + JsonSchema + DeserializeOwned;
727
728 type Res: IntoToolResult + Send;
730
731 fn call(
742 &self,
743 arguments: Self::Arguments,
744 cx: ToolContext,
745 ) -> impl Future<Output = Result<Self::Res>> + Send;
746}
747
748trait ToolImpl: Send + Sync + Any {
749 fn call(
750 &self,
751 args: &str,
752 cx: ToolContext,
753 ) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send + '_>>;
754
755 fn definition(&self) -> &ToolDefinition;
758
759 fn as_any(&self) -> &dyn Any;
764
765 fn as_any_mut(&mut self) -> &mut dyn Any;
767}
768
769struct DynToolImpl<F>
771where
772 F: Fn(&str, ToolContext) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send>>
773 + Send
774 + Sync,
775{
776 definition: ToolDefinition,
777 handler: F,
778}
779
780impl<F> ToolImpl for DynToolImpl<F>
781where
782 F: Fn(&str, ToolContext) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send>>
783 + Send
784 + Sync
785 + 'static,
786{
787 fn call(
788 &self,
789 args: &str,
790 cx: ToolContext,
791 ) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send + '_>> {
792 (self.handler)(args, cx)
793 }
794
795 fn definition(&self) -> &ToolDefinition {
796 &self.definition
797 }
798
799 fn as_any(&self) -> &dyn Any {
800 self
801 }
802
803 fn as_any_mut(&mut self) -> &mut dyn Any {
804 self
805 }
806}
807
808fn schema_is_object(value: &Value) -> bool {
811 matches!(value.get("type").and_then(Value::as_str), Some("object"))
812 || value.get("properties").is_some()
813 || value.get("oneOf").is_some()
814 || value.get("anyOf").is_some()
815 || value.get("$defs").is_some()
816}
817
818fn is_object<T: JsonSchema>() -> bool {
819 schema_is_object(&schema_for!(T).to_value())
820}
821
822fn arguments_schema<T: JsonSchema>() -> Schema {
825 if is_object::<T>() {
826 schema_for!(T)
827 } else {
828 schema_for!(ToolArgument<T>)
829 }
830}
831
832fn description_from_schema<T: JsonSchema>() -> Option<Cow<'static, str>> {
834 schema_for!(T)
835 .to_value()
836 .get("description")
837 .and_then(Value::as_str)
838 .filter(|text| !text.trim().is_empty())
839 .map(|text| Cow::Owned(text.to_string()))
840}
841
842struct RegisteredTool<T: Tool> {
848 tool: T,
849 definition: ToolDefinition,
850 args_are_object: bool,
851}
852
853impl<T: Tool> RegisteredTool<T> {
854 fn new(tool: T) -> Self {
855 let definition = ToolDefinition::new(&tool);
856 let args_are_object = is_object::<T::Arguments>();
857 Self {
858 tool,
859 definition,
860 args_are_object,
861 }
862 }
863}
864
865#[derive(askama::Template)]
867#[template(path = "invalid_arguments.txt", escape = "none")]
868struct InvalidArguments<'a> {
869 tool: &'a str,
870 error: &'a serde_json::Error,
871}
872
873impl<T: Tool + 'static> ToolImpl for RegisteredTool<T> {
874 fn call(
875 &self,
876 args: &str,
877 cx: ToolContext,
878 ) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send + '_>> {
879 let result = if self.args_are_object {
880 serde_json::from_str::<T::Arguments>(args)
881 } else {
882 serde_json::from_str::<ToolArgument<T::Arguments>>(args).map(|wrapper| wrapper.value)
883 };
884
885 let arguments = match result {
886 Ok(arguments) => arguments,
887 Err(error) => {
888 let message = InvalidArguments {
889 tool: self.definition.name(),
890 error: &error,
891 }
892 .to_string();
893 return Box::pin(async move { Err(anyhow::Error::msg(message)) });
894 }
895 };
896
897 Box::pin(async move {
898 Tool::call(&self.tool, arguments, cx)
899 .await?
900 .into_tool_result()
901 })
902 }
903
904 fn definition(&self) -> &ToolDefinition {
905 &self.definition
906 }
907
908 fn as_any(&self) -> &dyn Any {
909 &self.tool
910 }
911
912 fn as_any_mut(&mut self) -> &mut dyn Any {
913 &mut self.tool
914 }
915}
916
917#[derive(Debug, Clone, PartialEq, Eq)]
922pub struct InvalidSchema {
923 name: Cow<'static, str>,
925}
926
927impl InvalidSchema {
928 #[must_use]
930 pub fn name(&self) -> &str {
931 &self.name
932 }
933}
934
935impl Display for InvalidSchema {
936 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
937 write!(
938 f,
939 "tool '{}' has an argument schema that is neither an object nor a boolean",
940 self.name
941 )
942 }
943}
944
945impl core::error::Error for InvalidSchema {}
946
947#[derive(Debug, Clone, PartialEq, Eq)]
949pub enum RegisterError {
950 DuplicateName(Cow<'static, str>),
954
955 EmptyDescription(Cow<'static, str>),
960}
961
962impl Display for RegisterError {
963 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
964 match self {
965 Self::DuplicateName(name) => {
966 write!(f, "a tool named '{name}' is already registered")
967 }
968 Self::EmptyDescription(name) => write!(
969 f,
970 "tool '{name}' has an empty description; add a rustdoc comment to its \
971 Arguments type or implement Tool::description"
972 ),
973 }
974 }
975}
976
977impl core::error::Error for RegisterError {}
978
979pub struct Tools {
995 tools: BTreeMap<Cow<'static, str>, Box<dyn ToolImpl>>,
996}
997
998impl Debug for Tools {
999 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1000 f.debug_struct("Tools")
1001 .field("tools", &self.tools.keys().collect::<Vec<_>>())
1002 .finish()
1003 }
1004}
1005
1006#[derive(Debug, Clone, PartialEq)]
1010#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1011pub struct ToolDefinition {
1012 name: Cow<'static, str>,
1014 description: Cow<'static, str>,
1016 arguments: Schema,
1018}
1019
1020impl ToolDefinition {
1021 #[must_use]
1026 pub fn new<T: Tool>(tool: &T) -> Self {
1027 Self {
1028 name: tool.name(),
1029 description: tool.description(),
1030 arguments: arguments_schema::<T::Arguments>(),
1031 }
1032 }
1033
1034 pub fn from_parts(
1045 name: Cow<'static, str>,
1046 description: Cow<'static, str>,
1047 schema: Value,
1048 ) -> core::result::Result<Self, InvalidSchema> {
1049 let arguments: Schema = schema
1050 .try_into()
1051 .map_err(|_| InvalidSchema { name: name.clone() })?;
1052
1053 Ok(Self {
1054 name,
1055 description,
1056 arguments,
1057 })
1058 }
1059
1060 #[must_use]
1062 pub fn name(&self) -> &str {
1063 &self.name
1064 }
1065
1066 #[must_use]
1068 pub fn description(&self) -> &str {
1069 &self.description
1070 }
1071
1072 #[must_use]
1076 pub fn arguments_openai_schema(&self) -> serde_json::Value {
1077 let mut inner = self.arguments.clone().to_value();
1078 clean_schema(&mut inner);
1079
1080 inner
1081 }
1082}
1083
1084#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
1085struct ToolArgument<T> {
1086 value: T,
1087}
1088
1089fn clean_schema(value: &mut Value) {
1090 let defs = extract_defs(value);
1092
1093 resolve_and_clean(value, &defs);
1095
1096 if let Value::Object(map) = value {
1098 map.remove("description");
1101
1102 if map.contains_key("properties") && !map.contains_key("type") {
1104 map.insert("type".to_string(), Value::String("object".to_string()));
1105 }
1106 }
1107}
1108
1109fn extract_defs(value: &Value) -> serde_json::Map<String, Value> {
1111 if let Value::Object(map) = value
1112 && let Some(Value::Object(defs)) = map.get("$defs").or_else(|| map.get("definitions"))
1113 {
1114 return defs.clone();
1115 }
1116 serde_json::Map::new()
1117}
1118
1119#[allow(clippy::too_many_lines)]
1121fn resolve_and_clean(value: &mut Value, defs: &serde_json::Map<String, Value>) {
1122 resolve_and_clean_inner(value, defs, false);
1123}
1124
1125#[allow(clippy::too_many_lines)]
1127fn resolve_and_clean_inner(
1128 value: &mut Value,
1129 defs: &serde_json::Map<String, Value>,
1130 inside_properties: bool,
1131) {
1132 match value {
1133 Value::Object(map) => {
1134 if let Some(Value::String(ref_path)) = map.remove("$ref")
1136 && let Some(Value::Object(resolved_map)) = resolve_ref(&ref_path, defs)
1137 {
1138 let existing_description = map.remove("description");
1141 for (k, v) in resolved_map {
1142 map.entry(k).or_insert(v);
1143 }
1144 if let Some(desc) = existing_description {
1146 map.insert("description".to_string(), desc);
1147 }
1148 }
1149
1150 if let Some(const_val) = map.remove("const") {
1152 map.insert("enum".to_string(), Value::Array(alloc::vec![const_val]));
1153 }
1154
1155 if let Some(Value::Array(variants)) =
1157 map.remove("oneOf").or_else(|| map.remove("anyOf"))
1158 {
1159 let is_simple_enum = variants.iter().all(|v| {
1161 if let Value::Object(vm) = v {
1162 (vm.contains_key("const") || vm.contains_key("enum"))
1163 && !vm.contains_key("properties")
1164 } else {
1165 false
1166 }
1167 });
1168
1169 if is_simple_enum {
1170 let mut enum_values: alloc::vec::Vec<Value> = alloc::vec::Vec::new();
1172 let mut variant_type: Option<String> = None;
1173
1174 for variant in &variants {
1175 if let Value::Object(vm) = variant {
1176 if let Some(const_val) = vm.get("const")
1177 && !enum_values.contains(const_val)
1178 {
1179 enum_values.push(const_val.clone());
1180 }
1181 if let Some(Value::Array(arr)) = vm.get("enum") {
1182 for val in arr {
1183 if !enum_values.contains(val) {
1184 enum_values.push(val.clone());
1185 }
1186 }
1187 }
1188 if variant_type.is_none()
1189 && let Some(Value::String(t)) = vm.get("type")
1190 {
1191 variant_type = Some(t.clone());
1192 }
1193 }
1194 }
1195
1196 if !enum_values.is_empty() {
1197 map.insert("enum".to_string(), Value::Array(enum_values));
1198 if let Some(t) = variant_type {
1199 map.insert("type".to_string(), Value::String(t));
1200 }
1201 }
1202 } else {
1203 let mut all_properties = serde_json::Map::new();
1205
1206 for variant in variants {
1207 if let Value::Object(variant_map) = variant
1208 && let Some(Value::Object(props)) = variant_map.get("properties")
1209 {
1210 for (key, val) in props {
1211 let new_values: Option<alloc::vec::Vec<Value>> =
1213 if let Value::Object(val_obj) = val {
1214 if let Some(Value::Array(arr)) = val_obj.get("enum") {
1215 Some(arr.clone())
1216 } else {
1217 val_obj
1218 .get("const")
1219 .map(|const_val| alloc::vec![const_val.clone()])
1220 }
1221 } else {
1222 None
1223 };
1224
1225 if all_properties.contains_key(key) {
1226 if let Some(values) = new_values
1228 && let Some(Value::Object(existing_obj)) =
1229 all_properties.get_mut(key)
1230 && let Some(Value::Array(existing_enum)) =
1231 existing_obj.get_mut("enum")
1232 {
1233 for e in values {
1234 if !existing_enum.contains(&e) {
1235 existing_enum.push(e);
1236 }
1237 }
1238 }
1239 } else {
1240 let mut val_clone = val.clone();
1242 if let Value::Object(obj) = &mut val_clone
1243 && let Some(const_val) = obj.remove("const")
1244 {
1245 obj.insert(
1246 "enum".to_string(),
1247 Value::Array(alloc::vec![const_val]),
1248 );
1249 }
1250 all_properties.insert(key.clone(), val_clone);
1251 }
1252 }
1253 }
1254 }
1255
1256 if !all_properties.is_empty() {
1258 map.insert("type".to_string(), Value::String("object".to_string()));
1259 map.insert("properties".to_string(), Value::Object(all_properties));
1260 }
1261 }
1262 }
1263
1264 if !inside_properties {
1267 let allowed = [
1268 "type",
1269 "description",
1270 "properties",
1271 "required",
1272 "items",
1273 "enum",
1274 "nullable",
1275 ];
1276 map.retain(|k, _| allowed.contains(&k.as_str()));
1277 }
1278
1279 if let Some(Value::Array(types)) = map.get("type") {
1281 let non_null: Vec<&Value> = types
1283 .iter()
1284 .filter(|t| !matches!(t, Value::String(s) if s == "null"))
1285 .collect();
1286 if non_null.len() == 1 {
1287 map.insert("type".to_string(), non_null[0].clone());
1288 }
1289 }
1290
1291 for (key, v) in map.iter_mut() {
1293 let child_inside_props = key == "properties";
1295 resolve_and_clean_inner(v, defs, child_inside_props);
1296 }
1297 }
1298 Value::Array(arr) => {
1299 for v in arr {
1300 resolve_and_clean_inner(v, defs, false);
1301 }
1302 }
1303 _ => {}
1304 }
1305}
1306
1307fn resolve_ref(ref_path: &str, defs: &serde_json::Map<String, Value>) -> Option<Value> {
1309 let name = ref_path
1311 .strip_prefix("#/$defs/")
1312 .or_else(|| ref_path.strip_prefix("#/definitions/"))?;
1313
1314 defs.get(name).cloned()
1315}
1316
1317impl Default for Tools {
1318 fn default() -> Self {
1319 Self::new()
1320 }
1321}
1322
1323impl Tools {
1324 #[must_use]
1326 pub const fn new() -> Self {
1327 Self {
1328 tools: BTreeMap::new(),
1329 }
1330 }
1331
1332 #[must_use]
1336 pub fn get<T>(&self) -> Option<&T>
1337 where
1338 T: Tool + 'static,
1339 {
1340 self.tools
1341 .values()
1342 .find_map(|tool| tool.as_any().downcast_ref::<T>())
1343 }
1344
1345 #[must_use]
1349 pub fn get_mut<T>(&mut self) -> Option<&mut T>
1350 where
1351 T: Tool + 'static,
1352 {
1353 self.tools
1354 .values_mut()
1355 .find_map(|tool| tool.as_any_mut().downcast_mut::<T>())
1356 }
1357
1358 #[must_use]
1360 pub fn definitions(&self) -> Vec<ToolDefinition> {
1361 self.tools
1362 .values()
1363 .map(|tool| tool.definition().clone())
1364 .collect()
1365 }
1366
1367 pub fn register<T: Tool + 'static>(
1378 &mut self,
1379 tool: T,
1380 ) -> core::result::Result<(), RegisterError> {
1381 self.insert(Box::new(RegisteredTool::new(tool)))
1382 }
1383
1384 pub fn register_dyn<F>(
1394 &mut self,
1395 definition: ToolDefinition,
1396 handler: F,
1397 ) -> core::result::Result<(), RegisterError>
1398 where
1399 F: Fn(&str, ToolContext) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send>>
1400 + Send
1401 + Sync
1402 + 'static,
1403 {
1404 self.insert(Box::new(DynToolImpl {
1405 definition,
1406 handler,
1407 }))
1408 }
1409
1410 fn insert(&mut self, tool: Box<dyn ToolImpl>) -> core::result::Result<(), RegisterError> {
1411 let name = tool.definition().name.clone();
1412 if self.tools.contains_key(&name) {
1413 return Err(RegisterError::DuplicateName(name));
1414 }
1415 if tool.definition().description().trim().is_empty() {
1416 return Err(RegisterError::EmptyDescription(name));
1417 }
1418 self.tools.insert(name, tool);
1419 Ok(())
1420 }
1421
1422 pub fn unregister(&mut self, name: &str) {
1424 self.tools.remove(name);
1425 }
1426
1427 pub async fn call(&self, name: &str, args: &str, cx: ToolContext) -> Result<ToolResult> {
1435 if let Some(tool) = self.tools.get(name) {
1436 tool.call(args, cx).await
1437 } else {
1438 Err(anyhow::Error::msg(format!("Tool '{name}' not found")))
1439 }
1440 }
1441}
1442
1443#[cfg(test)]
1444mod tests {
1445 use super::*;
1446 use alloc::{format, string::ToString, vec};
1447 use schemars::JsonSchema;
1448 use serde::Deserialize;
1449
1450 #[derive(JsonSchema, Deserialize, Debug, PartialEq)]
1452 struct CalculatorArgs {
1453 operation: String,
1454 a: f64,
1455 b: f64,
1456 }
1457
1458 struct Calculator;
1459
1460 impl Tool for Calculator {
1461 fn name(&self) -> Cow<'static, str> {
1462 "calculator".into()
1463 }
1464 type Arguments = CalculatorArgs;
1465 type Res = ToolResult;
1466
1467 fn call(
1468 &self,
1469 args: Self::Arguments,
1470 _cx: ToolContext,
1471 ) -> impl Future<Output = Result<Self::Res>> + Send {
1472 core::future::ready(match args.operation.as_str() {
1473 "add" => Ok(ToolResult::text((args.a + args.b).to_string())),
1474 "subtract" => Ok(ToolResult::text((args.a - args.b).to_string())),
1475 "multiply" => Ok(ToolResult::text((args.a * args.b).to_string())),
1476 "divide" => {
1477 if args.b == 0.0 {
1478 Err(anyhow::Error::msg("Division by zero"))
1479 } else {
1480 Ok(ToolResult::text((args.a / args.b).to_string()))
1481 }
1482 }
1483 _ => Err(anyhow::Error::msg(format!(
1484 "Unknown operation: {}",
1485 args.operation
1486 ))),
1487 })
1488 }
1489 }
1490
1491 #[derive(JsonSchema, Deserialize)]
1493 struct GreetArgs {
1494 name: String,
1495 }
1496
1497 struct Greeter;
1498
1499 impl Tool for Greeter {
1500 fn name(&self) -> Cow<'static, str> {
1501 "greeter".into()
1502 }
1503 type Arguments = GreetArgs;
1504 type Res = ToolResult;
1505
1506 fn call(
1507 &self,
1508 args: Self::Arguments,
1509 _cx: ToolContext,
1510 ) -> impl Future<Output = Result<Self::Res>> + Send {
1511 core::future::ready(Ok(ToolResult::text(format!("Hello, {}!", args.name))))
1512 }
1513 }
1514
1515 #[test]
1516 fn from_parts_accepts_object_and_boolean_schemas() {
1517 for schema in [
1519 serde_json::json!({"type": "object"}),
1520 serde_json::json!(true),
1521 ] {
1522 assert!(
1523 ToolDefinition::from_parts("t".into(), "does a thing".into(), schema.clone())
1524 .is_ok(),
1525 "{schema} should be accepted"
1526 );
1527 }
1528 }
1529
1530 #[test]
1531 fn from_parts_rejects_non_schema_values() {
1532 for schema in [
1535 serde_json::json!("a string"),
1536 serde_json::json!([1, 2, 3]),
1537 serde_json::json!(7),
1538 serde_json::json!(null),
1539 ] {
1540 let result = ToolDefinition::from_parts("weird".into(), "d".into(), schema.clone());
1541 let Err(err) = result else {
1542 panic!("{schema} should be rejected");
1543 };
1544 assert_eq!(err.name(), "weird");
1545 }
1546 }
1547
1548 #[test]
1549 fn tool_definition_creation() {
1550 let calculator = Calculator;
1551 let definition = ToolDefinition::new(&calculator);
1552
1553 assert_eq!(definition.name, "calculator");
1554 assert_eq!(
1555 definition.description,
1556 "Performs basic mathematical operations."
1557 );
1558 }
1561
1562 #[test]
1563 fn tools_creation() {
1564 let tools = Tools::new();
1565 assert_eq!(tools.definitions().len(), 0);
1566 }
1567
1568 #[test]
1569 fn tools_default() {
1570 let tools = Tools::default();
1571 assert_eq!(tools.definitions().len(), 0);
1572 }
1573
1574 #[tokio::test]
1575 async fn tools_register_and_call() {
1576 let mut tools = Tools::new();
1577 tools.register(Calculator).expect("calculator registers");
1578
1579 let definitions = tools.definitions();
1580 assert_eq!(definitions.len(), 1);
1581 assert_eq!(definitions[0].name, "calculator");
1582
1583 let result = tools
1584 .call(
1585 "calculator",
1586 r#"{"operation": "add", "a": 5, "b": 3}"#,
1587 ToolContext::new(),
1588 )
1589 .await;
1590 assert!(result.is_ok());
1591 assert_eq!(result.unwrap().as_text(), Some("8"));
1592 }
1593
1594 #[tokio::test]
1595 async fn invalid_arguments_report_the_parse_error_without_schema() {
1596 let mut tools = Tools::new();
1597 tools.register(Calculator).expect("calculator registers");
1598
1599 let error = tools
1600 .call("calculator", r#"{"operation": "add"}"#, ToolContext::new())
1601 .await
1602 .unwrap_err()
1603 .to_string();
1604 assert_eq!(
1605 error,
1606 "Invalid arguments for tool 'calculator': missing field `a` at line 1 column 20"
1607 );
1608 }
1609
1610 #[test]
1611 fn command_result_renders_its_text() {
1612 let result = ToolResult::Command(Box::new(CommandResult {
1613 stdout: CommandStream::Inline {
1614 text: "hi\n".to_string(),
1615 },
1616 stderr: None,
1617 outcome: CommandOutcome::Exited { code: 3 },
1618 }));
1619 assert_eq!(result.render_for_model(), "hi\nExit code: 3.\n");
1620 assert_eq!(
1621 result.command().map(|command| &command.outcome),
1622 Some(&CommandOutcome::Exited { code: 3 })
1623 );
1624 }
1625
1626 #[cfg(feature = "serde")]
1627 #[test]
1628 fn command_result_round_trips_through_serde() {
1629 let file = || crate::llm::StoredFile {
1630 url: "outputs/x.txt".to_string(),
1631 path: "/sandbox/outputs/x.txt".to_string(),
1632 };
1633 let results = [
1634 CommandResult {
1635 stdout: CommandStream::Inline {
1636 text: "hi\n".to_string(),
1637 },
1638 stderr: Some(CommandStream::Inline {
1639 text: "oops".to_string(),
1640 }),
1641 outcome: CommandOutcome::Exited { code: 1 },
1642 },
1643 CommandResult {
1644 stdout: CommandStream::Stored {
1645 file: file(),
1646 preview: "head".to_string(),
1647 truncated: true,
1648 },
1649 stderr: None,
1650 outcome: CommandOutcome::Exited { code: 0 },
1651 },
1652 CommandResult {
1653 stdout: CommandStream::Image {
1654 media_type: "image/png".to_string(),
1655 file: file(),
1656 },
1657 stderr: None,
1658 outcome: CommandOutcome::Running {
1659 task_id: "t1".to_string(),
1660 output: file(),
1661 },
1662 },
1663 ];
1664 for command in results {
1665 let result = ToolResult::Command(Box::new(command));
1666 let json = serde_json::to_string(&result).expect("a command result serializes");
1667 let back: ToolResult = serde_json::from_str(&json).expect("it deserializes back");
1668 assert_eq!(back, result, "{json}");
1669 }
1670 }
1671
1672 #[tokio::test]
1673 async fn calculator_operations() {
1674 let mut tools = Tools::new();
1675 tools.register(Calculator).expect("calculator registers");
1676
1677 let result = tools
1679 .call(
1680 "calculator",
1681 r#"{"operation": "add", "a": 10, "b": 5}"#,
1682 ToolContext::new(),
1683 )
1684 .await;
1685 assert_eq!(result.unwrap().as_text(), Some("15"));
1686
1687 let result = tools
1689 .call(
1690 "calculator",
1691 r#"{"operation": "subtract", "a": 10, "b": 3}"#,
1692 ToolContext::new(),
1693 )
1694 .await;
1695 assert_eq!(result.unwrap().as_text(), Some("7"));
1696
1697 let result = tools
1699 .call(
1700 "calculator",
1701 r#"{"operation": "multiply", "a": 4, "b": 3}"#,
1702 ToolContext::new(),
1703 )
1704 .await;
1705 assert_eq!(result.unwrap().as_text(), Some("12"));
1706
1707 let result = tools
1709 .call(
1710 "calculator",
1711 r#"{"operation": "divide", "a": 15, "b": 3}"#,
1712 ToolContext::new(),
1713 )
1714 .await;
1715 assert_eq!(result.unwrap().as_text(), Some("5"));
1716 }
1717
1718 #[tokio::test]
1719 async fn calculator_division_by_zero() {
1720 let mut tools = Tools::new();
1721 tools.register(Calculator).expect("calculator registers");
1722
1723 let result = tools
1724 .call(
1725 "calculator",
1726 r#"{"operation": "divide", "a": 10, "b": 0}"#,
1727 ToolContext::new(),
1728 )
1729 .await;
1730 assert!(result.is_err());
1731 assert!(result.unwrap_err().to_string().contains("Division by zero"));
1732 }
1733
1734 #[tokio::test]
1735 async fn calculator_unknown_operation() {
1736 let mut tools = Tools::new();
1737 tools.register(Calculator).expect("calculator registers");
1738
1739 let result = tools
1740 .call(
1741 "calculator",
1742 r#"{"operation": "modulo", "a": 10, "b": 3}"#,
1743 ToolContext::new(),
1744 )
1745 .await;
1746 assert!(result.is_err());
1747 assert!(
1748 result
1749 .unwrap_err()
1750 .to_string()
1751 .contains("Unknown operation")
1752 );
1753 }
1754
1755 #[tokio::test]
1756 async fn multiple_tools() {
1757 let mut tools = Tools::new();
1758 tools.register(Calculator).expect("calculator registers");
1759 tools.register(Greeter).expect("greeter registers");
1760
1761 let definitions = tools.definitions();
1762 assert_eq!(definitions.len(), 2);
1763
1764 let calc_def = definitions.iter().find(|d| d.name == "calculator").unwrap();
1766 let greet_def = definitions.iter().find(|d| d.name == "greeter").unwrap();
1767
1768 assert_eq!(
1769 calc_def.description,
1770 "Performs basic mathematical operations."
1771 );
1772 assert_eq!(greet_def.description, "Greets a person by name.");
1773
1774 let calc_result = tools
1776 .call(
1777 "calculator",
1778 r#"{"operation": "add", "a": 2, "b": 3}"#,
1779 ToolContext::new(),
1780 )
1781 .await;
1782 assert_eq!(calc_result.unwrap().as_text(), Some("5"));
1783
1784 let greet_result = tools
1785 .call("greeter", r#"{"name": "Alice"}"#, ToolContext::new())
1786 .await;
1787 assert_eq!(greet_result.unwrap().as_text(), Some("Hello, Alice!"));
1788 }
1789
1790 #[derive(Debug)]
1791 struct ToolFailure(&'static str);
1792
1793 impl core::fmt::Display for ToolFailure {
1794 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1795 f.write_str(self.0)
1796 }
1797 }
1798
1799 impl core::error::Error for ToolFailure {}
1800
1801 #[test]
1802 fn into_tool_result_string_is_plain_text() {
1803 assert_eq!(
1804 String::from("hello").into_tool_result().unwrap(),
1805 ToolResult::text("hello")
1806 );
1807 }
1808
1809 #[test]
1810 fn into_tool_result_str_is_plain_text() {
1811 assert_eq!(
1812 "hello".into_tool_result().unwrap(),
1813 ToolResult::text("hello")
1814 );
1815 }
1816
1817 #[test]
1818 fn into_tool_result_option_none_is_done() {
1819 let result = Option::<String>::None.into_tool_result().unwrap();
1820 assert_eq!(result, ToolResult::Done);
1821 }
1822
1823 #[test]
1824 fn into_tool_result_option_some_delegates() {
1825 let result = Some("hello").into_tool_result().unwrap();
1826 assert_eq!(result, ToolResult::text("hello"));
1827 }
1828
1829 #[test]
1830 fn into_tool_result_result_ok_string_is_text() {
1831 let result = core::result::Result::<String, ToolFailure>::Ok(String::from("hello"))
1832 .into_tool_result()
1833 .unwrap();
1834 assert_eq!(result, ToolResult::text("hello"));
1835 }
1836
1837 #[test]
1838 fn into_tool_result_result_err_is_typed_error() {
1839 let result = core::result::Result::<String, ToolFailure>::Err(ToolFailure("boom"))
1840 .into_tool_result()
1841 .unwrap();
1842 assert_eq!(result, ToolResult::error("boom"));
1843 assert!(result.is_error());
1844 assert_eq!(result.error_message(), Some("boom"));
1845 }
1846
1847 #[tokio::test]
1848 async fn tool_not_found() {
1849 let tools = Tools::new();
1850
1851 let result = tools.call("nonexistent", "{}", ToolContext::new()).await;
1852 assert!(result.is_err());
1853 assert!(
1854 result
1855 .unwrap_err()
1856 .to_string()
1857 .contains("Tool 'nonexistent' not found")
1858 );
1859 }
1860
1861 #[tokio::test]
1862 async fn invalid_json() {
1863 let mut tools = Tools::new();
1864 tools.register(Calculator).expect("calculator registers");
1865
1866 let result = tools
1867 .call("calculator", "invalid json", ToolContext::new())
1868 .await;
1869 assert!(result.is_err());
1870 }
1871
1872 #[test]
1873 fn tools_unregister() {
1874 let mut tools = Tools::new();
1875 tools.register(Calculator).expect("calculator registers");
1876 tools.register(Greeter).expect("greeter registers");
1877
1878 assert_eq!(tools.definitions().len(), 2);
1879
1880 tools.unregister("calculator");
1881 assert_eq!(tools.definitions().len(), 1);
1882
1883 let remaining = &tools.definitions()[0];
1884 assert_eq!(remaining.name, "greeter");
1885
1886 tools.unregister("greeter");
1887 assert_eq!(tools.definitions().len(), 0);
1888 }
1889
1890 #[test]
1891 fn tools_debug() {
1892 let mut tools = Tools::new();
1893 tools.register(Calculator).expect("calculator registers");
1894 tools.register(Greeter).expect("greeter registers");
1895
1896 let debug_str = format!("{tools:?}");
1897 assert!(debug_str.contains("Tools"));
1898 assert!(debug_str.contains("calculator"));
1899 assert!(debug_str.contains("greeter"));
1900 }
1901
1902 #[test]
1903 fn tool_definition_debug() {
1904 let calculator = Calculator;
1905 let definition = ToolDefinition::new(&calculator);
1906 let debug_str = format!("{definition:?}");
1907
1908 assert!(debug_str.contains("ToolDefinition"));
1909 assert!(debug_str.contains("calculator"));
1910 assert!(debug_str.contains("Performs basic mathematical operations"));
1911 }
1912
1913 #[test]
1914 fn tool_definition_clone() {
1915 let calculator = Calculator;
1916 let original = ToolDefinition::new(&calculator);
1917 let cloned = original.clone();
1918
1919 assert_eq!(original.name, cloned.name);
1920 assert_eq!(original.description, cloned.description);
1921 }
1922
1923 #[test]
1924 fn schema_preserves_enum() {
1925 #[derive(JsonSchema, Deserialize)]
1926 #[serde(rename_all = "snake_case")]
1927 enum Status {
1928 Pending,
1929 InProgress,
1930 Completed,
1931 }
1932
1933 #[allow(dead_code)]
1934 #[derive(JsonSchema, Deserialize)]
1935 struct Item {
1936 status: Status,
1937 }
1938
1939 #[allow(dead_code)]
1940 #[derive(JsonSchema, Deserialize)]
1941 struct Args {
1942 items: Vec<Item>,
1943 }
1944
1945 struct TestTool;
1946
1947 impl Tool for TestTool {
1948 fn name(&self) -> Cow<'static, str> {
1949 "test".into()
1950 }
1951 type Arguments = Args;
1952 type Res = ToolResult;
1953
1954 fn call(
1955 &self,
1956 _args: Self::Arguments,
1957 _cx: ToolContext,
1958 ) -> impl Future<Output = Result<Self::Res>> + Send {
1959 core::future::ready(Ok(ToolResult::text("ok")))
1960 }
1961 }
1962
1963 let tool = TestTool;
1964 let def = ToolDefinition::new(&tool);
1965 let schema = def.arguments_openai_schema();
1966
1967 let schema_obj = schema.as_object().expect("schema should be object");
1969 let properties = schema_obj
1970 .get("properties")
1971 .expect("should have properties")
1972 .as_object()
1973 .unwrap();
1974 let items = properties
1975 .get("items")
1976 .expect("should have items")
1977 .as_object()
1978 .unwrap();
1979 let item_props = items
1980 .get("items")
1981 .expect("items should have items schema")
1982 .as_object()
1983 .unwrap();
1984 let item_properties = item_props
1985 .get("properties")
1986 .expect("item should have properties")
1987 .as_object()
1988 .unwrap();
1989 let status = item_properties
1990 .get("status")
1991 .expect("should have status")
1992 .as_object()
1993 .unwrap();
1994
1995 assert!(
1997 status.contains_key("enum"),
1998 "Status should have enum field. Full schema: {}",
1999 serde_json::to_string_pretty(&schema).unwrap()
2000 );
2001 }
2002
2003 #[test]
2004 fn schema_ref_resolution() {
2005 let raw_schema = serde_json::json!({
2007 "type": "object",
2008 "properties": {
2009 "status": {
2010 "$ref": "#/$defs/Status"
2011 }
2012 },
2013 "$defs": {
2014 "Status": {
2015 "type": "string",
2016 "enum": ["pending", "in_progress", "completed"]
2017 }
2018 }
2019 });
2020
2021 let mut schema = raw_schema;
2022 clean_schema(&mut schema);
2023
2024 let props = schema.get("properties").unwrap().as_object().unwrap();
2025 let status = props.get("status").unwrap().as_object().unwrap();
2026
2027 assert!(
2028 status.contains_key("enum"),
2029 "Status should have enum after ref resolution. Got: {}",
2030 serde_json::to_string_pretty(&schema).unwrap()
2031 );
2032 }
2033
2034 #[test]
2035 fn schema_nested_ref_in_array() {
2036 let raw_schema = serde_json::json!({
2038 "type": "object",
2039 "properties": {
2040 "entries": {
2041 "type": "array",
2042 "items": {
2043 "$ref": "#/$defs/BatchItem"
2044 }
2045 }
2046 },
2047 "$defs": {
2048 "BatchItem": {
2049 "type": "object",
2050 "properties": {
2051 "content": { "type": "string" },
2052 "status": { "$ref": "#/$defs/BatchStatus" }
2053 },
2054 "required": ["content", "status"]
2055 },
2056 "BatchStatus": {
2057 "type": "string",
2058 "enum": ["pending", "in_progress", "completed"]
2059 }
2060 }
2061 });
2062
2063 let mut schema = raw_schema;
2064 clean_schema(&mut schema);
2065
2066 let props = schema.get("properties").unwrap().as_object().unwrap();
2068 let todos = props.get("entries").unwrap().as_object().unwrap();
2069 let items = todos.get("items").unwrap().as_object().unwrap();
2070 let item_props = items.get("properties").unwrap().as_object().unwrap();
2071 let status = item_props.get("status").unwrap().as_object().unwrap();
2072
2073 assert!(
2074 status.contains_key("enum"),
2075 "Nested status should have enum. Full schema: {}",
2076 serde_json::to_string_pretty(&schema).unwrap()
2077 );
2078 }
2079
2080 #[test]
2081 fn parts_collapses_degenerate_inputs() {
2082 assert_eq!(ToolResult::parts(vec![]), ToolResult::Done);
2083 assert_eq!(
2084 ToolResult::parts(vec![ToolResultPart::text("only")]),
2085 ToolResult::text("only")
2086 );
2087 let multi = ToolResult::parts(vec![
2088 ToolResultPart::text("a"),
2089 ToolResultPart::image(vec![1, 2], "image/png"),
2090 ]);
2091 let ToolResult::Parts { parts } = &multi else {
2092 panic!("two parts must stay a Parts result");
2093 };
2094 assert_eq!(parts.len(), 2);
2095 }
2096
2097 #[test]
2098 fn parts_render_joins_text_and_marks_binary() {
2099 let result = ToolResult::parts(vec![
2100 ToolResultPart::text("header"),
2101 ToolResultPart::tsv("n\n1\n"),
2102 ToolResultPart::image(vec![0x89], "image/png"),
2103 ]);
2104 assert_eq!(
2105 result.render_for_model(),
2106 "header\nn\n1\n\n[binary tool result: image/png, 1 bytes]"
2107 );
2108 assert!(result.mime().is_none() && result.content().is_none());
2109 }
2110
2111 #[test]
2112 fn parts_with_one_binary_expose_mime_and_content() {
2113 let result = ToolResult::Parts {
2114 parts: vec![ToolResultPart::image(vec![9, 9], "image/png")],
2115 };
2116 assert_eq!(result.mime().unwrap().essence_str(), "image/png");
2117 assert_eq!(result.content().unwrap(), &[9, 9]);
2118 }
2119
2120 #[cfg(feature = "serde")]
2121 #[test]
2122 fn parts_serde_roundtrip_uses_kind_tag() {
2123 let result = ToolResult::parts(vec![
2124 ToolResultPart::text("note"),
2125 ToolResultPart::tsv("k\ntrue\n"),
2126 ]);
2127 let value = serde_json::to_value(&result).unwrap();
2128 assert_eq!(value["kind"], "parts");
2129 assert_eq!(value["parts"][0]["kind"], "text");
2130 let back: ToolResult = serde_json::from_value(value).unwrap();
2131 assert_eq!(back, result);
2132 }
2133}