use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgenticResponse {
pub phase: Phase,
pub reasoning: String,
#[serde(default)]
pub needs_context: bool,
#[serde(default)]
pub tool_calls: Vec<ToolCall>,
#[serde(default)]
pub queries: Vec<super::schema::QueryCommand>,
#[serde(default)]
pub confidence: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Phase {
Assessment,
Gathering,
Final,
#[serde(skip)]
Evaluation,
#[serde(skip)]
Refinement,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ToolCall {
GatherContext {
#[serde(flatten)]
params: ContextGatheringParams,
},
ExploreCodebase {
description: String,
command: String,
},
AnalyzeStructure {
analysis_type: AnalysisType,
},
SearchDocumentation {
query: String,
#[serde(default)]
files: Option<Vec<String>>,
},
GetStatistics,
GetDependencies {
file_path: String,
#[serde(default)]
reverse: bool,
},
GetAnalysisSummary {
#[serde(default = "default_min_dependents")]
min_dependents: usize,
},
FindIslands {
#[serde(default = "default_min_island_size")]
min_size: usize,
#[serde(default = "default_max_island_size")]
max_size: usize,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextGatheringParams {
#[serde(default)]
pub structure: bool,
#[serde(default)]
pub file_types: bool,
#[serde(default)]
pub project_type: bool,
#[serde(default)]
pub framework: bool,
#[serde(default)]
pub entry_points: bool,
#[serde(default)]
pub test_layout: bool,
#[serde(default)]
pub config_files: bool,
#[serde(default = "default_depth")]
pub depth: usize,
#[serde(default)]
pub path: Option<String>,
}
fn default_depth() -> usize {
2
}
fn default_min_dependents() -> usize {
2
}
fn default_min_island_size() -> usize {
2
}
fn default_max_island_size() -> usize {
500
}
impl Default for ContextGatheringParams {
fn default() -> Self {
Self {
structure: false,
file_types: false,
project_type: false,
framework: false,
entry_points: false,
test_layout: false,
config_files: false,
depth: default_depth(),
path: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AnalysisType {
Hotspots,
Unused,
Circular,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvaluationReport {
pub success: bool,
pub issues: Vec<EvaluationIssue>,
pub suggestions: Vec<String>,
pub score: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvaluationIssue {
pub issue_type: IssueType,
pub description: String,
pub severity: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IssueType {
EmptyResults,
TooManyResults,
WrongFileTypes,
WrongLocations,
WrongSymbolType,
WrongLanguage,
}
pub const AGENTIC_RESPONSE_SCHEMA: &str = r#"{
"type": "object",
"properties": {
"phase": {
"type": "string",
"enum": ["assessment", "gathering", "final"],
"description": "Current phase: 'assessment' if deciding whether to gather context, 'gathering' if executing tools, 'final' if generating queries"
},
"reasoning": {
"type": "string",
"description": "Your thought process and reasoning for this response"
},
"needs_context": {
"type": "boolean",
"description": "Whether you need more context before generating final queries (only relevant in assessment phase)"
},
"tool_calls": {
"type": "array",
"description": "Array of tools to execute for gathering context (only for assessment/gathering phases)",
"items": {
"type": "object",
"oneOf": [
{
"properties": {
"type": { "const": "gather_context" },
"structure": { "type": "boolean" },
"file_types": { "type": "boolean" },
"project_type": { "type": "boolean" },
"framework": { "type": "boolean" },
"entry_points": { "type": "boolean" },
"test_layout": { "type": "boolean" },
"config_files": { "type": "boolean" },
"depth": { "type": "integer" },
"path": { "type": "string" }
},
"required": ["type"]
},
{
"properties": {
"type": { "const": "explore_codebase" },
"description": { "type": "string" },
"command": { "type": "string" }
},
"required": ["type", "description", "command"]
},
{
"properties": {
"type": { "const": "analyze_structure" },
"analysis_type": { "type": "string", "enum": ["hotspots", "unused", "circular"] }
},
"required": ["type", "analysis_type"]
},
{
"properties": {
"type": { "const": "search_documentation" },
"query": { "type": "string" },
"files": {
"type": "array",
"items": { "type": "string" },
"description": "Optional: specific files to search (defaults to [\"CLAUDE.md\", \"README.md\"])"
}
},
"required": ["type", "query"]
},
{
"properties": {
"type": { "const": "get_statistics" }
},
"required": ["type"]
},
{
"properties": {
"type": { "const": "get_dependencies" },
"file_path": { "type": "string" },
"reverse": { "type": "boolean" }
},
"required": ["type", "file_path"]
},
{
"properties": {
"type": { "const": "get_analysis_summary" },
"min_dependents": { "type": "integer" }
},
"required": ["type"]
},
{
"properties": {
"type": { "const": "find_islands" },
"min_size": { "type": "integer" },
"max_size": { "type": "integer" }
},
"required": ["type"]
}
]
}
},
"queries": {
"type": "array",
"description": "Array of rfx commands to execute (only for final phase)",
"items": {
"type": "object",
"properties": {
"command": { "type": "string" },
"order": { "type": "integer" },
"merge": { "type": "boolean" }
},
"required": ["command", "order", "merge"]
}
},
"confidence": {
"type": "number",
"minimum": 0.0,
"maximum": 1.0,
"description": "Confidence score (0.0-1.0) in your generated queries"
}
},
"required": ["phase", "reasoning"]
}"#;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deserialize_assessment_phase() {
let json = r#"{
"phase": "assessment",
"reasoning": "I need to understand the project structure",
"needs_context": true,
"tool_calls": [{
"type": "gather_context",
"structure": true,
"file_types": true
}],
"confidence": 0.0
}"#;
let response: AgenticResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.phase, Phase::Assessment);
assert!(response.needs_context);
assert_eq!(response.tool_calls.len(), 1);
}
#[test]
fn test_deserialize_final_phase() {
let json = r#"{
"phase": "final",
"reasoning": "Based on the context, I can generate queries",
"needs_context": false,
"queries": [{
"command": "query \"TODO\"",
"order": 1,
"merge": true
}],
"confidence": 0.85
}"#;
let response: AgenticResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.phase, Phase::Final);
assert!(!response.needs_context);
assert_eq!(response.queries.len(), 1);
assert_eq!(response.confidence, 0.85);
}
#[test]
fn test_deserialize_explore_tool() {
let json = r#"{
"type": "explore_codebase",
"description": "Find validation functions",
"command": "query \"validate\" --symbols --kind function"
}"#;
let tool: ToolCall = serde_json::from_str(json).unwrap();
match tool {
ToolCall::ExploreCodebase { description, command } => {
assert_eq!(description, "Find validation functions");
assert!(command.contains("validate"));
}
_ => panic!("Expected ExploreCodebase variant"),
}
}
#[test]
fn test_deserialize_analyze_tool() {
let json = r#"{
"type": "analyze_structure",
"analysis_type": "hotspots"
}"#;
let tool: ToolCall = serde_json::from_str(json).unwrap();
match tool {
ToolCall::AnalyzeStructure { analysis_type } => {
assert_eq!(analysis_type, AnalysisType::Hotspots);
}
_ => panic!("Expected AnalyzeStructure variant"),
}
}
#[test]
fn test_evaluation_report() {
let report = EvaluationReport {
success: false,
issues: vec![EvaluationIssue {
issue_type: IssueType::EmptyResults,
description: "No results found".to_string(),
severity: 0.9,
}],
suggestions: vec!["Try broader search pattern".to_string()],
score: 0.1,
};
assert!(!report.success);
assert_eq!(report.issues.len(), 1);
assert!(report.score < 0.5);
}
}