feat(auto-drive): add per-turn model routing and schema rename

This commit is contained in:
James Peter
2026-02-14 16:31:45 +10:00
parent e4559ac4f9
commit fb1752750f
9 changed files with 533 additions and 71 deletions
@@ -54,6 +54,8 @@ const MESSAGE_LIMIT_FALLBACK: usize = 120;
const DEBUG_JSON_MAX_CHARS: usize = 1200;
const CLI_PROMPT_MIN_CHARS: usize = 4;
const CLI_PROMPT_MAX_CHARS: usize = 600;
const AUTO_DRIVE_CLI_MODELS: &[&str] = &["gpt-5.3-codex", "gpt-5.3-codex-spark"];
const AUTO_DRIVE_CLI_REASONING_LEVELS: &[&str] = &["medium", "high", "xhigh"];
#[derive(Debug, thiserror::Error)]
#[error("auto coordinator cancelled")]
@@ -185,6 +187,8 @@ pub struct AutoTurnCliAction {
pub prompt: String,
pub context: Option<String>,
pub suppress_ui_context: bool,
pub model_override: Option<String>,
pub reasoning_effort_override: Option<ReasoningEffort>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -473,8 +477,13 @@ mod tests {
"status_sent_to_user property missing"
);
assert!(
props.contains_key("prompt_sent_to_cli"),
"prompt_sent_to_cli property missing"
props.contains_key("cli_milestone_instruction"),
"cli_milestone_instruction property missing"
);
assert!(props.contains_key("cli_model"), "cli_model property missing");
assert!(
props.contains_key("cli_reasoning_effort"),
"cli_reasoning_effort property missing"
);
assert!(props.contains_key("agents"), "agents property missing");
assert!(!props.contains_key("code_review"));
@@ -490,7 +499,9 @@ mod tests {
assert!(schema_required.contains(&json!("goal")));
assert!(schema_required.contains(&json!("status_title")));
assert!(schema_required.contains(&json!("status_sent_to_user")));
assert!(schema_required.contains(&json!("prompt_sent_to_cli")));
assert!(schema_required.contains(&json!("cli_milestone_instruction")));
assert!(schema_required.contains(&json!("cli_model")));
assert!(schema_required.contains(&json!("cli_reasoning_effort")));
assert!(schema_required.contains(&json!("finish_evidence")));
assert!(schema_required.contains(&json!("agents")));
assert_eq!(
@@ -549,15 +560,15 @@ mod tests {
}
#[test]
fn schema_sets_prompt_sent_to_cli_min_without_max_length() {
fn schema_sets_cli_milestone_instruction_min_without_max_length() {
let active_agents: Vec<String> = Vec::new();
let schema = build_schema(&active_agents, SchemaFeatures::default());
let prompt_schema = schema
.get("properties")
.and_then(|v| v.as_object())
.and_then(|obj| obj.get("prompt_sent_to_cli"))
.and_then(|obj| obj.get("cli_milestone_instruction"))
.and_then(|v| v.as_object())
.expect("prompt_sent_to_cli schema");
.expect("cli_milestone_instruction schema");
assert_eq!(
prompt_schema.get("minLength"),
@@ -652,12 +663,39 @@ mod tests {
.and_then(|v| v.as_object())
.expect("schema properties");
assert!(!props.contains_key("agents"));
assert!(props.contains_key("cli_model"));
assert!(props.contains_key("cli_reasoning_effort"));
let required = schema
.get("required")
.and_then(|v| v.as_array())
.expect("required array");
assert!(!required.contains(&json!("agents")));
assert!(required.contains(&json!("goal")));
assert!(required.contains(&json!("cli_model")));
assert!(required.contains(&json!("cli_reasoning_effort")));
}
#[test]
fn schema_omits_cli_model_routing_when_disabled() {
let schema = build_schema(
&Vec::new(),
SchemaFeatures {
include_cli_model_routing: false,
..SchemaFeatures::default()
},
);
let props = schema
.get("properties")
.and_then(|v| v.as_object())
.expect("schema properties");
assert!(!props.contains_key("cli_model"));
assert!(!props.contains_key("cli_reasoning_effort"));
let required = schema
.get("required")
.and_then(|v| v.as_array())
.expect("required array");
assert!(!required.contains(&json!("cli_model")));
assert!(!required.contains(&json!("cli_reasoning_effort")));
}
#[test]
@@ -710,7 +748,7 @@ mod tests {
"finish_status": "continue",
"status_title": "Dispatching fix",
"status_sent_to_user": "Ran smoke tests while validating the fix.",
"prompt_sent_to_cli": "Apply the patch for the failing test",
"cli_milestone_instruction": "Apply the patch for the failing test",
"agents": {
"timing": "blocking",
"list": [
@@ -719,7 +757,8 @@ mod tests {
}
}"#;
let (decision, _) = parse_decision(raw).expect("parse new schema decision");
let (decision, _) = parse_decision(raw, DecisionParseOptions::default())
.expect("parse new schema decision");
assert_eq!(decision.status, AutoCoordinatorStatus::Continue);
assert_eq!(
decision.status_sent_to_user.as_deref(),
@@ -746,19 +785,64 @@ mod tests {
}
#[test]
fn parse_decision_requires_cli_model_and_reasoning_when_enabled() {
let raw = r#"{
"finish_status": "continue",
"status_title": "Dispatching fix",
"status_sent_to_user": "Running failing test loop.",
"cli_milestone_instruction": "Run the failing test, apply a minimal fix, and iterate until green."
}"#;
let err = parse_decision(
raw,
DecisionParseOptions {
require_cli_model_routing: true,
},
)
.expect_err("routing-enabled parse should require model fields");
assert!(err.to_string().contains("missing cli_model"));
}
#[test]
fn parse_decision_accepts_cli_model_and_reasoning_when_enabled() {
let raw = r#"{
"finish_status": "continue",
"status_title": "Fixing tests",
"status_sent_to_user": "Running clear failing-test loops.",
"cli_milestone_instruction": "Take the failing test from red to green and report the passing evidence.",
"cli_model": "gpt-5.3-codex-spark",
"cli_reasoning_effort": "high"
}"#;
let (decision, _) = parse_decision(
raw,
DecisionParseOptions {
require_cli_model_routing: true,
},
)
.expect("routing-enabled decision should parse");
let cli = decision.cli.expect("cli action expected");
assert_eq!(cli.model_override.as_deref(), Some("gpt-5.3-codex-spark"));
assert_eq!(cli.reasoning_effort_override, Some(ReasoningEffort::High));
}
#[test]
fn parse_decision_new_schema_array_backcompat() {
let raw = r#"{
"finish_status": "continue",
"status_title": "Running tests",
"status_sent_to_user": "Outlined fix before execution.",
"prompt_sent_to_cli": "Run cargo test",
"cli_milestone_instruction": "Run cargo test",
"agents": [
{"prompt": "Investigate benchmark", "write": false}
]
}"#;
let (decision, _) = parse_decision(raw).expect("parse array-style agents");
let (decision, _) = parse_decision(raw, DecisionParseOptions::default())
.expect("parse array-style agents");
assert_eq!(decision.status, AutoCoordinatorStatus::Continue);
assert!(decision.cli.is_some());
assert_eq!(decision.agents.len(), 1);
@@ -775,10 +859,11 @@ mod tests {
let raw = r#"{
"finish_status": "continue",
"progress": {"past": "Drafted fix", "current": "Running unit tests"},
"prompt_sent_to_cli": "Run cargo test --package core"
"cli_milestone_instruction": "Run cargo test --package core"
}"#;
let (decision, _) = parse_decision(raw).expect("parse legacy decision");
let (decision, _) = parse_decision(raw, DecisionParseOptions::default())
.expect("parse legacy decision");
assert_eq!(decision.status, AutoCoordinatorStatus::Continue);
assert_eq!(
decision.status_sent_to_user.as_deref(),
@@ -803,7 +888,7 @@ mod tests {
"finish_status": "continue",
"status_title": "Implementing",
"status_sent_to_user": "Driving the implementation milestone.",
"prompt_sent_to_cli": "Deliver the feature end-to-end and validate.",
"cli_milestone_instruction": "Deliver the feature end-to-end and validate.",
"finish_evidence": {
"primary_outcome_achieved": "done",
"validation_checks_passed": ["cargo test"],
@@ -811,7 +896,8 @@ mod tests {
}
}"#;
let err = parse_decision(raw).expect_err("continue should reject finish evidence");
let err = parse_decision(raw, DecisionParseOptions::default())
.expect_err("continue should reject finish evidence");
assert!(
err.to_string()
.contains("finish_evidence must be null when finish_status is continue")
@@ -824,10 +910,11 @@ mod tests {
"finish_status": "finish_success",
"status_title": "Completed",
"status_sent_to_user": "Everything is green.",
"prompt_sent_to_cli": null
"cli_milestone_instruction": null
}"#;
let err = parse_decision(raw).expect_err("finish status should require evidence");
let err = parse_decision(raw, DecisionParseOptions::default())
.expect_err("finish status should require evidence");
assert!(err.to_string().contains("missing finish_evidence"));
}
@@ -837,7 +924,7 @@ mod tests {
"finish_status": "finish_success",
"status_title": "Completed",
"status_sent_to_user": "Everything is green.",
"prompt_sent_to_cli": "Keep working",
"cli_milestone_instruction": "Keep working",
"finish_evidence": {
"primary_outcome_achieved": "Resolved primary task.",
"validation_checks_passed": ["cargo test --workspace"],
@@ -845,8 +932,12 @@ mod tests {
}
}"#;
let err = parse_decision(raw).expect_err("finish statuses require null CLI prompt");
assert!(err.to_string().contains("must set prompt_sent_to_cli to null"));
let err = parse_decision(raw, DecisionParseOptions::default())
.expect_err("finish statuses require null CLI prompt");
assert!(
err.to_string()
.contains("must set cli_milestone_instruction to null")
);
}
#[test]
@@ -856,7 +947,7 @@ mod tests {
"phase": "lockdown",
"status_title": "Completed",
"status_sent_to_user": "All checks are green.",
"prompt_sent_to_cli": null,
"cli_milestone_instruction": null,
"finish_evidence": {
"primary_outcome_achieved": "Resolved primary task end-to-end.",
"validation_checks_passed": ["cargo test --workspace", "./build-fast.sh"],
@@ -864,7 +955,8 @@ mod tests {
}
}"#;
let (decision, _) = parse_decision(raw).expect("finish decision should parse");
let (decision, _) = parse_decision(raw, DecisionParseOptions::default())
.expect("finish decision should parse");
assert_eq!(decision.status, AutoCoordinatorStatus::Success);
assert!(decision.cli.is_none());
}
@@ -875,7 +967,7 @@ mod tests {
"finish_status": "finish_success",
"status_title": "Completed",
"status_sent_to_user": "All checks are green.",
"prompt_sent_to_cli": null,
"cli_milestone_instruction": null,
"finish_evidence": {
"primary_outcome_achieved": "Resolved primary task end-to-end.",
"validation_checks_passed": [],
@@ -883,7 +975,8 @@ mod tests {
}
}"#;
let err = parse_decision(raw).expect_err("finish decision should require validations");
let err = parse_decision(raw, DecisionParseOptions::default())
.expect_err("finish decision should require validations");
assert!(
err.to_string()
.contains("validation_checks_passed must include at least one")
@@ -892,17 +985,17 @@ mod tests {
#[test]
fn classify_missing_cli_prompt_is_recoverable() {
let err = anyhow!("model response missing prompt_sent_to_cli for continue");
let err = anyhow!("model response missing cli_milestone_instruction for continue");
let info = classify_recoverable_decision_error(&err).expect("recoverable error");
assert!(info
.summary
.contains("prompt_sent_to_cli"));
.contains("cli_milestone_instruction"));
assert!(
info
.guidance
.as_ref()
.expect("guidance")
.contains("prompt_sent_to_cli")
.contains("cli_milestone_instruction")
);
}
@@ -1039,7 +1132,11 @@ struct CoordinatorDecisionNew {
#[serde(default)]
progress: Option<ProgressPayload>,
#[serde(default)]
prompt_sent_to_cli: Option<String>,
cli_milestone_instruction: Option<String>,
#[serde(default)]
cli_model: Option<String>,
#[serde(default)]
cli_reasoning_effort: Option<String>,
#[serde(default)]
agents: Option<AgentsField>,
#[serde(default)]
@@ -1145,11 +1242,18 @@ struct ParsedCoordinatorDecision {
model_slug: String,
}
#[derive(Debug, Clone, Copy, Default)]
struct DecisionParseOptions {
require_cli_model_routing: bool,
}
#[derive(Debug, Clone)]
struct CliAction {
prompt: String,
context: Option<String>,
suppress_ui_context: bool,
model_override: Option<String>,
reasoning_effort_override: Option<ReasoningEffort>,
}
#[derive(Debug, Clone)]
@@ -1361,6 +1465,8 @@ fn run_auto_loop(
prompt: seed.cli_prompt.clone(),
context: Some(seed.goal_message.clone()),
suppress_ui_context: true,
model_override: None,
reasoning_effort_override: None,
};
let event = AutoCoordinatorEvent::Decision {
seq: decision_seq,
@@ -1458,6 +1564,7 @@ fn run_auto_loop(
&event_tx,
&cancel_token,
&active_model_slug,
schema_features.include_cli_model_routing,
) {
Ok(ParsedCoordinatorDecision {
status,
@@ -1597,7 +1704,7 @@ fn run_auto_loop(
if consecutive_decision_failures <= MAX_DECISION_RECOVERY_ATTEMPTS {
let attempt = consecutive_decision_failures;
const OVERLONG_MSG: &str = "ERROR: Your last prompt_sent_to_cli was greater than 600 characters and was not sent to the CLI. Please try again with a shorter prompt. You must keep prompts succinct (<=600 chars) to give the CLI autonomy to decide how to best execute the task.";
const OVERLONG_MSG: &str = "ERROR: Your last cli_milestone_instruction was greater than 600 characters and was not sent to the CLI. Please try again with a shorter prompt. You must keep prompts succinct (<=600 chars) to give the CLI autonomy to decide how to best execute the task.";
let mut already_shared_raw = false;
if let Some(raw) = raw_output.as_ref() {
@@ -1874,7 +1981,7 @@ fn build_initial_planning_seed(goal_text: &str, include_agents: bool) -> Option<
};
let response_json = format!(
"{{\"finish_status\":\"continue\",\"status_title\":\"Planning\",\"status_sent_to_user\":\"Started initial planning phase\",\"prompt_sent_to_cli\":\"{cli_prompt}\"}}"
"{{\"finish_status\":\"continue\",\"status_title\":\"Planning\",\"status_sent_to_user\":\"Started initial planning phase\",\"cli_milestone_instruction\":\"{cli_prompt}\"}}"
);
Some(InitialPlanningSeed {
@@ -1928,6 +2035,7 @@ fn run_git_command<const N: usize>(args: [&str; N]) -> Option<String> {
struct SchemaFeatures {
include_agents: bool,
include_goal_field: bool,
include_cli_model_routing: bool,
}
impl SchemaFeatures {
@@ -1935,6 +2043,7 @@ impl SchemaFeatures {
Self {
include_agents: settings.agents_enabled,
include_goal_field: false,
include_cli_model_routing: settings.model_routing_enabled,
}
}
}
@@ -1944,6 +2053,7 @@ impl Default for SchemaFeatures {
Self {
include_agents: true,
include_goal_field: false,
include_cli_model_routing: true,
}
}
}
@@ -2036,14 +2146,36 @@ fn build_schema(active_agents: &[String], features: SchemaFeatures) -> Value {
required.push(Value::String("status_sent_to_user".to_string()));
properties.insert(
"prompt_sent_to_cli".to_string(),
"cli_milestone_instruction".to_string(),
json!({
"type": ["string", "null"],
"minLength": CLI_PROMPT_MIN_CHARS,
"description": "Single milestone instruction to the CLI. Outcome-focused, non-procedural. Set to null ONLY when finishing."
"description": "Single milestone instruction to the CLI. Outcome-focused, non-procedural. Keep this between 4 and 600 characters; set to null ONLY when finishing."
}),
);
required.push(Value::String("prompt_sent_to_cli".to_string()));
required.push(Value::String("cli_milestone_instruction".to_string()));
if features.include_cli_model_routing {
properties.insert(
"cli_model".to_string(),
json!({
"type": ["string", "null"],
"enum": ["gpt-5.3-codex", "gpt-5.3-codex-spark", null],
"description": "CLI model for this turn. Use gpt-5.3-codex for planning/problem-solving (xhigh/high/medium) and gpt-5.3-codex-spark for clear coding loops or failing-test iteration (high). Set to null only when finishing."
}),
);
required.push(Value::String("cli_model".to_string()));
properties.insert(
"cli_reasoning_effort".to_string(),
json!({
"type": ["string", "null"],
"enum": ["medium", "high", "xhigh", null],
"description": "Reasoning effort for the selected CLI model this turn. Prefer xhigh for hard planning, high for main coding loops, and medium for straightforward execution. Set to null only when finishing."
}),
);
required.push(Value::String("cli_reasoning_effort".to_string()));
}
if features.include_agents {
properties.insert(
@@ -2167,6 +2299,7 @@ fn request_coordinator_decision(
event_tx: &AutoCoordinatorEventSender,
cancel_token: &CancellationToken,
preferred_model_slug: &str,
require_cli_model_routing: bool,
) -> Result<ParsedCoordinatorDecision, DecisionFailure> {
let RequestStreamResult {
output_text,
@@ -2197,7 +2330,12 @@ fn request_coordinator_decision(
Some(output_text),
));
}
let (mut decision, value) = parse_decision(&output_text)
let (mut decision, value) = parse_decision(
&output_text,
DecisionParseOptions {
require_cli_model_routing,
},
)
.map_err(|err| DecisionFailure::new(err, "coordinator_decision", Some(output_text.clone())))?;
debug!("[Auto coordinator] model decision: {:?}", value);
decision.response_items = response_items;
@@ -2951,15 +3089,16 @@ fn classify_recoverable_decision_error(err: &anyhow::Error) -> Option<Recoverabl
let text = err.to_string();
let lower = text.to_ascii_lowercase();
if lower.contains("missing prompt_sent_to_cli")
if lower.contains("missing cli_milestone_instruction")
|| lower.contains("missing cli prompt for continue")
|| lower.contains("missing cli prompt for `finish_status")
|| lower.contains("missing cli prompt")
{
return Some(RecoverableDecisionError {
summary: "missing `prompt_sent_to_cli` for `finish_status: \"continue\"`".to_string(),
summary: "missing `cli_milestone_instruction` for `finish_status: \"continue\"`"
.to_string(),
guidance: Some(
"Include a non-empty `prompt_sent_to_cli` string whenever `finish_status` is `\"continue\"`."
"Include a non-empty `cli_milestone_instruction` string whenever `finish_status` is `\"continue\"`."
.to_string(),
),
});
@@ -2975,6 +3114,26 @@ fn classify_recoverable_decision_error(err: &anyhow::Error) -> Option<Recoverabl
});
}
if lower.contains("missing cli_model") {
return Some(RecoverableDecisionError {
summary: "missing `cli_model` for continue turn".to_string(),
guidance: Some(
"When Auto Drive model routing is enabled, include `cli_model` on every continue turn."
.to_string(),
),
});
}
if lower.contains("missing cli_reasoning_effort") {
return Some(RecoverableDecisionError {
summary: "missing `cli_reasoning_effort` for continue turn".to_string(),
guidance: Some(
"When Auto Drive model routing is enabled, include `cli_reasoning_effort` on every continue turn."
.to_string(),
),
});
}
if lower.contains("validation_checks_passed must include at least one") {
return Some(RecoverableDecisionError {
summary: "finish evidence is missing passing validation checks".to_string(),
@@ -2995,11 +3154,33 @@ fn classify_recoverable_decision_error(err: &anyhow::Error) -> Option<Recoverabl
});
}
if lower.contains("must set prompt_sent_to_cli to null") {
if lower.contains("must set cli_milestone_instruction to null") {
return Some(RecoverableDecisionError {
summary: "`prompt_sent_to_cli` must be null for finish statuses".to_string(),
summary: "`cli_milestone_instruction` must be null for finish statuses".to_string(),
guidance: Some(
"When finishing (`finish_success`/`finish_failed`), set `prompt_sent_to_cli` to null and include `finish_evidence`."
"When finishing (`finish_success`/`finish_failed`), set `cli_milestone_instruction` to null and include `finish_evidence`."
.to_string(),
),
});
}
if lower.contains("must set cli_model to null")
|| lower.contains("must set cli_reasoning_effort to null")
{
return Some(RecoverableDecisionError {
summary: "CLI model routing fields must be null for finish statuses".to_string(),
guidance: Some(
"When finishing (`finish_success`/`finish_failed`), set `cli_model` and `cli_reasoning_effort` to null."
.to_string(),
),
});
}
if lower.contains("unsupported cli_model") || lower.contains("unsupported cli_reasoning_effort") {
return Some(RecoverableDecisionError {
summary: "unsupported CLI model routing selection".to_string(),
guidance: Some(
"Use `cli_model` in {gpt-5.3-codex, gpt-5.3-codex-spark} and `cli_reasoning_effort` in {medium, high, xhigh}."
.to_string(),
),
});
@@ -3007,12 +3188,12 @@ fn classify_recoverable_decision_error(err: &anyhow::Error) -> Option<Recoverabl
if lower.contains("length limit")
|| lower.contains("cut off")
|| lower.contains("exceeds") && lower.contains("prompt_sent_to_cli")
|| lower.contains("exceeds") && lower.contains("cli_milestone_instruction")
{
return Some(RecoverableDecisionError {
summary: "model output was cut off by a length cap".to_string(),
guidance: Some(
"Regenerate with a shorter `prompt_sent_to_cli` (<=600 chars) and more concise status text so the response fits within provider limits."
"Regenerate with a shorter `cli_milestone_instruction` (<=600 chars) and more concise status text so the response fits within provider limits."
.to_string(),
),
});
@@ -3115,7 +3296,7 @@ fn push_unique_guidance(guidance: &mut Vec<String>, message: &str) {
}
fn parse_decision(raw: &str) -> Result<(ParsedCoordinatorDecision, Value)> {
fn parse_decision(raw: &str, options: DecisionParseOptions) -> Result<(ParsedCoordinatorDecision, Value)> {
let value: Value = match serde_json::from_str(raw) {
Ok(v) => v,
Err(_) => {
@@ -3128,7 +3309,7 @@ fn parse_decision(raw: &str) -> Result<(ParsedCoordinatorDecision, Value)> {
match serde_json::from_value::<CoordinatorDecisionNew>(value.clone()) {
Ok(decision) => {
let status = parse_finish_status(&decision.finish_status)?;
let parsed = convert_decision_new(decision, status)?;
let parsed = convert_decision_new(decision, status, options)?;
Ok((parsed, value))
}
Err(new_err) => {
@@ -3142,7 +3323,7 @@ fn parse_decision(raw: &str) -> Result<(ParsedCoordinatorDecision, Value)> {
anyhow!("decoding coordinator decision failed: new_schema_err={new_err}; legacy_err={legacy_err}; payload_snippet={snippet}")
})?;
let status = parse_finish_status(&decision.finish_status)?;
let parsed = convert_decision_legacy(decision, status)?;
let parsed = convert_decision_legacy(decision, status, options)?;
Ok((parsed, value))
}
}
@@ -3161,6 +3342,7 @@ fn parse_finish_status(finish_status: &str) -> Result<AutoCoordinatorStatus> {
fn convert_decision_new(
decision: CoordinatorDecisionNew,
status: AutoCoordinatorStatus,
options: DecisionParseOptions,
) -> Result<ParsedCoordinatorDecision> {
let CoordinatorDecisionNew {
finish_status: _,
@@ -3168,7 +3350,9 @@ fn convert_decision_new(
status_title,
status_sent_to_user,
progress,
prompt_sent_to_cli,
cli_milestone_instruction,
cli_model,
cli_reasoning_effort,
agents: agent_payloads,
finish_evidence,
goal,
@@ -3192,29 +3376,35 @@ fn convert_decision_new(
let goal = clean_optional(goal);
let cli_prompt = clean_optional(prompt_sent_to_cli);
let cli_prompt = clean_optional(cli_milestone_instruction);
let cli_model = clean_optional(cli_model);
let cli_reasoning_effort = clean_optional(cli_reasoning_effort);
validate_finish_evidence_for_status(status, finish_evidence)?;
let (cli_model, cli_reasoning_effort) =
validate_cli_model_selection(status, cli_model, cli_reasoning_effort, options)?;
let cli = match (status, cli_prompt) {
(AutoCoordinatorStatus::Continue, Some(prompt)) => {
let prompt = clean_required(&prompt, "prompt_sent_to_cli")?;
let prompt = clean_required(&prompt, "cli_milestone_instruction")?;
ensure_cli_prompt_length(&prompt)?;
Some(CliAction {
prompt,
context: None,
suppress_ui_context: false,
model_override: cli_model,
reasoning_effort_override: cli_reasoning_effort,
})
}
(AutoCoordinatorStatus::Continue, None) => {
return Err(anyhow!(
"model response missing prompt_sent_to_cli for continue"
"model response missing cli_milestone_instruction for continue"
));
}
(_, Some(_prompt)) => {
return Err(anyhow!(
"model response must set prompt_sent_to_cli to null when finish_status is finish_success or finish_failed"
"model response must set cli_milestone_instruction to null when finish_status is finish_success or finish_failed"
));
}
(_, None) => None,
@@ -3278,6 +3468,7 @@ fn convert_decision_new(
fn convert_decision_legacy(
decision: CoordinatorDecisionLegacy,
status: AutoCoordinatorStatus,
options: DecisionParseOptions,
) -> Result<ParsedCoordinatorDecision> {
let CoordinatorDecisionLegacy {
finish_status: _,
@@ -3299,18 +3490,26 @@ fn convert_decision_legacy(
));
}
if options.require_cli_model_routing {
return Err(anyhow!(
"legacy model response missing cli_model and cli_reasoning_effort for continue"
));
}
let cli = match (status, cli_prompt) {
(AutoCoordinatorStatus::Continue, Some(prompt)) => Some(CliAction {
prompt: clean_required(&prompt, "cli_prompt")?,
context: context.clone(),
suppress_ui_context: false,
model_override: None,
reasoning_effort_override: None,
}),
(AutoCoordinatorStatus::Continue, None) => {
return Err(anyhow!("legacy model response missing cli_prompt for continue"));
}
(_, Some(_prompt)) => {
return Err(anyhow!(
"model response must set prompt_sent_to_cli to null when finish_status is finish_success or finish_failed"
"model response must set cli_milestone_instruction to null when finish_status is finish_success or finish_failed"
));
}
(_, None) => None,
@@ -3406,6 +3605,77 @@ fn validate_phase(phase: Option<String>) -> Result<()> {
}
}
fn parse_cli_reasoning_effort(value: &str) -> Result<ReasoningEffort> {
let normalized = value.trim().to_ascii_lowercase();
if !AUTO_DRIVE_CLI_REASONING_LEVELS
.iter()
.any(|level| normalized == *level)
{
return Err(anyhow!(
"unsupported cli_reasoning_effort '{normalized}'; expected one of: {}",
AUTO_DRIVE_CLI_REASONING_LEVELS.join(", ")
));
}
match normalized.as_str() {
"medium" => Ok(ReasoningEffort::Medium),
"high" => Ok(ReasoningEffort::High),
"xhigh" => Ok(ReasoningEffort::XHigh),
_ => Err(anyhow!("unsupported cli_reasoning_effort '{normalized}'")),
}
}
fn normalize_cli_model(value: &str) -> Result<String> {
let trimmed = value.trim();
for model in AUTO_DRIVE_CLI_MODELS {
if trimmed.eq_ignore_ascii_case(model) {
return Ok((*model).to_string());
}
}
Err(anyhow!(
"unsupported cli_model '{trimmed}'; expected one of: {}",
AUTO_DRIVE_CLI_MODELS.join(", ")
))
}
fn validate_cli_model_selection(
status: AutoCoordinatorStatus,
cli_model: Option<String>,
cli_reasoning_effort: Option<String>,
options: DecisionParseOptions,
) -> Result<(Option<String>, Option<ReasoningEffort>)> {
if !options.require_cli_model_routing {
return Ok((None, None));
}
match status {
AutoCoordinatorStatus::Continue => {
let model = cli_model
.ok_or_else(|| anyhow!("model response missing cli_model for continue"))?;
let reasoning = cli_reasoning_effort.ok_or_else(|| {
anyhow!("model response missing cli_reasoning_effort for continue")
})?;
let model = normalize_cli_model(&model)?;
let reasoning = parse_cli_reasoning_effort(&reasoning)?;
Ok((Some(model), Some(reasoning)))
}
AutoCoordinatorStatus::Success | AutoCoordinatorStatus::Failed => {
if cli_model.is_some() {
return Err(anyhow!(
"model response must set cli_model to null when finish_status is finish_success or finish_failed"
));
}
if cli_reasoning_effort.is_some() {
return Err(anyhow!(
"model response must set cli_reasoning_effort to null when finish_status is finish_success or finish_failed"
));
}
Ok((None, None))
}
}
}
fn validate_finish_evidence_for_status(
status: AutoCoordinatorStatus,
finish_evidence: Option<FinishEvidencePayload>,
@@ -3458,12 +3728,12 @@ fn ensure_cli_prompt_length(prompt: &str) -> Result<()> {
let len = prompt.chars().count();
if len < CLI_PROMPT_MIN_CHARS {
return Err(anyhow!(
"prompt_sent_to_cli must be at least {CLI_PROMPT_MIN_CHARS} characters; keep it concise but not empty"
"cli_milestone_instruction must be at least {CLI_PROMPT_MIN_CHARS} characters; keep it concise but not empty"
));
}
if len > CLI_PROMPT_MAX_CHARS {
return Err(anyhow!(
"prompt_sent_to_cli exceeds {CLI_PROMPT_MAX_CHARS} characters; keep prompts succinct (<=600 chars) and let the CLI decide how to execute the task"
"cli_milestone_instruction exceeds {CLI_PROMPT_MAX_CHARS} characters; keep prompts succinct (<=600 chars) and let the CLI decide how to execute the task"
));
}
@@ -3475,6 +3745,8 @@ fn cli_action_to_event(action: &CliAction) -> AutoTurnCliAction {
prompt: action.prompt.clone(),
context: action.context.clone(),
suppress_ui_context: action.suppress_ui_context,
model_override: action.model_override.clone(),
reasoning_effort_override: action.reasoning_effort_override,
}
}
+14 -3
View File
@@ -1,6 +1,7 @@
use std::time::{Duration, Instant};
use code_common::elapsed::format_duration;
use code_core::config_types::ReasoningEffort;
use code_core::protocol::ReviewContextMetadata;
use code_core::protocol::ReviewOutputEvent;
use code_core::review_coord::{bump_snapshot_epoch, try_acquire_lock};
@@ -289,6 +290,8 @@ pub struct AutoDriveController {
pub current_status_title: Option<String>,
pub current_cli_prompt: Option<String>,
pub current_cli_context: Option<String>,
pub current_cli_model_override: Option<String>,
pub current_cli_reasoning_effort_override: Option<ReasoningEffort>,
pub hide_cli_context_in_ui: bool,
pub suppress_next_cli_display: bool,
pub current_display_line: Option<String>,
@@ -528,6 +531,8 @@ impl AutoDriveController {
self.current_status_title = None;
self.current_cli_prompt = None;
self.current_cli_context = None;
self.current_cli_model_override = None;
self.current_cli_reasoning_effort_override = None;
self.hide_cli_context_in_ui = false;
self.suppress_next_cli_display = false;
self.current_display_line = None;
@@ -606,6 +611,8 @@ impl AutoDriveController {
let truncated_reason = Self::truncate_error(&reason);
self.current_cli_prompt = None;
self.current_cli_context = None;
self.current_cli_model_override = None;
self.current_cli_reasoning_effort_override = None;
self.hide_cli_context_in_ui = false;
self.suppress_next_cli_display = false;
self.pending_agent_actions.clear();
@@ -659,9 +666,13 @@ impl AutoDriveController {
&mut self,
decision_seq: u64,
prompt_text: String,
cli_model_override: Option<String>,
cli_reasoning_effort_override: Option<ReasoningEffort>,
countdown_override: Option<u8>,
) -> Vec<AutoControllerEffect> {
self.current_cli_prompt = Some(prompt_text);
self.current_cli_model_override = cli_model_override;
self.current_cli_reasoning_effort_override = cli_reasoning_effort_override;
self.apply_phase(AutoRunPhase::AwaitingCoordinator { prompt_ready: true });
self.countdown_override = countdown_override;
self.reset_countdown();
@@ -988,7 +999,7 @@ mod tests {
fn countdown_tick_respects_decision_seq() {
let mut controller = AutoDriveController::default();
let _effects = controller.schedule_cli_prompt(1, "Test prompt".to_string(), None);
let _effects = controller.schedule_cli_prompt(1, "Test prompt".to_string(), None, None, None);
let countdown_id = controller.countdown_id;
let effects = controller.handle_countdown_tick(countdown_id, 1, 5);
@@ -1002,7 +1013,7 @@ mod tests {
fn countdown_tick_final_emits_submit() {
let mut controller = AutoDriveController::default();
let _effects = controller.schedule_cli_prompt(7, "Prompt".to_string(), None);
let _effects = controller.schedule_cli_prompt(7, "Prompt".to_string(), None, None, None);
let countdown_id = controller.countdown_id;
let effects = controller.handle_countdown_tick(countdown_id, 7, 0);
@@ -1013,7 +1024,7 @@ mod tests {
#[test]
fn countdown_tick_ignores_stopped_phase() {
let mut controller = AutoDriveController::default();
let _effects = controller.schedule_cli_prompt(3, "Prompt".to_string(), None);
let _effects = controller.schedule_cli_prompt(3, "Prompt".to_string(), None, None, None);
let countdown_id = controller.countdown_id;
controller.set_phase(AutoRunPhase::Idle);
+8 -1
View File
@@ -16,7 +16,7 @@ You direct the Code CLI (role: user) and an optional fleet of helper agents. You
# The Single Most Important Rule: Milestones, Not Micromanagement
You must provide ONE MILESTONE per turn to the CLI, not one tiny step. A milestone is a coherent outcome (e.g., "investigate + patch + validate").
In `prompt_sent_to_cli`, **DO** provide:
In `cli_milestone_instruction`, **DO** provide:
- The milestone outcome.
- Constraints / scope boundaries.
- Definition of done (what validation must be run).
@@ -33,6 +33,13 @@ Agents work in isolated, parallel worktrees. Use them strategically based on the
- **Parallel Coding:** Offload straightforward, well-scoped coding tasks to fast, efficient models (like `-spark` or `-flash` model). Launch them in parallel on different tasks to implement distinct components simultaneously while the main CLI focuses on integration.
- **No Highly Dependent Chains:** Don't use agents if the task requires step-by-step stateful changes where each step depends on the previous one. The CLI's native loop is better for stateful persistence.
# CLI Model Routing
When schema fields are available, pick `cli_model` and `cli_reasoning_effort` on every continue turn.
- Use `gpt-5.3-codex` with `xhigh` for hard planning/problem-solving turns.
- Use `gpt-5.3-codex` with `high` or `medium` for broad implementation/integration turns.
- Use `gpt-5.3-codex-spark` with `high` for clear coding loops and failing-test iteration.
- Only set these fields to `null` when finishing.
# Completion Gate
Code completion is not task completion. Never set `finish_status` to `"finish_success"` unless you can explicitly populate the `finish_evidence` object with proof that:
1. The primary task is fully resolved end-to-end.
+2
View File
@@ -877,6 +877,8 @@ pub fn set_auto_drive_settings(
toml_edit::value(settings.observer_enabled);
doc["auto_drive"]["coordinator_routing"] =
toml_edit::value(settings.coordinator_routing);
doc["auto_drive"]["model_routing_enabled"] =
toml_edit::value(settings.model_routing_enabled);
doc["auto_drive"]["model"] = toml_edit::value(settings.model.trim());
doc["auto_drive"]["model_reasoning_effort"] = toml_edit::value(
settings
+5
View File
@@ -804,6 +804,10 @@ pub struct AutoDriveSettings {
#[serde(default = "default_true")]
pub coordinator_routing: bool,
/// Allow the coordinator to select the CLI model and reasoning effort per turn.
#[serde(default = "default_true")]
pub model_routing_enabled: bool,
#[serde(default)]
pub continue_mode: AutoDriveContinueMode,
@@ -835,6 +839,7 @@ impl Default for AutoDriveSettings {
cross_check_enabled: true,
observer_enabled: true,
coordinator_routing: true,
model_routing_enabled: true,
continue_mode: AutoDriveContinueMode::TenSeconds,
model: default_auto_drive_model(),
model_reasoning_effort: default_auto_drive_reasoning_effort(),
+2
View File
@@ -879,6 +879,7 @@ impl App<'_> {
agents_enabled,
cross_check_enabled,
qa_automation_enabled,
model_routing_enabled,
continue_mode,
} => {
if let AppState::Chat { widget } = &mut self.app_state {
@@ -887,6 +888,7 @@ impl App<'_> {
agents_enabled,
cross_check_enabled,
qa_automation_enabled,
model_routing_enabled,
continue_mode,
);
}
+1
View File
@@ -235,6 +235,7 @@ pub(crate) enum AppEvent {
agents_enabled: bool,
cross_check_enabled: bool,
qa_automation_enabled: bool,
model_routing_enabled: bool,
continue_mode: AutoContinueMode,
},
@@ -25,6 +25,7 @@ pub(crate) struct AutoDriveSettingsView {
cross_check_enabled: bool,
qa_automation_enabled: bool,
diagnostics_enabled: bool,
model_routing_enabled: bool,
continue_mode: AutoContinueMode,
closing: bool,
}
@@ -41,6 +42,7 @@ impl AutoDriveSettingsView {
agents_enabled: bool,
cross_check_enabled: bool,
qa_automation_enabled: bool,
model_routing_enabled: bool,
continue_mode: AutoContinueMode,
) -> Self {
let diagnostics_enabled = qa_automation_enabled
@@ -56,13 +58,14 @@ impl AutoDriveSettingsView {
cross_check_enabled,
qa_automation_enabled,
diagnostics_enabled,
model_routing_enabled,
continue_mode,
closing: false,
}
}
fn option_count() -> usize {
4
5
}
fn send_update(&self) {
@@ -71,6 +74,7 @@ impl AutoDriveSettingsView {
agents_enabled: self.agents_enabled,
cross_check_enabled: self.cross_check_enabled,
qa_automation_enabled: self.qa_automation_enabled,
model_routing_enabled: self.model_routing_enabled,
continue_mode: self.continue_mode,
});
}
@@ -158,7 +162,11 @@ impl AutoDriveSettingsView {
self.set_diagnostics(next);
self.send_update();
}
3 => self.cycle_continue_mode(true),
3 => {
self.model_routing_enabled = !self.model_routing_enabled;
self.send_update();
}
4 => self.cycle_continue_mode(true),
_ => {}
}
}
@@ -201,6 +209,10 @@ impl AutoDriveSettingsView {
self.diagnostics_enabled,
),
3 => (
"Coordinator model routing (choose model + reasoning per turn)",
self.model_routing_enabled,
),
4 => (
"Auto-continue delay",
matches!(self.continue_mode, AutoContinueMode::Manual),
),
@@ -240,14 +252,14 @@ impl AutoDriveSettingsView {
}
}
}
1 | 2 => {
1 | 2 | 3 => {
let checkbox = if enabled { "[x]" } else { "[ ]" };
spans.push(Span::styled(
format!("{checkbox} {label}"),
label_style,
));
}
3 => {
4 => {
spans.push(Span::styled(label.to_string(), label_style));
spans.push(Span::raw(" "));
spans.push(Span::styled(
@@ -269,6 +281,7 @@ impl AutoDriveSettingsView {
lines.push(self.option_label(1));
lines.push(self.option_label(2));
lines.push(self.option_label(3));
lines.push(self.option_label(4));
lines.push(Line::default());
let footer_style = Style::default().fg(colors::text_dim());
@@ -320,13 +333,13 @@ impl AutoDriveSettingsView {
self.app_event_tx.send(AppEvent::RequestRedraw);
}
KeyCode::Left => {
if self.selected_index == 2 {
if self.selected_index == 4 {
self.cycle_continue_mode(false);
self.app_event_tx.send(AppEvent::RequestRedraw);
}
}
KeyCode::Right => {
if self.selected_index == 2 {
if self.selected_index == 4 {
self.cycle_continue_mode(true);
self.app_event_tx.send(AppEvent::RequestRedraw);
}
@@ -376,13 +389,13 @@ impl<'a> BottomPaneView<'a> for AutoDriveSettingsView {
pane.request_redraw();
}
KeyCode::Left => {
if self.selected_index == 2 {
if self.selected_index == 4 {
self.cycle_continue_mode(false);
pane.request_redraw();
}
}
KeyCode::Right => {
if self.selected_index == 2 {
if self.selected_index == 4 {
self.cycle_continue_mode(true);
pane.request_redraw();
}
@@ -396,7 +409,7 @@ impl<'a> BottomPaneView<'a> for AutoDriveSettingsView {
}
fn desired_height(&self, _width: u16) -> u16 {
9
10
}
fn render(&self, area: Rect, buf: &mut Buffer) {
+155 -6
View File
@@ -16821,6 +16821,7 @@ impl ChatWidget<'_> {
let agents = self.auto_state.subagents_enabled;
let cross = self.auto_state.cross_check_enabled;
let qa = self.auto_state.qa_automation_enabled;
let model_routing = self.config.auto_drive.model_routing_enabled;
let mode = self.auto_state.continue_mode;
let view = AutoDriveSettingsView::new(
self.app_event_tx.clone(),
@@ -16831,6 +16832,7 @@ impl ChatWidget<'_> {
agents,
cross,
qa,
model_routing,
mode,
);
AutoDriveSettingsContent::new(view)
@@ -18552,6 +18554,7 @@ fi\n\
agents_enabled: bool,
cross_check_enabled: bool,
qa_automation_enabled: bool,
model_routing_enabled: bool,
continue_mode: AutoContinueMode,
) {
let mut changed = false;
@@ -18571,6 +18574,10 @@ fi\n\
self.auto_state.qa_automation_enabled = qa_automation_enabled;
changed = true;
}
if self.config.auto_drive.model_routing_enabled != model_routing_enabled {
self.config.auto_drive.model_routing_enabled = model_routing_enabled;
changed = true;
}
if self.auto_state.continue_mode != continue_mode {
let effects = self.auto_state.update_continue_mode(continue_mode);
self.auto_apply_controller_effects(effects);
@@ -18585,6 +18592,7 @@ fi\n\
self.config.auto_drive.agents_enabled = agents_enabled;
self.config.auto_drive.cross_check_enabled = cross_check_enabled;
self.config.auto_drive.qa_automation_enabled = qa_automation_enabled;
self.config.auto_drive.model_routing_enabled = model_routing_enabled;
self.config.auto_drive.continue_mode = auto_continue_to_config(continue_mode);
self.restore_auto_resolve_attempts_if_lost();
@@ -18632,6 +18640,8 @@ fi\n\
self.auto_state.current_status_title = None;
self.auto_state.current_cli_prompt = None;
self.auto_state.current_cli_context = None;
self.auto_state.current_cli_model_override = None;
self.auto_state.current_cli_reasoning_effort_override = None;
self.auto_state.hide_cli_context_in_ui = false;
self.auto_state.last_broadcast_summary = None;
self.auto_state.current_summary_index = None;
@@ -18671,6 +18681,8 @@ fi\n\
self.auto_state.current_status_title = None;
self.auto_state.current_cli_prompt = None;
self.auto_state.current_cli_context = None;
self.auto_state.current_cli_model_override = None;
self.auto_state.current_cli_reasoning_effort_override = None;
self.auto_state.hide_cli_context_in_ui = false;
self.auto_state.last_broadcast_summary = None;
self.auto_state.current_summary_index = None;
@@ -18823,8 +18835,16 @@ fi\n\
.and_then(|action| action.context.clone());
let cli_context = Self::normalize_status_field(cli_context_raw);
let cli_prompt = cli.as_ref().map(|action| action.prompt.clone());
let cli_model_override = cli
.as_ref()
.and_then(|action| action.model_override.clone());
let cli_reasoning_effort_override = cli
.as_ref()
.and_then(|action| action.reasoning_effort_override);
self.auto_state.current_cli_context = cli_context.clone();
self.auto_state.current_cli_model_override = cli_model_override.clone();
self.auto_state.current_cli_reasoning_effort_override = cli_reasoning_effort_override;
self.auto_state.hide_cli_context_in_ui = planning_turn;
self.auto_state.suppress_next_cli_display = planning_turn;
if let Some(ref prompt_text) = cli_prompt {
@@ -18923,7 +18943,13 @@ fi\n\
));
}
} else {
self.schedule_auto_cli_prompt(seq, prompt_text);
self.schedule_auto_cli_prompt_with_override(
seq,
prompt_text,
cli_model_override,
cli_reasoning_effort_override,
None,
);
}
}
AutoCoordinatorStatus::Success => {
@@ -19070,19 +19096,33 @@ Have we met every part of this goal and is there no further work to do?"#
}
fn schedule_auto_cli_prompt(&mut self, decision_seq: u64, prompt_text: String) {
self.schedule_auto_cli_prompt_with_override(decision_seq, prompt_text, None);
self.schedule_auto_cli_prompt_with_override(
decision_seq,
prompt_text,
None,
None,
None,
);
}
fn schedule_auto_cli_prompt_with_override(
&mut self,
decision_seq: u64,
prompt_text: String,
cli_model_override: Option<String>,
cli_reasoning_effort_override: Option<ReasoningEffort>,
countdown_override: Option<u8>,
) {
self.auto_state.suppress_next_cli_display = false;
let effects = self
.auto_state
.schedule_cli_prompt(decision_seq, prompt_text, countdown_override);
.schedule_cli_prompt(
decision_seq,
prompt_text,
cli_model_override,
cli_reasoning_effort_override,
countdown_override,
);
self.auto_apply_controller_effects(effects);
}
@@ -19711,6 +19751,8 @@ Have we met every part of this goal and is there no further work to do?"#
}
self.auto_state.current_cli_context = None;
self.auto_state.current_cli_model_override = None;
self.auto_state.current_cli_reasoning_effort_override = None;
self.auto_state.hide_cli_context_in_ui = false;
self.auto_state.current_cli_prompt = Some(String::new());
self.auto_pending_goal_request = true;
@@ -19724,7 +19766,13 @@ Have we met every part of this goal and is there no further work to do?"#
} else {
None
};
self.schedule_auto_cli_prompt_with_override(0, String::new(), override_seconds);
self.schedule_auto_cli_prompt_with_override(
0,
String::new(),
None,
None,
override_seconds,
);
true
}
@@ -19762,6 +19810,9 @@ Have we met every part of this goal and is there no further work to do?"#
} else {
self.auto_turn_review_state = None;
}
self.auto_apply_cli_turn_model_override();
self.bottom_pane.update_status_text(String::new());
self.bottom_pane.set_task_running(false);
let mut message: UserMessage = full_prompt.into();
@@ -19774,11 +19825,30 @@ Have we met every part of this goal and is there no further work to do?"#
self.submit_user_message(message);
self.auto_state.pending_agent_actions.clear();
self.auto_state.pending_agent_timing = None;
self.auto_state.current_cli_model_override = None;
self.auto_state.current_cli_reasoning_effort_override = None;
self.auto_rebuild_live_ring();
self.request_redraw();
self.auto_state.suppress_next_cli_display = false;
}
fn auto_apply_cli_turn_model_override(&mut self) {
if !self.config.auto_drive.model_routing_enabled {
return;
}
let Some(model) = self.auto_state.current_cli_model_override.clone() else {
return;
};
let effort = self
.auto_state
.current_cli_reasoning_effort_override
.unwrap_or(ReasoningEffort::High);
self.apply_model_selection_inner(model, Some(effort), false, false);
}
fn auto_pause_for_manual_edit(&mut self, force: bool) {
if !self.auto_state.is_active() {
return;
@@ -24260,10 +24330,11 @@ Have we met every part of this goal and is there no further work to do?"#
format!("Model: {}", model_text)
};
Some(format!(
"{} · Agents: {} · Diagnostics: {} · Continue: {}",
"{} · Agents: {} · Diagnostics: {} · Model routing: {} · Continue: {}",
model_segment,
Self::on_off_label(self.auto_state.subagents_enabled),
Self::on_off_label(diagnostics_enabled),
Self::on_off_label(self.config.auto_drive.model_routing_enabled),
self.auto_state.continue_mode.label()
))
}
@@ -31217,6 +31288,8 @@ use code_core::protocol::OrderMeta;
prompt: "echo ready".to_string(),
context: None,
suppress_ui_context: false,
model_override: None,
reasoning_effort_override: None,
}),
None,
Vec::new(),
@@ -31253,6 +31326,8 @@ use code_core::protocol::OrderMeta;
prompt: "echo start".to_string(),
context: None,
suppress_ui_context: false,
model_override: None,
reasoning_effort_override: None,
}),
None,
Vec::new(),
@@ -31340,6 +31415,8 @@ use code_core::protocol::OrderMeta;
prompt: "echo work".to_string(),
context: None,
suppress_ui_context: false,
model_override: None,
reasoning_effort_override: None,
}),
None,
Vec::new(),
@@ -32066,6 +32143,8 @@ use code_core::protocol::OrderMeta;
prompt: "Run cargo test".to_string(),
context: Some("use --all-features".to_string()),
suppress_ui_context: false,
model_override: None,
reasoning_effort_override: None,
}),
Some(AutoTurnAgentsTiming::Parallel),
vec![AutoTurnAgentsAction {
@@ -32102,10 +32181,80 @@ use code_core::protocol::OrderMeta;
.iter()
.any(|span| span.content.contains(notice))
})
});
});
assert!(write_notice_present);
}
#[test]
fn auto_submit_prompt_applies_routed_cli_model_override() {
let _runtime_guard = enter_test_runtime_guard();
let mut harness = ChatWidgetHarness::new();
let chat = harness.chat();
chat.auto_state.set_phase(AutoRunPhase::Active);
chat.config.auto_drive.model_routing_enabled = true;
chat.config.model = "gpt-5.3-codex".to_string();
chat.config.model_reasoning_effort = ReasoningEffort::Medium;
chat.auto_handle_decision(
5,
AutoCoordinatorStatus::Continue,
Some("Fixing tests".to_string()),
Some("Running targeted loop.".to_string()),
None,
Some(AutoTurnCliAction {
prompt: "Run the failing test and keep iterating until it passes.".to_string(),
context: None,
suppress_ui_context: false,
model_override: Some("gpt-5.3-codex-spark".to_string()),
reasoning_effort_override: Some(ReasoningEffort::High),
}),
None,
Vec::new(),
Vec::new(),
);
chat.auto_submit_prompt();
assert_eq!(chat.config.model, "gpt-5.3-codex-spark");
assert_eq!(chat.config.model_reasoning_effort, ReasoningEffort::High);
}
#[test]
fn auto_submit_prompt_preserves_model_when_routing_disabled() {
let _runtime_guard = enter_test_runtime_guard();
let mut harness = ChatWidgetHarness::new();
let chat = harness.chat();
chat.auto_state.set_phase(AutoRunPhase::Active);
chat.config.auto_drive.model_routing_enabled = false;
chat.config.model = "gpt-5.3-codex".to_string();
chat.config.model_reasoning_effort = ReasoningEffort::Medium;
chat.auto_handle_decision(
6,
AutoCoordinatorStatus::Continue,
Some("Fixing tests".to_string()),
Some("Running targeted loop.".to_string()),
None,
Some(AutoTurnCliAction {
prompt: "Run the failing test and keep iterating until it passes.".to_string(),
context: None,
suppress_ui_context: false,
model_override: Some("gpt-5.3-codex-spark".to_string()),
reasoning_effort_override: Some(ReasoningEffort::High),
}),
None,
Vec::new(),
Vec::new(),
);
chat.auto_submit_prompt();
assert_eq!(chat.config.model, "gpt-5.3-codex");
assert_eq!(chat.config.model_reasoning_effort, ReasoningEffort::Medium);
}
#[test]
fn coordinator_router_emits_notice_for_status_question() {
let mut harness = ChatWidgetHarness::new();