1use serde::{Deserialize, Serialize};
2
3use crate::enums::{ToolTier, ToolVisibility};
4use crate::tool::ToolDefinition;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
8pub struct ToolSummary {
9 pub id: String,
10 #[serde(default)]
11 pub name: String,
12 pub description: String,
13 #[serde(default)]
14 pub domain: String,
15 #[serde(default)]
16 pub tags: Vec<String>,
17 #[serde(default)]
18 pub visibility: ToolVisibility,
19 #[serde(default = "default_tier")]
20 pub tier: ToolTier,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub input_schema: Option<serde_json::Value>,
23}
24
25fn default_tier() -> ToolTier {
26 ToolTier::Warm
27}
28
29impl From<&ToolDefinition> for ToolSummary {
30 fn from(def: &ToolDefinition) -> Self {
31 Self {
32 id: def.id.clone(),
33 name: def.name.clone(),
34 description: def.description.clone(),
35 domain: def.capability.domain.clone(),
36 tags: def.capability.tags.clone(),
37 visibility: def.visibility,
38 tier: def.tier.unwrap_or_else(default_tier),
41 input_schema: Some(def.input_schema.clone()),
42 }
43 }
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49 use crate::enums::{BindingProtocol, SafetyLevel, TrustLevel};
50 use crate::tool::{ToolCapability, ToolResources, ToolSafety, ToolTrust};
51
52 fn def() -> ToolDefinition {
53 ToolDefinition {
54 id: "anos:fs.read".into(),
55 name: "Read File".into(),
56 description: "desc".into(),
57 version: "0.1.0".into(),
58 capability: ToolCapability {
59 domain: "fs".into(),
60 actions: vec!["read".into()],
61 tags: vec!["filesystem".into()],
62 intent_examples: vec![],
63 },
64 input_schema: serde_json::json!({}),
65 output_schema: serde_json::json!({}),
66 bindings: vec![crate::tool::ToolBinding {
67 protocol: BindingProtocol::Cli,
68 config: serde_json::json!({}),
69 }],
70 safety: ToolSafety {
71 level: SafetyLevel::Read,
72 dry_run: false,
73 side_effects: vec![],
74 data_sensitivity: None,
75 },
76 resources: ToolResources {
77 timeout_ms: 1000,
78 max_concurrent: 1,
79 rate_limit_per_min: None,
80 estimated_tokens: None,
81 },
82 trust: ToolTrust {
83 publisher: "anos".into(),
84 trust_level: TrustLevel::L2Tested,
85 signature: None,
86 },
87 visibility: ToolVisibility::Read,
88 required_capabilities: vec![],
89 tier: None,
90 errors: vec![],
91 }
92 }
93
94 #[test]
95 fn summary_is_derivable_from_definition() {
96 let s = ToolSummary::from(&def());
97 assert_eq!(s.id, "anos:fs.read");
98 assert_eq!(s.domain, "fs");
99 assert_eq!(s.tags, vec!["filesystem"]);
100 assert_eq!(s.tier, ToolTier::Warm);
101 assert!(s.input_schema.is_some());
103 }
104
105 #[test]
106 fn input_schema_absent_in_wire_when_none() {
107 let s = ToolSummary {
109 id: "x".into(),
110 name: "x".into(),
111 description: "d".into(),
112 domain: "d".into(),
113 tags: vec![],
114 visibility: ToolVisibility::Read,
115 tier: ToolTier::Warm,
116 input_schema: None,
117 };
118 let j = serde_json::to_string(&s).unwrap();
119 assert!(
120 !j.contains("input_schema"),
121 "field should be absent when None: {j}"
122 );
123 }
124
125 #[test]
126 fn summary_roundtrip_json() {
127 let s = ToolSummary::from(&def());
128 let j = serde_json::to_string(&s).unwrap();
129 let back: ToolSummary = serde_json::from_str(&j).unwrap();
130 assert_eq!(back.id, s.id);
131 }
132
133 #[test]
134 fn missing_tier_defaults_to_warm() {
135 let j = r#"{"id":"a","name":"A","description":"d","domain":"x","tags":[]}"#;
136 let s: ToolSummary = serde_json::from_str(j).unwrap();
137 assert_eq!(s.tier, ToolTier::Warm);
138 assert_eq!(s.visibility, ToolVisibility::Read);
139 }
140
141 #[test]
142 fn summary_parses_anos_shape_with_missing_name_domain_tags() {
143 let j = r#"{"id":"anos:fs.read","description":"File Read","tier":"hot","visibility":"read","lifecycle":"Active"}"#;
144 let s: ToolSummary = serde_json::from_str(j).unwrap();
145 assert_eq!(s.id, "anos:fs.read");
146 assert_eq!(s.description, "File Read");
147 assert_eq!(s.name, ""); assert_eq!(s.domain, ""); assert!(s.tags.is_empty());
150 }
152}