Files
g3/crates/g3-cli/src/autonomous.rs
Dhanji R. Prasanna f10374c925 Remove machine mode entirely from g3
- Delete machine_ui_writer.rs
- Remove --machine CLI flag from cli_args.rs
- Remove run_machine_mode(), run_interactive_machine(), run_autonomous_machine() functions
- Remove handle_machine_command() function
- Simplify OutputMode enum to just use SimpleOutput directly
- Simplify SimpleOutput struct (remove machine_mode field)
- Remove machine_mode parameter from setup_workspace_directory()
- Remove test_machine_option_accepted test
- Disable ACD by default in agent_mode (requires --acd flag)
- Change 'memory checkpoint' message formatting
- Remove dehydration status message
2026-01-12 06:01:31 +05:30

731 lines
25 KiB
Rust

//! Autonomous mode for G3 CLI - coach-player feedback loop.
use anyhow::Result;
use sha2::{Digest, Sha256};
use std::path::PathBuf;
use std::time::Instant;
use tracing::debug;
use g3_core::error_handling::{classify_error, ErrorType, RecoverableError};
use g3_core::project::Project;
use g3_core::{Agent, DiscoveryOptions};
use crate::coach_feedback;
use crate::metrics::{format_elapsed_time, generate_turn_histogram, TurnMetrics};
use crate::simple_output::SimpleOutput;
use crate::ui_writer_impl::ConsoleUiWriter;
/// Run autonomous mode with coach-player feedback loop (console output).
pub async fn run_autonomous(
mut agent: Agent<ConsoleUiWriter>,
project: Project,
show_prompt: bool,
show_code: bool,
max_turns: usize,
quiet: bool,
codebase_fast_start: Option<PathBuf>,
) -> Result<Agent<ConsoleUiWriter>> {
let start_time = std::time::Instant::now();
let output = SimpleOutput::new();
let mut turn_metrics: Vec<TurnMetrics> = Vec::new();
output.print("g3 programming agent - autonomous mode");
output.print(&format!(
"📁 Using workspace: {}",
project.workspace().display()
));
// Check if requirements exist
if !project.has_requirements() {
print_no_requirements_error(&output, &agent, &turn_metrics, start_time, max_turns);
return Ok(agent);
}
// Read requirements
let requirements = match project.read_requirements()? {
Some(content) => content,
None => {
print_cannot_read_requirements_error(
&output,
&agent,
&turn_metrics,
start_time,
max_turns,
);
return Ok(agent);
}
};
// Display appropriate message based on requirements source
if project.requirements_text.is_some() {
output.print("📋 Requirements loaded from --requirements flag");
} else {
output.print("📋 Requirements loaded from requirements.md");
}
// Calculate SHA256 of requirements
let mut hasher = Sha256::new();
hasher.update(requirements.as_bytes());
let requirements_sha = hex::encode(hasher.finalize());
output.print(&format!("🔒 Requirements SHA256: {}", requirements_sha));
// Pass SHA to agent for staleness checking
agent.set_requirements_sha(requirements_sha.clone());
let loop_start = Instant::now();
output.print("🔄 Starting coach-player feedback loop...");
// Load fast-discovery messages before the loop starts (if enabled)
let (discovery_messages, discovery_working_dir) =
load_discovery_messages(&agent, &output, &codebase_fast_start, &requirements).await;
let has_discovery = !discovery_messages.is_empty();
let mut turn = 1;
let mut coach_feedback_text = String::new();
let mut implementation_approved = false;
loop {
let turn_start_time = Instant::now();
let turn_start_tokens = agent.get_context_window().used_tokens;
output.print(&format!(
"\n=== TURN {}/{} - PLAYER MODE ===",
turn, max_turns
));
// Surface provider info for player agent
agent.print_provider_banner("Player");
// Player mode: implement requirements (with coach feedback if available)
let player_prompt = build_player_prompt(&requirements, &requirements_sha, &coach_feedback_text);
output.print(&format!(
"🎯 Starting player implementation... (elapsed: {})",
format_elapsed_time(loop_start.elapsed())
));
// Display what feedback the player is receiving
if coach_feedback_text.is_empty() {
if turn > 1 {
return Err(anyhow::anyhow!(
"Player mode error: No coach feedback received on turn {}",
turn
));
}
output.print("📋 Player starting initial implementation (no prior coach feedback)");
} else {
output.print(&format!(
"📋 Player received coach feedback ({} chars):",
coach_feedback_text.len()
));
output.print(&coach_feedback_text);
}
output.print(""); // Empty line for readability
// Execute player task with retry on error
let player_result = execute_player_turn(
&mut agent,
&player_prompt,
show_prompt,
show_code,
&output,
has_discovery,
&discovery_messages,
discovery_working_dir.as_deref(),
turn,
&turn_metrics,
start_time,
max_turns,
)
.await;
let player_failed = match player_result {
PlayerTurnResult::Success => false,
PlayerTurnResult::Failed => true,
PlayerTurnResult::Panic(e) => return Err(e),
};
// If player failed after max retries, increment turn and continue
if player_failed {
output.print(&format!(
"⚠️ Player turn {} failed after max retries. Moving to next turn.",
turn
));
record_turn_metrics(
&mut turn_metrics,
turn,
turn_start_time,
turn_start_tokens,
&agent,
);
turn += 1;
if turn > max_turns {
output.print("\n=== SESSION COMPLETED - MAX TURNS REACHED ===");
output.print(&format!("⏰ Maximum turns ({}) reached", max_turns));
break;
}
coach_feedback_text = String::new();
continue;
}
// Give some time for file operations to complete
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
// Execute coach turn
let coach_result = execute_coach_turn(
&agent,
&project,
&requirements,
show_prompt,
show_code,
quiet,
&output,
has_discovery,
&discovery_messages,
discovery_working_dir.as_deref(),
turn,
max_turns,
&turn_metrics,
start_time,
loop_start,
)
.await;
match coach_result {
CoachTurnResult::Approved => {
output.print("\n=== SESSION COMPLETED - IMPLEMENTATION APPROVED ===");
output.print("✅ Coach approved the implementation!");
implementation_approved = true;
break;
}
CoachTurnResult::Feedback(feedback) => {
output.print_smart(&format!("Coach feedback:\n{}", feedback));
coach_feedback_text = feedback;
}
CoachTurnResult::Failed => {
output.print(&format!(
"⚠️ Coach turn {} failed after max retries. Using default feedback.",
turn
));
coach_feedback_text = "The implementation needs review. Please ensure all requirements are met and the code compiles without errors.".to_string();
}
CoachTurnResult::Panic(e) => return Err(e),
}
// Check if we've reached max turns
if turn >= max_turns {
output.print("\n=== SESSION COMPLETED - MAX TURNS REACHED ===");
output.print(&format!("⏰ Maximum turns ({}) reached", max_turns));
break;
}
record_turn_metrics(
&mut turn_metrics,
turn,
turn_start_time,
turn_start_tokens,
&agent,
);
turn += 1;
output.print("🔄 Coach provided feedback for next iteration");
}
// Generate final report
print_final_report(
&output,
&agent,
&turn_metrics,
start_time,
turn,
max_turns,
implementation_approved,
);
if implementation_approved {
output.print(&format!(
"\n🎉 Autonomous mode completed successfully (total loop time: {})",
format_elapsed_time(loop_start.elapsed())
));
} else {
output.print(&format!(
"\n🔄 Autonomous mode terminated (max iterations) (total loop time: {})",
format_elapsed_time(loop_start.elapsed())
));
}
// Save session continuation for resume capability
agent.save_session_continuation(None);
Ok(agent)
}
// --- Helper types and functions ---
enum PlayerTurnResult {
Success,
Failed,
Panic(anyhow::Error),
}
enum CoachTurnResult {
Approved,
Feedback(String),
Failed,
Panic(anyhow::Error),
}
fn build_player_prompt(requirements: &str, requirements_sha: &str, coach_feedback: &str) -> String {
if coach_feedback.is_empty() {
format!(
"You are G3 in implementation mode. Read and implement the following requirements:\n\n{}\n\nRequirements SHA256: {}\n\nImplement this step by step, creating all necessary files and code.",
requirements, requirements_sha
)
} else {
format!(
"You are G3 in implementation mode. Address the following specific feedback from the coach:\n\n{}\n\nContext: You are improving an implementation based on these requirements:\n{}\n\nFocus on fixing the issues mentioned in the coach feedback above.",
coach_feedback, requirements
)
}
}
fn build_coach_prompt(requirements: &str) -> String {
format!(
"You are G3 in coach mode. Your role is to critique and review implementations against requirements and provide concise, actionable feedback.
REQUIREMENTS:
{}
IMPLEMENTATION REVIEW:
Review the current state of the project and provide a concise critique focusing on:
1. Whether the requirements are correctly implemented
2. Whether the project compiles successfully
3. What requirements are missing or incorrect
4. Specific improvements needed to satisfy requirements
5. Use UI tools such as webdriver to test functionality thoroughly
CRITICAL INSTRUCTIONS:
1. Provide your feedback as your final response message
2. Your feedback should be CONCISE and ACTIONABLE
3. Focus ONLY on what needs to be fixed or improved
4. Do NOT include your analysis process, file contents, or compilation output in your final feedback
If the implementation thoroughly meets all requirements, compiles and is fully tested (especially UI flows) *WITHOUT* minor gaps or errors:
- Respond with: 'IMPLEMENTATION_APPROVED'
If improvements are needed:
- Respond with a brief summary listing ONLY the specific issues to fix
Remember: Be clear in your review and concise in your feedback. APPROVE iff the implementation works and thoroughly fits the requirements (implementation > 95% complete). Be rigorous, especially by testing that all UI features work.",
requirements
)
}
async fn load_discovery_messages(
agent: &Agent<ConsoleUiWriter>,
output: &SimpleOutput,
codebase_fast_start: &Option<PathBuf>,
requirements: &str,
) -> (Vec<g3_providers::Message>, Option<String>) {
if let Some(ref codebase_path) = codebase_fast_start {
let canonical_path = codebase_path
.canonicalize()
.unwrap_or_else(|_| codebase_path.clone());
let path_str = canonical_path.to_string_lossy();
output.print(&format!(
"🔍 Fast-discovery mode: will explore codebase at {}",
path_str
));
match agent.get_provider() {
Ok(provider) => {
let output_clone = output.clone();
let status_callback: g3_planner::StatusCallback = Box::new(move |msg: &str| {
output_clone.print(msg);
});
match g3_planner::get_initial_discovery_messages(
&path_str,
Some(requirements),
provider,
Some(&status_callback),
)
.await
{
Ok(messages) => (messages, Some(path_str.to_string())),
Err(e) => {
output.print(&format!(
"⚠️ LLM discovery failed: {}, skipping fast-start",
e
));
(Vec::new(), None)
}
}
}
Err(e) => {
output.print(&format!(
"⚠️ Could not get provider: {}, skipping fast-start",
e
));
(Vec::new(), None)
}
}
} else {
(Vec::new(), None)
}
}
async fn execute_player_turn(
agent: &mut Agent<ConsoleUiWriter>,
player_prompt: &str,
show_prompt: bool,
show_code: bool,
output: &SimpleOutput,
has_discovery: bool,
discovery_messages: &[g3_providers::Message],
discovery_working_dir: Option<&str>,
turn: usize,
turn_metrics: &[TurnMetrics],
start_time: Instant,
max_turns: usize,
) -> PlayerTurnResult {
const MAX_PLAYER_RETRIES: u32 = 3;
let mut retry_count = 0;
loop {
let discovery_opts = if has_discovery {
Some(DiscoveryOptions {
messages: discovery_messages,
fast_start_path: discovery_working_dir,
})
} else {
None
};
match agent
.execute_task_with_timing(
player_prompt,
None,
false,
show_prompt,
show_code,
true,
discovery_opts,
)
.await
{
Ok(result) => {
output.print("📝 Player implementation completed:");
output.print_smart(&result.response);
return PlayerTurnResult::Success;
}
Err(e) => {
let error_type = classify_error(&e);
if matches!(
error_type,
ErrorType::Recoverable(RecoverableError::ContextLengthExceeded)
) {
output.print(&format!("⚠️ Context length exceeded in player turn: {}", e));
output.print("📝 Logging error to session and ending current turn...");
let forensic_context = format!(
"Turn: {}\nRole: Player\nContext tokens: {}\nTotal available: {}\nPercentage used: {:.1}%\nPrompt length: {} chars\nError occurred at: {}",
turn,
agent.get_context_window().used_tokens,
agent.get_context_window().total_tokens,
agent.get_context_window().percentage_used(),
player_prompt.len(),
chrono::Utc::now().to_rfc3339()
);
agent.log_error_to_session(&e, "assistant", Some(forensic_context));
return PlayerTurnResult::Failed;
} else if e.to_string().contains("panic") {
output.print(&format!("💥 Player panic detected: {}", e));
print_panic_report(output, agent, turn_metrics, start_time, turn, max_turns, "PLAYER PANIC");
return PlayerTurnResult::Panic(e);
}
retry_count += 1;
output.print(&format!(
"⚠️ Player error (attempt {}/{}): {}",
retry_count, MAX_PLAYER_RETRIES, e
));
if retry_count >= MAX_PLAYER_RETRIES {
output.print("🔄 Max retries reached for player, marking turn as failed...");
return PlayerTurnResult::Failed;
}
output.print("🔄 Retrying player implementation...");
}
}
}
}
async fn execute_coach_turn(
player_agent: &Agent<ConsoleUiWriter>,
project: &Project,
requirements: &str,
show_prompt: bool,
show_code: bool,
quiet: bool,
output: &SimpleOutput,
has_discovery: bool,
discovery_messages: &[g3_providers::Message],
discovery_working_dir: Option<&str>,
turn: usize,
max_turns: usize,
turn_metrics: &[TurnMetrics],
start_time: Instant,
loop_start: Instant,
) -> CoachTurnResult {
const MAX_COACH_RETRIES: u32 = 3;
// Create a new agent instance for coach mode to ensure fresh context
let base_config = player_agent.get_config().clone();
let coach_config = match base_config.for_coach() {
Ok(c) => c,
Err(e) => return CoachTurnResult::Panic(e),
};
// Reset filter suppression state before creating coach agent
crate::filter_json::reset_json_tool_state();
let ui_writer = ConsoleUiWriter::new();
let mut coach_agent =
match Agent::new_autonomous_with_readme_and_quiet(coach_config, ui_writer, None, quiet)
.await
{
Ok(a) => a,
Err(e) => return CoachTurnResult::Panic(e),
};
coach_agent.print_provider_banner("Coach");
if let Err(e) = project.enter_workspace() {
return CoachTurnResult::Panic(e);
}
output.print(&format!(
"\n=== TURN {}/{} - COACH MODE ===",
turn, max_turns
));
let coach_prompt = build_coach_prompt(requirements);
output.print(&format!(
"🎓 Starting coach review... (elapsed: {})",
format_elapsed_time(loop_start.elapsed())
));
let mut retry_count = 0;
loop {
let discovery_opts = if has_discovery {
Some(DiscoveryOptions {
messages: discovery_messages,
fast_start_path: discovery_working_dir,
})
} else {
None
};
match coach_agent
.execute_task_with_timing(
&coach_prompt,
None,
false,
show_prompt,
show_code,
true,
discovery_opts,
)
.await
{
Ok(result) => {
output.print("🎓 Coach review completed");
let feedback_text =
match coach_feedback::extract_from_logs(&result, &coach_agent, output) {
Ok(f) => f,
Err(e) => return CoachTurnResult::Panic(e),
};
debug!(
"Coach feedback extracted: {} characters (from {} total)",
feedback_text.len(),
result.response.len()
);
if feedback_text.is_empty() {
output.print("⚠️ Coach did not provide feedback. This may be a model issue.");
return CoachTurnResult::Failed;
}
if result.is_approved() || feedback_text.contains("IMPLEMENTATION_APPROVED") {
return CoachTurnResult::Approved;
}
return CoachTurnResult::Feedback(feedback_text);
}
Err(e) => {
let error_type = classify_error(&e);
if matches!(
error_type,
ErrorType::Recoverable(RecoverableError::ContextLengthExceeded)
) {
output.print(&format!("⚠️ Context length exceeded in coach turn: {}", e));
output.print("📝 Logging error to session and ending current turn...");
let forensic_context = format!(
"Turn: {}\nRole: Coach\nContext tokens: {}\nTotal available: {}\nPercentage used: {:.1}%\nPrompt length: {} chars\nError occurred at: {}",
turn,
coach_agent.get_context_window().used_tokens,
coach_agent.get_context_window().total_tokens,
coach_agent.get_context_window().percentage_used(),
coach_prompt.len(),
chrono::Utc::now().to_rfc3339()
);
coach_agent.log_error_to_session(&e, "assistant", Some(forensic_context));
return CoachTurnResult::Failed;
} else if e.to_string().contains("panic") {
output.print(&format!("💥 Coach panic detected: {}", e));
print_panic_report(output, player_agent, turn_metrics, start_time, turn, max_turns, "COACH PANIC");
return CoachTurnResult::Panic(e);
}
retry_count += 1;
output.print(&format!(
"⚠️ Coach error (attempt {}/{}): {}",
retry_count, MAX_COACH_RETRIES, e
));
if retry_count >= MAX_COACH_RETRIES {
output.print("🔄 Max retries reached for coach, using default feedback...");
return CoachTurnResult::Failed;
}
output.print("🔄 Retrying coach review...");
}
}
}
}
fn record_turn_metrics(
turn_metrics: &mut Vec<TurnMetrics>,
turn: usize,
turn_start_time: Instant,
turn_start_tokens: u32,
agent: &Agent<ConsoleUiWriter>,
) {
let turn_duration = turn_start_time.elapsed();
let turn_tokens = agent
.get_context_window()
.used_tokens
.saturating_sub(turn_start_tokens);
turn_metrics.push(TurnMetrics {
turn_number: turn,
tokens_used: turn_tokens,
wall_clock_time: turn_duration,
});
}
fn print_no_requirements_error(
output: &SimpleOutput,
agent: &Agent<ConsoleUiWriter>,
turn_metrics: &[TurnMetrics],
start_time: Instant,
max_turns: usize,
) {
output.print("❌ Error: requirements.md not found in workspace directory");
output.print(" Please either:");
output.print(" 1. Create a requirements.md file with your project requirements");
output.print(" 2. Or use the --requirements flag to provide requirements text directly:");
output.print(" g3 --autonomous --requirements \"Your requirements here\"");
output.print("");
print_final_report(output, agent, turn_metrics, start_time, 0, max_turns, false);
}
fn print_cannot_read_requirements_error(
output: &SimpleOutput,
agent: &Agent<ConsoleUiWriter>,
turn_metrics: &[TurnMetrics],
start_time: Instant,
max_turns: usize,
) {
output.print("❌ Error: Could not read requirements (neither --requirements flag nor requirements.md file provided)");
print_final_report(output, agent, turn_metrics, start_time, 0, max_turns, false);
}
fn print_panic_report(
output: &SimpleOutput,
agent: &Agent<ConsoleUiWriter>,
turn_metrics: &[TurnMetrics],
start_time: Instant,
turn: usize,
max_turns: usize,
status: &str,
) {
let elapsed = start_time.elapsed();
let context_window = agent.get_context_window();
output.print(&format!("\n{}", "=".repeat(60)));
output.print("📊 AUTONOMOUS MODE SESSION REPORT");
output.print(&"=".repeat(60));
output.print(&format!("⏱️ Total Duration: {:.2}s", elapsed.as_secs_f64()));
output.print(&format!("🔄 Turns Taken: {}/{}", turn, max_turns));
output.print(&format!("📝 Final Status: 💥 {}", status));
output.print("\n📈 Token Usage Statistics:");
output.print(&format!(" • Used Tokens: {}", context_window.used_tokens));
output.print(&format!(" • Total Available: {}", context_window.total_tokens));
output.print(&format!(" • Cumulative Tokens: {}", context_window.cumulative_tokens));
output.print(&format!(" • Usage Percentage: {:.1}%", context_window.percentage_used()));
output.print(&generate_turn_histogram(turn_metrics));
output.print(&"=".repeat(60));
}
fn print_final_report(
output: &SimpleOutput,
agent: &Agent<ConsoleUiWriter>,
turn_metrics: &[TurnMetrics],
start_time: Instant,
turn: usize,
max_turns: usize,
implementation_approved: bool,
) {
let elapsed = start_time.elapsed();
let context_window = agent.get_context_window();
output.print(&format!("\n{}", "=".repeat(60)));
output.print("📊 AUTONOMOUS MODE SESSION REPORT");
output.print(&"=".repeat(60));
output.print(&format!("⏱️ Total Duration: {:.2}s", elapsed.as_secs_f64()));
output.print(&format!("🔄 Turns Taken: {}/{}", turn, max_turns));
output.print(&format!(
"📝 Final Status: {}",
if implementation_approved {
"✅ APPROVED"
} else if turn >= max_turns {
"⏰ MAX TURNS REACHED"
} else {
"⚠️ INCOMPLETE"
}
));
output.print("\n📈 Token Usage Statistics:");
output.print(&format!(" • Used Tokens: {}", context_window.used_tokens));
output.print(&format!(" • Total Available: {}", context_window.total_tokens));
output.print(&format!(" • Cumulative Tokens: {}", context_window.cumulative_tokens));
output.print(&format!(" • Usage Percentage: {:.1}%", context_window.percentage_used()));
output.print(&generate_turn_histogram(turn_metrics));
output.print(&"=".repeat(60));
}