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
|
|
@ -14,11 +14,14 @@
|
|||
// with high divergence depend on memories the model
|
||||
// hasn't internalized. 2 API calls.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::agent::api::ApiClient;
|
||||
use crate::agent::context::{
|
||||
Ast, AstNode, ContextState, Role, WireImage,
|
||||
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;
|
||||
|
||||
const SCORE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
|
||||
|
|
@ -376,6 +379,108 @@ where
|
|||
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 ─────────────────────────────────────────
|
||||
|
||||
/// Score which recent responses are candidates for fine-tuning.
|
||||
|
|
@ -520,6 +625,100 @@ pub async fn score_finetune_candidates(
|
|||
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 ─────────────────────────────
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue