Files
wehub-resource-sync 26382a7ac6
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
JetBrains Plugin / Actionlint (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (rust) (push) Waiting to run
JetBrains Plugin / Validation (push) Waiting to run
JetBrains Plugin / Build (push) Waiting to run
JetBrains Plugin / Test (push) Blocked by required conditions
Security Check / Security Scan (push) Waiting to run
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:30 +08:00

308 lines
11 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Incremental delta playbook for checkpoints (#541, EFF-4 — ACE principle).
//!
//! ACE (Agentic Context Engineering, 2510.04618) showed that monolithic
//! checkpoint rewrites cause *brevity bias* (repeated summarization loses
//! detail) and *context collapse* (an observed 18k -> 122 token implosion,
//! 29% accuracy). The cure: contexts grow as structured, itemized delta
//! entries with stable IDs that are never rewritten — only appended, bumped
//! (dedup-confirm), voted on, and locally evicted.
//!
//! lean-ctx wires this into `ctx_compress`: every checkpoint distills the
//! session into playbook deltas instead of re-summarizing prior summaries.
//! Renders are ordered by stable ID, so unchanged prefixes stay prefix-cache
//! friendly across checkpoints.
use serde::{Deserialize, Serialize};
use crate::core::memory_consolidation::token_jaccard;
/// Entries with this token-Jaccard similarity to an existing entry are
/// duplicates: the existing entry gets confirmed instead of inserting.
const DEDUP_JACCARD: f64 = 0.7;
/// Entries unconfirmed for this many turns are evicted (locally).
const STALE_TURNS: u32 = 50;
/// Hard cap on entry content length (chars).
const MAX_CONTENT_CHARS: usize = 200;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EntryKind {
/// An approach that worked (decisions, successful tactics).
Strategy,
/// Something that bit us (gotchas, bounces, failed edits).
Pitfall,
/// A stable observation about the codebase or domain.
Fact,
/// A file worth remembering, with why.
FileRef,
}
impl EntryKind {
pub fn as_str(self) -> &'static str {
match self {
EntryKind::Strategy => "Strategy",
EntryKind::Pitfall => "Pitfall",
EntryKind::Fact => "Fact",
EntryKind::FileRef => "FileRef",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlaybookEntry {
/// Stable, monotonically assigned — never reused, never rewritten.
pub id: u32,
pub kind: EntryKind,
pub content: String,
pub created_turn: u32,
pub last_confirmed_turn: u32,
pub helpful_votes: u32,
pub harmful_votes: u32,
}
impl PlaybookEntry {
fn salience(&self) -> i64 {
i64::from(self.helpful_votes) - i64::from(self.harmful_votes)
}
}
/// Outcome of a delta insert.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeltaOutcome {
Added(u32),
Confirmed(u32),
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Playbook {
pub entries: Vec<PlaybookEntry>,
pub next_id: u32,
}
impl Playbook {
/// Grow-and-refine insert: near-duplicates confirm the existing entry
/// (bump + helpful vote) instead of creating drift. Existing entries are
/// NEVER rewritten — that is the ACE anti-collapse invariant.
pub fn add_delta(&mut self, kind: EntryKind, content: &str, turn: u32) -> DeltaOutcome {
let content: String = content.trim().chars().take(MAX_CONTENT_CHARS).collect();
if let Some(existing) = self
.entries
.iter_mut()
.find(|e| e.kind == kind && token_jaccard(&e.content, &content) >= DEDUP_JACCARD)
{
existing.last_confirmed_turn = turn;
existing.helpful_votes += 1;
return DeltaOutcome::Confirmed(existing.id);
}
self.next_id += 1;
let id = self.next_id;
self.entries.push(PlaybookEntry {
id,
kind,
content,
created_turn: turn,
last_confirmed_turn: turn,
helpful_votes: 0,
harmful_votes: 0,
});
DeltaOutcome::Added(id)
}
/// Vote on an entry by stable ID (agent feedback via ctx_session).
pub fn vote(&mut self, id: u32, helpful: bool) -> bool {
match self.entries.iter_mut().find(|e| e.id == id) {
Some(e) => {
if helpful {
e.helpful_votes += 1;
} else {
e.harmful_votes += 1;
}
true
}
None => false,
}
}
/// Local eviction only: net-harmful entries and entries unconfirmed for
/// STALE_TURNS die. No global re-summarization ever happens.
pub fn evict(&mut self, current_turn: u32) -> usize {
let before = self.entries.len();
self.entries.retain(|e| {
let net_harmful = e.harmful_votes > e.helpful_votes;
let stale = current_turn.saturating_sub(e.last_confirmed_turn) > STALE_TURNS;
!(net_harmful || stale)
});
before - self.entries.len()
}
/// Render ordered by stable ID (prefix-cache friendly: old entries keep
/// their byte positions). When `top_k` is exceeded, the lowest-salience
/// entries are elided — never rewritten.
pub fn render(&self, top_k: usize) -> String {
if self.entries.is_empty() {
return String::new();
}
let mut selected: Vec<&PlaybookEntry> = self.entries.iter().collect();
let elided = if selected.len() > top_k {
selected.sort_by_key(|e| std::cmp::Reverse((e.salience(), e.last_confirmed_turn)));
let n = selected.len() - top_k;
selected.truncate(top_k);
n
} else {
0
};
selected.sort_by_key(|e| e.id);
let mut out = String::from("PLAYBOOK (delta log, stable IDs):\n");
for e in selected {
let votes = if e.helpful_votes > 0 || e.harmful_votes > 0 {
format!(" (+{}/-{})", e.helpful_votes, e.harmful_votes)
} else {
String::new()
};
out.push_str(&format!(
"[P{}] {}: {}{votes}\n",
e.id,
e.kind.as_str(),
e.content
));
}
if elided > 0 {
out.push_str(&format!(
"… {elided} low-salience entries elided (recall via ctx_session)\n"
));
}
out
}
/// Total content volume (entries × chars) — used by the brevity-bias
/// regression test: repeated checkpoints must never shrink this.
pub fn information_volume(&self) -> usize {
self.entries.iter().map(|e| e.content.len()).sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_assigns_stable_monotonic_ids() {
let mut p = Playbook::default();
let a = p.add_delta(EntryKind::Fact, "billing service uses separate database", 1);
let b = p.add_delta(EntryKind::Strategy, "deploy via rsync then script", 1);
assert_eq!(a, DeltaOutcome::Added(1));
assert_eq!(b, DeltaOutcome::Added(2));
}
#[test]
fn near_duplicate_confirms_instead_of_inserting() {
let mut p = Playbook::default();
p.add_delta(
EntryKind::Fact,
"the webhook parses cancel_at from stripe",
1,
);
let out = p.add_delta(EntryKind::Fact, "webhook parses cancel_at from stripe", 5);
assert_eq!(out, DeltaOutcome::Confirmed(1));
assert_eq!(p.entries.len(), 1);
assert_eq!(p.entries[0].last_confirmed_turn, 5);
assert_eq!(p.entries[0].helpful_votes, 1);
}
#[test]
fn eviction_is_local_only() {
let mut p = Playbook::default();
p.add_delta(EntryKind::Strategy, "good strategy that keeps working", 1);
p.add_delta(EntryKind::Pitfall, "bad advice that hurt us twice", 1);
p.vote(2, false);
let evicted = p.evict(2);
assert_eq!(evicted, 1);
assert_eq!(p.entries.len(), 1);
assert_eq!(p.entries[0].id, 1, "untouched entry survives verbatim");
}
#[test]
fn stale_entries_evicted_after_50_turns() {
let mut p = Playbook::default();
p.add_delta(EntryKind::Fact, "old fact from early in the session", 1);
p.add_delta(EntryKind::Fact, "recent fact still being confirmed", 60);
let evicted = p.evict(60);
assert_eq!(evicted, 1);
assert_eq!(p.entries[0].created_turn, 60);
}
#[test]
fn render_is_stable_across_checkpoints() {
let mut p = Playbook::default();
p.add_delta(EntryKind::Fact, "fact one about the billing database", 1);
p.add_delta(EntryKind::Strategy, "strategy two for safe deploys", 1);
let r1 = p.render(10);
let r2 = p.render(10);
assert_eq!(r1, r2, "no drift without new deltas");
// A new delta only appends — existing lines keep their bytes.
p.add_delta(EntryKind::Pitfall, "pitfall three with the file lock", 2);
let r3 = p.render(10);
assert!(r3.contains("[P1]") && r3.contains("[P2]") && r3.contains("[P3]"));
for line in r1.lines().filter(|l| l.starts_with("[P")) {
assert!(r3.contains(line), "old line rewritten: {line}");
}
}
#[test]
fn brevity_bias_regression_volume_never_shrinks() {
let mut p = Playbook::default();
let mut last_volume = 0;
for turn in 1..=10 {
p.add_delta(
EntryKind::Fact,
&format!("distinct finding number {turn} about module {turn}"),
turn,
);
p.evict(turn);
let vol = p.information_volume();
assert!(
vol >= last_volume,
"checkpoint {turn} shrank information volume: {last_volume} -> {vol}"
);
last_volume = vol;
}
}
#[test]
fn render_caps_at_top_k_by_salience() {
let mut p = Playbook::default();
let topics = [
"webhook parses stripe cancellation timestamps",
"dashboard heatmap aggregates bounce counters",
"scent field decays claims exponentially",
"playbook entries keep stable identifiers",
"thresholds learn from edit failures",
"litm calibration shifts begin share",
"billing purge runs inside one transaction",
"goodbye email sends after account deletion",
"entropy mode rescues task keywords",
"bm25 index rebuilds on provider sync",
];
for (i, t) in topics.iter().enumerate() {
p.add_delta(EntryKind::Fact, t, i as u32 + 1);
}
assert_eq!(p.entries.len(), topics.len(), "fixtures must not dedup");
p.vote(7, true);
p.vote(7, true);
let r = p.render(5);
let entry_lines = r.lines().filter(|l| l.starts_with("[P")).count();
assert_eq!(entry_lines, 5);
assert!(r.contains("[P7]"), "high-salience entry survives the cut");
assert!(r.contains("elided"));
}
#[test]
fn content_capped_at_200_chars() {
let mut p = Playbook::default();
let long = "x".repeat(500);
p.add_delta(EntryKind::Fact, &long, 1);
assert_eq!(p.entries[0].content.len(), MAX_CONTENT_CHARS);
}
}