add code exploration fast start

This tries to short-circuit multiple round-trips to llm for reading code.
It's a precursor to trying to context engineer tailored to specific tasks.
In initial experiments, it's only marginally faster than regular mode, and burns more tokens.
This commit is contained in:
Jochen
2025-11-25 16:26:53 +11:00
parent f501751bdf
commit ad198a8501
17 changed files with 1418 additions and 22 deletions

View File

@@ -0,0 +1,13 @@
[package]
name = "g3-planner"
version = "0.1.0"
edition = "2021"
description = "Fast-discovery planner for G3 AI coding agent"
[dependencies]
g3-providers = { path = "../g3-providers" }
serde = { workspace = true }
serde_json = { workspace = true }
const_format = "0.2"
anyhow = { workspace = true }
tokio = { workspace = true }

View File

@@ -0,0 +1,724 @@
//! Code exploration module for analyzing codebases
//!
//! This module provides functions to explore and analyze codebases
//! for various programming languages, returning structured reports
//! about the code structure.
use std::path::Path;
use std::process::Command;
/// Main entry point for exploring a codebase at the given path.
/// Detects which languages are present and generates a comprehensive report.
pub fn explore_codebase(path: &str) -> String {
let path = expand_tilde(path);
let mut report = String::new();
let mut languages_found = Vec::new();
// Check for each language and add to report if found
if has_rust_files(&path) {
languages_found.push("Rust".to_string());
report.push_str(&explore_rust(&path));
}
if has_java_files(&path) {
languages_found.push("Java".to_string());
report.push_str(&explore_java(&path));
}
if has_kotlin_files(&path) {
languages_found.push("Kotlin".to_string());
report.push_str(&explore_kotlin(&path));
}
if has_swift_files(&path) {
languages_found.push("Swift".to_string());
report.push_str(&explore_swift(&path));
}
if has_go_files(&path) {
languages_found.push("Go".to_string());
report.push_str(&explore_go(&path));
}
if has_python_files(&path) {
languages_found.push("Python".to_string());
report.push_str(&explore_python(&path));
}
if has_typescript_files(&path) {
languages_found.push("TypeScript".to_string());
report.push_str(&explore_typescript(&path));
}
if has_javascript_files(&path) {
languages_found.push("JavaScript".to_string());
report.push_str(&explore_javascript(&path));
}
if has_cpp_files(&path) {
languages_found.push("C/C++".to_string());
report.push_str(&explore_cpp(&path));
}
if has_markdown_files(&path) {
languages_found.push("Markdown".to_string());
report.push_str(&explore_markdown(&path));
}
if has_yaml_files(&path) {
languages_found.push("YAML".to_string());
report.push_str(&explore_yaml(&path));
}
if has_sql_files(&path) {
languages_found.push("SQL".to_string());
report.push_str(&explore_sql(&path));
}
if has_ruby_files(&path) {
languages_found.push("Ruby".to_string());
report.push_str(&explore_ruby(&path));
}
if languages_found.is_empty() {
report.push_str("No recognized programming languages found in the codebase.\n");
} else {
let header = format!(
"=== CODEBASE ANALYSIS ===\nLanguages detected: {}\n\n",
languages_found.join(", ")
);
report = header + &report;
}
report
}
/// Expand tilde to home directory
fn expand_tilde(path: &str) -> String {
if path.starts_with("~/") {
if let Some(home) = std::env::var_os("HOME") {
return path.replacen("~", &home.to_string_lossy(), 1);
}
}
path.to_string()
}
/// Run a shell command and return its output
fn run_command(cmd: &str, working_dir: &str) -> String {
let output = Command::new("sh")
.arg("-c")
.arg(cmd)
.current_dir(working_dir)
.output();
match output {
Ok(out) => {
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
if !stdout.is_empty() {
stdout.to_string()
} else if !stderr.is_empty() {
format!("(stderr): {}", stderr)
} else {
String::new()
}
}
Err(e) => format!("Error running command: {}", e),
}
}
/// Check if files with given extension exist
fn has_files_with_extension(path: &str, extension: &str) -> bool {
let cmd = format!(
"find . -name '.git' -prune -o -type f -name '*.{}' -print | head -1",
extension
);
!run_command(&cmd, path).trim().is_empty()
}
// Language detection functions
fn has_rust_files(path: &str) -> bool {
has_files_with_extension(path, "rs") || Path::new(path).join("Cargo.toml").exists()
}
fn has_java_files(path: &str) -> bool {
has_files_with_extension(path, "java")
}
fn has_kotlin_files(path: &str) -> bool {
has_files_with_extension(path, "kt") || has_files_with_extension(path, "kts")
}
fn has_swift_files(path: &str) -> bool {
has_files_with_extension(path, "swift")
}
fn has_go_files(path: &str) -> bool {
has_files_with_extension(path, "go")
}
fn has_python_files(path: &str) -> bool {
has_files_with_extension(path, "py")
}
fn has_typescript_files(path: &str) -> bool {
has_files_with_extension(path, "ts") || has_files_with_extension(path, "tsx")
}
fn has_javascript_files(path: &str) -> bool {
has_files_with_extension(path, "js") || has_files_with_extension(path, "jsx")
}
fn has_cpp_files(path: &str) -> bool {
has_files_with_extension(path, "cpp")
|| has_files_with_extension(path, "cc")
|| has_files_with_extension(path, "c")
|| has_files_with_extension(path, "h")
|| has_files_with_extension(path, "hpp")
}
fn has_markdown_files(path: &str) -> bool {
has_files_with_extension(path, "md")
}
fn has_yaml_files(path: &str) -> bool {
has_files_with_extension(path, "yaml") || has_files_with_extension(path, "yml")
}
fn has_sql_files(path: &str) -> bool {
has_files_with_extension(path, "sql")
}
fn has_ruby_files(path: &str) -> bool {
has_files_with_extension(path, "rb")
}
/// Explore Rust codebase
pub fn explore_rust(path: &str) -> String {
let mut report = String::new();
report.push_str("\n=== RUST ===\n\n");
// File structure
report.push_str("--- File Structure ---\n");
let files = run_command(
"rg --files -g '*.rs' . 2>/dev/null | grep -v '/target/' | sort | head -100",
path,
);
report.push_str(&files);
report.push('\n');
// Dependencies (Cargo.toml)
report.push_str("--- Dependencies (Cargo.toml) ---\n");
let cargo = run_command("cat Cargo.toml 2>/dev/null | head -50", path);
report.push_str(&cargo);
report.push('\n');
// Data structures
report.push_str("--- Data Structures (Structs, Enums, Types) ---\n");
let structs = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.rs' '^(pub )?(struct|enum|type|union) ' . 2>/dev/null | grep -v '/target/' | head -100"#,
path,
);
report.push_str(&structs);
report.push('\n');
// Traits and implementations
report.push_str("--- Traits & Implementations ---\n");
let traits = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.rs' '^(pub )?trait |^impl ' . 2>/dev/null | grep -v '/target/' | head -100"#,
path,
);
report.push_str(&traits);
report.push('\n');
// Public functions
report.push_str("--- Public Functions ---\n");
let funcs = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.rs' '^pub (async )?fn ' . 2>/dev/null | grep -v '/target/' | head -100"#,
path,
);
report.push_str(&funcs);
report.push('\n');
report
}
/// Explore Java codebase
pub fn explore_java(path: &str) -> String {
let mut report = String::new();
report.push_str("\n=== JAVA ===\n\n");
// File structure
report.push_str("--- File Structure ---\n");
let files = run_command(
"rg --files -g '*.java' . 2>/dev/null | grep -v '/build/' | grep -v '/target/' | sort | head -100",
path,
);
report.push_str(&files);
report.push('\n');
// Build files
report.push_str("--- Build Configuration ---\n");
let build = run_command(
"cat pom.xml 2>/dev/null | head -50 || cat build.gradle 2>/dev/null | head -50",
path,
);
report.push_str(&build);
report.push('\n');
// Classes and interfaces
report.push_str("--- Classes & Interfaces ---\n");
let classes = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.java' '^(public |private |protected )?(abstract )?(class|interface|enum|record) ' . 2>/dev/null | grep -v '/build/' | head -100"#,
path,
);
report.push_str(&classes);
report.push('\n');
// Public methods
report.push_str("--- Public Methods ---\n");
let methods = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.java' '^\s+public .+\(' . 2>/dev/null | grep -v '/build/' | head -100"#,
path,
);
report.push_str(&methods);
report.push('\n');
report
}
/// Explore Kotlin codebase
pub fn explore_kotlin(path: &str) -> String {
let mut report = String::new();
report.push_str("\n=== KOTLIN ===\n\n");
// File structure
report.push_str("--- File Structure ---\n");
let files = run_command(
"rg --files -g '*.kt' -g '*.kts' . 2>/dev/null | grep -v '/build/' | sort | head -100",
path,
);
report.push_str(&files);
report.push('\n');
// Build files
report.push_str("--- Build Configuration ---\n");
let build = run_command("cat build.gradle.kts 2>/dev/null | head -50 || cat build.gradle 2>/dev/null | head -50", path);
report.push_str(&build);
report.push('\n');
// Classes, objects, interfaces
report.push_str("--- Classes, Objects & Interfaces ---\n");
let classes = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.kt' '^(data |sealed |open |abstract )?(class|interface|object|enum class) ' . 2>/dev/null | grep -v '/build/' | head -100"#,
path,
);
report.push_str(&classes);
report.push('\n');
// Functions
report.push_str("--- Functions ---\n");
let funcs = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.kt' '^(suspend |private |internal |public )?fun ' . 2>/dev/null | grep -v '/build/' | head -100"#,
path,
);
report.push_str(&funcs);
report.push('\n');
report
}
/// Explore Swift codebase
pub fn explore_swift(path: &str) -> String {
let mut report = String::new();
report.push_str("\n=== SWIFT ===\n\n");
// File structure
report.push_str("--- File Structure ---\n");
let files = run_command(
"rg --files -g '*.swift' . 2>/dev/null | grep -v '/.build/' | sort | head -100",
path,
);
report.push_str(&files);
report.push('\n');
// Package.swift
report.push_str("--- Package Configuration ---\n");
let pkg = run_command("cat Package.swift 2>/dev/null | head -50", path);
report.push_str(&pkg);
report.push('\n');
// Classes, structs, protocols
report.push_str("--- Types (Classes, Structs, Protocols, Enums) ---\n");
let types = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.swift' '^(public |private |internal |open |final )?(class|struct|protocol|enum|actor) ' . 2>/dev/null | grep -v '/.build/' | head -100"#,
path,
);
report.push_str(&types);
report.push('\n');
// Functions
report.push_str("--- Functions ---\n");
let funcs = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.swift' '^\s*(public |private |internal |open )?func ' . 2>/dev/null | grep -v '/.build/' | head -100"#,
path,
);
report.push_str(&funcs);
report.push('\n');
report
}
/// Explore Go codebase
pub fn explore_go(path: &str) -> String {
let mut report = String::new();
report.push_str("\n=== GO ===\n\n");
// File structure
report.push_str("--- File Structure ---\n");
let files = run_command(
"rg --files -g '*.go' . 2>/dev/null | grep -v '/vendor/' | sort | head -100",
path,
);
report.push_str(&files);
report.push('\n');
// go.mod
report.push_str("--- Module Configuration ---\n");
let gomod = run_command("cat go.mod 2>/dev/null | head -50", path);
report.push_str(&gomod);
report.push('\n');
// Types (structs, interfaces)
report.push_str("--- Types (Structs & Interfaces) ---\n");
let types = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.go' '^type .+ (struct|interface)' . 2>/dev/null | grep -v '/vendor/' | head -100"#,
path,
);
report.push_str(&types);
report.push('\n');
// Functions
report.push_str("--- Functions ---\n");
let funcs = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.go' '^func ' . 2>/dev/null | grep -v '/vendor/' | head -100"#,
path,
);
report.push_str(&funcs);
report.push('\n');
report
}
/// Explore Python codebase
pub fn explore_python(path: &str) -> String {
let mut report = String::new();
report.push_str("\n=== PYTHON ===\n\n");
// File structure
report.push_str("--- File Structure ---\n");
let files = run_command(
"rg --files -g '*.py' . 2>/dev/null | grep -v '/__pycache__/' | grep -v '/venv/' | grep -v '/.venv/' | sort | head -100",
path,
);
report.push_str(&files);
report.push('\n');
// Requirements/setup
report.push_str("--- Dependencies ---\n");
let deps = run_command(
"cat requirements.txt 2>/dev/null | head -30 || cat pyproject.toml 2>/dev/null | head -50 || cat setup.py 2>/dev/null | head -30",
path,
);
report.push_str(&deps);
report.push('\n');
// Classes
report.push_str("--- Classes ---\n");
let classes = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.py' '^class ' . 2>/dev/null | grep -v '/__pycache__/' | grep -v '/venv/' | head -100"#,
path,
);
report.push_str(&classes);
report.push('\n');
// Functions
report.push_str("--- Functions ---\n");
let funcs = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.py' '^def |^async def ' . 2>/dev/null | grep -v '/__pycache__/' | grep -v '/venv/' | head -100"#,
path,
);
report.push_str(&funcs);
report.push('\n');
report
}
/// Explore TypeScript codebase
pub fn explore_typescript(path: &str) -> String {
let mut report = String::new();
report.push_str("\n=== TYPESCRIPT ===\n\n");
// File structure
report.push_str("--- File Structure ---\n");
let files = run_command(
"rg --files -g '*.ts' -g '*.tsx' . 2>/dev/null | grep -v '/node_modules/' | grep -v '/dist/' | sort | head -100",
path,
);
report.push_str(&files);
report.push('\n');
// package.json
report.push_str("--- Package Configuration ---\n");
let pkg = run_command("cat package.json 2>/dev/null | head -50", path);
report.push_str(&pkg);
report.push('\n');
// Types, interfaces, classes
report.push_str("--- Types, Interfaces & Classes ---\n");
let types = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.ts' -g '*.tsx' '^export (type|interface|class|enum|abstract class) ' . 2>/dev/null | grep -v '/node_modules/' | head -100"#,
path,
);
report.push_str(&types);
report.push('\n');
// Functions
report.push_str("--- Exported Functions ---\n");
let funcs = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.ts' -g '*.tsx' '^export (async )?function |^export const .+ = (async )?\(' . 2>/dev/null | grep -v '/node_modules/' | head -100"#,
path,
);
report.push_str(&funcs);
report.push('\n');
report
}
/// Explore JavaScript codebase
pub fn explore_javascript(path: &str) -> String {
let mut report = String::new();
report.push_str("\n=== JAVASCRIPT ===\n\n");
// File structure
report.push_str("--- File Structure ---\n");
let files = run_command(
"rg --files -g '*.js' -g '*.jsx' . 2>/dev/null | grep -v '/node_modules/' | grep -v '/dist/' | sort | head -100",
path,
);
report.push_str(&files);
report.push('\n');
// package.json
report.push_str("--- Package Configuration ---\n");
let pkg = run_command("cat package.json 2>/dev/null | head -50", path);
report.push_str(&pkg);
report.push('\n');
// Classes
report.push_str("--- Classes ---\n");
let classes = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.js' -g '*.jsx' '^(export )?(default )?(class ) ' . 2>/dev/null | grep -v '/node_modules/' | head -100"#,
path,
);
report.push_str(&classes);
report.push('\n');
// Functions
report.push_str("--- Exported Functions ---\n");
let funcs = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.js' -g '*.jsx' '^(export )?(async )?function |^module\.exports' . 2>/dev/null | grep -v '/node_modules/' | head -100"#,
path,
);
report.push_str(&funcs);
report.push('\n');
report
}
/// Explore C/C++ codebase
pub fn explore_cpp(path: &str) -> String {
let mut report = String::new();
report.push_str("\n=== C/C++ ===\n\n");
// File structure
report.push_str("--- File Structure ---\n");
let files = run_command(
"rg --files -g '*.c' -g '*.cpp' -g '*.cc' -g '*.h' -g '*.hpp' . 2>/dev/null | grep -v '/build/' | sort | head -100",
path,
);
report.push_str(&files);
report.push('\n');
// Build files
report.push_str("--- Build Configuration ---\n");
let build = run_command(
"cat CMakeLists.txt 2>/dev/null | head -50 || cat Makefile 2>/dev/null | head -50",
path,
);
report.push_str(&build);
report.push('\n');
// Classes and structs
report.push_str("--- Classes & Structs ---\n");
let classes = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.cpp' -g '*.cc' -g '*.h' -g '*.hpp' '^(class|struct|enum|union|typedef) ' . 2>/dev/null | grep -v '/build/' | head -100"#,
path,
);
report.push_str(&classes);
report.push('\n');
// Functions (simplified pattern)
report.push_str("--- Function Declarations ---\n");
let funcs = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.h' -g '*.hpp' '^[a-zA-Z_][a-zA-Z0-9_<>: ]*\s+[a-zA-Z_][a-zA-Z0-9_]*\s*\(' . 2>/dev/null | grep -v '/build/' | head -100"#,
path,
);
report.push_str(&funcs);
report.push('\n');
report
}
/// Explore Markdown documentation
pub fn explore_markdown(path: &str) -> String {
let mut report = String::new();
report.push_str("\n=== MARKDOWN DOCUMENTATION ===\n\n");
// File structure
report.push_str("--- Documentation Files ---\n");
let files = run_command(
"rg --files -g '*.md' . 2>/dev/null | grep -v '/node_modules/' | grep -v '/vendor/' | sort | head -50",
path,
);
report.push_str(&files);
report.push('\n');
// README content
report.push_str("--- README Overview ---\n");
let readme = run_command(
"cat README.md 2>/dev/null | head -100 || cat readme.md 2>/dev/null | head -100",
path,
);
report.push_str(&readme);
report.push('\n');
// Headers from all markdown files
report.push_str("--- Document Headers ---\n");
let headers = run_command(
r#"rg --no-heading --line-number --with-filename -g '*.md' '^#{1,3} ' . 2>/dev/null | grep -v '/node_modules/' | head -100"#,
path,
);
report.push_str(&headers);
report.push('\n');
report
}
/// Explore YAML configuration files
pub fn explore_yaml(path: &str) -> String {
let mut report = String::new();
report.push_str("\n=== YAML CONFIGURATION ===\n\n");
// File structure
report.push_str("--- YAML Files ---\n");
let files = run_command(
"rg --files -g '*.yaml' -g '*.yml' . 2>/dev/null | grep -v '/node_modules/' | grep -v '/vendor/' | sort | head -50",
path,
);
report.push_str(&files);
report.push('\n');
// Top-level keys from YAML files
report.push_str("--- Top-Level Keys ---\n");
let keys = run_command(
r#"rg --no-heading --line-number --with-filename -g '*.yaml' -g '*.yml' '^[a-zA-Z_][a-zA-Z0-9_-]*:' . 2>/dev/null | grep -v '/node_modules/' | head -100"#,
path,
);
report.push_str(&keys);
report.push('\n');
report
}
/// Explore SQL files
pub fn explore_sql(path: &str) -> String {
let mut report = String::new();
report.push_str("\n=== SQL ===\n\n");
// File structure
report.push_str("--- SQL Files ---\n");
let files = run_command(
"rg --files -g '*.sql' . 2>/dev/null | sort | head -50",
path,
);
report.push_str(&files);
report.push('\n');
// Tables
report.push_str("--- Table Definitions ---\n");
let tables = run_command(
r#"rg --no-heading --line-number --with-filename -i -g '*.sql' 'CREATE TABLE' . 2>/dev/null | head -100"#,
path,
);
report.push_str(&tables);
report.push('\n');
// Views and procedures
report.push_str("--- Views & Procedures ---\n");
let views = run_command(
r#"rg --no-heading --line-number --with-filename -i -g '*.sql' 'CREATE (VIEW|PROCEDURE|FUNCTION)' . 2>/dev/null | head -100"#,
path,
);
report.push_str(&views);
report.push('\n');
report
}
/// Explore Ruby codebase
pub fn explore_ruby(path: &str) -> String {
let mut report = String::new();
report.push_str("\n=== RUBY ===\n\n");
// File structure
report.push_str("--- File Structure ---\n");
let files = run_command(
"rg --files -g '*.rb' . 2>/dev/null | grep -v '/vendor/' | sort | head -100",
path,
);
report.push_str(&files);
report.push('\n');
// Gemfile
report.push_str("--- Dependencies (Gemfile) ---\n");
let gemfile = run_command("cat Gemfile 2>/dev/null | head -50", path);
report.push_str(&gemfile);
report.push('\n');
// Classes and modules
report.push_str("--- Classes & Modules ---\n");
let classes = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.rb' '^(class|module) ' . 2>/dev/null | grep -v '/vendor/' | head -100"#,
path,
);
report.push_str(&classes);
report.push('\n');
// Methods
report.push_str("--- Methods ---\n");
let methods = run_command(
r#"rg --no-heading --line-number --with-filename --max-filesize 500K -g '*.rb' '^\s*def ' . 2>/dev/null | grep -v '/vendor/' | head -100"#,
path,
);
report.push_str(&methods);
report.push('\n');
report
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_expand_tilde() {
let path = expand_tilde("~/test");
assert!(!path.starts_with("~"));
}
#[test]
fn test_explore_codebase_returns_string() {
// Test with current directory
let result = explore_codebase(".");
assert!(!result.is_empty());
}
}

View File

@@ -0,0 +1,253 @@
//! g3-planner: Fast-discovery planner for G3 AI coding agent
//!
//! This crate provides functionality to generate initial discovery tool calls
//! that are injected into the conversation before the first LLM turn.
mod code_explore;
pub mod prompts;
pub use code_explore::explore_codebase;
use anyhow::Result;
use g3_providers::{CompletionRequest, LLMProvider, Message, MessageRole};
use prompts::{DISCOVERY_REQUIREMENTS_PROMPT, DISCOVERY_SYSTEM_PROMPT};
/// Type alias for a status callback function
pub type StatusCallback = Box<dyn Fn(&str) + Send + Sync>;
/// Generates initial discovery messages for fast codebase exploration.
///
/// This function:
/// 1. Runs explore_codebase to get a codebase report
/// 2. Sends the report to the LLM with DISCOVERY_SYSTEM_PROMPT
/// 3. Extracts shell commands from the LLM response
/// 4. Returns Assistant messages with tool calls for each command
///
/// # Arguments
///
/// * `codebase_path` - The path to the codebase to explore
/// * `provider` - An LLM provider to query for exploration commands
/// * `status_callback` - Optional callback for status updates
///
/// # Returns
///
/// A `Result<Vec<Message>>` containing Assistant messages with JSON tool call strings.
pub async fn get_initial_discovery_messages(
codebase_path: &str,
provider: &dyn LLMProvider,
status_callback: Option<&StatusCallback>,
) -> Result<Vec<Message>> {
// Helper to call status callback if provided
let status = |msg: &str| {
if let Some(cb) = status_callback {
cb(msg);
}
};
status("🔍 Starting code discovery...");
// Step 1: Run explore_codebase to get the codebase report
let codebase_report = explore_codebase(codebase_path);
// Step 2: Build the prompt with the codebase report appended
let user_prompt = format!(
"{}\n\n=== CODEBASE REPORT ===\n\n{}",
DISCOVERY_REQUIREMENTS_PROMPT, codebase_report
);
// Step 3: Create messages for the LLM
let messages = vec![
Message::new(MessageRole::System, DISCOVERY_SYSTEM_PROMPT.to_string()),
Message::new(MessageRole::User, user_prompt),
];
// Step 4: Send to LLM
let request = CompletionRequest {
messages,
max_tokens: Some(provider.max_tokens()),
temperature: Some(provider.temperature()),
stream: false,
tools: None,
};
status("🤖 Calling LLM for discovery commands...");
let response = provider.complete(request).await?;
// Step 5: Extract shell commands from the response
let shell_commands = extract_shell_commands(&response.content);
status(&format!("📋 Extracted {} discovery commands", shell_commands.len()));
// Step 6: Format as tool messages
let tool_messages = shell_commands
.into_iter()
.map(|cmd| create_tool_message("shell", &cmd))
.collect();
Ok(tool_messages)
}
/// Creates an Assistant message with a tool call in g3's JSON format.
pub fn create_tool_message(tool: &str, command: &str) -> Message {
let tool_call = serde_json::json!({
"tool": tool,
"args": {
"command": command
}
});
Message::new(MessageRole::Assistant, tool_call.to_string())
}
/// Extract shell commands from the LLM response.
/// Looks for {{CODE EXPLORATION COMMANDS}} section and extracts commands from code blocks.
pub fn extract_shell_commands(response: &str) -> Vec<String> {
let mut commands = Vec::new();
let section_marker = "{{CODE EXPLORATION COMMANDS}}";
let section_start = match response.find(section_marker) {
Some(pos) => pos + section_marker.len(),
None => return commands,
};
let section_content = &response[section_start..];
let mut in_code_block = false;
let mut current_block = String::new();
for line in section_content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("```") {
if in_code_block {
// End of code block - extract commands
for cmd_line in current_block.lines() {
let cmd = cmd_line.trim();
if !cmd.is_empty() && !cmd.starts_with('#') {
commands.push(cmd.to_string());
}
}
current_block.clear();
}
in_code_block = !in_code_block;
} else if in_code_block {
current_block.push_str(line);
current_block.push('\n');
}
}
commands
}
/// Extract the summary section from the LLM response
pub fn extract_summary(response: &str) -> Option<String> {
let section_marker = "{{SUMMARY BASED ON INITIAL INFO}}";
let section_start = match response.find(section_marker) {
Some(pos) => pos + section_marker.len(),
None => return None,
};
let section_content = &response[section_start..];
let section_end = section_content.find("{{").unwrap_or(section_content.len());
let summary = section_content[..section_end].trim().to_string();
if summary.is_empty() {
None
} else {
Some(summary)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_tool_message_format() {
let msg = create_tool_message("shell", "ls -la");
assert!(matches!(msg.role, MessageRole::Assistant));
let parsed: serde_json::Value = serde_json::from_str(&msg.content).unwrap();
assert_eq!(parsed["tool"], "shell");
assert_eq!(parsed["args"]["command"], "ls -la");
}
#[test]
fn test_extract_shell_commands_basic() {
let response = r#"
Some text here.
{{CODE EXPLORATION COMMANDS}}
```bash
ls -la
cat README.md
rg --files -g '*.rs'
```
More text.
"#;
let commands = extract_shell_commands(response);
assert_eq!(commands.len(), 3);
assert_eq!(commands[0], "ls -la");
assert_eq!(commands[1], "cat README.md");
assert_eq!(commands[2], "rg --files -g '*.rs'");
}
#[test]
fn test_extract_shell_commands_with_comments() {
let response = r#"
{{CODE EXPLORATION COMMANDS}}
```
# This is a comment
ls -la
# Another comment
cat file.txt
```
"#;
let commands = extract_shell_commands(response);
assert_eq!(commands.len(), 2);
assert_eq!(commands[0], "ls -la");
assert_eq!(commands[1], "cat file.txt");
}
#[test]
fn test_extract_shell_commands_no_section() {
let response = "Some response without the expected section.";
let commands = extract_shell_commands(response);
assert!(commands.is_empty());
}
#[test]
fn test_extract_summary() {
let response = r#"
{{SUMMARY BASED ON INITIAL INFO}}
This is a summary of the codebase.
It has multiple lines.
{{CODE EXPLORATION COMMANDS}}
```
ls -la
```
"#;
let summary = extract_summary(response);
assert!(summary.is_some());
let summary_text = summary.unwrap();
assert!(summary_text.contains("This is a summary"));
assert!(summary_text.contains("multiple lines"));
}
#[test]
fn test_extract_summary_no_section() {
let response = "Response without summary section.";
let summary = extract_summary(response);
assert!(summary.is_none());
}
}

View File

@@ -0,0 +1,31 @@
//! Prompts used for discovery phase
/// System prompt for discovery mode - instructs the LLM to analyze codebase and generate exploration commands
pub const DISCOVERY_SYSTEM_PROMPT: &str = r#"You are an expert code analyst. Your task is to analyze a codebase structure and generate shell commands to explore it further.
You will receive:
1. User requirements describing what needs to be implemented
2. A codebase report showing the structure and key elements of the codebase
Your job is to:
1. Understand the requirements and identify what parts of the codebase are relevant
2. Generate shell commands to explore those parts in more detail
IMPORTANT: Do NOT attempt to implement anything. Only generate exploration commands."#;
/// Discovery prompt template - used when we have a codebase report.
/// The codebase report should be appended after this prompt.
pub const DISCOVERY_REQUIREMENTS_PROMPT: &str = r#"**CRITICAL**: DO ABSOLUTELY NOT ATTEMPT TO IMPLEMENT THESE REQUIREMENTS AT THIS POINT. ONLY USE THEM TO
UNDERSTAND WHICH PARTS OF THE CODE YOU MIGHT BE INTERESTED IN, AND WHAT SEARCH/GREP EXPRESSIONS YOU MIGHT WANT TO USE
TO GET A BETTER UNDERSTANDING OF THE CODEBASE.
Your task is to analyze the codebase structure provided below and generate shell commands to explore it further.
Your output MUST include:
1. A section with heading {{SUMMARY BASED ON INITIAL INFO}} containing a brief summary of what you understand about the codebase structure (max 10000 tokens).
2. A section with heading {{CODE EXPLORATION COMMANDS}} containing shell commands to explore the codebase further.
- Use tools like `ls`, `rg` (ripgrep), `grep`, `sed`, `cat`, `head`, `tail` etc.
- Focus on commands that will help understand the code structure without dumping entire files.
- Mark the beginning and end of the commands with "```".
DO NOT ADD ANY COMMENTS OR OTHER EXPLANATION IN THE COMMANDS SECTION, JUST INCLUDE THE SHELL COMMANDS."#;

View File

@@ -0,0 +1,103 @@
//! Integration tests for g3-planner
use g3_planner::{create_tool_message, explore_codebase, extract_shell_commands};
use g3_providers::MessageRole;
#[test]
fn test_create_tool_message_format() {
let msg = create_tool_message("shell", "ls -la");
assert!(matches!(msg.role, MessageRole::Assistant));
let parsed: serde_json::Value = serde_json::from_str(&msg.content).unwrap();
assert_eq!(parsed["tool"], "shell");
assert_eq!(parsed["args"]["command"], "ls -la");
}
#[test]
fn test_explore_codebase_returns_report() {
// Test with current directory (should find Rust files in g3 project)
let report = explore_codebase(".");
// Should return a non-empty report
assert!(!report.is_empty(), "Report should not be empty");
// Should contain the codebase analysis header
assert!(
report.contains("CODEBASE ANALYSIS") || report.contains("No recognized"),
"Report should have analysis header or indicate no languages found"
);
}
#[test]
fn test_extract_shell_commands_basic() {
let response = r#"
Some text here.
{{CODE EXPLORATION COMMANDS}}
```bash
ls -la
cat README.md
rg --files -g '*.rs'
```
More text.
"#;
let commands = extract_shell_commands(response);
assert_eq!(commands.len(), 3);
assert_eq!(commands[0], "ls -la");
assert_eq!(commands[1], "cat README.md");
assert_eq!(commands[2], "rg --files -g '*.rs'");
}
#[test]
fn test_extract_shell_commands_with_comments() {
let response = r#"
{{CODE EXPLORATION COMMANDS}}
```
# This is a comment
ls -la
# Another comment
cat file.txt
```
"#;
let commands = extract_shell_commands(response);
assert_eq!(commands.len(), 2);
assert_eq!(commands[0], "ls -la");
assert_eq!(commands[1], "cat file.txt");
}
#[test]
fn test_extract_shell_commands_no_section() {
let response = "Some response without the expected section.";
let commands = extract_shell_commands(response);
assert!(commands.is_empty());
}
#[test]
fn test_extract_shell_commands_multiple_code_blocks() {
let response = r#"
{{CODE EXPLORATION COMMANDS}}
```bash
ls -la
```
Some explanation text.
```
cat README.md
head -50 src/main.rs
```
"#;
let commands = extract_shell_commands(response);
assert_eq!(commands.len(), 3);
assert_eq!(commands[0], "ls -la");
assert_eq!(commands[1], "cat README.md");
assert_eq!(commands[2], "head -50 src/main.rs");
}