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 argument_validation_error(tool: &str, text: impl Into<String>) -> Self {
154 let mut result = Self::error(text);
155 result.details = serde_json::json!({
156 "kind": "tool_argument_validation",
157 "recoverable": true,
158 "display_hidden": true,
159 "tool": tool,
160 });
161 result
162 }
163
164 pub fn with_narration(mut self, narration: impl Into<String>) -> Self {
168 let raw: String = narration.into();
169 let trimmed = raw.trim();
170 if !trimmed.is_empty() {
171 self.narration = Some(trimmed.to_string());
172 }
173 self
174 }
175}
176
177pub type ToolUpdateSink = mpsc::UnboundedSender<ToolResult>;
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub struct ToolHistoryPolicy {
191 pub dedup_arg: Option<&'static str>,
195 pub summary_arg: Option<&'static str>,
197 pub compactable_result: bool,
200 pub pins_active_plan: bool,
203}
204
205impl ToolHistoryPolicy {
206 pub const fn new() -> Self {
207 Self {
208 dedup_arg: None,
209 summary_arg: None,
210 compactable_result: false,
211 pins_active_plan: false,
212 }
213 }
214
215 pub const fn dedup_arg(mut self, arg: &'static str) -> Self {
216 self.dedup_arg = Some(arg);
217 self
218 }
219
220 pub const fn summary_arg(mut self, arg: &'static str) -> Self {
221 self.summary_arg = Some(arg);
222 self
223 }
224
225 pub const fn compactable_result(mut self) -> Self {
226 self.compactable_result = true;
227 self
228 }
229
230 pub const fn pins_active_plan(mut self) -> Self {
231 self.pins_active_plan = true;
232 self
233 }
234}
235
236impl Default for ToolHistoryPolicy {
237 fn default() -> Self {
238 Self::new()
239 }
240}
241
242#[async_trait]
247pub trait AgentTool: Send + Sync + 'static {
248 fn name(&self) -> &str;
249
250 fn description(&self) -> &str;
251
252 fn parameters_schema(&self) -> Value;
255
256 fn requires_exclusive_sandbox(&self) -> bool {
272 false
273 }
274
275 fn max_result_chars(&self) -> Option<usize> {
291 None
292 }
293
294 fn history_policy(&self) -> ToolHistoryPolicy {
298 ToolHistoryPolicy::default()
299 }
300
301 fn identity_policy(&self) -> crate::tool_identity::ToolIdentityPolicy {
309 crate::tool_identity::ToolIdentityPolicy::default()
310 }
311
312 fn aborts_siblings_on_error(&self) -> bool {
330 false
331 }
332
333 fn counts_toward_tool_call_limit(&self) -> bool {
342 true
343 }
344
345 fn parallel_safe_per_turn(&self) -> bool {
357 false
358 }
359
360 fn counts_toward_termination_vote(&self) -> bool {
376 true
377 }
378
379 fn prepare_arguments(&self, args: Value) -> Value {
382 args
383 }
384
385 fn validate(&self, _args: &Value) -> Result<(), ToolValidationError> {
389 Ok(())
390 }
391
392 async fn execute(
397 &self,
398 call_id: &str,
399 args: Value,
400 signal: CancellationToken,
401 update: ToolUpdateSink,
402 ) -> Result<ToolResult, ToolError>;
403}
404
405#[async_trait]
428pub trait TypedAgentTool: Send + Sync + 'static {
429 type Args: serde::de::DeserializeOwned + schemars::JsonSchema + Send + 'static;
433
434 fn name(&self) -> &str;
435 fn description(&self) -> &str;
436
437 fn requires_exclusive_sandbox(&self) -> bool {
439 false
440 }
441
442 fn max_result_chars(&self) -> Option<usize> {
445 None
446 }
447
448 fn history_policy(&self) -> ToolHistoryPolicy {
451 ToolHistoryPolicy::default()
452 }
453
454 fn identity_policy(&self) -> crate::tool_identity::ToolIdentityPolicy {
458 crate::tool_identity::ToolIdentityPolicy::default()
459 }
460
461 fn aborts_siblings_on_error(&self) -> bool {
464 false
465 }
466
467 fn counts_toward_tool_call_limit(&self) -> bool {
470 true
471 }
472
473 fn parallel_safe_per_turn(&self) -> bool {
477 false
478 }
479
480 fn counts_toward_termination_vote(&self) -> bool {
485 true
486 }
487
488 fn prepare_arguments(&self, args: Value) -> Value {
496 args
497 }
498
499 async fn run(
501 &self,
502 call_id: &str,
503 args: Self::Args,
504 signal: CancellationToken,
505 update: ToolUpdateSink,
506 ) -> Result<ToolResult, ToolError>;
507}
508
509#[async_trait]
516impl<T: TypedAgentTool> AgentTool for T {
517 fn name(&self) -> &str {
518 TypedAgentTool::name(self)
519 }
520
521 fn description(&self) -> &str {
522 TypedAgentTool::description(self)
523 }
524
525 fn parameters_schema(&self) -> Value {
526 let settings = schemars::gen::SchemaSettings::draft07().with(|s| {
527 s.inline_subschemas = true;
528 });
529 let generator = settings.into_generator();
530 let schema = generator.into_root_schema_for::<T::Args>();
531 let value = serde_json::to_value(schema).expect("typed-tool schema serializes");
532 let mut value = flatten_tagged_oneof_schema(value);
533 normalize_strict_validator_quirks(&mut value);
534 value
535 }
536
537 fn requires_exclusive_sandbox(&self) -> bool {
538 TypedAgentTool::requires_exclusive_sandbox(self)
539 }
540
541 fn max_result_chars(&self) -> Option<usize> {
542 TypedAgentTool::max_result_chars(self)
543 }
544
545 fn history_policy(&self) -> ToolHistoryPolicy {
546 TypedAgentTool::history_policy(self)
547 }
548
549 fn identity_policy(&self) -> crate::tool_identity::ToolIdentityPolicy {
550 TypedAgentTool::identity_policy(self)
551 }
552
553 fn aborts_siblings_on_error(&self) -> bool {
554 TypedAgentTool::aborts_siblings_on_error(self)
555 }
556
557 fn counts_toward_tool_call_limit(&self) -> bool {
558 TypedAgentTool::counts_toward_tool_call_limit(self)
559 }
560
561 fn parallel_safe_per_turn(&self) -> bool {
562 TypedAgentTool::parallel_safe_per_turn(self)
563 }
564
565 fn counts_toward_termination_vote(&self) -> bool {
566 TypedAgentTool::counts_toward_termination_vote(self)
567 }
568
569 fn prepare_arguments(&self, args: Value) -> Value {
570 TypedAgentTool::prepare_arguments(self, args)
571 }
572
573 async fn execute(
574 &self,
575 call_id: &str,
576 args: Value,
577 signal: CancellationToken,
578 update: ToolUpdateSink,
579 ) -> Result<ToolResult, ToolError> {
580 let prepared = AgentTool::prepare_arguments(self, args);
598 let stripped = strip_top_level_nulls(prepared);
599 let schema = AgentTool::parameters_schema(self);
608 let coerced = coerce_string_scalars_at_top_level(stripped, &schema);
609 let parsed: T::Args = match serde_json::from_value(coerced) {
610 Ok(v) => v,
611 Err(e) => {
612 let tool_name = TypedAgentTool::name(self);
613 return Ok(ToolResult::argument_validation_error(
614 tool_name,
615 format!(
616 "{}: invalid arguments: {}",
617 tool_name,
618 enrich_arg_parse_error_message(&e),
619 ),
620 ));
621 }
622 };
623 TypedAgentTool::run(self, call_id, parsed, signal, update).await
624 }
625}
626
627fn coerce_string_scalars_at_top_level(value: Value, schema: &Value) -> Value {
638 let Value::Object(mut map) = value else {
639 return value;
640 };
641 let Some(properties) = schema.get("properties").and_then(Value::as_object) else {
642 return Value::Object(map);
643 };
644 for (key, val) in map.iter_mut() {
645 let Some(prop_schema) = properties.get(key) else {
646 continue;
647 };
648 coerce_one_scalar_in_place(val, prop_schema);
649 }
650 Value::Object(map)
651}
652
653fn coerce_one_scalar_in_place(value: &mut Value, prop_schema: &Value) {
654 let Some(text) = value.as_str() else {
655 return;
656 };
657 let Some(target) = scalar_target_from_schema(prop_schema) else {
658 return;
659 };
660 match target {
661 ScalarTarget::Integer => {
662 let trimmed = text.trim();
663 if let Ok(n) = trimmed.parse::<i64>() {
664 *value = Value::Number(serde_json::Number::from(n));
665 } else if let Ok(n) = trimmed.parse::<u64>() {
666 *value = Value::Number(serde_json::Number::from(n));
667 }
668 }
669 ScalarTarget::Number => {
670 let trimmed = text.trim();
671 if let Ok(n) = trimmed.parse::<f64>() {
672 if let Some(num) = serde_json::Number::from_f64(n) {
673 *value = Value::Number(num);
674 }
675 }
676 }
677 ScalarTarget::Boolean => match text.trim() {
678 "true" | "True" | "TRUE" => *value = Value::Bool(true),
679 "false" | "False" | "FALSE" => *value = Value::Bool(false),
680 _ => {}
681 },
682 }
683}
684
685#[derive(Debug, Clone, Copy)]
686enum ScalarTarget {
687 Integer,
688 Number,
689 Boolean,
690}
691
692fn scalar_target_from_schema(prop_schema: &Value) -> Option<ScalarTarget> {
693 let type_field = prop_schema.get("type")?;
694 let single = match type_field {
695 Value::String(s) => Some(s.as_str()),
696 Value::Array(arr) => {
700 let non_null: Vec<&str> = arr
701 .iter()
702 .filter_map(|v| v.as_str())
703 .filter(|s| *s != "null")
704 .collect();
705 if non_null.len() == 1 {
706 Some(non_null[0])
707 } else {
708 None
709 }
710 }
711 _ => None,
712 }?;
713 match single {
714 "integer" => Some(ScalarTarget::Integer),
715 "number" => Some(ScalarTarget::Number),
716 "boolean" => Some(ScalarTarget::Boolean),
717 _ => None,
718 }
719}
720
721fn enrich_arg_parse_error_message(err: &serde_json::Error) -> String {
727 let raw = err.to_string();
728 match arg_parse_hint(&raw) {
729 Some(hint) => format!("{raw}. {hint}"),
730 None => raw,
731 }
732}
733
734fn arg_parse_hint(raw: &str) -> Option<String> {
735 let value = extract_invalid_string_value(raw)?;
736 if expects_integer(raw) {
737 let parsed: i128 = value.trim().parse().ok()?;
738 return Some(format!(
739 "Did you mean the integer {parsed}? Resend without quotes."
740 ));
741 }
742 if expects_number(raw) {
743 let parsed: f64 = value.trim().parse().ok()?;
744 return Some(format!(
745 "Did you mean the number {parsed}? Resend without quotes."
746 ));
747 }
748 if expects_boolean(raw) {
749 return match value.trim() {
750 "true" | "True" | "TRUE" => Some(
751 "Did you mean true? Resend as a boolean literal (lowercase, no quotes)."
752 .to_string(),
753 ),
754 "false" | "False" | "FALSE" => Some(
755 "Did you mean false? Resend as a boolean literal (lowercase, no quotes)."
756 .to_string(),
757 ),
758 _ => None,
759 };
760 }
761 if expects_sequence(raw) {
762 return Some(
763 "Expected a JSON array (e.g. `[{...}, {...}]`); the field cannot be a string. \
764 Resend the value as an array of structured items, not a string of XML-like markup."
765 .to_string(),
766 );
767 }
768 None
769}
770
771fn extract_invalid_string_value(raw: &str) -> Option<&str> {
772 let start = raw.find("string \"")? + "string \"".len();
776 let rest = &raw[start..];
777 let end = rest.find('\"')?;
778 Some(&rest[..end])
779}
780
781fn expects_integer(raw: &str) -> bool {
782 raw.contains("expected usize")
783 || raw.contains("expected isize")
784 || raw.contains("expected u8")
785 || raw.contains("expected u16")
786 || raw.contains("expected u32")
787 || raw.contains("expected u64")
788 || raw.contains("expected i8")
789 || raw.contains("expected i16")
790 || raw.contains("expected i32")
791 || raw.contains("expected i64")
792 || raw.contains("expected integer")
793}
794
795fn expects_number(raw: &str) -> bool {
796 raw.contains("expected f32")
797 || raw.contains("expected f64")
798 || raw.contains("expected floating point")
799}
800
801fn expects_boolean(raw: &str) -> bool {
802 raw.contains("expected a boolean") || raw.contains("expected bool")
803}
804
805fn expects_sequence(raw: &str) -> bool {
806 raw.contains("expected a sequence") || raw.contains("expected an array")
807}
808
809fn strip_top_level_nulls(value: Value) -> Value {
810 match value {
811 Value::Object(map) => {
812 Value::Object(map.into_iter().filter(|(_, v)| !v.is_null()).collect())
813 }
814 other => other,
815 }
816}
817
818fn flatten_tagged_oneof_schema(schema: Value) -> Value {
833 let Value::Object(mut root) = schema else {
834 return schema;
835 };
836 let Some(Value::Array(variants)) = root.remove("oneOf") else {
837 if !root.is_empty() {
839 return Value::Object(root);
840 }
841 return Value::Null;
842 };
843
844 struct VariantSpec {
854 tag_value_str: Option<String>,
855 own_field_names: Vec<String>,
856 }
857
858 let mut discriminator: Option<String> = None;
859 let mut variant_specs: Vec<VariantSpec> = Vec::with_capacity(variants.len());
860 let mut merged_props = serde_json::Map::new();
861 let mut required_set: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
862 let mut tag_in_required = true;
863 let mut tag_values: Vec<Value> = Vec::with_capacity(variants.len());
866
867 for variant in &variants {
868 let Some(obj) = variant.as_object() else {
869 return reassemble_oneof(root, variants);
870 };
871 let Some(Value::Object(props)) = obj.get("properties").cloned() else {
872 return reassemble_oneof(root, variants);
873 };
874 let mut variant_tag: Option<(String, Value)> = None;
877 for (name, prop) in props.iter() {
878 let Some(prop_obj) = prop.as_object() else {
879 continue;
880 };
881 let Some(Value::Array(enum_values)) = prop_obj.get("enum").cloned() else {
882 continue;
883 };
884 if enum_values.len() == 1 {
885 variant_tag = Some((name.clone(), enum_values.into_iter().next().unwrap()));
886 break;
887 }
888 }
889 let Some((tag_name, tag_value)) = variant_tag else {
890 return reassemble_oneof(root, variants);
891 };
892 match &discriminator {
893 None => discriminator = Some(tag_name.clone()),
894 Some(existing) if existing == &tag_name => {}
895 Some(_) => return reassemble_oneof(root, variants),
896 }
897 tag_values.push(tag_value.clone());
898
899 let mut own_field_names = Vec::new();
902 for (name, prop_schema) in props.iter() {
903 if name == &tag_name {
904 continue;
905 }
906 merged_props
907 .entry(name.clone())
908 .or_insert_with(|| prop_schema.clone());
909 own_field_names.push(name.clone());
910 }
911
912 let mut tag_required_here = false;
918 if let Some(Value::Array(req)) = obj.get("required") {
919 for r in req {
920 if let Some(s) = r.as_str() {
921 if s == tag_name {
922 tag_required_here = true;
923 }
924 }
925 }
926 }
927 if !tag_required_here {
928 tag_in_required = false;
929 }
930
931 variant_specs.push(VariantSpec {
932 tag_value_str: tag_value.as_str().map(str::to_string),
933 own_field_names,
934 });
935 }
936
937 let Some(discriminator) = discriminator else {
938 return reassemble_oneof(root, variants);
939 };
940
941 let total_variants = variant_specs.len();
946 let all_tags_are_strings = variant_specs.iter().all(|s| s.tag_value_str.is_some());
947 if all_tags_are_strings && total_variants > 1 {
948 let mut owners: std::collections::BTreeMap<String, Vec<String>> =
949 std::collections::BTreeMap::new();
950 for spec in &variant_specs {
951 let tag_label = spec.tag_value_str.clone().unwrap_or_default();
952 for field in &spec.own_field_names {
953 owners
954 .entry(field.clone())
955 .or_default()
956 .push(tag_label.clone());
957 }
958 }
959 for (field, mut variant_tags) in owners {
960 if variant_tags.len() == total_variants {
961 continue;
962 }
963 variant_tags.sort();
964 variant_tags.dedup();
965 let suffix = format!(
966 " (applies when {discriminator} in: [{}])",
967 variant_tags.join(", ")
968 );
969 if let Some(Value::Object(prop_map)) = merged_props.get_mut(&field) {
970 let new_desc = match prop_map.get("description") {
971 Some(Value::String(existing)) if !existing.is_empty() => {
972 format!("{existing}{suffix}")
973 }
974 _ => suffix.trim_start().to_string(),
975 };
976 prop_map.insert("description".to_string(), Value::String(new_desc));
977 }
978 }
979 }
980
981 let mut tag_prop = serde_json::Map::new();
987 tag_prop.insert("type".to_string(), Value::String("string".to_string()));
988 tag_prop.insert("enum".to_string(), Value::Array(tag_values));
989 let mut ordered_props = serde_json::Map::new();
990 ordered_props.insert(discriminator.clone(), Value::Object(tag_prop));
991 for (name, schema) in merged_props {
992 ordered_props.insert(name, schema);
993 }
994 if tag_in_required {
995 required_set.insert(discriminator);
996 }
997
998 let mut out = serde_json::Map::new();
999 if let Some(desc) = root.remove("description") {
1000 out.insert("description".to_string(), desc);
1001 }
1002 if let Some(schema) = root.remove("$schema") {
1003 out.insert("$schema".to_string(), schema);
1004 }
1005 out.insert("type".to_string(), Value::String("object".to_string()));
1006 out.insert("properties".to_string(), Value::Object(ordered_props));
1007 if !required_set.is_empty() {
1008 out.insert(
1009 "required".to_string(),
1010 Value::Array(required_set.into_iter().map(Value::String).collect()),
1011 );
1012 }
1013 Value::Object(out)
1014}
1015
1016fn reassemble_oneof(mut root: serde_json::Map<String, Value>, variants: Vec<Value>) -> Value {
1017 root.insert("oneOf".to_string(), Value::Array(variants));
1018 Value::Object(root)
1019}
1020
1021fn normalize_strict_validator_quirks(value: &mut Value) {
1033 match value {
1034 Value::Object(map) => {
1035 if let Some(items) = map.get_mut("items") {
1037 if matches!(items, Value::Bool(true)) {
1038 *items = Value::Object(serde_json::Map::new());
1039 }
1040 }
1041 for v in map.values_mut() {
1042 normalize_strict_validator_quirks(v);
1043 }
1044 }
1045 Value::Array(arr) => {
1046 for v in arr {
1047 normalize_strict_validator_quirks(v);
1048 }
1049 }
1050 _ => {}
1051 }
1052}
1053
1054#[derive(Default, Clone)]
1056pub struct ToolRegistry {
1057 tools: HashMap<String, Arc<dyn AgentTool>>,
1058 order: Vec<String>,
1059}
1060
1061impl ToolRegistry {
1062 pub fn new() -> Self {
1063 Self::default()
1064 }
1065
1066 pub fn with(mut self, tool: Arc<dyn AgentTool>) -> Self {
1067 self.register(tool);
1068 self
1069 }
1070
1071 pub fn register(&mut self, tool: Arc<dyn AgentTool>) {
1072 let name = tool.name().to_string();
1073 if !self.tools.contains_key(&name) {
1074 self.order.push(name.clone());
1075 }
1076 self.tools.insert(name, tool);
1077 }
1078
1079 pub fn get(&self, name: &str) -> Option<Arc<dyn AgentTool>> {
1080 self.tools.get(name).cloned()
1081 }
1082
1083 pub fn history_policy(&self, name: &str) -> ToolHistoryPolicy {
1084 self.tools
1085 .get(name)
1086 .map(|tool| tool.history_policy())
1087 .unwrap_or_default()
1088 }
1089
1090 pub fn identity_policy(&self, name: &str) -> crate::tool_identity::ToolIdentityPolicy {
1096 self.tools
1097 .get(name)
1098 .map(|tool| tool.identity_policy())
1099 .unwrap_or_default()
1100 }
1101
1102 pub fn identity_policies(
1107 &self,
1108 ) -> std::collections::HashMap<String, crate::tool_identity::ToolIdentityPolicy> {
1109 self.tools
1110 .iter()
1111 .map(|(name, tool)| (name.clone(), tool.identity_policy()))
1112 .collect()
1113 }
1114
1115 pub fn names(&self) -> Vec<&str> {
1116 self.order.iter().map(String::as_str).collect()
1117 }
1118
1119 pub fn iter(&self) -> impl Iterator<Item = &Arc<dyn AgentTool>> {
1120 self.order.iter().filter_map(|name| self.tools.get(name))
1121 }
1122
1123 pub fn is_empty(&self) -> bool {
1124 self.tools.is_empty()
1125 }
1126
1127 pub fn len(&self) -> usize {
1128 self.tools.len()
1129 }
1130}
1131
1132impl std::fmt::Debug for ToolRegistry {
1133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1134 f.debug_struct("ToolRegistry")
1135 .field("tools", &self.order)
1136 .finish()
1137 }
1138}
1139
1140#[cfg(test)]
1141mod tests {
1142 use super::*;
1143 use crate::types::TextContent;
1144 use schemars::JsonSchema;
1145 use serde::Deserialize;
1146
1147 #[derive(Deserialize, JsonSchema)]
1150 #[serde(deny_unknown_fields)]
1151 #[allow(dead_code)]
1152 struct DocVariantArgs {
1153 filename: String,
1154 #[serde(default)]
1155 title: Option<String>,
1156 }
1157
1158 #[derive(Deserialize, JsonSchema)]
1159 #[serde(deny_unknown_fields)]
1160 #[allow(dead_code)]
1161 struct ExcelVariantArgs {
1162 filename: String,
1163 #[serde(default)]
1164 rows: Vec<Vec<serde_json::Value>>,
1165 }
1166
1167 #[derive(Deserialize, JsonSchema)]
1168 #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1169 #[allow(dead_code)]
1170 enum ExampleArgs {
1171 Document(DocVariantArgs),
1172 Excel(ExcelVariantArgs),
1173 }
1174
1175 fn build_example_schema() -> Value {
1176 let settings = schemars::gen::SchemaSettings::draft07().with(|s| {
1177 s.inline_subschemas = true;
1178 });
1179 let g = settings.into_generator();
1180 let s = g.into_root_schema_for::<ExampleArgs>();
1181 let raw = serde_json::to_value(s).unwrap();
1182 flatten_tagged_oneof_schema(raw)
1183 }
1184
1185 #[derive(Deserialize, JsonSchema)]
1186 #[serde(deny_unknown_fields)]
1187 #[allow(dead_code)]
1188 struct NonAlphabeticOrderCanaryArgs {
1189 zeta_selector: String,
1190 alpha_payload: String,
1191 middle_payload: String,
1192 }
1193
1194 #[test]
1195 fn schema_runtime_preserves_insertion_order_for_tool_objects() {
1196 let mut object = serde_json::Map::new();
1201 object.insert("zeta_selector".to_string(), Value::String("z".to_string()));
1202 object.insert("alpha_payload".to_string(), Value::String("a".to_string()));
1203 object.insert("middle_payload".to_string(), Value::String("m".to_string()));
1204
1205 let keys = object.keys().map(String::as_str).collect::<Vec<_>>();
1206 assert_eq!(
1207 keys,
1208 ["zeta_selector", "alpha_payload", "middle_payload"],
1209 "serde_json::Map must keep insertion order; losing this breaks \
1210 model-facing tool-schema property order"
1211 );
1212
1213 let serialized = serde_json::to_string(&Value::Object(object)).unwrap();
1214 assert_eq!(
1215 serialized, r#"{"zeta_selector":"z","alpha_payload":"a","middle_payload":"m"}"#,
1216 "schema JSON serialization must preserve object insertion order"
1217 );
1218 }
1219
1220 #[test]
1221 fn schemars_preserves_declared_struct_order_for_tool_args() {
1222 let settings = schemars::gen::SchemaSettings::draft07().with(|s| {
1227 s.inline_subschemas = true;
1228 });
1229 let schema = serde_json::to_value(
1230 settings
1231 .into_generator()
1232 .into_root_schema_for::<NonAlphabeticOrderCanaryArgs>(),
1233 )
1234 .expect("schema serializes");
1235 let props = schema
1236 .get("properties")
1237 .and_then(Value::as_object)
1238 .expect("schema must expose properties");
1239 let order = props.keys().map(String::as_str).collect::<Vec<_>>();
1240 assert_eq!(
1241 order,
1242 ["zeta_selector", "alpha_payload", "middle_payload"],
1243 "schemars must emit Args fields in declaration order for \
1244 autoregressive tool-call conditioning"
1245 );
1246 }
1247
1248 #[test]
1249 fn flatten_tagged_oneof_produces_flat_object_schema() {
1250 let s = build_example_schema();
1251 assert_eq!(s.get("type").and_then(Value::as_str), Some("object"));
1252 assert!(s.get("oneOf").is_none());
1254 let kind_prop = s.pointer("/properties/kind").expect("kind property");
1257 assert_eq!(
1258 kind_prop.get("type").and_then(Value::as_str),
1259 Some("string")
1260 );
1261 let kind_enum = kind_prop
1262 .get("enum")
1263 .and_then(Value::as_array)
1264 .expect("enum");
1265 let mut kinds: Vec<&str> = kind_enum.iter().filter_map(Value::as_str).collect();
1266 kinds.sort();
1267 assert_eq!(kinds, vec!["document", "excel"]);
1268 let props = s
1269 .get("properties")
1270 .and_then(Value::as_object)
1271 .expect("properties");
1272 let order: Vec<&str> = props.keys().map(String::as_str).collect();
1273 assert_eq!(
1274 order.first().copied(),
1275 Some("kind"),
1276 "discriminator must be emitted before payload fields so \
1277 variant-specific keys are conditioned on the selected kind"
1278 );
1279 assert!(s.pointer("/properties/filename").is_some());
1281 assert!(s.pointer("/properties/title").is_some());
1282 assert!(s.pointer("/properties/rows").is_some());
1283 let req = s
1285 .get("required")
1286 .and_then(Value::as_array)
1287 .expect("required");
1288 assert!(req.iter().any(|v| v.as_str() == Some("kind")));
1289 }
1290
1291 #[test]
1292 fn flatten_tagged_oneof_annotates_variant_specific_property_descriptions() {
1293 let s = build_example_schema();
1308
1309 let filename_desc = s
1312 .pointer("/properties/filename/description")
1313 .and_then(Value::as_str)
1314 .unwrap_or_default();
1315 assert!(
1316 !filename_desc.contains("applies when kind in"),
1317 "shared property `filename` must NOT carry a narrowing \
1318 suffix; got: {filename_desc:?}"
1319 );
1320
1321 let title_desc = s
1323 .pointer("/properties/title/description")
1324 .and_then(Value::as_str)
1325 .expect("title description present");
1326 assert!(
1327 title_desc.contains("applies when kind in: [document]"),
1328 "Document-only `title` must declare its variant scope; \
1329 got: {title_desc:?}"
1330 );
1331 let rows_desc = s
1332 .pointer("/properties/rows/description")
1333 .and_then(Value::as_str)
1334 .expect("rows description present");
1335 assert!(
1336 rows_desc.contains("applies when kind in: [excel]"),
1337 "Excel-only `rows` must declare its variant scope; \
1338 got: {rows_desc:?}"
1339 );
1340
1341 assert!(
1344 s.get("allOf").is_none(),
1345 "top-level allOf would be rejected by Azure's tool validator"
1346 );
1347 assert!(s.get("oneOf").is_none());
1348 assert!(s.get("anyOf").is_none());
1349 }
1350
1351 #[test]
1352 fn normalize_strict_quirks_rewrites_items_true_to_empty_object() {
1353 let mut schema = serde_json::json!({
1359 "type": "object",
1360 "properties": {
1361 "rows": {
1362 "type": "array",
1363 "items": {
1364 "type": "array",
1365 "items": true
1366 }
1367 }
1368 }
1369 });
1370 normalize_strict_validator_quirks(&mut schema);
1371 assert_eq!(
1372 schema.pointer("/properties/rows/items/items"),
1373 Some(&serde_json::json!({})),
1374 );
1375 }
1376
1377 #[test]
1378 fn strip_top_level_nulls_removes_inapplicable_variant_fields() {
1379 let model_payload = serde_json::json!({
1387 "action": "run",
1388 "command": "echo hi",
1389 "workdir": "/home/user/workspace",
1390 "code": null,
1392 "interpreter": null,
1393 "ext": null,
1394 "exec_dir": null,
1395 "max_token": null,
1396 "truncate_from": null,
1397 "run_id": null,
1398 "after_seq": null,
1399 "max_events": null,
1400 "timeout_s": null,
1401 "timeout_ms": null,
1402 "terminal": null,
1403 "force": null,
1404 "timeout_secs": 60,
1406 });
1407 let stripped = strip_top_level_nulls(model_payload);
1408 let obj = stripped.as_object().expect("object");
1409 assert!(!obj.contains_key("code"));
1411 assert!(!obj.contains_key("ext"));
1412 assert!(!obj.contains_key("max_token"));
1413 assert!(!obj.contains_key("force"));
1414 assert_eq!(obj.get("action").and_then(Value::as_str), Some("run"));
1416 assert_eq!(obj.get("command").and_then(Value::as_str), Some("echo hi"));
1417 assert_eq!(obj.get("timeout_secs").and_then(Value::as_i64), Some(60));
1418 }
1419
1420 #[test]
1421 fn strip_top_level_nulls_passes_through_non_object_values() {
1422 assert_eq!(
1426 strip_top_level_nulls(serde_json::json!("text")),
1427 serde_json::json!("text")
1428 );
1429 assert_eq!(strip_top_level_nulls(Value::Null), Value::Null);
1430 }
1431
1432 fn make_schema(properties: Value) -> Value {
1443 serde_json::json!({
1444 "type": "object",
1445 "properties": properties,
1446 })
1447 }
1448
1449 #[test]
1450 fn coerce_string_to_integer_when_schema_says_integer() {
1451 let schema = make_schema(serde_json::json!({
1452 "max_iterations": {"type": "integer"},
1453 }));
1454 let coerced = coerce_string_scalars_at_top_level(
1455 serde_json::json!({"max_iterations": "50"}),
1456 &schema,
1457 );
1458 assert_eq!(coerced, serde_json::json!({"max_iterations": 50}));
1459 }
1460
1461 #[test]
1462 fn coerce_string_to_integer_handles_negative_and_whitespace() {
1463 let schema = make_schema(serde_json::json!({
1464 "offset": {"type": "integer"},
1465 "limit": {"type": "integer"},
1466 }));
1467 let coerced = coerce_string_scalars_at_top_level(
1468 serde_json::json!({"offset": "-7", "limit": " 42 "}),
1469 &schema,
1470 );
1471 assert_eq!(coerced, serde_json::json!({"offset": -7, "limit": 42}));
1472 }
1473
1474 #[test]
1475 fn coerce_string_to_boolean_for_each_case_variant() {
1476 let schema = make_schema(serde_json::json!({
1477 "full_page": {"type": "boolean"},
1478 "headless": {"type": "boolean"},
1479 "verbose": {"type": "boolean"},
1480 "untouched": {"type": "boolean"},
1481 }));
1482 let coerced = coerce_string_scalars_at_top_level(
1483 serde_json::json!({
1484 "full_page": "true",
1485 "headless": "True",
1486 "verbose": "FALSE",
1487 "untouched": "maybe",
1488 }),
1489 &schema,
1490 );
1491 assert_eq!(coerced["full_page"], serde_json::json!(true));
1494 assert_eq!(coerced["headless"], serde_json::json!(true));
1495 assert_eq!(coerced["verbose"], serde_json::json!(false));
1496 assert_eq!(coerced["untouched"], serde_json::json!("maybe"));
1497 }
1498
1499 #[test]
1500 fn coerce_string_to_number_for_float_schema() {
1501 let schema = make_schema(serde_json::json!({
1502 "temperature": {"type": "number"},
1503 }));
1504 let coerced =
1505 coerce_string_scalars_at_top_level(serde_json::json!({"temperature": "0.7"}), &schema);
1506 let n = coerced["temperature"].as_f64().expect("number");
1508 assert!((n - 0.7).abs() < 1e-9);
1509 }
1510
1511 #[test]
1512 fn coerce_leaves_string_fields_alone() {
1513 let schema = make_schema(serde_json::json!({
1514 "query": {"type": "string"},
1515 "count": {"type": "integer"},
1516 }));
1517 let coerced = coerce_string_scalars_at_top_level(
1518 serde_json::json!({"query": "50", "count": "50"}),
1519 &schema,
1520 );
1521 assert_eq!(coerced["query"], serde_json::json!("50"));
1524 assert_eq!(coerced["count"], serde_json::json!(50));
1525 }
1526
1527 #[test]
1528 fn coerce_leaves_unparseable_strings_alone() {
1529 let schema = make_schema(serde_json::json!({
1530 "max_iterations": {"type": "integer"},
1531 }));
1532 let coerced = coerce_string_scalars_at_top_level(
1533 serde_json::json!({"max_iterations": "fifty"}),
1534 &schema,
1535 );
1536 assert_eq!(coerced, serde_json::json!({"max_iterations": "fifty"}));
1540 }
1541
1542 #[test]
1543 fn coerce_treats_nullable_integer_as_integer() {
1544 let schema = make_schema(serde_json::json!({
1547 "max_iterations": {"type": ["integer", "null"]},
1548 }));
1549 let coerced = coerce_string_scalars_at_top_level(
1550 serde_json::json!({"max_iterations": "20"}),
1551 &schema,
1552 );
1553 assert_eq!(coerced, serde_json::json!({"max_iterations": 20}));
1554 }
1555
1556 #[test]
1557 fn coerce_skips_ambiguous_multi_type_schemas() {
1558 let schema = make_schema(serde_json::json!({
1563 "value": {"type": ["integer", "string"]},
1564 }));
1565 let coerced =
1566 coerce_string_scalars_at_top_level(serde_json::json!({"value": "42"}), &schema);
1567 assert_eq!(coerced, serde_json::json!({"value": "42"}));
1568 }
1569
1570 #[test]
1571 fn coerce_passes_through_object_without_properties() {
1572 let schema = serde_json::json!({"type": "object"});
1576 let coerced = coerce_string_scalars_at_top_level(serde_json::json!({"x": "50"}), &schema);
1577 assert_eq!(coerced, serde_json::json!({"x": "50"}));
1578 }
1579
1580 fn hint_for(json: Value, expected_target: &str) -> Option<String> {
1583 #[derive(Debug, Deserialize, JsonSchema)]
1587 #[allow(dead_code)]
1588 struct UsizeField {
1589 n: usize,
1590 }
1591 #[derive(Debug, Deserialize, JsonSchema)]
1592 #[allow(dead_code)]
1593 struct BoolField {
1594 b: bool,
1595 }
1596 #[derive(Debug, Deserialize, JsonSchema)]
1597 #[allow(dead_code)]
1598 struct VecField {
1599 items: Vec<serde_json::Value>,
1600 }
1601 let raw = match expected_target {
1602 "usize" => serde_json::from_value::<UsizeField>(json).unwrap_err(),
1603 "bool" => serde_json::from_value::<BoolField>(json).unwrap_err(),
1604 "sequence" => serde_json::from_value::<VecField>(json).unwrap_err(),
1605 _ => panic!("unknown target {expected_target}"),
1606 };
1607 Some(enrich_arg_parse_error_message(&raw))
1608 }
1609
1610 #[test]
1611 fn enrich_appends_integer_hint_for_string_encoded_int() {
1612 let msg = hint_for(serde_json::json!({"n": "50"}), "usize").unwrap();
1613 assert!(
1614 msg.contains("Did you mean the integer 50"),
1615 "expected integer hint, got: {msg}"
1616 );
1617 assert!(msg.contains("Resend without quotes"));
1618 }
1619
1620 #[test]
1621 fn enrich_appends_boolean_hint_for_string_encoded_bool() {
1622 let msg = hint_for(serde_json::json!({"b": "True"}), "bool").unwrap();
1623 assert!(
1624 msg.contains("Did you mean true"),
1625 "expected boolean hint, got: {msg}"
1626 );
1627 }
1628
1629 #[test]
1630 fn enrich_appends_sequence_hint_for_string_in_array_slot() {
1631 let xml_soup = "\n<ref>{\"kind\":\"file\",\"path\":\"x.md\"}</ref></artifact></file_write>";
1632 let msg = hint_for(serde_json::json!({"items": xml_soup}), "sequence").unwrap();
1633 assert!(
1634 msg.contains("Expected a JSON array"),
1635 "expected sequence hint, got: {msg}"
1636 );
1637 }
1638
1639 #[test]
1640 fn enrich_passes_through_unrecognised_errors_unchanged() {
1641 #[derive(Debug, Deserialize, JsonSchema)]
1644 #[allow(dead_code)]
1645 struct R {
1646 n: usize,
1647 }
1648 let err = serde_json::from_value::<R>(serde_json::json!({})).unwrap_err();
1649 let raw = err.to_string();
1650 let enriched = enrich_arg_parse_error_message(&err);
1651 assert_eq!(enriched, raw);
1652 }
1653
1654 #[test]
1655 fn flatten_tagged_oneof_passes_through_single_struct_schemas() {
1656 let raw = serde_json::json!({
1659 "type": "object",
1660 "properties": {"text": {"type": "string"}},
1661 "required": ["text"],
1662 });
1663 let out = flatten_tagged_oneof_schema(raw.clone());
1664 assert_eq!(out, raw);
1665 }
1666
1667 struct EchoTool;
1668
1669 #[async_trait]
1670 impl AgentTool for EchoTool {
1671 fn name(&self) -> &str {
1672 "echo"
1673 }
1674
1675 fn description(&self) -> &str {
1676 "Echo arguments back as text"
1677 }
1678
1679 fn parameters_schema(&self) -> Value {
1680 serde_json::json!({
1681 "type": "object",
1682 "properties": {"text": {"type": "string"}},
1683 "required": ["text"]
1684 })
1685 }
1686
1687 async fn execute(
1688 &self,
1689 _call_id: &str,
1690 args: Value,
1691 _signal: CancellationToken,
1692 _update: ToolUpdateSink,
1693 ) -> Result<ToolResult, ToolError> {
1694 let text = args
1695 .get("text")
1696 .and_then(Value::as_str)
1697 .unwrap_or("")
1698 .to_string();
1699 Ok(ToolResult {
1700 content: vec![ToolResultBlock::Text(TextContent { text })],
1701 is_error: false,
1702 details: Value::Null,
1703 terminate: false,
1704 narration: None,
1705 })
1706 }
1707 }
1708
1709 #[test]
1710 fn registry_lookup() {
1711 let registry = ToolRegistry::new().with(Arc::new(EchoTool));
1712 assert!(registry.get("echo").is_some());
1713 assert!(registry.get("missing").is_none());
1714 assert_eq!(registry.len(), 1);
1715 }
1716
1717 struct NamedTool(&'static str);
1718
1719 #[async_trait]
1720 impl AgentTool for NamedTool {
1721 fn name(&self) -> &str {
1722 self.0
1723 }
1724
1725 fn description(&self) -> &str {
1726 "named"
1727 }
1728
1729 fn parameters_schema(&self) -> Value {
1730 serde_json::json!({"type": "object", "properties": {}})
1731 }
1732
1733 async fn execute(
1734 &self,
1735 _call_id: &str,
1736 _args: Value,
1737 _signal: CancellationToken,
1738 _update: ToolUpdateSink,
1739 ) -> Result<ToolResult, ToolError> {
1740 Ok(ToolResult::text("ok"))
1741 }
1742 }
1743
1744 #[test]
1745 fn registry_preserves_registration_order() {
1746 let mut registry = ToolRegistry::new()
1747 .with(Arc::new(NamedTool("message_result")))
1748 .with(Arc::new(NamedTool("message_ask")))
1749 .with(Arc::new(NamedTool("plan")));
1750
1751 registry.register(Arc::new(NamedTool("message_result")));
1752
1753 assert_eq!(
1754 registry.names(),
1755 vec!["message_result", "message_ask", "plan"]
1756 );
1757 assert_eq!(
1758 registry.iter().map(|tool| tool.name()).collect::<Vec<_>>(),
1759 vec!["message_result", "message_ask", "plan"]
1760 );
1761 }
1762
1763 #[tokio::test]
1764 async fn echo_tool_executes() {
1765 let tool = EchoTool;
1766 let (tx, _rx) = mpsc::unbounded_channel();
1767 let result = tool
1768 .execute(
1769 "call_1",
1770 serde_json::json!({"text": "hi"}),
1771 CancellationToken::new(),
1772 tx,
1773 )
1774 .await
1775 .unwrap();
1776 let ToolResultBlock::Text(t) = &result.content[0] else {
1777 panic!("expected text")
1778 };
1779 assert_eq!(t.text, "hi");
1780 }
1781
1782 #[derive(Debug, Deserialize, JsonSchema)]
1791 #[serde(deny_unknown_fields)]
1792 struct CoercibleArgs {
1793 max_iterations: usize,
1794 full_page: bool,
1795 temperature: f32,
1796 label: String,
1797 }
1798
1799 struct CoercibleTool;
1800
1801 #[async_trait]
1802 impl TypedAgentTool for CoercibleTool {
1803 type Args = CoercibleArgs;
1804 fn name(&self) -> &str {
1805 "coercible"
1806 }
1807 fn description(&self) -> &str {
1808 "fixture"
1809 }
1810 async fn run(
1811 &self,
1812 _call_id: &str,
1813 args: Self::Args,
1814 _signal: CancellationToken,
1815 _update: ToolUpdateSink,
1816 ) -> Result<ToolResult, ToolError> {
1817 Ok(ToolResult::text(format!(
1819 "max_iterations={} full_page={} temperature={} label={}",
1820 args.max_iterations, args.full_page, args.temperature, args.label
1821 )))
1822 }
1823 }
1824
1825 #[tokio::test]
1826 async fn execute_coerces_string_encoded_scalars_end_to_end() {
1827 let tool = CoercibleTool;
1832 let (tx, _rx) = mpsc::unbounded_channel();
1833 let result = AgentTool::execute(
1834 &tool,
1835 "call_1",
1836 serde_json::json!({
1837 "max_iterations": "50",
1838 "full_page": "True",
1839 "temperature": "0.7",
1840 "label": "actual string",
1841 }),
1842 CancellationToken::new(),
1843 tx,
1844 )
1845 .await
1846 .unwrap();
1847 let ToolResultBlock::Text(t) = &result.content[0] else {
1848 panic!("expected text result");
1849 };
1850 assert!(
1851 t.text.contains("max_iterations=50"),
1852 "integer coercion missing: {}",
1853 t.text
1854 );
1855 assert!(
1856 t.text.contains("full_page=true"),
1857 "boolean coercion missing: {}",
1858 t.text
1859 );
1860 assert!(
1861 t.text.contains("temperature=0.7"),
1862 "float coercion missing: {}",
1863 t.text
1864 );
1865 assert!(
1866 t.text.contains("label=actual string"),
1867 "string field must NOT be coerced: {}",
1868 t.text
1869 );
1870 assert!(!result.is_error, "execute must succeed after coercion");
1871 }
1872
1873 #[tokio::test]
1874 async fn execute_appends_self_correcting_hint_on_unrecoverable_string_int() {
1875 let tool = CoercibleTool;
1881 let (tx, _rx) = mpsc::unbounded_channel();
1882 let result = AgentTool::execute(
1883 &tool,
1884 "call_2",
1885 serde_json::json!({
1886 "max_iterations": "fifty",
1887 "full_page": true,
1888 "temperature": 0.1,
1889 "label": "x",
1890 }),
1891 CancellationToken::new(),
1892 tx,
1893 )
1894 .await
1895 .unwrap();
1896 assert!(result.is_error, "expected validator rejection");
1897 assert_eq!(
1898 result.details,
1899 serde_json::json!({
1900 "kind": "tool_argument_validation",
1901 "recoverable": true,
1902 "display_hidden": true,
1903 "tool": "coercible",
1904 })
1905 );
1906 let ToolResultBlock::Text(t) = &result.content[0] else {
1907 panic!("expected text result");
1908 };
1909 assert!(
1910 t.text.starts_with("coercible: invalid arguments:"),
1911 "preserve canonical error prefix: {}",
1912 t.text
1913 );
1914 assert!(
1915 !t.text.contains("Did you mean the integer fifty"),
1916 "must not invent a hint when the value cannot parse: {}",
1917 t.text
1918 );
1919 }
1920
1921 #[derive(Debug, Deserialize, JsonSchema)]
1926 #[serde(tag = "action", rename_all = "snake_case")]
1927 enum TaggedArgs {
1928 Open { url: String },
1929 Reload {},
1930 }
1931
1932 struct TaggedTool;
1933
1934 #[async_trait]
1935 impl TypedAgentTool for TaggedTool {
1936 type Args = TaggedArgs;
1937 fn name(&self) -> &str {
1938 "tagged_fixture"
1939 }
1940 fn description(&self) -> &str {
1941 "fixture"
1942 }
1943 fn prepare_arguments(&self, args: Value) -> Value {
1944 let Value::Object(mut obj) = args else {
1948 return args;
1949 };
1950 if !obj.contains_key("action") && obj.contains_key("url") {
1951 obj.insert("action".to_string(), Value::String("open".to_string()));
1952 }
1953 Value::Object(obj)
1954 }
1955 async fn run(
1956 &self,
1957 _call_id: &str,
1958 args: Self::Args,
1959 _signal: CancellationToken,
1960 _update: ToolUpdateSink,
1961 ) -> Result<ToolResult, ToolError> {
1962 let label = match args {
1963 TaggedArgs::Open { url } => format!("open:{url}"),
1964 TaggedArgs::Reload {} => "reload".to_string(),
1965 };
1966 Ok(ToolResult::text(label))
1967 }
1968 }
1969
1970 #[tokio::test]
1971 async fn execute_runs_prepare_arguments_before_typed_deser() {
1972 let tool = TaggedTool;
1978 let (tx, _rx) = mpsc::unbounded_channel();
1979 let result = AgentTool::execute(
1980 &tool,
1981 "call_1",
1982 serde_json::json!({"url": "https://example.com"}),
1983 CancellationToken::new(),
1984 tx,
1985 )
1986 .await
1987 .unwrap();
1988 let ToolResultBlock::Text(t) = &result.content[0] else {
1989 panic!("expected text result");
1990 };
1991 assert!(
1992 !result.is_error,
1993 "execute must succeed after action inference"
1994 );
1995 assert_eq!(t.text, "open:https://example.com");
1996 }
1997
1998 #[tokio::test]
1999 async fn execute_prepare_arguments_does_not_override_explicit_action() {
2000 let tool = TaggedTool;
2001 let (tx, _rx) = mpsc::unbounded_channel();
2002 let result = AgentTool::execute(
2003 &tool,
2004 "call_2",
2005 serde_json::json!({"action": "reload"}),
2006 CancellationToken::new(),
2007 tx,
2008 )
2009 .await
2010 .unwrap();
2011 let ToolResultBlock::Text(t) = &result.content[0] else {
2012 panic!("expected text result");
2013 };
2014 assert!(!result.is_error);
2015 assert_eq!(t.text, "reload");
2016 }
2017}