Add language-specific prompt injection for toolchain guidance

- Add language_prompts module that auto-detects programming languages in workspace
- Scan for language files with depth limit (2) to inject relevant toolchain prompts
- Add prompts/langs/ directory for language-specific markdown files
- Include Racket/raco toolchain guidance as first language prompt
- Update combine_project_content() to accept language_content parameter
- Integrate language detection into main CLI flow and agent mode
- Update project memory with new feature documentation
This commit is contained in:
Dhanji R. Prasanna
2026-01-14 21:00:52 +05:30
parent 716d598bd8
commit afec65fd50
6 changed files with 230 additions and 6 deletions

View File

@@ -1,5 +1,5 @@
# Project Memory
> Updated: 2026-01-13T16:15:37Z | Size: 11.8k chars
> Updated: 2026-01-14T15:11:36Z | Size: 12.0k chars
### Remember Tool Wiring
- `crates/g3-core/src/tools/memory.rs` [0..5000] - `execute_remember()`, `get_memory_path()`, `merge_memory()`
@@ -192,4 +192,23 @@ Rich few-shot prompting for higher quality memory entries with per-symbol char r
- `crates/g3-core/src/lib.rs`
- `send_auto_memory_reminder()` [47800..48800] - MEMORY CHECKPOINT prompt with few-shot examples
- `crates/g3-core/src/prompts.rs`
- Memory Format section [3800..4500] - system prompt template and examples
- Memory Format section [3800..4500] - system prompt template and examples
### Language-Specific Prompt Injection
Auto-detects programming languages in workspace and injects toolchain guidance.
- `crates/g3-cli/src/language_prompts.rs`
- `LANGUAGE_PROMPTS` [12..19] - static array of (lang_name, extensions, prompt_content)
- `detect_languages()` [22..32] - scans workspace for language files
- `get_language_prompts_for_workspace()` [88..108] - returns formatted prompt for detected languages
- `scan_directory_for_extensions()` [42..77] - recursive scan with depth limit (2), skips hidden/vendor dirs
- `prompts/langs/` - directory for language prompt markdown files
- `racket.md` - Racket/raco toolchain guidance (compilation, testing, analysis, profiling)
- `crates/g3-cli/src/project_files.rs`
- `combine_project_content()` [89..106] - now accepts `language_content` parameter
To add a new language:
1. Create `prompts/langs/<lang>.md` with toolchain guidance
2. Add entry to `LANGUAGE_PROMPTS` in `language_prompts.rs` with extensions

View File

@@ -146,6 +146,7 @@ pub async fn run_agent_mode(
agents_content_opt,
readme_content_opt,
memory_content_opt,
crate::language_prompts::get_language_prompts_for_workspace(&workspace_dir),
&workspace_dir,
);

View File

@@ -0,0 +1,169 @@
//! Language-specific prompt injection.
//!
//! Detects programming languages in the workspace and injects relevant
//! toolchain guidance into the system prompt.
//!
//! Language prompts are embedded at compile time from `prompts/langs/*.md`.
use std::path::Path;
/// Embedded language prompts, keyed by language name.
/// The key should match common file extensions or language identifiers.
static LANGUAGE_PROMPTS: &[(&str, &[&str], &str)] = &[
// (language_name, file_extensions, prompt_content)
(
"racket",
&[".rkt", ".rktl", ".rktd", ".scrbl"],
include_str!("../../../prompts/langs/racket.md"),
),
];
/// Detect languages present in the workspace by scanning for file extensions.
/// Returns a list of detected language names.
pub fn detect_languages(workspace_dir: &Path) -> Vec<&'static str> {
let mut detected = Vec::new();
for (lang_name, extensions, _) in LANGUAGE_PROMPTS {
if has_files_with_extensions(workspace_dir, extensions) {
detected.push(*lang_name);
}
}
detected
}
/// Check if the workspace contains files with any of the given extensions.
/// Scans up to a reasonable depth to avoid slow startup on large repos.
fn has_files_with_extensions(workspace_dir: &Path, extensions: &[&str]) -> bool {
// Quick check: scan top-level and one level deep
// This avoids slow startup on large repos while catching most projects
scan_directory_for_extensions(workspace_dir, extensions, 2)
}
/// Recursively scan a directory for files with given extensions, up to max_depth.
fn scan_directory_for_extensions(dir: &Path, extensions: &[&str], max_depth: usize) -> bool {
if max_depth == 0 {
return false;
}
let entries = match std::fs::read_dir(dir) {
Ok(entries) => entries,
Err(_) => return false,
};
for entry in entries.flatten() {
let path = entry.path();
// Skip hidden directories and common non-source directories
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if name.starts_with('.') || name == "node_modules" || name == "target" || name == "vendor" {
continue;
}
}
if path.is_file() {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
for ext in extensions {
if name.ends_with(ext) {
return true;
}
}
}
} else if path.is_dir() {
if scan_directory_for_extensions(&path, extensions, max_depth - 1) {
return true;
}
}
}
false
}
/// Get the prompt content for a specific language.
pub fn get_language_prompt(lang: &str) -> Option<&'static str> {
LANGUAGE_PROMPTS
.iter()
.find(|(name, _, _)| *name == lang)
.map(|(_, _, content)| *content)
}
/// Get all language prompts for detected languages in the workspace.
/// Returns formatted content ready for injection into the system prompt.
pub fn get_language_prompts_for_workspace(workspace_dir: &Path) -> Option<String> {
let detected = detect_languages(workspace_dir);
if detected.is_empty() {
return None;
}
let mut prompts = Vec::new();
for lang in detected {
if let Some(content) = get_language_prompt(lang) {
prompts.push(content);
}
}
if prompts.is_empty() {
return None;
}
Some(format!(
"🔧 Language-Specific Guidance:\n\n{}",
prompts.join("\n\n---\n\n")
))
}
/// List all available language prompts.
pub fn list_available_languages() -> Vec<&'static str> {
LANGUAGE_PROMPTS.iter().map(|(name, _, _)| *name).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_racket_prompt_embedded() {
let prompt = get_language_prompt("racket");
assert!(prompt.is_some());
assert!(prompt.unwrap().contains("raco"));
}
#[test]
fn test_list_available_languages() {
let langs = list_available_languages();
assert!(langs.contains(&"racket"));
}
#[test]
fn test_detect_racket_files() {
let temp_dir = TempDir::new().unwrap();
let rkt_file = temp_dir.path().join("main.rkt");
fs::write(&rkt_file, "#lang racket\n").unwrap();
let detected = detect_languages(temp_dir.path());
assert!(detected.contains(&"racket"));
}
#[test]
fn test_no_detection_empty_dir() {
let temp_dir = TempDir::new().unwrap();
let detected = detect_languages(temp_dir.path());
assert!(detected.is_empty());
}
#[test]
fn test_get_prompts_for_workspace() {
let temp_dir = TempDir::new().unwrap();
let rkt_file = temp_dir.path().join("main.rkt");
fs::write(&rkt_file, "#lang racket\n").unwrap();
let prompts = get_language_prompts_for_workspace(temp_dir.path());
assert!(prompts.is_some());
let content = prompts.unwrap();
assert!(content.contains("🔧 Language-Specific Guidance"));
assert!(content.contains("raco"));
}
}

View File

@@ -5,6 +5,7 @@ pub mod metrics;
pub mod project_files;
pub mod streaming_markdown;
pub mod embedded_agents;
pub mod language_prompts;
mod accumulative;
mod agent_mode;
@@ -98,6 +99,7 @@ pub async fn run() -> Result<()> {
let agents_content = read_agents_config(&workspace_dir);
let readme_content = read_project_readme(&workspace_dir);
let memory_content = read_project_memory(&workspace_dir);
let language_content = language_prompts::get_language_prompts_for_workspace(&workspace_dir);
// Create project model
let project = create_project(&cli, &workspace_dir)?;
@@ -110,7 +112,7 @@ pub async fn run() -> Result<()> {
let config = load_config_with_cli_overrides(&cli)?;
// Combine AGENTS.md, README, and memory content
let combined_content = combine_project_content(agents_content, readme_content, memory_content, &workspace_dir);
let combined_content = combine_project_content(agents_content, readme_content, memory_content, language_content, &workspace_dir);
run_console_mode(cli, config, project, combined_content, workspace_dir).await
}

View File

@@ -90,12 +90,13 @@ pub fn combine_project_content(
agents_content: Option<String>,
readme_content: Option<String>,
memory_content: Option<String>,
language_content: Option<String>,
workspace_dir: &Path,
) -> Option<String> {
// Always include working directory to prevent LLM from hallucinating paths
let cwd_info = format!("📂 Working Directory: {}", workspace_dir.display());
let parts: Vec<String> = [Some(cwd_info), agents_content, readme_content, memory_content]
let parts: Vec<String> = [Some(cwd_info), agents_content, readme_content, memory_content, language_content]
.into_iter()
.flatten()
.collect();
@@ -222,6 +223,7 @@ mod tests {
Some("agents".to_string()),
Some("readme".to_string()),
Some("memory".to_string()),
Some("language".to_string()),
&workspace,
);
assert!(result.is_some());
@@ -230,12 +232,13 @@ mod tests {
assert!(content.contains("agents"));
assert!(content.contains("readme"));
assert!(content.contains("memory"));
assert!(content.contains("language"));
}
#[test]
fn test_combine_project_content_partial() {
let workspace = std::path::PathBuf::from("/test/workspace");
let result = combine_project_content(None, Some("readme".to_string()), None, &workspace);
let result = combine_project_content(None, Some("readme".to_string()), None, None, &workspace);
assert!(result.is_some());
let content = result.unwrap();
assert!(content.contains("📂 Working Directory: /test/workspace"));
@@ -245,7 +248,7 @@ mod tests {
#[test]
fn test_combine_project_content_all_none() {
let workspace = std::path::PathBuf::from("/test/workspace");
let result = combine_project_content(None, None, None, &workspace);
let result = combine_project_content(None, None, None, None, &workspace);
// Now always returns Some because we always include the working directory
assert!(result.is_some());
assert!(result.unwrap().contains("📂 Working Directory: /test/workspace"));

30
prompts/langs/racket.md Normal file
View File

@@ -0,0 +1,30 @@
RACKET LANGUAGE CODE EXPLORATION + RACO TOOLING
- Core `raco` commands to rely on:
- Documentation & discovery:
- `raco docs <id>` to open docs for identifiers, modules, or packages.
- Compilation & checks:
- `raco make <file.rkt>` to force compilation and surface errors early.
- Testing:
- `raco test <path>` to run `module+ test` blocks and test files.
- Packages & dependencies:
- `raco pkg show` to inspect installed packages and their locations.
- `raco pkg show <pkg>` to inspect package metadata and versions.
- Profiling & performance:
- `raco profile <file.rkt>` for CPU hot spots.
- Debugging & stack traces:
- `racket -l errortrace <file.rkt>` (or enabling errortrace) for readable stack traces.
- Structural analysis tools (use when reasoning about non-trivial codebases):
- `raco dependency-graph <path>`:
- Use to visualize or reason about module dependencies and layering.
- Identify cycles, high fan-in “core” modules, and accidental coupling.
- `raco modgraph`:
- Use for quick textual inspection of module graphs when visualization isnt needed.
- Treat dependency graphs as architectural signals, not just diagrams.
- `racket -e`-driven exploration:
- Use the one-shot script execution to:
- `require` modules incrementally and inspect exports.
- Probe functions with small concrete examples.
- Validate assumptions about data shapes and return values.