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-05 04:29:56 -04:00
|
|
|
use crate::config::{AppConfig, SessionConfig};
|
2026-04-07 02:31:52 -04:00
|
|
|
use crate::subconscious::learn;
|
2026-04-07 01:57:01 -04:00
|
|
|
|
2026-04-07 02:31:52 -04:00
|
|
|
pub use dmn::{SubconsciousSnapshot, Subconscious};
|
2026-04-07 01:59:09 -04:00
|
|
|
|
WIP: Fix mind/, dmn, UI layer — 35 errors remaining
mind/mod.rs and mind/dmn.rs fully migrated to AST types.
user/context.rs, user/widgets.rs, user/chat.rs partially migrated.
Killed working_stack tool, tokenize_conv_entry, context_old.rs.
Remaining: learn.rs (22), oneshot.rs (5), subconscious.rs (3),
chat.rs (3), widgets.rs (1), context.rs (1).
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 15:24:49 -04:00
|
|
|
use crate::agent::context::{AstNode, NodeBody, Section, Ast, ContextState};
|
2026-04-07 19:35:46 -04:00
|
|
|
|
WIP: Fix mind/, dmn, UI layer — 35 errors remaining
mind/mod.rs and mind/dmn.rs fully migrated to AST types.
user/context.rs, user/widgets.rs, user/chat.rs partially migrated.
Killed working_stack tool, tokenize_conv_entry, context_old.rs.
Remaining: learn.rs (22), oneshot.rs (5), subconscious.rs (3),
chat.rs (3), widgets.rs (1), context.rs (1).
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 15:24:49 -04:00
|
|
|
fn load_memory_scores(ctx: &mut ContextState, path: &std::path::Path) {
|
2026-04-07 19:35:46 -04:00
|
|
|
let data = match std::fs::read_to_string(path) {
|
|
|
|
|
Ok(d) => d,
|
|
|
|
|
Err(_) => return,
|
|
|
|
|
};
|
|
|
|
|
let scores: std::collections::BTreeMap<String, f64> = match serde_json::from_str(&data) {
|
|
|
|
|
Ok(s) => s,
|
|
|
|
|
Err(_) => return,
|
|
|
|
|
};
|
|
|
|
|
let mut applied = 0;
|
WIP: Fix mind/, dmn, UI layer — 35 errors remaining
mind/mod.rs and mind/dmn.rs fully migrated to AST types.
user/context.rs, user/widgets.rs, user/chat.rs partially migrated.
Killed working_stack tool, tokenize_conv_entry, context_old.rs.
Remaining: learn.rs (22), oneshot.rs (5), subconscious.rs (3),
chat.rs (3), widgets.rs (1), context.rs (1).
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 15:24:49 -04:00
|
|
|
for i in 0..ctx.conversation().len() {
|
|
|
|
|
if let AstNode::Leaf(leaf) = &ctx.conversation()[i] {
|
|
|
|
|
if let NodeBody::Memory { key, .. } = leaf.body() {
|
|
|
|
|
if let Some(&s) = scores.get(key.as_str()) {
|
|
|
|
|
ctx.set_score(Section::Conversation, i, Some(s));
|
|
|
|
|
applied += 1;
|
|
|
|
|
}
|
2026-04-07 19:35:46 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if applied > 0 {
|
|
|
|
|
dbglog!("[scoring] loaded {} scores from {}", applied, path.display());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 22:32:10 -04:00
|
|
|
/// Collect scored memory keys from conversation entries.
|
WIP: Fix mind/, dmn, UI layer — 35 errors remaining
mind/mod.rs and mind/dmn.rs fully migrated to AST types.
user/context.rs, user/widgets.rs, user/chat.rs partially migrated.
Killed working_stack tool, tokenize_conv_entry, context_old.rs.
Remaining: learn.rs (22), oneshot.rs (5), subconscious.rs (3),
chat.rs (3), widgets.rs (1), context.rs (1).
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 15:24:49 -04:00
|
|
|
fn collect_memory_scores(ctx: &ContextState) -> std::collections::BTreeMap<String, f64> {
|
|
|
|
|
ctx.conversation().iter()
|
|
|
|
|
.filter_map(|node| {
|
|
|
|
|
if let AstNode::Leaf(leaf) = node {
|
|
|
|
|
if let NodeBody::Memory { key, score: Some(s), .. } = leaf.body() {
|
|
|
|
|
return Some((key.clone(), *s));
|
|
|
|
|
}
|
2026-04-07 19:35:46 -04:00
|
|
|
}
|
WIP: Fix mind/, dmn, UI layer — 35 errors remaining
mind/mod.rs and mind/dmn.rs fully migrated to AST types.
user/context.rs, user/widgets.rs, user/chat.rs partially migrated.
Killed working_stack tool, tokenize_conv_entry, context_old.rs.
Remaining: learn.rs (22), oneshot.rs (5), subconscious.rs (3),
chat.rs (3), widgets.rs (1), context.rs (1).
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 15:24:49 -04:00
|
|
|
None
|
2026-04-07 19:35:46 -04:00
|
|
|
})
|
2026-04-07 22:32:10 -04:00
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Save memory scores to disk.
|
|
|
|
|
fn save_memory_scores(scores: &std::collections::BTreeMap<String, f64>, path: &std::path::Path) {
|
|
|
|
|
if let Ok(json) = serde_json::to_string_pretty(scores) {
|
2026-04-07 19:35:46 -04:00
|
|
|
let _ = std::fs::write(path, json);
|
|
|
|
|
dbglog!("[scoring] saved {} scores to {}", scores.len(), path.display());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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-08 15:40:36 -04:00
|
|
|
pub agent: Arc<Agent>,
|
2026-04-05 21:13:48 -04:00
|
|
|
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-08 20:37:19 -04:00
|
|
|
subconscious: Arc<tokio::sync::Mutex<Subconscious>>,
|
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 {
|
2026-04-08 15:47:21 -04:00
|
|
|
pub async fn new(
|
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
|
|
|
config: SessionConfig,
|
|
|
|
|
turn_tx: mpsc::Sender<(Result<TurnResult>, StreamTarget)>,
|
|
|
|
|
) -> Self {
|
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-08 15:47:21 -04:00
|
|
|
let agent = 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,
|
2026-04-08 16:45:56 -04:00
|
|
|
crate::agent::tools::ActiveTools::new(),
|
2026-04-08 15:47:21 -04:00
|
|
|
).await;
|
2026-04-05 04:20:49 -04:00
|
|
|
|
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-08 20:37:19 -04:00
|
|
|
let subconscious = Arc::new(tokio::sync::Mutex::new(Subconscious::new()));
|
|
|
|
|
subconscious.lock().await.init_output_tool(subconscious.clone());
|
|
|
|
|
|
2026-04-07 02:13:06 -04:00
|
|
|
Self { agent, shared, config,
|
2026-04-08 20:37:19 -04:00
|
|
|
subconscious, 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:31:52 -04:00
|
|
|
pub async fn subconscious_snapshots(&self) -> Vec<SubconsciousSnapshot> {
|
2026-04-07 19:27:36 -04:00
|
|
|
// Lock ordering: subconscious → store (store is bottom-most).
|
|
|
|
|
let sub = self.subconscious.lock().await;
|
2026-04-07 19:02:58 -04:00
|
|
|
let store = crate::store::Store::cached().await.ok();
|
|
|
|
|
let store_guard = match &store {
|
|
|
|
|
Some(s) => Some(s.lock().await),
|
|
|
|
|
None => None,
|
|
|
|
|
};
|
2026-04-07 19:27:36 -04:00
|
|
|
sub.snapshots(store_guard.as_deref())
|
2026-04-07 02:31:52 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn subconscious_walked(&self) -> Vec<String> {
|
2026-04-07 19:16:01 -04:00
|
|
|
self.subconscious.lock().await.walked()
|
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-08 15:47:21 -04:00
|
|
|
self.agent.restore_from_log().await;
|
2026-04-07 19:35:46 -04:00
|
|
|
|
|
|
|
|
// Restore persisted memory scores
|
|
|
|
|
let scores_path = self.config.session_dir.join("memory-scores.json");
|
2026-04-08 15:47:21 -04:00
|
|
|
load_memory_scores(&mut *self.agent.context.lock().await, &scores_path);
|
2026-04-07 19:35:46 -04:00
|
|
|
|
2026-04-08 15:47:21 -04:00
|
|
|
self.agent.state.lock().await.changed.notify_one();
|
2026-04-07 19:02:58 -04:00
|
|
|
|
|
|
|
|
// Load persistent subconscious state
|
|
|
|
|
let state_path = self.config.session_dir.join("subconscious-state.json");
|
|
|
|
|
self.subconscious.lock().await.set_state_path(state_path);
|
2026-04-05 04:02:16 -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
|
|
|
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-08 15:47:21 -04:00
|
|
|
if self.agent.context.lock().await.tokens() > threshold {
|
|
|
|
|
self.agent.compact().await;
|
|
|
|
|
self.agent.state.lock().await.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-08 16:45:56 -04:00
|
|
|
self.agent.state.lock().await.active_tools.abort_all();
|
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-08 15:47:21 -04:00
|
|
|
{
|
|
|
|
|
let mut ctx = self.agent.context.lock().await;
|
|
|
|
|
ctx.clear(Section::Conversation);
|
|
|
|
|
}
|
|
|
|
|
{
|
|
|
|
|
let mut st = self.agent.state.lock().await;
|
|
|
|
|
st.conversation_log = new_log;
|
|
|
|
|
st.generation += 1;
|
|
|
|
|
st.last_prompt_tokens = 0;
|
|
|
|
|
}
|
|
|
|
|
self.agent.compact().await;
|
2026-04-05 03:34:43 -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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
2026-04-07 19:35:46 -04:00
|
|
|
let scores_path = self.config.session_dir.join("memory-scores.json");
|
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-08 15:47:21 -04:00
|
|
|
let mut st = agent.state.lock().await;
|
|
|
|
|
if st.memory_scoring_in_flight { return; }
|
|
|
|
|
st.memory_scoring_in_flight = true;
|
|
|
|
|
drop(st);
|
|
|
|
|
let ctx = agent.context.lock().await.clone();
|
|
|
|
|
(ctx, agent.client.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-07 21:10:09 -04:00
|
|
|
let _result = learn::score_memories_incremental(
|
2026-04-05 22:34:48 -04:00
|
|
|
&context, max_age as i64, response_window, &client, &agent,
|
2026-04-07 21:10:09 -04:00
|
|
|
|key: String, score: f64| {
|
|
|
|
|
let agent = agent.clone();
|
|
|
|
|
let path = scores_path.clone();
|
|
|
|
|
async move {
|
2026-04-07 22:32:10 -04:00
|
|
|
let scores_snapshot = {
|
2026-04-08 15:47:21 -04:00
|
|
|
let mut ctx = agent.context.lock().await;
|
|
|
|
|
for i in 0..ctx.conversation().len() {
|
|
|
|
|
if let AstNode::Leaf(leaf) = &ctx.conversation()[i] {
|
WIP: Fix mind/, dmn, UI layer — 35 errors remaining
mind/mod.rs and mind/dmn.rs fully migrated to AST types.
user/context.rs, user/widgets.rs, user/chat.rs partially migrated.
Killed working_stack tool, tokenize_conv_entry, context_old.rs.
Remaining: learn.rs (22), oneshot.rs (5), subconscious.rs (3),
chat.rs (3), widgets.rs (1), context.rs (1).
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 15:24:49 -04:00
|
|
|
if let NodeBody::Memory { key: k, .. } = leaf.body() {
|
|
|
|
|
if *k == key {
|
2026-04-08 15:47:21 -04:00
|
|
|
ctx.set_score(Section::Conversation, i, Some(score));
|
WIP: Fix mind/, dmn, UI layer — 35 errors remaining
mind/mod.rs and mind/dmn.rs fully migrated to AST types.
user/context.rs, user/widgets.rs, user/chat.rs partially migrated.
Killed working_stack tool, tokenize_conv_entry, context_old.rs.
Remaining: learn.rs (22), oneshot.rs (5), subconscious.rs (3),
chat.rs (3), widgets.rs (1), context.rs (1).
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 15:24:49 -04:00
|
|
|
}
|
2026-04-07 22:32:10 -04:00
|
|
|
}
|
2026-04-07 20:15:31 -04:00
|
|
|
}
|
2026-04-07 03:14:24 -04:00
|
|
|
}
|
2026-04-08 15:47:21 -04:00
|
|
|
let snapshot = collect_memory_scores(&ctx);
|
|
|
|
|
drop(ctx);
|
|
|
|
|
agent.state.lock().await.changed.notify_one();
|
|
|
|
|
snapshot
|
2026-04-07 22:32:10 -04:00
|
|
|
};
|
|
|
|
|
save_memory_scores(&scores_snapshot, &path);
|
2026-04-07 03:14:24 -04:00
|
|
|
}
|
2026-04-07 21:10:09 -04:00
|
|
|
},
|
|
|
|
|
).await;
|
|
|
|
|
{
|
2026-04-08 15:47:21 -04:00
|
|
|
agent.state.lock().await.memory_scoring_in_flight = false;
|
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-07 01:33:07 -04:00
|
|
|
async fn start_turn(&self, text: &str, target: StreamTarget) {
|
|
|
|
|
{
|
2026-04-06 21:48:12 -04:00
|
|
|
match target {
|
|
|
|
|
StreamTarget::Conversation => {
|
2026-04-08 15:47:21 -04:00
|
|
|
self.agent.push_node(AstNode::user_msg(text)).await;
|
2026-04-06 21:48:12 -04:00
|
|
|
}
|
|
|
|
|
StreamTarget::Autonomous => {
|
2026-04-08 15:47:21 -04:00
|
|
|
self.agent.push_node(AstNode::dmn(text)).await;
|
2026-04-06 21:48:12 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compact if over budget before sending
|
|
|
|
|
let threshold = compaction_threshold(&self.config.app) as usize;
|
2026-04-08 15:47:21 -04:00
|
|
|
if self.agent.context.lock().await.tokens() > threshold {
|
|
|
|
|
self.agent.compact().await;
|
|
|
|
|
self.agent.state.lock().await.notify("compacted");
|
2026-04-06 21:48:12 -04:00
|
|
|
}
|
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:34:43 -04:00
|
|
|
cmds.push(MindCommand::Compact);
|
|
|
|
|
if !self.config.no_agents {
|
|
|
|
|
cmds.push(MindCommand::Score);
|
|
|
|
|
}
|
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-07 02:37:11 -04:00
|
|
|
// Subconscious: collect finished results, trigger due agents
|
|
|
|
|
if !self.config.no_agents {
|
|
|
|
|
let mut sub = self.subconscious.lock().await;
|
|
|
|
|
sub.collect_results(&self.agent).await;
|
|
|
|
|
sub.trigger(&self.agent).await;
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
}
|