diff --git a/src/agent/oneshot.rs b/src/agent/oneshot.rs index 2f3e2b1..3e0021b 100644 --- a/src/agent/oneshot.rs +++ b/src/agent/oneshot.rs @@ -56,6 +56,9 @@ pub struct AutoAgent { priority: i32, /// Memory keys the surface agent was exploring — persists between runs. pub walked: Vec, + /// Named outputs from the agent's output() tool calls. + /// Collected per-run, read by Mind after completion. + pub outputs: std::collections::HashMap, // Observable status pub current_phase: String, pub turn: usize, @@ -159,6 +162,7 @@ impl AutoAgent { }, priority, walked: Vec::new(), + outputs: std::collections::HashMap::new(), current_phase: String::new(), turn: 0, } @@ -206,6 +210,7 @@ impl AutoAgent { bail_fn: Option<&(dyn Fn(usize) -> Result<(), String> + Sync)>, ) -> Result { self.turn = 0; + self.outputs.clear(); self.current_phase = self.steps.first() .map(|s| s.phase.clone()).unwrap_or_default(); 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()); if has_tools { - Self::dispatch_tools(backend, &msg).await; + self.dispatch_tools(backend, &msg).await; continue; } @@ -324,7 +329,7 @@ impl AutoAgent { 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(); if let Some(ref mut calls) = sanitized.tool_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.push_raw(Message::tool_result(&call.id, &output)); } diff --git a/src/mind/mod.rs b/src/mind/mod.rs index 2505660..5664fb4 100644 --- a/src/mind/mod.rs +++ b/src/mind/mod.rs @@ -457,17 +457,30 @@ impl Mind { }; 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 { - Ok(Ok(_output)) => { - // Surfaced memories - let surface_path = output_dir.join("surface"); - if let Ok(content) = std::fs::read_to_string(&surface_path) { + Ok(Ok(_)) => { + // The outer task already put the AutoAgent back — + // read outputs from it + 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 = 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; - 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( &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 - let walked_path = output_dir.join("walked"); - if let Ok(content) = std::fs::read_to_string(&walked_path) { - let walked: Vec = 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() { + // Reflection → inject into conscious agent + if let Some(reflection) = outputs.get("reflection") { + if !reflection.trim().is_empty() { let mut ag = self.agent.lock().await; ag.push_message(crate::agent::api::types::Message::user(format!( "\n--- subconscious reflection ---\n{}\n", - content.trim(), + reflection.trim(), ))); } - std::fs::remove_file(&reflect_path).ok(); } dbglog!("[mind] {} completed", name); } - Ok(Err(e)) => dbglog!("[mind] {} failed: {}", name, e), - Err(e) => dbglog!("[mind] {} panicked: {}", name, e), + Ok(Err(e)) => dbglog!("[mind] subconscious agent failed: {}", e), + Err(e) => dbglog!("[mind] subconscious agent panicked: {}", e), } } } @@ -561,17 +556,12 @@ impl Mind { let conscious = self.agent.lock().await; let mut spawns = Vec::new(); 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); let forked = conscious.fork(auto.tools.clone()); let keys = memory_keys.clone(); let handle: tokio::task::JoinHandle<(AutoAgent, Result)> = tokio::spawn(async move { - unsafe { std::env::set_var("POC_AGENT_OUTPUT_DIR", &output_dir); } let result = auto.run_forked(&forked, &keys).await; (auto, result) });