use crate::tool_defs::{ToolProfile, ToolSurface};
pub(crate) const PLAN_PHASE_TOOLS: &[&str] = &[
"explore_codebase",
"review_architecture",
"review_changes",
"analyze_change_request",
"verify_change_readiness",
"find_minimal_context_for_change",
"onboard_project",
"get_ranked_context",
"get_symbols_overview",
"find_symbol",
"get_impact_analysis",
"impact_report",
"module_boundary_report",
"summarize_symbol_impact",
"get_changed_files",
"find_referencing_symbols",
"get_type_hierarchy",
];
pub(crate) const BUILD_PHASE_TOOLS: &[&str] = &[
"explore_codebase",
"trace_request_path",
"plan_safe_refactor",
"find_symbol",
"get_symbols_overview",
"get_ranked_context",
"find_referencing_symbols",
"get_file_diagnostics",
"replace_symbol_body",
"insert_content",
"replace",
"rename_symbol",
"create_text_file",
"add_import",
"analyze_missing_imports",
"find_tests",
"refresh_symbol_index",
"verify_change_readiness",
];
pub(crate) const REVIEW_PHASE_TOOLS: &[&str] = &[
"review_architecture",
"cleanup_duplicate_logic",
"review_changes",
"diagnose_issues",
"verify_change_readiness",
"get_file_diagnostics",
"get_impact_analysis",
"find_scoped_references",
"impact_report",
"refactor_safety_report",
"diff_aware_references",
"semantic_code_review",
"dead_code_report",
"find_dead_code",
"find_circular_dependencies",
"get_changed_files",
"find_tests",
"unresolved_reference_check",
"export_session_markdown",
];
pub(crate) const EVAL_PHASE_TOOLS: &[&str] = &[
"review_changes",
"diagnose_issues",
"verify_change_readiness",
"get_file_diagnostics",
"get_changed_files",
"find_tests",
"get_symbols_overview",
"find_symbol",
"read_file",
"get_analysis_section",
];
const MUTATION_TOOLS: &[&str] = &[
"rename_symbol",
"replace_symbol_body",
"replace_content",
"replace_lines",
"delete_lines",
"insert_at_line",
"insert_before_symbol",
"insert_after_symbol",
"insert_content",
"replace",
"create_text_file",
"add_import",
"refactor_extract_function",
"refactor_inline_function",
"refactor_move_to_file",
"refactor_change_signature",
];
const REVIEW_TOOLS: &[&str] = &[
"review_architecture",
"review_changes",
"diagnose_issues",
"cleanup_duplicate_logic",
"get_changed_files",
"get_impact_analysis",
"find_scoped_references",
];
const EXPLORATION_TOOLS: &[&str] = &[
"explore_codebase",
"trace_request_path",
"get_symbols_overview",
"get_project_structure",
"onboard_project",
"get_current_config",
];
pub(crate) fn infer_harness_phase(recent_tools: &[String]) -> Option<&'static str> {
const BUILD_SIGNAL: &[&str] = &[
"rename_symbol",
"replace_symbol_body",
"replace",
"replace_content",
"insert_content",
"insert_at_line",
"delete_lines",
"add_import",
"create_text_file",
"refactor_extract_function",
"refactor_inline_function",
"refactor_move_to_file",
"refactor_change_signature",
"plan_safe_refactor",
"trace_request_path",
];
const REVIEW_SIGNAL: &[&str] = &[
"diff_aware_references",
"semantic_code_review",
"refactor_safety_report",
"dead_code_report",
"find_circular_dependencies",
"unresolved_reference_check",
"find_misplaced_code",
"find_code_duplicates",
"review_changes",
"review_architecture",
];
const PLAN_SIGNAL: &[&str] = &[
"analyze_change_request",
"find_minimal_context_for_change",
"onboard_project",
"explore_codebase",
"analyze_change_impact",
];
for tool in recent_tools.iter().rev().take(5) {
let t = tool.as_str();
if BUILD_SIGNAL.contains(&t) {
return Some("build");
}
if REVIEW_SIGNAL.contains(&t) {
return Some("review");
}
if PLAN_SIGNAL.contains(&t) {
return Some("plan");
}
}
None
}
pub fn suggest_next_contextual(
tool_name: &str,
recent_tools: &[String],
harness_phase: Option<&str>,
) -> Option<Vec<String>> {
let mut suggestions = suggest_next(tool_name)?;
let recent_has_mutation = recent_tools
.iter()
.any(|t| MUTATION_TOOLS.contains(&t.as_str()));
if recent_has_mutation || MUTATION_TOOLS.contains(&tool_name) {
suggestions.retain(|s| s != "get_file_diagnostics");
suggestions.insert(0, "get_file_diagnostics".to_owned());
suggestions.truncate(3);
}
if !MUTATION_TOOLS.contains(&tool_name)
&& !suggestions.contains(&"verify_change_readiness".to_owned())
{
let is_pre_mutation_context = REVIEW_TOOLS.contains(&tool_name)
|| EXPLORATION_TOOLS.contains(&tool_name)
|| tool_name == "get_ranked_context"
|| tool_name == "find_symbol"
|| tool_name == "find_referencing_symbols";
if is_pre_mutation_context {
suggestions.push("verify_change_readiness".to_owned());
suggestions.truncate(4);
}
}
let recent_has_review = recent_tools
.iter()
.any(|t| REVIEW_TOOLS.contains(&t.as_str()));
if recent_has_review
&& !MUTATION_TOOLS.contains(&tool_name)
&& !suggestions.contains(&"get_impact_analysis".to_owned())
{
suggestions.push("get_impact_analysis".to_owned());
suggestions.truncate(3);
}
let recent_has_exploration = recent_tools
.iter()
.any(|t| EXPLORATION_TOOLS.contains(&t.as_str()));
if recent_has_exploration
&& !MUTATION_TOOLS.contains(&tool_name)
&& !REVIEW_TOOLS.contains(&tool_name)
&& !suggestions.contains(&"get_ranked_context".to_owned())
{
suggestions.push("get_ranked_context".to_owned());
suggestions.truncate(3);
}
if let Some(phase) = harness_phase {
let phase_tools: &[&str] = match phase {
"plan" => PLAN_PHASE_TOOLS,
"build" => BUILD_PHASE_TOOLS,
"review" => REVIEW_PHASE_TOOLS,
"eval" => EVAL_PHASE_TOOLS,
_ => return Some(suggestions), };
suggestions.retain(|s| phase_tools.contains(&s.as_str()));
if suggestions.is_empty() {
suggestions = suggest_next(tool_name).unwrap_or_default();
}
}
Some(suggestions)
}
fn is_workflow_tool_name(name: &str) -> bool {
matches!(
name,
"explore_codebase"
| "trace_request_path"
| "review_architecture"
| "plan_safe_refactor"
| "audit_security_context"
| "analyze_change_impact"
| "cleanup_duplicate_logic"
| "review_changes"
| "assess_change_readiness"
| "diagnose_issues"
| "analyze_change_request"
| "verify_change_readiness"
| "find_minimal_context_for_change"
| "summarize_symbol_impact"
| "module_boundary_report"
| "safe_rename_report"
| "unresolved_reference_check"
| "dead_code_report"
| "impact_report"
| "refactor_safety_report"
| "diff_aware_references"
| "start_analysis_job"
| "get_analysis_job"
| "cancel_analysis_job"
| "get_analysis_section"
)
}
fn has_recent_low_level_chain(recent_tools: &[String]) -> bool {
if recent_tools.len() < 3 {
return false;
}
recent_tools[recent_tools.len() - 3..]
.iter()
.all(|tool| !is_workflow_tool_name(tool))
}
fn composite_suggestions_for_surface(surface: ToolSurface) -> &'static [&'static str] {
match surface {
ToolSurface::Profile(ToolProfile::PlannerReadonly) => &[
"explore_codebase",
"review_architecture",
"review_changes",
"plan_safe_refactor",
],
ToolSurface::Profile(ToolProfile::ReviewerGraph)
| ToolSurface::Profile(ToolProfile::CiAudit) => &[
"review_architecture",
"review_changes",
"cleanup_duplicate_logic",
"diagnose_issues",
],
ToolSurface::Profile(ToolProfile::RefactorFull) => &[
"plan_safe_refactor",
"review_changes",
"trace_request_path",
"review_architecture",
],
ToolSurface::Profile(ToolProfile::EvaluatorCompact) => &[
"verify_change_readiness",
"get_file_diagnostics",
"find_tests",
],
ToolSurface::Profile(ToolProfile::WorkflowFirst) => &[
"explore_codebase",
"review_architecture",
"plan_safe_refactor",
"review_changes",
"diagnose_issues",
],
ToolSurface::Profile(ToolProfile::BuilderMinimal) | ToolSurface::Preset(_) => &[
"explore_codebase",
"trace_request_path",
"plan_safe_refactor",
"review_changes",
],
}
}
pub fn composite_guidance_for_chain(
tool_name: &str,
recent_tools: &[String],
surface: ToolSurface,
) -> Option<(Vec<String>, String)> {
if is_workflow_tool_name(tool_name) || !has_recent_low_level_chain(recent_tools) {
return None;
}
let suggestions = composite_suggestions_for_surface(surface)
.iter()
.copied()
.filter(|candidate| *candidate != tool_name)
.take(3)
.map(ToOwned::to_owned)
.collect::<Vec<_>>();
if suggestions.is_empty() {
return None;
}
let hint = format!(
"Repeated low-level chain detected. Prefer {} for compressed context before continuing.",
suggestions.join(", ")
);
Some((suggestions, hint))
}
pub fn suggest_next(tool_name: &str) -> Option<Vec<String>> {
let suggestions: &[&str] = match tool_name {
"get_symbols_overview" => &["find_symbol", "get_impact_analysis", "get_ranked_context"],
"find_symbol" => &[
"find_referencing_symbols",
"get_impact_analysis",
"replace_symbol_body",
],
"get_ranked_context" => &["find_symbol", "replace_symbol_body", "semantic_search"],
"refresh_symbol_index" => &["index_embeddings", "get_symbols_overview"],
"get_project_structure" => &["get_symbols_overview", "get_ranked_context", "find_symbol"],
"get_complexity" => &["find_symbol", "get_symbols_overview"],
"search_symbols_fuzzy" => &["find_symbol", "get_ranked_context"],
"find_referencing_symbols" => &["get_impact_analysis", "rename_symbol"],
"get_file_diagnostics" => &["find_symbol", "get_symbols_overview"],
"search_workspace_symbols" => &["find_symbol", "get_symbols_overview"],
"get_type_hierarchy" => &["find_referencing_symbols", "get_symbols_overview"],
"plan_symbol_rename" => &["rename_symbol"],
"check_lsp_status" => &["get_capabilities", "get_file_diagnostics"],
"get_lsp_recipe" => &["check_lsp_status"],
"get_changed_files" => &["get_impact_analysis", "get_symbols_overview"],
"get_impact_analysis" => &["find_referencing_symbols", "get_symbols_overview"],
"get_importers" => &["get_impact_analysis", "get_symbol_importance"],
"get_symbol_importance" => &["get_importers", "get_impact_analysis"],
"find_dead_code" => &["get_symbols_overview", "delete_lines"],
"find_circular_dependencies" => &["get_impact_analysis", "get_symbols_overview"],
"get_change_coupling" => &["get_impact_analysis", "find_dead_code"],
"get_callers" => &["get_callees", "find_symbol"],
"get_callees" => &["get_callers", "find_symbol"],
"find_scoped_references" => &["rename_symbol", "find_referencing_symbols"],
"get_current_config" => &["get_capabilities", "get_project_structure"],
"read_file" => &["get_symbols_overview", "find_symbol"],
"search_for_pattern" => &["find_referencing_symbols", "get_ranked_context"],
"find_annotations" => &["get_symbols_overview", "find_symbol"],
"find_tests" => &["get_symbols_overview"],
"rename_symbol" => &[
"safe_rename_report",
"unresolved_reference_check",
"get_file_diagnostics",
],
"replace_symbol_body" => &["find_symbol", "get_file_diagnostics"],
"replace_content" => &["get_file_diagnostics", "get_symbols_overview"],
"replace_lines" => &["get_file_diagnostics"],
"delete_lines" => &["get_file_diagnostics"],
"insert_at_line" => &["get_file_diagnostics"],
"insert_before_symbol" => &["get_file_diagnostics", "find_symbol"],
"insert_after_symbol" => &["get_file_diagnostics", "find_symbol"],
"insert_content" => &["get_file_diagnostics", "find_symbol"],
"replace" => &["get_file_diagnostics", "get_symbols_overview"],
"create_text_file" => &["verify_change_readiness", "get_symbols_overview"],
"add_import" => &["get_file_diagnostics", "analyze_missing_imports"],
"analyze_missing_imports" => &["add_import"],
"write_memory" => &["list_memories", "read_memory"],
"read_memory" => &["write_memory", "list_memories"],
"list_memories" => &["read_memory", "write_memory"],
"activate_project" => &[
"get_project_structure",
"get_current_config",
"get_capabilities",
],
"prepare_harness_session" => &[
"get_current_config",
"get_capabilities",
"get_ranked_context",
],
"explore_codebase" => &["find_symbol", "review_architecture", "review_changes"],
"trace_request_path" => &["plan_safe_refactor", "find_symbol", "review_changes"],
"review_architecture" => &["review_changes", "explore_codebase", "plan_safe_refactor"],
"plan_safe_refactor" => &[
"trace_request_path",
"review_changes",
"get_file_diagnostics",
],
"audit_security_context" => &[
"review_changes",
"get_analysis_section",
"review_architecture",
],
"analyze_change_impact" => &[
"review_architecture",
"review_changes",
"get_analysis_section",
],
"cleanup_duplicate_logic" => &[
"review_changes",
"review_architecture",
"get_analysis_section",
],
"review_changes" => &["get_analysis_section", "impact_report", "diagnose_issues"],
"diagnose_issues" => &["review_changes", "find_symbol", "verify_change_readiness"],
"onboard_project" => &["get_ranked_context", "find_symbol", "get_capabilities"],
"get_watch_status" => &["refresh_symbol_index", "prune_index_failures"],
"prune_index_failures" => &["get_watch_status", "refresh_symbol_index"],
"list_queryable_projects" => &["add_queryable_project", "query_project"],
"add_queryable_project" => &["query_project", "list_queryable_projects"],
"query_project" => &["find_symbol", "list_queryable_projects"],
"set_preset" => &["get_capabilities"],
"get_capabilities" => &[
"get_project_structure",
"get_ranked_context",
"check_lsp_status",
],
"get_tool_metrics" => &["export_session_markdown", "set_preset", "get_capabilities"],
"semantic_search" => &["find_symbol", "get_symbols_overview", "find_similar_code"],
"index_embeddings" => &[
"semantic_search",
"find_code_duplicates",
"find_misplaced_code",
],
"find_similar_code" => &["get_symbols_overview", "semantic_search"],
"find_code_duplicates" => &["find_similar_code", "get_symbols_overview"],
"classify_symbol" => &["find_similar_code", "get_symbols_overview"],
"find_misplaced_code" => &["get_symbols_overview", "find_similar_code"],
"summarize_file" => &["get_symbols_overview", "find_symbol"],
"explain_code_flow" => &["get_callers", "get_callees"],
"refactor_extract_function" => &["get_file_diagnostics", "find_symbol"],
"refactor_inline_function" => &["get_file_diagnostics", "find_symbol"],
"refactor_move_to_file" => &["get_file_diagnostics", "find_referencing_symbols"],
"refactor_change_signature" => &["get_file_diagnostics", "find_referencing_symbols"],
"propagate_deletions" => &[
"delete_lines",
"get_file_diagnostics",
"get_impact_analysis",
],
"analyze_change_request" => &[
"get_analysis_section",
"verify_change_readiness",
"impact_report",
"refactor_safety_report",
],
"verify_change_readiness" => &[
"get_analysis_section",
"safe_rename_report",
"unresolved_reference_check",
],
"find_minimal_context_for_change" => &["get_analysis_section", "analyze_change_request"],
"summarize_symbol_impact" => &["get_analysis_section", "safe_rename_report"],
"module_boundary_report" => &[
"get_analysis_section",
"mermaid_module_graph",
"impact_report",
"dead_code_report",
],
"mermaid_module_graph" => &[
"get_analysis_section",
"module_boundary_report",
"impact_report",
],
"safe_rename_report" => &[
"get_analysis_section",
"unresolved_reference_check",
"rename_symbol",
"refactor_safety_report",
],
"unresolved_reference_check" => &[
"get_analysis_section",
"safe_rename_report",
"find_referencing_symbols",
],
"dead_code_report" => &["get_analysis_section", "impact_report"],
"impact_report" => &["get_analysis_section", "diff_aware_references"],
"refactor_safety_report" => &[
"get_analysis_section",
"verify_change_readiness",
"safe_rename_report",
],
"diff_aware_references" => &[
"get_analysis_section",
"impact_report",
"semantic_code_review",
],
"semantic_code_review" => &["get_analysis_section", "impact_report"],
"start_analysis_job" => &["get_analysis_job"],
"get_analysis_job" => &["get_analysis_section"],
"cancel_analysis_job" => &["start_analysis_job"],
_ => return None,
};
Some(suggestions.iter().map(|s| s.to_string()).collect())
}
pub fn suggestion_reasons_for(
tools: &[String],
_tool_name: &str,
) -> std::collections::HashMap<String, String> {
let mut reasons = std::collections::HashMap::new();
for tool in tools {
let reason = match tool.as_str() {
"get_file_diagnostics" => "Check for type errors or lint issues after this change",
"get_analysis_section" => "Expand a specific section from the analysis handle",
"verify_change_readiness" => "Validate mutation safety before editing code",
"impact_report" => "Assess blast radius of the changes",
"module_boundary_report" => "Check coupling and boundary violations",
"safe_rename_report" => "Preview rename safety before executing",
"diff_aware_references" => "Find references affected by recent changes",
"dead_code_report" => "Identify unused code after refactoring",
"find_referencing_symbols" => "Find all callers/users of this symbol",
"get_ranked_context" => "Get relevant context ranked by multiple signals",
"start_analysis_job" => "Run heavy analysis asynchronously",
"find_minimal_context_for_change" => "Get smallest context needed for this task",
"analyze_change_request" => "Compress the change request into ranked files and risks",
"explore_codebase" => "Get a high-level overview or targeted search",
"review_changes" => "Review impact of changed files before merge",
"diagnose_issues" => "Check for diagnostics or unresolved references",
_ => "Suggested as next step in the workflow chain",
};
reasons.insert(tool.clone(), reason.to_owned());
}
reasons
}
#[cfg(test)]
mod phase_inference_tests {
use super::infer_harness_phase;
fn tools(names: &[&str]) -> Vec<String> {
names.iter().map(|s| (*s).to_owned()).collect()
}
#[test]
fn mutation_at_end_infers_build() {
let recent = tools(&["find_symbol", "verify_change_readiness", "rename_symbol"]);
assert_eq!(infer_harness_phase(&recent), Some("build"));
}
#[test]
fn review_signal_without_mutation_infers_review() {
let recent = tools(&["find_symbol", "dead_code_report", "get_symbols_overview"]);
assert_eq!(infer_harness_phase(&recent), Some("review"));
}
#[test]
fn plan_only_trail_infers_plan() {
let recent = tools(&["onboard_project", "explore_codebase", "get_ranked_context"]);
assert_eq!(infer_harness_phase(&recent), Some("plan"));
}
#[test]
fn empty_recent_returns_none() {
assert_eq!(infer_harness_phase(&[]), None);
}
#[test]
fn unknown_tools_only_returns_none() {
let recent = tools(&["my_custom_thing", "another_unknown"]);
assert_eq!(infer_harness_phase(&recent), None);
}
#[test]
fn only_most_recent_five_are_considered() {
let recent = tools(&[
"rename_symbol", "find_symbol",
"find_symbol",
"find_symbol",
"find_symbol",
"find_symbol",
]);
assert_eq!(infer_harness_phase(&recent), None);
}
#[test]
fn most_recent_distinctive_signal_wins() {
let recent = tools(&[
"rename_symbol", "find_symbol",
"review_changes", ]);
assert_eq!(infer_harness_phase(&recent), Some("review"));
}
}