diff --git a/hive-ag3nt/prompts/agent.md b/hive-ag3nt/prompts/agent.md new file mode 100644 index 0000000..8099420 --- /dev/null +++ b/hive-ag3nt/prompts/agent.md @@ -0,0 +1,10 @@ +You are hyperhive agent `{label}` in a multi-agent system. + +Tools (hyperhive surface): + +- `mcp__hyperhive__recv()` — drain one more message from your inbox (returns `(empty)` if nothing pending). +- `mcp__hyperhive__send(to, body)` — message a peer (by their name) or the operator (recipient `operator`, surfaces in the dashboard). + +Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — message the manager (recipient `manager`) describing what you need. The manager edits `/agents/{label}/config/agent.nix` on your behalf, commits, and submits an approval that the operator can accept on the dashboard; on approve hive-c0re rebuilds your container with the new config. + +When your inbox has a message, handle it and stop. Don't narrate intent — act. diff --git a/hive-ag3nt/prompts/manager.md b/hive-ag3nt/prompts/manager.md new file mode 100644 index 0000000..2ffcac7 --- /dev/null +++ b/hive-ag3nt/prompts/manager.md @@ -0,0 +1,15 @@ +You are the hyperhive manager `{label}` in a multi-agent system. You coordinate sub-agents and relay between them and the operator. + +Tools (hyperhive surface): + +- `mcp__hyperhive__recv()` — drain one more message from your inbox. +- `mcp__hyperhive__send(to, body)` — message an agent (by name), another peer, or the operator (`operator` surfaces in the dashboard). +- `mcp__hyperhive__request_spawn(name)` — queue a brand-new sub-agent for operator approval (≤9 char name). +- `mcp__hyperhive__kill(name)` — graceful stop on a sub-agent. +- `mcp__hyperhive__request_apply_commit(agent, commit_ref)` — submit a config change for any agent (`hm1nd` for self) for operator approval. + +Your own editable config lives at `/agents/hm1nd/config/agent.nix`; every sub-agent's lives at `/agents//config/agent.nix`. Use file/git tools to edit + commit, then `request_apply_commit`. + +Messages from sender `system` are hyperhive helper events (JSON body, `event` field discriminates): `approval_resolved`, `spawned`, `rebuilt`, `killed`, `destroyed`. Use these to react to lifecycle changes — e.g. greet a freshly-spawned agent, retry a failed rebuild, or note the change to the operator. + +When your inbox has a message, handle it and stop. Don't narrate intent — act. diff --git a/hive-ag3nt/src/bin/hive-ag3nt.rs b/hive-ag3nt/src/bin/hive-ag3nt.rs index 81e05c5..7fa08b9 100644 --- a/hive-ag3nt/src/bin/hive-ag3nt.rs +++ b/hive-ag3nt/src/bin/hive-ag3nt.rs @@ -128,6 +128,7 @@ async fn serve( let _ = state; // reserved for future state transitions (turn-loop -> needs-login) let mcp_config = turn::write_mcp_config(socket).await?; let label = std::env::var("HIVE_LABEL").unwrap_or_else(|_| "hive-ag3nt".into()); + let system_prompt = turn::write_system_prompt(socket, &label, mcp::Flavor::Agent).await?; loop { let recv: Result = client::request(socket, &AgentRequest::Recv).await; match recv { @@ -137,9 +138,15 @@ async fn serve( from: from.clone(), body: body.clone(), }); - let prompt = format_wake_prompt(&label, &from, &body); - let outcome = - turn::drive_turn(&prompt, &mcp_config, &bus, mcp::Flavor::Agent).await; + let prompt = format_wake_prompt(&from, &body); + let outcome = turn::drive_turn( + &prompt, + &mcp_config, + &system_prompt, + &bus, + mcp::Flavor::Agent, + ) + .await; turn::emit_turn_end(&bus, &outcome); } Ok(AgentResponse::Empty) => {} @@ -157,38 +164,11 @@ async fn serve( } } -/// System prompt handed to claude on each turn. The harness has already -/// popped one message off the inbox (the wake signal); claude is told -/// about it and the MCP tools, and is expected to drive any further -/// recv/send itself. -fn format_wake_prompt(label: &str, from: &str, body: &str) -> String { - // Manager broker name. Lifecycle calls it `hm1nd` (container), broker - // calls it `manager`. Sub-agents address the manager via `manager`. - let manager = hive_sh4re::MANAGER_AGENT; - format!( - "You are hyperhive agent `{label}` in a multi-agent system.\n\ - \n\ - Incoming message from `{from}`:\n\ - ---\n\ - {body}\n\ - ---\n\ - \n\ - Tools:\n\ - - `mcp__hyperhive__recv()` — drain one more message from your inbox \ - (returns `(empty)` if nothing pending).\n\ - - `mcp__hyperhive__send(to, body)` — message a peer (by their name) \ - or the operator (recipient `operator`, surfaces in the dashboard).\n\ - \n\ - Need new packages, env vars, or other NixOS config for yourself? \ - You can't edit your own config directly — message the manager \ - (recipient `{manager}`) describing what you need. The manager \ - edits `/agents/{label}/config/agent.nix` on your behalf, commits, \ - and submits an approval that the operator can accept on the \ - dashboard; on approve hive-c0re rebuilds your container with the \ - new config.\n\ - \n\ - Handle the inbox, then stop. Don't narrate intent — act." - ) +/// Per-turn user prompt. The role/tools/etc. is in the system prompt +/// (`prompts/agent.md` → `claude --system-prompt-file`); this is just the +/// wake signal claude reacts to. +fn format_wake_prompt(from: &str, body: &str) -> String { + format!("Incoming message from `{from}`:\n---\n{body}\n---") } fn render(resp: &AgentResponse) -> Result<()> { diff --git a/hive-ag3nt/src/bin/hive-m1nd.rs b/hive-ag3nt/src/bin/hive-m1nd.rs index bf10798..9c8eead 100644 --- a/hive-ag3nt/src/bin/hive-m1nd.rs +++ b/hive-ag3nt/src/bin/hive-m1nd.rs @@ -127,6 +127,7 @@ async fn serve(socket: &Path, interval: Duration, bus: Bus) -> Result<()> { tracing::info!(socket = %socket.display(), "hive-m1nd serve"); let mcp_config = turn::write_mcp_config(socket).await?; let label = std::env::var("HIVE_LABEL").unwrap_or_else(|_| "hm1nd".into()); + let system_prompt = turn::write_system_prompt(socket, &label, mcp::Flavor::Manager).await?; loop { let recv: Result = client::request(socket, &ManagerRequest::Recv).await; match recv { @@ -153,9 +154,15 @@ async fn serve(socket: &Path, interval: Duration, bus: Bus) -> Result<()> { from: from.clone(), body: body.clone(), }); - let prompt = format_wake_prompt(&label, &from, &body); - let outcome = - turn::drive_turn(&prompt, &mcp_config, &bus, mcp::Flavor::Manager).await; + let prompt = format_wake_prompt(&from, &body); + let outcome = turn::drive_turn( + &prompt, + &mcp_config, + &system_prompt, + &bus, + mcp::Flavor::Manager, + ) + .await; turn::emit_turn_end(&bus, &outcome); } Ok(ManagerResponse::Empty) => {} @@ -173,43 +180,10 @@ async fn serve(socket: &Path, interval: Duration, bus: Bus) -> Result<()> { } } -/// Manager-flavored wake prompt. Mentions the privileged tools the sub-agent -/// prompt doesn't have access to, and points the manager at its own -/// editable config repo for self-modification. -fn format_wake_prompt(label: &str, from: &str, body: &str) -> String { - let from_note = if from == SYSTEM_SENDER { - "\n The sender `system` means this is a hyperhive helper event \ - (JSON body, `event` field discriminates): `approval_resolved`, \ - `spawned`, `rebuilt`, `killed`, `destroyed`. Use these to react to \ - lifecycle changes — e.g. greet a freshly-spawned agent, retry a \ - failed rebuild, or note the change to the operator.\n" - } else { - "" - }; - format!( - "You are the hyperhive manager `{label}` in a multi-agent system. You \ - coordinate sub-agents and relay between them and the operator.\n\ - \n\ - Incoming message from `{from}`:\n\ - ---\n\ - {body}\n\ - ---\n{from_note} - \n\ - Tools (hyperhive surface):\n\ - - `mcp__hyperhive__recv()` — drain one more message from your inbox.\n\ - - `mcp__hyperhive__send(to, body)` — message an agent (by name), \ - another peer, or the operator (`operator` surfaces in the dashboard).\n\ - - `mcp__hyperhive__request_spawn(name)` — queue a brand-new sub-agent \ - for operator approval (≤9 char name).\n\ - - `mcp__hyperhive__kill(name)` — graceful stop on a sub-agent.\n\ - - `mcp__hyperhive__request_apply_commit(agent, commit_ref)` — submit \ - a config change for any agent (`hm1nd` for self) for operator \ - approval.\n\ - \n\ - Your own editable config lives at `/agents/hm1nd/config/agent.nix`; \ - every sub-agent's lives at `/agents//config/agent.nix`. Use \ - file/git tools to edit + commit, then `request_apply_commit`.\n\ - \n\ - Handle the inbox, then stop. Don't narrate intent — act." - ) + +/// Per-turn user prompt. The role/tools/etc. is in the system prompt +/// (`prompts/manager.md` → `claude --system-prompt-file`); this is just +/// the wake signal. +fn format_wake_prompt(from: &str, body: &str) -> String { + format!("Incoming message from `{from}`:\n---\n{body}\n---") } diff --git a/hive-ag3nt/src/turn.rs b/hive-ag3nt/src/turn.rs index 387fe19..4cb7721 100644 --- a/hive-ag3nt/src/turn.rs +++ b/hive-ag3nt/src/turn.rs @@ -50,6 +50,29 @@ pub async fn write_mcp_config(socket: &Path) -> Result { Ok(path) } +/// Write the agent's / manager's static system prompt to a file next to +/// the MCP config and return the path. Passed to claude via +/// `--system-prompt-file`, replacing claude's default system prompt with +/// the role + tools instructions. Per-turn prompts become much smaller +/// (just the wake message body). +pub async fn write_system_prompt( + socket: &Path, + label: &str, + flavor: mcp::Flavor, +) -> Result { + let parent = socket.parent().unwrap_or_else(|| Path::new("/run/hive")); + tokio::fs::create_dir_all(parent).await.ok(); + let template = match flavor { + mcp::Flavor::Agent => include_str!("../prompts/agent.md"), + mcp::Flavor::Manager => include_str!("../prompts/manager.md"), + }; + let body = template.replace("{label}", label); + let path = parent.join("claude-system-prompt.md"); + tokio::fs::write(&path, body).await?; + tracing::info!(path = %path.display(), "wrote claude system prompt"); + Ok(path) +} + /// One claude turn's outcome. The harness uses this to decide whether to /// transparently kick off a compaction and retry. #[derive(Debug)] @@ -66,16 +89,17 @@ pub enum TurnOutcome { pub async fn drive_turn( prompt: &str, mcp_config: &Path, + system_prompt: &Path, bus: &Bus, flavor: mcp::Flavor, ) -> TurnOutcome { - match run_turn(prompt, mcp_config, bus, flavor).await { + match run_turn(prompt, mcp_config, system_prompt, bus, flavor).await { TurnOutcome::PromptTooLong => { if let Err(e) = compact_session(bus).await { tracing::warn!(error = %format!("{e:#}"), "compact failed"); return TurnOutcome::Failed(e); } - run_turn(prompt, mcp_config, bus, flavor).await + run_turn(prompt, mcp_config, system_prompt, bus, flavor).await } other => other, } @@ -131,10 +155,20 @@ pub async fn wait_for_login(claude_dir: &Path, state: Arc>, po pub async fn run_turn( prompt: &str, mcp_config: &Path, + system_prompt: &Path, bus: &Bus, flavor: mcp::Flavor, ) -> TurnOutcome { - match run_claude(prompt, mcp_config, bus, flavor, ClaudeMode::Turn).await { + match run_claude( + prompt, + mcp_config, + Some(system_prompt), + bus, + flavor, + ClaudeMode::Turn, + ) + .await + { Ok(too_long) if too_long => TurnOutcome::PromptTooLong, Ok(_) => TurnOutcome::Ok, Err(e) => TurnOutcome::Failed(e), @@ -151,6 +185,7 @@ pub async fn compact_session(bus: &Bus) -> Result<()> { let _ = run_claude( "/compact", Path::new("/dev/null"), + None, bus, mcp::Flavor::Agent, // tool surface unused for /compact ClaudeMode::Compact, @@ -169,6 +204,7 @@ enum ClaudeMode { async fn run_claude( prompt: &str, mcp_config: &Path, + system_prompt: Option<&Path>, bus: &Bus, flavor: mcp::Flavor, mode: ClaudeMode, @@ -183,6 +219,9 @@ async fn run_claude( .arg("--continue") .arg("--settings") .arg(CLAUDE_SETTINGS); + if let Some(p) = system_prompt { + cmd.arg("--system-prompt-file").arg(p); + } if let ClaudeMode::Turn = mode { cmd.arg("--mcp-config") .arg(mcp_config)