Skip to main content

kimetsu_brain/
user_profile.rs

1//! v2.6: the user's standing preferences, delivered unconditionally.
2//!
3//! Preference following is Kimetsu's second-weakest measured ability
4//! (LongMemEval 66.7%), and the benchmark page already diagnoses why: *"a
5//! preference is a small aside semantically far from the question."*
6//!
7//! That diagnosis rules out the obvious fix. If "I prefer `thiserror` for
8//! library errors" is semantically distant from "add error handling to the
9//! parser", then no amount of re-ranking surfaces it — the candidate never
10//! enters the pool. Boosting preference-kind memories, adding a profile term to
11//! the composite score, tuning the floors: all of them operate on candidates
12//! retrieval already found, and this one it did not.
13//!
14//! So the profile does not go through retrieval. It rides the warm start,
15//! which every host now receives, and is therefore in context before the first
16//! question is asked — which is what a standing preference *is*. PPRO
17//! (arXiv 2607.00017) reaches the same conclusion from the other direction: it
18//! derives a user profile from accumulated memories and uses it as an explicit
19//! prior rather than as one more retrieval signal.
20//!
21//! ## What counts as a preference
22//!
23//! `MemoryKind::Preference` memories, in either the project brain or the
24//! cross-project user brain. Ranked by proven usefulness, then recency, and
25//! hard-capped — a profile that grows without bound stops being a profile and
26//! becomes a second corpus in every prompt.
27//!
28//! Model-free: this is a `SELECT` and a budget.
29
30use kimetsu_core::KimetsuResult;
31use rusqlite::Connection;
32
33/// How many preferences the profile may carry.
34///
35/// Small on purpose. This is injected on every session, so its cost is paid
36/// unconditionally; the top handful of proven preferences is what makes the
37/// difference, and the tail is what makes users turn warm start off.
38pub const MAX_PREFERENCES: usize = 8;
39
40/// Character budget for the rendered block (~100 tokens), on the same footing
41/// as the digest's ~400.
42pub const PROFILE_CHAR_BUDGET: usize = 400;
43
44/// Longest single preference kept, before it is skipped as an essay rather
45/// than a preference.
46const MAX_PREFERENCE_CHARS: usize = 160;
47
48/// One standing preference.
49#[derive(Debug, Clone, PartialEq)]
50pub struct Preference {
51    pub memory_id: String,
52    pub text: String,
53    /// True when this came from the cross-project user brain rather than this
54    /// project — worth knowing, because a global preference should not be
55    /// silently overridden by a project convention without the reader noticing.
56    pub global: bool,
57}
58
59/// Read the user's standing preferences from one brain, strongest first.
60///
61/// Ordered by proven usefulness before recency: a preference the agent has
62/// actually been rewarded for following outranks one stated yesterday.
63pub fn preferences(
64    conn: &Connection,
65    global: bool,
66    limit: usize,
67) -> KimetsuResult<Vec<Preference>> {
68    let mut stmt = conn.prepare(
69        "SELECT memory_id, text
70         FROM memories
71         WHERE kind = 'preference'
72           AND invalidated_at IS NULL
73           AND superseded_by IS NULL
74           AND (valid_from IS NULL OR julianday(valid_from) <= julianday('now'))
75           AND (valid_to IS NULL OR julianday(valid_to) > julianday('now'))
76         ORDER BY usefulness_score DESC, created_at DESC
77         LIMIT ?1",
78    )?;
79    let rows = stmt
80        .query_map(rusqlite::params![limit as i64], |row| {
81            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
82        })?
83        .collect::<Result<Vec<_>, _>>()?;
84    Ok(rows
85        .into_iter()
86        .map(|(memory_id, text)| Preference {
87            memory_id,
88            text,
89            global,
90        })
91        .collect())
92}
93
94/// Merge project and global preferences into one profile.
95///
96/// Project preferences lead: a preference stated for *this* repo is more
97/// specific than one carried across every project, and specificity should win
98/// when the budget runs out.
99pub fn build_profile(
100    project_conn: &Connection,
101    user_conn: Option<&Connection>,
102) -> KimetsuResult<Vec<Preference>> {
103    let mut profile = preferences(project_conn, false, MAX_PREFERENCES)?;
104    if let Some(user_conn) = user_conn {
105        let remaining = MAX_PREFERENCES.saturating_sub(profile.len());
106        if remaining > 0 {
107            let global = preferences(user_conn, true, remaining)?;
108            // A global preference whose text duplicates a project one adds
109            // nothing but tokens.
110            for pref in global {
111                if !profile.iter().any(|p| p.text == pref.text) {
112                    profile.push(pref);
113                }
114            }
115        }
116    }
117    Ok(profile)
118}
119
120/// Render the profile as a warm-start section, or `None` when there is nothing
121/// to say.
122///
123/// Framed as instructions rather than as retrieved facts, because that is what
124/// they are: the reader should follow them without being asked, which is the
125/// whole difference between a preference and a memory.
126pub fn render_profile(preferences: &[Preference]) -> Option<String> {
127    if preferences.is_empty() {
128        return None;
129    }
130    let mut lines = Vec::new();
131    let mut used = 0usize;
132    for pref in preferences {
133        let text = pref.text.trim();
134        if text.is_empty() || text.chars().count() > MAX_PREFERENCE_CHARS {
135            continue;
136        }
137        let line = if pref.global {
138            format!("- {text} (across all your projects)")
139        } else {
140            format!("- {text}")
141        };
142        if used + line.len() > PROFILE_CHAR_BUDGET {
143            break;
144        }
145        used += line.len();
146        lines.push(line);
147    }
148    if lines.is_empty() {
149        return None;
150    }
151    Some(format!(
152        "Standing preferences — follow these without being asked:\n{}",
153        lines.join("\n")
154    ))
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    fn conn() -> Connection {
162        let conn = Connection::open_in_memory().expect("open");
163        crate::schema::initialize(&conn).expect("schema");
164        conn
165    }
166
167    fn insert(conn: &Connection, id: &str, kind: &str, text: &str, usefulness: f32) {
168        conn.execute(
169            "INSERT INTO memories
170             (memory_id, scope, kind, text, normalized_text, confidence,
171              provenance_snapshot_json, created_at, usefulness_score)
172             VALUES (?1, 'project', ?2, ?3, ?3, 0.9, '{}', '2026-01-01T00:00:00Z', ?4)",
173            rusqlite::params![id, kind, text, usefulness],
174        )
175        .expect("insert");
176    }
177
178    #[test]
179    fn only_preference_memories_make_the_profile() {
180        let c = conn();
181        insert(
182            &c,
183            "p",
184            "preference",
185            "prefer thiserror for library errors",
186            0.0,
187        );
188        insert(&c, "c", "convention", "always run cargo fmt", 0.0);
189        insert(&c, "f", "fact", "the schema is at v11", 0.0);
190
191        let profile = preferences(&c, false, 10).expect("preferences");
192        assert_eq!(profile.len(), 1, "got: {profile:?}");
193        assert!(profile[0].text.contains("thiserror"));
194    }
195
196    /// A preference the agent has been rewarded for following outranks one
197    /// merely stated.
198    #[test]
199    fn proven_preferences_come_first() {
200        let c = conn();
201        insert(&c, "unproven", "preference", "prefer tabs", 0.0);
202        insert(&c, "proven", "preference", "prefer thiserror", 5.0);
203        let profile = preferences(&c, false, 10).expect("preferences");
204        assert_eq!(profile[0].memory_id, "proven", "got: {profile:?}");
205    }
206
207    #[test]
208    fn retired_preferences_are_excluded() {
209        let c = conn();
210        insert(&c, "live", "preference", "prefer thiserror", 0.0);
211        insert(&c, "dead", "preference", "prefer failure crate", 0.0);
212        c.execute(
213            "UPDATE memories SET invalidated_at = '2026-02-01T00:00:00Z' WHERE memory_id = 'dead'",
214            [],
215        )
216        .unwrap();
217        let profile = preferences(&c, false, 10).expect("preferences");
218        assert_eq!(profile.len(), 1);
219        assert_eq!(profile[0].memory_id, "live");
220    }
221
222    #[test]
223    fn hardening_profile_checks_numeric_start_and_expiry() {
224        let c = conn();
225        for id in ["current", "future", "expired", "invalid"] {
226            insert(&c, id, "preference", id, 0.0);
227        }
228        c.execute(
229            "UPDATE memories SET valid_from='2099-01-01T00:00:00Z' WHERE memory_id='future'",
230            [],
231        )
232        .unwrap();
233        c.execute("UPDATE memories SET valid_to=strftime('%Y-%m-%dT%H:%M:%SZ','now','-1 minute') WHERE memory_id='expired'", []).unwrap();
234        c.execute(
235            "UPDATE memories SET valid_from='not-a-date' WHERE memory_id='invalid'",
236            [],
237        )
238        .unwrap();
239        let profile = preferences(&c, false, 10).unwrap();
240        assert_eq!(
241            profile
242                .iter()
243                .map(|p| p.memory_id.as_str())
244                .collect::<Vec<_>>(),
245            vec!["current"]
246        );
247    }
248
249    /// Specificity wins when the budget runs out: a preference stated for this
250    /// repo beats one carried across every project.
251    #[test]
252    fn project_preferences_lead_and_globals_fill_the_remainder() {
253        let project = conn();
254        let user = conn();
255        insert(&project, "proj", "preference", "prefer thiserror here", 0.0);
256        insert(
257            &user,
258            "glob",
259            "preference",
260            "prefer 2-space indent everywhere",
261            0.0,
262        );
263
264        let profile = build_profile(&project, Some(&user)).expect("profile");
265        assert_eq!(profile.len(), 2);
266        assert!(!profile[0].global, "project first: {profile:?}");
267        assert!(profile[1].global);
268    }
269
270    #[test]
271    fn a_global_duplicate_is_not_repeated() {
272        let project = conn();
273        let user = conn();
274        insert(&project, "proj", "preference", "prefer thiserror", 0.0);
275        insert(&user, "glob", "preference", "prefer thiserror", 0.0);
276        let profile = build_profile(&project, Some(&user)).expect("profile");
277        assert_eq!(profile.len(), 1, "got: {profile:?}");
278    }
279
280    #[test]
281    fn the_profile_is_capped() {
282        let c = conn();
283        for i in 0..(MAX_PREFERENCES * 3) {
284            insert(
285                &c,
286                &format!("p{i}"),
287                "preference",
288                &format!("preference {i}"),
289                0.0,
290            );
291        }
292        let profile = build_profile(&c, None).expect("profile");
293        assert_eq!(profile.len(), MAX_PREFERENCES);
294    }
295
296    // ── Rendering ────────────────────────────────────────────────────────
297
298    #[test]
299    fn an_empty_profile_renders_nothing() {
300        assert!(render_profile(&[]).is_none());
301    }
302
303    /// Framed as instructions, not as retrieved facts — following them without
304    /// being asked is the whole difference between a preference and a memory.
305    #[test]
306    fn the_profile_reads_as_instructions() {
307        let rendered = render_profile(&[Preference {
308            memory_id: "p".into(),
309            text: "prefer thiserror for library errors".into(),
310            global: false,
311        }])
312        .expect("rendered");
313        assert!(
314            rendered.contains("follow these without being asked"),
315            "got: {rendered}"
316        );
317        assert!(rendered.contains("thiserror"));
318    }
319
320    #[test]
321    fn a_global_preference_is_marked_as_such() {
322        let rendered = render_profile(&[Preference {
323            memory_id: "p".into(),
324            text: "prefer 2-space indent".into(),
325            global: true,
326        }])
327        .expect("rendered");
328        assert!(
329            rendered.contains("across all your projects"),
330            "got: {rendered}"
331        );
332    }
333
334    /// An essay is not a preference, and a runaway profile is what makes users
335    /// turn warm start off.
336    #[test]
337    fn overlong_preferences_are_skipped_and_the_block_is_budgeted() {
338        let essay = Preference {
339            memory_id: "long".into(),
340            text: "x".repeat(MAX_PREFERENCE_CHARS + 1),
341            global: false,
342        };
343        assert!(
344            render_profile(std::slice::from_ref(&essay)).is_none(),
345            "a 160+ char 'preference' is an essay"
346        );
347
348        let many: Vec<Preference> = (0..MAX_PREFERENCES)
349            .map(|i| Preference {
350                memory_id: format!("p{i}"),
351                text: "prefer something quite specific and reasonably wordy".repeat(2),
352                global: false,
353            })
354            .collect();
355        let rendered = render_profile(&many).expect("rendered");
356        assert!(
357            rendered.len() <= PROFILE_CHAR_BUDGET + 80,
358            "block must stay budgeted: {} chars",
359            rendered.len()
360        );
361    }
362}