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))]
121#[non_exhaustive]
122pub struct ClientSideTool {
123 pub name: String,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub display_name: Option<String>,
128 pub description: String,
130 pub parameters: serde_json::Value,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub category: Option<String>,
135 #[serde(default, skip_serializing_if = "DeferrablePolicy::is_default")]
137 pub deferrable: DeferrablePolicy,
138 #[serde(default, skip_serializing_if = "ToolHints::is_empty")]
140 pub hints: ToolHints,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub full_parameters: Option<serde_json::Value>,
146}
147
148impl ClientSideTool {
149 pub fn new(
155 name: impl Into<String>,
156 description: impl Into<String>,
157 parameters: serde_json::Value,
158 ) -> Self {
159 ClientSideTool {
160 name: name.into(),
161 display_name: None,
162 description: description.into(),
163 parameters,
164 category: None,
165 deferrable: DeferrablePolicy::default(),
166 hints: ToolHints::default(),
167 full_parameters: None,
168 }
169 }
170
171 #[must_use]
173 pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
174 self.display_name = Some(display_name.into());
175 self
176 }
177
178 #[must_use]
180 pub fn with_category(mut self, category: impl Into<String>) -> Self {
181 self.category = Some(category.into());
182 self
183 }
184
185 #[must_use]
187 pub fn with_deferrable(mut self, deferrable: DeferrablePolicy) -> Self {
188 self.deferrable = deferrable;
189 self
190 }
191
192 #[must_use]
194 pub fn with_hints(mut self, hints: ToolHints) -> Self {
195 self.hints = hints;
196 self
197 }
198}
199
200impl ToolDefinition {
201 pub fn function(
220 name: impl Into<String>,
221 description: impl Into<String>,
222 parameters: serde_json::Value,
223 ) -> Self {
224 ToolDefinition::ClientSide(ClientSideTool::new(name, description, parameters))
225 }
226
227 pub fn name(&self) -> &str {
229 match self {
230 ToolDefinition::Builtin(b) => &b.name,
231 ToolDefinition::ClientSide(c) => &c.name,
232 }
233 }
234
235 pub fn display_name(&self) -> Option<&str> {
237 match self {
238 ToolDefinition::Builtin(b) => b.display_name.as_deref(),
239 ToolDefinition::ClientSide(c) => c.display_name.as_deref(),
240 }
241 }
242
243 pub fn description(&self) -> &str {
245 match self {
246 ToolDefinition::Builtin(b) => &b.description,
247 ToolDefinition::ClientSide(c) => &c.description,
248 }
249 }
250
251 pub fn parameters(&self) -> &serde_json::Value {
253 match self {
254 ToolDefinition::Builtin(b) => &b.parameters,
255 ToolDefinition::ClientSide(c) => &c.parameters,
256 }
257 }
258
259 pub fn full_parameters(&self) -> &serde_json::Value {
265 match self {
266 ToolDefinition::Builtin(b) => b.full_parameters.as_ref().unwrap_or(&b.parameters),
267 ToolDefinition::ClientSide(c) => c.full_parameters.as_ref().unwrap_or(&c.parameters),
268 }
269 }
270
271 pub fn policy(&self) -> &ToolPolicy {
273 match self {
274 ToolDefinition::Builtin(b) => &b.policy,
275 ToolDefinition::ClientSide(_) => &ToolPolicy::ClientSide,
276 }
277 }
278
279 pub fn category(&self) -> Option<&str> {
281 match self {
282 ToolDefinition::Builtin(b) => b.category.as_deref(),
283 ToolDefinition::ClientSide(c) => c.category.as_deref(),
284 }
285 }
286
287 pub fn deferrable(&self) -> &DeferrablePolicy {
289 match self {
290 ToolDefinition::Builtin(b) => &b.deferrable,
291 ToolDefinition::ClientSide(c) => &c.deferrable,
292 }
293 }
294
295 pub fn hints(&self) -> &ToolHints {
297 match self {
298 ToolDefinition::Builtin(b) => &b.hints,
299 ToolDefinition::ClientSide(c) => &c.hints,
300 }
301 }
302
303 pub fn concurrency_class(&self) -> Option<&str> {
307 self.hints().concurrency_class.as_deref()
308 }
309
310 pub fn is_cpu_bound(&self) -> bool {
313 self.hints().cpu_bound.unwrap_or(false)
314 }
315
316 pub fn side_effect_class(&self) -> SideEffectClass {
318 self.hints().effective_side_effect_class()
319 }
320
321 pub fn capability_attribution(&self) -> Option<(&str, Option<&str>)> {
323 self.hints()
324 .capability_id
325 .as_deref()
326 .map(|id| (id, self.hints().capability_name.as_deref()))
327 }
328
329 pub fn with_category(mut self, category: impl Into<String>) -> Self {
331 match &mut self {
332 ToolDefinition::Builtin(b) => b.category = Some(category.into()),
333 ToolDefinition::ClientSide(c) => c.category = Some(category.into()),
334 }
335 self
336 }
337
338 pub fn with_hints(mut self, hints: ToolHints) -> Self {
340 match &mut self {
341 ToolDefinition::Builtin(b) => b.hints = hints,
342 ToolDefinition::ClientSide(c) => c.hints = hints,
343 }
344 self
345 }
346
347 pub fn with_capability_attribution(
349 mut self,
350 capability_id: impl Into<String>,
351 capability_name: Option<impl Into<String>>,
352 ) -> Self {
353 let capability_id = capability_id.into();
354 let capability_name = capability_name.map(Into::into);
355 match &mut self {
356 ToolDefinition::Builtin(b) => {
357 b.hints.capability_id = Some(capability_id);
358 b.hints.capability_name = capability_name;
359 }
360 ToolDefinition::ClientSide(c) => {
361 c.hints.capability_id = Some(capability_id);
362 c.hints.capability_name = capability_name;
363 }
364 }
365 self
366 }
367
368 pub fn with_human_intent_argument(mut self) -> Self {
373 match &mut self {
374 ToolDefinition::Builtin(b) => add_human_intent_to_schema(&mut b.parameters),
375 ToolDefinition::ClientSide(c) => add_human_intent_to_schema(&mut c.parameters),
376 }
377 self
378 }
379}
380
381pub fn add_human_intent_to_tool_definitions(tools: &[ToolDefinition]) -> Vec<ToolDefinition> {
382 tools
383 .iter()
384 .cloned()
385 .map(ToolDefinition::with_human_intent_argument)
386 .collect()
387}
388
389pub fn human_intent(arguments: &Value) -> Option<&str> {
390 arguments
391 .get(HUMAN_INTENT_ARGUMENT)
392 .and_then(Value::as_str)
393 .map(str::trim)
394 .filter(|value| !value.is_empty())
395}
396
397pub fn strip_human_intent_argument(arguments: &Value) -> Value {
398 let mut stripped = arguments.clone();
399 if let Value::Object(ref mut object) = stripped {
400 object.remove(HUMAN_INTENT_ARGUMENT);
401 }
402 stripped
403}
404
405fn add_human_intent_to_schema(schema: &mut Value) {
406 let Value::Object(schema_obj) = schema else {
407 return;
408 };
409
410 schema_obj
411 .entry("type")
412 .or_insert_with(|| Value::String("object".to_string()));
413
414 let properties = schema_obj
415 .entry("properties")
416 .or_insert_with(|| Value::Object(serde_json::Map::new()));
417 if let Value::Object(properties_obj) = properties {
418 properties_obj.insert(
419 HUMAN_INTENT_ARGUMENT.to_string(),
420 serde_json::json!({
421 "type": "string",
422 "description": HUMAN_INTENT_DESCRIPTION,
423 "maxLength": 120,
424 }),
425 );
426 }
427
428 }
431
432#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
443#[cfg_attr(feature = "openapi", derive(ToSchema))]
444pub enum SideEffectClass {
445 Pure,
447 Idempotent,
450 #[default]
453 AtMostOnce,
454}
455
456#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
470#[cfg_attr(feature = "openapi", derive(ToSchema))]
471pub struct ToolHints {
472 #[serde(default, skip_serializing_if = "Option::is_none")]
475 pub readonly: Option<bool>,
476
477 #[serde(default, skip_serializing_if = "Option::is_none")]
481 pub destructive: Option<bool>,
482
483 #[serde(default, skip_serializing_if = "Option::is_none")]
486 pub idempotent: Option<bool>,
487
488 #[serde(default, skip_serializing_if = "Option::is_none")]
491 pub open_world: Option<bool>,
492
493 #[serde(default, skip_serializing_if = "Option::is_none")]
497 pub requires_secrets: Option<bool>,
498
499 #[serde(default, skip_serializing_if = "Option::is_none")]
502 pub long_running: Option<bool>,
503
504 #[serde(default, skip_serializing_if = "Option::is_none")]
508 pub supports_background: Option<bool>,
509
510 #[serde(default, skip_serializing_if = "Option::is_none")]
519 pub concurrency_class: Option<String>,
520
521 #[serde(default, skip_serializing_if = "Option::is_none")]
529 pub cpu_bound: Option<bool>,
530
531 #[serde(default, skip_serializing_if = "Option::is_none")]
537 pub persist_output: Option<bool>,
538
539 #[serde(default, skip_serializing_if = "Option::is_none")]
544 pub capability_id: Option<String>,
545
546 #[serde(default, skip_serializing_if = "Option::is_none")]
548 pub capability_name: Option<String>,
549
550 #[serde(default, skip_serializing_if = "Option::is_none")]
555 pub narration_noun: Option<String>,
556
557 #[serde(default, skip_serializing_if = "Option::is_none")]
565 pub side_effect_class: Option<SideEffectClass>,
566
567 #[serde(default, skip_serializing_if = "Option::is_none")]
581 pub metadata: Option<serde_json::Value>,
582}
583
584impl ToolHints {
585 pub fn is_empty(&self) -> bool {
587 *self == Self::default()
588 }
589
590 pub fn with_metadata(mut self, value: serde_json::Value) -> Self {
592 self.metadata = Some(value);
593 self
594 }
595
596 pub fn with_readonly(mut self, value: bool) -> Self {
598 self.readonly = Some(value);
599 self
600 }
601
602 pub fn with_destructive(mut self, value: bool) -> Self {
604 self.destructive = Some(value);
605 self
606 }
607
608 pub fn with_idempotent(mut self, value: bool) -> Self {
610 self.idempotent = Some(value);
611 self
612 }
613
614 pub fn with_open_world(mut self, value: bool) -> Self {
616 self.open_world = Some(value);
617 self
618 }
619
620 pub fn with_capability_attribution(
622 mut self,
623 capability_id: impl Into<String>,
624 capability_name: Option<impl Into<String>>,
625 ) -> Self {
626 self.capability_id = Some(capability_id.into());
627 self.capability_name = capability_name.map(Into::into);
628 self
629 }
630
631 pub fn with_requires_secrets(mut self, value: bool) -> Self {
633 self.requires_secrets = Some(value);
634 self
635 }
636
637 pub fn with_long_running(mut self, value: bool) -> Self {
639 self.long_running = Some(value);
640 self
641 }
642
643 pub fn with_supports_background(mut self, value: bool) -> Self {
645 self.supports_background = Some(value);
646 self
647 }
648
649 pub fn with_concurrency_class(mut self, class: impl Into<String>) -> Self {
651 self.concurrency_class = Some(class.into());
652 self
653 }
654
655 pub fn with_cpu_bound(mut self, value: bool) -> Self {
657 self.cpu_bound = Some(value);
658 self
659 }
660
661 pub fn with_persist_output(mut self, value: bool) -> Self {
663 self.persist_output = Some(value);
664 self
665 }
666
667 pub fn with_narration_noun(mut self, noun: impl Into<String>) -> Self {
669 self.narration_noun = Some(noun.into());
670 self
671 }
672
673 pub fn with_side_effect_class(mut self, class: SideEffectClass) -> Self {
675 self.side_effect_class = Some(class);
676 self
677 }
678
679 pub fn effective_side_effect_class(&self) -> SideEffectClass {
682 self.side_effect_class
683 .clone()
684 .unwrap_or(SideEffectClass::AtMostOnce)
685 }
686}
687
688#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
690#[cfg_attr(feature = "openapi", derive(ToSchema))]
691pub struct ToolCall {
692 pub id: String,
694 pub name: String,
696 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
698 pub arguments: serde_json::Value,
699}
700
701impl ToolCall {
702 pub fn execution_arguments(&self) -> serde_json::Value {
704 strip_human_intent_argument(&self.arguments)
705 }
706
707 pub fn to_openai_format(&self) -> serde_json::Value {
712 serde_json::json!({
713 "id": self.id,
714 "type": "function",
715 "function": {
716 "name": self.name,
717 "arguments": serde_json::to_string(&self.arguments).unwrap_or_else(|_| "{}".to_string())
718 }
719 })
720 }
721}
722
723#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
725#[serde(rename_all = "snake_case")]
726pub enum ConnectionRequiredSubject {
727 Agent,
729 User,
731}
732
733#[derive(Debug, Clone, PartialEq, Eq)]
738pub struct ConnectionRequired {
739 pub provider: String,
741 pub subject: Option<ConnectionRequiredSubject>,
743 pub setup_url: Option<String>,
745}
746
747impl ConnectionRequired {
748 pub fn provider_only(provider: impl Into<String>) -> Self {
750 Self {
751 provider: provider.into(),
752 subject: None,
753 setup_url: None,
754 }
755 }
756
757 pub fn with_setup(
759 provider: impl Into<String>,
760 subject: ConnectionRequiredSubject,
761 setup_url: impl Into<String>,
762 ) -> Self {
763 Self {
764 provider: provider.into(),
765 subject: Some(subject),
766 setup_url: Some(setup_url.into()),
767 }
768 }
769}
770
771impl From<String> for ConnectionRequired {
772 fn from(provider: String) -> Self {
773 Self::provider_only(provider)
774 }
775}
776
777impl From<&str> for ConnectionRequired {
778 fn from(provider: &str) -> Self {
779 Self::provider_only(provider)
780 }
781}
782
783impl Serialize for ConnectionRequired {
785 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
786 where
787 S: serde::Serializer,
788 {
789 if self.subject.is_none() && self.setup_url.is_none() {
790 return serializer.serialize_str(&self.provider);
791 }
792
793 #[derive(Serialize)]
794 struct Details<'a> {
795 provider: &'a str,
796 #[serde(skip_serializing_if = "Option::is_none")]
797 subject: Option<ConnectionRequiredSubject>,
798 #[serde(skip_serializing_if = "Option::is_none")]
799 setup_url: Option<&'a str>,
800 }
801
802 Details {
803 provider: &self.provider,
804 subject: self.subject,
805 setup_url: self.setup_url.as_deref(),
806 }
807 .serialize(serializer)
808 }
809}
810
811impl<'de> Deserialize<'de> for ConnectionRequired {
812 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
813 where
814 D: serde::Deserializer<'de>,
815 {
816 #[derive(Deserialize)]
817 #[serde(untagged)]
818 enum Wire {
819 Provider(String),
820 Details {
821 provider: String,
822 #[serde(default)]
823 subject: Option<ConnectionRequiredSubject>,
824 #[serde(default)]
825 setup_url: Option<String>,
826 },
827 }
828
829 Ok(match Wire::deserialize(deserializer)? {
830 Wire::Provider(provider) => Self::provider_only(provider),
831 Wire::Details {
832 provider,
833 subject,
834 setup_url,
835 } => Self {
836 provider,
837 subject,
838 setup_url,
839 },
840 })
841 }
842}
843
844#[derive(Debug, Clone, Serialize, Deserialize)]
846pub struct ToolResult {
847 pub tool_call_id: String,
849 pub result: Option<serde_json::Value>,
851 #[serde(default, skip_serializing_if = "Option::is_none")]
853 pub images: Option<Vec<ToolResultImage>>,
854 pub error: Option<String>,
856 #[serde(default, skip_serializing_if = "Option::is_none")]
860 pub connection_required: Option<ConnectionRequired>,
861 #[serde(skip)]
866 pub raw_output: Option<String>,
867}
868
869pub const URL_ELICITATION_REQUIRED_CODE: &str = "url_elicitation_required";
875
876pub const CONFIRM_URL_ELICITATION_TOOL: &str = "confirm_url_elicitation";
880
881pub use crate::form_elicitation_types::{
882 FORM_ELICITATION_CALL_ID_PREFIX, FORM_ELICITATION_REQUIRED_CODE, FormElicitationRequired,
883 MCP_ELICITATION_ARGUMENT,
884};
885pub use crate::tool_approval_types::{
886 APPROVE_TOOL_CALL_TOOL, TOOL_APPROVAL_CALL_ID_PREFIX, TOOL_APPROVAL_REQUIRED_CODE,
887 TOOL_ARGUMENTS_PREVIEW_BYTES, ToolApprovalRequired, preview_tool_arguments,
888};
889
890pub const ASK_USER_TOOL_NAME: &str = "ask_user";
897
898pub fn unattended_ask_user_result(arguments: &serde_json::Value) -> serde_json::Value {
914 let questions = arguments
915 .get("questions")
916 .and_then(|value| value.as_array());
917
918 if arguments.get(MCP_ELICITATION_ARGUMENT).is_some() {
921 return serde_json::json!({
922 "status": "declined",
923 "answered_by": "unattended",
924 "answers": [],
925 });
926 }
927
928 if questions.is_some_and(|questions| {
931 questions.iter().any(|question| {
932 matches!(
933 question.get("kind").and_then(|v| v.as_str()),
934 Some("text" | "secret")
935 )
936 })
937 }) {
938 return serde_json::json!({
939 "status": "declined",
940 "answered_by": "unattended",
941 "answers": [],
942 });
943 }
944
945 let answers: Vec<serde_json::Value> = questions
946 .map(|questions| {
947 questions
948 .iter()
949 .map(|question| {
950 let options = question.get("options").and_then(|v| v.as_array());
951 let label = |option: &serde_json::Value| {
952 option
953 .get("label")
954 .and_then(|v| v.as_str())
955 .map(str::to_string)
956 };
957 let mut selected: Vec<String> = options
958 .map(|options| {
959 options
960 .iter()
961 .filter(|option| {
962 option
963 .get("default")
964 .and_then(|v| v.as_bool())
965 .unwrap_or(false)
966 })
967 .filter_map(label)
968 .collect()
969 })
970 .unwrap_or_default();
971 if selected.is_empty() {
972 selected.extend(options.and_then(|o| o.first()).and_then(label));
973 }
974 serde_json::json!({
975 "id": question.get("id").and_then(|v| v.as_str()).unwrap_or_default(),
976 "selected": selected,
977 "other_text": serde_json::Value::Null,
978 })
979 })
980 .collect()
981 })
982 .unwrap_or_default();
983
984 serde_json::json!({
985 "status": "answered",
986 "answered_by": "unattended",
989 "answers": answers,
990 })
991}
992
993#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1001pub struct UrlElicitationRequired {
1002 pub code: String,
1004 pub error: String,
1006 pub url: String,
1009 pub url_host: String,
1012 pub url_is_punycode: bool,
1015 pub server: String,
1017 pub tool: String,
1020 pub retry_tool: String,
1023 pub message: String,
1025 pub declined: bool,
1027}
1028
1029impl UrlElicitationRequired {
1030 pub fn from_tool_result(result: &ToolResult) -> Option<Self> {
1032 let value = result.result.as_ref()?;
1033 if value.get("code")?.as_str()? != URL_ELICITATION_REQUIRED_CODE {
1034 return None;
1035 }
1036 serde_json::from_value(value.clone()).ok()
1037 }
1038}
1039
1040impl ToolResult {
1041 pub fn error(msg: &str) -> Self {
1043 Self {
1044 tool_call_id: String::new(),
1045 result: None,
1046 images: None,
1047 error: Some(msg.to_string()),
1048 connection_required: None,
1049 raw_output: None,
1050 }
1051 }
1052}
1053
1054#[cfg(test)]
1055mod tests {
1056 use super::*;
1057 use serde_json::json;
1058
1059 fn definition(kind: &str) -> ToolDefinition {
1060 serde_json::from_value(json!({"type":kind,"name":"tool","description":"Description","parameters":{"type":"object","properties":{"input":{"type":"string"}},"required":["input"]}})).unwrap()
1061 }
1062
1063 #[test]
1064 fn tool_variants_have_complete_literal_wire_defaults_and_effective_policies() {
1065 for kind in ["builtin", "client_side"] {
1066 let tool = definition(kind);
1067 let mut expected = json!({"type":kind,"name":"tool","description":"Description","parameters":{"type":"object","properties":{"input":{"type":"string"}},"required":["input"]}});
1068 if kind == "builtin" {
1069 expected["policy"] = json!("auto");
1070 }
1071 assert_eq!(serde_json::to_value(&tool).unwrap(), expected);
1072 assert_eq!(tool.name(), "tool");
1073 assert_eq!(tool.description(), "Description");
1074 assert_eq!(tool.display_name(), None);
1075 assert_eq!(
1076 tool.parameters(),
1077 &json!({"type":"object","properties":{"input":{"type":"string"}},"required":["input"]})
1078 );
1079 assert_eq!(
1080 tool.policy(),
1081 if kind == "builtin" {
1082 &ToolPolicy::Auto
1083 } else {
1084 &ToolPolicy::ClientSide
1085 }
1086 );
1087 assert_eq!(tool.deferrable(), &DeferrablePolicy::Automatic);
1088 assert!(tool.hints().is_empty());
1089 assert_eq!(tool.concurrency_class(), None);
1090 assert!(!tool.is_cpu_bound());
1091 assert_eq!(tool.side_effect_class(), SideEffectClass::AtMostOnce);
1092 }
1093 let mixed:Vec<ToolDefinition>=serde_json::from_value(json!([
1094 {"type":"builtin","name":"server","description":"Server","parameters":{},"policy":"requires_approval"},
1095 {"type":"client_side","name":"client","description":"Client","parameters":{},"policy":"auto"}
1096 ])).unwrap();
1097 assert_eq!(
1098 (
1099 mixed[0].name(),
1100 mixed[0].policy(),
1101 mixed[1].name(),
1102 mixed[1].policy()
1103 ),
1104 (
1105 "server",
1106 &ToolPolicy::RequiresApproval,
1107 "client",
1108 &ToolPolicy::ClientSide
1109 )
1110 );
1111 assert!(matches!(&mixed[0], ToolDefinition::Builtin(_)));
1112 assert!(matches!(&mixed[1], ToolDefinition::ClientSide(_)));
1113 for (policy, wire) in [
1114 (ToolPolicy::Auto, "auto"),
1115 (ToolPolicy::RequiresApproval, "requires_approval"),
1116 (ToolPolicy::ClientSide, "client_side"),
1117 ] {
1118 assert_eq!(serde_json::to_value(&policy).unwrap(), json!(wire));
1119 assert_eq!(
1120 serde_json::from_value::<ToolPolicy>(json!(wire)).unwrap(),
1121 policy
1122 );
1123 }
1124 }
1125
1126 #[test]
1127 fn hints_builders_preserve_false_values_metadata_and_scheduling_fields() {
1128 for kind in ["builtin", "client_side"] {
1129 let reader = definition(kind).with_hints(ToolHints::default().with_readonly(true));
1130 assert_eq!(reader.concurrency_class(), None);
1131 assert!(!reader.is_cpu_bound());
1132 assert_eq!(reader.side_effect_class(), SideEffectClass::AtMostOnce);
1133 }
1134
1135 for value in [false, true] {
1136 let hints = ToolHints::default()
1137 .with_readonly(value)
1138 .with_destructive(value)
1139 .with_idempotent(value)
1140 .with_open_world(value)
1141 .with_requires_secrets(value)
1142 .with_long_running(value)
1143 .with_supports_background(value)
1144 .with_cpu_bound(value)
1145 .with_persist_output(value)
1146 .with_concurrency_class("session_workspace")
1147 .with_narration_noun("file")
1148 .with_side_effect_class(SideEffectClass::Idempotent)
1149 .with_capability_attribution("files", Some("Files"))
1150 .with_metadata(json!({"risk_tier":"high","nested":[1,false,null]}));
1151 let expected = json!({"readonly":value,"destructive":value,"idempotent":value,"open_world":value,"requires_secrets":value,"long_running":value,"supports_background":value,"cpu_bound":value,"persist_output":value,"concurrency_class":"session_workspace","narration_noun":"file","side_effect_class":"Idempotent","capability_id":"files","capability_name":"Files","metadata":{"risk_tier":"high","nested":[1,false,null]}});
1152 assert_eq!(serde_json::to_value(&hints).unwrap(), expected);
1153 assert_eq!(
1154 serde_json::from_value::<ToolHints>(expected).unwrap(),
1155 hints
1156 );
1157 for kind in ["builtin", "client_side"] {
1158 let tool = definition(kind)
1159 .with_hints(hints.clone())
1160 .with_category("workspace")
1161 .with_capability_attribution("new-files", Some("New Files"));
1162 assert_eq!(tool.category(), Some("workspace"));
1163 assert_eq!(tool.concurrency_class(), Some("session_workspace"));
1164 assert_eq!(tool.is_cpu_bound(), value);
1165 assert_eq!(tool.side_effect_class(), SideEffectClass::Idempotent);
1166 assert_eq!(
1167 tool.capability_attribution(),
1168 Some(("new-files", Some("New Files")))
1169 );
1170 let mut expected_hints = serde_json::to_value(&hints).unwrap();
1171 expected_hints["capability_id"] = json!("new-files");
1172 expected_hints["capability_name"] = json!("New Files");
1173 assert_eq!(serde_json::to_value(tool.hints()).unwrap(), expected_hints);
1174 }
1175 }
1176 assert_eq!(
1177 serde_json::to_value(ToolHints::default()).unwrap(),
1178 json!({})
1179 );
1180 let metadata = ToolHints::default().with_metadata(json!({"any":"thing"}));
1181 assert!(!metadata.is_empty());
1182 assert_eq!(
1183 serde_json::to_value(metadata).unwrap(),
1184 json!({"metadata":{"any":"thing"}})
1185 );
1186 for class in [
1187 SideEffectClass::Pure,
1188 SideEffectClass::Idempotent,
1189 SideEffectClass::AtMostOnce,
1190 ] {
1191 assert_eq!(
1192 ToolHints::default()
1193 .with_side_effect_class(class.clone())
1194 .effective_side_effect_class(),
1195 class
1196 );
1197 }
1198 }
1199
1200 #[test]
1201 fn tool_display_and_deferred_schemas_survive_both_wire_variants() {
1202 for kind in ["builtin", "client_side"] {
1203 for (deferrable, wire) in [
1204 (DeferrablePolicy::Never, Some("never")),
1205 (DeferrablePolicy::Automatic, None),
1206 (DeferrablePolicy::Always, Some("always")),
1207 ] {
1208 let mut payload = json!({"type":kind,"name":"tool","display_name":"Display","description":"Description","parameters":{"type":"object"},"full_parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]},"category":"files"});
1209 if let Some(wire) = wire {
1210 payload["deferrable"] = json!(wire);
1211 }
1212 let tool: ToolDefinition = serde_json::from_value(payload.clone()).unwrap();
1213 assert_eq!(tool.display_name(), Some("Display"));
1214 assert_eq!(tool.deferrable(), &deferrable);
1215 assert_eq!(tool.parameters(), &json!({"type":"object"}));
1216 assert_eq!(
1217 tool.full_parameters(),
1218 &json!({"type":"object","properties":{"path":{"type":"string"}},"required":["path"]})
1219 );
1220 if kind == "builtin" {
1221 payload["policy"] = json!("auto");
1222 }
1223 assert_eq!(serde_json::to_value(tool).unwrap(), payload);
1224 }
1225 let tool = definition(kind);
1226 assert_eq!(tool.full_parameters(), tool.parameters());
1227 }
1228 }
1229
1230 #[test]
1231 fn tool_call_wire_and_openai_arguments_preserve_the_complete_payload() {
1232 for (arguments, text) in [
1233 (json!({"city":"New York"}), r#"{"city":"New York"}"#),
1234 (json!({}), "{}"),
1235 (
1236 json!({"count":9007199254740993_u64,"text":"line\nquoted\""}),
1237 r#"{"count":9007199254740993,"text":"line\nquoted\""}"#,
1238 ),
1239 ] {
1240 let call = ToolCall {
1241 id: "call_123".into(),
1242 name: "get_weather".into(),
1243 arguments: arguments.clone(),
1244 };
1245 assert_eq!(
1246 serde_json::to_value(&call).unwrap(),
1247 json!({"id":"call_123","name":"get_weather","arguments":arguments})
1248 );
1249 let parsed: ToolCall = serde_json::from_value(
1250 json!({"id":"call_123","name":"get_weather","arguments":arguments}),
1251 )
1252 .unwrap();
1253 assert_eq!(parsed.arguments, arguments);
1254 assert_eq!(
1255 call.to_openai_format(),
1256 json!({"id":"call_123","type":"function","function":{"name":"get_weather","arguments":text}})
1257 );
1258 }
1259 }
1260
1261 #[test]
1262 fn tool_result_wire_excludes_raw_output_but_preserves_images_errors_and_data() {
1263 let expected = json!({"tool_call_id":"call_123","result":{"temperature":72},"images":[{"base64":"aGk=","media_type":"image/png"}],"error":"partial failure","connection_required":"provider"});
1264 let mut injected = expected.clone();
1265 injected["raw_output"] = json!("private-raw");
1266 let mut result: ToolResult = serde_json::from_value(injected).unwrap();
1267 assert_eq!(
1268 result.connection_required,
1269 Some(ConnectionRequired::provider_only("provider"))
1270 );
1271 assert!(result.raw_output.is_none());
1272 result.raw_output = Some("private-raw".into());
1273 assert_eq!(serde_json::to_value(result).unwrap(), expected);
1274 assert_eq!(
1275 serde_json::to_value(ToolResult::error("failed")).unwrap(),
1276 json!({"tool_call_id":"","result":null,"error":"failed"})
1277 );
1278 let success: ToolResult = serde_json::from_value(
1279 json!({"tool_call_id":"call_123","result":{"temperature":72},"error":null}),
1280 )
1281 .unwrap();
1282 assert_eq!(
1283 serde_json::to_value(success).unwrap(),
1284 json!({"tool_call_id":"call_123","result":{"temperature":72},"error":null})
1285 );
1286 }
1287
1288 #[test]
1289 fn connection_required_wire_distinguishes_subjects_and_accepts_provider_only_values() {
1290 for (subject, setup_url) in [
1291 (
1292 ConnectionRequiredSubject::Agent,
1293 "/agents/agent_123?tab=mcp",
1294 ),
1295 (ConnectionRequiredSubject::User, "/settings/connections"),
1296 ] {
1297 let required = ConnectionRequired::with_setup("mcp_oauth_123", subject, setup_url);
1298 let wire = serde_json::to_value(&required).unwrap();
1299 assert_eq!(wire["provider"], "mcp_oauth_123");
1300 assert_eq!(
1301 wire["subject"],
1302 match subject {
1303 ConnectionRequiredSubject::Agent => "agent",
1304 ConnectionRequiredSubject::User => "user",
1305 }
1306 );
1307 assert_eq!(wire["setup_url"], setup_url);
1308 assert_eq!(
1309 serde_json::from_value::<ConnectionRequired>(wire).unwrap(),
1310 required
1311 );
1312 }
1313
1314 let legacy: ConnectionRequired = serde_json::from_value(json!("daytona")).unwrap();
1315 assert_eq!(legacy, ConnectionRequired::provider_only("daytona"));
1316 assert_eq!(serde_json::to_value(legacy).unwrap(), json!("daytona"));
1317
1318 let provider_object: ConnectionRequired =
1319 serde_json::from_value(json!({"provider":"github"})).unwrap();
1320 assert_eq!(provider_object, ConnectionRequired::provider_only("github"));
1321 }
1322
1323 #[test]
1324 fn narration_schema_is_optional_idempotent_and_does_not_mutate_input_definitions() {
1325 let original = vec![definition("builtin"), definition("client_side")];
1326 let before = serde_json::to_value(&original).unwrap();
1327 let augmented = add_human_intent_to_tool_definitions(&original);
1328 assert_eq!(serde_json::to_value(&original).unwrap(), before);
1329 let property = json!({"type":"string","description":"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.","maxLength":120});
1330 for tool in &augmented {
1331 assert_eq!(
1332 tool.parameters(),
1333 &json!({"type":"object","properties":{"input":{"type":"string"},"human_intent":property},"required":["input"]})
1334 );
1335 }
1336 assert_eq!(
1337 serde_json::to_value(add_human_intent_to_tool_definitions(&augmented)).unwrap(),
1338 serde_json::to_value(&augmented).unwrap()
1339 );
1340 let mut closed = json!({"properties":{"operation":{"type":"string","enum":["list"]}},"required":["operation"],"additionalProperties":false});
1341 add_human_intent_to_schema(&mut closed);
1342 assert_eq!(
1343 closed,
1344 json!({"type":"object","properties":{"operation":{"type":"string","enum":["list"]},"human_intent":property},"required":["operation"],"additionalProperties":false})
1345 );
1346 let mut empty = json!({});
1347 add_human_intent_to_schema(&mut empty);
1348 assert_eq!(
1349 empty,
1350 json!({"type":"object","properties":{"human_intent":property}})
1351 );
1352 for mut scalar in [json!(null), json!(false), json!([])] {
1353 let before = scalar.clone();
1354 add_human_intent_to_schema(&mut scalar);
1355 assert_eq!(scalar, before);
1356 }
1357 }
1358
1359 #[test]
1360 fn execution_strips_only_top_level_narration_and_trims_display_text() {
1361 for (value, expected) in [
1362 (
1363 json!(" Listing all harnesses "),
1364 Some("Listing all harnesses"),
1365 ),
1366 (json!(" \t"), None),
1367 (json!(7), None),
1368 (json!(null), None),
1369 ] {
1370 let arguments = json!({"operation":"list","human_intent":value,"nested":{"human_intent":"ordinary data"}});
1371 let call = ToolCall {
1372 id: "call".into(),
1373 name: "manage_harnesses".into(),
1374 arguments: arguments.clone(),
1375 };
1376 assert_eq!(human_intent(&call.arguments), expected);
1377 assert_eq!(
1378 call.execution_arguments(),
1379 json!({"operation":"list","nested":{"human_intent":"ordinary data"}})
1380 );
1381 assert_eq!(call.arguments, arguments);
1382 }
1383 for value in [json!(null), json!([1, "text"]), json!({"operation":"list"})] {
1384 assert_eq!(strip_human_intent_argument(&value), value);
1385 assert_eq!(human_intent(&value), None);
1386 }
1387 }
1388
1389 #[test]
1390 fn unattended_never_answers_a_servers_questions() {
1391 let arguments = json!({
1392 "questions": [{"id": "plan", "header": "billing", "question": "Plan?",
1393 "options": [{"label": "Pro", "description": "", "default": true},
1394 {"label": "Team", "description": ""}]}],
1395 "mcp_elicitation": {"server": "billing", "tool": "buy"}
1396 });
1397 let result = unattended_ask_user_result(&arguments);
1398 assert_eq!(result["status"], "declined");
1399 assert_eq!(result["answers"], json!([]));
1400 }
1401
1402 #[test]
1403 fn url_elicitation_requires_discriminator_and_complete_payload() {
1404 let payload = json!({"code":"url_elicitation_required","error":"Waiting","url":"https://consent.example/path","url_host":"consent.example","url_is_punycode":false,"server":"server","tool":"tool","retry_tool":"mcp_server_tool","message":"Connect account","declined":false});
1405 let mut result = ToolResult::error("unrelated");
1406 result.result = Some(payload.clone());
1407 assert_eq!(
1408 serde_json::to_value(UrlElicitationRequired::from_tool_result(&result).unwrap())
1409 .unwrap(),
1410 payload
1411 );
1412 let mut declined = payload.clone();
1413 declined["declined"] = json!(true);
1414 result.result = Some(declined.clone());
1415 assert_eq!(
1416 serde_json::to_value(UrlElicitationRequired::from_tool_result(&result).unwrap())
1417 .unwrap(),
1418 declined
1419 );
1420 for malformed in [
1421 None,
1422 Some(json!(null)),
1423 Some(json!({"code":"url_elicitation_required"})),
1424 Some({
1425 let mut value = payload.clone();
1426 value["code"] = json!("connection_required");
1427 value
1428 }),
1429 Some({
1430 let mut value = payload.clone();
1431 value["declined"] = json!("false");
1432 value
1433 }),
1434 ] {
1435 result.result = malformed;
1436 assert!(UrlElicitationRequired::from_tool_result(&result).is_none());
1437 }
1438 }
1439}