2026-04-04 02:46:32 -04:00
|
|
|
// mind/ — Cognitive layer
|
|
|
|
|
//
|
2026-04-05 01:55:14 -04:00
|
|
|
// Mind state machine, DMN, identity, observation socket.
|
2026-04-04 02:46:32 -04:00
|
|
|
// Everything about how the mind operates, separate from the
|
|
|
|
|
// user interface (TUI, CLI) and the agent execution (tools, API).
|
|
|
|
|
|
|
|
|
|
pub mod dmn;
|
|
|
|
|
pub mod identity;
|
2026-04-05 01:48:11 -04:00
|
|
|
pub mod log;
|
2026-04-04 02:46:32 -04:00
|
|
|
|
2026-04-05 01:55:14 -04:00
|
|
|
// consciousness.rs — Mind state machine and event loop
|
2026-04-04 02:46:32 -04:00
|
|
|
//
|
2026-04-05 01:55:14 -04:00
|
|
|
// The core runtime for the consciousness binary. Mind manages turns,
|
2026-04-04 02:46:32 -04:00
|
|
|
// DMN state, compaction, scoring, and slash commands. The event loop
|
2026-04-05 01:55:14 -04:00
|
|
|
// bridges Mind (cognitive state) with App (TUI rendering).
|
2026-04-04 02:46:32 -04:00
|
|
|
//
|
|
|
|
|
// The event loop uses biased select! so priorities are deterministic:
|
|
|
|
|
// keyboard events > turn results > render ticks > DMN timer > UI messages.
|
|
|
|
|
|
|
|
|
|
use anyhow::Result;
|
|
|
|
|
use std::sync::Arc;
|
2026-04-05 03:46:29 -04:00
|
|
|
use std::time::Instant;
|
2026-04-05 19:56:56 -04:00
|
|
|
use tokio::sync::mpsc;
|
2026-04-04 02:46:32 -04:00
|
|
|
use crate::agent::{Agent, TurnResult};
|
|
|
|
|
use crate::agent::api::ApiClient;
|
2026-04-07 01:33:07 -04:00
|
|
|
use crate::agent::oneshot::{AutoAgent, AutoStep};
|
2026-04-05 04:29:56 -04:00
|
|
|
use crate::config::{AppConfig, SessionConfig};
|
2026-04-07 01:33:07 -04:00
|
|
|
use crate::subconscious::{defs, learn};
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Subconscious agents — forked from conscious agent, run on schedule
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
/// A subconscious agent managed by Mind.
|
|
|
|
|
struct SubconsciousAgent {
|
2026-04-07 01:57:01 -04:00
|
|
|
auto: AutoAgent,
|
2026-04-07 01:33:07 -04:00
|
|
|
/// Conversation bytes at last trigger.
|
|
|
|
|
last_trigger_bytes: u64,
|
|
|
|
|
/// When the agent last ran.
|
|
|
|
|
last_run: Option<Instant>,
|
2026-04-07 01:57:01 -04:00
|
|
|
/// Running task handle.
|
2026-04-07 01:33:07 -04:00
|
|
|
handle: Option<tokio::task::JoinHandle<Result<String, String>>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Names and byte-interval triggers for the built-in subconscious agents.
|
|
|
|
|
const SUBCONSCIOUS_AGENTS: &[(&str, u64)] = &[
|
2026-04-07 02:25:11 -04:00
|
|
|
("subconscious-surface", 0), // every trigger
|
|
|
|
|
("subconscious-observe", 0), // every trigger (after surface)
|
|
|
|
|
("subconscious-thalamus", 0), // every trigger
|
|
|
|
|
("subconscious-journal", 20_000), // every ~20KB of conversation
|
|
|
|
|
("subconscious-reflect", 100_000), // every ~100KB of conversation
|
2026-04-07 01:33:07 -04:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
impl SubconsciousAgent {
|
2026-04-07 01:57:01 -04:00
|
|
|
fn new(name: &str, _interval_bytes: u64) -> Option<Self> {
|
2026-04-07 01:33:07 -04:00
|
|
|
let def = defs::get_def(name)?;
|
2026-04-07 01:57:01 -04:00
|
|
|
|
|
|
|
|
let all_tools = crate::agent::tools::memory_and_journal_tools();
|
|
|
|
|
let tools: Vec<crate::agent::tools::Tool> = if def.tools.is_empty() {
|
|
|
|
|
all_tools.to_vec()
|
|
|
|
|
} else {
|
|
|
|
|
all_tools.into_iter()
|
|
|
|
|
.filter(|t| def.tools.iter().any(|w| w == t.name))
|
|
|
|
|
.collect()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let steps: Vec<AutoStep> = def.steps.iter().map(|s| AutoStep {
|
|
|
|
|
prompt: s.prompt.clone(),
|
|
|
|
|
phase: s.phase.clone(),
|
|
|
|
|
}).collect();
|
|
|
|
|
|
|
|
|
|
let auto = AutoAgent::new(
|
|
|
|
|
name.to_string(), tools, steps,
|
|
|
|
|
def.temperature.unwrap_or(0.6), def.priority,
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-07 01:33:07 -04:00
|
|
|
Some(Self {
|
2026-04-07 01:57:01 -04:00
|
|
|
auto,
|
2026-04-07 01:33:07 -04:00
|
|
|
last_trigger_bytes: 0,
|
|
|
|
|
last_run: None,
|
|
|
|
|
handle: None,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn is_running(&self) -> bool {
|
|
|
|
|
self.handle.as_ref().is_some_and(|h| !h.is_finished())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn should_trigger(&self, conversation_bytes: u64, interval: u64) -> bool {
|
|
|
|
|
if self.is_running() { return false; }
|
2026-04-07 01:57:01 -04:00
|
|
|
if interval == 0 { return true; }
|
2026-04-07 01:33:07 -04:00
|
|
|
conversation_bytes.saturating_sub(self.last_trigger_bytes) >= interval
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-07 01:41:11 -04:00
|
|
|
|
2026-04-07 02:13:06 -04:00
|
|
|
/// State shared between all subconscious agents. Lives on Mind,
|
|
|
|
|
/// passed to agents at run time. Enables splitting surface/observe
|
|
|
|
|
/// into separate agents that share walked keys.
|
|
|
|
|
#[derive(Clone, Default)]
|
|
|
|
|
pub struct SubconsciousSharedState {
|
|
|
|
|
pub walked: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 01:59:09 -04:00
|
|
|
/// Lightweight snapshot of subconscious agent state for the TUI.
|
|
|
|
|
#[derive(Clone, Default)]
|
|
|
|
|
pub struct SubconsciousSnapshot {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub running: bool,
|
|
|
|
|
pub current_phase: String,
|
|
|
|
|
pub turn: usize,
|
|
|
|
|
pub last_run_secs_ago: Option<f64>,
|
2026-04-07 02:08:48 -04:00
|
|
|
/// Entries from the last forked run (after fork point).
|
|
|
|
|
pub last_run_entries: Vec<crate::agent::context::ConversationEntry>,
|
2026-04-07 01:59:09 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SubconsciousAgent {
|
|
|
|
|
fn snapshot(&self) -> SubconsciousSnapshot {
|
|
|
|
|
SubconsciousSnapshot {
|
|
|
|
|
name: self.auto.name.clone(),
|
|
|
|
|
running: self.is_running(),
|
|
|
|
|
current_phase: self.auto.current_phase.clone(),
|
|
|
|
|
turn: self.auto.turn,
|
|
|
|
|
last_run_secs_ago: self.last_run.map(|t| t.elapsed().as_secs_f64()),
|
2026-04-07 02:08:48 -04:00
|
|
|
last_run_entries: self.auto.last_run_entries.clone(),
|
2026-04-07 01:59:09 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 22:34:48 -04:00
|
|
|
/// Which pane streaming text should go to.
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
|
|
|
pub enum StreamTarget {
|
|
|
|
|
/// User-initiated turn — text goes to conversation pane.
|
|
|
|
|
Conversation,
|
|
|
|
|
/// DMN-initiated turn — text goes to autonomous pane.
|
|
|
|
|
Autonomous,
|
|
|
|
|
}
|
2026-04-04 02:46:32 -04:00
|
|
|
|
|
|
|
|
/// Compaction threshold — context is rebuilt when prompt tokens exceed this.
|
|
|
|
|
fn compaction_threshold(app: &AppConfig) -> u32 {
|
|
|
|
|
(crate::agent::context::context_window() as u32) * app.compaction.hard_threshold_pct / 100
|
|
|
|
|
}
|
|
|
|
|
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
/// Shared state between Mind and UI.
|
2026-04-05 02:52:56 -04:00
|
|
|
pub struct MindState {
|
|
|
|
|
/// Pending user input — UI pushes, Mind consumes after turn completes.
|
|
|
|
|
pub input: Vec<String>,
|
|
|
|
|
/// True while a turn is in progress.
|
|
|
|
|
pub turn_active: bool,
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
/// DMN state
|
|
|
|
|
pub dmn: dmn::State,
|
|
|
|
|
pub dmn_turns: u32,
|
|
|
|
|
pub max_dmn_turns: u32,
|
2026-04-05 03:34:43 -04:00
|
|
|
/// Whether memory scoring is running.
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
pub scoring_in_flight: bool,
|
2026-04-05 03:34:43 -04:00
|
|
|
/// Whether compaction is running.
|
|
|
|
|
pub compaction_in_flight: bool,
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
/// Per-turn tracking
|
|
|
|
|
pub last_user_input: Instant,
|
|
|
|
|
pub consecutive_errors: u32,
|
|
|
|
|
pub last_turn_had_tools: bool,
|
2026-04-05 04:42:50 -04:00
|
|
|
/// Handle to the currently running turn task.
|
|
|
|
|
pub turn_handle: Option<tokio::task::JoinHandle<()>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Clone for MindState {
|
|
|
|
|
fn clone(&self) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
input: self.input.clone(),
|
|
|
|
|
turn_active: self.turn_active,
|
|
|
|
|
dmn: self.dmn.clone(),
|
|
|
|
|
dmn_turns: self.dmn_turns,
|
|
|
|
|
max_dmn_turns: self.max_dmn_turns,
|
|
|
|
|
scoring_in_flight: self.scoring_in_flight,
|
|
|
|
|
compaction_in_flight: self.compaction_in_flight,
|
|
|
|
|
last_user_input: self.last_user_input,
|
|
|
|
|
consecutive_errors: self.consecutive_errors,
|
|
|
|
|
last_turn_had_tools: self.last_turn_had_tools,
|
|
|
|
|
turn_handle: None, // Not cloned — only Mind's loop uses this
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-05 02:52:56 -04:00
|
|
|
}
|
|
|
|
|
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
/// What should happen after a state transition.
|
2026-04-05 03:34:43 -04:00
|
|
|
pub enum MindCommand {
|
|
|
|
|
/// Run compaction check
|
|
|
|
|
Compact,
|
|
|
|
|
/// Run memory scoring
|
|
|
|
|
Score,
|
2026-04-05 03:41:47 -04:00
|
|
|
/// Abort current turn, kill processes
|
|
|
|
|
Interrupt,
|
2026-04-05 03:34:43 -04:00
|
|
|
/// Reset session
|
|
|
|
|
NewSession,
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
/// Nothing to do
|
|
|
|
|
None,
|
2026-04-05 02:52:56 -04:00
|
|
|
}
|
|
|
|
|
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
impl MindState {
|
|
|
|
|
pub fn new(max_dmn_turns: u32) -> Self {
|
2026-04-04 02:46:32 -04:00
|
|
|
Self {
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
input: Vec::new(),
|
|
|
|
|
turn_active: false,
|
|
|
|
|
dmn: if dmn::is_off() { dmn::State::Off }
|
|
|
|
|
else { dmn::State::Resting { since: Instant::now() } },
|
2026-04-04 02:46:32 -04:00
|
|
|
dmn_turns: 0,
|
|
|
|
|
max_dmn_turns,
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
scoring_in_flight: false,
|
2026-04-05 03:34:43 -04:00
|
|
|
compaction_in_flight: false,
|
2026-04-04 02:46:32 -04:00
|
|
|
last_user_input: Instant::now(),
|
|
|
|
|
consecutive_errors: 0,
|
|
|
|
|
last_turn_had_tools: false,
|
2026-04-05 04:42:50 -04:00
|
|
|
turn_handle: None,
|
2026-04-04 02:46:32 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 20:34:51 -04:00
|
|
|
/// Consume pending user input if no turn is active.
|
|
|
|
|
/// Returns the text to send; caller is responsible for pushing it
|
|
|
|
|
/// into the Agent's context and starting the turn.
|
|
|
|
|
fn take_pending_input(&mut self) -> Option<String> {
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
if self.turn_active || self.input.is_empty() {
|
2026-04-06 20:34:51 -04:00
|
|
|
return None;
|
2026-04-04 02:46:32 -04:00
|
|
|
}
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
let text = self.input.join("\n");
|
|
|
|
|
self.input.clear();
|
2026-04-05 02:52:56 -04:00
|
|
|
self.dmn_turns = 0;
|
|
|
|
|
self.consecutive_errors = 0;
|
|
|
|
|
self.last_user_input = Instant::now();
|
|
|
|
|
self.dmn = dmn::State::Engaged;
|
2026-04-06 20:34:51 -04:00
|
|
|
Some(text)
|
2026-04-04 02:46:32 -04:00
|
|
|
}
|
|
|
|
|
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
/// Process turn completion, return model switch name if requested.
|
2026-04-05 04:29:56 -04:00
|
|
|
fn complete_turn(&mut self, result: &Result<TurnResult>, target: StreamTarget) -> Option<String> {
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
self.turn_active = false;
|
2026-04-04 02:46:32 -04:00
|
|
|
match result {
|
|
|
|
|
Ok(turn_result) => {
|
|
|
|
|
if turn_result.tool_errors > 0 {
|
|
|
|
|
self.consecutive_errors += turn_result.tool_errors;
|
|
|
|
|
} else {
|
|
|
|
|
self.consecutive_errors = 0;
|
|
|
|
|
}
|
|
|
|
|
self.last_turn_had_tools = turn_result.had_tool_calls;
|
|
|
|
|
self.dmn = dmn::transition(
|
|
|
|
|
&self.dmn,
|
|
|
|
|
turn_result.yield_requested,
|
|
|
|
|
turn_result.had_tool_calls,
|
|
|
|
|
target == StreamTarget::Conversation,
|
|
|
|
|
);
|
|
|
|
|
if turn_result.dmn_pause {
|
|
|
|
|
self.dmn = dmn::State::Paused;
|
|
|
|
|
self.dmn_turns = 0;
|
|
|
|
|
}
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
turn_result.model_switch.clone()
|
2026-04-04 02:46:32 -04:00
|
|
|
}
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
Err(_) => {
|
2026-04-04 02:46:32 -04:00
|
|
|
self.consecutive_errors += 1;
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
self.dmn = dmn::State::Resting { since: Instant::now() };
|
|
|
|
|
None
|
2026-04-04 02:46:32 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 20:34:51 -04:00
|
|
|
/// DMN tick — returns a prompt and target if we should run a turn.
|
|
|
|
|
fn dmn_tick(&mut self) -> Option<(String, StreamTarget)> {
|
2026-04-04 02:46:32 -04:00
|
|
|
if matches!(self.dmn, dmn::State::Paused | dmn::State::Off) {
|
2026-04-06 20:34:51 -04:00
|
|
|
return None;
|
2026-04-04 02:46:32 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.dmn_turns += 1;
|
|
|
|
|
if self.dmn_turns > self.max_dmn_turns {
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
self.dmn = dmn::State::Resting { since: Instant::now() };
|
2026-04-04 02:46:32 -04:00
|
|
|
self.dmn_turns = 0;
|
2026-04-06 20:34:51 -04:00
|
|
|
return None;
|
2026-04-04 02:46:32 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let dmn_ctx = dmn::DmnContext {
|
|
|
|
|
user_idle: self.last_user_input.elapsed(),
|
|
|
|
|
consecutive_errors: self.consecutive_errors,
|
|
|
|
|
last_turn_had_tools: self.last_turn_had_tools,
|
|
|
|
|
};
|
|
|
|
|
let prompt = self.dmn.prompt(&dmn_ctx);
|
2026-04-06 20:34:51 -04:00
|
|
|
Some((prompt, StreamTarget::Autonomous))
|
mind: move all slash commands to event_loop dispatch
All slash command routing now lives in user/event_loop.rs. Mind
receives typed messages (NewSession, Score, DmnSleep, etc.) and
handles them as named methods. No more handle_command() dispatch
table or Command enum.
Commands that only need Agent state (/model, /retry) run directly
in the UI task. Commands that need Mind state (/new, /score, /dmn,
/sleep, /wake, /pause) send a MindMessage.
Mind is now purely: turn lifecycle, DMN state machine, and the
named handlers for each message type.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 02:40:45 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 04:29:56 -04:00
|
|
|
fn interrupt(&mut self) {
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
self.input.clear();
|
|
|
|
|
self.dmn = dmn::State::Resting { since: Instant::now() };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 05:01:45 -04:00
|
|
|
/// Background task completion events.
|
|
|
|
|
enum BgEvent {
|
|
|
|
|
ScoringDone,
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Mind: cognitive state machine ---
|
|
|
|
|
|
2026-04-05 05:01:45 -04:00
|
|
|
pub type SharedMindState = std::sync::Mutex<MindState>;
|
|
|
|
|
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
pub struct Mind {
|
2026-04-05 21:13:48 -04:00
|
|
|
pub agent: Arc<tokio::sync::Mutex<Agent>>,
|
|
|
|
|
pub shared: Arc<SharedMindState>,
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
pub config: SessionConfig,
|
2026-04-07 01:57:01 -04:00
|
|
|
subconscious: Arc<tokio::sync::Mutex<Vec<SubconsciousAgent>>>,
|
2026-04-07 02:13:06 -04:00
|
|
|
subconscious_state: Arc<tokio::sync::Mutex<SubconsciousSharedState>>,
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
turn_tx: mpsc::Sender<(Result<TurnResult>, StreamTarget)>,
|
|
|
|
|
turn_watch: tokio::sync::watch::Sender<bool>,
|
2026-04-05 05:01:45 -04:00
|
|
|
bg_tx: mpsc::UnboundedSender<BgEvent>,
|
|
|
|
|
bg_rx: std::sync::Mutex<Option<mpsc::UnboundedReceiver<BgEvent>>>,
|
2026-04-05 04:32:11 -04:00
|
|
|
_supervisor: crate::thalamus::supervisor::Supervisor,
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Mind {
|
|
|
|
|
pub fn new(
|
|
|
|
|
config: SessionConfig,
|
|
|
|
|
turn_tx: mpsc::Sender<(Result<TurnResult>, StreamTarget)>,
|
|
|
|
|
) -> Self {
|
2026-04-05 22:34:48 -04:00
|
|
|
let shared_context = crate::agent::context::shared_context_state();
|
|
|
|
|
let shared_active_tools = crate::agent::tools::shared_active_tools();
|
2026-04-05 04:20:49 -04:00
|
|
|
|
|
|
|
|
let client = ApiClient::new(&config.api_base, &config.api_key, &config.model);
|
|
|
|
|
let conversation_log = log::ConversationLog::new(
|
|
|
|
|
config.session_dir.join("conversation.jsonl"),
|
|
|
|
|
).ok();
|
|
|
|
|
|
2026-04-06 21:48:12 -04:00
|
|
|
let ag = Agent::new(
|
2026-04-05 04:20:49 -04:00
|
|
|
client,
|
|
|
|
|
config.system_prompt.clone(),
|
|
|
|
|
config.context_parts.clone(),
|
|
|
|
|
config.app.clone(),
|
|
|
|
|
config.prompt_file.clone(),
|
|
|
|
|
conversation_log,
|
|
|
|
|
shared_context,
|
|
|
|
|
shared_active_tools,
|
2026-04-06 21:48:12 -04:00
|
|
|
);
|
|
|
|
|
let agent = Arc::new(tokio::sync::Mutex::new(ag));
|
2026-04-05 04:20:49 -04:00
|
|
|
|
2026-04-07 01:33:07 -04:00
|
|
|
let subconscious = SUBCONSCIOUS_AGENTS.iter()
|
|
|
|
|
.filter_map(|(name, interval)| SubconsciousAgent::new(name, *interval))
|
|
|
|
|
.collect();
|
|
|
|
|
|
2026-04-05 21:13:48 -04:00
|
|
|
let shared = Arc::new(std::sync::Mutex::new(MindState::new(config.app.dmn.max_turns)));
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
let (turn_watch, _) = tokio::sync::watch::channel(false);
|
2026-04-05 05:01:45 -04:00
|
|
|
let (bg_tx, bg_rx) = mpsc::unbounded_channel();
|
2026-04-05 04:32:11 -04:00
|
|
|
|
|
|
|
|
let mut sup = crate::thalamus::supervisor::Supervisor::new();
|
|
|
|
|
sup.load_config();
|
|
|
|
|
sup.ensure_running();
|
|
|
|
|
|
2026-04-07 02:13:06 -04:00
|
|
|
let subconscious_state = Arc::new(tokio::sync::Mutex::new(SubconsciousSharedState::default()));
|
|
|
|
|
Self { agent, shared, config,
|
|
|
|
|
subconscious: Arc::new(tokio::sync::Mutex::new(subconscious)),
|
|
|
|
|
subconscious_state,
|
2026-04-06 21:48:12 -04:00
|
|
|
turn_tx, turn_watch, bg_tx,
|
2026-04-05 05:01:45 -04:00
|
|
|
bg_rx: std::sync::Mutex::new(Some(bg_rx)), _supervisor: sup }
|
2026-04-04 02:46:32 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 04:17:04 -04:00
|
|
|
/// Initialize — restore log, start daemons and background agents.
|
2026-04-07 02:13:06 -04:00
|
|
|
pub async fn subconscious_snapshots(&self) -> (Vec<SubconsciousSnapshot>, SubconsciousSharedState) {
|
|
|
|
|
let snaps = self.subconscious.lock().await.iter().map(|s| s.snapshot()).collect();
|
|
|
|
|
let shared = self.subconscious_state.lock().await.clone();
|
|
|
|
|
(snaps, shared)
|
2026-04-07 01:59:09 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 04:42:50 -04:00
|
|
|
pub async fn init(&self) {
|
2026-04-05 04:17:04 -04:00
|
|
|
// Restore conversation
|
2026-04-05 21:13:48 -04:00
|
|
|
let mut ag = self.agent.lock().await;
|
2026-04-05 04:02:16 -04:00
|
|
|
ag.restore_from_log();
|
2026-04-05 23:04:10 -04:00
|
|
|
ag.changed.notify_one();
|
2026-04-05 04:02:16 -04:00
|
|
|
drop(ag);
|
|
|
|
|
}
|
|
|
|
|
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
pub fn turn_watch(&self) -> tokio::sync::watch::Receiver<bool> {
|
|
|
|
|
self.turn_watch.subscribe()
|
2026-04-04 02:46:32 -04:00
|
|
|
}
|
|
|
|
|
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
/// Execute an Action from a MindState method.
|
2026-04-05 04:42:50 -04:00
|
|
|
async fn run_commands(&self, cmds: Vec<MindCommand>) {
|
2026-04-05 03:34:43 -04:00
|
|
|
for cmd in cmds {
|
|
|
|
|
match cmd {
|
|
|
|
|
MindCommand::None => {}
|
2026-04-05 03:43:53 -04:00
|
|
|
MindCommand::Compact => {
|
2026-04-06 20:34:51 -04:00
|
|
|
let threshold = compaction_threshold(&self.config.app) as usize;
|
2026-04-05 21:13:48 -04:00
|
|
|
let mut ag = self.agent.lock().await;
|
2026-04-06 20:34:51 -04:00
|
|
|
let sections = ag.shared_context.read().map(|s| s.clone()).unwrap_or_default();
|
|
|
|
|
if crate::agent::context::sections_used(§ions) > threshold {
|
2026-04-05 03:43:53 -04:00
|
|
|
ag.compact();
|
2026-04-05 22:18:07 -04:00
|
|
|
ag.notify("compacted");
|
2026-04-05 03:43:53 -04:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-05 03:34:43 -04:00
|
|
|
MindCommand::Score => {
|
|
|
|
|
let mut s = self.shared.lock().unwrap();
|
|
|
|
|
if !s.scoring_in_flight {
|
|
|
|
|
s.scoring_in_flight = true;
|
|
|
|
|
drop(s);
|
|
|
|
|
self.start_memory_scoring();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-05 03:41:47 -04:00
|
|
|
MindCommand::Interrupt => {
|
2026-04-05 03:34:43 -04:00
|
|
|
self.shared.lock().unwrap().interrupt();
|
2026-04-05 21:13:48 -04:00
|
|
|
let ag = self.agent.lock().await;
|
2026-04-05 03:34:43 -04:00
|
|
|
let mut tools = ag.active_tools.lock().unwrap();
|
|
|
|
|
for entry in tools.drain(..) { entry.handle.abort(); }
|
|
|
|
|
drop(tools); drop(ag);
|
2026-04-05 04:42:50 -04:00
|
|
|
if let Some(h) = self.shared.lock().unwrap().turn_handle.take() { h.abort(); }
|
2026-04-05 03:34:43 -04:00
|
|
|
self.shared.lock().unwrap().turn_active = false;
|
|
|
|
|
let _ = self.turn_watch.send(false);
|
|
|
|
|
}
|
|
|
|
|
MindCommand::NewSession => {
|
2026-04-05 03:46:29 -04:00
|
|
|
{
|
|
|
|
|
let mut s = self.shared.lock().unwrap();
|
|
|
|
|
s.dmn = dmn::State::Resting { since: Instant::now() };
|
|
|
|
|
s.dmn_turns = 0;
|
|
|
|
|
}
|
2026-04-05 03:34:43 -04:00
|
|
|
let new_log = log::ConversationLog::new(
|
|
|
|
|
self.config.session_dir.join("conversation.jsonl"),
|
|
|
|
|
).ok();
|
2026-04-05 21:13:48 -04:00
|
|
|
let mut ag = self.agent.lock().await;
|
2026-04-05 03:34:43 -04:00
|
|
|
let shared_ctx = ag.shared_context.clone();
|
|
|
|
|
let shared_tools = ag.active_tools.clone();
|
|
|
|
|
*ag = Agent::new(
|
|
|
|
|
ApiClient::new(&self.config.api_base, &self.config.api_key, &self.config.model),
|
|
|
|
|
self.config.system_prompt.clone(), self.config.context_parts.clone(),
|
|
|
|
|
self.config.app.clone(), self.config.prompt_file.clone(),
|
|
|
|
|
new_log, shared_ctx, shared_tools,
|
|
|
|
|
);
|
|
|
|
|
}
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn start_memory_scoring(&self) {
|
|
|
|
|
let agent = self.agent.clone();
|
2026-04-05 05:01:45 -04:00
|
|
|
let bg_tx = self.bg_tx.clone();
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
let cfg = crate::config::get();
|
|
|
|
|
let max_age = cfg.scoring_interval_secs;
|
|
|
|
|
let response_window = cfg.scoring_response_window;
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
|
let (context, client) = {
|
2026-04-05 21:13:48 -04:00
|
|
|
let mut ag = agent.lock().await;
|
2026-04-06 21:48:12 -04:00
|
|
|
if ag.memory_scoring_in_flight { return; }
|
|
|
|
|
ag.memory_scoring_in_flight = true;
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
(ag.context.clone(), ag.client_clone())
|
|
|
|
|
};
|
|
|
|
|
let result = learn::score_memories_incremental(
|
2026-04-05 22:34:48 -04:00
|
|
|
&context, max_age as i64, response_window, &client, &agent,
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
).await;
|
|
|
|
|
{
|
2026-04-05 21:13:48 -04:00
|
|
|
let mut ag = agent.lock().await;
|
2026-04-06 21:48:12 -04:00
|
|
|
ag.memory_scoring_in_flight = false;
|
|
|
|
|
if let Ok(ref scores) = result { ag.memory_scores = scores.clone(); }
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
}
|
2026-04-05 05:01:45 -04:00
|
|
|
let _ = bg_tx.send(BgEvent::ScoringDone);
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 20:34:51 -04:00
|
|
|
/// Push user/DMN message into the agent's context and spawn a turn.
|
|
|
|
|
/// The text moves from pending_input to ContextState atomically —
|
|
|
|
|
/// by the time this returns, the message is in context and the turn
|
|
|
|
|
/// is running.
|
2026-04-07 01:33:07 -04:00
|
|
|
/// Collect results from finished subconscious agents and inject
|
|
|
|
|
/// their output into the conscious agent's context.
|
|
|
|
|
async fn collect_subconscious_results(&self) {
|
|
|
|
|
// Collect finished handles without holding the lock across await
|
2026-04-07 01:57:01 -04:00
|
|
|
let finished: Vec<(usize, tokio::task::JoinHandle<Result<String, String>>)> = {
|
2026-04-07 01:33:07 -04:00
|
|
|
let mut subs = self.subconscious.lock().await;
|
2026-04-07 01:57:01 -04:00
|
|
|
subs.iter_mut().enumerate().filter_map(|(i, sub)| {
|
2026-04-07 01:33:07 -04:00
|
|
|
if sub.handle.as_ref().is_some_and(|h| h.is_finished()) {
|
|
|
|
|
sub.last_run = Some(Instant::now());
|
2026-04-07 01:57:01 -04:00
|
|
|
Some((i, sub.handle.take().unwrap()))
|
2026-04-07 01:33:07 -04:00
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}).collect()
|
|
|
|
|
};
|
2026-04-06 21:48:12 -04:00
|
|
|
|
2026-04-07 01:57:01 -04:00
|
|
|
for (idx, handle) in finished {
|
2026-04-07 01:33:07 -04:00
|
|
|
match handle.await {
|
2026-04-07 02:04:29 -04:00
|
|
|
Ok(Ok(_)) => {
|
|
|
|
|
// The outer task already put the AutoAgent back —
|
|
|
|
|
// read outputs from it
|
|
|
|
|
let mut subs = self.subconscious.lock().await;
|
|
|
|
|
let name = subs[idx].auto.name.clone();
|
|
|
|
|
let outputs = std::mem::take(&mut subs[idx].auto.outputs);
|
|
|
|
|
|
2026-04-07 02:13:06 -04:00
|
|
|
// Walked keys — update shared state
|
2026-04-07 02:04:29 -04:00
|
|
|
if let Some(walked_str) = outputs.get("walked") {
|
|
|
|
|
let walked: Vec<String> = walked_str.lines()
|
|
|
|
|
.map(|l| l.trim().to_string())
|
|
|
|
|
.filter(|l| !l.is_empty())
|
|
|
|
|
.collect();
|
2026-04-07 02:13:06 -04:00
|
|
|
self.subconscious_state.lock().await.walked = walked;
|
2026-04-07 02:04:29 -04:00
|
|
|
}
|
|
|
|
|
drop(subs);
|
|
|
|
|
|
|
|
|
|
// Surfaced memories → inject into conscious agent
|
|
|
|
|
if let Some(surface_str) = outputs.get("surface") {
|
2026-04-07 01:33:07 -04:00
|
|
|
let mut ag = self.agent.lock().await;
|
2026-04-07 02:04:29 -04:00
|
|
|
for key in surface_str.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
|
2026-04-07 01:33:07 -04:00
|
|
|
if let Some(rendered) = crate::cli::node::render_node(
|
|
|
|
|
&crate::store::Store::load().unwrap_or_default(), key,
|
|
|
|
|
) {
|
|
|
|
|
let mut msg = crate::agent::api::types::Message::user(format!(
|
|
|
|
|
"<system-reminder>\n--- {} (surfaced) ---\n{}\n</system-reminder>",
|
|
|
|
|
key, rendered,
|
|
|
|
|
));
|
|
|
|
|
msg.stamp();
|
|
|
|
|
ag.push_entry(crate::agent::context::ConversationEntry::Memory {
|
|
|
|
|
key: key.to_string(), message: msg,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 02:04:29 -04:00
|
|
|
// Reflection → inject into conscious agent
|
|
|
|
|
if let Some(reflection) = outputs.get("reflection") {
|
|
|
|
|
if !reflection.trim().is_empty() {
|
2026-04-07 01:33:07 -04:00
|
|
|
let mut ag = self.agent.lock().await;
|
|
|
|
|
ag.push_message(crate::agent::api::types::Message::user(format!(
|
|
|
|
|
"<system-reminder>\n--- subconscious reflection ---\n{}\n</system-reminder>",
|
2026-04-07 02:04:29 -04:00
|
|
|
reflection.trim(),
|
2026-04-07 01:33:07 -04:00
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 02:25:11 -04:00
|
|
|
// Thalamus nudge → inject into conscious agent
|
|
|
|
|
if let Some(nudge) = outputs.get("thalamus") {
|
|
|
|
|
let nudge = nudge.trim();
|
|
|
|
|
if !nudge.is_empty() && nudge != "ok" {
|
|
|
|
|
let mut ag = self.agent.lock().await;
|
|
|
|
|
ag.push_message(crate::agent::api::types::Message::user(format!(
|
|
|
|
|
"<system-reminder>\n--- thalamus ---\n{}\n</system-reminder>",
|
|
|
|
|
nudge,
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 01:33:07 -04:00
|
|
|
dbglog!("[mind] {} completed", name);
|
2026-04-06 21:48:12 -04:00
|
|
|
}
|
2026-04-07 02:04:29 -04:00
|
|
|
Ok(Err(e)) => dbglog!("[mind] subconscious agent failed: {}", e),
|
|
|
|
|
Err(e) => dbglog!("[mind] subconscious agent panicked: {}", e),
|
2026-04-06 21:48:12 -04:00
|
|
|
}
|
2026-04-07 01:33:07 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Trigger subconscious agents that are due to run.
|
|
|
|
|
async fn trigger_subconscious(&self) {
|
|
|
|
|
if self.config.no_agents { return; }
|
|
|
|
|
|
2026-04-07 01:57:01 -04:00
|
|
|
// Get conversation size + memory keys from conscious agent
|
|
|
|
|
let (conversation_bytes, memory_keys) = {
|
2026-04-07 01:33:07 -04:00
|
|
|
let ag = self.agent.lock().await;
|
2026-04-07 01:57:01 -04:00
|
|
|
let bytes = ag.context.entries.iter()
|
2026-04-07 01:33:07 -04:00
|
|
|
.filter(|e| !e.is_log() && !e.is_memory())
|
|
|
|
|
.map(|e| e.message().content_text().len() as u64)
|
2026-04-07 01:57:01 -04:00
|
|
|
.sum::<u64>();
|
|
|
|
|
let keys: Vec<String> = ag.context.entries.iter().filter_map(|e| {
|
2026-04-07 01:43:00 -04:00
|
|
|
if let crate::agent::context::ConversationEntry::Memory { key, .. } = e {
|
|
|
|
|
Some(key.clone())
|
2026-04-07 01:57:01 -04:00
|
|
|
} else { None }
|
|
|
|
|
}).collect();
|
|
|
|
|
(bytes, keys)
|
2026-04-07 01:41:11 -04:00
|
|
|
};
|
|
|
|
|
|
2026-04-07 01:57:01 -04:00
|
|
|
// Find which agents to trigger, take their AutoAgents out
|
|
|
|
|
let mut to_run: Vec<(usize, AutoAgent)> = Vec::new();
|
|
|
|
|
{
|
2026-04-07 01:33:07 -04:00
|
|
|
let mut subs = self.subconscious.lock().await;
|
|
|
|
|
for (i, &(_name, interval)) in SUBCONSCIOUS_AGENTS.iter().enumerate() {
|
|
|
|
|
if i >= subs.len() { continue; }
|
|
|
|
|
if !subs[i].should_trigger(conversation_bytes, interval) { continue; }
|
2026-04-07 01:57:01 -04:00
|
|
|
subs[i].last_trigger_bytes = conversation_bytes;
|
2026-04-07 01:33:07 -04:00
|
|
|
|
2026-04-07 01:57:01 -04:00
|
|
|
// Take the AutoAgent out — task owns it, returns it when done
|
|
|
|
|
let auto = std::mem::replace(&mut subs[i].auto,
|
|
|
|
|
AutoAgent::new(String::new(), vec![], vec![], 0.0, 0));
|
|
|
|
|
to_run.push((i, auto));
|
2026-04-06 21:48:12 -04:00
|
|
|
}
|
2026-04-07 01:57:01 -04:00
|
|
|
}
|
2026-04-06 21:48:12 -04:00
|
|
|
|
2026-04-07 01:57:01 -04:00
|
|
|
if to_run.is_empty() { return; }
|
2026-04-07 01:33:07 -04:00
|
|
|
|
2026-04-07 01:57:01 -04:00
|
|
|
// Fork from conscious agent and spawn tasks
|
2026-04-07 01:33:07 -04:00
|
|
|
let conscious = self.agent.lock().await;
|
2026-04-07 02:13:06 -04:00
|
|
|
let walked = self.subconscious_state.lock().await.walked.clone();
|
2026-04-07 01:33:07 -04:00
|
|
|
let mut spawns = Vec::new();
|
2026-04-07 01:57:01 -04:00
|
|
|
for (idx, mut auto) in to_run {
|
|
|
|
|
dbglog!("[mind] triggering {}", auto.name);
|
2026-04-07 01:33:07 -04:00
|
|
|
|
2026-04-07 01:57:01 -04:00
|
|
|
let forked = conscious.fork(auto.tools.clone());
|
|
|
|
|
let keys = memory_keys.clone();
|
2026-04-07 02:13:06 -04:00
|
|
|
let w = walked.clone();
|
2026-04-07 01:57:01 -04:00
|
|
|
let handle: tokio::task::JoinHandle<(AutoAgent, Result<String, String>)> =
|
|
|
|
|
tokio::spawn(async move {
|
2026-04-07 02:13:06 -04:00
|
|
|
let result = auto.run_forked(&forked, &keys, &w).await;
|
2026-04-07 01:57:01 -04:00
|
|
|
(auto, result)
|
|
|
|
|
});
|
2026-04-07 01:33:07 -04:00
|
|
|
spawns.push((idx, handle));
|
|
|
|
|
}
|
|
|
|
|
drop(conscious);
|
|
|
|
|
|
2026-04-07 01:57:01 -04:00
|
|
|
// Store handles (type-erased — we'll extract AutoAgent on completion)
|
|
|
|
|
// We need to store the JoinHandle that returns (AutoAgent, Result)
|
|
|
|
|
// but SubconsciousAgent.handle expects JoinHandle<Result<String, String>>.
|
|
|
|
|
// Wrap: spawn an outer task that extracts the result and puts back the AutoAgent.
|
|
|
|
|
let subconscious = self.subconscious.clone();
|
2026-04-07 01:33:07 -04:00
|
|
|
for (idx, handle) in spawns {
|
2026-04-07 01:57:01 -04:00
|
|
|
let subs = subconscious.clone();
|
|
|
|
|
let outer = tokio::spawn(async move {
|
|
|
|
|
let (auto, result) = handle.await.unwrap_or_else(
|
|
|
|
|
|e| (AutoAgent::new(String::new(), vec![], vec![], 0.0, 0),
|
|
|
|
|
Err(format!("task panicked: {}", e))));
|
|
|
|
|
// Put the AutoAgent back
|
|
|
|
|
let mut locked = subs.lock().await;
|
|
|
|
|
if idx < locked.len() {
|
|
|
|
|
locked[idx].auto = auto;
|
|
|
|
|
}
|
|
|
|
|
result
|
|
|
|
|
});
|
|
|
|
|
let mut subs = self.subconscious.lock().await;
|
2026-04-07 01:33:07 -04:00
|
|
|
if idx < subs.len() {
|
2026-04-07 01:57:01 -04:00
|
|
|
subs[idx].handle = Some(outer);
|
2026-04-07 01:33:07 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn start_turn(&self, text: &str, target: StreamTarget) {
|
|
|
|
|
{
|
|
|
|
|
let mut ag = self.agent.lock().await;
|
2026-04-06 21:48:12 -04:00
|
|
|
match target {
|
|
|
|
|
StreamTarget::Conversation => {
|
|
|
|
|
ag.push_message(crate::agent::api::types::Message::user(text));
|
|
|
|
|
}
|
|
|
|
|
StreamTarget::Autonomous => {
|
|
|
|
|
let mut msg = crate::agent::api::types::Message::user(text);
|
|
|
|
|
msg.stamp();
|
|
|
|
|
ag.push_entry(crate::agent::context::ConversationEntry::Dmn(msg));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compact if over budget before sending
|
|
|
|
|
let threshold = compaction_threshold(&self.config.app) as usize;
|
|
|
|
|
ag.publish_context_state();
|
|
|
|
|
let used = {
|
|
|
|
|
let sections = ag.shared_context.read().map(|s| s.clone()).unwrap_or_default();
|
|
|
|
|
crate::agent::context::sections_used(§ions)
|
|
|
|
|
};
|
|
|
|
|
if used > threshold {
|
|
|
|
|
ag.compact();
|
|
|
|
|
ag.notify("compacted");
|
|
|
|
|
}
|
2026-04-06 20:34:51 -04:00
|
|
|
}
|
|
|
|
|
self.shared.lock().unwrap().turn_active = true;
|
|
|
|
|
let _ = self.turn_watch.send(true);
|
|
|
|
|
let agent = self.agent.clone();
|
|
|
|
|
let result_tx = self.turn_tx.clone();
|
|
|
|
|
self.shared.lock().unwrap().turn_handle = Some(tokio::spawn(async move {
|
|
|
|
|
let result = Agent::turn(agent).await;
|
|
|
|
|
let _ = result_tx.send((result, target)).await;
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 04:42:50 -04:00
|
|
|
pub async fn shutdown(&self) {
|
|
|
|
|
if let Some(handle) = self.shared.lock().unwrap().turn_handle.take() { handle.abort(); }
|
2026-04-04 02:46:32 -04:00
|
|
|
}
|
mind: split event loop — Mind and UI run independently
Mind::run() owns the cognitive event loop: user input, turn results,
DMN ticks, hotkey actions. The UI event loop (user/event_loop.rs) owns
the terminal: key events, render ticks, channel status display.
They communicate through channels: UI sends MindMessage (user input,
hotkey actions) to Mind. Mind sends UiMessage (status, info) to UI.
UI reads shared state (active tools, context) directly for rendering.
Removes direct coupling between Mind and App:
- cycle_reasoning no longer takes &mut App
- AdjustSampling updates agent only, UI reads from shared state
- /quit handled by UI directly, not routed through Mind
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 02:11:32 -04:00
|
|
|
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
/// Mind event loop — locks MindState, calls state methods, executes actions.
|
mind: split event loop — Mind and UI run independently
Mind::run() owns the cognitive event loop: user input, turn results,
DMN ticks, hotkey actions. The UI event loop (user/event_loop.rs) owns
the terminal: key events, render ticks, channel status display.
They communicate through channels: UI sends MindMessage (user input,
hotkey actions) to Mind. Mind sends UiMessage (status, info) to UI.
UI reads shared state (active tools, context) directly for rendering.
Removes direct coupling between Mind and App:
- cycle_reasoning no longer takes &mut App
- AdjustSampling updates agent only, UI reads from shared state
- /quit handled by UI directly, not routed through Mind
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 02:11:32 -04:00
|
|
|
pub async fn run(
|
2026-04-05 04:42:50 -04:00
|
|
|
&self,
|
2026-04-05 03:34:43 -04:00
|
|
|
mut input_rx: tokio::sync::mpsc::UnboundedReceiver<MindCommand>,
|
mind: split event loop — Mind and UI run independently
Mind::run() owns the cognitive event loop: user input, turn results,
DMN ticks, hotkey actions. The UI event loop (user/event_loop.rs) owns
the terminal: key events, render ticks, channel status display.
They communicate through channels: UI sends MindMessage (user input,
hotkey actions) to Mind. Mind sends UiMessage (status, info) to UI.
UI reads shared state (active tools, context) directly for rendering.
Removes direct coupling between Mind and App:
- cycle_reasoning no longer takes &mut App
- AdjustSampling updates agent only, UI reads from shared state
- /quit handled by UI directly, not routed through Mind
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 02:11:32 -04:00
|
|
|
mut turn_rx: mpsc::Receiver<(Result<TurnResult>, StreamTarget)>,
|
|
|
|
|
) {
|
2026-04-05 05:01:45 -04:00
|
|
|
let mut bg_rx = self.bg_rx.lock().unwrap().take()
|
|
|
|
|
.expect("Mind::run() called twice");
|
mind: split event loop — Mind and UI run independently
Mind::run() owns the cognitive event loop: user input, turn results,
DMN ticks, hotkey actions. The UI event loop (user/event_loop.rs) owns
the terminal: key events, render ticks, channel status display.
They communicate through channels: UI sends MindMessage (user input,
hotkey actions) to Mind. Mind sends UiMessage (status, info) to UI.
UI reads shared state (active tools, context) directly for rendering.
Removes direct coupling between Mind and App:
- cycle_reasoning no longer takes &mut App
- AdjustSampling updates agent only, UI reads from shared state
- /quit handled by UI directly, not routed through Mind
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 02:11:32 -04:00
|
|
|
loop {
|
2026-04-05 03:41:47 -04:00
|
|
|
let timeout = self.shared.lock().unwrap().dmn.interval();
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
let turn_active = self.shared.lock().unwrap().turn_active;
|
mind: split event loop — Mind and UI run independently
Mind::run() owns the cognitive event loop: user input, turn results,
DMN ticks, hotkey actions. The UI event loop (user/event_loop.rs) owns
the terminal: key events, render ticks, channel status display.
They communicate through channels: UI sends MindMessage (user input,
hotkey actions) to Mind. Mind sends UiMessage (status, info) to UI.
UI reads shared state (active tools, context) directly for rendering.
Removes direct coupling between Mind and App:
- cycle_reasoning no longer takes &mut App
- AdjustSampling updates agent only, UI reads from shared state
- /quit handled by UI directly, not routed through Mind
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 02:11:32 -04:00
|
|
|
|
2026-04-05 03:34:43 -04:00
|
|
|
let mut cmds = Vec::new();
|
|
|
|
|
|
mind: split event loop — Mind and UI run independently
Mind::run() owns the cognitive event loop: user input, turn results,
DMN ticks, hotkey actions. The UI event loop (user/event_loop.rs) owns
the terminal: key events, render ticks, channel status display.
They communicate through channels: UI sends MindMessage (user input,
hotkey actions) to Mind. Mind sends UiMessage (status, info) to UI.
UI reads shared state (active tools, context) directly for rendering.
Removes direct coupling between Mind and App:
- cycle_reasoning no longer takes &mut App
- AdjustSampling updates agent only, UI reads from shared state
- /quit handled by UI directly, not routed through Mind
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 02:11:32 -04:00
|
|
|
tokio::select! {
|
|
|
|
|
biased;
|
|
|
|
|
|
2026-04-05 16:18:10 -04:00
|
|
|
cmd = input_rx.recv() => {
|
|
|
|
|
match cmd {
|
|
|
|
|
Some(cmd) => cmds.push(cmd),
|
|
|
|
|
None => break, // UI shut down
|
|
|
|
|
}
|
mind: split event loop — Mind and UI run independently
Mind::run() owns the cognitive event loop: user input, turn results,
DMN ticks, hotkey actions. The UI event loop (user/event_loop.rs) owns
the terminal: key events, render ticks, channel status display.
They communicate through channels: UI sends MindMessage (user input,
hotkey actions) to Mind. Mind sends UiMessage (status, info) to UI.
UI reads shared state (active tools, context) directly for rendering.
Removes direct coupling between Mind and App:
- cycle_reasoning no longer takes &mut App
- AdjustSampling updates agent only, UI reads from shared state
- /quit handled by UI directly, not routed through Mind
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 02:11:32 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 05:01:45 -04:00
|
|
|
Some(bg) = bg_rx.recv() => {
|
|
|
|
|
match bg {
|
|
|
|
|
BgEvent::ScoringDone => {
|
|
|
|
|
self.shared.lock().unwrap().scoring_in_flight = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
mind: split event loop — Mind and UI run independently
Mind::run() owns the cognitive event loop: user input, turn results,
DMN ticks, hotkey actions. The UI event loop (user/event_loop.rs) owns
the terminal: key events, render ticks, channel status display.
They communicate through channels: UI sends MindMessage (user input,
hotkey actions) to Mind. Mind sends UiMessage (status, info) to UI.
UI reads shared state (active tools, context) directly for rendering.
Removes direct coupling between Mind and App:
- cycle_reasoning no longer takes &mut App
- AdjustSampling updates agent only, UI reads from shared state
- /quit handled by UI directly, not routed through Mind
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 02:11:32 -04:00
|
|
|
Some((result, target)) = turn_rx.recv() => {
|
2026-04-05 04:42:50 -04:00
|
|
|
self.shared.lock().unwrap().turn_handle = None;
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
let model_switch = self.shared.lock().unwrap().complete_turn(&result, target);
|
|
|
|
|
let _ = self.turn_watch.send(false);
|
|
|
|
|
|
|
|
|
|
if let Some(name) = model_switch {
|
2026-04-05 22:18:07 -04:00
|
|
|
crate::user::chat::cmd_switch_model(&self.agent, &name).await;
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 03:38:42 -04:00
|
|
|
// Post-turn maintenance
|
|
|
|
|
{
|
2026-04-05 21:13:48 -04:00
|
|
|
let mut ag = self.agent.lock().await;
|
2026-04-05 03:38:42 -04:00
|
|
|
ag.age_out_images();
|
|
|
|
|
ag.publish_context_state();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 03:34:43 -04:00
|
|
|
cmds.push(MindCommand::Compact);
|
|
|
|
|
if !self.config.no_agents {
|
|
|
|
|
cmds.push(MindCommand::Score);
|
|
|
|
|
}
|
2026-04-07 01:33:07 -04:00
|
|
|
|
|
|
|
|
// Trigger subconscious agents after conscious turn completes
|
|
|
|
|
self.collect_subconscious_results().await;
|
|
|
|
|
self.trigger_subconscious().await;
|
mind: split event loop — Mind and UI run independently
Mind::run() owns the cognitive event loop: user input, turn results,
DMN ticks, hotkey actions. The UI event loop (user/event_loop.rs) owns
the terminal: key events, render ticks, channel status display.
They communicate through channels: UI sends MindMessage (user input,
hotkey actions) to Mind. Mind sends UiMessage (status, info) to UI.
UI reads shared state (active tools, context) directly for rendering.
Removes direct coupling between Mind and App:
- cycle_reasoning no longer takes &mut App
- AdjustSampling updates agent only, UI reads from shared state
- /quit handled by UI directly, not routed through Mind
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 02:11:32 -04:00
|
|
|
}
|
|
|
|
|
|
mind: move state to MindState, Mind becomes thin event loop
MindState (behind Arc<Mutex<>>) holds all cognitive state: DMN,
turn tracking, pending input, scoring, error counters. Pure state
transition methods (take_pending_input, complete_turn, dmn_tick)
return Action values instead of directly spawning turns.
Mind is now just the event loop: lock MindState, call state methods,
execute returned actions (spawn turns, send UiMessages). No state
of its own except agent handle, turn handle, and watch channel.
mind/mod.rs: 957 → 586 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 03:05:28 -04:00
|
|
|
_ = tokio::time::sleep(timeout), if !turn_active => {
|
2026-04-05 03:34:43 -04:00
|
|
|
let tick = self.shared.lock().unwrap().dmn_tick();
|
2026-04-06 20:34:51 -04:00
|
|
|
if let Some((prompt, target)) = tick {
|
|
|
|
|
self.start_turn(&prompt, target).await;
|
|
|
|
|
}
|
mind: split event loop — Mind and UI run independently
Mind::run() owns the cognitive event loop: user input, turn results,
DMN ticks, hotkey actions. The UI event loop (user/event_loop.rs) owns
the terminal: key events, render ticks, channel status display.
They communicate through channels: UI sends MindMessage (user input,
hotkey actions) to Mind. Mind sends UiMessage (status, info) to UI.
UI reads shared state (active tools, context) directly for rendering.
Removes direct coupling between Mind and App:
- cycle_reasoning no longer takes &mut App
- AdjustSampling updates agent only, UI reads from shared state
- /quit handled by UI directly, not routed through Mind
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 02:11:32 -04:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-05 03:34:43 -04:00
|
|
|
|
2026-04-06 20:34:51 -04:00
|
|
|
// Check for pending user input → push to agent context and start turn
|
|
|
|
|
let pending = self.shared.lock().unwrap().take_pending_input();
|
|
|
|
|
if let Some(text) = pending {
|
|
|
|
|
self.start_turn(&text, StreamTarget::Conversation).await;
|
|
|
|
|
}
|
2026-04-05 03:34:43 -04:00
|
|
|
|
|
|
|
|
self.run_commands(cmds).await;
|
mind: split event loop — Mind and UI run independently
Mind::run() owns the cognitive event loop: user input, turn results,
DMN ticks, hotkey actions. The UI event loop (user/event_loop.rs) owns
the terminal: key events, render ticks, channel status display.
They communicate through channels: UI sends MindMessage (user input,
hotkey actions) to Mind. Mind sends UiMessage (status, info) to UI.
UI reads shared state (active tools, context) directly for rendering.
Removes direct coupling between Mind and App:
- cycle_reasoning no longer takes &mut App
- AdjustSampling updates agent only, UI reads from shared state
- /quit handled by UI directly, not routed through Mind
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 02:11:32 -04:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-04 02:46:32 -04:00
|
|
|
}
|