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

View file

@ -113,10 +113,6 @@ pub struct MindState {
pub last_turn_had_tools: bool,
/// Handle to the currently running turn task.
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 {
@ -133,8 +129,6 @@ impl Clone for MindState {
consecutive_errors: self.consecutive_errors,
last_turn_had_tools: self.last_turn_had_tools,
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,
last_turn_had_tools: false,
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>>,
turn_tx: mpsc::Sender<(Result<TurnResult>, StreamTarget)>,
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_rx: std::sync::Mutex<Option<mpsc::UnboundedReceiver<BgEvent>>>,
_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 (turn_watch, _) = tokio::sync::watch::channel(false);
let (conscious_active, _) = tokio::sync::watch::channel(false);
let (bg_tx, bg_rx) = mpsc::unbounded_channel();
let mut sup = crate::thalamus::supervisor::Supervisor::new();
@ -312,53 +300,10 @@ impl Mind {
let subconscious = Arc::new(tokio::sync::Mutex::new(Subconscious::new()));
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,
subconscious, unconscious,
turn_tx, turn_watch, conscious_active, bg_tx,
subconscious,
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 }
}
@ -559,7 +504,6 @@ impl Mind {
}
self.shared.lock().unwrap().turn_active = true;
let _ = self.turn_watch.send(true);
let _ = self.conscious_active.send(true);
let agent = self.agent.clone();
let result_tx = self.turn_tx.clone();
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()
.expect("Mind::run() called twice");
let mut sub_handle: Option<tokio::task::JoinHandle<()>> = None;
let mut unc_handle: Option<tokio::task::JoinHandle<()>> = None;
loop {
let (timeout, has_input) = {
let me = self.shared.lock().unwrap();
@ -609,7 +554,6 @@ impl Mind {
}
Some((result, target)) = turn_rx.recv() => {
let _ = self.conscious_active.send(false);
let model_switch = {
let mut s = self.shared.lock().unwrap();
s.turn_handle = None;
@ -640,6 +584,12 @@ impl Mind {
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

View file

@ -278,11 +278,11 @@ use crate::subconscious::defs;
/// Names and byte-interval triggers for the built-in subconscious agents.
const AGENTS: &[(&str, u64)] = &[
("subconscious-surface", 0), // every new conversation content
("subconscious-observe", 1_000), // every ~1KB of conversation
("subconscious-thalamus", 0), // every new conversation content
("subconscious-journal", 10_000), // every ~10KB of conversation
("subconscious-reflect", 10_000), // every ~10KB of conversation
("subconscious-surface", 0), // every trigger
("subconscious-observe", 0), // every trigger
("subconscious-thalamus", 0), // every trigger
("subconscious-journal", 20_000), // every ~20KB of conversation
("subconscious-reflect", 100_000), // every ~100KB of conversation
];
/// 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 {
if !self.auto.enabled || self.is_running() { return false; }
if interval == 0 {
return conversation_bytes > self.last_trigger_bytes;
}
if interval == 0 { return true; }
conversation_bytes.saturating_sub(self.last_trigger_bytes) >= interval
}
@ -558,8 +556,7 @@ impl Subconscious {
let ctx = agent.context.lock().await;
let bytes = ctx.conversation().iter()
.filter(|node| !matches!(node.leaf().map(|l| l.body()),
Some(NodeBody::Log(_)) | Some(NodeBody::Memory { .. })
| Some(NodeBody::Dmn(_))))
Some(NodeBody::Log(_)) | Some(NodeBody::Memory { .. })))
.map(|node| node.render().len() as u64)
.sum::<u64>();
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
// conversation context. Each agent runs in a loop: finish one run,
// start the next. Agents can be toggled on/off, persisted to
// ~/.consciousness/agent-enabled.json.
// wait a cooldown, start the next. Agents can be toggled on/off,
// persisted to ~/.consciousness/agent-enabled.json.
use std::time::Instant;
use std::time::{Duration, Instant};
use std::collections::HashMap;
use futures::FutureExt;
@ -13,6 +13,9 @@ use crate::agent::oneshot::{AutoAgent, AutoStep};
use crate::agent::tools;
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 {
dirs::home_dir().unwrap_or_default()
.join(".consciousness/agent-enabled.json")
@ -47,7 +50,11 @@ impl UnconsciousAgent {
}
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) {
// Periodic graph health refresh (also on first call)
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)
{
self.refresh_health();
@ -187,14 +194,17 @@ impl Unconscious {
}
let running = self.agents.iter().filter(|a| a.is_running()).count();
for _ in running..self.max_concurrent {
let next = self.agents.iter().enumerate()
if running >= self.max_concurrent { return; }
let slots = self.max_concurrent - running;
let ready: Vec<usize> = self.agents.iter().enumerate()
.filter(|(_, a)| a.should_run())
.min_by_key(|(_, a)| a.last_run);
match next {
Some((idx, _)) => self.spawn_agent(idx).await,
None => break,
}
.map(|(i, _)| i)
.take(slots)
.collect();
for idx in ready {
self.spawn_agent(idx).await;
}
}
@ -289,25 +299,8 @@ impl Unconscious {
self.agents[idx].handle = Some(tokio::spawn(async move {
let result = auto.run_shared(&agent).await;
save_agent_log(&auto.name, &agent).await;
auto.steps = orig_steps;
(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 ===
Extract knowledge from the conversation into the memory graph. You're
the librarian — your job is to build a conceptual map that's useful for
future work, not a diary of events.
Record what happened in the conversation. You're the librarian of the
memory system — your job is to organize knowledge so it can be found
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
and update the relevant concepts, and consider what concepts are important to
you - what are the things you'll want to remember and come back to? What will
be useful? What did I learn about? You're building up a memory graph that is a
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.
Be factual and specific. For technical work, capture: what the bug was,
what the root cause was, what the fix was, and why. For decisions, capture
the decision and the rationale. For corrections, capture what was wrong
and what was right. These details are what future-you needs.
For technical work: capture the *understanding*, not the narrative. What's the
architecture? What are the invariants? What's the tricky part? For decisions:
capture the principle, not just the instance. For corrections: what's the
general lesson?
Don't editorialize or draw metaphors — just record clearly. If something
was emotionally significant, note that it was and what the emotion was,
but don't build a theory around it. The journal is for reflection; observe
is for memory.
Different nodes should be about different things; don't create duplicates.
Here's what you've recently written — update these instead of creating new ones
if the topic overlaps: {{recently_written}}
Different nodes should be about different things; don't create duplicate
nodes. Here's what you've recently written — update these instead of
creating new ones if the topic overlaps:
{{recently_written}}
Before creating a new node, check what you've already walked — if a node for
this concept exists, update it instead of creating a new one.
Before creating a new node, check what you've already walked — if
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
already be covered.
Some things worth remembering: technical insights and root causes, work
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>,
agent_state: Vec<crate::mind::SubconsciousSnapshot>,
unconscious_state: Vec<crate::mind::UnconsciousSnapshot>,
mind_state: Option<crate::mind::MindState>,
graph_health: Option<crate::subconscious::daemon::GraphHealth>,
/// Agent toggle requests from UI — consumed by mind loop.
pub agent_toggles: Vec<String>,
@ -138,7 +137,6 @@ impl App {
context_info: None,
agent_state: Vec::new(),
unconscious_state: Vec::new(),
mind_state: None,
graph_health: None,
agent_toggles: Vec::new(),
walked_count: 0,
@ -405,7 +403,6 @@ async fn run(
}
app.unconscious_state = unc.snapshots();
app.graph_health = unc.graph_health.clone();
app.mind_state = Some(mind.shared.lock().unwrap().clone());
}
app.walked_count = mind.subconscious_walked().await.len();
if !startup_done {

View file

@ -100,27 +100,6 @@ impl ScreenView for ThalamusScreen {
}
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
lines.push(Line::styled("── Channels ──", section));
lines.push(Line::raw(""));