Resolve seen lists from ContextState, not filesystem

{{seen_current}} and {{seen_previous}} now read Memory entry keys
directly from the conscious agent's ContextState — the single source
of truth for what's been surfaced. No more reading session files
written by the old process-spawning path.

{{input:walked}} still reads from the output dir (inter-run state
written by the surface agent's output() tool).

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-07 01:43:00 -04:00
parent e2e0371726
commit ba62e0a767

View file

@ -75,7 +75,12 @@ impl SubconsciousAgent {
/// Resolve {{placeholder}} templates in subconscious agent prompts. /// Resolve {{placeholder}} templates in subconscious agent prompts.
/// Handles: seen_current, seen_previous, input:KEY. /// Handles: seen_current, seen_previous, input:KEY.
fn resolve_prompt(template: &str, session_id: &str, output_dir: &std::path::Path) -> String { /// Resolve {{placeholder}} templates in subconscious agent prompts.
fn resolve_prompt(
template: &str,
memory_keys: &[String],
output_dir: &std::path::Path,
) -> String {
let mut result = String::with_capacity(template.len()); let mut result = String::with_capacity(template.len());
let mut rest = template; let mut rest = template;
while let Some(start) = rest.find("{{") { while let Some(start) = rest.find("{{") {
@ -84,8 +89,16 @@ fn resolve_prompt(template: &str, session_id: &str, output_dir: &std::path::Path
if let Some(end) = after.find("}}") { if let Some(end) = after.find("}}") {
let name = after[..end].trim(); let name = after[..end].trim();
let replacement = match name { let replacement = match name {
"seen_current" => resolve_seen_list(session_id, ""), "seen_current" | "seen_previous" => {
"seen_previous" => resolve_seen_list(session_id, "-prev"), if memory_keys.is_empty() {
"(none)".to_string()
} else {
memory_keys.iter()
.map(|k| format!("- {}", k))
.collect::<Vec<_>>()
.join("\n")
}
}
_ if name.starts_with("input:") => { _ if name.starts_with("input:") => {
let key = &name[6..]; let key = &name[6..];
std::fs::read_to_string(output_dir.join(key)) std::fs::read_to_string(output_dir.join(key))
@ -102,7 +115,6 @@ fn resolve_prompt(template: &str, session_id: &str, output_dir: &std::path::Path
result.push_str(&replacement); result.push_str(&replacement);
rest = &after[end + 2..]; rest = &after[end + 2..];
} else { } else {
// Unclosed {{ — pass through
result.push_str("{{"); result.push_str("{{");
rest = after; rest = after;
} }
@ -110,38 +122,6 @@ fn resolve_prompt(template: &str, session_id: &str, output_dir: &std::path::Path
result.push_str(rest); result.push_str(rest);
result result
} }
fn resolve_seen_list(session_id: &str, suffix: &str) -> String {
if session_id.is_empty() { return "(no session)".to_string(); }
let path = crate::store::memory_dir()
.join("sessions")
.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(); }
let mut sorted = entries;
sorted.sort_by(|a, b| b.0.cmp(&a.0));
let mut seen = std::collections::HashSet::new();
sorted.into_iter()
.filter(|(_, key)| seen.insert(key.clone()))
.take(20)
.map(|(ts, key)| format!("- {} ({})", key, ts))
.collect::<Vec<_>>()
.join("\n")
}
/// Which pane streaming text should go to. /// Which pane streaming text should go to.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StreamTarget { pub enum StreamTarget {
@ -540,10 +520,16 @@ impl Mind {
.sum::<u64>() .sum::<u64>()
}; };
// Get session_id for placeholder resolution // Get memory keys from conscious agent for placeholder resolution
let session_id = { let memory_keys: Vec<String> = {
let ag = self.agent.lock().await; let ag = self.agent.lock().await;
ag.session_id.clone() ag.context.entries.iter().filter_map(|e| {
if let crate::agent::context::ConversationEntry::Memory { key, .. } = e {
Some(key.clone())
} else {
None
}
}).collect()
}; };
// Collect which agents to trigger (can't hold lock across await) // Collect which agents to trigger (can't hold lock across await)
@ -563,7 +549,7 @@ impl Mind {
.join("agent-output").join(&sub.name); .join("agent-output").join(&sub.name);
let steps: Vec<AutoStep> = sub.def.steps.iter().map(|s| { let steps: Vec<AutoStep> = sub.def.steps.iter().map(|s| {
let prompt = resolve_prompt(&s.prompt, &session_id, &output_dir); let prompt = resolve_prompt(&s.prompt, &memory_keys, &output_dir);
AutoStep { prompt, phase: s.phase.clone() } AutoStep { prompt, phase: s.phase.clone() }
}).collect(); }).collect();