1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct AgenticResponse {
14 pub phase: Phase,
16
17 pub reasoning: String,
19
20 #[serde(default)]
22 pub needs_context: bool,
23
24 #[serde(default)]
26 pub tool_calls: Vec<ToolCall>,
27
28 #[serde(default)]
30 pub queries: Vec<super::schema::QueryCommand>,
31
32 #[serde(default)]
34 pub confidence: f32,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "lowercase")]
40pub enum Phase {
41 Assessment,
43
44 Gathering,
46
47 Final,
49
50 #[serde(skip)]
52 Evaluation,
53
54 #[serde(skip)]
56 Refinement,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61#[serde(tag = "type", rename_all = "snake_case")]
62pub enum ToolCall {
63 GatherContext {
65 #[serde(flatten)]
67 params: ContextGatheringParams,
68 },
69
70 ExploreCodebase {
72 description: String,
74
75 command: String,
77 },
78
79 AnalyzeStructure {
81 analysis_type: AnalysisType,
83 },
84
85 SearchDocumentation {
87 query: String,
89
90 #[serde(default)]
92 files: Option<Vec<String>>,
93 },
94
95 GetStatistics,
97
98 GetDependencies {
100 file_path: String,
102
103 #[serde(default)]
105 reverse: bool,
106 },
107
108 GetAnalysisSummary {
110 #[serde(default = "default_min_dependents")]
112 min_dependents: usize,
113 },
114
115 FindIslands {
117 #[serde(default = "default_min_island_size")]
119 min_size: usize,
120
121 #[serde(default = "default_max_island_size")]
123 max_size: usize,
124 },
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct ContextGatheringParams {
130 #[serde(default)]
132 pub structure: bool,
133
134 #[serde(default)]
136 pub file_types: bool,
137
138 #[serde(default)]
140 pub project_type: bool,
141
142 #[serde(default)]
144 pub framework: bool,
145
146 #[serde(default)]
148 pub entry_points: bool,
149
150 #[serde(default)]
152 pub test_layout: bool,
153
154 #[serde(default)]
156 pub config_files: bool,
157
158 #[serde(default = "default_depth")]
160 pub depth: usize,
161
162 #[serde(default)]
164 pub path: Option<String>,
165}
166
167fn default_depth() -> usize {
168 2
169}
170
171fn default_min_dependents() -> usize {
172 2
173}
174
175fn default_min_island_size() -> usize {
176 2
177}
178
179fn default_max_island_size() -> usize {
180 500
181}
182
183impl Default for ContextGatheringParams {
184 fn default() -> Self {
185 Self {
186 structure: false,
187 file_types: false,
188 project_type: false,
189 framework: false,
190 entry_points: false,
191 test_layout: false,
192 config_files: false,
193 depth: default_depth(),
194 path: None,
195 }
196 }
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
201#[serde(rename_all = "lowercase")]
202pub enum AnalysisType {
203 Hotspots,
205
206 Unused,
208
209 Circular,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct EvaluationReport {
216 pub success: bool,
218
219 pub issues: Vec<EvaluationIssue>,
221
222 pub suggestions: Vec<String>,
224
225 pub score: f32,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct EvaluationIssue {
232 pub issue_type: IssueType,
234
235 pub description: String,
237
238 pub severity: f32,
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
244#[serde(rename_all = "snake_case")]
245pub enum IssueType {
246 EmptyResults,
248
249 TooManyResults,
251
252 WrongFileTypes,
254
255 WrongLocations,
257
258 WrongSymbolType,
260
261 WrongLanguage,
263}
264
265pub const AGENTIC_RESPONSE_SCHEMA: &str = r#"{
267 "type": "object",
268 "properties": {
269 "phase": {
270 "type": "string",
271 "enum": ["assessment", "gathering", "final"],
272 "description": "Current phase: 'assessment' if deciding whether to gather context, 'gathering' if executing tools, 'final' if generating queries"
273 },
274 "reasoning": {
275 "type": "string",
276 "description": "Your thought process and reasoning for this response"
277 },
278 "needs_context": {
279 "type": "boolean",
280 "description": "Whether you need more context before generating final queries (only relevant in assessment phase)"
281 },
282 "tool_calls": {
283 "type": "array",
284 "description": "Array of tools to execute for gathering context (only for assessment/gathering phases)",
285 "items": {
286 "type": "object",
287 "oneOf": [
288 {
289 "properties": {
290 "type": { "const": "gather_context" },
291 "structure": { "type": "boolean" },
292 "file_types": { "type": "boolean" },
293 "project_type": { "type": "boolean" },
294 "framework": { "type": "boolean" },
295 "entry_points": { "type": "boolean" },
296 "test_layout": { "type": "boolean" },
297 "config_files": { "type": "boolean" },
298 "depth": { "type": "integer" },
299 "path": { "type": "string" }
300 },
301 "required": ["type"]
302 },
303 {
304 "properties": {
305 "type": { "const": "explore_codebase" },
306 "description": { "type": "string" },
307 "command": { "type": "string" }
308 },
309 "required": ["type", "description", "command"]
310 },
311 {
312 "properties": {
313 "type": { "const": "analyze_structure" },
314 "analysis_type": { "type": "string", "enum": ["hotspots", "unused", "circular"] }
315 },
316 "required": ["type", "analysis_type"]
317 },
318 {
319 "properties": {
320 "type": { "const": "search_documentation" },
321 "query": { "type": "string" },
322 "files": {
323 "type": "array",
324 "items": { "type": "string" },
325 "description": "Optional: specific files to search (defaults to [\"CLAUDE.md\", \"README.md\"])"
326 }
327 },
328 "required": ["type", "query"]
329 },
330 {
331 "properties": {
332 "type": { "const": "get_statistics" }
333 },
334 "required": ["type"]
335 },
336 {
337 "properties": {
338 "type": { "const": "get_dependencies" },
339 "file_path": { "type": "string" },
340 "reverse": { "type": "boolean" }
341 },
342 "required": ["type", "file_path"]
343 },
344 {
345 "properties": {
346 "type": { "const": "get_analysis_summary" },
347 "min_dependents": { "type": "integer" }
348 },
349 "required": ["type"]
350 },
351 {
352 "properties": {
353 "type": { "const": "find_islands" },
354 "min_size": { "type": "integer" },
355 "max_size": { "type": "integer" }
356 },
357 "required": ["type"]
358 }
359 ]
360 }
361 },
362 "queries": {
363 "type": "array",
364 "description": "Array of rfx commands to execute (only for final phase)",
365 "items": {
366 "type": "object",
367 "properties": {
368 "command": { "type": "string" },
369 "order": { "type": "integer" },
370 "merge": { "type": "boolean" }
371 },
372 "required": ["command", "order", "merge"]
373 }
374 },
375 "confidence": {
376 "type": "number",
377 "minimum": 0.0,
378 "maximum": 1.0,
379 "description": "Confidence score (0.0-1.0) in your generated queries"
380 }
381 },
382 "required": ["phase", "reasoning"]
383}"#;
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
390 fn test_deserialize_assessment_phase() {
391 let json = r#"{
392 "phase": "assessment",
393 "reasoning": "I need to understand the project structure",
394 "needs_context": true,
395 "tool_calls": [{
396 "type": "gather_context",
397 "structure": true,
398 "file_types": true
399 }],
400 "confidence": 0.0
401 }"#;
402
403 let response: AgenticResponse = serde_json::from_str(json).unwrap();
404 assert_eq!(response.phase, Phase::Assessment);
405 assert!(response.needs_context);
406 assert_eq!(response.tool_calls.len(), 1);
407 }
408
409 #[test]
410 fn test_deserialize_final_phase() {
411 let json = r#"{
412 "phase": "final",
413 "reasoning": "Based on the context, I can generate queries",
414 "needs_context": false,
415 "queries": [{
416 "command": "query \"TODO\"",
417 "order": 1,
418 "merge": true
419 }],
420 "confidence": 0.85
421 }"#;
422
423 let response: AgenticResponse = serde_json::from_str(json).unwrap();
424 assert_eq!(response.phase, Phase::Final);
425 assert!(!response.needs_context);
426 assert_eq!(response.queries.len(), 1);
427 assert_eq!(response.confidence, 0.85);
428 }
429
430 #[test]
431 fn test_deserialize_explore_tool() {
432 let json = r#"{
433 "type": "explore_codebase",
434 "description": "Find validation functions",
435 "command": "query \"validate\" --symbols --kind function"
436 }"#;
437
438 let tool: ToolCall = serde_json::from_str(json).unwrap();
439 match tool {
440 ToolCall::ExploreCodebase { description, command } => {
441 assert_eq!(description, "Find validation functions");
442 assert!(command.contains("validate"));
443 }
444 _ => panic!("Expected ExploreCodebase variant"),
445 }
446 }
447
448 #[test]
449 fn test_deserialize_analyze_tool() {
450 let json = r#"{
451 "type": "analyze_structure",
452 "analysis_type": "hotspots"
453 }"#;
454
455 let tool: ToolCall = serde_json::from_str(json).unwrap();
456 match tool {
457 ToolCall::AnalyzeStructure { analysis_type } => {
458 assert_eq!(analysis_type, AnalysisType::Hotspots);
459 }
460 _ => panic!("Expected AnalyzeStructure variant"),
461 }
462 }
463
464 #[test]
465 fn test_evaluation_report() {
466 let report = EvaluationReport {
467 success: false,
468 issues: vec![EvaluationIssue {
469 issue_type: IssueType::EmptyResults,
470 description: "No results found".to_string(),
471 severity: 0.9,
472 }],
473 suggestions: vec!["Try broader search pattern".to_string()],
474 score: 0.1,
475 };
476
477 assert!(!report.success);
478 assert_eq!(report.issues.len(), 1);
479 assert!(report.score < 0.5);
480 }
481}