diff --git a/hive-ag3nt/Cargo.toml b/hive-ag3nt/Cargo.toml index c268836..f5bc59d 100644 --- a/hive-ag3nt/Cargo.toml +++ b/hive-ag3nt/Cargo.toml @@ -3,6 +3,16 @@ name = "hive-ag3nt" edition.workspace = true version.workspace = true +[dependencies] +anyhow.workspace = true +clap.workspace = true +hive-sh4re.workspace = true +serde.workspace = true +serde_json.workspace = true +tokio.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true + [[bin]] name = "hive-ag3nt" path = "src/bin/hive-ag3nt.rs" diff --git a/hive-ag3nt/src/bin/hive-ag3nt.rs b/hive-ag3nt/src/bin/hive-ag3nt.rs index 03c1e0c..021c400 100644 --- a/hive-ag3nt/src/bin/hive-ag3nt.rs +++ b/hive-ag3nt/src/bin/hive-ag3nt.rs @@ -1,3 +1,91 @@ -fn main() { - println!("hive-ag3nt placeholder"); +use std::path::PathBuf; +use std::time::Duration; + +use anyhow::{Result, bail}; +use clap::{Parser, Subcommand}; +use hive_ag3nt::{DEFAULT_SOCKET, client}; +use hive_sh4re::{AgentRequest, AgentResponse}; + +#[derive(Parser)] +#[command(name = "hive-ag3nt", about = "hyperhive sub-agent harness")] +struct Cli { + /// Path to the per-agent MCP socket (bind-mounted from the host). + #[arg(long, global = true, default_value = DEFAULT_SOCKET)] + socket: PathBuf, + + #[command(subcommand)] + cmd: Cmd, +} + +#[derive(Subcommand)] +enum Cmd { + /// Run the long-lived harness loop. Polls inbox; prints messages to stdout. + Serve { + /// Inbox poll interval in milliseconds. + #[arg(long, default_value_t = 1000)] + poll_ms: u64, + }, + /// Send a message to another agent. + Send { to: String, body: String }, + /// Pop one message from the inbox. + Recv, +} + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); + + let cli = Cli::parse(); + match cli.cmd { + Cmd::Serve { poll_ms } => serve(&cli.socket, Duration::from_millis(poll_ms)).await, + Cmd::Send { to, body } => { + let resp = client::request(&cli.socket, AgentRequest::Send { to, body }).await?; + render(&resp)?; + check(&resp) + } + Cmd::Recv => { + let resp = client::request(&cli.socket, AgentRequest::Recv).await?; + render(&resp)?; + check(&resp) + } + } +} + +async fn serve(socket: &std::path::Path, interval: Duration) -> Result<()> { + tracing::info!(socket = %socket.display(), "hive-ag3nt serve"); + loop { + match client::request(socket, AgentRequest::Recv).await { + Ok(AgentResponse::Message { from, body }) => { + tracing::info!(%from, %body, "inbox"); + } + Ok(AgentResponse::Empty) => {} + Ok(AgentResponse::Ok) => { + tracing::warn!("recv produced Ok (unexpected)"); + } + Ok(AgentResponse::Err { message }) => { + tracing::warn!(%message, "recv error"); + } + Err(e) => { + tracing::warn!(error = ?e, "recv failed; retrying"); + } + } + tokio::time::sleep(interval).await; + } +} + +fn render(resp: &AgentResponse) -> Result<()> { + println!("{}", serde_json::to_string_pretty(resp)?); + Ok(()) +} + +fn check(resp: &AgentResponse) -> Result<()> { + if let AgentResponse::Err { message } = resp { + bail!("{message}"); + } + Ok(()) } diff --git a/hive-ag3nt/src/bin/hive-m1nd.rs b/hive-ag3nt/src/bin/hive-m1nd.rs index 314550c..a63ce9d 100644 --- a/hive-ag3nt/src/bin/hive-m1nd.rs +++ b/hive-ag3nt/src/bin/hive-m1nd.rs @@ -1,3 +1,5 @@ fn main() { + // Phase 4 — manager tool surface. For now, a placeholder so the binary + // exists and can be referenced from the manager nixos-container template. println!("hive-m1nd placeholder"); } diff --git a/hive-ag3nt/src/client.rs b/hive-ag3nt/src/client.rs new file mode 100644 index 0000000..8deb726 --- /dev/null +++ b/hive-ag3nt/src/client.rs @@ -0,0 +1,27 @@ +use std::path::Path; + +use anyhow::{Context, Result, bail}; +use hive_sh4re::{AgentRequest, AgentResponse}; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::net::UnixStream; + +pub async fn request(socket: &Path, req: AgentRequest) -> Result { + let stream = UnixStream::connect(socket) + .await + .with_context(|| format!("connect to {}", socket.display()))?; + let (read, mut write) = stream.into_split(); + + let mut payload = serde_json::to_string(&req)?; + payload.push('\n'); + write.write_all(payload.as_bytes()).await?; + write.flush().await?; + + let mut reader = BufReader::new(read); + let mut line = String::new(); + reader.read_line(&mut line).await?; + if line.is_empty() { + bail!("server closed connection without responding"); + } + let resp: AgentResponse = serde_json::from_str(line.trim())?; + Ok(resp) +} diff --git a/hive-ag3nt/src/lib.rs b/hive-ag3nt/src/lib.rs index 8b13789..3890473 100644 --- a/hive-ag3nt/src/lib.rs +++ b/hive-ag3nt/src/lib.rs @@ -1 +1,7 @@ +//! Shared in-container harness code used by both `hive-ag3nt` (agent) and +//! `hive-m1nd` (manager) binaries. +pub mod client; + +/// Default socket path inside the container — bind-mounted by `hive-c0re`. +pub const DEFAULT_SOCKET: &str = "/run/hive/mcp.sock"; diff --git a/nix/templates/agent-base.nix b/nix/templates/agent-base.nix index af425b5..fa0b8e6 100644 --- a/nix/templates/agent-base.nix +++ b/nix/templates/agent-base.nix @@ -7,9 +7,11 @@ systemd.services.hive-ag3nt = { description = "hive-ag3nt harness"; wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; serviceConfig = { - ExecStart = "${pkgs.hyperhive}/bin/hive-ag3nt"; - Type = "oneshot"; + ExecStart = "${pkgs.hyperhive}/bin/hive-ag3nt serve"; + Restart = "on-failure"; + RestartSec = 2; }; };