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::{Serialize, de::DeserializeOwned};
223
224mod context;
225
226pub use context::{Progress, ProgressError, ProgressSink, ToolContext};
227
228#[derive(Debug, Clone, PartialEq, Eq)]
247#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
248#[cfg_attr(feature = "serde", serde(tag = "kind", rename_all = "snake_case"))]
249pub enum ToolResult {
250 Done,
252
253 Text {
255 text: String,
257 },
258
259 Tsv {
261 text: String,
263 },
264
265 Json {
267 value: Value,
269 },
270
271 Binary {
273 mime: String,
275 content: Vec<u8>,
277 },
278
279 Error {
281 message: String,
283 },
284
285 Parts {
292 parts: Vec<ToolResultPart>,
294 },
295}
296
297#[derive(Debug, Clone, PartialEq, Eq)]
302#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
303#[cfg_attr(feature = "serde", serde(tag = "kind", rename_all = "snake_case"))]
304pub enum ToolResultPart {
305 Text {
307 text: String,
309 },
310
311 Tsv {
313 text: String,
315 },
316
317 Json {
319 value: Value,
321 },
322
323 Binary {
325 mime: String,
327 content: Vec<u8>,
329 },
330}
331
332impl ToolResult {
333 #[must_use]
335 pub fn text(s: impl Into<String>) -> Self {
336 Self::Text { text: s.into() }
337 }
338
339 #[must_use]
341 pub fn tsv(s: impl Into<String>) -> Self {
342 Self::Tsv { text: s.into() }
343 }
344
345 pub fn json<T: Serialize>(value: &T) -> Result<Self> {
351 Ok(Self::Json {
352 value: serde_json::to_value(value)?,
353 })
354 }
355
356 #[must_use]
358 pub const fn json_value(value: Value) -> Self {
359 Self::Json { value }
360 }
361
362 #[must_use]
364 pub fn image(data: Vec<u8>, media_type: &str) -> Self {
365 Self::Binary {
366 mime: parse_media_type_or_octet_stream(media_type),
367 content: data,
368 }
369 }
370
371 #[must_use]
373 pub fn binary(data: Vec<u8>) -> Self {
374 Self::Binary {
375 mime: mime::APPLICATION_OCTET_STREAM.essence_str().to_string(),
376 content: data,
377 }
378 }
379
380 #[must_use]
382 pub fn error(message: impl Into<String>) -> Self {
383 Self::Error {
384 message: message.into(),
385 }
386 }
387
388 #[must_use]
394 pub fn parts(parts: Vec<ToolResultPart>) -> Self {
395 match <[ToolResultPart; 1]>::try_from(parts) {
396 Ok([part]) => part.into_result(),
397 Err(parts) if parts.is_empty() => Self::Done,
398 Err(parts) => Self::Parts { parts },
399 }
400 }
401
402 #[must_use]
404 pub const fn is_done(&self) -> bool {
405 matches!(self, Self::Done)
406 }
407
408 #[must_use]
410 pub const fn is_error(&self) -> bool {
411 matches!(self, Self::Error { .. })
412 }
413
414 #[must_use]
416 pub fn as_text(&self) -> Option<&str> {
417 match self {
418 Self::Text { text } | Self::Tsv { text } => Some(text),
419 Self::Error { message } => Some(message),
420 Self::Done | Self::Json { .. } | Self::Binary { .. } | Self::Parts { .. } => None,
421 }
422 }
423
424 #[must_use]
426 pub fn error_message(&self) -> Option<&str> {
427 match self {
428 Self::Error { message } => Some(message),
429 Self::Done
430 | Self::Text { .. }
431 | Self::Tsv { .. }
432 | Self::Json { .. }
433 | Self::Binary { .. }
434 | Self::Parts { .. } => None,
435 }
436 }
437
438 pub fn render_for_model(&self) -> Result<String> {
444 match self {
445 Self::Done => Ok(String::new()),
446 Self::Text { text } | Self::Tsv { text } => Ok(text.clone()),
447 Self::Json { value } => Ok(serde_json::to_string(value)?),
448 Self::Binary { mime, content } => Ok(render_binary_placeholder(mime, content)),
449 Self::Error { message } => Ok(message.clone()),
450 Self::Parts { parts } => join_part_renders(parts, false),
451 }
452 }
453
454 pub fn render_for_cli(&self) -> Result<String> {
460 match self {
461 Self::Done => Ok(String::new()),
462 Self::Text { text } | Self::Tsv { text } => Ok(text.clone()),
463 Self::Json { value } => Ok(serde_json::to_string_pretty(value)?),
464 Self::Binary { mime, content } => Ok(render_binary_placeholder(mime, content)),
465 Self::Error { message } => Ok(message.clone()),
466 Self::Parts { parts } => join_part_renders(parts, true),
467 }
468 }
469
470 #[must_use]
475 pub fn mime(&self) -> Option<Mime> {
476 match self {
477 Self::Binary { mime, .. } => mime.parse().ok(),
478 Self::Parts { parts } => match parts.as_slice() {
479 [ToolResultPart::Binary { mime, .. }] => mime.parse().ok(),
480 _ => None,
481 },
482 Self::Done
483 | Self::Text { .. }
484 | Self::Tsv { .. }
485 | Self::Json { .. }
486 | Self::Error { .. } => None,
487 }
488 }
489
490 #[must_use]
494 pub fn content(&self) -> Option<&[u8]> {
495 match self {
496 Self::Binary { content, .. } => Some(content),
497 Self::Parts { parts } => match parts.as_slice() {
498 [ToolResultPart::Binary { content, .. }] => Some(content),
499 _ => None,
500 },
501 Self::Done
502 | Self::Text { .. }
503 | Self::Tsv { .. }
504 | Self::Json { .. }
505 | Self::Error { .. } => None,
506 }
507 }
508}
509
510impl ToolResultPart {
511 #[must_use]
513 pub fn text(s: impl Into<String>) -> Self {
514 Self::Text { text: s.into() }
515 }
516
517 #[must_use]
519 pub fn tsv(s: impl Into<String>) -> Self {
520 Self::Tsv { text: s.into() }
521 }
522
523 pub fn json<T: Serialize>(value: &T) -> Result<Self> {
529 Ok(Self::Json {
530 value: serde_json::to_value(value)?,
531 })
532 }
533
534 #[must_use]
536 pub const fn json_value(value: Value) -> Self {
537 Self::Json { value }
538 }
539
540 #[must_use]
542 pub fn image(data: Vec<u8>, media_type: &str) -> Self {
543 Self::Binary {
544 mime: parse_media_type_or_octet_stream(media_type),
545 content: data,
546 }
547 }
548
549 #[must_use]
551 pub fn binary(data: Vec<u8>) -> Self {
552 Self::Binary {
553 mime: mime::APPLICATION_OCTET_STREAM.essence_str().to_string(),
554 content: data,
555 }
556 }
557
558 #[must_use]
560 pub fn as_text(&self) -> Option<&str> {
561 match self {
562 Self::Text { text } | Self::Tsv { text } => Some(text),
563 Self::Json { .. } | Self::Binary { .. } => None,
564 }
565 }
566
567 pub fn render_for_model(&self) -> Result<String> {
573 self.render(false)
574 }
575
576 pub fn render_for_cli(&self) -> Result<String> {
582 self.render(true)
583 }
584
585 fn render(&self, pretty: bool) -> Result<String> {
586 match self {
587 Self::Text { text } | Self::Tsv { text } => Ok(text.clone()),
588 Self::Json { value } => Ok(if pretty {
589 serde_json::to_string_pretty(value)?
590 } else {
591 serde_json::to_string(value)?
592 }),
593 Self::Binary { mime, content } => Ok(render_binary_placeholder(mime, content)),
594 }
595 }
596
597 #[must_use]
599 pub fn mime(&self) -> Option<Mime> {
600 match self {
601 Self::Binary { mime, .. } => mime.parse().ok(),
602 Self::Text { .. } | Self::Tsv { .. } | Self::Json { .. } => None,
603 }
604 }
605
606 #[must_use]
608 pub fn content(&self) -> Option<&[u8]> {
609 match self {
610 Self::Binary { content, .. } => Some(content),
611 Self::Text { .. } | Self::Tsv { .. } | Self::Json { .. } => None,
612 }
613 }
614
615 #[must_use]
617 pub fn into_result(self) -> ToolResult {
618 match self {
619 Self::Text { text } | Self::Tsv { text } => ToolResult::Text { text },
620 Self::Json { value } => ToolResult::Json { value },
621 Self::Binary { mime, content } => ToolResult::Binary { mime, content },
622 }
623 }
624}
625
626fn render_binary_placeholder(mime: &str, content: &[u8]) -> String {
627 let mut rendered = String::new();
628 rendered.push_str("[binary tool result: ");
629 rendered.push_str(mime);
630 rendered.push_str(", ");
631 rendered.push_str(content.len().to_string().as_str());
632 rendered.push_str(" bytes]");
633 rendered
634}
635
636fn join_part_renders(parts: &[ToolResultPart], pretty: bool) -> Result<String> {
637 let mut rendered = String::new();
638 for part in parts {
639 let text = if pretty {
640 part.render_for_cli()?
641 } else {
642 part.render_for_model()?
643 };
644 if text.is_empty() {
645 continue;
646 }
647 if !rendered.is_empty() {
648 rendered.push('\n');
649 }
650 rendered.push_str(&text);
651 }
652 Ok(rendered)
653}
654
655pub trait IntoToolResult {
661 fn into_tool_result(self) -> Result<ToolResult>;
667}
668
669impl IntoToolResult for ToolResult {
670 fn into_tool_result(self) -> Result<ToolResult> {
671 Ok(self)
672 }
673}
674
675impl IntoToolResult for () {
676 fn into_tool_result(self) -> Result<ToolResult> {
677 Ok(ToolResult::Done)
678 }
679}
680
681impl IntoToolResult for String {
682 fn into_tool_result(self) -> Result<ToolResult> {
683 Ok(ToolResult::text(self))
684 }
685}
686
687impl IntoToolResult for &str {
688 fn into_tool_result(self) -> Result<ToolResult> {
689 Ok(ToolResult::text(self))
690 }
691}
692
693impl IntoToolResult for Cow<'_, str> {
694 fn into_tool_result(self) -> Result<ToolResult> {
695 Ok(ToolResult::text(self.into_owned()))
696 }
697}
698
699impl IntoToolResult for Value {
700 fn into_tool_result(self) -> Result<ToolResult> {
701 Ok(ToolResult::json_value(self))
702 }
703}
704
705impl<T> IntoToolResult for Option<T>
706where
707 T: IntoToolResult,
708{
709 fn into_tool_result(self) -> Result<ToolResult> {
710 self.map_or_else(|| Ok(ToolResult::Done), IntoToolResult::into_tool_result)
711 }
712}
713
714impl IntoToolResult for Vec<ToolResultPart> {
715 fn into_tool_result(self) -> Result<ToolResult> {
716 Ok(ToolResult::parts(self))
717 }
718}
719
720impl<T, E> IntoToolResult for core::result::Result<T, E>
721where
722 T: Serialize,
723 E: Display,
724{
725 fn into_tool_result(self) -> Result<ToolResult> {
726 match self {
727 Ok(value) => serialize_success_value(&value),
728 Err(error) => Ok(ToolResult::error(error.to_string())),
729 }
730 }
731}
732
733fn parse_media_type_or_octet_stream(media_type: &str) -> String {
734 media_type
735 .parse::<Mime>()
736 .unwrap_or(mime::APPLICATION_OCTET_STREAM)
737 .essence_str()
738 .to_string()
739}
740
741fn serialize_success_value<T: Serialize>(value: &T) -> Result<ToolResult> {
742 let value = serde_json::to_value(value)?;
743 if let Some(tsv) = json_value_to_tsv(&value) {
744 return Ok(ToolResult::tsv(tsv));
745 }
746
747 match value {
748 Value::String(text) => Ok(ToolResult::text(text)),
749 other => Ok(ToolResult::json_value(other)),
750 }
751}
752
753#[must_use]
755pub fn json_value_to_tsv(value: &Value) -> Option<String> {
756 let rows = match value {
757 Value::Array(arr) if !arr.is_empty() => arr
758 .iter()
759 .map(|value| flatten_json_value(value, ""))
760 .collect::<Vec<_>>(),
761 Value::Object(_) => alloc::vec![flatten_json_value(value, "")],
762 Value::Array(_) | Value::String(_) | Value::Number(_) | Value::Bool(_) | Value::Null => {
763 return None;
764 }
765 };
766
767 if rows.is_empty() {
768 return None;
769 }
770
771 let mut columns: Vec<String> = Vec::new();
772 let mut seen: alloc::collections::BTreeSet<String> = alloc::collections::BTreeSet::new();
773 for row in &rows {
774 for (key, _) in row {
775 if seen.insert(key.clone()) {
776 columns.push(key.clone());
777 }
778 }
779 }
780
781 if columns.is_empty() {
782 return None;
783 }
784
785 let mut tsv = String::new();
786 for (index, column) in columns.iter().enumerate() {
787 if index > 0 {
788 tsv.push('\t');
789 }
790 tsv.push_str(&escape_tsv_field(column));
791 }
792 tsv.push('\n');
793
794 for row in &rows {
795 let row_map: alloc::collections::BTreeMap<&str, &str> = row
796 .iter()
797 .map(|(key, value)| (key.as_str(), value.as_str()))
798 .collect::<alloc::collections::BTreeMap<&str, &str>>();
799 for (index, column) in columns.iter().enumerate() {
800 if index > 0 {
801 tsv.push('\t');
802 }
803 if let Some(value) = row_map.get(column.as_str()) {
804 tsv.push_str(&escape_tsv_field(value));
805 }
806 }
807 tsv.push('\n');
808 }
809
810 Some(tsv)
811}
812
813fn flatten_json_value(value: &Value, prefix: &str) -> Vec<(String, String)> {
814 let mut flattened = Vec::new();
815 match value {
816 Value::Object(map) => {
817 for (key, child) in map {
818 let full_key = if prefix.is_empty() {
819 key.clone()
820 } else {
821 format!("{prefix}.{key}")
822 };
823 flattened.extend(flatten_json_value(child, &full_key));
824 }
825 }
826 Value::Array(_) => {
827 let serialized = serde_json::to_string(value).unwrap_or_default();
828 flattened.push((prefix.to_string(), serialized));
829 }
830 Value::String(text) => {
831 flattened.push((prefix.to_string(), text.clone()));
832 }
833 Value::Number(number) => {
834 flattened.push((prefix.to_string(), number.to_string()));
835 }
836 Value::Bool(boolean) => {
837 flattened.push((prefix.to_string(), boolean.to_string()));
838 }
839 Value::Null => {
840 flattened.push((prefix.to_string(), String::new()));
841 }
842 }
843 flattened
844}
845
846fn escape_tsv_field(value: &str) -> String {
847 value.replace(['\t', '\n', '\r'], " ")
848}
849
850pub trait Tool: Send + Sync {
890 fn name(&self) -> Cow<'static, str>;
892
893 fn description(&self) -> Cow<'static, str> {
903 description_from_schema::<Self::Arguments>().unwrap_or_default()
904 }
905
906 type Arguments: Send + JsonSchema + DeserializeOwned;
909
910 type Res: IntoToolResult + Send;
912
913 fn call(
924 &self,
925 arguments: Self::Arguments,
926 cx: ToolContext,
927 ) -> impl Future<Output = Result<Self::Res>> + Send;
928}
929
930pub fn json<T: Serialize>(value: &T) -> Result<String> {
954 let value = serde_json::to_value(value)?;
955
956 Ok(value
957 .as_str()
958 .map_or_else(|| format!("{value:#}"), ToString::to_string))
959}
960
961trait ToolImpl: Send + Sync + Any {
962 fn call(
963 &self,
964 args: &str,
965 cx: ToolContext,
966 ) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send + '_>>;
967
968 fn definition(&self) -> &ToolDefinition;
971
972 fn as_any(&self) -> &dyn Any;
977
978 fn as_any_mut(&mut self) -> &mut dyn Any;
980}
981
982struct DynToolImpl<F>
984where
985 F: Fn(&str, ToolContext) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send>>
986 + Send
987 + Sync,
988{
989 definition: ToolDefinition,
990 handler: F,
991}
992
993impl<F> ToolImpl for DynToolImpl<F>
994where
995 F: Fn(&str, ToolContext) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send>>
996 + Send
997 + Sync
998 + 'static,
999{
1000 fn call(
1001 &self,
1002 args: &str,
1003 cx: ToolContext,
1004 ) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send + '_>> {
1005 (self.handler)(args, cx)
1006 }
1007
1008 fn definition(&self) -> &ToolDefinition {
1009 &self.definition
1010 }
1011
1012 fn as_any(&self) -> &dyn Any {
1013 self
1014 }
1015
1016 fn as_any_mut(&mut self) -> &mut dyn Any {
1017 self
1018 }
1019}
1020
1021fn schema_is_object(value: &Value) -> bool {
1024 matches!(value.get("type").and_then(Value::as_str), Some("object"))
1025 || value.get("properties").is_some()
1026 || value.get("oneOf").is_some()
1027 || value.get("anyOf").is_some()
1028 || value.get("$defs").is_some()
1029}
1030
1031fn is_object<T: JsonSchema>() -> bool {
1032 schema_is_object(&schema_for!(T).to_value())
1033}
1034
1035fn arguments_schema<T: JsonSchema>() -> Schema {
1038 if is_object::<T>() {
1039 schema_for!(T)
1040 } else {
1041 schema_for!(ToolArgument<T>)
1042 }
1043}
1044
1045fn description_from_schema<T: JsonSchema>() -> Option<Cow<'static, str>> {
1047 schema_for!(T)
1048 .to_value()
1049 .get("description")
1050 .and_then(Value::as_str)
1051 .filter(|text| !text.trim().is_empty())
1052 .map(|text| Cow::Owned(text.to_string()))
1053}
1054
1055struct RegisteredTool<T: Tool> {
1061 tool: T,
1062 definition: ToolDefinition,
1063 args_are_object: bool,
1064}
1065
1066impl<T: Tool> RegisteredTool<T> {
1067 fn new(tool: T) -> Self {
1068 let definition = ToolDefinition::new(&tool);
1069 let args_are_object = is_object::<T::Arguments>();
1070 Self {
1071 tool,
1072 definition,
1073 args_are_object,
1074 }
1075 }
1076}
1077
1078impl<T: Tool + 'static> ToolImpl for RegisteredTool<T> {
1079 fn call(
1080 &self,
1081 args: &str,
1082 cx: ToolContext,
1083 ) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send + '_>> {
1084 let result = if self.args_are_object {
1085 serde_json::from_str::<T::Arguments>(args)
1086 } else {
1087 serde_json::from_str::<ToolArgument<T::Arguments>>(args).map(|wrapper| wrapper.value)
1088 };
1089
1090 let Ok(arguments) = result else {
1091 let name = self.definition.name().to_string();
1094 let schema_str =
1095 serde_json::to_string_pretty(&self.definition.arguments_openai_schema())
1096 .unwrap_or_else(|_| "{}".to_string());
1097 return Box::pin(async move {
1098 Err(anyhow::Error::msg(format!(
1099 "Invalid arguments for tool '{name}'. Expected schema:\n{schema_str}"
1100 )))
1101 });
1102 };
1103
1104 Box::pin(async move {
1105 Tool::call(&self.tool, arguments, cx)
1106 .await?
1107 .into_tool_result()
1108 })
1109 }
1110
1111 fn definition(&self) -> &ToolDefinition {
1112 &self.definition
1113 }
1114
1115 fn as_any(&self) -> &dyn Any {
1116 &self.tool
1117 }
1118
1119 fn as_any_mut(&mut self) -> &mut dyn Any {
1120 &mut self.tool
1121 }
1122}
1123
1124#[derive(Debug, Clone, PartialEq, Eq)]
1129pub struct InvalidSchema {
1130 name: Cow<'static, str>,
1132}
1133
1134impl InvalidSchema {
1135 #[must_use]
1137 pub fn name(&self) -> &str {
1138 &self.name
1139 }
1140}
1141
1142impl Display for InvalidSchema {
1143 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1144 write!(
1145 f,
1146 "tool '{}' has an argument schema that is neither an object nor a boolean",
1147 self.name
1148 )
1149 }
1150}
1151
1152impl core::error::Error for InvalidSchema {}
1153
1154#[derive(Debug, Clone, PartialEq, Eq)]
1156pub enum RegisterError {
1157 DuplicateName(Cow<'static, str>),
1161
1162 EmptyDescription(Cow<'static, str>),
1167}
1168
1169impl Display for RegisterError {
1170 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1171 match self {
1172 Self::DuplicateName(name) => {
1173 write!(f, "a tool named '{name}' is already registered")
1174 }
1175 Self::EmptyDescription(name) => write!(
1176 f,
1177 "tool '{name}' has an empty description; add a rustdoc comment to its \
1178 Arguments type or implement Tool::description"
1179 ),
1180 }
1181 }
1182}
1183
1184impl core::error::Error for RegisterError {}
1185
1186pub struct Tools {
1202 tools: BTreeMap<Cow<'static, str>, Box<dyn ToolImpl>>,
1203}
1204
1205impl Debug for Tools {
1206 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1207 f.debug_struct("Tools")
1208 .field("tools", &self.tools.keys().collect::<Vec<_>>())
1209 .finish()
1210 }
1211}
1212
1213#[derive(Debug, Clone, PartialEq)]
1217#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1218pub struct ToolDefinition {
1219 name: Cow<'static, str>,
1221 description: Cow<'static, str>,
1223 arguments: Schema,
1225}
1226
1227impl ToolDefinition {
1228 #[must_use]
1233 pub fn new<T: Tool>(tool: &T) -> Self {
1234 Self {
1235 name: tool.name(),
1236 description: tool.description(),
1237 arguments: arguments_schema::<T::Arguments>(),
1238 }
1239 }
1240
1241 pub fn from_parts(
1252 name: Cow<'static, str>,
1253 description: Cow<'static, str>,
1254 schema: Value,
1255 ) -> core::result::Result<Self, InvalidSchema> {
1256 let arguments: Schema = schema
1257 .try_into()
1258 .map_err(|_| InvalidSchema { name: name.clone() })?;
1259
1260 Ok(Self {
1261 name,
1262 description,
1263 arguments,
1264 })
1265 }
1266
1267 #[must_use]
1269 pub fn name(&self) -> &str {
1270 &self.name
1271 }
1272
1273 #[must_use]
1275 pub fn description(&self) -> &str {
1276 &self.description
1277 }
1278
1279 #[must_use]
1283 pub fn arguments_openai_schema(&self) -> serde_json::Value {
1284 let mut inner = self.arguments.clone().to_value();
1285 clean_schema(&mut inner);
1286
1287 inner
1288 }
1289}
1290
1291#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
1292struct ToolArgument<T> {
1293 value: T,
1294}
1295
1296fn clean_schema(value: &mut Value) {
1297 let defs = extract_defs(value);
1299
1300 resolve_and_clean(value, &defs);
1302
1303 if let Value::Object(map) = value {
1305 map.remove("description");
1308
1309 if map.contains_key("properties") && !map.contains_key("type") {
1311 map.insert("type".to_string(), Value::String("object".to_string()));
1312 }
1313 }
1314}
1315
1316fn extract_defs(value: &Value) -> serde_json::Map<String, Value> {
1318 if let Value::Object(map) = value
1319 && let Some(Value::Object(defs)) = map.get("$defs").or_else(|| map.get("definitions"))
1320 {
1321 return defs.clone();
1322 }
1323 serde_json::Map::new()
1324}
1325
1326#[allow(clippy::too_many_lines)]
1328fn resolve_and_clean(value: &mut Value, defs: &serde_json::Map<String, Value>) {
1329 resolve_and_clean_inner(value, defs, false);
1330}
1331
1332#[allow(clippy::too_many_lines)]
1334fn resolve_and_clean_inner(
1335 value: &mut Value,
1336 defs: &serde_json::Map<String, Value>,
1337 inside_properties: bool,
1338) {
1339 match value {
1340 Value::Object(map) => {
1341 if let Some(Value::String(ref_path)) = map.remove("$ref")
1343 && let Some(Value::Object(resolved_map)) = resolve_ref(&ref_path, defs)
1344 {
1345 let existing_description = map.remove("description");
1348 for (k, v) in resolved_map {
1349 map.entry(k).or_insert(v);
1350 }
1351 if let Some(desc) = existing_description {
1353 map.insert("description".to_string(), desc);
1354 }
1355 }
1356
1357 if let Some(const_val) = map.remove("const") {
1359 map.insert("enum".to_string(), Value::Array(alloc::vec![const_val]));
1360 }
1361
1362 if let Some(Value::Array(variants)) =
1364 map.remove("oneOf").or_else(|| map.remove("anyOf"))
1365 {
1366 let is_simple_enum = variants.iter().all(|v| {
1368 if let Value::Object(vm) = v {
1369 (vm.contains_key("const") || vm.contains_key("enum"))
1370 && !vm.contains_key("properties")
1371 } else {
1372 false
1373 }
1374 });
1375
1376 if is_simple_enum {
1377 let mut enum_values: alloc::vec::Vec<Value> = alloc::vec::Vec::new();
1379 let mut variant_type: Option<String> = None;
1380
1381 for variant in &variants {
1382 if let Value::Object(vm) = variant {
1383 if let Some(const_val) = vm.get("const")
1384 && !enum_values.contains(const_val)
1385 {
1386 enum_values.push(const_val.clone());
1387 }
1388 if let Some(Value::Array(arr)) = vm.get("enum") {
1389 for val in arr {
1390 if !enum_values.contains(val) {
1391 enum_values.push(val.clone());
1392 }
1393 }
1394 }
1395 if variant_type.is_none()
1396 && let Some(Value::String(t)) = vm.get("type")
1397 {
1398 variant_type = Some(t.clone());
1399 }
1400 }
1401 }
1402
1403 if !enum_values.is_empty() {
1404 map.insert("enum".to_string(), Value::Array(enum_values));
1405 if let Some(t) = variant_type {
1406 map.insert("type".to_string(), Value::String(t));
1407 }
1408 }
1409 } else {
1410 let mut all_properties = serde_json::Map::new();
1412
1413 for variant in variants {
1414 if let Value::Object(variant_map) = variant
1415 && let Some(Value::Object(props)) = variant_map.get("properties")
1416 {
1417 for (key, val) in props {
1418 let new_values: Option<alloc::vec::Vec<Value>> =
1420 if let Value::Object(val_obj) = val {
1421 if let Some(Value::Array(arr)) = val_obj.get("enum") {
1422 Some(arr.clone())
1423 } else {
1424 val_obj
1425 .get("const")
1426 .map(|const_val| alloc::vec![const_val.clone()])
1427 }
1428 } else {
1429 None
1430 };
1431
1432 if all_properties.contains_key(key) {
1433 if let Some(values) = new_values
1435 && let Some(Value::Object(existing_obj)) =
1436 all_properties.get_mut(key)
1437 && let Some(Value::Array(existing_enum)) =
1438 existing_obj.get_mut("enum")
1439 {
1440 for e in values {
1441 if !existing_enum.contains(&e) {
1442 existing_enum.push(e);
1443 }
1444 }
1445 }
1446 } else {
1447 let mut val_clone = val.clone();
1449 if let Value::Object(obj) = &mut val_clone
1450 && let Some(const_val) = obj.remove("const")
1451 {
1452 obj.insert(
1453 "enum".to_string(),
1454 Value::Array(alloc::vec![const_val]),
1455 );
1456 }
1457 all_properties.insert(key.clone(), val_clone);
1458 }
1459 }
1460 }
1461 }
1462
1463 if !all_properties.is_empty() {
1465 map.insert("type".to_string(), Value::String("object".to_string()));
1466 map.insert("properties".to_string(), Value::Object(all_properties));
1467 }
1468 }
1469 }
1470
1471 if !inside_properties {
1474 let allowed = [
1475 "type",
1476 "description",
1477 "properties",
1478 "required",
1479 "items",
1480 "enum",
1481 "nullable",
1482 ];
1483 map.retain(|k, _| allowed.contains(&k.as_str()));
1484 }
1485
1486 if let Some(Value::Array(types)) = map.get("type") {
1488 let non_null: Vec<&Value> = types
1490 .iter()
1491 .filter(|t| !matches!(t, Value::String(s) if s == "null"))
1492 .collect();
1493 if non_null.len() == 1 {
1494 map.insert("type".to_string(), non_null[0].clone());
1495 }
1496 }
1497
1498 for (key, v) in map.iter_mut() {
1500 let child_inside_props = key == "properties";
1502 resolve_and_clean_inner(v, defs, child_inside_props);
1503 }
1504 }
1505 Value::Array(arr) => {
1506 for v in arr {
1507 resolve_and_clean_inner(v, defs, false);
1508 }
1509 }
1510 _ => {}
1511 }
1512}
1513
1514fn resolve_ref(ref_path: &str, defs: &serde_json::Map<String, Value>) -> Option<Value> {
1516 let name = ref_path
1518 .strip_prefix("#/$defs/")
1519 .or_else(|| ref_path.strip_prefix("#/definitions/"))?;
1520
1521 defs.get(name).cloned()
1522}
1523
1524impl Default for Tools {
1525 fn default() -> Self {
1526 Self::new()
1527 }
1528}
1529
1530impl Tools {
1531 #[must_use]
1533 pub const fn new() -> Self {
1534 Self {
1535 tools: BTreeMap::new(),
1536 }
1537 }
1538
1539 #[must_use]
1543 pub fn get<T>(&self) -> Option<&T>
1544 where
1545 T: Tool + 'static,
1546 {
1547 self.tools
1548 .values()
1549 .find_map(|tool| tool.as_any().downcast_ref::<T>())
1550 }
1551
1552 #[must_use]
1556 pub fn get_mut<T>(&mut self) -> Option<&mut T>
1557 where
1558 T: Tool + 'static,
1559 {
1560 self.tools
1561 .values_mut()
1562 .find_map(|tool| tool.as_any_mut().downcast_mut::<T>())
1563 }
1564
1565 #[must_use]
1567 pub fn definitions(&self) -> Vec<ToolDefinition> {
1568 self.tools
1569 .values()
1570 .map(|tool| tool.definition().clone())
1571 .collect()
1572 }
1573
1574 pub fn register<T: Tool + 'static>(
1585 &mut self,
1586 tool: T,
1587 ) -> core::result::Result<(), RegisterError> {
1588 self.insert(Box::new(RegisteredTool::new(tool)))
1589 }
1590
1591 pub fn register_dyn<F>(
1601 &mut self,
1602 definition: ToolDefinition,
1603 handler: F,
1604 ) -> core::result::Result<(), RegisterError>
1605 where
1606 F: Fn(&str, ToolContext) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send>>
1607 + Send
1608 + Sync
1609 + 'static,
1610 {
1611 self.insert(Box::new(DynToolImpl {
1612 definition,
1613 handler,
1614 }))
1615 }
1616
1617 fn insert(&mut self, tool: Box<dyn ToolImpl>) -> core::result::Result<(), RegisterError> {
1618 let name = tool.definition().name.clone();
1619 if self.tools.contains_key(&name) {
1620 return Err(RegisterError::DuplicateName(name));
1621 }
1622 if tool.definition().description().trim().is_empty() {
1623 return Err(RegisterError::EmptyDescription(name));
1624 }
1625 self.tools.insert(name, tool);
1626 Ok(())
1627 }
1628
1629 pub fn unregister(&mut self, name: &str) {
1631 self.tools.remove(name);
1632 }
1633
1634 pub async fn call(&self, name: &str, args: &str, cx: ToolContext) -> Result<ToolResult> {
1642 if let Some(tool) = self.tools.get(name) {
1643 tool.call(args, cx).await
1644 } else {
1645 Err(anyhow::Error::msg(format!("Tool '{name}' not found")))
1646 }
1647 }
1648}
1649
1650#[cfg(test)]
1651mod tests {
1652 use super::*;
1653 use alloc::{format, string::ToString, vec};
1654 use schemars::JsonSchema;
1655 use serde::{Deserialize, Serialize};
1656
1657 #[derive(JsonSchema, Deserialize, Debug, PartialEq)]
1659 struct CalculatorArgs {
1660 operation: String,
1661 a: f64,
1662 b: f64,
1663 }
1664
1665 struct Calculator;
1666
1667 impl Tool for Calculator {
1668 fn name(&self) -> Cow<'static, str> {
1669 "calculator".into()
1670 }
1671 type Arguments = CalculatorArgs;
1672 type Res = ToolResult;
1673
1674 fn call(
1675 &self,
1676 args: Self::Arguments,
1677 _cx: ToolContext,
1678 ) -> impl Future<Output = Result<Self::Res>> + Send {
1679 core::future::ready(match args.operation.as_str() {
1680 "add" => Ok(ToolResult::text((args.a + args.b).to_string())),
1681 "subtract" => Ok(ToolResult::text((args.a - args.b).to_string())),
1682 "multiply" => Ok(ToolResult::text((args.a * args.b).to_string())),
1683 "divide" => {
1684 if args.b == 0.0 {
1685 Err(anyhow::Error::msg("Division by zero"))
1686 } else {
1687 Ok(ToolResult::text((args.a / args.b).to_string()))
1688 }
1689 }
1690 _ => Err(anyhow::Error::msg(format!(
1691 "Unknown operation: {}",
1692 args.operation
1693 ))),
1694 })
1695 }
1696 }
1697
1698 #[derive(JsonSchema, Deserialize)]
1700 struct GreetArgs {
1701 name: String,
1702 }
1703
1704 struct Greeter;
1705
1706 impl Tool for Greeter {
1707 fn name(&self) -> Cow<'static, str> {
1708 "greeter".into()
1709 }
1710 type Arguments = GreetArgs;
1711 type Res = ToolResult;
1712
1713 fn call(
1714 &self,
1715 args: Self::Arguments,
1716 _cx: ToolContext,
1717 ) -> impl Future<Output = Result<Self::Res>> + Send {
1718 core::future::ready(Ok(ToolResult::text(format!("Hello, {}!", args.name))))
1719 }
1720 }
1721
1722 #[test]
1723 fn from_parts_accepts_object_and_boolean_schemas() {
1724 for schema in [
1726 serde_json::json!({"type": "object"}),
1727 serde_json::json!(true),
1728 ] {
1729 assert!(
1730 ToolDefinition::from_parts("t".into(), "does a thing".into(), schema.clone())
1731 .is_ok(),
1732 "{schema} should be accepted"
1733 );
1734 }
1735 }
1736
1737 #[test]
1738 fn from_parts_rejects_non_schema_values() {
1739 for schema in [
1742 serde_json::json!("a string"),
1743 serde_json::json!([1, 2, 3]),
1744 serde_json::json!(7),
1745 serde_json::json!(null),
1746 ] {
1747 let result = ToolDefinition::from_parts("weird".into(), "d".into(), schema.clone());
1748 let Err(err) = result else {
1749 panic!("{schema} should be rejected");
1750 };
1751 assert_eq!(err.name(), "weird");
1752 }
1753 }
1754
1755 #[test]
1756 fn json_utility() {
1757 let value = serde_json::json!({
1758 "name": "test",
1759 "value": 42
1760 });
1761
1762 let json_str = json(&value).expect("a JSON value always serializes");
1763 assert!(json_str.contains("\"name\": \"test\""));
1764 assert!(json_str.contains("\"value\": 42"));
1765 }
1766
1767 #[test]
1768 fn tool_definition_creation() {
1769 let calculator = Calculator;
1770 let definition = ToolDefinition::new(&calculator);
1771
1772 assert_eq!(definition.name, "calculator");
1773 assert_eq!(
1774 definition.description,
1775 "Performs basic mathematical operations."
1776 );
1777 }
1780
1781 #[test]
1782 fn tools_creation() {
1783 let tools = Tools::new();
1784 assert_eq!(tools.definitions().len(), 0);
1785 }
1786
1787 #[test]
1788 fn tools_default() {
1789 let tools = Tools::default();
1790 assert_eq!(tools.definitions().len(), 0);
1791 }
1792
1793 #[tokio::test]
1794 async fn tools_register_and_call() {
1795 let mut tools = Tools::new();
1796 tools.register(Calculator).expect("calculator registers");
1797
1798 let definitions = tools.definitions();
1799 assert_eq!(definitions.len(), 1);
1800 assert_eq!(definitions[0].name, "calculator");
1801
1802 let result = tools
1803 .call(
1804 "calculator",
1805 r#"{"operation": "add", "a": 5, "b": 3}"#,
1806 ToolContext::new(),
1807 )
1808 .await;
1809 assert!(result.is_ok());
1810 assert_eq!(result.unwrap().as_text(), Some("8"));
1811 }
1812
1813 #[tokio::test]
1814 async fn calculator_operations() {
1815 let mut tools = Tools::new();
1816 tools.register(Calculator).expect("calculator registers");
1817
1818 let result = tools
1820 .call(
1821 "calculator",
1822 r#"{"operation": "add", "a": 10, "b": 5}"#,
1823 ToolContext::new(),
1824 )
1825 .await;
1826 assert_eq!(result.unwrap().as_text(), Some("15"));
1827
1828 let result = tools
1830 .call(
1831 "calculator",
1832 r#"{"operation": "subtract", "a": 10, "b": 3}"#,
1833 ToolContext::new(),
1834 )
1835 .await;
1836 assert_eq!(result.unwrap().as_text(), Some("7"));
1837
1838 let result = tools
1840 .call(
1841 "calculator",
1842 r#"{"operation": "multiply", "a": 4, "b": 3}"#,
1843 ToolContext::new(),
1844 )
1845 .await;
1846 assert_eq!(result.unwrap().as_text(), Some("12"));
1847
1848 let result = tools
1850 .call(
1851 "calculator",
1852 r#"{"operation": "divide", "a": 15, "b": 3}"#,
1853 ToolContext::new(),
1854 )
1855 .await;
1856 assert_eq!(result.unwrap().as_text(), Some("5"));
1857 }
1858
1859 #[tokio::test]
1860 async fn calculator_division_by_zero() {
1861 let mut tools = Tools::new();
1862 tools.register(Calculator).expect("calculator registers");
1863
1864 let result = tools
1865 .call(
1866 "calculator",
1867 r#"{"operation": "divide", "a": 10, "b": 0}"#,
1868 ToolContext::new(),
1869 )
1870 .await;
1871 assert!(result.is_err());
1872 assert!(result.unwrap_err().to_string().contains("Division by zero"));
1873 }
1874
1875 #[tokio::test]
1876 async fn calculator_unknown_operation() {
1877 let mut tools = Tools::new();
1878 tools.register(Calculator).expect("calculator registers");
1879
1880 let result = tools
1881 .call(
1882 "calculator",
1883 r#"{"operation": "modulo", "a": 10, "b": 3}"#,
1884 ToolContext::new(),
1885 )
1886 .await;
1887 assert!(result.is_err());
1888 assert!(
1889 result
1890 .unwrap_err()
1891 .to_string()
1892 .contains("Unknown operation")
1893 );
1894 }
1895
1896 #[tokio::test]
1897 async fn multiple_tools() {
1898 let mut tools = Tools::new();
1899 tools.register(Calculator).expect("calculator registers");
1900 tools.register(Greeter).expect("greeter registers");
1901
1902 let definitions = tools.definitions();
1903 assert_eq!(definitions.len(), 2);
1904
1905 let calc_def = definitions.iter().find(|d| d.name == "calculator").unwrap();
1907 let greet_def = definitions.iter().find(|d| d.name == "greeter").unwrap();
1908
1909 assert_eq!(
1910 calc_def.description,
1911 "Performs basic mathematical operations."
1912 );
1913 assert_eq!(greet_def.description, "Greets a person by name.");
1914
1915 let calc_result = tools
1917 .call(
1918 "calculator",
1919 r#"{"operation": "add", "a": 2, "b": 3}"#,
1920 ToolContext::new(),
1921 )
1922 .await;
1923 assert_eq!(calc_result.unwrap().as_text(), Some("5"));
1924
1925 let greet_result = tools
1926 .call("greeter", r#"{"name": "Alice"}"#, ToolContext::new())
1927 .await;
1928 assert_eq!(greet_result.unwrap().as_text(), Some("Hello, Alice!"));
1929 }
1930
1931 #[derive(Debug, Serialize)]
1932 struct TableRow {
1933 name: &'static str,
1934 count: u32,
1935 }
1936
1937 #[derive(Debug, Serialize)]
1938 struct NestedTableRow {
1939 user: TableRow,
1940 ok: bool,
1941 }
1942
1943 #[derive(Debug)]
1944 struct ToolFailure(&'static str);
1945
1946 impl core::fmt::Display for ToolFailure {
1947 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1948 f.write_str(self.0)
1949 }
1950 }
1951
1952 impl core::error::Error for ToolFailure {}
1953
1954 #[test]
1955 fn into_tool_result_string_is_plain_text() {
1956 assert_eq!(
1957 String::from("hello").into_tool_result().unwrap(),
1958 ToolResult::text("hello")
1959 );
1960 }
1961
1962 #[test]
1963 fn into_tool_result_str_is_plain_text() {
1964 assert_eq!(
1965 "hello".into_tool_result().unwrap(),
1966 ToolResult::text("hello")
1967 );
1968 }
1969
1970 #[test]
1971 fn into_tool_result_option_none_is_done() {
1972 let result = Option::<String>::None.into_tool_result().unwrap();
1973 assert_eq!(result, ToolResult::Done);
1974 }
1975
1976 #[test]
1977 fn into_tool_result_option_some_delegates() {
1978 let result = Some("hello").into_tool_result().unwrap();
1979 assert_eq!(result, ToolResult::text("hello"));
1980 }
1981
1982 #[test]
1983 fn into_tool_result_result_ok_string_is_text() {
1984 let result = core::result::Result::<String, ToolFailure>::Ok(String::from("hello"))
1985 .into_tool_result()
1986 .unwrap();
1987 assert_eq!(result, ToolResult::text("hello"));
1988 }
1989
1990 #[test]
1991 fn into_tool_result_result_ok_object_is_tsv() {
1992 let result = core::result::Result::<TableRow, ToolFailure>::Ok(TableRow {
1993 name: "alpha",
1994 count: 3,
1995 })
1996 .into_tool_result()
1997 .unwrap();
1998
1999 assert_eq!(result, ToolResult::tsv("count\tname\n3\talpha\n"));
2000 }
2001
2002 #[test]
2003 fn into_tool_result_result_ok_array_of_objects_is_tsv() {
2004 let result = core::result::Result::<Vec<TableRow>, ToolFailure>::Ok(vec![
2005 TableRow {
2006 name: "alpha",
2007 count: 3,
2008 },
2009 TableRow {
2010 name: "beta",
2011 count: 5,
2012 },
2013 ])
2014 .into_tool_result()
2015 .unwrap();
2016
2017 assert_eq!(result, ToolResult::tsv("count\tname\n3\talpha\n5\tbeta\n"));
2018 }
2019
2020 #[test]
2021 fn into_tool_result_result_ok_scalar_is_json() {
2022 let result = core::result::Result::<bool, ToolFailure>::Ok(true)
2023 .into_tool_result()
2024 .unwrap();
2025 assert_eq!(result, ToolResult::json_value(Value::Bool(true)));
2026 }
2027
2028 #[test]
2029 fn into_tool_result_result_err_is_typed_error() {
2030 let result = core::result::Result::<TableRow, ToolFailure>::Err(ToolFailure("boom"))
2031 .into_tool_result()
2032 .unwrap();
2033 assert_eq!(result, ToolResult::error("boom"));
2034 assert!(result.is_error());
2035 assert_eq!(result.error_message(), Some("boom"));
2036 }
2037
2038 #[test]
2039 fn json_value_to_tsv_flattens_nested_objects() {
2040 let value = serde_json::to_value(NestedTableRow {
2041 user: TableRow {
2042 name: "alpha",
2043 count: 3,
2044 },
2045 ok: true,
2046 })
2047 .unwrap();
2048
2049 assert_eq!(
2050 json_value_to_tsv(&value),
2051 Some("ok\tuser.count\tuser.name\ntrue\t3\talpha\n".to_string())
2052 );
2053 }
2054
2055 #[tokio::test]
2056 async fn tool_not_found() {
2057 let tools = Tools::new();
2058
2059 let result = tools.call("nonexistent", "{}", ToolContext::new()).await;
2060 assert!(result.is_err());
2061 assert!(
2062 result
2063 .unwrap_err()
2064 .to_string()
2065 .contains("Tool 'nonexistent' not found")
2066 );
2067 }
2068
2069 #[tokio::test]
2070 async fn invalid_json() {
2071 let mut tools = Tools::new();
2072 tools.register(Calculator).expect("calculator registers");
2073
2074 let result = tools
2075 .call("calculator", "invalid json", ToolContext::new())
2076 .await;
2077 assert!(result.is_err());
2078 }
2079
2080 #[test]
2081 fn tools_unregister() {
2082 let mut tools = Tools::new();
2083 tools.register(Calculator).expect("calculator registers");
2084 tools.register(Greeter).expect("greeter registers");
2085
2086 assert_eq!(tools.definitions().len(), 2);
2087
2088 tools.unregister("calculator");
2089 assert_eq!(tools.definitions().len(), 1);
2090
2091 let remaining = &tools.definitions()[0];
2092 assert_eq!(remaining.name, "greeter");
2093
2094 tools.unregister("greeter");
2095 assert_eq!(tools.definitions().len(), 0);
2096 }
2097
2098 #[test]
2099 fn tools_debug() {
2100 let mut tools = Tools::new();
2101 tools.register(Calculator).expect("calculator registers");
2102 tools.register(Greeter).expect("greeter registers");
2103
2104 let debug_str = format!("{tools:?}");
2105 assert!(debug_str.contains("Tools"));
2106 assert!(debug_str.contains("calculator"));
2107 assert!(debug_str.contains("greeter"));
2108 }
2109
2110 #[test]
2111 fn tool_definition_debug() {
2112 let calculator = Calculator;
2113 let definition = ToolDefinition::new(&calculator);
2114 let debug_str = format!("{definition:?}");
2115
2116 assert!(debug_str.contains("ToolDefinition"));
2117 assert!(debug_str.contains("calculator"));
2118 assert!(debug_str.contains("Performs basic mathematical operations"));
2119 }
2120
2121 #[test]
2122 fn tool_definition_clone() {
2123 let calculator = Calculator;
2124 let original = ToolDefinition::new(&calculator);
2125 let cloned = original.clone();
2126
2127 assert_eq!(original.name, cloned.name);
2128 assert_eq!(original.description, cloned.description);
2129 }
2130
2131 #[test]
2132 fn schema_preserves_enum() {
2133 #[derive(JsonSchema, Deserialize)]
2134 #[serde(rename_all = "snake_case")]
2135 enum Status {
2136 Pending,
2137 InProgress,
2138 Completed,
2139 }
2140
2141 #[allow(dead_code)]
2142 #[derive(JsonSchema, Deserialize)]
2143 struct Item {
2144 status: Status,
2145 }
2146
2147 #[allow(dead_code)]
2148 #[derive(JsonSchema, Deserialize)]
2149 struct Args {
2150 items: Vec<Item>,
2151 }
2152
2153 struct TestTool;
2154
2155 impl Tool for TestTool {
2156 fn name(&self) -> Cow<'static, str> {
2157 "test".into()
2158 }
2159 type Arguments = Args;
2160 type Res = ToolResult;
2161
2162 fn call(
2163 &self,
2164 _args: Self::Arguments,
2165 _cx: ToolContext,
2166 ) -> impl Future<Output = Result<Self::Res>> + Send {
2167 core::future::ready(Ok(ToolResult::text("ok")))
2168 }
2169 }
2170
2171 let tool = TestTool;
2172 let def = ToolDefinition::new(&tool);
2173 let schema = def.arguments_openai_schema();
2174
2175 let schema_obj = schema.as_object().expect("schema should be object");
2177 let properties = schema_obj
2178 .get("properties")
2179 .expect("should have properties")
2180 .as_object()
2181 .unwrap();
2182 let items = properties
2183 .get("items")
2184 .expect("should have items")
2185 .as_object()
2186 .unwrap();
2187 let item_props = items
2188 .get("items")
2189 .expect("items should have items schema")
2190 .as_object()
2191 .unwrap();
2192 let item_properties = item_props
2193 .get("properties")
2194 .expect("item should have properties")
2195 .as_object()
2196 .unwrap();
2197 let status = item_properties
2198 .get("status")
2199 .expect("should have status")
2200 .as_object()
2201 .unwrap();
2202
2203 assert!(
2205 status.contains_key("enum"),
2206 "Status should have enum field. Full schema: {}",
2207 serde_json::to_string_pretty(&schema).unwrap()
2208 );
2209 }
2210
2211 #[test]
2212 fn schema_ref_resolution() {
2213 let raw_schema = serde_json::json!({
2215 "type": "object",
2216 "properties": {
2217 "status": {
2218 "$ref": "#/$defs/Status"
2219 }
2220 },
2221 "$defs": {
2222 "Status": {
2223 "type": "string",
2224 "enum": ["pending", "in_progress", "completed"]
2225 }
2226 }
2227 });
2228
2229 let mut schema = raw_schema;
2230 clean_schema(&mut schema);
2231
2232 let props = schema.get("properties").unwrap().as_object().unwrap();
2233 let status = props.get("status").unwrap().as_object().unwrap();
2234
2235 assert!(
2236 status.contains_key("enum"),
2237 "Status should have enum after ref resolution. Got: {}",
2238 serde_json::to_string_pretty(&schema).unwrap()
2239 );
2240 }
2241
2242 #[test]
2243 fn schema_nested_ref_in_array() {
2244 let raw_schema = serde_json::json!({
2246 "type": "object",
2247 "properties": {
2248 "todos": {
2249 "type": "array",
2250 "items": {
2251 "$ref": "#/$defs/TodoItem"
2252 }
2253 }
2254 },
2255 "$defs": {
2256 "TodoItem": {
2257 "type": "object",
2258 "properties": {
2259 "content": { "type": "string" },
2260 "status": { "$ref": "#/$defs/TodoStatus" }
2261 },
2262 "required": ["content", "status"]
2263 },
2264 "TodoStatus": {
2265 "type": "string",
2266 "enum": ["pending", "in_progress", "completed"]
2267 }
2268 }
2269 });
2270
2271 let mut schema = raw_schema;
2272 clean_schema(&mut schema);
2273
2274 let props = schema.get("properties").unwrap().as_object().unwrap();
2276 let todos = props.get("todos").unwrap().as_object().unwrap();
2277 let items = todos.get("items").unwrap().as_object().unwrap();
2278 let item_props = items.get("properties").unwrap().as_object().unwrap();
2279 let status = item_props.get("status").unwrap().as_object().unwrap();
2280
2281 assert!(
2282 status.contains_key("enum"),
2283 "Nested status should have enum. Full schema: {}",
2284 serde_json::to_string_pretty(&schema).unwrap()
2285 );
2286 }
2287
2288 #[test]
2289 fn parts_collapses_degenerate_inputs() {
2290 assert_eq!(ToolResult::parts(vec![]), ToolResult::Done);
2291 assert_eq!(
2292 ToolResult::parts(vec![ToolResultPart::text("only")]),
2293 ToolResult::text("only")
2294 );
2295 let multi = ToolResult::parts(vec![
2296 ToolResultPart::text("a"),
2297 ToolResultPart::image(vec![1, 2], "image/png"),
2298 ]);
2299 let ToolResult::Parts { parts } = &multi else {
2300 panic!("two parts must stay a Parts result");
2301 };
2302 assert_eq!(parts.len(), 2);
2303 }
2304
2305 #[test]
2306 fn parts_render_joins_text_and_marks_binary() {
2307 let result = ToolResult::parts(vec![
2308 ToolResultPart::text("header"),
2309 ToolResultPart::json_value(serde_json::json!({"n": 1})),
2310 ToolResultPart::image(vec![0x89], "image/png"),
2311 ]);
2312 assert_eq!(
2313 result.render_for_model().unwrap(),
2314 "header\n{\"n\":1}\n[binary tool result: image/png, 1 bytes]"
2315 );
2316 assert!(result.mime().is_none() && result.content().is_none());
2317 }
2318
2319 #[test]
2320 fn parts_with_one_binary_expose_mime_and_content() {
2321 let result = ToolResult::Parts {
2322 parts: vec![ToolResultPart::image(vec![9, 9], "image/png")],
2323 };
2324 assert_eq!(result.mime().unwrap().essence_str(), "image/png");
2325 assert_eq!(result.content().unwrap(), &[9, 9]);
2326 }
2327
2328 #[cfg(feature = "serde")]
2329 #[test]
2330 fn parts_serde_roundtrip_uses_kind_tag() {
2331 let result = ToolResult::parts(vec![
2332 ToolResultPart::text("note"),
2333 ToolResultPart::json_value(serde_json::json!({"k": true})),
2334 ]);
2335 let value = serde_json::to_value(&result).unwrap();
2336 assert_eq!(value["kind"], "parts");
2337 assert_eq!(value["parts"][0]["kind"], "text");
2338 let back: ToolResult = serde_json::from_value(value).unwrap();
2339 assert_eq!(back, result);
2340 }
2341}