feat(exec): add timeboxed auto-exec guidance

This commit is contained in:
James Peter
2025-12-24 17:08:49 +10:00
parent 376fc8ffc0
commit 8dbfdbba50
6 changed files with 88 additions and 14 deletions
@@ -59,7 +59,7 @@ struct AutoCoordinatorCancelled;
pub const MODEL_SLUG: &str = "gpt-5.1";
const USER_TURN_SCHEMA_NAME: &str = "auto_coordinator_user_turn";
const COORDINATOR_PROMPT: &str = include_str!("../../core/prompt_coordinator.md");
const TIMEBOXED_FINISH_GUIDANCE: &str = "Time budget mode is enabled for this run. Strategy: (1) front-load parallel reconnaissance (agents if enabled; otherwise batch independent tool calls) to quickly identify the smallest reliable repro or failing test, (2) apply a focused fix, (3) verify with the narrowest check that proves correctness. Parallelize only cheap setup (searches/downloads/formatting) while long commands run; avoid launching multiple heavy builds at once. Always get one cheap proof before finish_success (rerun the repro, a targeted test, or a small build/compile check) and confirm required output files/binaries exist at the exact paths mentioned in the prompt/tests. Check exit codes from key commands and do not proceed on failure. Avoid full test suites unless required. If a rate-limit wait would consume most remaining time, summarize progress, note the next verification step, and use finish_failed instead of waiting indefinitely. Avoid repeated 'one more check' loops: do at most one final confirmation step, then use finish_success.";
const TIMEBOXED_EXEC_COORDINATOR_GUIDANCE: &str = "SYSTEM: Time-boxed autonomous exec is enabled.\n\nOperate contract-first and evidence-led. Steer the CLI to identify the single most trustworthy acceptance signal available (explicit success criteria/commands in the task description, a local verifier script, or the smallest relevant test). Treat that signal as the contract: exact paths, ports, formats, exit codes, and constraints.\n\nDrive convergence, not exploration. Get a failing signal early, ship a minimal first-pass fix quickly, then iterate in small diffs against the same check. Avoid speculative redesigns, broad refactors, or dependency churn unless the contract forces it.\n\nDelegate by outcomes only: each turn, provide one short, outcome-oriented directive (\"make X pass / produce Y artifact\"), and let the CLI decide tactics.\n\nTime discipline: prioritize cheap proofs before expensive runs (long builds, downloads, services). If verification is not available in the workspace, mirror the contract with a minimal self-check and keep changes conservative.\n\nFinish standard: finish_success only with proof (acceptance check green + required artifacts present exactly as specified). Otherwise finish_failed with the last check, the current error, what changed, and the next verification step.";
const ALL_TEXT_VERBOSITY: &[TextVerbosity] = &[
TextVerbosity::Low,
@@ -1642,7 +1642,7 @@ fn read_coordinator_prompt(config: &Config) -> Option<String> {
None
} else {
if config.max_run_seconds.is_some() {
Some(format!("{trimmed}\n\n# Time Budget\n{TIMEBOXED_FINISH_GUIDANCE}"))
Some(format!("{trimmed}\n\n# Time Budget\n{TIMEBOXED_EXEC_COORDINATOR_GUIDANCE}"))
} else {
Some(trimmed.to_string())
}
@@ -1698,10 +1698,6 @@ fn build_initial_planning_seed(
.to_string()
};
if time_budget_enabled {
cli_prompt.push_str(" Time budget is enabled: front-load parallel reconnaissance (agents/tool-call batching) and end with a lightweight verification step before declaring success.");
}
Some(InitialPlanningSeed {
response_json: format!(
"{{\"finish_status\":\"continue\",\"status_title\":\"Planning\",\"status_sent_to_user\":\"Started initial planning phase\",\"prompt_sent_to_cli\":\"{cli_prompt}\"}}"
+37 -1
View File
@@ -943,6 +943,10 @@ use crate::environment_context::{
};
use crate::user_instructions::UserInstructions;
use crate::config::{persist_model_selection, Config};
use crate::timeboxed_exec_guidance::{
AUTO_EXEC_TIMEBOXED_CLI_GUIDANCE,
AUTO_EXEC_TIMEBOXED_REVIEW_GUIDANCE,
};
use crate::config_types::ProjectHookEvent;
use crate::config_types::ShellEnvironmentPolicy;
use crate::conversation_history::ConversationHistory;
@@ -5470,6 +5474,32 @@ async fn submission_loop(
debug!("Agent loop exited");
}
fn merge_developer_message(existing: Option<String>, extra: &str) -> Option<String> {
let extra_trimmed = extra.trim();
if extra_trimmed.is_empty() {
return existing;
}
match existing {
Some(mut message) => {
if !message.trim().is_empty() {
message.push_str("\n\n");
}
message.push_str(extra_trimmed);
Some(message)
}
None => Some(extra_trimmed.to_string()),
}
}
fn build_timeboxed_review_message(base: Option<String>) -> Option<String> {
let mut message = merge_developer_message(base.clone(), AUTO_EXEC_TIMEBOXED_REVIEW_GUIDANCE);
if base.as_deref() == Some(AUTO_EXEC_TIMEBOXED_CLI_GUIDANCE) {
message = Some(AUTO_EXEC_TIMEBOXED_REVIEW_GUIDANCE.to_string());
}
message
}
async fn spawn_review_thread(
sess: Arc<Session>,
config: Arc<Config>,
@@ -5531,12 +5561,18 @@ async fn spawn_review_thread(
review_debug_logger,
);
let review_demo_message = if config.timeboxed_exec_mode {
build_timeboxed_review_message(parent_turn_context.demo_developer_message.clone())
} else {
parent_turn_context.demo_developer_message.clone()
};
let review_turn_context = Arc::new(TurnContext {
client: review_client,
cwd: parent_turn_context.cwd.clone(),
base_instructions: Some(REVIEW_PROMPT.to_string()),
user_instructions: None,
demo_developer_message: parent_turn_context.demo_developer_message.clone(),
demo_developer_message: review_demo_message,
compact_prompt_override: parent_turn_context.compact_prompt_override.clone(),
approval_policy: parent_turn_context.approval_policy,
sandbox_policy: parent_turn_context.sandbox_policy.clone(),
+6
View File
@@ -460,6 +460,11 @@ pub struct Config {
/// When present, countdown nudges are anchored to this deadline instead of
/// the session creation time so startup work doesn't delay warnings.
pub max_run_deadline: Option<Instant>,
/// True when exec is running Auto Drive with a max time budget.
///
/// Set by exec; not loaded from config.toml.
pub timeboxed_exec_mode: bool,
}
impl Config {
@@ -3145,6 +3150,7 @@ impl Config {
experimental_resume: cfg.experimental_resume,
max_run_seconds: None,
max_run_deadline: None,
timeboxed_exec_mode: false,
// Surface TUI notifications preference from config when present.
tui_notifications: cfg
.tui
+1
View File
@@ -39,6 +39,7 @@ mod environment_context;
mod reasoning;
pub mod retention;
pub mod telemetry;
pub mod timeboxed_exec_guidance;
pub use environment_context::BrowserSnapshot;
pub use environment_context::EnvironmentContextDelta;
pub use environment_context::EnvironmentContextEmission;
@@ -0,0 +1,5 @@
pub const AUTO_EXEC_TIMEBOXED_CLI_GUIDANCE: &str = "SYSTEM: Time-boxed autonomous exec is enabled.\n\nTreat the task statement as the spec. Identify the narrowest acceptance check (explicit command, local verifier, or targeted test) and run it to get a concrete failure signal.\n\nBias toward a working first draft over perfect design: implement the smallest safe fix, rerun the same check, and iterate. Keep diffs tight; avoid refactors and new dependencies unless required by the contract.\n\nMatch the contract literally (paths, ports, output formats, permissions/executability, non-interactive behavior, and any stated constraints like allowed libraries or \"do not modify\" inputs). Never mask failures (skipped checks, unconditional success output, \"|| true\", broad catch/ignore, or weakening tests just to go green).\n\nIf verifiers/tests are not available, do not stall; create a minimal self-check that mirrors the acceptance criteria (remove it unless it belongs in the permanent suite).\n\nBefore committing to expensive steps (long builds/services/downloads), get a cheap proof of viability (smoke build, quick run, reachability, minimal example). Before finishing, rerun the acceptance check end-to-end and briefly report what changed and what commands you ran.";
pub const AUTO_EXEC_TIMEBOXED_REVIEW_GUIDANCE: &str = "SYSTEM: Time-boxed exec review focus.\n\nReview for contract correctness, not style. Validate that the diff satisfies the task's explicit acceptance criteria (paths/ports/formats/commands/constraints) and would still pass under an external verifier or full test run.\n\nPrioritize catching timebox shortcuts: skipped checks, \"|| true\", unconditional success output, silent fallbacks, overly broad exception swallowing, hard-coded environment assumptions, or tests weakened to \"pass\" without fixing behavior. Also check for leftover debug scaffolding or temporary artifacts.\n\nFlag only discrete, introduced issues likely to break correctness, security, or maintainability.";
pub const AUTO_EXEC_TIMEBOXED_GOAL_SUFFIX: &str = "Completion gate: do not end as success without proof. Rerun the best available acceptance check for this goal and confirm exit 0, and confirm all required artifacts/paths/ports/output formats match the spec exactly. If proof is not achievable, end as failed and report the last check, the error, and the single next verification step.";
+37 -7
View File
@@ -79,8 +79,10 @@ use code_auto_drive_core::AutoResolveState;
use code_core::{entry_to_rollout_path, AutoDriveMode, AutoDrivePidFile, SessionCatalog, SessionQuery};
use code_core::protocol::SandboxPolicy;
use code_core::git_info::current_branch_name;
const AUTO_DRIVE_TEST_SUFFIX: &str = "After planning, but before you start, please ensure you can test the outcome of your changes. Test first to ensure it's failing, then again at the end to ensure it passes. Do not use work arounds or mock code to pass - solve the underlying issue. Create new tests as you work if needed. Once done, clean up your tests unless added to an existing test suite.";
use code_core::timeboxed_exec_guidance::{
AUTO_EXEC_TIMEBOXED_CLI_GUIDANCE,
AUTO_EXEC_TIMEBOXED_GOAL_SUFFIX,
};
/// How long exec waits after task completion before sending Shutdown when Auto Review
/// may be about to start. Guarded so sub-agents are not delayed.
@@ -190,8 +192,11 @@ pub async fn run_main(cli: Cli, code_linux_sandbox_exe: Option<PathBuf>) -> anyh
std::process::exit(1);
}
if let Some(goal) = auto_drive_goal.as_mut() {
*goal = append_auto_drive_test_suffix(goal);
let timeboxed_auto_exec = auto_drive_goal.is_some() && max_seconds.is_some();
if timeboxed_auto_exec {
if let Some(goal) = auto_drive_goal.as_mut() {
*goal = append_timeboxed_auto_drive_goal(goal);
}
}
let mut prompt_to_send = prompt.clone();
@@ -290,6 +295,13 @@ pub async fn run_main(cli: Cli, code_linux_sandbox_exe: Option<PathBuf>) -> anyh
config.max_run_seconds = max_seconds;
config.max_run_deadline = run_deadline_std;
config.demo_developer_message = cli.demo_developer_message.clone();
config.timeboxed_exec_mode = timeboxed_auto_exec;
if timeboxed_auto_exec {
config.demo_developer_message = merge_developer_message(
config.demo_developer_message.take(),
AUTO_EXEC_TIMEBOXED_CLI_GUIDANCE,
);
}
if auto_drive_goal.is_some() {
// Exec is non-interactive; don't burn time on countdown delays between Auto Drive turns.
config.auto_drive.continue_mode = AutoDriveContinueMode::Immediate;
@@ -1640,13 +1652,31 @@ async fn run_auto_drive_session(
Ok(())
}
fn append_auto_drive_test_suffix(goal: &str) -> String {
fn append_timeboxed_auto_drive_goal(goal: &str) -> String {
let trimmed_goal = goal.trim();
if trimmed_goal.is_empty() {
return AUTO_DRIVE_TEST_SUFFIX.to_string();
return AUTO_EXEC_TIMEBOXED_GOAL_SUFFIX.to_string();
}
format!("{trimmed_goal}\n\n{AUTO_DRIVE_TEST_SUFFIX}")
format!("{trimmed_goal}\n\n{AUTO_EXEC_TIMEBOXED_GOAL_SUFFIX}")
}
fn merge_developer_message(existing: Option<String>, extra: &str) -> Option<String> {
let extra_trimmed = extra.trim();
if extra_trimmed.is_empty() {
return existing;
}
match existing {
Some(mut message) => {
if !message.trim().is_empty() {
message.push_str("\n\n");
}
message.push_str(extra_trimmed);
Some(message)
}
None => Some(extra_trimmed.to_string()),
}
}
#[derive(Default, Debug, Clone)]