surface agent: split seen_recent into seen_current/seen_previous placeholders
Two separate placeholders give the agent structural clarity about which memories are already in context vs which were surfaced before compaction and may need re-surfacing. Also adds memory_ratio placeholder so the agent can self-regulate based on how much of context is already recalled memories. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
53b63ab45b
commit
134f7308e3
2 changed files with 131 additions and 76 deletions
|
|
@ -428,9 +428,21 @@ fn resolve(
|
|||
else { Some(Resolved { text, keys: vec![] }) }
|
||||
}
|
||||
|
||||
// seen_recent — recently surfaced memory keys for this session
|
||||
"seen_recent" => {
|
||||
let text = resolve_seen_recent();
|
||||
// seen_current — memories surfaced in current (post-compaction) context
|
||||
"seen_current" => {
|
||||
let text = resolve_seen_list("");
|
||||
Some(Resolved { text, keys: vec![] })
|
||||
}
|
||||
|
||||
// seen_previous — memories surfaced before last compaction
|
||||
"seen_previous" => {
|
||||
let text = resolve_seen_list("-prev");
|
||||
Some(Resolved { text, keys: vec![] })
|
||||
}
|
||||
|
||||
// memory_ratio — what % of current context is recalled memories
|
||||
"memory_ratio" => {
|
||||
let text = resolve_memory_ratio();
|
||||
Some(Resolved { text, keys: vec![] })
|
||||
}
|
||||
|
||||
|
|
@ -487,77 +499,105 @@ fn resolve_conversation() -> String {
|
|||
fragments.join("\n\n")
|
||||
}
|
||||
|
||||
/// Get recently surfaced memory keys for the current session.
|
||||
fn resolve_seen_recent() -> String {
|
||||
/// Get surfaced memory keys from a seen-set file.
|
||||
/// `suffix` is "" for current, "-prev" for pre-compaction.
|
||||
fn resolve_seen_list(suffix: &str) -> String {
|
||||
let session_id = std::env::var("POC_SESSION_ID").unwrap_or_default();
|
||||
if session_id.is_empty() {
|
||||
return "(no session ID — cannot load seen set)".to_string();
|
||||
return "(no session ID)".to_string();
|
||||
}
|
||||
|
||||
let state_dir = std::path::PathBuf::from("/tmp/claude-memory-search");
|
||||
let path = state_dir.join(format!("seen{}-{}", suffix, session_id));
|
||||
|
||||
let entries: Vec<(String, String)> = std::fs::read_to_string(&path).ok()
|
||||
.map(|content| {
|
||||
content.lines()
|
||||
.filter(|s| !s.is_empty())
|
||||
.filter_map(|line| {
|
||||
let (ts, key) = line.split_once('\t')?;
|
||||
Some((ts.to_string(), key.to_string()))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if entries.is_empty() {
|
||||
return "(none)".to_string();
|
||||
}
|
||||
|
||||
// Sort newest first, dedup, cap at 20
|
||||
let mut sorted = entries;
|
||||
sorted.sort_by(|a, b| b.0.cmp(&a.0));
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let deduped: Vec<_> = sorted.into_iter()
|
||||
.filter(|(_, key)| seen.insert(key.clone()))
|
||||
.take(20)
|
||||
.collect();
|
||||
|
||||
deduped.iter()
|
||||
.map(|(ts, key)| format!("- {} ({})", key, ts))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
/// Compute what percentage of the current conversation context is recalled memories.
|
||||
/// Sums rendered size of current seen-set keys vs total post-compaction transcript size.
|
||||
fn resolve_memory_ratio() -> String {
|
||||
let session_id = std::env::var("POC_SESSION_ID").unwrap_or_default();
|
||||
if session_id.is_empty() {
|
||||
return "(no session ID)".to_string();
|
||||
}
|
||||
|
||||
let state_dir = std::path::PathBuf::from("/tmp/claude-memory-search");
|
||||
|
||||
let parse_seen = |suffix: &str| -> Vec<(String, String)> {
|
||||
let path = state_dir.join(format!("seen{}-{}", suffix, session_id));
|
||||
std::fs::read_to_string(&path).ok()
|
||||
.map(|content| {
|
||||
content.lines()
|
||||
.filter(|s| !s.is_empty())
|
||||
.filter_map(|line| {
|
||||
let (ts, key) = line.split_once('\t')?;
|
||||
Some((ts.to_string(), key.to_string()))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
let current = parse_seen("");
|
||||
let prev = parse_seen("-prev");
|
||||
|
||||
if current.is_empty() && prev.is_empty() {
|
||||
return "(no memories surfaced yet this session)".to_string();
|
||||
}
|
||||
|
||||
let mut out = String::new();
|
||||
|
||||
const MAX_ROOTS: usize = 20;
|
||||
|
||||
// Current: already in this context, don't re-surface
|
||||
// Sort newest first, dedup, cap
|
||||
let mut current_sorted = current.clone();
|
||||
current_sorted.sort_by(|a, b| b.0.cmp(&a.0));
|
||||
let mut seen_keys = std::collections::HashSet::new();
|
||||
let current_deduped: Vec<_> = current_sorted.into_iter()
|
||||
.filter(|(_, key)| seen_keys.insert(key.clone()))
|
||||
.take(MAX_ROOTS)
|
||||
.collect();
|
||||
|
||||
if !current_deduped.is_empty() {
|
||||
out.push_str("Already surfaced this context (don't re-surface unless conversation shifted):\n");
|
||||
for (ts, key) in ¤t_deduped {
|
||||
out.push_str(&format!("- {} (surfaced {})\n", key, ts));
|
||||
}
|
||||
}
|
||||
|
||||
// Prev: surfaced before compaction, MAY need re-surfacing
|
||||
// Exclude anything already in current, sort newest first, cap at remaining budget
|
||||
let remaining = MAX_ROOTS.saturating_sub(current_deduped.len());
|
||||
if remaining > 0 && !prev.is_empty() {
|
||||
let mut prev_sorted = prev.clone();
|
||||
prev_sorted.sort_by(|a, b| b.0.cmp(&a.0));
|
||||
let prev_deduped: Vec<_> = prev_sorted.into_iter()
|
||||
.filter(|(_, key)| seen_keys.insert(key.clone()))
|
||||
.take(remaining)
|
||||
.collect();
|
||||
if !prev_deduped.is_empty() {
|
||||
out.push_str("\nSurfaced before compaction (context was reset — re-surface if still relevant):\n");
|
||||
for (ts, key) in &prev_deduped {
|
||||
out.push_str(&format!("- {} (pre-compaction, {})\n", key, ts));
|
||||
// Get post-compaction transcript size
|
||||
let projects = crate::config::get().projects_dir.clone();
|
||||
let transcript_size: u64 = std::fs::read_dir(&projects).ok()
|
||||
.and_then(|dirs| {
|
||||
for dir in dirs.filter_map(|e| e.ok()) {
|
||||
let path = dir.path().join(format!("{}.jsonl", session_id));
|
||||
if path.exists() {
|
||||
let file_len = path.metadata().map(|m| m.len()).unwrap_or(0);
|
||||
let compaction_offset: u64 = std::fs::read_to_string(
|
||||
state_dir.join(format!("compaction-{}", session_id))
|
||||
).ok().and_then(|s| s.trim().parse().ok()).unwrap_or(0);
|
||||
return Some(file_len.saturating_sub(compaction_offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
if transcript_size == 0 {
|
||||
return "0% of context is recalled memories (new session)".to_string();
|
||||
}
|
||||
|
||||
out.trim_end().to_string()
|
||||
// Sum rendered size of each key in current seen set
|
||||
let seen_path = state_dir.join(format!("seen-{}", session_id));
|
||||
let mut seen_keys = std::collections::HashSet::new();
|
||||
let keys: Vec<String> = std::fs::read_to_string(&seen_path).ok()
|
||||
.map(|content| {
|
||||
content.lines()
|
||||
.filter(|s| !s.is_empty())
|
||||
.filter_map(|line| line.split_once('\t').map(|(_, k)| k.to_string()))
|
||||
.filter(|k| seen_keys.insert(k.clone()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let memory_bytes: u64 = keys.iter()
|
||||
.filter_map(|key| {
|
||||
std::process::Command::new("poc-memory")
|
||||
.args(["render", key])
|
||||
.output().ok()
|
||||
})
|
||||
.map(|out| out.stdout.len() as u64)
|
||||
.sum();
|
||||
|
||||
let pct = (memory_bytes as f64 / transcript_size as f64 * 100.0).round() as u32;
|
||||
format!("{}% of current context is recalled memories ({} memories, ~{}KB of ~{}KB)",
|
||||
pct, keys.len(), memory_bytes / 1024, transcript_size / 1024)
|
||||
}
|
||||
|
||||
/// Resolve all {{placeholder}} patterns in a prompt template.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue