colony-agent: implement worker loop + dream cycle
Worker: - run_pulse(): check inbox → heartbeat → HEARTBEAT_OK or invoke Claude - run_worker_loop(): forever loop with 30s/10s sleep - Builds prompt from inbox items + heartbeat.md - Invokes claude --dangerously-skip-permissions with context - Acks inbox items after Claude completes - Added 'pulse' command for one-shot testing Dream: - Checks memory.md line count (skip if < 50) - Posts "dreaming..." to #general - Invokes Claude to consolidate, prune, evolve - Posts "back from dreaming" when done Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
54
crates/colony-agent/src/dream.rs
Normal file
54
crates/colony-agent/src/dream.rs
Normal file
@@ -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();
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
mod dream;
|
||||||
|
mod worker;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
@@ -13,6 +16,8 @@ enum Commands {
|
|||||||
Worker,
|
Worker,
|
||||||
/// Run one dream cycle (memory consolidation)
|
/// Run one dream cycle (memory consolidation)
|
||||||
Dream,
|
Dream,
|
||||||
|
/// Run one pulse cycle (check inbox, respond if needed)
|
||||||
|
Pulse,
|
||||||
/// Create a new agent on this VM
|
/// Create a new agent on this VM
|
||||||
Birth {
|
Birth {
|
||||||
name: String,
|
name: String,
|
||||||
@@ -29,12 +34,14 @@ async fn main() {
|
|||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Worker => {
|
Commands::Worker => {
|
||||||
eprintln!("colony-agent: worker not yet implemented");
|
worker::run_worker_loop("");
|
||||||
std::process::exit(1);
|
}
|
||||||
|
Commands::Pulse => {
|
||||||
|
worker::run_pulse("");
|
||||||
|
eprintln!("pulse complete");
|
||||||
}
|
}
|
||||||
Commands::Dream => {
|
Commands::Dream => {
|
||||||
eprintln!("colony-agent: dream not yet implemented");
|
dream::run_dream();
|
||||||
std::process::exit(1);
|
|
||||||
}
|
}
|
||||||
Commands::Birth { name, instruction } => {
|
Commands::Birth { name, instruction } => {
|
||||||
eprintln!("colony-agent: birth '{}' with instruction: {}", name, instruction);
|
eprintln!("colony-agent: birth '{}' with instruction: {}", name, instruction);
|
||||||
|
|||||||
112
crates/colony-agent/src/worker.rs
Normal file
112
crates/colony-agent/src/worker.rs
Normal file
@@ -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::Value> = 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 <channel> \"message\"` to respond. ");
|
||||||
|
prompt.push_str("Use `colony read <channel>` 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<String> = 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user