This commit is contained in:
eric
2026-04-04 05:57:58 +02:00
commit 97f329c825
55 changed files with 10026 additions and 0 deletions

206
src/controller/engine.rs Normal file
View File

@@ -0,0 +1,206 @@
use std::path::PathBuf;
use std::sync::mpsc::{Receiver, Sender, TryRecvError};
use anyhow::Result;
use crate::app::{AppEvent, ControlCommand};
use crate::controller::{executor, goal_checker, planner, verifier};
use crate::model::{
ControllerPhase, GoalStatus, SessionEntry, SessionSource, SessionStream, TaskConfig,
};
use crate::repo;
use crate::storage::toon;
pub fn runtime_loop(
repo_root: PathBuf,
config: TaskConfig,
control_rx: Receiver<ControlCommand>,
event_tx: Sender<AppEvent>,
) -> Result<()> {
toon::ensure_controller_files(&config)?;
let _ = event_tx.send(AppEvent::Session(SessionEntry {
source: SessionSource::Controller,
stream: SessionStream::Status,
title: "Session".to_string(),
tag: Some(config.controller_id()),
body: format!("Controller task loaded from {}", config.plan_file.display()),
run_id: repo::next_run_id(),
}));
loop {
let mut plan = toon::read_plan(&config.plan_file)?;
let mut state = toon::read_state(&config.state_file)?;
let goal_md = toon::read_markdown(&config.goal_file)?;
let standards_md = toon::read_markdown(&config.standards_file)?;
let _ = event_tx.send(AppEvent::Snapshot {
goal_md,
standards_md,
plan: plan.clone(),
state: state.clone(),
});
match control_rx.try_recv() {
Ok(ControlCommand::Quit) => break,
Ok(ControlCommand::Pause) => {
crate::controller::state::pause(&mut state);
toon::write_state(&config.state_file, &state)?;
continue;
}
Ok(ControlCommand::Resume) => {
crate::controller::state::resume(&mut state);
toon::write_state(&config.state_file, &state)?;
continue;
}
Ok(ControlCommand::Stop) => {
state.phase = ControllerPhase::Blocked;
state.goal_status = GoalStatus::Blocked;
state.notes.push("Stopped by user".to_string());
toon::write_state(&config.state_file, &state)?;
continue;
}
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(),
}));
}
} else {
let _ = event_tx.send(AppEvent::Session(SessionEntry {
source: SessionSource::Warning,
stream: SessionStream::Status,
title: "Execution".to_string(),
tag: Some(config.controller_id()),
body: "Execution is autonomous. Use /pause, /resume, /stop, /status, /diff, /tests, or /goal update."
.to_string(),
run_id: repo::next_run_id(),
}));
}
continue;
}
Err(TryRecvError::Disconnected) => break,
Err(TryRecvError::Empty) => {}
}
match state.phase {
ControllerPhase::Planning
| ControllerPhase::Paused
| ControllerPhase::Blocked
| ControllerPhase::Done => {
std::thread::sleep(std::time::Duration::from_millis(100));
continue;
}
ControllerPhase::Executing => {}
}
if goal_checker::is_done(&plan, &state)? {
state.phase = ControllerPhase::Done;
state.goal_status = GoalStatus::Done;
toon::write_state(&config.state_file, &state)?;
let _ = event_tx.send(AppEvent::Session(SessionEntry {
source: SessionSource::Controller,
stream: SessionStream::Status,
title: "Goal".to_string(),
tag: Some(config.controller_id()),
body: "Goal complete".to_string(),
run_id: repo::next_run_id(),
}));
continue;
}
if state.replan_required || plan.has_no_actionable_steps() {
let _ = event_tx.send(AppEvent::Session(SessionEntry {
source: SessionSource::Planner,
stream: SessionStream::Status,
title: "Planner".to_string(),
tag: Some(config.controller_id()),
body: "Refining plan".to_string(),
run_id: repo::next_run_id(),
}));
plan =
planner::refine_without_user_input(&repo_root, &config, &plan, &state, &event_tx)?;
state.replan_required = false;
toon::write_plan(&config.plan_file, &plan)?;
toon::write_state(&config.state_file, &state)?;
continue;
}
let Some(step) = planner::next_step(&plan, &state) else {
state.phase = ControllerPhase::Blocked;
state.goal_status = GoalStatus::Blocked;
state.notes.push(
"No actionable step remained and autonomous replan produced nothing.".to_string(),
);
toon::write_state(&config.state_file, &state)?;
continue;
};
let _ = event_tx.send(AppEvent::Session(SessionEntry {
source: SessionSource::Executor,
stream: SessionStream::Status,
title: "Step".to_string(),
tag: Some(step.id.clone()),
body: format!("Executing {}", step.title),
run_id: repo::next_run_id(),
}));
plan.mark_active(&step.id);
state.current_step_id = Some(step.id.clone());
state.iteration += 1;
toon::write_plan(&config.plan_file, &plan)?;
toon::write_state(&config.state_file, &state)?;
let exec = executor::implement(&repo_root, &config, &plan, &step, &event_tx)?;
if goal_checker::needs_goal_clarification(&exec) {
state.phase = ControllerPhase::Planning;
state.notes.push(format!(
"Execution requested goal clarification while processing {}",
step.id
));
toon::write_state(&config.state_file, &state)?;
continue;
}
let verification = verifier::verify_step(&repo_root, &exec, &event_tx)?;
if !verification.passed {
plan.mark_blocked(&step.id);
state.last_verification = Some(verification);
state.blocked_steps.push(step.id.clone());
state.replan_required = true;
toon::write_plan(&config.plan_file, &plan)?;
toon::write_state(&config.state_file, &state)?;
continue;
}
let cleanup = verifier::verify_cleanup(&config, &step, &exec)?;
if !cleanup.passed {
plan.mark_todo(&step.id);
state.last_cleanup_summary = Some(cleanup);
toon::write_plan(&config.plan_file, &plan)?;
toon::write_state(&config.state_file, &state)?;
continue;
}
let tests = verifier::run_tests(&repo_root, &exec, &event_tx)?;
if !tests.passed {
plan.mark_todo(&step.id);
state.last_full_test_summary = Some(tests);
toon::write_plan(&config.plan_file, &plan)?;
toon::write_state(&config.state_file, &state)?;
continue;
}
plan.mark_done(&step.id);
state.complete_step(&step, verification, cleanup, tests);
toon::write_plan(&config.plan_file, &plan)?;
toon::write_state(&config.state_file, &state)?;
}
Ok(())
}