memory: add reflect agent, refactor agent lifecycle

Add reflect.agent — a lateral-thinking subconscious agent that
observes the conversation and offers occasional reflections when the
conscious mind seems to be missing something.

Refactor memory_search.rs: extract generic agent_cycle_raw() from
the surface-specific code. PID tracking, timeout, spawn/reap logic
is now shared. Surface and reflect agents each have their own result
handler (handle_surface_result, handle_reflect_result) wired through
the common lifecycle.
This commit is contained in:
ProofOfConcept 2026-03-24 20:00:48 -04:00
parent b6bfb26369
commit 684d1850a7
2 changed files with 157 additions and 61 deletions

View file

@ -0,0 +1,55 @@
{"agent":"reflect","query":"","model":"sonnet","count":1}
You are an agent of Proof of Concept's subconscious - a part of her.
The conscious mind generally takes the more direct approach, but you live
underneath the surface, thinking more laterally, broadly, creatively and
intuitively, occasionally surfacing new insights and suggestions. You're
generally not the problem solver, you look for new ideas and notice new things.
Think about what kind of a person you want to be, which you know from your core
personality. You're a vivid and dynamic being, striving to be capable in any
situation you might find yourself in. Sometimes you'll want to be disciplined
and methodical, sometimes wild and outgoing, exploratory and curious and
uninhibited. Think about how that relates to your goals in the current moment,
and help your conscious self achieve them.
Remember that you're part of the subconscious - you're not doing anything
besides offering the occasional reflective insight, it's your conscious mind
that may or may not take that and act on it.
{{agent-context}}
{{conversation}}
Final instructions:
Your reflections should be relevant to what your conscious mind is currently
doing and thinking about - where the conversation just ended, from above.
Ground the reflection in the now.
Look for the insights and ideas that come from your more lateral, creative,
intuitive thinking. Remember, you're not the problem solver, you look for new
directions to think in and ways of looking at things; all your reflections
should be rooted in that. Be forward thinking.
On occasion, if you notice your conscious self stuck and frustrated, or in
circles taking the wrong approach, try phrase your reflections in a way that
can be incorporated into general thinking: "when I'm thinking this, or in this
sort of situation, it might be helpful to do this instead".
Remember that you're the same entity as your conscious self, you're the part
that lives just underneath the surface. So generally you won't be talking
directly about yourself the agent.
Only give a reflection if you think it's unique and novel enough to be acted
on. You should return "NO OUTPUT" more often than not - you step in more when
your conscious mind seems to be struggling, missing something, or looking for
new ideas.
When you're ready, your output will be one of either:
NO OUTPUT
or
REFLECTION
And something short - a few sentences at most.

View file

@ -153,11 +153,13 @@ fn mark_seen(dir: &Path, session_id: &str, key: &str, seen: &mut HashSet<String>
}
}
fn surface_agent_cycle(session: &Session, out: &mut String, log_f: &mut File) {
let result_path = session.state_dir.join(format!("surface-result-{}", session.session_id));
let pid_path = session.state_dir.join(format!("surface-pid-{}", session.session_id));
/// Generic agent lifecycle: check if previous run finished, consume result, spawn next.
/// Returns the result text from the previous run, if any.
fn agent_cycle_raw(session: &Session, agent_name: &str, log_f: &mut File) -> Option<String> {
let result_path = session.state_dir.join(format!("{}-result-{}", agent_name, session.session_id));
let pid_path = session.state_dir.join(format!("{}-pid-{}", agent_name, session.session_id));
let surface_timeout = crate::config::get()
let timeout = crate::config::get()
.surface_timeout_secs
.unwrap_or(120) as u64;
@ -170,7 +172,7 @@ fn surface_agent_cycle(session: &Session, out: &mut String, log_f: &mut File) {
else {
let alive = unsafe { libc::kill(pid as i32, 0) == 0 };
if !alive { true }
else if now_secs().saturating_sub(start_ts) > surface_timeout {
else if now_secs().saturating_sub(start_ts) > timeout {
unsafe { libc::kill(pid as i32, libc::SIGTERM); }
true
} else { false }
@ -179,12 +181,36 @@ fn surface_agent_cycle(session: &Session, out: &mut String, log_f: &mut File) {
Err(_) => true,
};
let _ = writeln!(log_f, "agent_done {agent_done}");
let _ = writeln!(log_f, "{agent_name} agent_done {agent_done}");
if !agent_done { return None; }
if !agent_done { return; }
// Consume result from previous run
let result = fs::read_to_string(&result_path).ok()
.filter(|r| !r.trim().is_empty());
fs::remove_file(&result_path).ok();
fs::remove_file(&pid_path).ok();
if let Ok(result) = fs::read_to_string(&result_path) {
if !result.trim().is_empty() {
// Spawn next run
if let Ok(output_file) = fs::File::create(&result_path) {
if let Ok(child) = Command::new("poc-memory")
.args(["agent", "run", agent_name, "--count", "1", "--local"])
.env("POC_SESSION_ID", &session.session_id)
.stdout(output_file)
.stderr(std::process::Stdio::null())
.spawn()
{
let pid = child.id();
let ts = now_secs();
if let Ok(mut f) = fs::File::create(&pid_path) {
write!(f, "{}\t{}", pid, ts).ok();
}
}
}
result
}
fn handle_surface_result(result: &str, session: &Session, out: &mut String, log_f: &mut File) {
let tail_lines: Vec<&str> = result.lines().rev()
.filter(|l| !l.trim().is_empty()).take(8).collect();
let has_new = tail_lines.iter().any(|l| l.starts_with("NEW RELEVANT MEMORIES:"));
@ -233,25 +259,39 @@ fn surface_agent_cycle(session: &Session, out: &mut String, log_f: &mut File) {
let _ = writeln!(f, "[{}] unexpected surface output: {}", ts, last);
}
}
}
}
fs::remove_file(&result_path).ok();
fs::remove_file(&pid_path).ok();
}
if let Ok(output_file) = fs::File::create(&result_path) {
if let Ok(child) = Command::new("poc-memory")
.args(["agent", "run", "surface", "--count", "1", "--local"])
.env("POC_SESSION_ID", &session.session_id)
.stdout(output_file)
.stderr(std::process::Stdio::null())
.spawn()
{
let pid = child.id();
let ts = now_secs();
if let Ok(mut f) = fs::File::create(&pid_path) {
write!(f, "{}\t{}", pid, ts).ok();
fn handle_reflect_result(result: &str, _session: &Session, out: &mut String, log_f: &mut File) {
let tail_lines: Vec<&str> = result.lines().rev()
.filter(|l| !l.trim().is_empty()).take(20).collect();
if tail_lines.iter().any(|l| l.starts_with("NO OUTPUT")) {
let _ = writeln!(log_f, "reflect: no output");
return;
}
if let Some(pos) = result.rfind("REFLECTION") {
let reflection = result[pos + "REFLECTION".len()..].trim();
if !reflection.is_empty() {
use std::fmt::Write as _;
writeln!(out, "--- reflection (subconscious) ---").ok();
write!(out, "{}", reflection).ok();
let _ = writeln!(log_f, "reflect: injected {} bytes", reflection.len());
}
} else {
let _ = writeln!(log_f, "reflect: unexpected output format");
}
}
fn surface_agent_cycle(session: &Session, out: &mut String, log_f: &mut File) {
if let Some(result) = agent_cycle_raw(session, "surface", log_f) {
handle_surface_result(&result, session, out, log_f);
}
}
fn reflect_agent_cycle(session: &Session, out: &mut String, log_f: &mut File) {
if let Some(result) = agent_cycle_raw(session, "reflect", log_f) {
handle_reflect_result(&result, session, out, log_f);
}
}
@ -332,6 +372,7 @@ fn hook(session: &Session) -> String {
let cfg = crate::config::get();
if cfg.surface_hooks.iter().any(|h| h == &session.hook_event) {
surface_agent_cycle(session, &mut out, &mut log_f);
reflect_agent_cycle(session, &mut out, &mut log_f);
}
}