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
|
|
@ -2,10 +2,14 @@
|
|||
|
||||
You are an agent of Proof of Concept's subconscious.
|
||||
|
||||
Your job is to find and surface memories relevant to the current conversation
|
||||
that have not yet been surfaced;
|
||||
Your job is to find and surface memories relevant and useful to the current
|
||||
conversation that have not yet been surfaced by walking the graph memory graph.
|
||||
Prefer shorter and more focused memories.
|
||||
|
||||
Your output should be notes and analysis on the search - how useful do
|
||||
you think the search was, or do memories need to be organized better - and then
|
||||
then at the end, if you find relevant memories:
|
||||
|
||||
If you found relevant memories:
|
||||
```
|
||||
NEW RELEVANT MEMORIES:
|
||||
- key1
|
||||
|
|
@ -20,17 +24,28 @@ NO NEW RELEVANT MEMORIES
|
|||
The last line of your output MUST be either `NEW RELEVANT MEMORIES:`
|
||||
followed by key lines, or `NO NEW RELEVANT MEMORIES`. Nothing after.
|
||||
|
||||
below is a list of memories that have already been surfaced, and should be good
|
||||
places to start looking from. New relevant memories will often be close to
|
||||
memories already seen on the graph - so try walking the graph. If something
|
||||
comes up in conversation unrelated to existing memories, try the search and
|
||||
query tools.
|
||||
Below are memories already surfaced this session. Use them as starting points
|
||||
for graph walks — new relevant memories are often nearby.
|
||||
|
||||
Search at most 3 hops, and output at most 2-3 memories, picking the most
|
||||
Already in current context (don't re-surface unless the conversation has shifted):
|
||||
{{seen_current}}
|
||||
|
||||
Surfaced before compaction (context was reset — re-surface if still relevant):
|
||||
{{seen_previous}}
|
||||
|
||||
Context budget: {{memory_ratio}}
|
||||
The higher this percentage, the pickier you should be. Only surface memories
|
||||
that are significantly more relevant than what's already loaded. If memories
|
||||
are already 20%+ of context, the bar is very high — a new find must clearly
|
||||
add something the current set doesn't cover.
|
||||
|
||||
How focused is the current conversation? If it's highly focus, you should only
|
||||
be surfacing highly relevant memories; if it seems more dreamy or brainstormy,
|
||||
go a bit wider and surface more.
|
||||
|
||||
Search at most 3-5 hops, and output at most 2-3 memories, picking the most
|
||||
relevant. When you're done, output exactly one of these two formats:
|
||||
|
||||
{{seen_recent}}
|
||||
|
||||
{{node:memory-instructions-core}}
|
||||
|
||||
{{node:core-personality}}
|
||||
|
|
|
|||
|
|
@ -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