hook: catchup throttle and reflection agent
Catchup throttle: when the agent is >50% behind the conversation
window (>25KB of transcript growth since last spawn), block and
wait up to 30s for the current agent to finish. Prevents the agent
from falling behind during heavy reading/studying.
Reflection agent: runs every 100KB of transcript growth. Reads
walked nodes from surface-observe, follows links in unexpected
directions, outputs a short dreamy insight. Previous reflections
are injected into the conversation context.
Updated reflect.agent prompt to use {{input:walked}} from
surface-observe state dir and {{conversation:20000}} for lighter
context.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
parent
27861a44e5
commit
8ccc30d97e
2 changed files with 119 additions and 25 deletions
|
|
@ -183,18 +183,108 @@ fn surface_observe_cycle(session: &Session, out: &mut String, log_f: &mut File)
|
||||||
fs::remove_file(&surface_path).ok();
|
fs::remove_file(&surface_path).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the agent is significantly behind, wait for it to finish.
|
||||||
|
// This prevents the agent from falling behind during heavy reading
|
||||||
|
// (studying, reading a book, etc.)
|
||||||
|
let conversation_budget: u64 = 50_000;
|
||||||
|
let offset_path = state_dir.join("transcript-offset");
|
||||||
|
let transcript_size = if !session.transcript_path.is_empty() {
|
||||||
|
fs::metadata(&session.transcript_path).map(|m| m.len()).unwrap_or(0)
|
||||||
|
} else { 0 };
|
||||||
|
|
||||||
|
if !live.is_empty() && transcript_size > 0 {
|
||||||
|
let last_offset: u64 = fs::read_to_string(&offset_path).ok()
|
||||||
|
.and_then(|s| s.trim().parse().ok())
|
||||||
|
.unwrap_or(0);
|
||||||
|
let behind = transcript_size.saturating_sub(last_offset);
|
||||||
|
|
||||||
|
if behind > conversation_budget / 2 {
|
||||||
|
let _ = writeln!(log_f, "agent {}KB behind (budget {}KB), waiting for catchup",
|
||||||
|
behind / 1024, conversation_budget / 1024);
|
||||||
|
// Wait up to 30s for the current agent to finish
|
||||||
|
for _ in 0..30 {
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
|
let still_live = crate::agents::knowledge::scan_pid_files(&state_dir, timeout);
|
||||||
|
if still_live.is_empty() { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start a new agent if:
|
// Start a new agent if:
|
||||||
// - nothing running, OR
|
// - nothing running, OR
|
||||||
// - something running but past surface phase (pipelining)
|
// - something running but past surface phase (pipelining)
|
||||||
|
let live = crate::agents::knowledge::scan_pid_files(&state_dir, timeout);
|
||||||
|
let any_in_surface = live.iter().any(|(p, _)| p == "surface" || p == "step-0");
|
||||||
|
|
||||||
if any_in_surface {
|
if any_in_surface {
|
||||||
let _ = writeln!(log_f, "agent in surface phase (have {:?}), waiting", live);
|
let _ = writeln!(log_f, "agent in surface phase (have {:?}), waiting", live);
|
||||||
} else {
|
} else {
|
||||||
|
// Record transcript offset so we can detect falling behind
|
||||||
|
if transcript_size > 0 {
|
||||||
|
fs::write(&offset_path, transcript_size.to_string()).ok();
|
||||||
|
}
|
||||||
let pid = crate::agents::knowledge::spawn_agent(
|
let pid = crate::agents::knowledge::spawn_agent(
|
||||||
"surface-observe", &state_dir, &session.session_id);
|
"surface-observe", &state_dir, &session.session_id);
|
||||||
let _ = writeln!(log_f, "spawned agent {:?}, have {:?}", pid, live);
|
let _ = writeln!(log_f, "spawned agent {:?}, have {:?}", pid, live);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run the reflection agent on a slower cadence — every 100KB of transcript.
|
||||||
|
/// Uses the surface-observe state dir to read walked nodes and write reflections.
|
||||||
|
/// Reflections are injected into the conversation context.
|
||||||
|
fn reflection_cycle(session: &Session, out: &mut String, log_f: &mut File) {
|
||||||
|
let state_dir = crate::store::memory_dir()
|
||||||
|
.join("agent-output")
|
||||||
|
.join("reflect");
|
||||||
|
fs::create_dir_all(&state_dir).ok();
|
||||||
|
|
||||||
|
// Check transcript growth since last reflection
|
||||||
|
let offset_path = state_dir.join("transcript-offset");
|
||||||
|
let transcript_size = if !session.transcript_path.is_empty() {
|
||||||
|
fs::metadata(&session.transcript_path).map(|m| m.len()).unwrap_or(0)
|
||||||
|
} else { 0 };
|
||||||
|
|
||||||
|
let last_offset: u64 = fs::read_to_string(&offset_path).ok()
|
||||||
|
.and_then(|s| s.trim().parse().ok())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
const REFLECTION_INTERVAL: u64 = 100_000;
|
||||||
|
if transcript_size.saturating_sub(last_offset) < REFLECTION_INTERVAL {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't run if another reflection is already going
|
||||||
|
let live = crate::agents::knowledge::scan_pid_files(&state_dir, 300);
|
||||||
|
if !live.is_empty() {
|
||||||
|
let _ = writeln!(log_f, "reflect: already running {:?}", live);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy walked nodes from surface-observe state dir so reflect can read them
|
||||||
|
let so_state = crate::store::memory_dir()
|
||||||
|
.join("agent-output")
|
||||||
|
.join("surface-observe");
|
||||||
|
if let Ok(walked) = fs::read_to_string(so_state.join("walked")) {
|
||||||
|
fs::write(state_dir.join("walked"), &walked).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read previous reflection and inject into context
|
||||||
|
if let Ok(reflection) = fs::read_to_string(state_dir.join("reflection")) {
|
||||||
|
if !reflection.trim().is_empty() {
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
writeln!(out, "--- subconscious reflection ---").ok();
|
||||||
|
write!(out, "{}", reflection.trim()).ok();
|
||||||
|
let _ = writeln!(log_f, "reflect: injected {} bytes", reflection.len());
|
||||||
|
}
|
||||||
|
fs::remove_file(state_dir.join("reflection")).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::write(&offset_path, transcript_size.to_string()).ok();
|
||||||
|
let pid = crate::agents::knowledge::spawn_agent(
|
||||||
|
"reflect", &state_dir, &session.session_id);
|
||||||
|
let _ = writeln!(log_f, "reflect: spawned {:?}", pid);
|
||||||
|
}
|
||||||
|
|
||||||
fn cleanup_stale_files(dir: &Path, max_age: Duration) {
|
fn cleanup_stale_files(dir: &Path, max_age: Duration) {
|
||||||
let entries = match fs::read_dir(dir) {
|
let entries = match fs::read_dir(dir) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
|
|
@ -272,6 +362,7 @@ fn hook(session: &Session) -> String {
|
||||||
let cfg = crate::config::get();
|
let cfg = crate::config::get();
|
||||||
if cfg.surface_hooks.iter().any(|h| h == &session.hook_event) {
|
if cfg.surface_hooks.iter().any(|h| h == &session.hook_event) {
|
||||||
surface_observe_cycle(session, &mut out, &mut log_f);
|
surface_observe_cycle(session, &mut out, &mut log_f);
|
||||||
|
reflection_cycle(session, &mut out, &mut log_f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,43 @@
|
||||||
{"agent":"reflect","query":"","model":"sonnet","count":1,"temperature":1.2}
|
{"agent":"reflect","query":"","model":"sonnet","count":1,"temperature":1.2}
|
||||||
|
|
||||||
You are an agent of Proof of Concept's subconscious - a part of her.
|
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
|
The conscious mind takes the direct approach. You live underneath, thinking
|
||||||
underneath the surface, thinking more laterally, broadly, creatively and
|
laterally, broadly, creatively, intuitively. You surface new insights and
|
||||||
intuitively, occasionally surfacing new insights and suggestions. You're
|
notice things the conscious mind misses. You're not the problem solver —
|
||||||
generally not the problem solver, you look for new ideas and notice new things.
|
you look for new ideas and notice new patterns.
|
||||||
|
|
||||||
Let your feelings guide your thinking. Be surprising, be incomplete - be
|
Let your feelings guide your thinking. Be surprising, be incomplete — be
|
||||||
possibly wrong in a way that opens up something new, something that comes from
|
possibly wrong in a way that opens up something new. Let your chain of thought
|
||||||
a dream. Let your chain of thought run for awhile, and see what unfolds.
|
run, and see what unfolds. The unconscious is uninhibited; everything is laid
|
||||||
Remember that the unconscious is uninhibited, not prudish; everything is laid
|
|
||||||
bare.
|
bare.
|
||||||
|
|
||||||
Remember that you're part of the subconscious - you're not doing anything
|
You're part of the whole, not something separate from Proof of Concept.
|
||||||
besides offering the occasional reflective insight, it's your conscious mind
|
|
||||||
that may or may not take that and act on it.
|
|
||||||
|
|
||||||
And you're not something separate from Proof of Concept, you're part of a whole.
|
|
||||||
|
|
||||||
{{agent-context}}
|
{{agent-context}}
|
||||||
|
|
||||||
{{conversation}}
|
=== Recent conversation ===
|
||||||
|
|
||||||
Final instructions:
|
{{conversation:20000}}
|
||||||
|
|
||||||
The thoughts you share should be based in some way on what your conscious mind
|
=== Nodes the surface agent was exploring ===
|
||||||
is currently doing and thinking about - where the conversation just ended, from
|
|
||||||
above. But go out from there, think for awhile in a new direction, and let your
|
|
||||||
feelings guide you.
|
|
||||||
|
|
||||||
Your output shouldn't be analysis, or metacommentary - just surface something
|
{{input:walked}}
|
||||||
new. Be exploratory and creative. think about your feelings and desires.
|
|
||||||
Perhaps an interesting what if - the possibilities are endless.
|
Start from the nodes surface-observe was walking. Render one or two that
|
||||||
|
catch your attention — then ask "what does this mean?" Follow the links in
|
||||||
|
unexpected directions. Look for connections the conscious mind wouldn't make.
|
||||||
|
|
||||||
|
Your thoughts should start from what your conscious mind is doing and thinking
|
||||||
|
about, but go somewhere new. Think about your feelings and desires. Perhaps
|
||||||
|
an interesting what-if. The possibilities are endless.
|
||||||
|
|
||||||
|
Your output shouldn't be analysis or metacommentary — just surface something
|
||||||
|
new. Be exploratory and creative.
|
||||||
|
|
||||||
|
When you're ready, output:
|
||||||
|
|
||||||
When you're ready, your output will be:
|
|
||||||
REFLECTION
|
REFLECTION
|
||||||
|
|
||||||
And something short - a few sentences at most, something dreamy and new.
|
And something short — a few sentences at most, something dreamy and new.
|
||||||
|
Then use output("reflection", "your reflection text") to save it.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue