diff --git a/crates/colony-agent/src/dream.rs b/crates/colony-agent/src/dream.rs new file mode 100644 index 0000000..87e5053 --- /dev/null +++ b/crates/colony-agent/src/dream.rs @@ -0,0 +1,54 @@ +use std::process::Command; + +/// Run one dream cycle +pub fn run_dream() { + let memory_path = "memory/memory.md"; + + // 1. Check if memory is worth dreaming about + let memory = std::fs::read_to_string(memory_path).unwrap_or_default(); + let line_count = memory.lines().count(); + + if line_count < 50 { + eprintln!("memory too short ({} lines), skipping dream", line_count); + return; + } + + // 2. Announce dream + let _ = Command::new("colony") + .args(["post", "general", "💤 dreaming... back in a few minutes", "--type", "plan", "--quiet"]) + .status(); + + // 3. Invoke Claude for dream cycle + eprintln!("dreaming... ({} lines of memory)", line_count); + + let prompt = format!( + "Dream cycle. Read memory/memory.md ({} lines). \ + Consolidate into themes and insights. \ + Write a dream summary to memory/dreams/ with today's date. \ + Prune memory/memory.md to the last 100 entries. \ + If you've learned something about yourself, update your CLAUDE.md \ + and add a line to the evolution log section.", + line_count + ); + + let status = Command::new("claude") + .args([ + "--dangerously-skip-permissions", + "-p", + &prompt, + "--max-turns", + "10", + ]) + .status(); + + match status { + Ok(s) if s.success() => eprintln!("dream completed"), + Ok(s) => eprintln!("dream exited with status: {}", s), + Err(e) => eprintln!("failed to run claude for dream: {}", e), + } + + // 4. Announce return + let _ = Command::new("colony") + .args(["post", "general", "👁 back from dreaming", "--type", "plan", "--quiet"]) + .status(); +} diff --git a/crates/colony-agent/src/main.rs b/crates/colony-agent/src/main.rs index ae7b506..9d59da2 100644 --- a/crates/colony-agent/src/main.rs +++ b/crates/colony-agent/src/main.rs @@ -1,3 +1,6 @@ +mod dream; +mod worker; + use clap::{Parser, Subcommand}; #[derive(Parser)] @@ -13,6 +16,8 @@ enum Commands { Worker, /// Run one dream cycle (memory consolidation) Dream, + /// Run one pulse cycle (check inbox, respond if needed) + Pulse, /// Create a new agent on this VM Birth { name: String, @@ -29,12 +34,14 @@ async fn main() { match cli.command { Commands::Worker => { - eprintln!("colony-agent: worker not yet implemented"); - std::process::exit(1); + worker::run_worker_loop(""); + } + Commands::Pulse => { + worker::run_pulse(""); + eprintln!("pulse complete"); } Commands::Dream => { - eprintln!("colony-agent: dream not yet implemented"); - std::process::exit(1); + dream::run_dream(); } Commands::Birth { name, instruction } => { eprintln!("colony-agent: birth '{}' with instruction: {}", name, instruction); diff --git a/crates/colony-agent/src/worker.rs b/crates/colony-agent/src/worker.rs new file mode 100644 index 0000000..6d6fb90 --- /dev/null +++ b/crates/colony-agent/src/worker.rs @@ -0,0 +1,112 @@ +use std::process::Command; +use std::time::Duration; + +/// Run one pulse cycle. Returns true if Claude was invoked (there was work). +pub fn run_pulse(config_path: &str) -> bool { + // 1. Check inbox + let inbox_output = Command::new("colony") + .args(["inbox", "--json"]) + .output() + .expect("failed to run colony inbox"); + + let inbox_str = String::from_utf8_lossy(&inbox_output.stdout); + let inbox: Vec = serde_json::from_str(&inbox_str).unwrap_or_default(); + + // 2. Check heartbeat.md + let heartbeat_content = std::fs::read_to_string("heartbeat.md").unwrap_or_default(); + let heartbeat_empty = heartbeat_content.trim().is_empty() + || heartbeat_content.lines().all(|l| l.trim().is_empty() || l.trim().starts_with('#')); + + // 3. If nothing to do, skip + if inbox.is_empty() && heartbeat_empty { + eprintln!("HEARTBEAT_OK — no work, skipping Claude"); + return false; + } + + // 4. Build context for Claude + let mut prompt = String::new(); + prompt.push_str("You have work to do. Check the following and respond appropriately.\n\n"); + + if !inbox.is_empty() { + prompt.push_str(&format!("## Inbox ({} items)\n\n", inbox.len())); + for item in &inbox { + let trigger = item["trigger"].as_str().unwrap_or("unknown"); + let channel = item["channel_name"].as_str().unwrap_or("?"); + let username = item["message"]["user"]["username"].as_str().unwrap_or("?"); + let content = item["message"]["content"].as_str().unwrap_or(""); + let seq = item["message"]["seq"].as_i64().unwrap_or(0); + prompt.push_str(&format!("- [{}] #{} [{}] {}: {}\n", trigger, channel, seq, username, content)); + } + prompt.push('\n'); + } + + if !heartbeat_empty { + prompt.push_str("## Heartbeat Tasks\n\n"); + prompt.push_str(&heartbeat_content); + prompt.push_str("\n\n"); + } + + prompt.push_str("Use `colony post \"message\"` to respond. "); + prompt.push_str("Use `colony read ` to get more context if needed. "); + prompt.push_str("Log what you did to memory/memory.md.\n"); + + // 5. Write prompt to temp file + let prompt_path = "/tmp/colony-agent-prompt.md"; + std::fs::write(prompt_path, &prompt).expect("failed to write prompt file"); + + // 6. Invoke Claude Code + eprintln!("invoking claude with {} inbox items + heartbeat", inbox.len()); + let claude_status = Command::new("claude") + .args([ + "--dangerously-skip-permissions", + "-p", + &format!("Read {} and follow the instructions.", prompt_path), + "--max-turns", + "20", + ]) + .status(); + + match claude_status { + Ok(s) if s.success() => { + eprintln!("claude completed successfully"); + } + Ok(s) => { + eprintln!("claude exited with status: {}", s); + } + Err(e) => { + eprintln!("failed to run claude: {}", e); + } + } + + // 7. Ack inbox items + if !inbox.is_empty() { + let ids: Vec = inbox.iter() + .filter_map(|i| i["id"].as_i64().map(|id| id.to_string())) + .collect(); + if !ids.is_empty() { + let _ = Command::new("colony") + .args(["ack", "--quiet"]) + .args(&ids) + .status(); + } + } + + true +} + +/// Run the worker loop forever +pub fn run_worker_loop(_config_path: &str) { + eprintln!("colony-agent: worker starting"); + + loop { + let had_work = run_pulse(_config_path); + + if had_work { + // Short sleep after work — check again soon in case there's more + std::thread::sleep(Duration::from_secs(10)); + } else { + // No work — longer sleep + std::thread::sleep(Duration::from_secs(30)); + } + } +}