This commit is contained in:
@@ -6,8 +6,10 @@ use anyhow::Result;
|
||||
use crate::app::{AppEvent, ControlCommand};
|
||||
use crate::controller::{executor, goal_checker, planner, verifier};
|
||||
use crate::model::{
|
||||
ControllerPhase, GoalStatus, SessionEntry, SessionSource, SessionStream, StepStatus, TaskConfig,
|
||||
ControllerPhase, GoalStatus, PlannerResponse, SessionEntry, SessionSource, SessionStream,
|
||||
StepStatus, TaskConfig,
|
||||
};
|
||||
use crate::prompt;
|
||||
use crate::repo;
|
||||
use crate::storage::toon;
|
||||
|
||||
@@ -36,6 +38,18 @@ pub fn runtime_loop(
|
||||
let goal_md = toon::read_markdown(&config.goal_file)?;
|
||||
let standards_md = toon::read_markdown(&config.standards_file)?;
|
||||
refresh_usage_state(&mut state);
|
||||
if matches!(state.phase, ControllerPhase::Planning)
|
||||
&& state.planning_session.pending_input.is_some()
|
||||
{
|
||||
let pending_input = state
|
||||
.planning_session
|
||||
.pending_input
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
process_planning_submission(&repo_root, &config, &pending_input, &event_tx)?;
|
||||
continue;
|
||||
}
|
||||
emit_snapshot(&event_tx, &goal_md, &standards_md, &plan, &state);
|
||||
|
||||
match control_rx.try_recv() {
|
||||
@@ -71,19 +85,18 @@ pub fn runtime_loop(
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
Ok(ControlCommand::Submit(text)) => {
|
||||
Ok(ControlCommand::Submit(_text)) => {
|
||||
if matches!(state.phase, ControllerPhase::Planning) {
|
||||
let response =
|
||||
crate::planning::session::advance(&repo_root, &config, &text, &event_tx)?;
|
||||
if let Some(question) = response.question {
|
||||
let _ = event_tx.send(AppEvent::Session(SessionEntry {
|
||||
source: SessionSource::Planner,
|
||||
stream: SessionStream::Status,
|
||||
title: "Question".to_string(),
|
||||
tag: Some(config.controller_id()),
|
||||
body: question,
|
||||
run_id: repo::next_run_id(),
|
||||
}));
|
||||
let persisted_state = toon::read_state(&config.state_file)?;
|
||||
if let Some(pending_input) =
|
||||
persisted_state.planning_session.pending_input.as_deref()
|
||||
{
|
||||
process_planning_submission(
|
||||
&repo_root,
|
||||
&config,
|
||||
pending_input,
|
||||
&event_tx,
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
let _ = event_tx.send(AppEvent::Session(SessionEntry {
|
||||
@@ -117,6 +130,13 @@ pub fn runtime_loop(
|
||||
state.phase = ControllerPhase::Done;
|
||||
state.clear_stop_reason();
|
||||
state.goal_status = GoalStatus::Done;
|
||||
let completion_summary = build_completion_summary(&plan);
|
||||
state.set_completion_summary(completion_summary.clone());
|
||||
state.history.push(crate::model::HistoryEvent {
|
||||
timestamp: repo::now_timestamp(),
|
||||
kind: "goal-complete".to_string(),
|
||||
detail: completion_summary.clone(),
|
||||
});
|
||||
toon::write_state(&config.state_file, &state)?;
|
||||
emit_snapshot(&event_tx, &goal_md, &standards_md, &plan, &state);
|
||||
let _ = event_tx.send(AppEvent::Session(SessionEntry {
|
||||
@@ -124,7 +144,7 @@ pub fn runtime_loop(
|
||||
stream: SessionStream::Status,
|
||||
title: "Goal".to_string(),
|
||||
tag: Some(config.controller_id()),
|
||||
body: "Goal complete".to_string(),
|
||||
body: completion_summary,
|
||||
run_id: repo::next_run_id(),
|
||||
}));
|
||||
continue;
|
||||
@@ -180,6 +200,7 @@ pub fn runtime_loop(
|
||||
run_id: repo::next_run_id(),
|
||||
}));
|
||||
state.clear_stop_reason();
|
||||
state.clear_completion_summary();
|
||||
state.replan_required = false;
|
||||
state
|
||||
.blocked_steps
|
||||
@@ -245,6 +266,7 @@ pub fn runtime_loop(
|
||||
continue;
|
||||
}
|
||||
|
||||
plan.append_step_note(&step.id, completion_note(&exec));
|
||||
plan.mark_done(&step.id);
|
||||
state.complete_step(&step, verification, cleanup, tests);
|
||||
toon::write_plan(&config.plan_file, &plan)?;
|
||||
@@ -260,6 +282,32 @@ fn refresh_usage_state(state: &mut crate::model::ControllerState) {
|
||||
crate::process::persist_usage_snapshot(state, &snapshot);
|
||||
}
|
||||
|
||||
fn process_planning_submission(
|
||||
repo_root: &std::path::Path,
|
||||
config: &TaskConfig,
|
||||
latest_user_input: &str,
|
||||
event_tx: &Sender<AppEvent>,
|
||||
) -> Result<PlannerResponse> {
|
||||
let response = crate::planning::session::advance(
|
||||
repo_root,
|
||||
config,
|
||||
latest_user_input,
|
||||
event_tx,
|
||||
)?;
|
||||
if let Some(question) = response.question.clone() {
|
||||
let _ = event_tx.send(AppEvent::Session(SessionEntry {
|
||||
source: SessionSource::Planner,
|
||||
stream: SessionStream::Status,
|
||||
title: "Question".to_string(),
|
||||
tag: Some(config.controller_id()),
|
||||
body: question,
|
||||
run_id: repo::next_run_id(),
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn emit_snapshot(
|
||||
event_tx: &Sender<AppEvent>,
|
||||
goal_md: &str,
|
||||
@@ -292,6 +340,79 @@ fn resumable_step(
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn completion_note(exec: &crate::model::ExecutionResponse) -> String {
|
||||
let mut parts = Vec::new();
|
||||
|
||||
let summary = exec.summary.trim();
|
||||
if !summary.is_empty() {
|
||||
parts.push(prompt::truncate_text(summary, 180));
|
||||
}
|
||||
|
||||
let notes = exec
|
||||
.notes
|
||||
.iter()
|
||||
.map(|note| note.trim())
|
||||
.filter(|note| !note.is_empty())
|
||||
.map(|note| prompt::truncate_text(note, 120))
|
||||
.collect::<Vec<_>>();
|
||||
if !notes.is_empty() {
|
||||
parts.push(format!("Agent notes: {}", notes.join("; ")));
|
||||
}
|
||||
|
||||
if parts.is_empty() {
|
||||
"Completed the step.".to_string()
|
||||
} else {
|
||||
prompt::truncate_text(&parts.join(" "), 240)
|
||||
}
|
||||
}
|
||||
|
||||
fn build_completion_summary(plan: &crate::model::Plan) -> String {
|
||||
let completed_steps = plan
|
||||
.steps
|
||||
.iter()
|
||||
.filter(|step| step.status.is_done())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if completed_steps.is_empty() {
|
||||
return "Goal complete.".to_string();
|
||||
}
|
||||
|
||||
let mut details = completed_steps
|
||||
.iter()
|
||||
.take(4)
|
||||
.map(|step| {
|
||||
let mut item = format!(
|
||||
"{}: {}",
|
||||
step.id,
|
||||
prompt::truncate_text(&step.title, 80)
|
||||
);
|
||||
if !step.notes.trim().is_empty() {
|
||||
item.push_str(" - ");
|
||||
item.push_str(&prompt::truncate_text(&step.notes, 120));
|
||||
}
|
||||
item
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let omitted = completed_steps.len().saturating_sub(details.len());
|
||||
if omitted > 0 {
|
||||
details.push(format!(
|
||||
"... and {} more completed step{}",
|
||||
omitted,
|
||||
if omitted == 1 { "" } else { "s" }
|
||||
));
|
||||
}
|
||||
|
||||
prompt::truncate_text(
|
||||
&format!(
|
||||
"Completed {} step{}: {}",
|
||||
completed_steps.len(),
|
||||
if completed_steps.len() == 1 { "" } else { "s" },
|
||||
details.join("; ")
|
||||
),
|
||||
320,
|
||||
)
|
||||
}
|
||||
|
||||
fn recover_stale_execution_state(
|
||||
config: &TaskConfig,
|
||||
plan: &mut crate::model::Plan,
|
||||
@@ -542,4 +663,49 @@ mod tests {
|
||||
other => panic!("unexpected event: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completion_note_uses_execution_summary_and_notes() {
|
||||
let note = completion_note(&crate::model::ExecutionResponse {
|
||||
summary: "Implemented the board note flow".to_string(),
|
||||
notes: vec![
|
||||
"Kept the change localized to completion handling".to_string(),
|
||||
"Verified the board still renders done steps".to_string(),
|
||||
],
|
||||
..crate::model::ExecutionResponse::default()
|
||||
});
|
||||
|
||||
assert!(note.contains("Implemented the board note flow"));
|
||||
assert!(note.contains("Agent notes:"));
|
||||
assert!(note.contains("Kept the change localized"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_completion_summary_lists_done_steps() {
|
||||
let plan = Plan {
|
||||
version: 1,
|
||||
goal_summary: "goal".to_string(),
|
||||
steps: vec![
|
||||
PlanStep {
|
||||
id: "s1".to_string(),
|
||||
title: "First".to_string(),
|
||||
notes: "Finished the first change.".to_string(),
|
||||
status: StepStatus::Done,
|
||||
..PlanStep::default()
|
||||
},
|
||||
PlanStep {
|
||||
id: "s2".to_string(),
|
||||
title: "Second".to_string(),
|
||||
notes: "Finished the second change.".to_string(),
|
||||
status: StepStatus::Done,
|
||||
..PlanStep::default()
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let summary = build_completion_summary(&plan);
|
||||
assert!(summary.contains("Completed 2 steps"));
|
||||
assert!(summary.contains("s1: First"));
|
||||
assert!(summary.contains("s2: Second"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user