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:
Dhanji R. Prasanna
2026-01-14 16:27:03 +05:30
parent 5104bd53b6
commit 03143ec7f8
5 changed files with 182 additions and 46 deletions

View File

@@ -322,6 +322,41 @@ G3 automatically saves session logs for each interaction in the `.g3/sessions/`
The `.g3/` directory is created automatically on first use and is excluded from version control. The `.g3/` directory is created automatically on first use and is excluded from version control.
## Agent Mode
Agent mode runs specialized AI agents with custom prompts tailored for specific tasks. Each agent has a distinct personality and focus area.
### Built-in Agents
g3 comes with several embedded agents that work out of the box:
| Agent | Focus |
|-------|-------|
| **carmack** | Code readability and craft - simplifies, refactors, improves naming |
| **hopper** | Testing and quality - writes tests, finds edge cases |
| **euler** | Architecture and dependencies - analyzes structure, finds coupling |
| **lamport** | Concurrency and correctness - reviews async code, finds race conditions |
| **fowler** | Refactoring patterns - applies design patterns, reduces duplication |
| **breaker** | Adversarial testing - finds bugs, creates minimal repros |
| **scout** | Research - investigates APIs, libraries, approaches |
### Usage
```bash
# List all available agents
g3 --list-agents
# Run an agent on the current project
g3 --agent carmack
# Run an agent with a specific task
g3 --agent hopper "add tests for the parser module"
```
### Custom Agents
Create custom agents by adding markdown files to `agents/<name>.md` in your workspace. Workspace agents override embedded agents with the same name, allowing per-project customization.
## Studio - Multi-Agent Workspace Manager ## Studio - Multi-Agent Workspace Manager
Studio is a companion tool for managing multiple g3 agent sessions using git worktrees. Each session runs in an isolated worktree with its own branch, allowing multiple agents to work on the same codebase without conflicts. Studio is a companion tool for managing multiple g3 agent sessions using git worktrees. Each session runs in an isolated worktree with its own branch, allowing multiple agents to work on the same codebase without conflicts.

View File

@@ -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::project_files::{combine_project_content, read_agents_config, read_project_memory, read_project_readme};
use crate::simple_output::SimpleOutput; use crate::simple_output::SimpleOutput;
use crate::embedded_agents::load_agent_prompt;
use crate::ui_writer_impl::ConsoleUiWriter; use crate::ui_writer_impl::ConsoleUiWriter;
/// Run agent mode - loads a specialized agent prompt and executes a single task. /// 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(""); output.print("");
} }
// Load agent prompt from agents/<name>.md // Load agent prompt: workspace agents/<name>.md first, then embedded fallback
let agent_prompt_path = workspace_dir let (agent_prompt, from_disk) = load_agent_prompt(agent_name, &workspace_dir).ok_or_else(|| {
.join("agents")
.join(format!("{}.md", 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!( anyhow::anyhow!(
"Failed to read agent prompt from {:?}: {}", "Agent '{}' not found.\nAvailable embedded agents: breaker, carmack, euler, fowler, hopper, lamport, scout\nOr create agents/{}.md in your workspace.",
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_name,
agent_prompt_path agent_name
) )
})? })?;
};
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 ~ // Format workspace path, replacing home dir with ~
let workspace_display = { let workspace_display = {
let path_str = workspace_dir.display().to_string(); let path_str = workspace_dir.display().to_string();

View File

@@ -99,6 +99,10 @@ pub struct Cli {
#[arg(long, value_name = "NAME", conflicts_with_all = ["autonomous", "auto", "chat", "planning"])] #[arg(long, value_name = "NAME", conflicts_with_all = ["autonomous", "auto", "chat", "planning"])]
pub agent: Option<String>, 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) /// Skip session resumption and force a new session (for agent mode)
#[arg(long)] #[arg(long)]
pub new_session: bool, pub new_session: bool,

View 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");
}
}

View File

@@ -4,6 +4,7 @@ pub mod filter_json;
pub mod metrics; pub mod metrics;
pub mod project_files; pub mod project_files;
pub mod streaming_markdown; pub mod streaming_markdown;
pub mod embedded_agents;
mod accumulative; mod accumulative;
mod agent_mode; mod agent_mode;
@@ -44,6 +45,22 @@ pub async fn run() -> Result<()> {
std::process::exit(1); 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 // Check if planning mode is enabled
if cli.planning { if cli.planning {
let codepath = cli.codepath.clone(); let codepath = cli.codepath.clone();