syncable_cli/agent/tools/
retrieve.rs1use rig::completion::ToolDefinition;
7use rig::tool::Tool;
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10
11use super::output_store;
12
13#[derive(Debug, Deserialize)]
15pub struct RetrieveOutputArgs {
16 pub ref_id: String,
18 pub query: Option<String>,
21}
22
23#[derive(Debug, thiserror::Error)]
25#[error("Retrieve error: {0}")]
26pub struct RetrieveError(String);
27
28#[derive(Debug, Clone, Default, Serialize, Deserialize)]
30pub struct RetrieveOutputTool;
31
32impl RetrieveOutputTool {
33 pub fn new() -> Self {
34 Self
35 }
36}
37
38impl Tool for RetrieveOutputTool {
39 const NAME: &'static str = "retrieve_output";
40
41 type Error = RetrieveError;
42 type Args = RetrieveOutputArgs;
43 type Output = String;
44
45 async fn definition(&self, _prompt: String) -> ToolDefinition {
46 ToolDefinition {
47 name: Self::NAME.to_string(),
48 description: r#"Retrieve detailed data from a previous tool output that was compressed.
49
50Use this tool when:
51- You received a compressed summary with a 'full_data_ref' field
52- You need full details about specific issues mentioned in a summary
53- You want to filter issues by severity, file, code, or container
54
55The ref_id comes from the 'full_data_ref' field in compressed outputs from tools like kubelint, k8s_optimize, or analyze_project.
56
57## Query examples for lint tools (kubelint, hadolint, etc.):
58- "severity:critical" - Get all critical issues
59- "severity:high" - Get all high severity issues
60- "file:deployment.yaml" - Get issues in a specific file
61- "code:DL3008" - Get all issues with a specific code
62- "container:nginx" - Get issues for a specific container
63
64## Query examples for analyze_project outputs:
65IMPORTANT: For analyze_project outputs, ALWAYS use a query to avoid context overflow!
66- "section:summary" - Get project summary (recommended first query)
67- "section:projects" - List all projects with basic info
68- "section:frameworks" - List all detected frameworks
69- "section:languages" - List all detected languages
70- "section:services" - List all detected services
71- "project:name" - Get details for a specific project (e.g., "project:api-gateway")
72- "service:name" - Get details for a specific service
73- "language:Go" - Get language detection details for Go
74- "framework:React" - Get framework details
75- "compact:true" - Get compacted output (file arrays → counts)
76
77Without a query, analyze_project returns compacted output (file arrays replaced with counts)."#.to_string(),
78 parameters: json!({
79 "type": "object",
80 "properties": {
81 "ref_id": {
82 "type": "string",
83 "description": "Reference ID from the compressed output's 'full_data_ref' field (e.g., 'kubelint_abc123', 'analyze_project_xyz')"
84 },
85 "query": {
86 "type": "string",
87 "description": "Filter query. For lint tools: 'severity:critical', 'file:path', 'code:DL3008'. For analyze_project: 'section:summary', 'section:projects', 'project:name', 'language:Go', 'framework:*'. IMPORTANT: For analyze_project, always use a query to prevent context overflow."
88 }
89 },
90 "required": ["ref_id"]
91 }),
92 }
93 }
94
95 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
96 let result = output_store::retrieve_filtered(&args.ref_id, args.query.as_deref());
98
99 match result {
100 Some(data) => {
101 let json_str = serde_json::to_string_pretty(&data)
102 .map_err(|e| RetrieveError(format!("Failed to serialize: {}", e)))?;
103
104 if json_str.len() > 50_000 {
106 Ok(format!(
107 "{}\n\n[NOTE: Large result ({} bytes). Consider using a more specific query to filter results.]",
108 json_str,
109 json_str.len()
110 ))
111 } else {
112 Ok(json_str)
113 }
114 }
115 None => {
116 let outputs = output_store::list_outputs();
118 let available: Vec<&str> =
119 outputs.iter().map(|o| o.ref_id.as_str()).take(5).collect();
120
121 if available.is_empty() {
122 Err(RetrieveError(format!(
123 "Output '{}' not found. No stored outputs available. Outputs are stored temporarily and may have expired.",
124 args.ref_id
125 )))
126 } else {
127 Err(RetrieveError(format!(
128 "Output '{}' not found. Available outputs: {:?}. Note: Outputs expire after 1 hour.",
129 args.ref_id, available
130 )))
131 }
132 }
133 }
134 }
135}
136
137#[derive(Debug, Clone, Default, Serialize, Deserialize)]
139pub struct ListOutputsTool;
140
141impl ListOutputsTool {
142 pub fn new() -> Self {
143 Self
144 }
145}
146
147#[derive(Debug, Deserialize)]
149pub struct ListOutputsArgs {}
150
151impl Tool for ListOutputsTool {
152 const NAME: &'static str = "list_stored_outputs";
153
154 type Error = RetrieveError;
155 type Args = ListOutputsArgs;
156 type Output = String;
157
158 async fn definition(&self, _prompt: String) -> ToolDefinition {
159 ToolDefinition {
160 name: Self::NAME.to_string(),
161 description: "List all stored tool outputs that can be retrieved. Shows ref_id, tool name, timestamp, and size for each stored output.".to_string(),
162 parameters: json!({
163 "type": "object",
164 "properties": {}
165 }),
166 }
167 }
168
169 async fn call(&self, _args: Self::Args) -> Result<Self::Output, Self::Error> {
170 let outputs = output_store::list_outputs();
171
172 if outputs.is_empty() {
173 return Ok("No stored outputs available. Outputs are created when tools like kubelint, k8s_optimize, or analyze_project produce large results.".to_string());
174 }
175
176 let mut result = String::from("Available stored outputs:\n\n");
177
178 for output in &outputs {
179 let age_secs = std::time::SystemTime::now()
180 .duration_since(std::time::UNIX_EPOCH)
181 .map(|d| d.as_secs())
182 .unwrap_or(0)
183 .saturating_sub(output.timestamp);
184
185 let age_str = if age_secs < 60 {
186 format!("{}s ago", age_secs)
187 } else if age_secs < 3600 {
188 format!("{}m ago", age_secs / 60)
189 } else {
190 format!("{}h ago", age_secs / 3600)
191 };
192
193 let size_str = if output.size_bytes < 1024 {
194 format!("{} B", output.size_bytes)
195 } else {
196 format!("{:.1} KB", output.size_bytes as f64 / 1024.0)
197 };
198
199 result.push_str(&format!(
200 "- {} (tool: {}, {}, {})\n",
201 output.ref_id, output.tool, size_str, age_str
202 ));
203 }
204
205 result.push_str(&format!("\nTotal: {} outputs\n", outputs.len()));
206 result.push_str("\nUse retrieve_output(ref_id, query) to get details.");
207
208 Ok(result)
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[tokio::test]
217 async fn test_retrieve_nonexistent() {
218 let tool = RetrieveOutputTool::new();
219 let args = RetrieveOutputArgs {
220 ref_id: "nonexistent_12345".to_string(),
221 query: None,
222 };
223
224 let result = tool.call(args).await;
225 assert!(result.is_err());
226 }
227
228 #[tokio::test]
229 async fn test_list_outputs() {
230 let tool = ListOutputsTool::new();
231 let args = ListOutputsArgs {};
232
233 let result = tool.call(args).await;
234 assert!(result.is_ok());
235 }
236}