mind: MindTriggered trait for background scoring flows
Mind's impl had accumulated ~50 lines of setup glue per scoring flow
(memory, memory-full, finetune): snapshot config, clone handles,
resolve context, spawn task, route results back through BgEvent,
write stats. The shape was identical; only the middle changed.
Introduce the MindTriggered trait:
pub trait MindTriggered {
fn trigger(&self);
}
Each flow becomes a struct next to its scoring code that owns its
dependencies and a JoinHandle (behind a sync Mutex for interior
mutability):
subconscious::learn::MemoryScoring (Score, ScoreFull)
subconscious::learn::FinetuneScoring (ScoreFinetune)
Mind holds one of each and dispatches in one line:
MindCommand::Score => self.memory_scoring.trigger(),
MindCommand::ScoreFull => self.memory_scoring.trigger_full(),
MindCommand::ScoreFinetune => self.finetune_scoring.trigger(),
Each struct picks its own trigger semantics — memory scoring is
no-op-if-running (!handle.is_finished()); finetune is abort-restart.
Falls out:
- BgEvent / bg_tx / bg_rx disappear entirely. Tasks write directly
to their slice of MindState and call agent.state.changed.notify_one()
to wake the UI. The bg_rx arm in Mind's select loop is gone.
- agent.state.memory_scoring_in_flight was duplicating
shared.scoring_in_flight via BgEvent routing; now the JoinHandle
alone tells us, and shared.scoring_in_flight is written directly
by the task for the UI.
- start_memory_scoring / start_full_scoring / start_finetune_scoring
methods on Mind are deleted; Mind no longer knows the setup shape
of any scoring flow.
- FinetuneScoringStats moves from mind/ to subconscious/learn.rs
next to the function that produces it.
No behavior change — same flows, same trigger points, same semantics.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
c5745e38e2
commit
575325e855
4 changed files with 258 additions and 232 deletions
|
|
@ -172,7 +172,6 @@ pub struct AgentState {
|
||||||
pub pending_dmn_pause: bool,
|
pub pending_dmn_pause: bool,
|
||||||
pub provenance: String,
|
pub provenance: String,
|
||||||
pub generation: u64,
|
pub generation: u64,
|
||||||
pub memory_scoring_in_flight: bool,
|
|
||||||
pub active_tools: tools::ActiveTools,
|
pub active_tools: tools::ActiveTools,
|
||||||
/// vLLM scheduling priority (lower = higher priority).
|
/// vLLM scheduling priority (lower = higher priority).
|
||||||
/// 0 = interactive, 1 = surface agent, 2 = other subconscious, 10 = unconscious.
|
/// 0 = interactive, 1 = surface agent, 2 = other subconscious, 10 = unconscious.
|
||||||
|
|
@ -237,7 +236,6 @@ impl Agent {
|
||||||
pending_dmn_pause: false,
|
pending_dmn_pause: false,
|
||||||
provenance: "manual".to_string(),
|
provenance: "manual".to_string(),
|
||||||
generation: 0,
|
generation: 0,
|
||||||
memory_scoring_in_flight: false,
|
|
||||||
active_tools,
|
active_tools,
|
||||||
priority: Some(0),
|
priority: Some(0),
|
||||||
no_compact: false,
|
no_compact: false,
|
||||||
|
|
@ -275,7 +273,6 @@ impl Agent {
|
||||||
pending_dmn_pause: false,
|
pending_dmn_pause: false,
|
||||||
provenance: st.provenance.clone(),
|
provenance: st.provenance.clone(),
|
||||||
generation: 0,
|
generation: 0,
|
||||||
memory_scoring_in_flight: false,
|
|
||||||
active_tools: tools::ActiveTools::new(),
|
active_tools: tools::ActiveTools::new(),
|
||||||
priority: None,
|
priority: None,
|
||||||
no_compact: true,
|
no_compact: true,
|
||||||
|
|
|
||||||
287
src/mind/mod.rs
287
src/mind/mod.rs
|
|
@ -9,6 +9,44 @@ pub mod unconscious;
|
||||||
pub mod identity;
|
pub mod identity;
|
||||||
pub mod log;
|
pub mod log;
|
||||||
|
|
||||||
|
/// A background operation wired off Mind. Each flow (memory scoring,
|
||||||
|
/// finetune scoring, compare) is a struct holding its dependencies and
|
||||||
|
/// a TaskHandle; `trigger()` picks the flow's own "start a fresh run"
|
||||||
|
/// semantics (abort-restart vs no-op-if-running).
|
||||||
|
pub trait MindTriggered {
|
||||||
|
fn trigger(&self);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Owns a JoinHandle for a background task with two trigger semantics.
|
||||||
|
/// Uses a sync Mutex for interior mutability so callers can `trigger()`
|
||||||
|
/// off `&self` (Mind is shared via Arc).
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct TaskHandle(std::sync::Mutex<Option<tokio::task::JoinHandle<()>>>);
|
||||||
|
|
||||||
|
impl TaskHandle {
|
||||||
|
pub fn new() -> Self { Self::default() }
|
||||||
|
|
||||||
|
/// Abort any running task and start a fresh one.
|
||||||
|
pub fn trigger<F>(&self, fut: F)
|
||||||
|
where F: std::future::Future<Output = ()> + Send + 'static
|
||||||
|
{
|
||||||
|
let mut h = self.0.lock().unwrap();
|
||||||
|
if let Some(old) = h.take() { old.abort(); }
|
||||||
|
*h = Some(tokio::spawn(fut));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// No-op if a task is still running; otherwise start a fresh one.
|
||||||
|
pub fn trigger_if_idle<F>(&self, fut: F)
|
||||||
|
where F: std::future::Future<Output = ()> + Send + 'static
|
||||||
|
{
|
||||||
|
let mut h = self.0.lock().unwrap();
|
||||||
|
if let Some(old) = &*h {
|
||||||
|
if !old.is_finished() { return; }
|
||||||
|
}
|
||||||
|
*h = Some(tokio::spawn(fut));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// consciousness.rs — Mind state machine and event loop
|
// consciousness.rs — Mind state machine and event loop
|
||||||
//
|
//
|
||||||
// The core runtime for the consciousness binary. Mind manages turns,
|
// The core runtime for the consciousness binary. Mind manages turns,
|
||||||
|
|
@ -48,7 +86,7 @@ fn match_scores(
|
||||||
}).collect()
|
}).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_memory_by_key(ctx: &ContextState, key: &str) -> Option<(Section, usize)> {
|
pub(crate) fn find_memory_by_key(ctx: &ContextState, key: &str) -> Option<(Section, usize)> {
|
||||||
[(Section::Identity, ctx.identity()), (Section::Conversation, ctx.conversation())]
|
[(Section::Identity, ctx.identity()), (Section::Conversation, ctx.conversation())]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find_map(|(section, nodes)| {
|
.find_map(|(section, nodes)| {
|
||||||
|
|
@ -87,7 +125,7 @@ fn load_memory_scores(ctx: &mut ContextState, path: &std::path::Path) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Collect scored memory keys from identity and conversation entries.
|
/// Collect scored memory keys from identity and conversation entries.
|
||||||
fn collect_memory_scores(ctx: &ContextState) -> std::collections::BTreeMap<String, f64> {
|
pub(crate) fn collect_memory_scores(ctx: &ContextState) -> std::collections::BTreeMap<String, f64> {
|
||||||
ctx.identity().iter()
|
ctx.identity().iter()
|
||||||
.chain(ctx.conversation().iter())
|
.chain(ctx.conversation().iter())
|
||||||
.filter_map(|node| {
|
.filter_map(|node| {
|
||||||
|
|
@ -102,7 +140,7 @@ fn collect_memory_scores(ctx: &ContextState) -> std::collections::BTreeMap<Strin
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save memory scores to disk.
|
/// Save memory scores to disk.
|
||||||
fn save_memory_scores(scores: &std::collections::BTreeMap<String, f64>, path: &std::path::Path) {
|
pub(crate) fn save_memory_scores(scores: &std::collections::BTreeMap<String, f64>, path: &std::path::Path) {
|
||||||
match serde_json::to_string_pretty(scores) {
|
match serde_json::to_string_pretty(scores) {
|
||||||
Ok(json) => match std::fs::write(path, &json) {
|
Ok(json) => match std::fs::write(path, &json) {
|
||||||
Ok(()) => dbglog!("[scoring] saved {} scores to {} ({} bytes)",
|
Ok(()) => dbglog!("[scoring] saved {} scores to {} ({} bytes)",
|
||||||
|
|
@ -154,22 +192,7 @@ pub struct MindState {
|
||||||
/// Fine-tuning candidates identified by scoring.
|
/// Fine-tuning candidates identified by scoring.
|
||||||
pub finetune_candidates: Vec<learn::FinetuneCandidate>,
|
pub finetune_candidates: Vec<learn::FinetuneCandidate>,
|
||||||
/// Last scoring run stats for UI display.
|
/// Last scoring run stats for UI display.
|
||||||
pub finetune_last_run: Option<FinetuneScoringStats>,
|
pub finetune_last_run: Option<learn::FinetuneScoringStats>,
|
||||||
}
|
|
||||||
|
|
||||||
/// Stats from the last finetune scoring run.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct FinetuneScoringStats {
|
|
||||||
/// Count of assistant responses we considered (recent half of context).
|
|
||||||
pub responses_considered: usize,
|
|
||||||
/// How many exceeded the divergence threshold.
|
|
||||||
pub above_threshold: usize,
|
|
||||||
/// Threshold used for this run.
|
|
||||||
pub threshold: f64,
|
|
||||||
/// Highest divergence observed.
|
|
||||||
pub max_divergence: f64,
|
|
||||||
/// Error message if the run failed.
|
|
||||||
pub error: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for MindState {
|
impl Clone for MindState {
|
||||||
|
|
@ -318,11 +341,6 @@ impl MindState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Background task completion events.
|
|
||||||
enum BgEvent {
|
|
||||||
ScoringDone,
|
|
||||||
FinetuneCandidate(learn::FinetuneCandidate),
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Mind: cognitive state machine ---
|
// --- Mind: cognitive state machine ---
|
||||||
|
|
||||||
|
|
@ -339,8 +357,8 @@ pub struct Mind {
|
||||||
/// Signals conscious activity to the unconscious loop.
|
/// Signals conscious activity to the unconscious loop.
|
||||||
/// true = active, false = idle opportunity.
|
/// true = active, false = idle opportunity.
|
||||||
conscious_active: tokio::sync::watch::Sender<bool>,
|
conscious_active: tokio::sync::watch::Sender<bool>,
|
||||||
bg_tx: mpsc::UnboundedSender<BgEvent>,
|
memory_scoring: learn::MemoryScoring,
|
||||||
bg_rx: std::sync::Mutex<Option<mpsc::UnboundedReceiver<BgEvent>>>,
|
finetune_scoring: learn::FinetuneScoring,
|
||||||
_supervisor: crate::thalamus::supervisor::Supervisor,
|
_supervisor: crate::thalamus::supervisor::Supervisor,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -380,7 +398,6 @@ impl Mind {
|
||||||
)));
|
)));
|
||||||
let (turn_watch, _) = tokio::sync::watch::channel(false);
|
let (turn_watch, _) = tokio::sync::watch::channel(false);
|
||||||
let (conscious_active, _) = tokio::sync::watch::channel(false);
|
let (conscious_active, _) = tokio::sync::watch::channel(false);
|
||||||
let (bg_tx, bg_rx) = mpsc::unbounded_channel();
|
|
||||||
|
|
||||||
let mut sup = crate::thalamus::supervisor::Supervisor::new();
|
let mut sup = crate::thalamus::supervisor::Supervisor::new();
|
||||||
sup.load_config();
|
sup.load_config();
|
||||||
|
|
@ -465,10 +482,17 @@ impl Mind {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let scores_path = config.session_dir.join("memory-scores.json");
|
||||||
|
let memory_scoring = learn::MemoryScoring::new(
|
||||||
|
agent.clone(), shared.clone(), scores_path);
|
||||||
|
let finetune_scoring = learn::FinetuneScoring::new(agent.clone(), shared.clone());
|
||||||
|
|
||||||
Self { agent, shared, config,
|
Self { agent, shared, config,
|
||||||
subconscious, unconscious,
|
subconscious, unconscious,
|
||||||
turn_tx, turn_watch, conscious_active, bg_tx,
|
turn_tx, turn_watch, conscious_active,
|
||||||
bg_rx: std::sync::Mutex::new(Some(bg_rx)), _supervisor: sup }
|
memory_scoring,
|
||||||
|
finetune_scoring,
|
||||||
|
_supervisor: sup }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize — restore log, start daemons and background agents.
|
/// Initialize — restore log, start daemons and background agents.
|
||||||
|
|
@ -513,14 +537,7 @@ impl Mind {
|
||||||
|
|
||||||
// Kick off an incremental scoring pass on startup so memories due
|
// Kick off an incremental scoring pass on startup so memories due
|
||||||
// for re-scoring get evaluated without requiring a user message.
|
// for re-scoring get evaluated without requiring a user message.
|
||||||
{
|
self.memory_scoring.trigger();
|
||||||
let mut s = self.shared.lock().unwrap();
|
|
||||||
if !s.scoring_in_flight {
|
|
||||||
s.scoring_in_flight = true;
|
|
||||||
drop(s);
|
|
||||||
self.start_memory_scoring();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn turn_watch(&self) -> tokio::sync::watch::Receiver<bool> {
|
pub fn turn_watch(&self) -> tokio::sync::watch::Receiver<bool> {
|
||||||
|
|
@ -540,24 +557,10 @@ impl Mind {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MindCommand::Score => {
|
MindCommand::Score => {
|
||||||
let mut s = self.shared.lock().unwrap();
|
self.memory_scoring.trigger();
|
||||||
if !s.scoring_in_flight {
|
|
||||||
s.scoring_in_flight = true;
|
|
||||||
drop(s);
|
|
||||||
self.start_memory_scoring();
|
|
||||||
} else {
|
|
||||||
dbglog!("[scoring] skipped: scoring_in_flight=true");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
MindCommand::ScoreFull => {
|
MindCommand::ScoreFull => {
|
||||||
let mut s = self.shared.lock().unwrap();
|
self.memory_scoring.trigger_full();
|
||||||
if !s.scoring_in_flight {
|
|
||||||
s.scoring_in_flight = true;
|
|
||||||
drop(s);
|
|
||||||
self.start_full_scoring();
|
|
||||||
} else {
|
|
||||||
dbglog!("[scoring-full] skipped: scoring_in_flight=true");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
MindCommand::Interrupt => {
|
MindCommand::Interrupt => {
|
||||||
self.shared.lock().unwrap().interrupt();
|
self.shared.lock().unwrap().interrupt();
|
||||||
|
|
@ -588,7 +591,7 @@ impl Mind {
|
||||||
self.agent.compact().await;
|
self.agent.compact().await;
|
||||||
}
|
}
|
||||||
MindCommand::ScoreFinetune => {
|
MindCommand::ScoreFinetune => {
|
||||||
self.start_finetune_scoring();
|
self.finetune_scoring.trigger();
|
||||||
}
|
}
|
||||||
MindCommand::SetLearnThreshold(value) => {
|
MindCommand::SetLearnThreshold(value) => {
|
||||||
if let Err(e) = crate::config_writer::set_learn_threshold(value) {
|
if let Err(e) = crate::config_writer::set_learn_threshold(value) {
|
||||||
|
|
@ -605,167 +608,6 @@ impl Mind {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_memory_scoring(&self) {
|
|
||||||
let agent = self.agent.clone();
|
|
||||||
let bg_tx = self.bg_tx.clone();
|
|
||||||
let scores_path = self.config.session_dir.join("memory-scores.json");
|
|
||||||
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) = {
|
|
||||||
let mut st = agent.state.lock().await;
|
|
||||||
if st.memory_scoring_in_flight {
|
|
||||||
dbglog!("[scoring] skipped: memory_scoring_in_flight=true");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
st.memory_scoring_in_flight = true;
|
|
||||||
drop(st);
|
|
||||||
let ctx = agent.context.lock().await.clone();
|
|
||||||
(ctx, agent.client.clone())
|
|
||||||
};
|
|
||||||
let _result = learn::score_memories_incremental(
|
|
||||||
&context, max_age as i64, response_window, &client, &agent,
|
|
||||||
|key: String, score: f64| {
|
|
||||||
let agent = agent.clone();
|
|
||||||
let path = scores_path.clone();
|
|
||||||
async move {
|
|
||||||
let scores_snapshot = {
|
|
||||||
let mut ctx = agent.context.lock().await;
|
|
||||||
// Find memory by key in identity or conversation
|
|
||||||
let found = find_memory_by_key(&ctx, &key);
|
|
||||||
match found {
|
|
||||||
Some((section, i)) => {
|
|
||||||
ctx.set_score(section, i, Some(score));
|
|
||||||
let nodes: &[crate::agent::context::AstNode] = match section {
|
|
||||||
Section::Identity => ctx.identity(),
|
|
||||||
Section::Conversation => ctx.conversation(),
|
|
||||||
_ => &[],
|
|
||||||
};
|
|
||||||
let read_back = match nodes.get(i) {
|
|
||||||
Some(crate::agent::context::AstNode::Leaf(l)) => match l.body() {
|
|
||||||
crate::agent::context::NodeBody::Memory { score, .. } => format!("{:?}", score),
|
|
||||||
_ => "not-memory".to_string(),
|
|
||||||
},
|
|
||||||
_ => "out-of-bounds".to_string(),
|
|
||||||
};
|
|
||||||
dbglog!("[scoring] persisted {} → {:.3} ({:?}[{}]) read_back={}",
|
|
||||||
key, score, section, i, read_back);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
dbglog!(
|
|
||||||
"[scoring] DROP {}: find_memory_by_key None (id={}, cv={})",
|
|
||||||
key, ctx.identity().len(), ctx.conversation().len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let snapshot = collect_memory_scores(&ctx);
|
|
||||||
let in_snapshot = snapshot.contains_key(&key);
|
|
||||||
dbglog!("[scoring] snapshot size={} contains({})={}",
|
|
||||||
snapshot.len(), key, in_snapshot);
|
|
||||||
drop(ctx);
|
|
||||||
agent.state.lock().await.changed.notify_one();
|
|
||||||
snapshot
|
|
||||||
};
|
|
||||||
dbglog!("[scoring] about to save {} entries", scores_snapshot.len());
|
|
||||||
save_memory_scores(&scores_snapshot, &path);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
).await;
|
|
||||||
{
|
|
||||||
agent.state.lock().await.memory_scoring_in_flight = false;
|
|
||||||
}
|
|
||||||
let _ = bg_tx.send(BgEvent::ScoringDone);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run full N×M scoring matrix — scores every memory against every response.
|
|
||||||
pub fn start_full_scoring(&self) {
|
|
||||||
let agent = self.agent.clone();
|
|
||||||
let bg_tx = self.bg_tx.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
{
|
|
||||||
let mut st = agent.state.lock().await;
|
|
||||||
if st.memory_scoring_in_flight {
|
|
||||||
dbglog!("[scoring-full] skipped: memory_scoring_in_flight=true");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
st.memory_scoring_in_flight = true;
|
|
||||||
}
|
|
||||||
let client = agent.client.clone();
|
|
||||||
match learn::score_memories(&client, &agent).await {
|
|
||||||
Ok(()) => { let _ = bg_tx.send(BgEvent::ScoringDone); }
|
|
||||||
Err(e) => { dbglog!("[scoring-full] FAILED: {:#}", e); }
|
|
||||||
}
|
|
||||||
agent.state.lock().await.memory_scoring_in_flight = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Score responses for fine-tuning candidates.
|
|
||||||
///
|
|
||||||
/// Scores the most recent half of the context — responses near the end
|
|
||||||
/// of the context window were generated with the most context available,
|
|
||||||
/// which is what we want to train on. The threshold is a temporary knob;
|
|
||||||
/// once this runs continuously, we'll just train whatever lands at full
|
|
||||||
/// context without filtering.
|
|
||||||
pub fn start_finetune_scoring(&self) {
|
|
||||||
// Snapshot the config values we need before spawning — the scoring
|
|
||||||
// task shouldn't hold the config read lock across async work.
|
|
||||||
let (threshold, gen_alternates) = {
|
|
||||||
let app = crate::config::app();
|
|
||||||
(app.learn.threshold, app.learn.generate_alternates)
|
|
||||||
};
|
|
||||||
// Clear the previous run's candidates so this run's stream is fresh.
|
|
||||||
self.shared.lock().unwrap().finetune_candidates.clear();
|
|
||||||
|
|
||||||
let agent = self.agent.clone();
|
|
||||||
let bg_tx = self.bg_tx.clone();
|
|
||||||
let shared = self.shared.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let activity = crate::agent::start_activity(&agent, "finetune: scoring...").await;
|
|
||||||
|
|
||||||
let (context, client) = {
|
|
||||||
let ctx = agent.context.lock().await;
|
|
||||||
(ctx.clone(), agent.client.clone())
|
|
||||||
};
|
|
||||||
|
|
||||||
let entries = context.conversation();
|
|
||||||
let score_count = entries.len() / 2;
|
|
||||||
let range_start = entries.len() - score_count;
|
|
||||||
let responses_considered: usize = entries[range_start..].iter()
|
|
||||||
.filter(|n| matches!(n, crate::agent::context::AstNode::Branch { role: crate::agent::context::Role::Assistant, .. }))
|
|
||||||
.count();
|
|
||||||
|
|
||||||
activity.update(format!("finetune: scoring {} responses...", responses_considered)).await;
|
|
||||||
|
|
||||||
let bg_tx_cb = bg_tx.clone();
|
|
||||||
let stats = match learn::score_finetune_candidates(
|
|
||||||
&context, score_count, &client, threshold,
|
|
||||||
gen_alternates, &activity,
|
|
||||||
|c| { let _ = bg_tx_cb.send(BgEvent::FinetuneCandidate(c)); },
|
|
||||||
).await {
|
|
||||||
Ok((above_threshold, max_div)) => {
|
|
||||||
FinetuneScoringStats {
|
|
||||||
responses_considered,
|
|
||||||
above_threshold,
|
|
||||||
threshold,
|
|
||||||
max_divergence: max_div,
|
|
||||||
error: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => FinetuneScoringStats {
|
|
||||||
responses_considered,
|
|
||||||
above_threshold: 0,
|
|
||||||
threshold,
|
|
||||||
max_divergence: 0.0,
|
|
||||||
error: Some(format!("{}", e)),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
shared.lock().unwrap().finetune_last_run = Some(stats);
|
|
||||||
// activity drops here, marking completion and notifying observers
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn start_turn(&self, text: &str, target: StreamTarget) {
|
async fn start_turn(&self, text: &str, target: StreamTarget) {
|
||||||
{
|
{
|
||||||
|
|
@ -828,13 +670,11 @@ impl Mind {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut bg_rx = self.bg_rx.lock().unwrap().take()
|
|
||||||
.expect("Mind::run() called twice");
|
|
||||||
let mut sub_handle: Option<tokio::task::JoinHandle<()>> = None;
|
let mut sub_handle: Option<tokio::task::JoinHandle<()>> = None;
|
||||||
|
|
||||||
// Start finetune scoring at startup (scores existing conversation)
|
// Start finetune scoring at startup (scores existing conversation)
|
||||||
if !self.config.no_agents {
|
if !self.config.no_agents {
|
||||||
self.start_finetune_scoring();
|
self.finetune_scoring.trigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
|
@ -857,17 +697,6 @@ impl Mind {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(bg) = bg_rx.recv() => {
|
|
||||||
match bg {
|
|
||||||
BgEvent::ScoringDone => {
|
|
||||||
self.shared.lock().unwrap().scoring_in_flight = false;
|
|
||||||
}
|
|
||||||
BgEvent::FinetuneCandidate(c) => {
|
|
||||||
self.shared.lock().unwrap().finetune_candidates.push(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some((result, target)) = turn_rx.recv() => {
|
Some((result, target)) = turn_rx.recv() => {
|
||||||
let _ = self.conscious_active.send(false);
|
let _ = self.conscious_active.send(false);
|
||||||
let model_switch = {
|
let model_switch = {
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,14 @@
|
||||||
// with high divergence depend on memories the model
|
// with high divergence depend on memories the model
|
||||||
// hasn't internalized. 2 API calls.
|
// hasn't internalized. 2 API calls.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::agent::api::ApiClient;
|
use crate::agent::api::ApiClient;
|
||||||
use crate::agent::context::{
|
use crate::agent::context::{
|
||||||
Ast, AstNode, ContextState, Role, WireImage,
|
Ast, AstNode, ContextState, Role, WireImage,
|
||||||
is_assistant, is_memory_node, memory_key, render_branch_text, render_prior_context,
|
is_assistant, is_memory_node, memory_key, render_branch_text, render_prior_context,
|
||||||
};
|
};
|
||||||
|
use crate::mind::{MindState, MindTriggered, TaskHandle};
|
||||||
use crate::subconscious::generate::gen_continuation;
|
use crate::subconscious::generate::gen_continuation;
|
||||||
|
|
||||||
const SCORE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
|
const SCORE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
|
||||||
|
|
@ -376,6 +379,108 @@ where
|
||||||
Ok(scored)
|
Ok(scored)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Memory scoring — two modes sharing an in-flight handle (only one
|
||||||
|
/// runs at a time): `trigger()` for incremental, `trigger_full()` for
|
||||||
|
/// the N×M debug matrix.
|
||||||
|
pub struct MemoryScoring {
|
||||||
|
agent: Arc<crate::agent::Agent>,
|
||||||
|
shared: Arc<std::sync::Mutex<MindState>>,
|
||||||
|
scores_path: std::path::PathBuf,
|
||||||
|
task: TaskHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemoryScoring {
|
||||||
|
pub fn new(
|
||||||
|
agent: Arc<crate::agent::Agent>,
|
||||||
|
shared: Arc<std::sync::Mutex<MindState>>,
|
||||||
|
scores_path: std::path::PathBuf,
|
||||||
|
) -> Self {
|
||||||
|
Self { agent, shared, scores_path, task: TaskHandle::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn trigger_full(&self) {
|
||||||
|
self.task.trigger_if_idle(run_full(self.agent.clone(), self.shared.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MindTriggered for MemoryScoring {
|
||||||
|
fn trigger(&self) {
|
||||||
|
self.task.trigger_if_idle(run_incremental(
|
||||||
|
self.agent.clone(), self.shared.clone(), self.scores_path.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_incremental(
|
||||||
|
agent: Arc<crate::agent::Agent>,
|
||||||
|
shared: Arc<std::sync::Mutex<MindState>>,
|
||||||
|
scores_path: std::path::PathBuf,
|
||||||
|
) {
|
||||||
|
shared.lock().unwrap().scoring_in_flight = true;
|
||||||
|
agent.state.lock().await.changed.notify_one();
|
||||||
|
|
||||||
|
let cfg = crate::config::get();
|
||||||
|
let max_age = cfg.scoring_interval_secs;
|
||||||
|
let response_window = cfg.scoring_response_window;
|
||||||
|
|
||||||
|
let (context, client) = {
|
||||||
|
let ctx = agent.context.lock().await.clone();
|
||||||
|
(ctx, agent.client.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
let _result = score_memories_incremental(
|
||||||
|
&context, max_age as i64, response_window, &client, &agent,
|
||||||
|
|key: String, score: f64| {
|
||||||
|
let agent = agent.clone();
|
||||||
|
let path = scores_path.clone();
|
||||||
|
async move {
|
||||||
|
let scores_snapshot = {
|
||||||
|
let mut ctx = agent.context.lock().await;
|
||||||
|
let found = crate::mind::find_memory_by_key(&ctx, &key);
|
||||||
|
match found {
|
||||||
|
Some((section, i)) => {
|
||||||
|
ctx.set_score(section, i, Some(score));
|
||||||
|
dbglog!("[scoring] persisted {} → {:.3} ({:?}[{}])",
|
||||||
|
key, score, section, i);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
dbglog!(
|
||||||
|
"[scoring] DROP {}: find_memory_by_key None (id={}, cv={})",
|
||||||
|
key, ctx.identity().len(), ctx.conversation().len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let snapshot = crate::mind::collect_memory_scores(&ctx);
|
||||||
|
drop(ctx);
|
||||||
|
agent.state.lock().await.changed.notify_one();
|
||||||
|
snapshot
|
||||||
|
};
|
||||||
|
crate::mind::save_memory_scores(&scores_snapshot, &path);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
).await;
|
||||||
|
|
||||||
|
shared.lock().unwrap().scoring_in_flight = false;
|
||||||
|
agent.state.lock().await.changed.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_full(
|
||||||
|
agent: Arc<crate::agent::Agent>,
|
||||||
|
shared: Arc<std::sync::Mutex<MindState>>,
|
||||||
|
) {
|
||||||
|
shared.lock().unwrap().scoring_in_flight = true;
|
||||||
|
agent.state.lock().await.changed.notify_one();
|
||||||
|
|
||||||
|
let client = agent.client.clone();
|
||||||
|
match score_memories(&client, &agent).await {
|
||||||
|
Ok(()) => {},
|
||||||
|
Err(e) => { dbglog!("[scoring-full] FAILED: {:#}", e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
shared.lock().unwrap().scoring_in_flight = false;
|
||||||
|
agent.state.lock().await.changed.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Fine-tuning scoring ─────────────────────────────────────────
|
// ── Fine-tuning scoring ─────────────────────────────────────────
|
||||||
|
|
||||||
/// Score which recent responses are candidates for fine-tuning.
|
/// Score which recent responses are candidates for fine-tuning.
|
||||||
|
|
@ -520,6 +625,100 @@ pub async fn score_finetune_candidates(
|
||||||
Ok((total, max_divergence))
|
Ok((total, max_divergence))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stats from a finetune scoring run. Stored on MindState for UI display.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct FinetuneScoringStats {
|
||||||
|
pub responses_considered: usize,
|
||||||
|
pub above_threshold: usize,
|
||||||
|
pub threshold: f64,
|
||||||
|
pub max_divergence: f64,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finetune scoring — `trigger()` aborts any in-flight run and starts
|
||||||
|
/// a fresh one, clearing the previous candidates.
|
||||||
|
pub struct FinetuneScoring {
|
||||||
|
agent: Arc<crate::agent::Agent>,
|
||||||
|
shared: Arc<std::sync::Mutex<MindState>>,
|
||||||
|
task: TaskHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FinetuneScoring {
|
||||||
|
pub fn new(
|
||||||
|
agent: Arc<crate::agent::Agent>,
|
||||||
|
shared: Arc<std::sync::Mutex<MindState>>,
|
||||||
|
) -> Self {
|
||||||
|
Self { agent, shared, task: TaskHandle::new() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MindTriggered for FinetuneScoring {
|
||||||
|
fn trigger(&self) {
|
||||||
|
self.task.trigger(run_finetune(self.agent.clone(), self.shared.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_finetune(
|
||||||
|
agent: Arc<crate::agent::Agent>,
|
||||||
|
shared: Arc<std::sync::Mutex<MindState>>,
|
||||||
|
) {
|
||||||
|
let (threshold, gen_alternates) = {
|
||||||
|
let app = crate::config::app();
|
||||||
|
(app.learn.threshold, app.learn.generate_alternates)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fresh run — clear previous candidates.
|
||||||
|
shared.lock().unwrap().finetune_candidates.clear();
|
||||||
|
agent.state.lock().await.changed.notify_one();
|
||||||
|
|
||||||
|
let activity = crate::agent::start_activity(&agent, "finetune: scoring...").await;
|
||||||
|
|
||||||
|
let (context, client) = {
|
||||||
|
let ctx = agent.context.lock().await;
|
||||||
|
(ctx.clone(), agent.client.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
let entries = context.conversation();
|
||||||
|
let score_count = entries.len() / 2;
|
||||||
|
let range_start = entries.len() - score_count;
|
||||||
|
let responses_considered: usize = entries[range_start..].iter()
|
||||||
|
.filter(|n| matches!(n, AstNode::Branch { role: Role::Assistant, .. }))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
activity.update(format!("finetune: scoring {} responses...", responses_considered)).await;
|
||||||
|
|
||||||
|
let stats = {
|
||||||
|
let shared = shared.clone();
|
||||||
|
let agent = agent.clone();
|
||||||
|
match score_finetune_candidates(
|
||||||
|
&context, score_count, &client, threshold,
|
||||||
|
gen_alternates, &activity,
|
||||||
|
move |c| {
|
||||||
|
shared.lock().unwrap().finetune_candidates.push(c);
|
||||||
|
if let Ok(st) = agent.state.try_lock() { st.changed.notify_one(); }
|
||||||
|
},
|
||||||
|
).await {
|
||||||
|
Ok((above_threshold, max_div)) => FinetuneScoringStats {
|
||||||
|
responses_considered,
|
||||||
|
above_threshold,
|
||||||
|
threshold,
|
||||||
|
max_divergence: max_div,
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
Err(e) => FinetuneScoringStats {
|
||||||
|
responses_considered,
|
||||||
|
above_threshold: 0,
|
||||||
|
threshold,
|
||||||
|
max_divergence: 0.0,
|
||||||
|
error: Some(format!("{}", e)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
shared.lock().unwrap().finetune_last_run = Some(stats);
|
||||||
|
agent.state.lock().await.changed.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Finetune config and persistence ─────────────────────────────
|
// ── Finetune config and persistence ─────────────────────────────
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
|
||||||
|
|
@ -504,6 +504,7 @@ async fn run(
|
||||||
keep
|
keep
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.mind_state = Some(ms.clone());
|
app.mind_state = Some(ms.clone());
|
||||||
}
|
}
|
||||||
app.walked_count = mind.subconscious_walked().await.len();
|
app.walked_count = mind.subconscious_walked().await.len();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue