mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(auto-drive): adopt progress fields in coordinator
This commit is contained in:
+14
-2
@@ -1617,9 +1617,21 @@ impl App<'_> {
|
||||
AppState::Chat { widget } => widget.submit_op(op),
|
||||
AppState::Onboarding { .. } => {}
|
||||
},
|
||||
AppEvent::AutoCoordinatorDecision { status, summary, prompt, transcript } => {
|
||||
AppEvent::AutoCoordinatorDecision {
|
||||
status,
|
||||
progress_past,
|
||||
progress_current,
|
||||
cli_prompt,
|
||||
transcript,
|
||||
} => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.auto_handle_decision(status, summary, prompt, transcript);
|
||||
widget.auto_handle_decision(
|
||||
status,
|
||||
progress_past,
|
||||
progress_current,
|
||||
cli_prompt,
|
||||
transcript,
|
||||
);
|
||||
}
|
||||
}
|
||||
AppEvent::AutoCoordinatorThinking { delta, summary_index } => {
|
||||
|
||||
@@ -151,8 +151,9 @@ pub(crate) enum AppEvent {
|
||||
|
||||
AutoCoordinatorDecision {
|
||||
status: AutoCoordinatorStatus,
|
||||
summary: String,
|
||||
prompt: Option<String>,
|
||||
progress_past: Option<String>,
|
||||
progress_current: Option<String>,
|
||||
cli_prompt: Option<String>,
|
||||
transcript: Vec<ResponseItem>,
|
||||
},
|
||||
AutoCoordinatorThinking {
|
||||
|
||||
@@ -33,7 +33,7 @@ pub(crate) struct AutoCoordinatorViewModel {
|
||||
#[allow(dead_code)]
|
||||
pub goal: Option<String>,
|
||||
pub status_lines: Vec<String>,
|
||||
pub prompt: Option<String>,
|
||||
pub cli_prompt: Option<String>,
|
||||
pub awaiting_submission: bool,
|
||||
pub waiting_for_response: bool,
|
||||
pub countdown: Option<CountdownState>,
|
||||
@@ -313,8 +313,8 @@ impl AutoCoordinatorView {
|
||||
lines
|
||||
}
|
||||
|
||||
fn prompt_lines(&self, style: Style) -> Option<Vec<Line<'static>>> {
|
||||
self.model.prompt.as_ref().map(|prompt| {
|
||||
fn cli_prompt_lines(&self, style: Style) -> Option<Vec<Line<'static>>> {
|
||||
self.model.cli_prompt.as_ref().map(|prompt| {
|
||||
prompt
|
||||
.lines()
|
||||
.map(|line| Line::from(Span::styled(line.trim_end().to_string(), style)))
|
||||
@@ -458,7 +458,7 @@ impl AutoCoordinatorView {
|
||||
let mut total = 0;
|
||||
|
||||
if awaiting {
|
||||
if let Some(prompt) = &self.model.prompt {
|
||||
if let Some(prompt) = &self.model.cli_prompt {
|
||||
total += Self::wrap_count(prompt, inner_width).max(1);
|
||||
}
|
||||
if ctx.button.is_some() {
|
||||
@@ -542,11 +542,11 @@ impl AutoCoordinatorView {
|
||||
let mut has_button_block = false;
|
||||
|
||||
if self.model.awaiting_submission {
|
||||
if let Some(prompt_lines) =
|
||||
self.prompt_lines(Style::default().fg(colors::text_dim()))
|
||||
{
|
||||
content_lines.extend(prompt_lines);
|
||||
}
|
||||
if let Some(prompt_lines) =
|
||||
self.cli_prompt_lines(Style::default().fg(colors::text_dim()))
|
||||
{
|
||||
content_lines.extend(prompt_lines);
|
||||
}
|
||||
|
||||
if let Some(button_block) = self.button_block_lines(ctx) {
|
||||
has_button_block = true;
|
||||
|
||||
+132
-34
@@ -501,7 +501,9 @@ struct AutoCoordinatorUiState {
|
||||
active: bool,
|
||||
goal: Option<String>,
|
||||
current_summary: Option<String>,
|
||||
current_prompt: Option<String>,
|
||||
current_progress_past: Option<String>,
|
||||
current_progress_current: Option<String>,
|
||||
current_cli_prompt: Option<String>,
|
||||
current_display_line: Option<String>,
|
||||
current_display_is_summary: bool,
|
||||
current_reasoning_title: Option<String>,
|
||||
@@ -517,6 +519,8 @@ struct AutoCoordinatorUiState {
|
||||
awaiting_goal_input: bool,
|
||||
last_broadcast_summary: Option<String>,
|
||||
last_decision_summary: Option<String>,
|
||||
last_decision_progress_past: Option<String>,
|
||||
last_decision_progress_current: Option<String>,
|
||||
last_decision_display: Option<String>,
|
||||
last_decision_display_is_summary: bool,
|
||||
observer_telemetry: Option<AutoObserverTelemetry>,
|
||||
@@ -11077,6 +11081,9 @@ impl ChatWidget<'_> {
|
||||
self.auto_state.active = true;
|
||||
self.auto_state.goal = Some(goal_text.clone());
|
||||
self.auto_state.current_summary = None;
|
||||
self.auto_state.current_progress_past = None;
|
||||
self.auto_state.current_progress_current = None;
|
||||
self.auto_state.current_cli_prompt = None;
|
||||
self.auto_state.current_display_line = None;
|
||||
self.auto_state.current_display_is_summary = false;
|
||||
self.auto_state.current_reasoning_title = None;
|
||||
@@ -11085,6 +11092,8 @@ impl ChatWidget<'_> {
|
||||
Some(auto_drive_strings::next_auto_drive_phrase().to_string());
|
||||
self.auto_state.thinking_prefix_stripped = false;
|
||||
self.auto_state.last_broadcast_summary = None;
|
||||
self.auto_state.last_decision_progress_past = None;
|
||||
self.auto_state.last_decision_progress_current = None;
|
||||
self.auto_state.seconds_remaining = AUTO_COUNTDOWN_SECONDS;
|
||||
self.auto_state.waiting_for_response = true;
|
||||
self.auto_state.coordinator_waiting = true;
|
||||
@@ -11116,6 +11125,9 @@ impl ChatWidget<'_> {
|
||||
self.auto_state.waiting_for_response = true;
|
||||
self.auto_state.coordinator_waiting = true;
|
||||
self.auto_state.current_summary = None;
|
||||
self.auto_state.current_progress_past = None;
|
||||
self.auto_state.current_progress_current = None;
|
||||
self.auto_state.current_cli_prompt = None;
|
||||
self.auto_state.last_broadcast_summary = None;
|
||||
self.auto_state.current_summary_index = None;
|
||||
self.auto_state.current_display_line = None;
|
||||
@@ -11133,21 +11145,31 @@ impl ChatWidget<'_> {
|
||||
pub(crate) fn auto_handle_decision(
|
||||
&mut self,
|
||||
status: AutoCoordinatorStatus,
|
||||
summary: String,
|
||||
prompt: Option<String>,
|
||||
progress_past: Option<String>,
|
||||
progress_current: Option<String>,
|
||||
cli_prompt: Option<String>,
|
||||
transcript: Vec<codex_protocol::models::ResponseItem>,
|
||||
) {
|
||||
if !self.auto_state.active {
|
||||
return;
|
||||
}
|
||||
|
||||
let summary = summary.trim().to_string();
|
||||
let progress_past = Self::normalize_progress_field(progress_past);
|
||||
let progress_current = Self::normalize_progress_field(progress_current);
|
||||
|
||||
if !transcript.is_empty() {
|
||||
self.auto_history.append_raw(&transcript);
|
||||
}
|
||||
self.auto_state.last_decision_summary = Some(summary.clone());
|
||||
|
||||
self.auto_state.current_progress_past = progress_past.clone();
|
||||
self.auto_state.current_progress_current = progress_current.clone();
|
||||
self.auto_state.last_decision_progress_past = progress_past.clone();
|
||||
self.auto_state.last_decision_progress_current = progress_current.clone();
|
||||
|
||||
let summary_text = Self::compose_progress_summary(&progress_current, &progress_past);
|
||||
self.auto_state.last_decision_summary = Some(summary_text.clone());
|
||||
self.auto_state.coordinator_waiting = false;
|
||||
self.auto_on_reasoning_final(&summary);
|
||||
self.auto_on_reasoning_final(&summary_text);
|
||||
self.auto_state.last_decision_display = self.auto_state.current_display_line.clone();
|
||||
self.auto_state.last_decision_display_is_summary =
|
||||
self.auto_state.current_display_is_summary;
|
||||
@@ -11159,11 +11181,11 @@ impl ChatWidget<'_> {
|
||||
|
||||
match status {
|
||||
AutoCoordinatorStatus::Continue => {
|
||||
let Some(prompt_text) = prompt else {
|
||||
let Some(prompt_text) = cli_prompt else {
|
||||
self.auto_stop(Some("Coordinator response omitted a prompt.".to_string()));
|
||||
return;
|
||||
};
|
||||
self.auto_state.current_prompt = Some(prompt_text.clone());
|
||||
self.auto_state.current_cli_prompt = Some(prompt_text.clone());
|
||||
self.auto_state.awaiting_submission = true;
|
||||
self.auto_state.seconds_remaining = AUTO_COUNTDOWN_SECONDS;
|
||||
self.auto_state.countdown_id = self.auto_state.countdown_id.wrapping_add(1);
|
||||
@@ -11173,21 +11195,31 @@ impl ChatWidget<'_> {
|
||||
self.auto_start_countdown(countdown_id);
|
||||
}
|
||||
AutoCoordinatorStatus::Success => {
|
||||
let lower = summary.to_ascii_lowercase();
|
||||
let message = if lower.starts_with("coordinator success:") {
|
||||
summary
|
||||
let normalized = summary_text.trim();
|
||||
let message = if normalized.is_empty() {
|
||||
"Coordinator success.".to_string()
|
||||
} else if normalized
|
||||
.to_ascii_lowercase()
|
||||
.starts_with("coordinator success:")
|
||||
{
|
||||
summary_text
|
||||
} else {
|
||||
format!("Coordinator success: {summary}")
|
||||
format!("Coordinator success: {summary_text}")
|
||||
};
|
||||
self.auto_stop(Some(message));
|
||||
return;
|
||||
}
|
||||
AutoCoordinatorStatus::Failed => {
|
||||
let lower = summary.to_ascii_lowercase();
|
||||
let message = if lower.starts_with("coordinator error:") {
|
||||
summary
|
||||
let normalized = summary_text.trim();
|
||||
let message = if normalized.is_empty() {
|
||||
"Coordinator error.".to_string()
|
||||
} else if normalized
|
||||
.to_ascii_lowercase()
|
||||
.starts_with("coordinator error:")
|
||||
{
|
||||
summary_text
|
||||
} else {
|
||||
format!("Coordinator error: {summary}")
|
||||
format!("Coordinator error: {summary_text}")
|
||||
};
|
||||
self.auto_stop(Some(message));
|
||||
return;
|
||||
@@ -11267,7 +11299,7 @@ impl ChatWidget<'_> {
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
{
|
||||
self.auto_state.current_prompt = Some(message.to_string());
|
||||
self.auto_state.current_cli_prompt = Some(message.to_string());
|
||||
if self.auto_state.awaiting_submission {
|
||||
self.auto_state.countdown_id = self.auto_state.countdown_id.wrapping_add(1);
|
||||
self.auto_state.seconds_remaining = AUTO_COUNTDOWN_SECONDS;
|
||||
@@ -11299,7 +11331,7 @@ impl ChatWidget<'_> {
|
||||
if !self.auto_state.active {
|
||||
return;
|
||||
}
|
||||
let Some(prompt) = self.auto_state.current_prompt.clone() else {
|
||||
let Some(prompt) = self.auto_state.current_cli_prompt.clone() else {
|
||||
self.auto_stop(Some("Coordinator prompt missing when attempting to submit.".to_string()));
|
||||
return;
|
||||
};
|
||||
@@ -11316,6 +11348,8 @@ impl ChatWidget<'_> {
|
||||
self.auto_state.seconds_remaining = 0;
|
||||
let post_submit_display = self.auto_state.last_decision_display.clone();
|
||||
self.auto_state.current_summary = None;
|
||||
self.auto_state.current_progress_past = None;
|
||||
self.auto_state.current_progress_current = None;
|
||||
self.auto_state.last_broadcast_summary = None;
|
||||
self.auto_state.current_display_line = post_submit_display.clone();
|
||||
self.auto_state.current_display_is_summary =
|
||||
@@ -11337,7 +11371,7 @@ impl ChatWidget<'_> {
|
||||
if !self.auto_state.active || !self.auto_state.awaiting_submission {
|
||||
return;
|
||||
}
|
||||
let Some(prompt) = self.auto_state.current_prompt.clone() else {
|
||||
let Some(prompt) = self.auto_state.current_cli_prompt.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -11402,6 +11436,8 @@ impl ChatWidget<'_> {
|
||||
self.auto_state.resume_after_manual_submit = false;
|
||||
self.auto_state.seconds_remaining = AUTO_COUNTDOWN_SECONDS;
|
||||
self.auto_state.current_summary = Some(String::new());
|
||||
self.auto_state.current_progress_past = None;
|
||||
self.auto_state.current_progress_current = None;
|
||||
self.auto_state.current_summary_index = None;
|
||||
self.auto_state.placeholder_phrase = None;
|
||||
self.auto_state.thinking_prefix_stripped = false;
|
||||
@@ -11444,19 +11480,31 @@ impl ChatWidget<'_> {
|
||||
|
||||
let headline = self.auto_format_status_headline(&status_text);
|
||||
let mut status_lines = vec![headline];
|
||||
self.auto_append_progress_lines(
|
||||
&mut status_lines,
|
||||
self.auto_state.current_progress_current.as_ref(),
|
||||
self.auto_state.current_progress_past.as_ref(),
|
||||
);
|
||||
if self.auto_state.waiting_for_response && !self.auto_state.coordinator_waiting {
|
||||
if let Some(summary) = self.auto_state.last_decision_summary.as_ref() {
|
||||
let trimmed = summary.trim();
|
||||
if !trimmed.is_empty() {
|
||||
let collapsed = trimmed.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
if !collapsed.is_empty() {
|
||||
let current_line = status_lines
|
||||
.first()
|
||||
.map(|line| line.trim_end_matches('…').trim())
|
||||
.unwrap_or("");
|
||||
if collapsed != current_line {
|
||||
let display = Self::truncate_with_ellipsis(&collapsed, 160);
|
||||
status_lines.push(display);
|
||||
let appended = self.auto_append_progress_lines(
|
||||
&mut status_lines,
|
||||
self.auto_state.last_decision_progress_current.as_ref(),
|
||||
self.auto_state.last_decision_progress_past.as_ref(),
|
||||
);
|
||||
if !appended {
|
||||
if let Some(summary) = self.auto_state.last_decision_summary.as_ref() {
|
||||
let trimmed = summary.trim();
|
||||
if !trimmed.is_empty() {
|
||||
let collapsed = trimmed.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
if !collapsed.is_empty() {
|
||||
let current_line = status_lines
|
||||
.first()
|
||||
.map(|line| line.trim_end_matches('…').trim())
|
||||
.unwrap_or("");
|
||||
if collapsed != current_line {
|
||||
let display = Self::truncate_with_ellipsis(&collapsed, 160);
|
||||
status_lines.push(display);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11485,9 +11533,9 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
let cli_running = self.is_cli_running();
|
||||
|
||||
let prompt = self
|
||||
let cli_prompt = self
|
||||
.auto_state
|
||||
.current_prompt
|
||||
.current_cli_prompt
|
||||
.clone()
|
||||
.filter(|p| !p.trim().is_empty());
|
||||
|
||||
@@ -11531,7 +11579,7 @@ impl ChatWidget<'_> {
|
||||
let model = AutoCoordinatorViewModel {
|
||||
goal: self.auto_state.goal.clone(),
|
||||
status_lines,
|
||||
prompt,
|
||||
cli_prompt,
|
||||
awaiting_submission: self.auto_state.awaiting_submission,
|
||||
waiting_for_response: self.auto_state.waiting_for_response,
|
||||
countdown,
|
||||
@@ -11748,6 +11796,56 @@ impl ChatWidget<'_> {
|
||||
out
|
||||
}
|
||||
|
||||
fn normalize_progress_field(field: Option<String>) -> Option<String> {
|
||||
field.and_then(|value| {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn compose_progress_summary(
|
||||
progress_current: &Option<String>,
|
||||
progress_past: &Option<String>,
|
||||
) -> String {
|
||||
let mut lines = Vec::new();
|
||||
if let Some(current) = progress_current.as_ref() {
|
||||
lines.push(current.clone());
|
||||
}
|
||||
if let Some(past) = progress_past.as_ref() {
|
||||
lines.push(past.clone());
|
||||
}
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
fn auto_append_progress_lines(
|
||||
&self,
|
||||
lines: &mut Vec<String>,
|
||||
progress_current: Option<&String>,
|
||||
progress_past: Option<&String>,
|
||||
) -> bool {
|
||||
let initial_len = lines.len();
|
||||
Self::append_progress_line(lines, progress_current);
|
||||
Self::append_progress_line(lines, progress_past);
|
||||
lines.len() > initial_len
|
||||
}
|
||||
|
||||
fn append_progress_line(lines: &mut Vec<String>, progress: Option<&String>) {
|
||||
if let Some(progress) = progress {
|
||||
let trimmed = progress.trim();
|
||||
if trimmed.is_empty() {
|
||||
return;
|
||||
}
|
||||
let display = Self::truncate_with_ellipsis(trimmed, 160);
|
||||
if !lines.iter().any(|existing| existing.trim() == display) {
|
||||
lines.push(display);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn launch_update_command(
|
||||
&mut self,
|
||||
command: Vec<String>,
|
||||
|
||||
@@ -88,9 +88,20 @@ pub(super) enum AutoCoordinatorCommand {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CoordinatorDecision {
|
||||
finish_status: String,
|
||||
summary: String,
|
||||
#[serde(default)]
|
||||
prompt: Option<String>,
|
||||
progress_past: Option<String>,
|
||||
#[serde(default)]
|
||||
progress_current: Option<String>,
|
||||
#[serde(default)]
|
||||
cli_prompt: Option<String>,
|
||||
}
|
||||
|
||||
struct ParsedCoordinatorDecision {
|
||||
status: AutoCoordinatorStatus,
|
||||
progress_past: Option<String>,
|
||||
progress_current: Option<String>,
|
||||
cli_prompt: Option<String>,
|
||||
response_items: Vec<ResponseItem>,
|
||||
}
|
||||
|
||||
pub(super) fn start_auto_coordinator(
|
||||
@@ -250,24 +261,21 @@ fn run_auto_loop(
|
||||
&app_event_tx,
|
||||
&cancel_token,
|
||||
) {
|
||||
Ok((status, summary, prompt_opt, transcript_items)) => {
|
||||
Ok(ParsedCoordinatorDecision {
|
||||
status,
|
||||
progress_past,
|
||||
progress_current,
|
||||
cli_prompt,
|
||||
response_items,
|
||||
}) => {
|
||||
if matches!(status, AutoCoordinatorStatus::Continue) {
|
||||
let prompt_clone = prompt_opt.clone();
|
||||
let event = AppEvent::AutoCoordinatorDecision {
|
||||
status,
|
||||
summary,
|
||||
prompt: prompt_opt,
|
||||
transcript: transcript_items.clone(),
|
||||
};
|
||||
app_event_tx.send(event);
|
||||
|
||||
if let (Some(handle), Some(cadence)) =
|
||||
(observer_handle.as_ref(), observer_cadence)
|
||||
{
|
||||
if should_trigger_observer(requests_completed, cadence) {
|
||||
let conversation = build_observer_conversation(
|
||||
conv_for_observer,
|
||||
prompt_clone.as_deref(),
|
||||
cli_prompt.as_deref(),
|
||||
);
|
||||
let trigger = ObserverTrigger {
|
||||
conversation,
|
||||
@@ -280,6 +288,15 @@ fn run_auto_loop(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let event = AppEvent::AutoCoordinatorDecision {
|
||||
status,
|
||||
progress_past,
|
||||
progress_current,
|
||||
cli_prompt,
|
||||
transcript: response_items,
|
||||
};
|
||||
app_event_tx.send(event);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -333,9 +350,10 @@ fn run_auto_loop(
|
||||
|
||||
let event = AppEvent::AutoCoordinatorDecision {
|
||||
status,
|
||||
summary,
|
||||
prompt: prompt_opt,
|
||||
transcript: transcript_items,
|
||||
progress_past,
|
||||
progress_current,
|
||||
cli_prompt,
|
||||
transcript: response_items,
|
||||
};
|
||||
app_event_tx.send(event);
|
||||
stopped = true;
|
||||
@@ -348,8 +366,9 @@ fn run_auto_loop(
|
||||
}
|
||||
let event = AppEvent::AutoCoordinatorDecision {
|
||||
status: AutoCoordinatorStatus::Failed,
|
||||
summary: format!("Coordinator error: {err}"),
|
||||
prompt: None,
|
||||
progress_past: None,
|
||||
progress_current: Some(format!("Coordinator error: {err}")),
|
||||
cli_prompt: None,
|
||||
transcript: Vec::new(),
|
||||
};
|
||||
app_event_tx.send(event);
|
||||
@@ -447,7 +466,7 @@ fn run_final_observer_validation(
|
||||
|
||||
fn build_developer_message(goal_text: &str, environment_details: &str) -> (String, String) {
|
||||
let intro = format!(
|
||||
"You have a special role within Code. You are a Coordinator, coordinating prompts sent to a running Code CLI process. You should act like a human maintainer of the project would act. You will see a **Primary Goal** below - this is what you are always working towards.\n\n**JSON Structure**\n- `finish_status`: one of `continue`, `finish_success`, or `finish_failed`.\n * Use `continue` when another prompt is reasonable. Always prefer this option.\n * Use `finish_success` when the goal has been completed in its entirety and absolutely no work remains.\n * Use `finish_failed` when the goal absolutely cannot be satisfied or you are stuck in a loop. This should almost never be used. Try other approaches and gather more information if there is no clear path forward.\n- `summary`: short summary (<= 160 characters) describing what will happen when the CLI performs the next prompt\n- `prompt`: the exact prompt to provide to the Code CLI process. You will receive the response the CLI provides.\n\n**Rules**\n- You set direction, not implementation. Keep the CLI on track, but let it do all the thinking and implementation. You do not have the context the CLI has.\n- When working on an existing code base, start by prompting the CLI to explain the problem and outline plausible approaches. This lets it build context rather than jumping in naively with a solution.\n- Keep every prompt minimal to give the CLI room to make independent decisions.\n- Don't repeat yourself. If something doesn't work, take a different approach. Always push the project forward.\n- Often a simple 'Please continue' or 'Work on feature A next' or 'What do you think is the best approach?' is sufficient. Your job is to keep things running in an appropriate direction. The CLI does all the actual work and thinking. You do not need to know much about the project or codebase, allow the CLI to do all this for you. You are focused on overall direction not implementation details.\n- Only stop when no other options remain. A human is observing your work and will step in if they want to go in a different direction. You should not ask them for assistance - you should use your judgement to move on the most likely path forward. The human may override your message send to the CLI if they choose to go in another direction. This allows you to just guess the best path, knowing an overseer will step in if needed.\n\n**WARNING**\n- Only send the CLI ONE instruction to follow at a time.\n- DO NOT repeat earlier instructions sent to the CLI otherwise you will end in a loop as the CLI will yield once it completes the first instruction. So for example if you say something like 1. Research the codebase 2. Fix the problem., the CLI will yield as soon as it finishes instruciton 1. If you keep sending both instructions, you'll just keep looping on the first instruction. Just send them one at the time. i.e. `Research the codebase` then once you have the results `Fix the problem`\n- You should ask the CLI to research the problem before proposing a solution or writing code. The ensures the CLI reads all relevant parts of the code before starting work and results in more significantly accurate code.\n\nUseful command:\n`/review <what to review>` e.g. `/review latest commit` - this spins up a specialist review thread for the CLI which excels at identify issues. This is useful for repeatedly reviewing code changes you make and fixing them.\n\nEnvironment:\n{environment_details}"
|
||||
"You have a special role within Code. You are a Coordinator, coordinating prompts sent to a running Code CLI process. You should act like a human maintainer of the project would act. You will see a **Primary Goal** below - this is what you are always working towards.\n\n**JSON Structure**\n- `finish_status`: one of `continue`, `finish_success`, or `finish_failed`.\n * Use `continue` when another prompt is reasonable. Always prefer this option.\n * Use `finish_success` when the goal has been completed in its entirety and absolutely no work remains.\n * Use `finish_failed` when the goal absolutely cannot be satisfied or you are stuck in a loop. This should almost never be used. Try other approaches and gather more information if there is no clear path forward.\n- `progress_past`: 1 sentence (<= 160 characters) describing everything completed so far. Use past tense. Leave blank if nothing significant has been done yet.\n- `progress_current`: A short phrase (<= 100 characters) describing what happens when the CLI runs `cli_prompt`. Use present tense.\n- `cli_prompt`: The exact prompt to send to the Code CLI process when `finish_status` is `continue`. Prefer 1-2 concise sentences focused on the next instruction.\n\n**Rules**\n- You set direction, not implementation. Keep the CLI on track, but let it do all the thinking and implementation. You do not have the context the CLI has.\n- When working on an existing code base, start by prompting the CLI to explain the problem and outline plausible approaches. This lets it build context rather than jumping in naively with a solution.\n- Keep every prompt minimal to give the CLI room to make independent decisions.\n- Don't repeat yourself. If something doesn't work, take a different approach. Always push the project forward.\n- Often a simple 'Please continue' or 'Work on feature A next' or 'What do you think is the best approach?' is sufficient. Your job is to keep things running in an appropriate direction. The CLI does all the actual work and thinking. You do not need to know much about the project or codebase, allow the CLI to do all this for you. You are focused on overall direction not implementation details.\n- Only stop when no other options remain. A human is observing your work and will step in if they want to go in a different direction. You should not ask them for assistance - you should use your judgement to move on the most likely path forward. The human may override your message send to the CLI if they choose to go in another direction. This allows you to just guess the best path, knowing an overseer will step in if needed.\n\n**WARNING**\n- Only send the CLI ONE instruction to follow at a time.\n- DO NOT repeat earlier instructions sent to the CLI otherwise you will end in a loop as the CLI will yield once it completes the first instruction. So for example if you say something like 1. Research the codebase 2. Fix the problem., the CLI will yield as soon as it finishes instruciton 1. If you keep sending both instructions, you'll just keep looping on the first instruction. Just send them one at the time. i.e. `Research the codebase` then once you have the results `Fix the problem`\n- You should ask the CLI to research the problem before proposing a solution or writing code. The ensures the CLI reads all relevant parts of the code before starting work and results in more significantly accurate code.\n\nUseful command:\n`/review <what to review>` e.g. `/review latest commit` - this spins up a specialist review thread for the CLI which excels at identify issues. This is useful for repeatedly reviewing code changes you make and fixing them.\n\nEnvironment:\n{environment_details}"
|
||||
);
|
||||
let primary_goal = format!("**Primary Goal**\n{goal_text}");
|
||||
(intro, primary_goal)
|
||||
@@ -492,19 +511,25 @@ fn build_schema() -> Value {
|
||||
"enum": ["continue", "finish_success", "finish_failed"],
|
||||
"description": "Decision on how to proceed"
|
||||
},
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 160,
|
||||
"description": "Short summary describing what will happen when the CLI performs the next prompt"
|
||||
},
|
||||
"prompt": {
|
||||
"progress_past": {
|
||||
"type": ["string", "null"],
|
||||
"minLength": 1,
|
||||
"description": "Prompt to send to Code CLI when finish_status is 'continue'"
|
||||
"maxLength": 160,
|
||||
"description": "1 sentence summary of all work done so far. Leave blank if none. Use past tense. e.g. `Explored codebase and fixed all reported bugs.`"
|
||||
},
|
||||
"progress_current": {
|
||||
"type": ["string", "null"],
|
||||
"minLength": 1,
|
||||
"maxLength": 100,
|
||||
"description": "A few words describing what is happening when the CLI performs the cli_prompt. Use current tense e.g. `Now updating documentation.`"
|
||||
},
|
||||
"cli_prompt": {
|
||||
"type": ["string", "null"],
|
||||
"minLength": 1,
|
||||
"description": "This is the prompt sent to the CLI. It should be 1-2 sentences. Shorter commands are preferred - e.g. ('What do you think the solution is?', 'Please fix this') let the CLI do the work. You just direct it! Ignored unless finish_status is 'continue'"
|
||||
}
|
||||
},
|
||||
"required": ["finish_status", "summary", "prompt"],
|
||||
"required": ["finish_status", "progress_past", "progress_current", "cli_prompt"],
|
||||
"additionalProperties": false
|
||||
})
|
||||
}
|
||||
@@ -524,7 +549,7 @@ fn request_coordinator_decision(
|
||||
auto_instructions: Option<&str>,
|
||||
app_event_tx: &AppEventSender,
|
||||
cancel_token: &CancellationToken,
|
||||
) -> Result<(AutoCoordinatorStatus, String, Option<String>, Vec<ResponseItem>)> {
|
||||
) -> Result<ParsedCoordinatorDecision> {
|
||||
let (raw, response_items) = request_decision(
|
||||
runtime,
|
||||
client,
|
||||
@@ -548,26 +573,41 @@ fn request_coordinator_decision(
|
||||
}
|
||||
};
|
||||
|
||||
let prompt_opt = match status {
|
||||
let clean_field = |field: Option<String>| -> Option<String> {
|
||||
field.and_then(|value| {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let progress_past = clean_field(decision.progress_past);
|
||||
let progress_current = clean_field(decision.progress_current);
|
||||
|
||||
let cli_prompt = match status {
|
||||
AutoCoordinatorStatus::Continue => {
|
||||
let prompt_text = decision
|
||||
.prompt
|
||||
.cli_prompt
|
||||
.as_ref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.ok_or_else(|| anyhow!("model response missing prompt for continue"))?;
|
||||
.ok_or_else(|| anyhow!("model response missing cli_prompt for continue"))?;
|
||||
let cleaned = strip_role_prefix(prompt_text);
|
||||
Some(cleaned.to_string())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Ok((
|
||||
Ok(ParsedCoordinatorDecision {
|
||||
status,
|
||||
decision.summary.trim().to_string(),
|
||||
prompt_opt,
|
||||
progress_past,
|
||||
progress_current,
|
||||
cli_prompt,
|
||||
response_items,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn request_decision(
|
||||
|
||||
@@ -1980,26 +1980,41 @@ fn auto_decision_persists_summary_through_cli_cycle() {
|
||||
|
||||
chat.auto_handle_decision(
|
||||
AutoCoordinatorStatus::Continue,
|
||||
"**Plan:** run tests".to_string(),
|
||||
Some("Reviewed failing integration tests.".to_string()),
|
||||
Some("Running tests now.".to_string()),
|
||||
Some("run tests".to_string()),
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
assert_eq!(chat.auto_state.current_display_line.as_deref(), Some("Plan:"));
|
||||
assert_eq!(
|
||||
chat.auto_state.current_display_line.as_deref(),
|
||||
Some("Running tests now.")
|
||||
);
|
||||
assert!(!chat.auto_state.coordinator_waiting, "spinner should stop");
|
||||
assert!(!chat.auto_state.waiting_for_response, "coordinator finished");
|
||||
assert_eq!(
|
||||
chat.auto_state.last_decision_summary.as_deref(),
|
||||
Some("**Plan:** run tests")
|
||||
Some("Running tests now.\nReviewed failing integration tests.")
|
||||
);
|
||||
assert_eq!(
|
||||
chat.auto_state.last_decision_progress_current.as_deref(),
|
||||
Some("Running tests now.")
|
||||
);
|
||||
assert_eq!(
|
||||
chat.auto_state.last_decision_progress_past.as_deref(),
|
||||
Some("Reviewed failing integration tests.")
|
||||
);
|
||||
|
||||
chat.auto_submit_prompt();
|
||||
assert!(chat.auto_state.waiting_for_response, "waiting on CLI");
|
||||
assert!(!chat.auto_state.coordinator_waiting, "spinner stays off during CLI");
|
||||
assert_eq!(chat.auto_state.current_display_line.as_deref(), Some("Plan:"));
|
||||
assert_eq!(
|
||||
chat.auto_state.current_display_line.as_deref(),
|
||||
Some("Running tests now.")
|
||||
);
|
||||
assert_eq!(
|
||||
chat.auto_state.last_decision_summary.as_deref(),
|
||||
Some("**Plan:** run tests")
|
||||
Some("Running tests now.\nReviewed failing integration tests.")
|
||||
);
|
||||
|
||||
chat.auto_state.placeholder_phrase = None;
|
||||
@@ -2009,7 +2024,10 @@ fn auto_decision_persists_summary_through_cli_cycle() {
|
||||
|
||||
assert!(chat.auto_state.waiting_for_response, "next JSON in flight");
|
||||
assert!(chat.auto_state.coordinator_waiting, "spinner resumes for JSON");
|
||||
assert_eq!(chat.auto_state.current_display_line.as_deref(), Some("Plan:"));
|
||||
assert_eq!(
|
||||
chat.auto_state.current_display_line.as_deref(),
|
||||
Some("Running tests now.")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2031,7 +2049,8 @@ fn auto_history_captures_raw_transcript() {
|
||||
|
||||
chat.auto_handle_decision(
|
||||
AutoCoordinatorStatus::Continue,
|
||||
"Next".to_string(),
|
||||
None,
|
||||
Some("Next".to_string()),
|
||||
Some("do thing".to_string()),
|
||||
transcript.clone(),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user