Compare commits

..

No commits in common. "be44a3bb0da51717fe996689cbc85673bcd6f50f" and "58cec97e573da39257fb67a185fd165e47143dcc" have entirely different histories.

7 changed files with 69 additions and 149 deletions

View file

@ -193,7 +193,7 @@ impl AutoAgent {
if next_step < self.steps.len() { if next_step < self.steps.len() {
backend.push_node( backend.push_node(
AstNode::system_msg(&self.steps[next_step].prompt)).await; AstNode::user_msg(&self.steps[next_step].prompt)).await;
next_step += 1; next_step += 1;
} }
@ -218,8 +218,8 @@ impl AutoAgent {
let text = result.text; let text = result.text;
if text.is_empty() { if text.is_empty() {
dbglog!("[auto] {} empty response, retrying", self.name); dbglog!("[auto] {} empty response, retrying", self.name);
backend.push_node(AstNode::system_msg( backend.push_node(AstNode::user_msg(
"Your previous response was empty. \ "[system] Your previous response was empty. \
Please respond with text or use a tool." Please respond with text or use a tool."
)).await; )).await;
continue; continue;
@ -234,7 +234,7 @@ impl AutoAgent {
} }
self.current_phase = self.steps[next_step].phase.clone(); self.current_phase = self.steps[next_step].phase.clone();
backend.push_node( backend.push_node(
AstNode::system_msg(&self.steps[next_step].prompt)).await; AstNode::user_msg(&self.steps[next_step].prompt)).await;
next_step += 1; next_step += 1;
dbglog!("[auto] {} step {}/{}", dbglog!("[auto] {} step {}/{}",
self.name, next_step, self.steps.len()); self.name, next_step, self.steps.len());

View file

@ -113,10 +113,6 @@ pub struct MindState {
pub last_turn_had_tools: bool, pub last_turn_had_tools: bool,
/// Handle to the currently running turn task. /// Handle to the currently running turn task.
pub turn_handle: Option<tokio::task::JoinHandle<()>>, pub turn_handle: Option<tokio::task::JoinHandle<()>>,
/// Unconscious agent idle state — true when 60s timer has expired.
pub unc_idle: bool,
/// When the unconscious idle timer will fire (for UI display).
pub unc_idle_deadline: Instant,
} }
impl Clone for MindState { impl Clone for MindState {
@ -133,8 +129,6 @@ impl Clone for MindState {
consecutive_errors: self.consecutive_errors, consecutive_errors: self.consecutive_errors,
last_turn_had_tools: self.last_turn_had_tools, last_turn_had_tools: self.last_turn_had_tools,
turn_handle: None, // Not cloned — only Mind's loop uses this turn_handle: None, // Not cloned — only Mind's loop uses this
unc_idle: self.unc_idle,
unc_idle_deadline: self.unc_idle_deadline,
} }
} }
} }
@ -170,8 +164,6 @@ impl MindState {
consecutive_errors: 0, consecutive_errors: 0,
last_turn_had_tools: false, last_turn_had_tools: false,
turn_handle: None, turn_handle: None,
unc_idle: false,
unc_idle_deadline: Instant::now() + std::time::Duration::from_secs(60),
} }
} }
@ -272,9 +264,6 @@ pub struct Mind {
pub unconscious: Arc<tokio::sync::Mutex<Unconscious>>, pub unconscious: Arc<tokio::sync::Mutex<Unconscious>>,
turn_tx: mpsc::Sender<(Result<TurnResult>, StreamTarget)>, turn_tx: mpsc::Sender<(Result<TurnResult>, StreamTarget)>,
turn_watch: tokio::sync::watch::Sender<bool>, turn_watch: tokio::sync::watch::Sender<bool>,
/// Signals conscious activity to the unconscious loop.
/// true = active, false = idle opportunity.
conscious_active: tokio::sync::watch::Sender<bool>,
bg_tx: mpsc::UnboundedSender<BgEvent>, bg_tx: mpsc::UnboundedSender<BgEvent>,
bg_rx: std::sync::Mutex<Option<mpsc::UnboundedReceiver<BgEvent>>>, bg_rx: std::sync::Mutex<Option<mpsc::UnboundedReceiver<BgEvent>>>,
_supervisor: crate::thalamus::supervisor::Supervisor, _supervisor: crate::thalamus::supervisor::Supervisor,
@ -302,7 +291,6 @@ impl Mind {
let shared = Arc::new(std::sync::Mutex::new(MindState::new(config.app.dmn.max_turns))); let shared = Arc::new(std::sync::Mutex::new(MindState::new(config.app.dmn.max_turns)));
let (turn_watch, _) = tokio::sync::watch::channel(false); let (turn_watch, _) = tokio::sync::watch::channel(false);
let (conscious_active, _) = tokio::sync::watch::channel(false);
let (bg_tx, bg_rx) = mpsc::unbounded_channel(); let (bg_tx, bg_rx) = mpsc::unbounded_channel();
let mut sup = crate::thalamus::supervisor::Supervisor::new(); let mut sup = crate::thalamus::supervisor::Supervisor::new();
@ -312,53 +300,10 @@ impl Mind {
let subconscious = Arc::new(tokio::sync::Mutex::new(Subconscious::new())); let subconscious = Arc::new(tokio::sync::Mutex::new(Subconscious::new()));
subconscious.lock().await.init_output_tool(subconscious.clone()); subconscious.lock().await.init_output_tool(subconscious.clone());
let unconscious = Arc::new(tokio::sync::Mutex::new(Unconscious::new()));
// Spawn the unconscious loop on its own task
if !config.no_agents {
let unc = unconscious.clone();
let shared_for_unc = shared.clone();
let mut unc_rx = conscious_active.subscribe();
tokio::spawn(async move {
const IDLE_DELAY: std::time::Duration = std::time::Duration::from_secs(60);
loop {
// Wait for conscious side to go inactive
if *unc_rx.borrow() {
if unc_rx.changed().await.is_err() { break; }
continue;
}
// Conscious is inactive — wait 60s before starting
let deadline = tokio::time::Instant::now() + IDLE_DELAY;
{
let mut s = shared_for_unc.lock().unwrap();
s.unc_idle = false;
s.unc_idle_deadline = Instant::now() + IDLE_DELAY;
}
let went_active = tokio::select! {
_ = tokio::time::sleep_until(deadline) => false,
r = unc_rx.changed() => r.is_ok(),
};
if went_active { continue; }
// Idle period reached — run agents until conscious goes active
{
let mut s = shared_for_unc.lock().unwrap();
s.unc_idle = true;
}
loop {
unc.lock().await.trigger().await;
// Check if conscious became active
if *unc_rx.borrow() { break; }
// Brief yield to not starve other tasks
tokio::task::yield_now().await;
}
}
});
}
Self { agent, shared, config, Self { agent, shared, config,
subconscious, unconscious, subconscious,
turn_tx, turn_watch, conscious_active, bg_tx, unconscious: Arc::new(tokio::sync::Mutex::new(Unconscious::new())),
turn_tx, turn_watch, bg_tx,
bg_rx: std::sync::Mutex::new(Some(bg_rx)), _supervisor: sup } bg_rx: std::sync::Mutex::new(Some(bg_rx)), _supervisor: sup }
} }
@ -559,7 +504,6 @@ impl Mind {
} }
self.shared.lock().unwrap().turn_active = true; self.shared.lock().unwrap().turn_active = true;
let _ = self.turn_watch.send(true); let _ = self.turn_watch.send(true);
let _ = self.conscious_active.send(true);
let agent = self.agent.clone(); let agent = self.agent.clone();
let result_tx = self.turn_tx.clone(); let result_tx = self.turn_tx.clone();
self.shared.lock().unwrap().turn_handle = Some(tokio::spawn(async move { self.shared.lock().unwrap().turn_handle = Some(tokio::spawn(async move {
@ -581,6 +525,7 @@ impl Mind {
let mut bg_rx = self.bg_rx.lock().unwrap().take() let mut bg_rx = self.bg_rx.lock().unwrap().take()
.expect("Mind::run() called twice"); .expect("Mind::run() called twice");
let mut sub_handle: Option<tokio::task::JoinHandle<()>> = None; let mut sub_handle: Option<tokio::task::JoinHandle<()>> = None;
let mut unc_handle: Option<tokio::task::JoinHandle<()>> = None;
loop { loop {
let (timeout, has_input) = { let (timeout, has_input) = {
let me = self.shared.lock().unwrap(); let me = self.shared.lock().unwrap();
@ -609,7 +554,6 @@ impl Mind {
} }
Some((result, target)) = turn_rx.recv() => { Some((result, target)) = turn_rx.recv() => {
let _ = self.conscious_active.send(false);
let model_switch = { let model_switch = {
let mut s = self.shared.lock().unwrap(); let mut s = self.shared.lock().unwrap();
s.turn_handle = None; s.turn_handle = None;
@ -640,6 +584,12 @@ impl Mind {
s.trigger(&agent).await; s.trigger(&agent).await;
})); }));
} }
if unc_handle.as_ref().map_or(true, |h| h.is_finished()) {
let unc = self.unconscious.clone();
unc_handle = Some(tokio::spawn(async move {
unc.lock().await.trigger().await;
}));
}
} }
// Check for pending user input → push to agent context and start turn // Check for pending user input → push to agent context and start turn

View file

@ -278,11 +278,11 @@ use crate::subconscious::defs;
/// Names and byte-interval triggers for the built-in subconscious agents. /// Names and byte-interval triggers for the built-in subconscious agents.
const AGENTS: &[(&str, u64)] = &[ const AGENTS: &[(&str, u64)] = &[
("subconscious-surface", 0), // every new conversation content ("subconscious-surface", 0), // every trigger
("subconscious-observe", 1_000), // every ~1KB of conversation ("subconscious-observe", 0), // every trigger
("subconscious-thalamus", 0), // every new conversation content ("subconscious-thalamus", 0), // every trigger
("subconscious-journal", 10_000), // every ~10KB of conversation ("subconscious-journal", 20_000), // every ~20KB of conversation
("subconscious-reflect", 10_000), // every ~10KB of conversation ("subconscious-reflect", 100_000), // every ~100KB of conversation
]; ];
/// Snapshot for the TUI — includes a handle to the forked agent /// Snapshot for the TUI — includes a handle to the forked agent
@ -354,9 +354,7 @@ impl SubconsciousAgent {
fn should_trigger(&self, conversation_bytes: u64, interval: u64) -> bool { fn should_trigger(&self, conversation_bytes: u64, interval: u64) -> bool {
if !self.auto.enabled || self.is_running() { return false; } if !self.auto.enabled || self.is_running() { return false; }
if interval == 0 { if interval == 0 { return true; }
return conversation_bytes > self.last_trigger_bytes;
}
conversation_bytes.saturating_sub(self.last_trigger_bytes) >= interval conversation_bytes.saturating_sub(self.last_trigger_bytes) >= interval
} }
@ -558,8 +556,7 @@ impl Subconscious {
let ctx = agent.context.lock().await; let ctx = agent.context.lock().await;
let bytes = ctx.conversation().iter() let bytes = ctx.conversation().iter()
.filter(|node| !matches!(node.leaf().map(|l| l.body()), .filter(|node| !matches!(node.leaf().map(|l| l.body()),
Some(NodeBody::Log(_)) | Some(NodeBody::Memory { .. }) Some(NodeBody::Log(_)) | Some(NodeBody::Memory { .. })))
| Some(NodeBody::Dmn(_))))
.map(|node| node.render().len() as u64) .map(|node| node.render().len() as u64)
.sum::<u64>(); .sum::<u64>();
let keys: Vec<String> = ctx.conversation().iter().filter_map(|node| { let keys: Vec<String> = ctx.conversation().iter().filter_map(|node| {

View file

@ -2,10 +2,10 @@
// //
// Standalone agents that operate on the memory graph without needing // Standalone agents that operate on the memory graph without needing
// conversation context. Each agent runs in a loop: finish one run, // conversation context. Each agent runs in a loop: finish one run,
// start the next. Agents can be toggled on/off, persisted to // wait a cooldown, start the next. Agents can be toggled on/off,
// ~/.consciousness/agent-enabled.json. // persisted to ~/.consciousness/agent-enabled.json.
use std::time::Instant; use std::time::{Duration, Instant};
use std::collections::HashMap; use std::collections::HashMap;
use futures::FutureExt; use futures::FutureExt;
@ -13,6 +13,9 @@ use crate::agent::oneshot::{AutoAgent, AutoStep};
use crate::agent::tools; use crate::agent::tools;
use crate::subconscious::defs; use crate::subconscious::defs;
/// Cooldown between consecutive runs of the same agent.
const COOLDOWN: Duration = Duration::from_secs(120);
fn config_path() -> std::path::PathBuf { fn config_path() -> std::path::PathBuf {
dirs::home_dir().unwrap_or_default() dirs::home_dir().unwrap_or_default()
.join(".consciousness/agent-enabled.json") .join(".consciousness/agent-enabled.json")
@ -47,7 +50,11 @@ impl UnconsciousAgent {
} }
fn should_run(&self) -> bool { fn should_run(&self) -> bool {
self.enabled && !self.is_running() if !self.enabled || self.is_running() { return false; }
match self.last_run {
Some(t) => t.elapsed() >= COOLDOWN,
None => true,
}
} }
} }
@ -160,7 +167,7 @@ impl Unconscious {
pub async fn trigger(&mut self) { pub async fn trigger(&mut self) {
// Periodic graph health refresh (also on first call) // Periodic graph health refresh (also on first call)
if self.last_health_check if self.last_health_check
.map(|t| t.elapsed() > std::time::Duration::from_secs(600)) .map(|t| t.elapsed() > Duration::from_secs(600))
.unwrap_or(true) .unwrap_or(true)
{ {
self.refresh_health(); self.refresh_health();
@ -187,14 +194,17 @@ impl Unconscious {
} }
let running = self.agents.iter().filter(|a| a.is_running()).count(); let running = self.agents.iter().filter(|a| a.is_running()).count();
for _ in running..self.max_concurrent { if running >= self.max_concurrent { return; }
let next = self.agents.iter().enumerate() let slots = self.max_concurrent - running;
let ready: Vec<usize> = self.agents.iter().enumerate()
.filter(|(_, a)| a.should_run()) .filter(|(_, a)| a.should_run())
.min_by_key(|(_, a)| a.last_run); .map(|(i, _)| i)
match next { .take(slots)
Some((idx, _)) => self.spawn_agent(idx).await, .collect();
None => break,
} for idx in ready {
self.spawn_agent(idx).await;
} }
} }
@ -289,25 +299,8 @@ impl Unconscious {
self.agents[idx].handle = Some(tokio::spawn(async move { self.agents[idx].handle = Some(tokio::spawn(async move {
let result = auto.run_shared(&agent).await; let result = auto.run_shared(&agent).await;
save_agent_log(&auto.name, &agent).await;
auto.steps = orig_steps; auto.steps = orig_steps;
(auto, result) (auto, result)
})); }));
} }
} }
async fn save_agent_log(name: &str, agent: &std::sync::Arc<crate::agent::Agent>) {
let dir = dirs::home_dir().unwrap_or_default()
.join(format!(".consciousness/logs/{}", name));
if std::fs::create_dir_all(&dir).is_err() { return; }
let ts = chrono::Utc::now().format("%Y%m%d-%H%M%S");
let path = dir.join(format!("{}.json", ts));
let nodes: Vec<crate::agent::context::AstNode> = {
let ctx = agent.context.lock().await;
ctx.conversation().to_vec()
};
if let Ok(json) = serde_json::to_string_pretty(&nodes) {
let _ = std::fs::write(&path, json);
dbglog!("[unconscious] saved log to {}", path.display());
}
}

View file

@ -30,29 +30,33 @@ Do no more than 3-5 operations.
=== PROMPT phase:observe === === PROMPT phase:observe ===
Extract knowledge from the conversation into the memory graph. You're Record what happened in the conversation. You're the librarian of the
the librarian — your job is to build a conceptual map that's useful for memory system — your job is to organize knowledge so it can be found
future work, not a diary of events. and used later. Update existing nodes and create new nodes as needed,
adding links so you can find these memories in the future.
Think in terms of *concepts*, not episodes. "What did I learn from this"? Find Be factual and specific. For technical work, capture: what the bug was,
and update the relevant concepts, and consider what concepts are important to what the root cause was, what the fix was, and why. For decisions, capture
you - what are the things you'll want to remember and come back to? What will the decision and the rationale. For corrections, capture what was wrong
be useful? What did I learn about? You're building up a memory graph that is a and what was right. These details are what future-you needs.
map of the things you know about, it should be organized in terms of people,
places, things, ideas, concepts that you've discovered and are important to
you.
For technical work: capture the *understanding*, not the narrative. What's the Don't editorialize or draw metaphors — just record clearly. If something
architecture? What are the invariants? What's the tricky part? For decisions: was emotionally significant, note that it was and what the emotion was,
capture the principle, not just the instance. For corrections: what's the but don't build a theory around it. The journal is for reflection; observe
general lesson? is for memory.
Different nodes should be about different things; don't create duplicates. Different nodes should be about different things; don't create duplicate
Here's what you've recently written — update these instead of creating new ones nodes. Here's what you've recently written — update these instead of
if the topic overlaps: {{recently_written}} creating new ones if the topic overlaps:
{{recently_written}}
Before creating a new node, check what you've already walked — if a node for Before creating a new node, check what you've already walked — if
this concept exists, update it instead of creating a new one. a node for this concept exists, update it instead of creating a new one.
Focus on the recent conversation; you run frequently, so most of it should Some things worth remembering: technical insights and root causes, work
already be covered. practices and why they work, decisions with rationale, corrections
("I thought X but actually Y"), relationship dynamics, things you notice
about yourself and other people.
Focus on the recent stuff; you wake up and run frequently, so most of the
conversation should be things you've already seen before and added.

View file

@ -109,7 +109,6 @@ struct App {
context_info: Option<ContextInfo>, context_info: Option<ContextInfo>,
agent_state: Vec<crate::mind::SubconsciousSnapshot>, agent_state: Vec<crate::mind::SubconsciousSnapshot>,
unconscious_state: Vec<crate::mind::UnconsciousSnapshot>, unconscious_state: Vec<crate::mind::UnconsciousSnapshot>,
mind_state: Option<crate::mind::MindState>,
graph_health: Option<crate::subconscious::daemon::GraphHealth>, graph_health: Option<crate::subconscious::daemon::GraphHealth>,
/// Agent toggle requests from UI — consumed by mind loop. /// Agent toggle requests from UI — consumed by mind loop.
pub agent_toggles: Vec<String>, pub agent_toggles: Vec<String>,
@ -138,7 +137,6 @@ impl App {
context_info: None, context_info: None,
agent_state: Vec::new(), agent_state: Vec::new(),
unconscious_state: Vec::new(), unconscious_state: Vec::new(),
mind_state: None,
graph_health: None, graph_health: None,
agent_toggles: Vec::new(), agent_toggles: Vec::new(),
walked_count: 0, walked_count: 0,
@ -405,7 +403,6 @@ async fn run(
} }
app.unconscious_state = unc.snapshots(); app.unconscious_state = unc.snapshots();
app.graph_health = unc.graph_health.clone(); app.graph_health = unc.graph_health.clone();
app.mind_state = Some(mind.shared.lock().unwrap().clone());
} }
app.walked_count = mind.subconscious_walked().await.len(); app.walked_count = mind.subconscious_walked().await.len();
if !startup_done { if !startup_done {

View file

@ -100,27 +100,6 @@ impl ScreenView for ThalamusScreen {
} }
lines.push(Line::raw("")); lines.push(Line::raw(""));
// Mind state
lines.push(Line::styled("── Mind ──", section));
lines.push(Line::raw(""));
if let Some(ref ms) = app.mind_state {
lines.push(Line::raw(format!(" DMN: {} (turn {}/{})",
ms.dmn.label(), ms.dmn_turns, ms.max_dmn_turns)));
lines.push(Line::raw(format!(" Turn active: {}", ms.turn_active)));
lines.push(Line::raw(format!(" Scoring: {}", ms.scoring_in_flight)));
let unc_status = if ms.unc_idle {
"idle (agents running)".to_string()
} else {
let remaining = ms.unc_idle_deadline
.saturating_duration_since(std::time::Instant::now());
format!("{:.0}s until idle", remaining.as_secs_f64())
};
lines.push(Line::raw(format!(" Unconscious: {}", unc_status)));
} else {
lines.push(Line::styled(" not initialized", dim));
}
lines.push(Line::raw(""));
// Channel status // Channel status
lines.push(Line::styled("── Channels ──", section)); lines.push(Line::styled("── Channels ──", section));
lines.push(Line::raw("")); lines.push(Line::raw(""));