syncable_cli/agent/tools/
retrieve.rs

1//! Retrieve Output Tool - RAG retrieval for compressed tool outputs
2//!
3//! Allows the agent to retrieve full details from previously compressed outputs.
4//! This is the retrieval part of the RAG pattern.
5
6use rig::completion::ToolDefinition;
7use rig::tool::Tool;
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10
11use super::output_store;
12
13/// Arguments for the retrieve_output tool
14#[derive(Debug, Deserialize)]
15pub struct RetrieveOutputArgs {
16    /// Reference ID from a compressed tool output (e.g., "kubelint_abc123")
17    pub ref_id: String,
18    /// Optional query to filter results
19    /// Examples: "severity:critical", "file:deployment.yaml", "code:DL3008", "container:nginx"
20    pub query: Option<String>,
21}
22
23/// Error type for retrieve tool
24#[derive(Debug, thiserror::Error)]
25#[error("Retrieve error: {0}")]
26pub struct RetrieveError(String);
27
28/// Tool to retrieve detailed data from compressed tool outputs
29#[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        // Try to retrieve filtered data
97        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                // Check if result is too large and warn
105                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                // Check if the ref_id exists at all
117                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/// Tool to list all available stored outputs
138#[derive(Debug, Clone, Default, Serialize, Deserialize)]
139pub struct ListOutputsTool;
140
141impl ListOutputsTool {
142    pub fn new() -> Self {
143        Self
144    }
145}
146
147/// Arguments for list_outputs tool (none required)
148#[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}