Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/hive-subagent-mcp/src/main.rs
atlas d6f94e5247 swarm: show subagent terminals in the swarm UI
An agent's subagent daemon publishes each subagent's output as terminal
rows on `$SWARM.term.<agent>.sub.<subagent>`, as the agent, into a
per-agent stream it creates itself; swarm-controller lists an agent's
subagents from that stream's subjects and relays one subagent's rows as
SSE; the swarm UI lists them under the agent's terminal preview and
reuses AgentTermPreview, full-screen tab included, with no input.

- swarm-nats.nix: the agent token may also publish
  `$SWARM.term.{agent}.sub.>` and `$JS.API.STREAM.CREATE|INFO` on
  `term-sub-{agent}`, and nothing else of JetStream. A module-eval arm
  pins the agent-token grant as an exact list.
- mcp.nix: hive-subagent-daemon loads the agent's store identity
  (`hive-agent-bao-cert/-key/-server-ca`, the ones hive-agent loads)
  whenever the agent has a store, not only on the opencode preset. The
  agent's own queue secret lives in the store, so this is the credential
  the harness connects with.
- hive-subagent-mcp: `swarm_term` reads the agent's queue secret under
  that identity, connects with the agent token, opens or creates
  `term-sub-<agent>` (max_age 24h), and publishes classified rows from
  the sink every subagent line already passes through. The sink only
  queues (bounded, drop-and-count); a missing store, refused credential,
  failed stream create or failed publish is a log line.
- The stream-json classifier (`stream_enrich`) and the `TermMsg` row
  types plus `fit` move from the hive-agent binary into hive-sh4re, so
  the subagent daemon publishes the rows AgentTermPreview already
  renders. hive-agent keeps its LiveEvent classifier on top.
- swarm-controller: `GET /api/agents/{name}/subagents` and
  `GET /api/agents/{name}/subagents/{subagent}/term/stream`.
- docs/swarm: what the UI shows and what the queue carries.

Closes #4827
2026-10-03 01:34:01 +02:00

67 lines
2.7 KiB
Rust

//! `hive-subagent-daemon` binary — spawns nested claude sessions on
//! request and serves the `start`/`continue`/`status`/`interrupt` MCP tool
//! surface directly over streamable-http on `--http <addr>` — no stdio
//! bridge, no separate bin claude has to respawn every turn. The same
//! address also carries the subagent-facing `goal_reached`/`need_help`
//! route, which is the only place the daemon can learn its own URL from.
use std::sync::Arc;
use anyhow::Result;
use clap::Parser;
#[derive(Parser)]
#[command(
name = "hive-subagent-daemon",
about = "claude-subagent runner + MCP daemon"
)]
struct Cli {
/// Serve the MCP tools over streamable-http on this address (e.g.
/// `127.0.0.1:8793`). Bind loopback only.
#[arg(long)]
http: std::net::SocketAddr,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_env("RUST_LOG")
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
// This is a systemd-managed daemon — stdout always goes to journald,
// never a human terminal, and journald doesn't strip ANSI escapes:
// they land in victorialogs as raw byte-array spam otherwise.
.with_ansi(false)
.init();
let cli = Cli::parse();
let todo_socket = hive_subagent_mcp::paths::agent_socket();
tracing::info!(
http = %cli.http,
todo = %todo_socket.display(),
"hive-subagent-daemon starting"
);
// The one place the subagent-facing signal URLs can come from: the
// address this process was told to listen on. Anything else would be a
// guess at the deployment's own port assignment. This is only their
// common prefix — each session's actual URL is this plus a token minted
// for that session alone, which is what makes a signal's identity a
// property of the endpoint rather than of the payload.
let signal_base = format!("http://{}{}", cli.http, hive_subagent_mcp::mcp::SIGNAL_PATH);
// The parent agent's runtime, from the same variables the harness reads.
let runtime = hive_runtime::RuntimeSpec::from_env()?;
let state = Arc::new(
hive_subagent_mcp::session::State::new(todo_socket, signal_base)
.on_runtime(runtime)
.publishing_to(hive_subagent_mcp::swarm_term::Publisher::spawn()),
);
// Serve the MCP tools over streamable-http forever. No background poll
// loop to start — unlike the bash daemon's task-file queue, `start`/
// `continue` spawn their subagent's background turn directly from the
// tool call itself, nothing to scan for.
hive_subagent_mcp::mcp::serve_http(cli.http, state).await
}