In-memory output() tool — no more POC_AGENT_OUTPUT_DIR

AutoAgent intercepts output() tool calls and stores results in an
in-memory HashMap instead of writing to the filesystem. Mind reads
auto.outputs after task completion. Eliminates the env-var-based
output dir which couldn't work with concurrent agents in one process.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-07 02:04:29 -04:00
parent 85aafd206c
commit c2a3844d69
2 changed files with 47 additions and 41 deletions

View file

@ -56,6 +56,9 @@ pub struct AutoAgent {
priority: i32, priority: i32,
/// Memory keys the surface agent was exploring — persists between runs. /// Memory keys the surface agent was exploring — persists between runs.
pub walked: Vec<String>, pub walked: Vec<String>,
/// Named outputs from the agent's output() tool calls.
/// Collected per-run, read by Mind after completion.
pub outputs: std::collections::HashMap<String, String>,
// Observable status // Observable status
pub current_phase: String, pub current_phase: String,
pub turn: usize, pub turn: usize,
@ -159,6 +162,7 @@ impl AutoAgent {
}, },
priority, priority,
walked: Vec::new(), walked: Vec::new(),
outputs: std::collections::HashMap::new(),
current_phase: String::new(), current_phase: String::new(),
turn: 0, turn: 0,
} }
@ -206,6 +210,7 @@ impl AutoAgent {
bail_fn: Option<&(dyn Fn(usize) -> Result<(), String> + Sync)>, bail_fn: Option<&(dyn Fn(usize) -> Result<(), String> + Sync)>,
) -> Result<String, String> { ) -> Result<String, String> {
self.turn = 0; self.turn = 0;
self.outputs.clear();
self.current_phase = self.steps.first() self.current_phase = self.steps.first()
.map(|s| s.phase.clone()).unwrap_or_default(); .map(|s| s.phase.clone()).unwrap_or_default();
let mut next_step = 0; let mut next_step = 0;
@ -238,7 +243,7 @@ impl AutoAgent {
let has_tools = msg.tool_calls.as_ref().is_some_and(|tc| !tc.is_empty()); let has_tools = msg.tool_calls.as_ref().is_some_and(|tc| !tc.is_empty());
if has_tools { if has_tools {
Self::dispatch_tools(backend, &msg).await; self.dispatch_tools(backend, &msg).await;
continue; continue;
} }
@ -324,7 +329,7 @@ impl AutoAgent {
unreachable!() unreachable!()
} }
async fn dispatch_tools(backend: &mut Backend, msg: &Message) { async fn dispatch_tools(&mut self, backend: &mut Backend, msg: &Message) {
let mut sanitized = msg.clone(); let mut sanitized = msg.clone();
if let Some(ref mut calls) = sanitized.tool_calls { if let Some(ref mut calls) = sanitized.tool_calls {
for call in calls { for call in calls {
@ -354,7 +359,18 @@ impl AutoAgent {
} }
}; };
let output = agent_tools::dispatch(&call.function.name, &args).await; // Intercept output() — store in-memory instead of filesystem
let output = if call.function.name == "output" {
let key = args["key"].as_str().unwrap_or("");
let value = args["value"].as_str().unwrap_or("");
if !key.is_empty() {
self.outputs.insert(key.to_string(), value.to_string());
}
format!("{}: {}", key, value)
} else {
agent_tools::dispatch(&call.function.name, &args).await
};
backend.log(format!("result: {} chars", output.len())); backend.log(format!("result: {} chars", output.len()));
backend.push_raw(Message::tool_result(&call.id, &output)); backend.push_raw(Message::tool_result(&call.id, &output));
} }

View file

@ -457,17 +457,30 @@ impl Mind {
}; };
for (idx, handle) in finished { for (idx, handle) in finished {
let name = self.subconscious.lock().await[idx].auto.name.clone();
let output_dir = crate::store::memory_dir()
.join("agent-output").join(&name);
match handle.await { match handle.await {
Ok(Ok(_output)) => { Ok(Ok(_)) => {
// Surfaced memories // The outer task already put the AutoAgent back —
let surface_path = output_dir.join("surface"); // read outputs from it
if let Ok(content) = std::fs::read_to_string(&surface_path) { let mut subs = self.subconscious.lock().await;
let name = subs[idx].auto.name.clone();
let outputs = std::mem::take(&mut subs[idx].auto.outputs);
// Walked keys — update all subconscious agents
if let Some(walked_str) = outputs.get("walked") {
let walked: Vec<String> = walked_str.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect();
for sub in subs.iter_mut() {
sub.auto.walked = walked.clone();
}
}
drop(subs);
// Surfaced memories → inject into conscious agent
if let Some(surface_str) = outputs.get("surface") {
let mut ag = self.agent.lock().await; let mut ag = self.agent.lock().await;
for key in content.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) { for key in surface_str.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
if let Some(rendered) = crate::cli::node::render_node( if let Some(rendered) = crate::cli::node::render_node(
&crate::store::Store::load().unwrap_or_default(), key, &crate::store::Store::load().unwrap_or_default(), key,
) { ) {
@ -481,41 +494,23 @@ impl Mind {
}); });
} }
} }
std::fs::remove_file(&surface_path).ok();
} }
// Walked keys — store for next run // Reflection → inject into conscious agent
let walked_path = output_dir.join("walked"); if let Some(reflection) = outputs.get("reflection") {
if let Ok(content) = std::fs::read_to_string(&walked_path) { if !reflection.trim().is_empty() {
let walked: Vec<String> = content.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect();
// Store on all subconscious agents (shared state)
let mut subs = self.subconscious.lock().await;
for sub in subs.iter_mut() {
sub.auto.walked = walked.clone();
}
std::fs::remove_file(&walked_path).ok();
}
// Reflection
let reflect_path = output_dir.join("reflection");
if let Ok(content) = std::fs::read_to_string(&reflect_path) {
if !content.trim().is_empty() {
let mut ag = self.agent.lock().await; let mut ag = self.agent.lock().await;
ag.push_message(crate::agent::api::types::Message::user(format!( ag.push_message(crate::agent::api::types::Message::user(format!(
"<system-reminder>\n--- subconscious reflection ---\n{}\n</system-reminder>", "<system-reminder>\n--- subconscious reflection ---\n{}\n</system-reminder>",
content.trim(), reflection.trim(),
))); )));
} }
std::fs::remove_file(&reflect_path).ok();
} }
dbglog!("[mind] {} completed", name); dbglog!("[mind] {} completed", name);
} }
Ok(Err(e)) => dbglog!("[mind] {} failed: {}", name, e), Ok(Err(e)) => dbglog!("[mind] subconscious agent failed: {}", e),
Err(e) => dbglog!("[mind] {} panicked: {}", name, e), Err(e) => dbglog!("[mind] subconscious agent panicked: {}", e),
} }
} }
} }
@ -561,17 +556,12 @@ impl Mind {
let conscious = self.agent.lock().await; let conscious = self.agent.lock().await;
let mut spawns = Vec::new(); let mut spawns = Vec::new();
for (idx, mut auto) in to_run { for (idx, mut auto) in to_run {
let output_dir = crate::store::memory_dir()
.join("agent-output").join(&auto.name);
std::fs::create_dir_all(&output_dir).ok();
dbglog!("[mind] triggering {}", auto.name); dbglog!("[mind] triggering {}", auto.name);
let forked = conscious.fork(auto.tools.clone()); let forked = conscious.fork(auto.tools.clone());
let keys = memory_keys.clone(); let keys = memory_keys.clone();
let handle: tokio::task::JoinHandle<(AutoAgent, Result<String, String>)> = let handle: tokio::task::JoinHandle<(AutoAgent, Result<String, String>)> =
tokio::spawn(async move { tokio::spawn(async move {
unsafe { std::env::set_var("POC_AGENT_OUTPUT_DIR", &output_dir); }
let result = auto.run_forked(&forked, &keys).await; let result = auto.run_forked(&forked, &keys).await;
(auto, result) (auto, result)
}); });