move startup orchestration from mind to event_loop

The top-level run() that creates Mind, wires channels, spawns the
Mind event loop, and starts the UI event loop is orchestration —
it belongs with the UI entry point, not in the cognitive layer.

Renamed to event_loop::start(cli). mind/mod.rs is now purely the
Mind struct: state machine, MindCommand, and the run loop.

Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
Kent Overstreet 2026-04-05 04:29:56 -04:00
parent e449cda40f
commit 57b0f94b54
3 changed files with 38 additions and 40 deletions

View file

@ -24,10 +24,9 @@ use tokio::sync::{mpsc, Mutex};
use crate::agent::{Agent, TurnResult}; use crate::agent::{Agent, TurnResult};
use crate::agent::api::ApiClient; use crate::agent::api::ApiClient;
use crate::config::{self, AppConfig, SessionConfig}; use crate::config::{AppConfig, SessionConfig};
use crate::user::{self as tui};
use crate::user::ui_channel::{self, StreamTarget};
use crate::subconscious::learn; use crate::subconscious::learn;
use crate::user::ui_channel::{self, StreamTarget};
/// Compaction threshold — context is rebuilt when prompt tokens exceed this. /// Compaction threshold — context is rebuilt when prompt tokens exceed this.
fn compaction_threshold(app: &AppConfig) -> u32 { fn compaction_threshold(app: &AppConfig) -> u32 {
@ -92,7 +91,7 @@ impl MindState {
} }
/// Consume pending input, return a Turn command if ready. /// Consume pending input, return a Turn command if ready.
pub fn take_pending_input(&mut self) -> MindCommand { fn take_pending_input(&mut self) -> MindCommand {
if self.turn_active || self.input.is_empty() { if self.turn_active || self.input.is_empty() {
return MindCommand::None; return MindCommand::None;
} }
@ -106,7 +105,7 @@ impl MindState {
} }
/// Process turn completion, return model switch name if requested. /// Process turn completion, return model switch name if requested.
pub fn complete_turn(&mut self, result: &Result<TurnResult>, target: StreamTarget) -> Option<String> { fn complete_turn(&mut self, result: &Result<TurnResult>, target: StreamTarget) -> Option<String> {
self.turn_active = false; self.turn_active = false;
match result { match result {
Ok(turn_result) => { Ok(turn_result) => {
@ -137,7 +136,7 @@ impl MindState {
} }
/// DMN tick — returns a Turn action with the DMN prompt, or None. /// DMN tick — returns a Turn action with the DMN prompt, or None.
pub fn dmn_tick(&mut self) -> MindCommand { fn dmn_tick(&mut self) -> MindCommand {
if matches!(self.dmn, dmn::State::Paused | dmn::State::Off) { if matches!(self.dmn, dmn::State::Paused | dmn::State::Off) {
return MindCommand::None; return MindCommand::None;
} }
@ -158,7 +157,7 @@ impl MindState {
MindCommand::Turn(prompt, StreamTarget::Autonomous) MindCommand::Turn(prompt, StreamTarget::Autonomous)
} }
pub fn interrupt(&mut self) { fn interrupt(&mut self) {
self.input.clear(); self.input.clear();
self.dmn = dmn::State::Resting { since: Instant::now() }; self.dmn = dmn::State::Resting { since: Instant::now() };
} }
@ -383,35 +382,3 @@ impl Mind {
} }
} }
} }
// --- Startup ---
pub async fn run(cli: crate::user::CliArgs) -> Result<()> {
let (config, _figment) = config::load_session(&cli)?;
if config.app.debug {
unsafe { std::env::set_var("POC_DEBUG", "1") };
}
let (ui_tx, ui_rx) = ui_channel::channel();
let (turn_tx, turn_rx) = mpsc::channel::<(Result<TurnResult>, StreamTarget)>(1);
let (mind_tx, mind_rx) = tokio::sync::mpsc::unbounded_channel();
let mut mind = Mind::new(config, ui_tx.clone(), turn_tx);
mind.init().await;
let ui_agent = mind.agent.clone();
let shared_mind = mind.shared.clone();
let shared_context = mind.agent.lock().await.shared_context.clone();
let shared_active_tools = mind.agent.lock().await.active_tools.clone();
let turn_watch = mind.turn_watch();
tokio::spawn(async move {
mind.run(mind_rx, turn_rx).await;
});
crate::user::event_loop::run(
tui::App::new(String::new(), shared_context, shared_active_tools),
ui_agent, shared_mind, turn_watch, mind_tx, ui_tx, ui_rx,
).await
}

View file

@ -18,6 +18,37 @@ use crate::user::ui_channel::{self, UiMessage};
pub use crate::mind::MindCommand; pub use crate::mind::MindCommand;
/// Top-level entry point — creates Mind and UI, wires them together.
pub async fn start(cli: crate::user::CliArgs) -> Result<()> {
let (config, _figment) = crate::config::load_session(&cli)?;
if config.app.debug {
unsafe { std::env::set_var("POC_DEBUG", "1") };
}
let (ui_tx, ui_rx) = ui_channel::channel();
let (turn_tx, turn_rx) = tokio::sync::mpsc::channel(1);
let (mind_tx, mind_rx) = tokio::sync::mpsc::unbounded_channel();
let mut mind = crate::mind::Mind::new(config, ui_tx.clone(), turn_tx);
mind.init().await;
let ui_agent = mind.agent.clone();
let shared_mind = mind.shared.clone();
let shared_context = mind.agent.lock().await.shared_context.clone();
let shared_active_tools = mind.agent.lock().await.active_tools.clone();
let turn_watch = mind.turn_watch();
tokio::spawn(async move {
mind.run(mind_rx, turn_rx).await;
});
run(
tui::App::new(String::new(), shared_context, shared_active_tools),
ui_agent, shared_mind, turn_watch, mind_tx, ui_tx, ui_rx,
).await
}
fn send_help(ui_tx: &ui_channel::UiSender) { fn send_help(ui_tx: &ui_channel::UiSender) {
let commands = &[ let commands = &[
("/quit", "Exit consciousness"), ("/quit", "Exit consciousness"),

View file

@ -757,7 +757,7 @@ pub async fn main() {
return; return;
} }
if let Err(e) = crate::mind::run(cli).await { if let Err(e) = crate::user::event_loop::start(cli).await {
let _ = crossterm::terminal::disable_raw_mode(); let _ = crossterm::terminal::disable_raw_mode();
let _ = crossterm::execute!( let _ = crossterm::execute!(
std::io::stdout(), std::io::stdout(),