1use crate::error::AgentError;
7use crate::types::*;
8use std::collections::HashMap;
9use std::sync::{Mutex, OnceLock};
10
11pub const CONFIG_TOOL_NAME: &str = "Config";
12
13static CONFIG: OnceLock<Mutex<HashMap<String, serde_json::Value>>> = OnceLock::new();
15
16fn get_config_map() -> &'static Mutex<HashMap<String, serde_json::Value>> {
17 CONFIG.get_or_init(|| Mutex::new(HashMap::new()))
18}
19
20fn get_nested_config(
22 config: &HashMap<String, serde_json::Value>,
23 key: &str,
24) -> Option<serde_json::Value> {
25 if key.contains('.') {
27 let parts: Vec<&str> = key.split('.').collect();
28 let mut current: Option<&serde_json::Value> = None;
29 for part in &parts {
30 if current.is_none() {
31 current = config.get(*part);
32 } else {
33 current = current
34 .and_then(|v| v.as_object())
35 .and_then(|obj| obj.get(*part));
36 }
37 }
38 current.cloned()
39 } else {
40 config.get(key).cloned()
41 }
42}
43
44pub struct ConfigTool;
46
47impl ConfigTool {
48 pub fn new() -> Self {
49 Self
50 }
51
52 pub fn name(&self) -> &str {
53 CONFIG_TOOL_NAME
54 }
55
56 pub fn description(&self) -> &str {
57 "Read or update dynamic configuration settings. Use 'get' to read a setting, 'set' to update a setting, or 'list' to see all settings."
58 }
59
60 pub fn user_facing_name(&self, _input: Option<&serde_json::Value>) -> String {
61 "Config".to_string()
62 }
63
64 pub fn get_tool_use_summary(&self, input: Option<&serde_json::Value>) -> Option<String> {
65 input.and_then(|inp| inp["action"].as_str().map(String::from))
66 }
67
68 pub fn render_tool_result_message(
69 &self,
70 content: &serde_json::Value,
71 ) -> Option<String> {
72 content["content"].as_str().map(|s| s.to_string())
73 }
74
75 pub fn input_schema(&self) -> ToolInputSchema {
76 ToolInputSchema {
77 schema_type: "object".to_string(),
78 properties: serde_json::json!({
79 "action": {
80 "type": "string",
81 "enum": ["get", "set", "list"],
82 "description": "Action to perform: get (read a setting), set (update a setting), or list (show all settings)"
83 },
84 "key": {
85 "type": "string",
86 "description": "Configuration key (for get/set actions). Supports dot notation for nested keys (e.g., 'settings.theme')"
87 },
88 "value": {
89 "type": "string",
90 "description": "Configuration value (for set action). Will be parsed as JSON if possible, otherwise treated as string"
91 }
92 }),
93 required: Some(vec!["action".to_string()]),
94 }
95 }
96
97 pub async fn execute(
98 &self,
99 input: serde_json::Value,
100 _context: &ToolContext,
101 ) -> Result<ToolResult, AgentError> {
102 let action = input["action"].as_str().unwrap_or("list");
103 let key = input["key"].as_str().unwrap_or("");
104 let value_str = input["value"].as_str().unwrap_or("");
105
106 match action {
107 "get" => {
108 if key.is_empty() {
109 return Ok(ToolResult {
110 result_type: "text".to_string(),
111 tool_use_id: "".to_string(),
112 content: "Error: key is required for 'get' action".to_string(),
113 is_error: Some(true),
114 was_persisted: None,
115 });
116 }
117 let mut guard = get_config_map().lock().unwrap();
118 if let Some(val) = get_nested_config(&guard, key) {
119 Ok(ToolResult {
120 result_type: "text".to_string(),
121 tool_use_id: "".to_string(),
122 content: format!("Config '{}': {}", key, val),
123 is_error: Some(false),
124 was_persisted: None,
125 })
126 } else {
127 Ok(ToolResult {
128 result_type: "text".to_string(),
129 tool_use_id: "".to_string(),
130 content: format!("Config '{}' is not set", key),
131 is_error: None,
132 was_persisted: None,
133 })
134 }
135 }
136 "set" => {
137 if key.is_empty() {
138 return Ok(ToolResult {
139 result_type: "text".to_string(),
140 tool_use_id: "".to_string(),
141 content: "Error: key is required for 'set' action".to_string(),
142 is_error: Some(true),
143 was_persisted: None,
144 });
145 }
146 if value_str.is_empty() {
147 return Ok(ToolResult {
148 result_type: "text".to_string(),
149 tool_use_id: "".to_string(),
150 content: "Error: value is required for 'set' action".to_string(),
151 is_error: Some(true),
152 was_persisted: None,
153 });
154 }
155 let value: serde_json::Value =
157 serde_json::from_str(value_str).unwrap_or(serde_json::json!(value_str));
158
159 let mut guard = get_config_map().lock().unwrap();
160 guard.insert(key.to_string(), value);
161 drop(guard);
162
163 Ok(ToolResult {
164 result_type: "text".to_string(),
165 tool_use_id: "".to_string(),
166 content: format!("Config '{}' has been updated", key),
167 is_error: Some(false),
168 was_persisted: None,
169 })
170 }
171 "list" => {
172 let mut guard = get_config_map().lock().unwrap();
173 if guard.is_empty() {
174 Ok(ToolResult {
175 result_type: "text".to_string(),
176 tool_use_id: "".to_string(),
177 content: "No configuration settings set.".to_string(),
178 is_error: None,
179 was_persisted: None,
180 })
181 } else {
182 let items: Vec<String> = guard
183 .iter()
184 .map(|(k, v)| format!(" {}: {}", k, v))
185 .collect();
186 Ok(ToolResult {
187 result_type: "text".to_string(),
188 tool_use_id: "".to_string(),
189 content: format!("Configuration settings:\n{}", items.join("\n")),
190 is_error: Some(false),
191 was_persisted: None,
192 })
193 }
194 }
195 _ => Ok(ToolResult {
196 result_type: "text".to_string(),
197 tool_use_id: "".to_string(),
198 content: format!(
199 "Invalid action: '{}'. Must be 'get', 'set', or 'list'.",
200 action
201 ),
202 is_error: Some(true),
203 was_persisted: None,
204 }),
205 }
206 }
207}
208
209impl Default for ConfigTool {
210 fn default() -> Self {
211 Self::new()
212 }
213}
214
215pub fn reset_config_for_testing() {
217 let mut guard = get_config_map().lock().unwrap();
218 guard.clear();
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 use crate::tests::common::clear_all_test_state;
226
227 #[test]
228 fn test_config_tool_name() {
229 clear_all_test_state();
230 let tool = ConfigTool::new();
231 assert_eq!(tool.name(), CONFIG_TOOL_NAME);
232 }
233
234 #[test]
235 fn test_config_tool_schema() {
236 clear_all_test_state();
237 let tool = ConfigTool::new();
238 let schema = tool.input_schema();
239 assert_eq!(schema.schema_type, "object");
240 assert!(schema.properties.get("action").is_some());
241 }
242
243 #[tokio::test]
244 async fn test_config_tool_list_empty() {
245 clear_all_test_state();
246 let tool = ConfigTool::new();
247 let input = serde_json::json!({ "action": "list" });
248 let context = ToolContext::default();
249 let result = tool.execute(input, &context).await;
250 assert!(result.is_ok());
251 }
252
253 #[tokio::test]
254 async fn test_config_tool_set_and_get() {
255 clear_all_test_state();
256 let tool = ConfigTool::new();
257 let context = ToolContext::default();
258
259 let set_result = tool
261 .execute(
262 serde_json::json!({ "action": "set", "key": "test_key", "value": "\"hello\"" }),
263 &context,
264 )
265 .await;
266 assert!(set_result.is_ok());
267
268 let get_result = tool
270 .execute(
271 serde_json::json!({ "action": "get", "key": "test_key" }),
272 &context,
273 )
274 .await;
275 assert!(get_result.is_ok());
276 assert!(get_result.unwrap().content.contains("hello"));
277 }
278}