1use async_trait::async_trait;
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use std::collections::HashMap;
15use std::sync::Arc;
16use tokio::sync::mpsc;
17use tokio_util::sync::CancellationToken;
18
19use crate::error::{ToolError, ToolValidationError};
20pub use crate::types::ToolResultBlock;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum ExecutionMode {
32 Parallel,
33 Sequential,
34}
35
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38pub struct ToolCall {
39 pub id: String,
40 pub name: String,
41 pub arguments: Value,
42}
43
44pub const ARG_PARSE_ERROR_MARKER: &str = "__clark_arg_parse_error";
53
54pub const ARG_PARSE_RAW_MARKER: &str = "__clark_arg_raw";
58
59pub fn arg_parse_error_value(error: impl Into<String>, raw: impl Into<String>) -> Value {
63 serde_json::json!({
64 ARG_PARSE_ERROR_MARKER: error.into(),
65 ARG_PARSE_RAW_MARKER: raw.into(),
66 })
67}
68
69pub fn detect_arg_parse_error(args: &Value) -> Option<(&str, &str)> {
72 let obj = args.as_object()?;
73 let err = obj.get(ARG_PARSE_ERROR_MARKER)?.as_str()?;
74 let raw = obj.get(ARG_PARSE_RAW_MARKER)?.as_str()?;
75 Some((err, raw))
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct ToolResult {
91 pub content: Vec<ToolResultBlock>,
92 #[serde(default, skip_serializing_if = "is_false")]
93 pub is_error: bool,
94 #[serde(default, skip_serializing_if = "Value::is_null")]
95 pub details: Value,
96 #[serde(default, skip_serializing_if = "is_false")]
97 pub terminate: bool,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub narration: Option<String>,
100}
101
102fn is_false(b: &bool) -> bool {
103 !*b
104}
105
106impl ToolResult {
107 pub fn text(text: impl Into<String>) -> Self {
109 Self {
110 content: vec![ToolResultBlock::Text(crate::types::TextContent {
111 text: text.into(),
112 })],
113 is_error: false,
114 details: Value::Null,
115 terminate: false,
116 narration: None,
117 }
118 }
119
120 pub fn terminal(text: impl Into<String>) -> Self {
122 Self {
123 content: vec![ToolResultBlock::Text(crate::types::TextContent {
124 text: text.into(),
125 })],
126 is_error: false,
127 details: Value::Null,
128 terminate: true,
129 narration: None,
130 }
131 }
132
133 pub fn error(text: impl Into<String>) -> Self {
136 Self {
137 content: vec![ToolResultBlock::Text(crate::types::TextContent {
138 text: text.into(),
139 })],
140 is_error: true,
141 details: Value::Null,
142 terminate: false,
143 narration: None,
144 }
145 }
146
147 pub fn with_narration(mut self, narration: impl Into<String>) -> Self {
151 let raw: String = narration.into();
152 let trimmed = raw.trim();
153 if !trimmed.is_empty() {
154 self.narration = Some(trimmed.to_string());
155 }
156 self
157 }
158}
159
160pub type ToolUpdateSink = mpsc::UnboundedSender<ToolResult>;
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub struct ToolHistoryPolicy {
174 pub dedup_arg: Option<&'static str>,
178 pub summary_arg: Option<&'static str>,
180 pub compactable_result: bool,
183 pub pins_active_plan: bool,
186}
187
188impl ToolHistoryPolicy {
189 pub const fn new() -> Self {
190 Self {
191 dedup_arg: None,
192 summary_arg: None,
193 compactable_result: false,
194 pins_active_plan: false,
195 }
196 }
197
198 pub const fn dedup_arg(mut self, arg: &'static str) -> Self {
199 self.dedup_arg = Some(arg);
200 self
201 }
202
203 pub const fn summary_arg(mut self, arg: &'static str) -> Self {
204 self.summary_arg = Some(arg);
205 self
206 }
207
208 pub const fn compactable_result(mut self) -> Self {
209 self.compactable_result = true;
210 self
211 }
212
213 pub const fn pins_active_plan(mut self) -> Self {
214 self.pins_active_plan = true;
215 self
216 }
217}
218
219impl Default for ToolHistoryPolicy {
220 fn default() -> Self {
221 Self::new()
222 }
223}
224
225#[async_trait]
230pub trait AgentTool: Send + Sync + 'static {
231 fn name(&self) -> &str;
232
233 fn description(&self) -> &str;
234
235 fn parameters_schema(&self) -> Value;
238
239 fn requires_exclusive_sandbox(&self) -> bool {
255 false
256 }
257
258 fn max_result_chars(&self) -> Option<usize> {
274 None
275 }
276
277 fn history_policy(&self) -> ToolHistoryPolicy {
281 ToolHistoryPolicy::default()
282 }
283
284 fn identity_policy(&self) -> crate::tool_identity::ToolIdentityPolicy {
292 crate::tool_identity::ToolIdentityPolicy::default()
293 }
294
295 fn aborts_siblings_on_error(&self) -> bool {
313 false
314 }
315
316 fn counts_toward_tool_call_limit(&self) -> bool {
325 true
326 }
327
328 fn parallel_safe_per_turn(&self) -> bool {
340 false
341 }
342
343 fn counts_toward_termination_vote(&self) -> bool {
359 true
360 }
361
362 fn prepare_arguments(&self, args: Value) -> Value {
365 args
366 }
367
368 fn validate(&self, _args: &Value) -> Result<(), ToolValidationError> {
372 Ok(())
373 }
374
375 async fn execute(
380 &self,
381 call_id: &str,
382 args: Value,
383 signal: CancellationToken,
384 update: ToolUpdateSink,
385 ) -> Result<ToolResult, ToolError>;
386}
387
388#[async_trait]
411pub trait TypedAgentTool: Send + Sync + 'static {
412 type Args: serde::de::DeserializeOwned + schemars::JsonSchema + Send + 'static;
416
417 fn name(&self) -> &str;
418 fn description(&self) -> &str;
419
420 fn requires_exclusive_sandbox(&self) -> bool {
422 false
423 }
424
425 fn max_result_chars(&self) -> Option<usize> {
428 None
429 }
430
431 fn history_policy(&self) -> ToolHistoryPolicy {
434 ToolHistoryPolicy::default()
435 }
436
437 fn identity_policy(&self) -> crate::tool_identity::ToolIdentityPolicy {
441 crate::tool_identity::ToolIdentityPolicy::default()
442 }
443
444 fn aborts_siblings_on_error(&self) -> bool {
447 false
448 }
449
450 fn counts_toward_tool_call_limit(&self) -> bool {
453 true
454 }
455
456 fn parallel_safe_per_turn(&self) -> bool {
460 false
461 }
462
463 fn counts_toward_termination_vote(&self) -> bool {
468 true
469 }
470
471 fn prepare_arguments(&self, args: Value) -> Value {
479 args
480 }
481
482 async fn run(
484 &self,
485 call_id: &str,
486 args: Self::Args,
487 signal: CancellationToken,
488 update: ToolUpdateSink,
489 ) -> Result<ToolResult, ToolError>;
490}
491
492#[async_trait]
499impl<T: TypedAgentTool> AgentTool for T {
500 fn name(&self) -> &str {
501 TypedAgentTool::name(self)
502 }
503
504 fn description(&self) -> &str {
505 TypedAgentTool::description(self)
506 }
507
508 fn parameters_schema(&self) -> Value {
509 let settings = schemars::gen::SchemaSettings::draft07().with(|s| {
510 s.inline_subschemas = true;
511 });
512 let generator = settings.into_generator();
513 let schema = generator.into_root_schema_for::<T::Args>();
514 let value = serde_json::to_value(schema).expect("typed-tool schema serializes");
515 let mut value = flatten_tagged_oneof_schema(value);
516 normalize_strict_validator_quirks(&mut value);
517 value
518 }
519
520 fn requires_exclusive_sandbox(&self) -> bool {
521 TypedAgentTool::requires_exclusive_sandbox(self)
522 }
523
524 fn max_result_chars(&self) -> Option<usize> {
525 TypedAgentTool::max_result_chars(self)
526 }
527
528 fn history_policy(&self) -> ToolHistoryPolicy {
529 TypedAgentTool::history_policy(self)
530 }
531
532 fn identity_policy(&self) -> crate::tool_identity::ToolIdentityPolicy {
533 TypedAgentTool::identity_policy(self)
534 }
535
536 fn aborts_siblings_on_error(&self) -> bool {
537 TypedAgentTool::aborts_siblings_on_error(self)
538 }
539
540 fn counts_toward_tool_call_limit(&self) -> bool {
541 TypedAgentTool::counts_toward_tool_call_limit(self)
542 }
543
544 fn parallel_safe_per_turn(&self) -> bool {
545 TypedAgentTool::parallel_safe_per_turn(self)
546 }
547
548 fn counts_toward_termination_vote(&self) -> bool {
549 TypedAgentTool::counts_toward_termination_vote(self)
550 }
551
552 fn prepare_arguments(&self, args: Value) -> Value {
553 TypedAgentTool::prepare_arguments(self, args)
554 }
555
556 async fn execute(
557 &self,
558 call_id: &str,
559 args: Value,
560 signal: CancellationToken,
561 update: ToolUpdateSink,
562 ) -> Result<ToolResult, ToolError> {
563 let prepared = AgentTool::prepare_arguments(self, args);
581 let stripped = strip_top_level_nulls(prepared);
582 let schema = AgentTool::parameters_schema(self);
591 let coerced = coerce_string_scalars_at_top_level(stripped, &schema);
592 let parsed: T::Args = match serde_json::from_value(coerced) {
593 Ok(v) => v,
594 Err(e) => {
595 return Ok(ToolResult::error(format!(
596 "{}: invalid arguments: {}",
597 TypedAgentTool::name(self),
598 enrich_arg_parse_error_message(&e),
599 )));
600 }
601 };
602 TypedAgentTool::run(self, call_id, parsed, signal, update).await
603 }
604}
605
606fn coerce_string_scalars_at_top_level(value: Value, schema: &Value) -> Value {
617 let Value::Object(mut map) = value else {
618 return value;
619 };
620 let Some(properties) = schema.get("properties").and_then(Value::as_object) else {
621 return Value::Object(map);
622 };
623 for (key, val) in map.iter_mut() {
624 let Some(prop_schema) = properties.get(key) else {
625 continue;
626 };
627 coerce_one_scalar_in_place(val, prop_schema);
628 }
629 Value::Object(map)
630}
631
632fn coerce_one_scalar_in_place(value: &mut Value, prop_schema: &Value) {
633 let Some(text) = value.as_str() else {
634 return;
635 };
636 let Some(target) = scalar_target_from_schema(prop_schema) else {
637 return;
638 };
639 match target {
640 ScalarTarget::Integer => {
641 let trimmed = text.trim();
642 if let Ok(n) = trimmed.parse::<i64>() {
643 *value = Value::Number(serde_json::Number::from(n));
644 } else if let Ok(n) = trimmed.parse::<u64>() {
645 *value = Value::Number(serde_json::Number::from(n));
646 }
647 }
648 ScalarTarget::Number => {
649 let trimmed = text.trim();
650 if let Ok(n) = trimmed.parse::<f64>() {
651 if let Some(num) = serde_json::Number::from_f64(n) {
652 *value = Value::Number(num);
653 }
654 }
655 }
656 ScalarTarget::Boolean => match text.trim() {
657 "true" | "True" | "TRUE" => *value = Value::Bool(true),
658 "false" | "False" | "FALSE" => *value = Value::Bool(false),
659 _ => {}
660 },
661 }
662}
663
664#[derive(Debug, Clone, Copy)]
665enum ScalarTarget {
666 Integer,
667 Number,
668 Boolean,
669}
670
671fn scalar_target_from_schema(prop_schema: &Value) -> Option<ScalarTarget> {
672 let type_field = prop_schema.get("type")?;
673 let single = match type_field {
674 Value::String(s) => Some(s.as_str()),
675 Value::Array(arr) => {
679 let non_null: Vec<&str> = arr
680 .iter()
681 .filter_map(|v| v.as_str())
682 .filter(|s| *s != "null")
683 .collect();
684 if non_null.len() == 1 {
685 Some(non_null[0])
686 } else {
687 None
688 }
689 }
690 _ => None,
691 }?;
692 match single {
693 "integer" => Some(ScalarTarget::Integer),
694 "number" => Some(ScalarTarget::Number),
695 "boolean" => Some(ScalarTarget::Boolean),
696 _ => None,
697 }
698}
699
700fn enrich_arg_parse_error_message(err: &serde_json::Error) -> String {
706 let raw = err.to_string();
707 match arg_parse_hint(&raw) {
708 Some(hint) => format!("{raw}. {hint}"),
709 None => raw,
710 }
711}
712
713fn arg_parse_hint(raw: &str) -> Option<String> {
714 let value = extract_invalid_string_value(raw)?;
715 if expects_integer(raw) {
716 let parsed: i128 = value.trim().parse().ok()?;
717 return Some(format!(
718 "Did you mean the integer {parsed}? Resend without quotes."
719 ));
720 }
721 if expects_number(raw) {
722 let parsed: f64 = value.trim().parse().ok()?;
723 return Some(format!(
724 "Did you mean the number {parsed}? Resend without quotes."
725 ));
726 }
727 if expects_boolean(raw) {
728 return match value.trim() {
729 "true" | "True" | "TRUE" => Some(
730 "Did you mean true? Resend as a boolean literal (lowercase, no quotes)."
731 .to_string(),
732 ),
733 "false" | "False" | "FALSE" => Some(
734 "Did you mean false? Resend as a boolean literal (lowercase, no quotes)."
735 .to_string(),
736 ),
737 _ => None,
738 };
739 }
740 if expects_sequence(raw) {
741 return Some(
742 "Expected a JSON array (e.g. `[{...}, {...}]`); the field cannot be a string. \
743 Resend the value as an array of structured items, not a string of XML-like markup."
744 .to_string(),
745 );
746 }
747 None
748}
749
750fn extract_invalid_string_value(raw: &str) -> Option<&str> {
751 let start = raw.find("string \"")? + "string \"".len();
755 let rest = &raw[start..];
756 let end = rest.find('\"')?;
757 Some(&rest[..end])
758}
759
760fn expects_integer(raw: &str) -> bool {
761 raw.contains("expected usize")
762 || raw.contains("expected isize")
763 || raw.contains("expected u8")
764 || raw.contains("expected u16")
765 || raw.contains("expected u32")
766 || raw.contains("expected u64")
767 || raw.contains("expected i8")
768 || raw.contains("expected i16")
769 || raw.contains("expected i32")
770 || raw.contains("expected i64")
771 || raw.contains("expected integer")
772}
773
774fn expects_number(raw: &str) -> bool {
775 raw.contains("expected f32")
776 || raw.contains("expected f64")
777 || raw.contains("expected floating point")
778}
779
780fn expects_boolean(raw: &str) -> bool {
781 raw.contains("expected a boolean") || raw.contains("expected bool")
782}
783
784fn expects_sequence(raw: &str) -> bool {
785 raw.contains("expected a sequence") || raw.contains("expected an array")
786}
787
788fn strip_top_level_nulls(value: Value) -> Value {
789 match value {
790 Value::Object(map) => {
791 Value::Object(map.into_iter().filter(|(_, v)| !v.is_null()).collect())
792 }
793 other => other,
794 }
795}
796
797fn flatten_tagged_oneof_schema(schema: Value) -> Value {
812 let Value::Object(mut root) = schema else {
813 return schema;
814 };
815 let Some(Value::Array(variants)) = root.remove("oneOf") else {
816 if !root.is_empty() {
818 return Value::Object(root);
819 }
820 return Value::Null;
821 };
822
823 struct VariantSpec {
833 tag_value_str: Option<String>,
834 own_field_names: Vec<String>,
835 }
836
837 let mut discriminator: Option<String> = None;
838 let mut variant_specs: Vec<VariantSpec> = Vec::with_capacity(variants.len());
839 let mut merged_props = serde_json::Map::new();
840 let mut required_set: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
841 let mut tag_in_required = true;
842 let mut tag_values: Vec<Value> = Vec::with_capacity(variants.len());
845
846 for variant in &variants {
847 let Some(obj) = variant.as_object() else {
848 return reassemble_oneof(root, variants);
849 };
850 let Some(Value::Object(props)) = obj.get("properties").cloned() else {
851 return reassemble_oneof(root, variants);
852 };
853 let mut variant_tag: Option<(String, Value)> = None;
856 for (name, prop) in props.iter() {
857 let Some(prop_obj) = prop.as_object() else {
858 continue;
859 };
860 let Some(Value::Array(enum_values)) = prop_obj.get("enum").cloned() else {
861 continue;
862 };
863 if enum_values.len() == 1 {
864 variant_tag = Some((name.clone(), enum_values.into_iter().next().unwrap()));
865 break;
866 }
867 }
868 let Some((tag_name, tag_value)) = variant_tag else {
869 return reassemble_oneof(root, variants);
870 };
871 match &discriminator {
872 None => discriminator = Some(tag_name.clone()),
873 Some(existing) if existing == &tag_name => {}
874 Some(_) => return reassemble_oneof(root, variants),
875 }
876 tag_values.push(tag_value.clone());
877
878 let mut own_field_names = Vec::new();
881 for (name, prop_schema) in props.iter() {
882 if name == &tag_name {
883 continue;
884 }
885 merged_props
886 .entry(name.clone())
887 .or_insert_with(|| prop_schema.clone());
888 own_field_names.push(name.clone());
889 }
890
891 let mut tag_required_here = false;
897 if let Some(Value::Array(req)) = obj.get("required") {
898 for r in req {
899 if let Some(s) = r.as_str() {
900 if s == tag_name {
901 tag_required_here = true;
902 }
903 }
904 }
905 }
906 if !tag_required_here {
907 tag_in_required = false;
908 }
909
910 variant_specs.push(VariantSpec {
911 tag_value_str: tag_value.as_str().map(str::to_string),
912 own_field_names,
913 });
914 }
915
916 let Some(discriminator) = discriminator else {
917 return reassemble_oneof(root, variants);
918 };
919
920 let total_variants = variant_specs.len();
925 let all_tags_are_strings = variant_specs.iter().all(|s| s.tag_value_str.is_some());
926 if all_tags_are_strings && total_variants > 1 {
927 let mut owners: std::collections::BTreeMap<String, Vec<String>> =
928 std::collections::BTreeMap::new();
929 for spec in &variant_specs {
930 let tag_label = spec.tag_value_str.clone().unwrap_or_default();
931 for field in &spec.own_field_names {
932 owners
933 .entry(field.clone())
934 .or_default()
935 .push(tag_label.clone());
936 }
937 }
938 for (field, mut variant_tags) in owners {
939 if variant_tags.len() == total_variants {
940 continue;
941 }
942 variant_tags.sort();
943 variant_tags.dedup();
944 let suffix = format!(
945 " (applies when {discriminator} in: [{}])",
946 variant_tags.join(", ")
947 );
948 if let Some(Value::Object(prop_map)) = merged_props.get_mut(&field) {
949 let new_desc = match prop_map.get("description") {
950 Some(Value::String(existing)) if !existing.is_empty() => {
951 format!("{existing}{suffix}")
952 }
953 _ => suffix.trim_start().to_string(),
954 };
955 prop_map.insert("description".to_string(), Value::String(new_desc));
956 }
957 }
958 }
959
960 let mut tag_prop = serde_json::Map::new();
966 tag_prop.insert("type".to_string(), Value::String("string".to_string()));
967 tag_prop.insert("enum".to_string(), Value::Array(tag_values));
968 let mut ordered_props = serde_json::Map::new();
969 ordered_props.insert(discriminator.clone(), Value::Object(tag_prop));
970 for (name, schema) in merged_props {
971 ordered_props.insert(name, schema);
972 }
973 if tag_in_required {
974 required_set.insert(discriminator);
975 }
976
977 let mut out = serde_json::Map::new();
978 if let Some(desc) = root.remove("description") {
979 out.insert("description".to_string(), desc);
980 }
981 if let Some(schema) = root.remove("$schema") {
982 out.insert("$schema".to_string(), schema);
983 }
984 out.insert("type".to_string(), Value::String("object".to_string()));
985 out.insert("properties".to_string(), Value::Object(ordered_props));
986 if !required_set.is_empty() {
987 out.insert(
988 "required".to_string(),
989 Value::Array(required_set.into_iter().map(Value::String).collect()),
990 );
991 }
992 Value::Object(out)
993}
994
995fn reassemble_oneof(mut root: serde_json::Map<String, Value>, variants: Vec<Value>) -> Value {
996 root.insert("oneOf".to_string(), Value::Array(variants));
997 Value::Object(root)
998}
999
1000fn normalize_strict_validator_quirks(value: &mut Value) {
1012 match value {
1013 Value::Object(map) => {
1014 if let Some(items) = map.get_mut("items") {
1016 if matches!(items, Value::Bool(true)) {
1017 *items = Value::Object(serde_json::Map::new());
1018 }
1019 }
1020 for v in map.values_mut() {
1021 normalize_strict_validator_quirks(v);
1022 }
1023 }
1024 Value::Array(arr) => {
1025 for v in arr {
1026 normalize_strict_validator_quirks(v);
1027 }
1028 }
1029 _ => {}
1030 }
1031}
1032
1033#[derive(Default, Clone)]
1035pub struct ToolRegistry {
1036 tools: HashMap<String, Arc<dyn AgentTool>>,
1037 order: Vec<String>,
1038}
1039
1040impl ToolRegistry {
1041 pub fn new() -> Self {
1042 Self::default()
1043 }
1044
1045 pub fn with(mut self, tool: Arc<dyn AgentTool>) -> Self {
1046 self.register(tool);
1047 self
1048 }
1049
1050 pub fn register(&mut self, tool: Arc<dyn AgentTool>) {
1051 let name = tool.name().to_string();
1052 if !self.tools.contains_key(&name) {
1053 self.order.push(name.clone());
1054 }
1055 self.tools.insert(name, tool);
1056 }
1057
1058 pub fn get(&self, name: &str) -> Option<Arc<dyn AgentTool>> {
1059 self.tools.get(name).cloned()
1060 }
1061
1062 pub fn history_policy(&self, name: &str) -> ToolHistoryPolicy {
1063 self.tools
1064 .get(name)
1065 .map(|tool| tool.history_policy())
1066 .unwrap_or_default()
1067 }
1068
1069 pub fn identity_policy(&self, name: &str) -> crate::tool_identity::ToolIdentityPolicy {
1075 self.tools
1076 .get(name)
1077 .map(|tool| tool.identity_policy())
1078 .unwrap_or_default()
1079 }
1080
1081 pub fn identity_policies(
1086 &self,
1087 ) -> std::collections::HashMap<String, crate::tool_identity::ToolIdentityPolicy> {
1088 self.tools
1089 .iter()
1090 .map(|(name, tool)| (name.clone(), tool.identity_policy()))
1091 .collect()
1092 }
1093
1094 pub fn names(&self) -> Vec<&str> {
1095 self.order.iter().map(String::as_str).collect()
1096 }
1097
1098 pub fn iter(&self) -> impl Iterator<Item = &Arc<dyn AgentTool>> {
1099 self.order.iter().filter_map(|name| self.tools.get(name))
1100 }
1101
1102 pub fn is_empty(&self) -> bool {
1103 self.tools.is_empty()
1104 }
1105
1106 pub fn len(&self) -> usize {
1107 self.tools.len()
1108 }
1109}
1110
1111impl std::fmt::Debug for ToolRegistry {
1112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1113 f.debug_struct("ToolRegistry")
1114 .field("tools", &self.order)
1115 .finish()
1116 }
1117}
1118
1119#[cfg(test)]
1120mod tests {
1121 use super::*;
1122 use crate::types::TextContent;
1123 use schemars::JsonSchema;
1124 use serde::Deserialize;
1125
1126 #[derive(Deserialize, JsonSchema)]
1129 #[serde(deny_unknown_fields)]
1130 #[allow(dead_code)]
1131 struct DocVariantArgs {
1132 filename: String,
1133 #[serde(default)]
1134 title: Option<String>,
1135 }
1136
1137 #[derive(Deserialize, JsonSchema)]
1138 #[serde(deny_unknown_fields)]
1139 #[allow(dead_code)]
1140 struct ExcelVariantArgs {
1141 filename: String,
1142 #[serde(default)]
1143 rows: Vec<Vec<serde_json::Value>>,
1144 }
1145
1146 #[derive(Deserialize, JsonSchema)]
1147 #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1148 #[allow(dead_code)]
1149 enum ExampleArgs {
1150 Document(DocVariantArgs),
1151 Excel(ExcelVariantArgs),
1152 }
1153
1154 fn build_example_schema() -> Value {
1155 let settings = schemars::gen::SchemaSettings::draft07().with(|s| {
1156 s.inline_subschemas = true;
1157 });
1158 let g = settings.into_generator();
1159 let s = g.into_root_schema_for::<ExampleArgs>();
1160 let raw = serde_json::to_value(s).unwrap();
1161 flatten_tagged_oneof_schema(raw)
1162 }
1163
1164 #[derive(Deserialize, JsonSchema)]
1165 #[serde(deny_unknown_fields)]
1166 #[allow(dead_code)]
1167 struct NonAlphabeticOrderCanaryArgs {
1168 zeta_selector: String,
1169 alpha_payload: String,
1170 middle_payload: String,
1171 }
1172
1173 #[test]
1174 fn schema_runtime_preserves_insertion_order_for_tool_objects() {
1175 let mut object = serde_json::Map::new();
1180 object.insert("zeta_selector".to_string(), Value::String("z".to_string()));
1181 object.insert("alpha_payload".to_string(), Value::String("a".to_string()));
1182 object.insert("middle_payload".to_string(), Value::String("m".to_string()));
1183
1184 let keys = object.keys().map(String::as_str).collect::<Vec<_>>();
1185 assert_eq!(
1186 keys,
1187 ["zeta_selector", "alpha_payload", "middle_payload"],
1188 "serde_json::Map must keep insertion order; losing this breaks \
1189 model-facing tool-schema property order"
1190 );
1191
1192 let serialized = serde_json::to_string(&Value::Object(object)).unwrap();
1193 assert_eq!(
1194 serialized, r#"{"zeta_selector":"z","alpha_payload":"a","middle_payload":"m"}"#,
1195 "schema JSON serialization must preserve object insertion order"
1196 );
1197 }
1198
1199 #[test]
1200 fn schemars_preserves_declared_struct_order_for_tool_args() {
1201 let settings = schemars::gen::SchemaSettings::draft07().with(|s| {
1206 s.inline_subschemas = true;
1207 });
1208 let schema = serde_json::to_value(
1209 settings
1210 .into_generator()
1211 .into_root_schema_for::<NonAlphabeticOrderCanaryArgs>(),
1212 )
1213 .expect("schema serializes");
1214 let props = schema
1215 .get("properties")
1216 .and_then(Value::as_object)
1217 .expect("schema must expose properties");
1218 let order = props.keys().map(String::as_str).collect::<Vec<_>>();
1219 assert_eq!(
1220 order,
1221 ["zeta_selector", "alpha_payload", "middle_payload"],
1222 "schemars must emit Args fields in declaration order for \
1223 autoregressive tool-call conditioning"
1224 );
1225 }
1226
1227 #[test]
1228 fn flatten_tagged_oneof_produces_flat_object_schema() {
1229 let s = build_example_schema();
1230 assert_eq!(s.get("type").and_then(Value::as_str), Some("object"));
1231 assert!(s.get("oneOf").is_none());
1233 let kind_prop = s.pointer("/properties/kind").expect("kind property");
1236 assert_eq!(
1237 kind_prop.get("type").and_then(Value::as_str),
1238 Some("string")
1239 );
1240 let kind_enum = kind_prop
1241 .get("enum")
1242 .and_then(Value::as_array)
1243 .expect("enum");
1244 let mut kinds: Vec<&str> = kind_enum.iter().filter_map(Value::as_str).collect();
1245 kinds.sort();
1246 assert_eq!(kinds, vec!["document", "excel"]);
1247 let props = s
1248 .get("properties")
1249 .and_then(Value::as_object)
1250 .expect("properties");
1251 let order: Vec<&str> = props.keys().map(String::as_str).collect();
1252 assert_eq!(
1253 order.first().copied(),
1254 Some("kind"),
1255 "discriminator must be emitted before payload fields so \
1256 variant-specific keys are conditioned on the selected kind"
1257 );
1258 assert!(s.pointer("/properties/filename").is_some());
1260 assert!(s.pointer("/properties/title").is_some());
1261 assert!(s.pointer("/properties/rows").is_some());
1262 let req = s
1264 .get("required")
1265 .and_then(Value::as_array)
1266 .expect("required");
1267 assert!(req.iter().any(|v| v.as_str() == Some("kind")));
1268 }
1269
1270 #[test]
1271 fn flatten_tagged_oneof_annotates_variant_specific_property_descriptions() {
1272 let s = build_example_schema();
1287
1288 let filename_desc = s
1291 .pointer("/properties/filename/description")
1292 .and_then(Value::as_str)
1293 .unwrap_or_default();
1294 assert!(
1295 !filename_desc.contains("applies when kind in"),
1296 "shared property `filename` must NOT carry a narrowing \
1297 suffix; got: {filename_desc:?}"
1298 );
1299
1300 let title_desc = s
1302 .pointer("/properties/title/description")
1303 .and_then(Value::as_str)
1304 .expect("title description present");
1305 assert!(
1306 title_desc.contains("applies when kind in: [document]"),
1307 "Document-only `title` must declare its variant scope; \
1308 got: {title_desc:?}"
1309 );
1310 let rows_desc = s
1311 .pointer("/properties/rows/description")
1312 .and_then(Value::as_str)
1313 .expect("rows description present");
1314 assert!(
1315 rows_desc.contains("applies when kind in: [excel]"),
1316 "Excel-only `rows` must declare its variant scope; \
1317 got: {rows_desc:?}"
1318 );
1319
1320 assert!(
1323 s.get("allOf").is_none(),
1324 "top-level allOf would be rejected by Azure's tool validator"
1325 );
1326 assert!(s.get("oneOf").is_none());
1327 assert!(s.get("anyOf").is_none());
1328 }
1329
1330 #[test]
1331 fn normalize_strict_quirks_rewrites_items_true_to_empty_object() {
1332 let mut schema = serde_json::json!({
1338 "type": "object",
1339 "properties": {
1340 "rows": {
1341 "type": "array",
1342 "items": {
1343 "type": "array",
1344 "items": true
1345 }
1346 }
1347 }
1348 });
1349 normalize_strict_validator_quirks(&mut schema);
1350 assert_eq!(
1351 schema.pointer("/properties/rows/items/items"),
1352 Some(&serde_json::json!({})),
1353 );
1354 }
1355
1356 #[test]
1357 fn strip_top_level_nulls_removes_inapplicable_variant_fields() {
1358 let model_payload = serde_json::json!({
1366 "action": "run",
1367 "command": "echo hi",
1368 "workdir": "/home/user/workspace",
1369 "code": null,
1371 "interpreter": null,
1372 "ext": null,
1373 "exec_dir": null,
1374 "max_token": null,
1375 "truncate_from": null,
1376 "run_id": null,
1377 "after_seq": null,
1378 "max_events": null,
1379 "timeout_s": null,
1380 "timeout_ms": null,
1381 "terminal": null,
1382 "force": null,
1383 "timeout_secs": 60,
1385 });
1386 let stripped = strip_top_level_nulls(model_payload);
1387 let obj = stripped.as_object().expect("object");
1388 assert!(!obj.contains_key("code"));
1390 assert!(!obj.contains_key("ext"));
1391 assert!(!obj.contains_key("max_token"));
1392 assert!(!obj.contains_key("force"));
1393 assert_eq!(obj.get("action").and_then(Value::as_str), Some("run"));
1395 assert_eq!(obj.get("command").and_then(Value::as_str), Some("echo hi"));
1396 assert_eq!(obj.get("timeout_secs").and_then(Value::as_i64), Some(60));
1397 }
1398
1399 #[test]
1400 fn strip_top_level_nulls_passes_through_non_object_values() {
1401 assert_eq!(
1405 strip_top_level_nulls(serde_json::json!("text")),
1406 serde_json::json!("text")
1407 );
1408 assert_eq!(strip_top_level_nulls(Value::Null), Value::Null);
1409 }
1410
1411 fn make_schema(properties: Value) -> Value {
1422 serde_json::json!({
1423 "type": "object",
1424 "properties": properties,
1425 })
1426 }
1427
1428 #[test]
1429 fn coerce_string_to_integer_when_schema_says_integer() {
1430 let schema = make_schema(serde_json::json!({
1431 "max_iterations": {"type": "integer"},
1432 }));
1433 let coerced = coerce_string_scalars_at_top_level(
1434 serde_json::json!({"max_iterations": "50"}),
1435 &schema,
1436 );
1437 assert_eq!(coerced, serde_json::json!({"max_iterations": 50}));
1438 }
1439
1440 #[test]
1441 fn coerce_string_to_integer_handles_negative_and_whitespace() {
1442 let schema = make_schema(serde_json::json!({
1443 "offset": {"type": "integer"},
1444 "limit": {"type": "integer"},
1445 }));
1446 let coerced = coerce_string_scalars_at_top_level(
1447 serde_json::json!({"offset": "-7", "limit": " 42 "}),
1448 &schema,
1449 );
1450 assert_eq!(coerced, serde_json::json!({"offset": -7, "limit": 42}));
1451 }
1452
1453 #[test]
1454 fn coerce_string_to_boolean_for_each_case_variant() {
1455 let schema = make_schema(serde_json::json!({
1456 "full_page": {"type": "boolean"},
1457 "headless": {"type": "boolean"},
1458 "verbose": {"type": "boolean"},
1459 "untouched": {"type": "boolean"},
1460 }));
1461 let coerced = coerce_string_scalars_at_top_level(
1462 serde_json::json!({
1463 "full_page": "true",
1464 "headless": "True",
1465 "verbose": "FALSE",
1466 "untouched": "maybe",
1467 }),
1468 &schema,
1469 );
1470 assert_eq!(coerced["full_page"], serde_json::json!(true));
1473 assert_eq!(coerced["headless"], serde_json::json!(true));
1474 assert_eq!(coerced["verbose"], serde_json::json!(false));
1475 assert_eq!(coerced["untouched"], serde_json::json!("maybe"));
1476 }
1477
1478 #[test]
1479 fn coerce_string_to_number_for_float_schema() {
1480 let schema = make_schema(serde_json::json!({
1481 "temperature": {"type": "number"},
1482 }));
1483 let coerced =
1484 coerce_string_scalars_at_top_level(serde_json::json!({"temperature": "0.7"}), &schema);
1485 let n = coerced["temperature"].as_f64().expect("number");
1487 assert!((n - 0.7).abs() < 1e-9);
1488 }
1489
1490 #[test]
1491 fn coerce_leaves_string_fields_alone() {
1492 let schema = make_schema(serde_json::json!({
1493 "query": {"type": "string"},
1494 "count": {"type": "integer"},
1495 }));
1496 let coerced = coerce_string_scalars_at_top_level(
1497 serde_json::json!({"query": "50", "count": "50"}),
1498 &schema,
1499 );
1500 assert_eq!(coerced["query"], serde_json::json!("50"));
1503 assert_eq!(coerced["count"], serde_json::json!(50));
1504 }
1505
1506 #[test]
1507 fn coerce_leaves_unparseable_strings_alone() {
1508 let schema = make_schema(serde_json::json!({
1509 "max_iterations": {"type": "integer"},
1510 }));
1511 let coerced = coerce_string_scalars_at_top_level(
1512 serde_json::json!({"max_iterations": "fifty"}),
1513 &schema,
1514 );
1515 assert_eq!(coerced, serde_json::json!({"max_iterations": "fifty"}));
1519 }
1520
1521 #[test]
1522 fn coerce_treats_nullable_integer_as_integer() {
1523 let schema = make_schema(serde_json::json!({
1526 "max_iterations": {"type": ["integer", "null"]},
1527 }));
1528 let coerced = coerce_string_scalars_at_top_level(
1529 serde_json::json!({"max_iterations": "20"}),
1530 &schema,
1531 );
1532 assert_eq!(coerced, serde_json::json!({"max_iterations": 20}));
1533 }
1534
1535 #[test]
1536 fn coerce_skips_ambiguous_multi_type_schemas() {
1537 let schema = make_schema(serde_json::json!({
1542 "value": {"type": ["integer", "string"]},
1543 }));
1544 let coerced =
1545 coerce_string_scalars_at_top_level(serde_json::json!({"value": "42"}), &schema);
1546 assert_eq!(coerced, serde_json::json!({"value": "42"}));
1547 }
1548
1549 #[test]
1550 fn coerce_passes_through_object_without_properties() {
1551 let schema = serde_json::json!({"type": "object"});
1555 let coerced = coerce_string_scalars_at_top_level(serde_json::json!({"x": "50"}), &schema);
1556 assert_eq!(coerced, serde_json::json!({"x": "50"}));
1557 }
1558
1559 fn hint_for(json: Value, expected_target: &str) -> Option<String> {
1562 #[derive(Debug, Deserialize, JsonSchema)]
1566 #[allow(dead_code)]
1567 struct UsizeField {
1568 n: usize,
1569 }
1570 #[derive(Debug, Deserialize, JsonSchema)]
1571 #[allow(dead_code)]
1572 struct BoolField {
1573 b: bool,
1574 }
1575 #[derive(Debug, Deserialize, JsonSchema)]
1576 #[allow(dead_code)]
1577 struct VecField {
1578 items: Vec<serde_json::Value>,
1579 }
1580 let raw = match expected_target {
1581 "usize" => serde_json::from_value::<UsizeField>(json).unwrap_err(),
1582 "bool" => serde_json::from_value::<BoolField>(json).unwrap_err(),
1583 "sequence" => serde_json::from_value::<VecField>(json).unwrap_err(),
1584 _ => panic!("unknown target {expected_target}"),
1585 };
1586 Some(enrich_arg_parse_error_message(&raw))
1587 }
1588
1589 #[test]
1590 fn enrich_appends_integer_hint_for_string_encoded_int() {
1591 let msg = hint_for(serde_json::json!({"n": "50"}), "usize").unwrap();
1592 assert!(
1593 msg.contains("Did you mean the integer 50"),
1594 "expected integer hint, got: {msg}"
1595 );
1596 assert!(msg.contains("Resend without quotes"));
1597 }
1598
1599 #[test]
1600 fn enrich_appends_boolean_hint_for_string_encoded_bool() {
1601 let msg = hint_for(serde_json::json!({"b": "True"}), "bool").unwrap();
1602 assert!(
1603 msg.contains("Did you mean true"),
1604 "expected boolean hint, got: {msg}"
1605 );
1606 }
1607
1608 #[test]
1609 fn enrich_appends_sequence_hint_for_string_in_array_slot() {
1610 let xml_soup = "\n<ref>{\"kind\":\"file\",\"path\":\"x.md\"}</ref></artifact></file_write>";
1611 let msg = hint_for(serde_json::json!({"items": xml_soup}), "sequence").unwrap();
1612 assert!(
1613 msg.contains("Expected a JSON array"),
1614 "expected sequence hint, got: {msg}"
1615 );
1616 }
1617
1618 #[test]
1619 fn enrich_passes_through_unrecognised_errors_unchanged() {
1620 #[derive(Debug, Deserialize, JsonSchema)]
1623 #[allow(dead_code)]
1624 struct R {
1625 n: usize,
1626 }
1627 let err = serde_json::from_value::<R>(serde_json::json!({})).unwrap_err();
1628 let raw = err.to_string();
1629 let enriched = enrich_arg_parse_error_message(&err);
1630 assert_eq!(enriched, raw);
1631 }
1632
1633 #[test]
1634 fn flatten_tagged_oneof_passes_through_single_struct_schemas() {
1635 let raw = serde_json::json!({
1638 "type": "object",
1639 "properties": {"text": {"type": "string"}},
1640 "required": ["text"],
1641 });
1642 let out = flatten_tagged_oneof_schema(raw.clone());
1643 assert_eq!(out, raw);
1644 }
1645
1646 struct EchoTool;
1647
1648 #[async_trait]
1649 impl AgentTool for EchoTool {
1650 fn name(&self) -> &str {
1651 "echo"
1652 }
1653
1654 fn description(&self) -> &str {
1655 "Echo arguments back as text"
1656 }
1657
1658 fn parameters_schema(&self) -> Value {
1659 serde_json::json!({
1660 "type": "object",
1661 "properties": {"text": {"type": "string"}},
1662 "required": ["text"]
1663 })
1664 }
1665
1666 async fn execute(
1667 &self,
1668 _call_id: &str,
1669 args: Value,
1670 _signal: CancellationToken,
1671 _update: ToolUpdateSink,
1672 ) -> Result<ToolResult, ToolError> {
1673 let text = args
1674 .get("text")
1675 .and_then(Value::as_str)
1676 .unwrap_or("")
1677 .to_string();
1678 Ok(ToolResult {
1679 content: vec![ToolResultBlock::Text(TextContent { text })],
1680 is_error: false,
1681 details: Value::Null,
1682 terminate: false,
1683 narration: None,
1684 })
1685 }
1686 }
1687
1688 #[test]
1689 fn registry_lookup() {
1690 let registry = ToolRegistry::new().with(Arc::new(EchoTool));
1691 assert!(registry.get("echo").is_some());
1692 assert!(registry.get("missing").is_none());
1693 assert_eq!(registry.len(), 1);
1694 }
1695
1696 struct NamedTool(&'static str);
1697
1698 #[async_trait]
1699 impl AgentTool for NamedTool {
1700 fn name(&self) -> &str {
1701 self.0
1702 }
1703
1704 fn description(&self) -> &str {
1705 "named"
1706 }
1707
1708 fn parameters_schema(&self) -> Value {
1709 serde_json::json!({"type": "object", "properties": {}})
1710 }
1711
1712 async fn execute(
1713 &self,
1714 _call_id: &str,
1715 _args: Value,
1716 _signal: CancellationToken,
1717 _update: ToolUpdateSink,
1718 ) -> Result<ToolResult, ToolError> {
1719 Ok(ToolResult::text("ok"))
1720 }
1721 }
1722
1723 #[test]
1724 fn registry_preserves_registration_order() {
1725 let mut registry = ToolRegistry::new()
1726 .with(Arc::new(NamedTool("message_result")))
1727 .with(Arc::new(NamedTool("message_ask")))
1728 .with(Arc::new(NamedTool("plan")));
1729
1730 registry.register(Arc::new(NamedTool("message_result")));
1731
1732 assert_eq!(
1733 registry.names(),
1734 vec!["message_result", "message_ask", "plan"]
1735 );
1736 assert_eq!(
1737 registry.iter().map(|tool| tool.name()).collect::<Vec<_>>(),
1738 vec!["message_result", "message_ask", "plan"]
1739 );
1740 }
1741
1742 #[tokio::test]
1743 async fn echo_tool_executes() {
1744 let tool = EchoTool;
1745 let (tx, _rx) = mpsc::unbounded_channel();
1746 let result = tool
1747 .execute(
1748 "call_1",
1749 serde_json::json!({"text": "hi"}),
1750 CancellationToken::new(),
1751 tx,
1752 )
1753 .await
1754 .unwrap();
1755 let ToolResultBlock::Text(t) = &result.content[0] else {
1756 panic!("expected text")
1757 };
1758 assert_eq!(t.text, "hi");
1759 }
1760
1761 #[derive(Debug, Deserialize, JsonSchema)]
1770 #[serde(deny_unknown_fields)]
1771 struct CoercibleArgs {
1772 max_iterations: usize,
1773 full_page: bool,
1774 temperature: f32,
1775 label: String,
1776 }
1777
1778 struct CoercibleTool;
1779
1780 #[async_trait]
1781 impl TypedAgentTool for CoercibleTool {
1782 type Args = CoercibleArgs;
1783 fn name(&self) -> &str {
1784 "coercible"
1785 }
1786 fn description(&self) -> &str {
1787 "fixture"
1788 }
1789 async fn run(
1790 &self,
1791 _call_id: &str,
1792 args: Self::Args,
1793 _signal: CancellationToken,
1794 _update: ToolUpdateSink,
1795 ) -> Result<ToolResult, ToolError> {
1796 Ok(ToolResult::text(format!(
1798 "max_iterations={} full_page={} temperature={} label={}",
1799 args.max_iterations, args.full_page, args.temperature, args.label
1800 )))
1801 }
1802 }
1803
1804 #[tokio::test]
1805 async fn execute_coerces_string_encoded_scalars_end_to_end() {
1806 let tool = CoercibleTool;
1811 let (tx, _rx) = mpsc::unbounded_channel();
1812 let result = AgentTool::execute(
1813 &tool,
1814 "call_1",
1815 serde_json::json!({
1816 "max_iterations": "50",
1817 "full_page": "True",
1818 "temperature": "0.7",
1819 "label": "actual string",
1820 }),
1821 CancellationToken::new(),
1822 tx,
1823 )
1824 .await
1825 .unwrap();
1826 let ToolResultBlock::Text(t) = &result.content[0] else {
1827 panic!("expected text result");
1828 };
1829 assert!(
1830 t.text.contains("max_iterations=50"),
1831 "integer coercion missing: {}",
1832 t.text
1833 );
1834 assert!(
1835 t.text.contains("full_page=true"),
1836 "boolean coercion missing: {}",
1837 t.text
1838 );
1839 assert!(
1840 t.text.contains("temperature=0.7"),
1841 "float coercion missing: {}",
1842 t.text
1843 );
1844 assert!(
1845 t.text.contains("label=actual string"),
1846 "string field must NOT be coerced: {}",
1847 t.text
1848 );
1849 assert!(!result.is_error, "execute must succeed after coercion");
1850 }
1851
1852 #[tokio::test]
1853 async fn execute_appends_self_correcting_hint_on_unrecoverable_string_int() {
1854 let tool = CoercibleTool;
1860 let (tx, _rx) = mpsc::unbounded_channel();
1861 let result = AgentTool::execute(
1862 &tool,
1863 "call_2",
1864 serde_json::json!({
1865 "max_iterations": "fifty",
1866 "full_page": true,
1867 "temperature": 0.1,
1868 "label": "x",
1869 }),
1870 CancellationToken::new(),
1871 tx,
1872 )
1873 .await
1874 .unwrap();
1875 assert!(result.is_error, "expected validator rejection");
1876 let ToolResultBlock::Text(t) = &result.content[0] else {
1877 panic!("expected text result");
1878 };
1879 assert!(
1880 t.text.starts_with("coercible: invalid arguments:"),
1881 "preserve canonical error prefix: {}",
1882 t.text
1883 );
1884 assert!(
1885 !t.text.contains("Did you mean the integer fifty"),
1886 "must not invent a hint when the value cannot parse: {}",
1887 t.text
1888 );
1889 }
1890
1891 #[derive(Debug, Deserialize, JsonSchema)]
1896 #[serde(tag = "action", rename_all = "snake_case")]
1897 enum TaggedArgs {
1898 Open { url: String },
1899 Reload {},
1900 }
1901
1902 struct TaggedTool;
1903
1904 #[async_trait]
1905 impl TypedAgentTool for TaggedTool {
1906 type Args = TaggedArgs;
1907 fn name(&self) -> &str {
1908 "tagged_fixture"
1909 }
1910 fn description(&self) -> &str {
1911 "fixture"
1912 }
1913 fn prepare_arguments(&self, args: Value) -> Value {
1914 let Value::Object(mut obj) = args else {
1918 return args;
1919 };
1920 if !obj.contains_key("action") && obj.contains_key("url") {
1921 obj.insert("action".to_string(), Value::String("open".to_string()));
1922 }
1923 Value::Object(obj)
1924 }
1925 async fn run(
1926 &self,
1927 _call_id: &str,
1928 args: Self::Args,
1929 _signal: CancellationToken,
1930 _update: ToolUpdateSink,
1931 ) -> Result<ToolResult, ToolError> {
1932 let label = match args {
1933 TaggedArgs::Open { url } => format!("open:{url}"),
1934 TaggedArgs::Reload {} => "reload".to_string(),
1935 };
1936 Ok(ToolResult::text(label))
1937 }
1938 }
1939
1940 #[tokio::test]
1941 async fn execute_runs_prepare_arguments_before_typed_deser() {
1942 let tool = TaggedTool;
1948 let (tx, _rx) = mpsc::unbounded_channel();
1949 let result = AgentTool::execute(
1950 &tool,
1951 "call_1",
1952 serde_json::json!({"url": "https://example.com"}),
1953 CancellationToken::new(),
1954 tx,
1955 )
1956 .await
1957 .unwrap();
1958 let ToolResultBlock::Text(t) = &result.content[0] else {
1959 panic!("expected text result");
1960 };
1961 assert!(
1962 !result.is_error,
1963 "execute must succeed after action inference"
1964 );
1965 assert_eq!(t.text, "open:https://example.com");
1966 }
1967
1968 #[tokio::test]
1969 async fn execute_prepare_arguments_does_not_override_explicit_action() {
1970 let tool = TaggedTool;
1971 let (tx, _rx) = mpsc::unbounded_channel();
1972 let result = AgentTool::execute(
1973 &tool,
1974 "call_2",
1975 serde_json::json!({"action": "reload"}),
1976 CancellationToken::new(),
1977 tx,
1978 )
1979 .await
1980 .unwrap();
1981 let ToolResultBlock::Text(t) = &result.content[0] else {
1982 panic!("expected text result");
1983 };
1984 assert!(!result.is_error);
1985 assert_eq!(t.text, "reload");
1986 }
1987}