use crate::memory::config::MemoryConfig;
use crate::memory::error::MemoryEngineResult;
use super::store;
use super::types::GoalsDoc;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GoalMutation {
Add { text: String },
Edit { id: String, text: String },
Delete { id: String },
}
pub trait GoalsGenerator {
fn propose(&self, doc: &GoalsDoc, context: &str, first_run: bool) -> Vec<GoalMutation>;
}
pub struct NoopGenerator;
impl GoalsGenerator for NoopGenerator {
fn propose(&self, _doc: &GoalsDoc, _context: &str, _first_run: bool) -> Vec<GoalMutation> {
Vec::new()
}
}
#[derive(Debug, Clone)]
pub struct ReflectOutcome {
pub first_run: bool,
pub applied: usize,
pub skipped: usize,
pub summary: String,
pub goals: GoalsDoc,
}
pub fn build_prompt(context_input: &str, first_run: bool) -> String {
let mode = if first_run {
"The goals list is currently EMPTY. This is the first run — populate \
an initial set of the user's durable long-term goals (max ~8) from \
the context below. Start by calling goals_list to confirm, then use \
goals_add for each goal."
} else {
"Maintain the existing goals list. Call goals_list first, then make \
the MINIMAL set of changes (goals_add / goals_edit / goals_delete) \
justified by the context below. Do not churn goals that are still \
valid."
};
format!(
"{mode}\n\n\
Keep goals concise (one sentence each), durable (long-term, not \
per-task), and free of secrets or PII.\n\n\
## Context\n\n{context_input}\n"
)
}
fn normalise(text: &str) -> String {
text.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.to_lowercase()
}
fn apply_mutations(doc: &mut GoalsDoc, mutations: &[GoalMutation]) -> (usize, usize) {
let mut applied = 0usize;
let mut skipped = 0usize;
for mutation in mutations {
match mutation {
GoalMutation::Add { text } => {
let norm = normalise(text);
let duplicate =
!norm.is_empty() && doc.items.iter().any(|i| normalise(&i.text) == norm);
if duplicate {
skipped += 1;
continue;
}
match doc.add(text) {
Ok(_) => applied += 1,
Err(_) => skipped += 1,
}
}
GoalMutation::Edit { id, text } => match doc.edit(id, text) {
Ok(()) => applied += 1,
Err(_) => skipped += 1,
},
GoalMutation::Delete { id } => match doc.delete(id) {
Ok(()) => applied += 1,
Err(_) => skipped += 1,
},
}
}
(applied, skipped)
}
pub fn reflect(
config: &MemoryConfig,
context: &str,
generator: &dyn GoalsGenerator,
) -> MemoryEngineResult<ReflectOutcome> {
let _guard = store::goals_mutation_lock().lock();
let mut doc = store::load(&config.workspace)?;
let first_run = doc.is_empty();
let mutations = generator.propose(&doc, context, first_run);
let (applied, skipped) = apply_mutations(&mut doc, &mutations);
if applied > 0 {
store::save(&config.workspace, &mut doc)?;
} else {
doc = store::load(&config.workspace)?;
}
let summary = if first_run {
format!("first run: populated {applied} goal(s) ({skipped} skipped)")
} else {
format!("maintenance: applied {applied} change(s) ({skipped} skipped)")
};
Ok(ReflectOutcome {
first_run,
applied,
skipped,
summary,
goals: doc,
})
}
#[cfg(test)]
#[path = "reflect_tests.rs"]
mod tests;