feat: ui
This commit is contained in:
206
src/controller/engine.rs
Normal file
206
src/controller/engine.rs
Normal 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user