Agent Mode Enhancements
• Agent prompts are now embedded within the g3 binary • README.md - Added new "Agent Mode" section documenting: • All 7 built-in agents with their focus areas • Usage examples (--list-agents, --agent <name>) • How to create custom workspace agents Behavior 1. Workspace agents take priority - If agents/<name>.md exists in the workspace, it's used 2. Embedded fallback - If no workspace agent exists, the embedded version is used 3. Portability - g3 binary now works on any repo without needing the agents/ directory 4. Discoverability - g3 --list-agents shows all available agents and their source
This commit is contained in:
@@ -9,6 +9,7 @@ use g3_core::Agent;
|
||||
|
||||
use crate::project_files::{combine_project_content, read_agents_config, read_project_memory, read_project_readme};
|
||||
use crate::simple_output::SimpleOutput;
|
||||
use crate::embedded_agents::load_agent_prompt;
|
||||
use crate::ui_writer_impl::ConsoleUiWriter;
|
||||
|
||||
/// Run agent mode - loads a specialized agent prompt and executes a single task.
|
||||
@@ -71,53 +72,17 @@ pub async fn run_agent_mode(
|
||||
output.print("");
|
||||
}
|
||||
|
||||
// Load agent prompt from agents/<name>.md
|
||||
let agent_prompt_path = workspace_dir
|
||||
.join("agents")
|
||||
.join(format!("{}.md", agent_name));
|
||||
// Load agent prompt: workspace agents/<name>.md first, then embedded fallback
|
||||
let (agent_prompt, from_disk) = load_agent_prompt(agent_name, &workspace_dir).ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Agent '{}' not found.\nAvailable embedded agents: breaker, carmack, euler, fowler, hopper, lamport, scout\nOr create agents/{}.md in your workspace.",
|
||||
agent_name,
|
||||
agent_name
|
||||
)
|
||||
})?;
|
||||
|
||||
// Also check in the g3 installation directory
|
||||
let agent_prompt = if agent_prompt_path.exists() {
|
||||
std::fs::read_to_string(&agent_prompt_path).map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"Failed to read agent prompt from {:?}: {}",
|
||||
agent_prompt_path,
|
||||
e
|
||||
)
|
||||
})?
|
||||
} else {
|
||||
// Try to find agents/ relative to the executable or in common locations
|
||||
let exe_dir = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| p.parent().map(|p| p.to_path_buf()));
|
||||
|
||||
let possible_paths = [
|
||||
exe_dir
|
||||
.as_ref()
|
||||
.map(|d| d.join("agents").join(format!("{}.md", agent_name))),
|
||||
Some(PathBuf::from(format!("agents/{}.md", agent_name))),
|
||||
];
|
||||
|
||||
let mut found_prompt = None;
|
||||
for path_opt in possible_paths.iter().flatten() {
|
||||
if path_opt.exists() {
|
||||
found_prompt = Some(std::fs::read_to_string(path_opt).map_err(|e| {
|
||||
anyhow::anyhow!("Failed to read agent prompt from {:?}: {}", path_opt, e)
|
||||
})?);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
found_prompt.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Agent prompt not found: agents/{}.md\nSearched in: {:?} and current directory",
|
||||
agent_name,
|
||||
agent_prompt_path
|
||||
)
|
||||
})?
|
||||
};
|
||||
|
||||
output.print(&format!(">> agent mode | {}", agent_name));
|
||||
let source = if from_disk { "workspace" } else { "embedded" };
|
||||
output.print(&format!(">> agent mode | {} ({})", agent_name, source));
|
||||
// Format workspace path, replacing home dir with ~
|
||||
let workspace_display = {
|
||||
let path_str = workspace_dir.display().to_string();
|
||||
|
||||
@@ -99,6 +99,10 @@ pub struct Cli {
|
||||
#[arg(long, value_name = "NAME", conflicts_with_all = ["autonomous", "auto", "chat", "planning"])]
|
||||
pub agent: Option<String>,
|
||||
|
||||
/// List all available agents (embedded and workspace)
|
||||
#[arg(long)]
|
||||
pub list_agents: bool,
|
||||
|
||||
/// Skip session resumption and force a new session (for agent mode)
|
||||
#[arg(long)]
|
||||
pub new_session: bool,
|
||||
|
||||
115
crates/g3-cli/src/embedded_agents.rs
Normal file
115
crates/g3-cli/src/embedded_agents.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
//! Embedded agent prompts - compiled into the binary for portability.
|
||||
//!
|
||||
//! Agent prompts are embedded at compile time using `include_str!`.
|
||||
//! This allows g3 to run on any repository without needing the agents/ directory.
|
||||
//!
|
||||
//! Priority order for loading agent prompts:
|
||||
//! 1. Workspace `agents/<name>.md` (allows per-project customization)
|
||||
//! 2. Embedded prompts (fallback, always available)
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
/// Embedded agent prompts, keyed by agent name.
|
||||
static EMBEDDED_AGENTS: &[(&str, &str)] = &[
|
||||
("breaker", include_str!("../../../agents/breaker.md")),
|
||||
("carmack", include_str!("../../../agents/carmack.md")),
|
||||
("euler", include_str!("../../../agents/euler.md")),
|
||||
("fowler", include_str!("../../../agents/fowler.md")),
|
||||
("hopper", include_str!("../../../agents/hopper.md")),
|
||||
("lamport", include_str!("../../../agents/lamport.md")),
|
||||
("scout", include_str!("../../../agents/scout.md")),
|
||||
];
|
||||
|
||||
/// Get an embedded agent prompt by name.
|
||||
pub fn get_embedded_agent(name: &str) -> Option<&'static str> {
|
||||
EMBEDDED_AGENTS
|
||||
.iter()
|
||||
.find(|(n, _)| *n == name)
|
||||
.map(|(_, content)| *content)
|
||||
}
|
||||
|
||||
/// Get all available embedded agent names.
|
||||
pub fn list_embedded_agents() -> Vec<&'static str> {
|
||||
EMBEDDED_AGENTS.iter().map(|(name, _)| *name).collect()
|
||||
}
|
||||
|
||||
/// Load an agent prompt, checking workspace first, then falling back to embedded.
|
||||
///
|
||||
/// Returns the prompt content and a boolean indicating if it was loaded from disk (true)
|
||||
/// or embedded (false).
|
||||
pub fn load_agent_prompt(name: &str, workspace_dir: &Path) -> Option<(String, bool)> {
|
||||
// First, try workspace agents/<name>.md
|
||||
let workspace_path = workspace_dir.join("agents").join(format!("{}.md", name));
|
||||
if workspace_path.exists() {
|
||||
if let Ok(content) = std::fs::read_to_string(&workspace_path) {
|
||||
return Some((content, true));
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to embedded prompt
|
||||
get_embedded_agent(name).map(|content| (content.to_string(), false))
|
||||
}
|
||||
|
||||
/// Get a map of all available agents (both embedded and from workspace).
|
||||
pub fn get_available_agents(workspace_dir: &Path) -> HashMap<String, bool> {
|
||||
let mut agents = HashMap::new();
|
||||
|
||||
// Add all embedded agents
|
||||
for name in list_embedded_agents() {
|
||||
agents.insert(name.to_string(), false); // false = embedded
|
||||
}
|
||||
|
||||
// Check for workspace agents (these override embedded)
|
||||
let agents_dir = workspace_dir.join("agents");
|
||||
if agents_dir.is_dir() {
|
||||
if let Ok(entries) = std::fs::read_dir(&agents_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().map_or(false, |ext| ext == "md") {
|
||||
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
|
||||
agents.insert(stem.to_string(), true); // true = from disk
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
agents
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_embedded_agents_exist() {
|
||||
// Verify all expected agents are embedded
|
||||
let expected = ["breaker", "carmack", "euler", "fowler", "hopper", "lamport", "scout"];
|
||||
for name in expected {
|
||||
assert!(
|
||||
get_embedded_agent(name).is_some(),
|
||||
"Agent '{}' should be embedded",
|
||||
name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_embedded_agents() {
|
||||
let agents = list_embedded_agents();
|
||||
assert!(agents.len() >= 7, "Should have at least 7 embedded agents");
|
||||
assert!(agents.contains(&"carmack"));
|
||||
assert!(agents.contains(&"hopper"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_embedded_agent_content() {
|
||||
// Verify the content looks reasonable
|
||||
let carmack = get_embedded_agent("carmack").unwrap();
|
||||
assert!(carmack.contains("Carmack"), "Carmack prompt should mention Carmack");
|
||||
|
||||
let hopper = get_embedded_agent("hopper").unwrap();
|
||||
assert!(hopper.contains("Hopper"), "Hopper prompt should mention Hopper");
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ pub mod filter_json;
|
||||
pub mod metrics;
|
||||
pub mod project_files;
|
||||
pub mod streaming_markdown;
|
||||
pub mod embedded_agents;
|
||||
|
||||
mod accumulative;
|
||||
mod agent_mode;
|
||||
@@ -44,6 +45,22 @@ pub async fn run() -> Result<()> {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Check if --list-agents was requested
|
||||
if cli.list_agents {
|
||||
let workspace_dir = cli.workspace.clone().unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
|
||||
let agents = embedded_agents::get_available_agents(&workspace_dir);
|
||||
println!("Available agents:");
|
||||
let mut names: Vec<_> = agents.keys().collect();
|
||||
names.sort();
|
||||
for name in names {
|
||||
let source = if agents[name] { "workspace" } else { "embedded" };
|
||||
println!(" {} ({})", name, source);
|
||||
}
|
||||
println!("\nUse: g3 --agent <name> [task]");
|
||||
println!("Workspace agents override embedded agents with the same name.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check if planning mode is enabled
|
||||
if cli.planning {
|
||||
let codepath = cli.codepath.clone();
|
||||
|
||||
Reference in New Issue
Block a user