Spacebar toggle for all agents, persist to config, scan agent directory
- Scan agents directory for all .agent files instead of hardcoded list - Persist enabled state to ~/.consciousness/agent-enabled.json - Spacebar on F3 agent list toggles selected agent on/off - Both subconscious and unconscious agents support toggle - Disabled agents shown dimmed with "off" indicator - New agents default to disabled (safe default) Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
7aba17e5f0
commit
c73f037265
6 changed files with 98 additions and 28 deletions
|
|
@ -258,7 +258,7 @@ pub struct Mind {
|
|||
pub agent: Arc<Agent>,
|
||||
pub shared: Arc<SharedMindState>,
|
||||
pub config: SessionConfig,
|
||||
subconscious: Arc<tokio::sync::Mutex<Subconscious>>,
|
||||
pub subconscious: Arc<tokio::sync::Mutex<Subconscious>>,
|
||||
pub unconscious: Arc<tokio::sync::Mutex<Unconscious>>,
|
||||
turn_tx: mpsc::Sender<(Result<TurnResult>, StreamTarget)>,
|
||||
turn_watch: tokio::sync::watch::Sender<bool>,
|
||||
|
|
|
|||
|
|
@ -439,6 +439,13 @@ impl Subconscious {
|
|||
}
|
||||
}
|
||||
|
||||
/// Toggle an agent on/off by name. Returns new enabled state.
|
||||
pub fn toggle(&mut self, name: &str) -> Option<bool> {
|
||||
let agent = self.agents.iter_mut().find(|a| a.name == name)?;
|
||||
agent.auto.enabled = !agent.auto.enabled;
|
||||
Some(agent.auto.enabled)
|
||||
}
|
||||
|
||||
pub fn walked(&self) -> Vec<String> {
|
||||
self.state.get("walked")
|
||||
.map(|s| s.lines().map(|l| l.trim().to_string()).filter(|l| !l.is_empty()).collect())
|
||||
|
|
|
|||
|
|
@ -2,26 +2,36 @@
|
|||
//
|
||||
// Standalone agents that operate on the memory graph without needing
|
||||
// conversation context. Each agent runs in a loop: finish one run,
|
||||
// wait a cooldown, start the next. Agents can be toggled on/off.
|
||||
// wait a cooldown, start the next. Agents can be toggled on/off,
|
||||
// persisted to ~/.consciousness/agent-enabled.json.
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::agent::oneshot::{AutoAgent, AutoStep};
|
||||
use crate::agent::tools;
|
||||
use crate::subconscious::defs;
|
||||
|
||||
/// Agent types to run. Each must have a matching .agent file.
|
||||
const AGENTS: &[&str] = &[
|
||||
"organize",
|
||||
"linker",
|
||||
"distill",
|
||||
"split",
|
||||
"separator",
|
||||
];
|
||||
|
||||
/// 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")
|
||||
}
|
||||
|
||||
fn load_enabled_config() -> HashMap<String, bool> {
|
||||
std::fs::read_to_string(config_path()).ok()
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn save_enabled_config(map: &HashMap<String, bool>) {
|
||||
if let Ok(json) = serde_json::to_string_pretty(map) {
|
||||
let _ = std::fs::write(config_path(), json);
|
||||
}
|
||||
}
|
||||
|
||||
struct UnconsciousAgent {
|
||||
name: String,
|
||||
enabled: bool,
|
||||
|
|
@ -31,16 +41,6 @@ struct UnconsciousAgent {
|
|||
}
|
||||
|
||||
impl UnconsciousAgent {
|
||||
fn new(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
enabled: true,
|
||||
handle: None,
|
||||
last_run: None,
|
||||
runs: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_running(&self) -> bool {
|
||||
self.handle.as_ref().is_some_and(|h| !h.is_finished())
|
||||
}
|
||||
|
|
@ -73,10 +73,25 @@ pub struct Unconscious {
|
|||
|
||||
impl Unconscious {
|
||||
pub fn new() -> Self {
|
||||
let agents = AGENTS.iter()
|
||||
.filter(|name| defs::get_def(name).is_some())
|
||||
.map(|name| UnconsciousAgent::new(name))
|
||||
.collect();
|
||||
let enabled_map = load_enabled_config();
|
||||
|
||||
// Scan all .agent files, exclude subconscious-* and surface-observe
|
||||
let mut agents: Vec<UnconsciousAgent> = Vec::new();
|
||||
for def in defs::load_defs() {
|
||||
if def.agent.starts_with("subconscious-") { continue; }
|
||||
if def.agent == "surface-observe" { continue; }
|
||||
let enabled = enabled_map.get(&def.agent).copied()
|
||||
.unwrap_or(false); // new agents default to off
|
||||
agents.push(UnconsciousAgent {
|
||||
name: def.agent.clone(),
|
||||
enabled,
|
||||
handle: None,
|
||||
last_run: None,
|
||||
runs: 0,
|
||||
});
|
||||
}
|
||||
agents.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
let mut s = Self {
|
||||
agents, max_concurrent: 2,
|
||||
graph_health: None,
|
||||
|
|
@ -90,7 +105,16 @@ impl Unconscious {
|
|||
pub fn toggle(&mut self, name: &str) -> Option<bool> {
|
||||
let agent = self.agents.iter_mut().find(|a| a.name == name)?;
|
||||
agent.enabled = !agent.enabled;
|
||||
Some(agent.enabled)
|
||||
let new_state = agent.enabled;
|
||||
self.save_enabled();
|
||||
Some(new_state)
|
||||
}
|
||||
|
||||
fn save_enabled(&self) {
|
||||
let map: HashMap<String, bool> = self.agents.iter()
|
||||
.map(|a| (a.name.clone(), a.enabled))
|
||||
.collect();
|
||||
save_enabled_config(&map);
|
||||
}
|
||||
|
||||
pub fn snapshots(&self) -> Vec<UnconsciousSnapshot> {
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ pub fn agents_dir() -> PathBuf {
|
|||
}
|
||||
|
||||
/// Load all agent definitions.
|
||||
fn load_defs() -> Vec<AgentDef> {
|
||||
pub fn load_defs() -> Vec<AgentDef> {
|
||||
let dir = agents_dir();
|
||||
let Ok(entries) = std::fs::read_dir(&dir) else { return Vec::new() };
|
||||
|
||||
|
|
|
|||
|
|
@ -109,6 +109,8 @@ struct App {
|
|||
agent_state: Vec<crate::mind::SubconsciousSnapshot>,
|
||||
unconscious_state: Vec<crate::mind::UnconsciousSnapshot>,
|
||||
graph_health: Option<crate::subconscious::daemon::GraphHealth>,
|
||||
/// Agent toggle requests from UI — consumed by mind loop.
|
||||
pub agent_toggles: Vec<String>,
|
||||
walked_count: usize,
|
||||
channel_status: Vec<ChannelStatus>,
|
||||
idle_info: Option<IdleInfo>,
|
||||
|
|
@ -134,6 +136,7 @@ impl App {
|
|||
agent_state: Vec::new(),
|
||||
unconscious_state: Vec::new(),
|
||||
graph_health: None,
|
||||
agent_toggles: Vec::new(),
|
||||
walked_count: 0,
|
||||
channel_status: Vec::new(), idle_info: None,
|
||||
}
|
||||
|
|
@ -374,6 +377,18 @@ async fn run(
|
|||
idle_state.decay_ewma();
|
||||
app.update_idle(&idle_state);
|
||||
app.agent_state = mind.subconscious_snapshots().await;
|
||||
{
|
||||
let toggles: Vec<String> = app.agent_toggles.drain(..).collect();
|
||||
if !toggles.is_empty() {
|
||||
let mut sub = mind.subconscious.lock().await;
|
||||
let mut unc = mind.unconscious.lock().await;
|
||||
for name in &toggles {
|
||||
if sub.toggle(name).is_none() {
|
||||
unc.toggle(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
let unc = mind.unconscious.lock().await;
|
||||
app.unconscious_state = unc.snapshots();
|
||||
|
|
|
|||
|
|
@ -79,6 +79,11 @@ impl ScreenView for SubconsciousScreen {
|
|||
self.list_state.select_next();
|
||||
self.reset_pane_state();
|
||||
}
|
||||
KeyCode::Char(' ') => {
|
||||
if let Some(name) = self.selected_agent_name(app) {
|
||||
app.agent_toggles.push(name);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Pane::Outputs => self.output_tree.handle_nav(code, &output_sections, area.height),
|
||||
|
|
@ -124,6 +129,20 @@ impl ScreenView for SubconsciousScreen {
|
|||
}
|
||||
|
||||
impl SubconsciousScreen {
|
||||
/// Map the selected list index to an agent name.
|
||||
/// Accounts for the separator line between subconscious and unconscious.
|
||||
fn selected_agent_name(&self, app: &App) -> Option<String> {
|
||||
let idx = self.selected();
|
||||
let sub_count = app.agent_state.len();
|
||||
if idx < sub_count {
|
||||
// Subconscious agent
|
||||
return Some(app.agent_state[idx].name.clone());
|
||||
}
|
||||
// Skip separator line
|
||||
let unc_idx = idx.checked_sub(sub_count + 1)?;
|
||||
app.unconscious_state.get(unc_idx).map(|s| s.name.clone())
|
||||
}
|
||||
|
||||
fn reset_pane_state(&mut self) {
|
||||
self.output_tree = SectionTree::new();
|
||||
self.context_tree = SectionTree::new();
|
||||
|
|
@ -165,7 +184,12 @@ impl SubconsciousScreen {
|
|||
|
||||
fn draw_list(&mut self, frame: &mut Frame, area: Rect, app: &App) {
|
||||
let mut items: Vec<ListItem> = app.agent_state.iter().map(|snap| {
|
||||
if snap.running {
|
||||
if !snap.enabled {
|
||||
ListItem::from(Line::from(vec![
|
||||
Span::styled(&snap.name, Style::default().fg(Color::DarkGray)),
|
||||
Span::styled(" ○ off", Style::default().fg(Color::DarkGray)),
|
||||
]))
|
||||
} else if snap.running {
|
||||
ListItem::from(Line::from(vec![
|
||||
Span::styled(&snap.name, Style::default().fg(Color::Green)),
|
||||
Span::styled(" ● ", Style::default().fg(Color::Green)),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue