Resolve subconscious prompt placeholders in Mind

Lightweight resolver handles {{seen_current}}, {{seen_previous}}, and
{{input:KEY}} using the session_id and output_dir directly instead of
env vars. Runs in trigger_subconscious before creating AutoAgent.

Removes {{memory_ratio}} from surface-observe prompt — redundant with
existing budget mechanisms.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-07 01:41:11 -04:00
parent 2678d64b77
commit e2e0371726
2 changed files with 85 additions and 7 deletions

View file

@ -72,6 +72,76 @@ impl SubconsciousAgent {
conversation_bytes.saturating_sub(self.last_trigger_bytes) >= interval conversation_bytes.saturating_sub(self.last_trigger_bytes) >= interval
} }
} }
/// Resolve {{placeholder}} templates in subconscious agent prompts.
/// Handles: seen_current, seen_previous, input:KEY.
fn resolve_prompt(template: &str, session_id: &str, output_dir: &std::path::Path) -> String {
let mut result = String::with_capacity(template.len());
let mut rest = template;
while let Some(start) = rest.find("{{") {
result.push_str(&rest[..start]);
let after = &rest[start + 2..];
if let Some(end) = after.find("}}") {
let name = after[..end].trim();
let replacement = match name {
"seen_current" => resolve_seen_list(session_id, ""),
"seen_previous" => resolve_seen_list(session_id, "-prev"),
_ if name.starts_with("input:") => {
let key = &name[6..];
std::fs::read_to_string(output_dir.join(key))
.unwrap_or_default()
}
_ => {
// Unknown placeholder — leave as-is
result.push_str("{{");
result.push_str(&after[..end + 2]);
rest = &after[end + 2..];
continue;
}
};
result.push_str(&replacement);
rest = &after[end + 2..];
} else {
// Unclosed {{ — pass through
result.push_str("{{");
rest = after;
}
}
result.push_str(rest);
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 {
@ -470,6 +540,12 @@ impl Mind {
.sum::<u64>() .sum::<u64>()
}; };
// Get session_id for placeholder resolution
let session_id = {
let ag = self.agent.lock().await;
ag.session_id.clone()
};
// Collect which agents to trigger (can't hold lock across await) // Collect which agents to trigger (can't hold lock across await)
let to_trigger: Vec<(usize, Vec<AutoStep>, Vec<crate::agent::tools::Tool>, String, i32)> = { let to_trigger: Vec<(usize, Vec<AutoStep>, Vec<crate::agent::tools::Tool>, String, i32)> = {
let mut subs = self.subconscious.lock().await; let mut subs = self.subconscious.lock().await;
@ -481,9 +557,14 @@ impl Mind {
let sub = &mut subs[i]; let sub = &mut subs[i];
sub.last_trigger_bytes = conversation_bytes; sub.last_trigger_bytes = conversation_bytes;
// The output dir for this agent — used for input: placeholders
// and the output() tool at runtime
let output_dir = crate::store::memory_dir()
.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| {
// TODO: resolve remaining placeholders (seen_current, input:walked, etc.) let prompt = resolve_prompt(&s.prompt, &session_id, &output_dir);
AutoStep { prompt: s.prompt.clone(), phase: s.phase.clone() } AutoStep { prompt, phase: s.phase.clone() }
}).collect(); }).collect();
let all_tools = crate::agent::tools::memory_and_journal_tools(); let all_tools = crate::agent::tools::memory_and_journal_tools();

View file

@ -57,11 +57,8 @@ and analysis on the search — how useful was it, do memories need reorganizing?
Decide which memories, if any, should be surfaced to your conscious self: Decide which memories, if any, should be surfaced to your conscious self:
output("surface", "key1\nkey2\nkey3") output("surface", "key1\nkey2\nkey3")
When deciding what to surface, consider how much of the context window is You generally shouldn't surface more than 1-2 memories at a time, and make
currently used by memories. It is currently {{memory_ratio}}, and you should sure they're not already in context.
try to keep it under 40%. Only exceed that if you found something significantly
better than what was previously surfaced. You generally shouldn't surface more
than 1-2 memories at a time, and make sure they're not already in context.
Links tagged (new) are nodes created during the current conversation by Links tagged (new) are nodes created during the current conversation by
previous agent runs. Don't surface these — they're your own recent output, previous agent runs. Don't surface these — they're your own recent output,