1use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11#[cfg(feature = "openapi")]
12use utoipa::ToSchema;
13
14pub const HUMAN_INTENT_ARGUMENT: &str = "human_intent";
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ToolResultImage {
22 pub base64: String,
24 pub media_type: String,
26}
27
28const HUMAN_INTENT_DESCRIPTION: &str = "Short user-facing narration of what this tool call will do, written as an action phrase like \"Listing all harnesses\". Do not include hidden reasoning, private chain of thought, secrets, or credential values.";
29
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
32#[cfg_attr(feature = "openapi", derive(ToSchema))]
33#[serde(rename_all = "snake_case")]
34pub enum ToolPolicy {
35 #[default]
37 Auto,
38 RequiresApproval,
40 ClientSide,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
51#[cfg_attr(feature = "openapi", derive(ToSchema))]
52#[serde(rename_all = "snake_case")]
53pub enum DeferrablePolicy {
54 Never,
56 #[default]
58 Automatic,
59 Always,
61}
62
63impl DeferrablePolicy {
64 pub fn is_default(&self) -> bool {
66 matches!(self, DeferrablePolicy::Automatic)
67 }
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72#[cfg_attr(feature = "openapi", derive(ToSchema))]
73#[serde(tag = "type", rename_all = "snake_case")]
74pub enum ToolDefinition {
75 Builtin(BuiltinTool),
77 ClientSide(ClientSideTool),
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
87#[cfg_attr(feature = "openapi", derive(ToSchema))]
88pub struct BuiltinTool {
89 pub name: String,
91 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub display_name: Option<String>,
94 pub description: String,
96 pub parameters: serde_json::Value,
98 #[serde(default)]
100 pub policy: ToolPolicy,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub category: Option<String>,
104 #[serde(default, skip_serializing_if = "DeferrablePolicy::is_default")]
106 pub deferrable: DeferrablePolicy,
107 #[serde(default, skip_serializing_if = "ToolHints::is_empty")]
109 pub hints: ToolHints,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub full_parameters: Option<serde_json::Value>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
120#[cfg_attr(feature = "openapi", derive(ToSchema))]
121pub struct ClientSideTool {
122 pub name: String,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub display_name: Option<String>,
127 pub description: String,
129 pub parameters: serde_json::Value,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub category: Option<String>,
134 #[serde(default, skip_serializing_if = "DeferrablePolicy::is_default")]
136 pub deferrable: DeferrablePolicy,
137 #[serde(default, skip_serializing_if = "ToolHints::is_empty")]
139 pub hints: ToolHints,
140 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub full_parameters: Option<serde_json::Value>,
145}
146
147impl ToolDefinition {
148 pub fn name(&self) -> &str {
150 match self {
151 ToolDefinition::Builtin(b) => &b.name,
152 ToolDefinition::ClientSide(c) => &c.name,
153 }
154 }
155
156 pub fn display_name(&self) -> Option<&str> {
158 match self {
159 ToolDefinition::Builtin(b) => b.display_name.as_deref(),
160 ToolDefinition::ClientSide(c) => c.display_name.as_deref(),
161 }
162 }
163
164 pub fn description(&self) -> &str {
166 match self {
167 ToolDefinition::Builtin(b) => &b.description,
168 ToolDefinition::ClientSide(c) => &c.description,
169 }
170 }
171
172 pub fn parameters(&self) -> &serde_json::Value {
174 match self {
175 ToolDefinition::Builtin(b) => &b.parameters,
176 ToolDefinition::ClientSide(c) => &c.parameters,
177 }
178 }
179
180 pub fn full_parameters(&self) -> &serde_json::Value {
186 match self {
187 ToolDefinition::Builtin(b) => b.full_parameters.as_ref().unwrap_or(&b.parameters),
188 ToolDefinition::ClientSide(c) => c.full_parameters.as_ref().unwrap_or(&c.parameters),
189 }
190 }
191
192 pub fn policy(&self) -> &ToolPolicy {
194 match self {
195 ToolDefinition::Builtin(b) => &b.policy,
196 ToolDefinition::ClientSide(_) => &ToolPolicy::ClientSide,
197 }
198 }
199
200 pub fn category(&self) -> Option<&str> {
202 match self {
203 ToolDefinition::Builtin(b) => b.category.as_deref(),
204 ToolDefinition::ClientSide(c) => c.category.as_deref(),
205 }
206 }
207
208 pub fn deferrable(&self) -> &DeferrablePolicy {
210 match self {
211 ToolDefinition::Builtin(b) => &b.deferrable,
212 ToolDefinition::ClientSide(c) => &c.deferrable,
213 }
214 }
215
216 pub fn hints(&self) -> &ToolHints {
218 match self {
219 ToolDefinition::Builtin(b) => &b.hints,
220 ToolDefinition::ClientSide(c) => &c.hints,
221 }
222 }
223
224 pub fn concurrency_class(&self) -> Option<&str> {
228 self.hints().concurrency_class.as_deref()
229 }
230
231 pub fn is_cpu_bound(&self) -> bool {
234 self.hints().cpu_bound.unwrap_or(false)
235 }
236
237 pub fn side_effect_class(&self) -> SideEffectClass {
239 self.hints().effective_side_effect_class()
240 }
241
242 pub fn capability_attribution(&self) -> Option<(&str, Option<&str>)> {
244 self.hints()
245 .capability_id
246 .as_deref()
247 .map(|id| (id, self.hints().capability_name.as_deref()))
248 }
249
250 pub fn with_category(mut self, category: impl Into<String>) -> Self {
252 match &mut self {
253 ToolDefinition::Builtin(b) => b.category = Some(category.into()),
254 ToolDefinition::ClientSide(c) => c.category = Some(category.into()),
255 }
256 self
257 }
258
259 pub fn with_hints(mut self, hints: ToolHints) -> Self {
261 match &mut self {
262 ToolDefinition::Builtin(b) => b.hints = hints,
263 ToolDefinition::ClientSide(c) => c.hints = hints,
264 }
265 self
266 }
267
268 pub fn with_capability_attribution(
270 mut self,
271 capability_id: impl Into<String>,
272 capability_name: Option<impl Into<String>>,
273 ) -> Self {
274 let capability_id = capability_id.into();
275 let capability_name = capability_name.map(Into::into);
276 match &mut self {
277 ToolDefinition::Builtin(b) => {
278 b.hints.capability_id = Some(capability_id);
279 b.hints.capability_name = capability_name;
280 }
281 ToolDefinition::ClientSide(c) => {
282 c.hints.capability_id = Some(capability_id);
283 c.hints.capability_name = capability_name;
284 }
285 }
286 self
287 }
288
289 pub fn with_human_intent_argument(mut self) -> Self {
294 match &mut self {
295 ToolDefinition::Builtin(b) => add_human_intent_to_schema(&mut b.parameters),
296 ToolDefinition::ClientSide(c) => add_human_intent_to_schema(&mut c.parameters),
297 }
298 self
299 }
300}
301
302pub fn add_human_intent_to_tool_definitions(tools: &[ToolDefinition]) -> Vec<ToolDefinition> {
303 tools
304 .iter()
305 .cloned()
306 .map(ToolDefinition::with_human_intent_argument)
307 .collect()
308}
309
310pub fn human_intent(arguments: &Value) -> Option<&str> {
311 arguments
312 .get(HUMAN_INTENT_ARGUMENT)
313 .and_then(Value::as_str)
314 .map(str::trim)
315 .filter(|value| !value.is_empty())
316}
317
318pub fn strip_human_intent_argument(arguments: &Value) -> Value {
319 let mut stripped = arguments.clone();
320 if let Value::Object(ref mut object) = stripped {
321 object.remove(HUMAN_INTENT_ARGUMENT);
322 }
323 stripped
324}
325
326fn add_human_intent_to_schema(schema: &mut Value) {
327 let Value::Object(schema_obj) = schema else {
328 return;
329 };
330
331 schema_obj
332 .entry("type")
333 .or_insert_with(|| Value::String("object".to_string()));
334
335 let properties = schema_obj
336 .entry("properties")
337 .or_insert_with(|| Value::Object(serde_json::Map::new()));
338 if let Value::Object(properties_obj) = properties {
339 properties_obj.insert(
340 HUMAN_INTENT_ARGUMENT.to_string(),
341 serde_json::json!({
342 "type": "string",
343 "description": HUMAN_INTENT_DESCRIPTION,
344 "maxLength": 120,
345 }),
346 );
347 }
348
349 }
352
353#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
364#[cfg_attr(feature = "openapi", derive(ToSchema))]
365pub enum SideEffectClass {
366 Pure,
368 Idempotent,
371 #[default]
374 AtMostOnce,
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
391#[cfg_attr(feature = "openapi", derive(ToSchema))]
392pub struct ToolHints {
393 #[serde(default, skip_serializing_if = "Option::is_none")]
396 pub readonly: Option<bool>,
397
398 #[serde(default, skip_serializing_if = "Option::is_none")]
402 pub destructive: Option<bool>,
403
404 #[serde(default, skip_serializing_if = "Option::is_none")]
407 pub idempotent: Option<bool>,
408
409 #[serde(default, skip_serializing_if = "Option::is_none")]
412 pub open_world: Option<bool>,
413
414 #[serde(default, skip_serializing_if = "Option::is_none")]
418 pub requires_secrets: Option<bool>,
419
420 #[serde(default, skip_serializing_if = "Option::is_none")]
423 pub long_running: Option<bool>,
424
425 #[serde(default, skip_serializing_if = "Option::is_none")]
429 pub supports_background: Option<bool>,
430
431 #[serde(default, skip_serializing_if = "Option::is_none")]
440 pub concurrency_class: Option<String>,
441
442 #[serde(default, skip_serializing_if = "Option::is_none")]
450 pub cpu_bound: Option<bool>,
451
452 #[serde(default, skip_serializing_if = "Option::is_none")]
458 pub persist_output: Option<bool>,
459
460 #[serde(default, skip_serializing_if = "Option::is_none")]
465 pub capability_id: Option<String>,
466
467 #[serde(default, skip_serializing_if = "Option::is_none")]
469 pub capability_name: Option<String>,
470
471 #[serde(default, skip_serializing_if = "Option::is_none")]
476 pub narration_noun: Option<String>,
477
478 #[serde(default, skip_serializing_if = "Option::is_none")]
486 pub side_effect_class: Option<SideEffectClass>,
487
488 #[serde(default, skip_serializing_if = "Option::is_none")]
502 pub metadata: Option<serde_json::Value>,
503}
504
505impl ToolHints {
506 pub fn is_empty(&self) -> bool {
508 *self == Self::default()
509 }
510
511 pub fn with_metadata(mut self, value: serde_json::Value) -> Self {
513 self.metadata = Some(value);
514 self
515 }
516
517 pub fn with_readonly(mut self, value: bool) -> Self {
519 self.readonly = Some(value);
520 self
521 }
522
523 pub fn with_destructive(mut self, value: bool) -> Self {
525 self.destructive = Some(value);
526 self
527 }
528
529 pub fn with_idempotent(mut self, value: bool) -> Self {
531 self.idempotent = Some(value);
532 self
533 }
534
535 pub fn with_open_world(mut self, value: bool) -> Self {
537 self.open_world = Some(value);
538 self
539 }
540
541 pub fn with_capability_attribution(
543 mut self,
544 capability_id: impl Into<String>,
545 capability_name: Option<impl Into<String>>,
546 ) -> Self {
547 self.capability_id = Some(capability_id.into());
548 self.capability_name = capability_name.map(Into::into);
549 self
550 }
551
552 pub fn with_requires_secrets(mut self, value: bool) -> Self {
554 self.requires_secrets = Some(value);
555 self
556 }
557
558 pub fn with_long_running(mut self, value: bool) -> Self {
560 self.long_running = Some(value);
561 self
562 }
563
564 pub fn with_supports_background(mut self, value: bool) -> Self {
566 self.supports_background = Some(value);
567 self
568 }
569
570 pub fn with_concurrency_class(mut self, class: impl Into<String>) -> Self {
572 self.concurrency_class = Some(class.into());
573 self
574 }
575
576 pub fn with_cpu_bound(mut self, value: bool) -> Self {
578 self.cpu_bound = Some(value);
579 self
580 }
581
582 pub fn with_persist_output(mut self, value: bool) -> Self {
584 self.persist_output = Some(value);
585 self
586 }
587
588 pub fn with_narration_noun(mut self, noun: impl Into<String>) -> Self {
590 self.narration_noun = Some(noun.into());
591 self
592 }
593
594 pub fn with_side_effect_class(mut self, class: SideEffectClass) -> Self {
596 self.side_effect_class = Some(class);
597 self
598 }
599
600 pub fn effective_side_effect_class(&self) -> SideEffectClass {
603 self.side_effect_class
604 .clone()
605 .unwrap_or(SideEffectClass::AtMostOnce)
606 }
607}
608
609#[derive(Debug, Clone, Serialize, Deserialize)]
611#[cfg_attr(feature = "openapi", derive(ToSchema))]
612pub struct ToolCall {
613 pub id: String,
615 pub name: String,
617 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
619 pub arguments: serde_json::Value,
620}
621
622impl ToolCall {
623 pub fn execution_arguments(&self) -> serde_json::Value {
625 strip_human_intent_argument(&self.arguments)
626 }
627
628 pub fn to_openai_format(&self) -> serde_json::Value {
633 serde_json::json!({
634 "id": self.id,
635 "type": "function",
636 "function": {
637 "name": self.name,
638 "arguments": serde_json::to_string(&self.arguments).unwrap_or_else(|_| "{}".to_string())
639 }
640 })
641 }
642}
643
644#[derive(Debug, Clone, Serialize, Deserialize)]
646pub struct ToolResult {
647 pub tool_call_id: String,
649 pub result: Option<serde_json::Value>,
651 #[serde(default, skip_serializing_if = "Option::is_none")]
653 pub images: Option<Vec<ToolResultImage>>,
654 pub error: Option<String>,
656 #[serde(default, skip_serializing_if = "Option::is_none")]
659 pub connection_required: Option<String>,
660 #[serde(skip)]
665 pub raw_output: Option<String>,
666}
667
668impl ToolResult {
669 pub fn error(msg: &str) -> Self {
671 Self {
672 tool_call_id: String::new(),
673 result: None,
674 images: None,
675 error: Some(msg.to_string()),
676 connection_required: None,
677 raw_output: None,
678 }
679 }
680}
681
682#[cfg(test)]
683mod tests {
684 use super::*;
685
686 #[test]
687 fn test_scheduling_hints_builders_and_accessors() {
688 let reader = ToolDefinition::Builtin(BuiltinTool {
691 name: "read_file".to_string(),
692 display_name: None,
693 description: "read".to_string(),
694 parameters: serde_json::json!({}),
695 policy: ToolPolicy::Auto,
696 category: None,
697 deferrable: DeferrablePolicy::default(),
698 hints: ToolHints::default().with_readonly(true),
699 full_parameters: None,
700 });
701 assert_eq!(reader.concurrency_class(), None);
702 assert!(!reader.is_cpu_bound());
703
704 let bash = ToolDefinition::Builtin(BuiltinTool {
706 name: "bash".to_string(),
707 display_name: None,
708 description: "bash".to_string(),
709 parameters: serde_json::json!({}),
710 policy: ToolPolicy::Auto,
711 category: None,
712 deferrable: DeferrablePolicy::default(),
713 hints: ToolHints::default()
714 .with_concurrency_class("session_workspace")
715 .with_cpu_bound(true),
716 full_parameters: None,
717 });
718 assert_eq!(bash.concurrency_class(), Some("session_workspace"));
719 assert!(bash.is_cpu_bound());
720
721 let json = serde_json::to_string(bash.hints()).unwrap();
723 let parsed: ToolHints = serde_json::from_str(&json).unwrap();
724 assert_eq!(
725 parsed.concurrency_class.as_deref(),
726 Some("session_workspace")
727 );
728 assert_eq!(parsed.cpu_bound, Some(true));
729 }
730
731 #[test]
732 fn test_builtin_tool_serialization() {
733 let json = r#"{
734 "type": "builtin",
735 "name": "fetch_data",
736 "description": "Fetch data from URL",
737 "parameters": {"type": "object"}
738 }"#;
739
740 let tool: ToolDefinition = serde_json::from_str(json).unwrap();
741 match tool {
742 ToolDefinition::Builtin(builtin) => {
743 assert_eq!(builtin.name, "fetch_data");
744 assert_eq!(builtin.policy, ToolPolicy::Auto);
745 }
746 _ => panic!("expected Builtin variant"),
747 }
748 }
749
750 #[test]
751 fn test_builtin_tool_requires_approval() {
752 let json = r#"{
753 "type": "builtin",
754 "name": "delete_file",
755 "description": "Delete a file",
756 "parameters": {"type": "object"},
757 "policy": "requires_approval"
758 }"#;
759
760 let tool: ToolDefinition = serde_json::from_str(json).unwrap();
761 match tool {
762 ToolDefinition::Builtin(builtin) => {
763 assert_eq!(builtin.policy, ToolPolicy::RequiresApproval);
764 }
765 _ => panic!("expected Builtin variant"),
766 }
767 }
768
769 #[test]
770 fn test_tool_call_serialization() {
771 let tool_call = ToolCall {
772 id: "call_123".to_string(),
773 name: "get_weather".to_string(),
774 arguments: serde_json::json!({"city": "New York"}),
775 };
776
777 let json = serde_json::to_string(&tool_call).unwrap();
778 let parsed: ToolCall = serde_json::from_str(&json).unwrap();
779
780 assert_eq!(parsed.id, tool_call.id);
781 assert_eq!(parsed.name, tool_call.name);
782 }
783
784 #[test]
785 fn test_tool_result_serialization() {
786 let result = ToolResult {
787 tool_call_id: "call_123".to_string(),
788 result: Some(serde_json::json!({"temperature": 72})),
789 images: None,
790 error: None,
791 connection_required: None,
792 raw_output: None,
793 };
794
795 let json = serde_json::to_string(&result).unwrap();
796 let parsed: ToolResult = serde_json::from_str(&json).unwrap();
797
798 assert_eq!(parsed.tool_call_id, result.tool_call_id);
799 assert!(parsed.result.is_some());
800 assert!(parsed.error.is_none());
801 }
802
803 #[test]
804 fn test_tool_definition_accessor_methods() {
805 let tool = ToolDefinition::Builtin(BuiltinTool {
806 name: "test_tool".to_string(),
807 display_name: None,
808 description: "A test tool".to_string(),
809 parameters: serde_json::json!({"type": "object"}),
810 policy: ToolPolicy::RequiresApproval,
811 category: None,
812 deferrable: DeferrablePolicy::default(),
813 hints: ToolHints::default(),
814 full_parameters: None,
815 });
816
817 assert_eq!(tool.name(), "test_tool");
818 assert_eq!(tool.display_name(), None);
819 assert_eq!(tool.description(), "A test tool");
820 assert_eq!(tool.parameters(), &serde_json::json!({"type": "object"}));
821 assert_eq!(tool.policy(), &ToolPolicy::RequiresApproval);
822 }
823
824 #[test]
825 fn test_tool_definition_display_name_accessor() {
826 let builtin = ToolDefinition::Builtin(BuiltinTool {
827 name: "get_weather".to_string(),
828 display_name: Some("Get Weather".to_string()),
829 description: "Gets weather".to_string(),
830 parameters: serde_json::json!({}),
831 policy: ToolPolicy::Auto,
832 category: None,
833 deferrable: DeferrablePolicy::default(),
834 hints: ToolHints::default(),
835 full_parameters: None,
836 });
837 assert_eq!(builtin.display_name(), Some("Get Weather"));
838
839 let client = ToolDefinition::ClientSide(ClientSideTool {
840 name: "deploy".to_string(),
841 display_name: Some("Deploy".to_string()),
842 description: "Deploys".to_string(),
843 parameters: serde_json::json!({}),
844 category: None,
845 deferrable: DeferrablePolicy::default(),
846 hints: ToolHints::default(),
847 full_parameters: None,
848 });
849 assert_eq!(client.display_name(), Some("Deploy"));
850 }
851
852 #[test]
853 fn test_display_name_serialization_skip_none() {
854 let tool = BuiltinTool {
855 name: "test".to_string(),
856 display_name: None,
857 description: "test".to_string(),
858 parameters: serde_json::json!({}),
859 policy: ToolPolicy::Auto,
860 category: None,
861 deferrable: DeferrablePolicy::default(),
862 hints: ToolHints::default(),
863 full_parameters: None,
864 };
865 let json = serde_json::to_string(&tool).unwrap();
866 assert!(!json.contains("display_name"));
867
868 let tool_with = BuiltinTool {
869 name: "test".to_string(),
870 display_name: Some("Test".to_string()),
871 description: "test".to_string(),
872 parameters: serde_json::json!({}),
873 policy: ToolPolicy::Auto,
874 category: None,
875 deferrable: DeferrablePolicy::default(),
876 hints: ToolHints::default(),
877 full_parameters: None,
878 };
879 let json = serde_json::to_string(&tool_with).unwrap();
880 assert!(json.contains("\"display_name\":\"Test\""));
881 }
882
883 #[test]
884 fn test_tool_call_to_openai_format() {
885 let tool_call = ToolCall {
886 id: "call_123".to_string(),
887 name: "get_weather".to_string(),
888 arguments: serde_json::json!({"location": "Tokyo", "units": "celsius"}),
889 };
890
891 let converted = tool_call.to_openai_format();
892
893 assert_eq!(converted["id"], "call_123");
894 assert_eq!(converted["type"], "function");
895 assert_eq!(converted["function"]["name"], "get_weather");
896 let args: serde_json::Value =
898 serde_json::from_str(converted["function"]["arguments"].as_str().unwrap()).unwrap();
899 assert_eq!(args["location"], "Tokyo");
900 assert_eq!(args["units"], "celsius");
901 }
902
903 #[test]
904 fn test_tool_call_to_openai_format_empty_arguments() {
905 let tool_call = ToolCall {
906 id: "call_456".to_string(),
907 name: "list_files".to_string(),
908 arguments: serde_json::json!({}),
909 };
910
911 let converted = tool_call.to_openai_format();
912
913 assert_eq!(converted["id"], "call_456");
914 assert_eq!(converted["function"]["name"], "list_files");
915 assert_eq!(converted["function"]["arguments"], "{}");
916 }
917
918 #[test]
919 fn test_client_side_tool_serialization() {
920 let json = r#"{
921 "type": "client_side",
922 "name": "browser_click",
923 "description": "Click an element in the browser",
924 "parameters": {"type": "object", "properties": {"selector": {"type": "string"}}}
925 }"#;
926
927 let tool: ToolDefinition = serde_json::from_str(json).unwrap();
928 match &tool {
929 ToolDefinition::ClientSide(client) => {
930 assert_eq!(client.name, "browser_click");
931 assert_eq!(client.description, "Click an element in the browser");
932 }
933 _ => panic!("expected ClientSide variant"),
934 }
935
936 assert_eq!(tool.name(), "browser_click");
937 assert_eq!(tool.policy(), &ToolPolicy::ClientSide);
938 }
939
940 #[test]
941 fn test_client_side_tool_roundtrip() {
942 let tool = ToolDefinition::ClientSide(ClientSideTool {
943 name: "run_test".to_string(),
944 display_name: None,
945 description: "Run a test suite".to_string(),
946 parameters: serde_json::json!({"type": "object"}),
947 category: None,
948 deferrable: DeferrablePolicy::default(),
949 hints: ToolHints::default(),
950 full_parameters: None,
951 });
952
953 let json = serde_json::to_string(&tool).unwrap();
954 let parsed: ToolDefinition = serde_json::from_str(&json).unwrap();
955
956 assert_eq!(parsed.name(), "run_test");
957 assert_eq!(parsed.description(), "Run a test suite");
958 assert_eq!(parsed.policy(), &ToolPolicy::ClientSide);
959 }
960
961 #[test]
962 fn test_client_side_tool_accessor_methods() {
963 let tool = ToolDefinition::ClientSide(ClientSideTool {
964 name: "deploy_app".to_string(),
965 display_name: None,
966 description: "Deploy application to staging".to_string(),
967 parameters: serde_json::json!({
968 "type": "object",
969 "properties": {
970 "env": {"type": "string"}
971 },
972 "required": ["env"]
973 }),
974 category: None,
975 deferrable: DeferrablePolicy::default(),
976 hints: ToolHints::default(),
977 full_parameters: None,
978 });
979
980 assert_eq!(tool.name(), "deploy_app");
981 assert_eq!(tool.description(), "Deploy application to staging");
982 assert_eq!(tool.policy(), &ToolPolicy::ClientSide);
983 assert!(tool.parameters().get("properties").is_some());
984 }
985
986 #[test]
987 fn test_client_side_tool_policy_always_client_side() {
988 let tool = ToolDefinition::ClientSide(ClientSideTool {
990 name: "any_tool".to_string(),
991 display_name: None,
992 description: "".to_string(),
993 parameters: serde_json::json!({}),
994 category: None,
995 deferrable: DeferrablePolicy::default(),
996 hints: ToolHints::default(),
997 full_parameters: None,
998 });
999 assert_eq!(tool.policy(), &ToolPolicy::ClientSide);
1000 }
1001
1002 #[test]
1003 fn test_tool_policy_serialization() {
1004 assert_eq!(
1005 serde_json::to_string(&ToolPolicy::ClientSide).unwrap(),
1006 r#""client_side""#
1007 );
1008 assert_eq!(
1009 serde_json::to_string(&ToolPolicy::Auto).unwrap(),
1010 r#""auto""#
1011 );
1012 assert_eq!(
1013 serde_json::to_string(&ToolPolicy::RequiresApproval).unwrap(),
1014 r#""requires_approval""#
1015 );
1016 }
1017
1018 #[test]
1019 fn test_mixed_tool_definitions_in_vec() {
1020 let tools = vec![
1021 ToolDefinition::Builtin(BuiltinTool {
1022 name: "server_tool".to_string(),
1023 display_name: None,
1024 description: "A server tool".to_string(),
1025 parameters: serde_json::json!({"type": "object"}),
1026 policy: ToolPolicy::Auto,
1027 category: None,
1028 deferrable: DeferrablePolicy::default(),
1029 hints: ToolHints::default(),
1030 full_parameters: None,
1031 }),
1032 ToolDefinition::ClientSide(ClientSideTool {
1033 name: "client_tool".to_string(),
1034 display_name: None,
1035 description: "A client tool".to_string(),
1036 parameters: serde_json::json!({"type": "object"}),
1037 category: None,
1038 deferrable: DeferrablePolicy::default(),
1039 hints: ToolHints::default(),
1040 full_parameters: None,
1041 }),
1042 ];
1043
1044 let json = serde_json::to_string(&tools).unwrap();
1045 let parsed: Vec<ToolDefinition> = serde_json::from_str(&json).unwrap();
1046
1047 assert_eq!(parsed.len(), 2);
1048 assert!(matches!(&parsed[0], ToolDefinition::Builtin(_)));
1049 assert!(matches!(&parsed[1], ToolDefinition::ClientSide(_)));
1050 assert_eq!(parsed[0].policy(), &ToolPolicy::Auto);
1051 assert_eq!(parsed[1].policy(), &ToolPolicy::ClientSide);
1052 }
1053
1054 #[test]
1055 fn test_tool_hints_default_is_empty() {
1056 let hints = ToolHints::default();
1057 assert!(hints.is_empty());
1058 assert_eq!(hints.readonly, None);
1059 assert_eq!(hints.destructive, None);
1060 assert_eq!(hints.idempotent, None);
1061 assert_eq!(hints.open_world, None);
1062 assert_eq!(hints.requires_secrets, None);
1063 assert_eq!(hints.long_running, None);
1064 }
1065
1066 #[test]
1067 fn test_tool_hints_builder() {
1068 let hints = ToolHints::default()
1069 .with_readonly(true)
1070 .with_destructive(false)
1071 .with_idempotent(true)
1072 .with_open_world(true)
1073 .with_requires_secrets(true)
1074 .with_long_running(false);
1075
1076 assert!(!hints.is_empty());
1077 assert_eq!(hints.readonly, Some(true));
1078 assert_eq!(hints.destructive, Some(false));
1079 assert_eq!(hints.idempotent, Some(true));
1080 assert_eq!(hints.open_world, Some(true));
1081 assert_eq!(hints.requires_secrets, Some(true));
1082 assert_eq!(hints.long_running, Some(false));
1083 }
1084
1085 #[test]
1086 fn test_tool_hints_serialization_skip_empty() {
1087 let tool = BuiltinTool {
1088 name: "test".to_string(),
1089 display_name: None,
1090 description: "test".to_string(),
1091 parameters: serde_json::json!({}),
1092 policy: ToolPolicy::Auto,
1093 category: None,
1094 deferrable: DeferrablePolicy::default(),
1095 hints: ToolHints::default(),
1096 full_parameters: None,
1097 };
1098 let json = serde_json::to_string(&tool).unwrap();
1099 assert!(!json.contains("hints"), "empty hints should be skipped");
1100 }
1101
1102 #[test]
1103 fn test_tool_hints_serialization_present() {
1104 let tool = BuiltinTool {
1105 name: "test".to_string(),
1106 display_name: None,
1107 description: "test".to_string(),
1108 parameters: serde_json::json!({}),
1109 policy: ToolPolicy::Auto,
1110 category: None,
1111 deferrable: DeferrablePolicy::default(),
1112 hints: ToolHints::default()
1113 .with_readonly(true)
1114 .with_idempotent(true),
1115 full_parameters: None,
1116 };
1117 let json = serde_json::to_string(&tool).unwrap();
1118 assert!(json.contains("\"hints\""));
1119 assert!(json.contains("\"readonly\":true"));
1120 assert!(json.contains("\"idempotent\":true"));
1121 assert!(!json.contains("destructive"));
1123 assert!(!json.contains("open_world"));
1124 }
1125
1126 #[test]
1127 fn test_tool_hints_deserialization_missing() {
1128 let json = r#"{
1129 "type": "builtin",
1130 "name": "test",
1131 "description": "test",
1132 "parameters": {}
1133 }"#;
1134 let tool: ToolDefinition = serde_json::from_str(json).unwrap();
1135 assert!(tool.hints().is_empty());
1136 }
1137
1138 #[test]
1139 fn test_tool_hints_deserialization_present() {
1140 let json = r#"{
1141 "type": "builtin",
1142 "name": "test",
1143 "description": "test",
1144 "parameters": {},
1145 "hints": {"readonly": true, "open_world": true, "requires_secrets": true}
1146 }"#;
1147 let tool: ToolDefinition = serde_json::from_str(json).unwrap();
1148 let hints = tool.hints();
1149 assert_eq!(hints.readonly, Some(true));
1150 assert_eq!(hints.open_world, Some(true));
1151 assert_eq!(hints.requires_secrets, Some(true));
1152 assert_eq!(hints.destructive, None);
1153 assert_eq!(hints.idempotent, None);
1154 assert_eq!(hints.long_running, None);
1155 }
1156
1157 #[test]
1158 fn test_tool_definition_with_hints_builder() {
1159 let tool = ToolDefinition::Builtin(BuiltinTool {
1160 name: "test".to_string(),
1161 display_name: None,
1162 description: "test".to_string(),
1163 parameters: serde_json::json!({}),
1164 policy: ToolPolicy::Auto,
1165 category: None,
1166 deferrable: DeferrablePolicy::default(),
1167 hints: ToolHints::default(),
1168 full_parameters: None,
1169 })
1170 .with_hints(ToolHints::default().with_readonly(true));
1171
1172 assert_eq!(tool.hints().readonly, Some(true));
1173 }
1174
1175 #[test]
1176 fn test_with_human_intent_argument_adds_optional_schema_property() {
1177 let tool = ToolDefinition::Builtin(BuiltinTool {
1178 name: "manage_harnesses".to_string(),
1179 display_name: Some("Manage Harnesses".to_string()),
1180 description: "Manage harnesses".to_string(),
1181 parameters: serde_json::json!({
1182 "type": "object",
1183 "properties": {
1184 "operation": { "type": "string", "enum": ["list"] }
1185 },
1186 "required": ["operation"],
1187 "additionalProperties": false
1188 }),
1189 policy: ToolPolicy::Auto,
1190 category: None,
1191 deferrable: DeferrablePolicy::default(),
1192 hints: ToolHints::default(),
1193 full_parameters: None,
1194 })
1195 .with_human_intent_argument();
1196
1197 let params = tool.parameters();
1198 assert_eq!(
1199 params["properties"][HUMAN_INTENT_ARGUMENT]["type"],
1200 "string"
1201 );
1202 assert!(
1203 params["properties"][HUMAN_INTENT_ARGUMENT]["description"]
1204 .as_str()
1205 .unwrap()
1206 .contains("Listing all harnesses")
1207 );
1208 assert!(
1209 !params["required"]
1210 .as_array()
1211 .unwrap()
1212 .iter()
1213 .any(|item| item.as_str() == Some(HUMAN_INTENT_ARGUMENT))
1214 );
1215 assert_eq!(params["additionalProperties"], false);
1216 }
1217
1218 #[test]
1219 fn test_tool_call_execution_arguments_strip_human_intent() {
1220 let tool_call = ToolCall {
1221 id: "call_1".to_string(),
1222 name: "manage_harnesses".to_string(),
1223 arguments: serde_json::json!({
1224 "operation": "list",
1225 "human_intent": "Listing all harnesses"
1226 }),
1227 };
1228
1229 assert_eq!(
1230 tool_call.execution_arguments(),
1231 serde_json::json!({ "operation": "list" })
1232 );
1233 assert_eq!(
1234 human_intent(&tool_call.arguments),
1235 Some("Listing all harnesses")
1236 );
1237 }
1238
1239 #[test]
1240 fn tool_hints_metadata_is_an_opaque_host_owned_hatch() {
1241 let hints = ToolHints::default()
1242 .with_readonly(true)
1243 .with_metadata(serde_json::json!({"risk_tier": "high"}));
1244
1245 let json = serde_json::to_value(&hints).unwrap();
1248 assert_eq!(json["metadata"]["risk_tier"], "high");
1249 let restored: ToolHints = serde_json::from_value(json).unwrap();
1250 assert_eq!(restored, hints);
1251
1252 let bare = serde_json::to_value(ToolHints::default().with_readonly(true)).unwrap();
1254 assert!(bare.get("metadata").is_none());
1255 }
1256
1257 #[test]
1258 fn tool_hints_with_only_metadata_are_not_empty() {
1259 assert!(ToolHints::default().is_empty());
1260 assert!(
1261 !ToolHints::default()
1262 .with_metadata(serde_json::json!({"any": "thing"}))
1263 .is_empty(),
1264 "metadata alone must keep the hints serialized"
1265 );
1266 }
1267}