remove Role::Manager + ManagerSurface + Flavor::Manager — there is only one role: agent
This commit is contained in:
parent
186ee430b5
commit
f56b272a23
8 changed files with 100 additions and 489 deletions
|
|
@ -1,10 +1,7 @@
|
|||
//! Unified hyperhive harness binary. Picks role from `HIVE_ROLE`
|
||||
//! (`"agent"` | `"manager"`), dispatches one of three subcommands
|
||||
//! (`serve` / `mcp` / `wake`), and runs the turn loop through a
|
||||
//! generic `Surface` trait so both wire surfaces stay in lockstep.
|
||||
//!
|
||||
//! Architecture (single-binary rationale, Surface-trait + zero-sized
|
||||
//! type tags, boot wiring, turn-outcome branch) lives in
|
||||
//! Unified hyperhive harness binary. Dispatches one of three subcommands
|
||||
//! (`serve` / `mcp` / `wake`). There is one role: agent. The `Surface`
|
||||
//! trait + `AgentSurface` zero-sized type tag keeps the turn loop
|
||||
//! generic and testable. Architecture lives in
|
||||
//! [`docs/turn-loop.md::Harness binary shape`](../../../docs/turn-loop.md).
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
@ -13,7 +10,7 @@ use std::time::Duration;
|
|||
|
||||
use hive_ag3nt::web_ui::TurnLock;
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use hive_ag3nt::events::{Bus, LiveEvent, TurnState};
|
||||
use hive_ag3nt::login::{self, LoginState};
|
||||
|
|
@ -21,15 +18,10 @@ use hive_ag3nt::turn_stats::TurnStats;
|
|||
use hive_ag3nt::{
|
||||
DEFAULT_SOCKET, DEFAULT_WEB_PORT, client, mcp, plugins, serve_common, turn, web_ui,
|
||||
};
|
||||
use hive_sh4re::{
|
||||
AgentRequest, AgentResponse, HelperEvent, ManagerRequest, ManagerResponse, SYSTEM_SENDER,
|
||||
};
|
||||
use hive_sh4re::{AgentRequest, AgentResponse, HelperEvent, SYSTEM_SENDER};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "hive",
|
||||
about = "hyperhive harness — role from $HIVE_ROLE (agent|manager)"
|
||||
)]
|
||||
#[command(name = "hive", about = "hyperhive harness")]
|
||||
struct Cli {
|
||||
/// Path to the per-agent MCP socket (bind-mounted from the host).
|
||||
#[arg(long, global = true, default_value = DEFAULT_SOCKET)]
|
||||
|
|
@ -48,16 +40,14 @@ enum Cmd {
|
|||
#[arg(long, default_value_t = 1000)]
|
||||
poll_ms: u64,
|
||||
},
|
||||
/// Run this role's MCP server on stdio. Spawned by `claude` via
|
||||
/// Run the MCP server on stdio. Spawned by `claude` via
|
||||
/// `--mcp-config`; tools dispatch through `/run/hive/mcp.sock` back
|
||||
/// into the hyperhive broker.
|
||||
Mcp,
|
||||
/// Inject a wake-up event into this harness's inbox so the next
|
||||
/// turn fires with the given body. Intended for extra MCP servers
|
||||
/// / helpers (matrix bridge, scraper, webhook listener, etc.) that
|
||||
/// need to nudge claude on external events. Available on both
|
||||
/// agent and manager roles; mirrors the `AgentRequest::Wake` /
|
||||
/// `ManagerRequest::Wake` pair already on the wire.
|
||||
/// need to nudge claude on external events.
|
||||
Wake {
|
||||
#[arg(long)]
|
||||
from: String,
|
||||
|
|
@ -67,20 +57,6 @@ enum Cmd {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum Role {
|
||||
Agent,
|
||||
Manager,
|
||||
}
|
||||
|
||||
fn resolve_role() -> Result<Role> {
|
||||
match std::env::var("HIVE_ROLE").as_deref() {
|
||||
Ok("agent") | Err(_) => Ok(Role::Agent),
|
||||
Ok("manager") => Ok(Role::Manager),
|
||||
Ok(other) => bail!("unknown HIVE_ROLE={other:?}; expected 'agent' or 'manager'"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
|
|
@ -91,26 +67,11 @@ async fn main() -> Result<()> {
|
|||
.init();
|
||||
|
||||
let cli = Cli::parse();
|
||||
let role = resolve_role()?;
|
||||
|
||||
// Generic dispatch: one `serve_main` / `wake` body, two
|
||||
// monomorphisations driven by the `Surface` type parameter. See
|
||||
// `docs/turn-loop.md::Surface trait + zero-sized type tags`.
|
||||
match (role, cli.cmd) {
|
||||
(Role::Agent, Cmd::Serve { poll_ms }) => {
|
||||
serve_main::<AgentSurface>(&cli.socket, poll_ms).await
|
||||
}
|
||||
(Role::Manager, Cmd::Serve { poll_ms }) => {
|
||||
serve_main::<ManagerSurface>(&cli.socket, poll_ms).await
|
||||
}
|
||||
(Role::Agent, Cmd::Mcp) => mcp::serve_agent_stdio(cli.socket).await,
|
||||
(Role::Manager, Cmd::Mcp) => mcp::serve_agent_stdio(cli.socket).await,
|
||||
(Role::Agent, Cmd::Wake { from, body }) => {
|
||||
wake::<AgentSurface>(&cli.socket, from, body).await
|
||||
}
|
||||
(Role::Manager, Cmd::Wake { from, body }) => {
|
||||
wake::<ManagerSurface>(&cli.socket, from, body).await
|
||||
}
|
||||
match cli.cmd {
|
||||
Cmd::Serve { poll_ms } => serve_main::<AgentSurface>(&cli.socket, poll_ms).await,
|
||||
Cmd::Mcp => mcp::serve_agent_stdio(cli.socket).await,
|
||||
Cmd::Wake { from, body } => wake::<AgentSurface>(&cli.socket, from, body).await,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -186,16 +147,11 @@ enum RecvOutcome {
|
|||
TransportError,
|
||||
}
|
||||
|
||||
/// Per-role wire surface. Two impls — `AgentSurface`, `ManagerSurface`
|
||||
/// — wrap the disjoint `Request`/`Response` enums plus a handful of
|
||||
/// boot-time constants that vary by role. Every other function in this
|
||||
/// binary that talks to the broker goes through this trait so the turn
|
||||
/// loop itself has zero per-role branches.
|
||||
/// Wire surface abstraction. `AgentSurface` is the only impl — the trait
|
||||
/// exists to keep the turn loop generic and testable. Every function that
|
||||
/// talks to the broker goes through this so there are zero hard-coded
|
||||
/// `AgentRequest` / `AgentResponse` references in the turn loop itself.
|
||||
trait Surface {
|
||||
/// MCP flavor passed to `TurnFiles::prepare`. Picks which static
|
||||
/// system-prompt block + tool registration goes into the spawned
|
||||
/// `claude` process.
|
||||
const FLAVOR: mcp::Flavor;
|
||||
/// Ack the in-flight turn. Logs warnings on transport/broker
|
||||
/// errors but never propagates — turn loop continues either way.
|
||||
fn ack_turn(socket: &Path) -> impl Future<Output = ()>;
|
||||
|
|
@ -237,12 +193,11 @@ trait Surface {
|
|||
|
||||
// ---------- AgentSurface ----------
|
||||
|
||||
/// Zero-sized type tag for the sub-agent wire surface.
|
||||
/// Zero-sized type tag for the agent wire surface.
|
||||
/// Talks `AgentRequest` / `AgentResponse`.
|
||||
struct AgentSurface;
|
||||
|
||||
impl Surface for AgentSurface {
|
||||
const FLAVOR: mcp::Flavor = mcp::Flavor::Agent;
|
||||
|
||||
async fn ack_turn(socket: &Path) {
|
||||
match client::request::<_, AgentResponse>(socket, &AgentRequest::AckTurn).await {
|
||||
|
|
@ -382,159 +337,11 @@ impl Surface for AgentSurface {
|
|||
}
|
||||
}
|
||||
|
||||
// ---------- ManagerSurface ----------
|
||||
|
||||
/// Zero-sized type tag for the manager wire surface.
|
||||
/// Talks `ManagerRequest` / `ManagerResponse`.
|
||||
struct ManagerSurface;
|
||||
|
||||
impl Surface for ManagerSurface {
|
||||
const FLAVOR: mcp::Flavor = mcp::Flavor::Manager;
|
||||
|
||||
async fn ack_turn(socket: &Path) {
|
||||
match client::request::<_, ManagerResponse>(socket, &ManagerRequest::AckTurn).await {
|
||||
Ok(ManagerResponse::Ok) => {}
|
||||
Ok(ManagerResponse::Err { message }) => {
|
||||
tracing::warn!(%message, "ack_turn rejected by broker");
|
||||
}
|
||||
Ok(other) => tracing::warn!(?other, "ack_turn unexpected response"),
|
||||
Err(e) => tracing::warn!(error = ?e, "ack_turn transport error"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn requeue_inflight(socket: &Path) {
|
||||
match client::request::<_, ManagerResponse>(socket, &ManagerRequest::RequeueInflight).await
|
||||
{
|
||||
Ok(ManagerResponse::Ok) => {}
|
||||
Ok(ManagerResponse::Err { message }) => {
|
||||
tracing::warn!(%message, "requeue_inflight rejected by broker");
|
||||
}
|
||||
Ok(other) => tracing::warn!(?other, "requeue_inflight unexpected response"),
|
||||
Err(e) => tracing::warn!(error = ?e, "requeue_inflight transport error"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn inbox_unread(socket: &Path) -> u64 {
|
||||
match client::request::<_, ManagerResponse>(socket, &ManagerRequest::Status).await {
|
||||
Ok(ManagerResponse::Status { unread }) => unread,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
async fn post_turn_counts(socket: &Path) -> (Option<u64>, Option<u64>) {
|
||||
let threads = match client::request::<_, ManagerResponse>(
|
||||
socket,
|
||||
&ManagerRequest::GetLooseEnds { agent: None },
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(ManagerResponse::LooseEnds { loose_ends }) => u64::try_from(loose_ends.len()).ok(),
|
||||
_ => None,
|
||||
};
|
||||
let reminders = match client::request::<_, ManagerResponse>(
|
||||
socket,
|
||||
&ManagerRequest::CountPendingReminders { agent: None },
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(ManagerResponse::PendingRemindersCount { count }) => Some(count),
|
||||
_ => None,
|
||||
};
|
||||
(threads, reminders)
|
||||
}
|
||||
|
||||
async fn send_to_parent(socket: &Path, body: String) {
|
||||
let res = client::request::<_, ManagerResponse>(
|
||||
socket,
|
||||
&ManagerRequest::Send {
|
||||
to: hive_sh4re::PARENT_RECIPIENT.into(),
|
||||
body,
|
||||
in_reply_to: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
tracing::warn!(error = ?e, "failed to notify parent of turn failure");
|
||||
}
|
||||
}
|
||||
|
||||
async fn self_wake(socket: &Path) {
|
||||
let res = client::request::<_, ManagerResponse>(
|
||||
socket,
|
||||
&ManagerRequest::Wake {
|
||||
from: "self".into(),
|
||||
body: "continue".into(),
|
||||
transient: false,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
match res {
|
||||
Ok(ManagerResponse::Ok) => {
|
||||
tracing::info!("request_next_turn: injected self-continue wake");
|
||||
}
|
||||
Ok(ManagerResponse::Err { message }) => {
|
||||
tracing::warn!(%message, "check_and_inject_continue: wake rejected");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(error = ?e, "check_and_inject_continue: wake transport error");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
async fn recv_next(socket: &Path) -> RecvOutcome {
|
||||
let recv: Result<ManagerResponse> = client::request(
|
||||
socket,
|
||||
&ManagerRequest::Recv {
|
||||
wait_seconds: Some(180),
|
||||
max: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
match recv {
|
||||
Ok(ManagerResponse::Messages { messages }) if !messages.is_empty() => {
|
||||
let first = messages.into_iter().next().expect("checked non-empty");
|
||||
RecvOutcome::Message(first)
|
||||
}
|
||||
Ok(ManagerResponse::Messages { .. }) => RecvOutcome::Empty,
|
||||
Ok(ManagerResponse::Err { message }) => {
|
||||
tracing::warn!(%message, "recv error");
|
||||
RecvOutcome::TransportError
|
||||
}
|
||||
Ok(other) => {
|
||||
tracing::warn!(?other, "recv produced unexpected response kind");
|
||||
RecvOutcome::TransportError
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(error = ?e, "recv failed; retrying");
|
||||
RecvOutcome::TransportError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn wake_external(socket: &Path, from: String, body: String) -> Result<()> {
|
||||
let resp: ManagerResponse = client::request(
|
||||
socket,
|
||||
&ManagerRequest::Wake {
|
||||
from,
|
||||
body,
|
||||
transient: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
match resp {
|
||||
ManagerResponse::Ok => Ok(()),
|
||||
ManagerResponse::Err { message } => anyhow::bail!("wake: {message}"),
|
||||
other => anyhow::bail!("wake: unexpected response {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- generic turn loop ----------
|
||||
|
||||
/// Per-role boot — wires up the web UI, login state, stats, plugins,
|
||||
/// forge notifier, and either drops into `serve_loop` directly
|
||||
/// (`Online`) or parks on the login flow first (`NeedsLogin`). See
|
||||
/// Boot — wires up the web UI, login state, stats, plugins, forge
|
||||
/// notifier, and either drops into `serve_loop` directly (`Online`) or
|
||||
/// parks on the login flow first (`NeedsLogin`). See
|
||||
/// `docs/turn-loop.md::Boot wiring`.
|
||||
async fn serve_main<S: Surface>(socket: &Path, poll_ms: u64) -> Result<()> {
|
||||
let port = std::env::var("HIVE_PORT")
|
||||
|
|
@ -559,13 +366,12 @@ async fn serve_main<S: Surface>(socket: &Path, poll_ms: u64) -> Result<()> {
|
|||
bus.seed_usage(ctx, cost);
|
||||
}
|
||||
}
|
||||
let files = turn::TurnFiles::prepare(socket, &label, S::FLAVOR).await?;
|
||||
let files = turn::TurnFiles::prepare(socket, &label).await?;
|
||||
let turn_lock: TurnLock = Arc::new(tokio::sync::Mutex::new(()));
|
||||
// Plugin install runs role-agnostic: failures come back as a
|
||||
// Vec<String> and we route each through `<parent>` via the same
|
||||
// `send_to_parent` failure-notify path the turn loop uses. The
|
||||
// broker resolves `<parent>` per `topology::parent_of`; root
|
||||
// agents and the manager fall through to operator.
|
||||
// Plugin install failures come back as a Vec<String> — route each
|
||||
// through `<parent>` via the `send_to_parent` failure-notify path.
|
||||
// The broker resolves `<parent>` per `topology::parent_of`;
|
||||
// root agents fall through to operator.
|
||||
for failure in plugins::install_configured(socket).await {
|
||||
S::send_to_parent(socket, failure).await;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1744,13 +1744,11 @@ pub const SERVER_NAME: &str = "hyperhive";
|
|||
/// should plan in /state notes instead.
|
||||
pub const ALLOWED_BUILTIN_TOOLS: &[&str] = &["Edit", "Glob", "Grep", "Read", "Write"];
|
||||
|
||||
/// Which MCP tool surface to advertise via `--allowedTools`. The agent
|
||||
/// list is the strict subset of the manager list, so we just thread the
|
||||
/// flavor through.
|
||||
/// Kept for API stability — was two-variant (Agent/Manager) but the manager
|
||||
/// role no longer exists. Only `Agent` remains; everything is perms + caps.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Flavor {
|
||||
Agent,
|
||||
Manager,
|
||||
}
|
||||
|
||||
/// Env var written by the meta renderer with a comma-separated list of
|
||||
|
|
@ -1800,18 +1798,11 @@ fn allowed_capability_tools() -> Vec<String> {
|
|||
/// token is matched (case-insensitive) against the `ToolGroup` serde names
|
||||
/// (`messaging`, `meta`, `inbox`, `lifecycle`, `approvals`, `scheduling`,
|
||||
/// `diagnostics`, `execution`). Unrecognised tokens are logged and skipped.
|
||||
/// Falls back to the flavor default when the env var is absent or empty.
|
||||
fn effective_tool_groups(flavor: Flavor) -> Vec<hive_sh4re::ToolGroup> {
|
||||
/// Falls back to `AGENT_DEFAULT` when the env var is absent or empty.
|
||||
fn effective_tool_groups() -> Vec<hive_sh4re::ToolGroup> {
|
||||
let raw = match std::env::var(TOOL_GROUPS_ENV) {
|
||||
Ok(v) if !v.trim().is_empty() => v,
|
||||
_ => {
|
||||
// No env var — use the flavor default unchanged.
|
||||
let defaults = match flavor {
|
||||
Flavor::Agent => hive_sh4re::ToolGroup::AGENT_DEFAULT,
|
||||
Flavor::Manager => hive_sh4re::ToolGroup::MANAGER_DEFAULT,
|
||||
};
|
||||
return defaults.to_vec();
|
||||
}
|
||||
_ => return hive_sh4re::ToolGroup::AGENT_DEFAULT.to_vec(),
|
||||
};
|
||||
let mut groups = Vec::new();
|
||||
for token in raw.split(',') {
|
||||
|
|
@ -1829,12 +1820,9 @@ fn effective_tool_groups(flavor: Flavor) -> Vec<hive_sh4re::ToolGroup> {
|
|||
if groups.is_empty() {
|
||||
tracing::warn!(
|
||||
"{TOOL_GROUPS_ENV} set but contained no recognised groups; \
|
||||
falling back to flavor default"
|
||||
falling back to AGENT_DEFAULT"
|
||||
);
|
||||
return match flavor {
|
||||
Flavor::Agent => hive_sh4re::ToolGroup::AGENT_DEFAULT.to_vec(),
|
||||
Flavor::Manager => hive_sh4re::ToolGroup::MANAGER_DEFAULT.to_vec(),
|
||||
};
|
||||
return hive_sh4re::ToolGroup::AGENT_DEFAULT.to_vec();
|
||||
}
|
||||
groups
|
||||
}
|
||||
|
|
@ -1872,8 +1860,8 @@ pub fn allowed_mcp_tools(groups: &[hive_sh4re::ToolGroup]) -> Vec<String> {
|
|||
/// Combined allow-list passed to `--allowedTools` (auto-approve) — covers
|
||||
/// both the built-ins and the MCP surface.
|
||||
#[must_use]
|
||||
pub fn allowed_tools_arg(flavor: Flavor) -> String {
|
||||
let groups = effective_tool_groups(flavor);
|
||||
pub fn allowed_tools_arg() -> String {
|
||||
let groups = effective_tool_groups();
|
||||
// Base built-ins always present.
|
||||
let mut all: Vec<String> = ALLOWED_BUILTIN_TOOLS
|
||||
.iter()
|
||||
|
|
@ -1903,15 +1891,7 @@ pub fn allowed_tools_arg(flavor: Flavor) -> String {
|
|||
/// `WebFetch`/`WebSearch` when the `web_tools` group is active).
|
||||
#[must_use]
|
||||
pub fn builtin_tools_arg() -> String {
|
||||
builtin_tools_arg_for_flavor(Flavor::Agent)
|
||||
}
|
||||
|
||||
/// Flavor-aware variant used by `turn.rs` via `builtin_tools_arg`. Reads
|
||||
/// the effective tool groups for `flavor` so `--tools` matches what
|
||||
/// `--allowedTools` includes for the same session.
|
||||
#[must_use]
|
||||
pub fn builtin_tools_arg_for_flavor(flavor: Flavor) -> String {
|
||||
let groups = effective_tool_groups(flavor);
|
||||
let groups = effective_tool_groups();
|
||||
let mut tools: Vec<&str> = ALLOWED_BUILTIN_TOOLS.to_vec();
|
||||
for group in &groups {
|
||||
for t in group.builtin_tools() {
|
||||
|
|
|
|||
|
|
@ -1,38 +1,31 @@
|
|||
//! System-prompt renderer. Single `prompts/system.md` with
|
||||
//! HTML-comment markers gating role-specific blocks; this module
|
||||
//! assembles the final prompt for a given flavor. Marker grammar +
|
||||
//! placeholder substitution rules in
|
||||
//! assembles the final prompt (always "agent" role — there is only one
|
||||
//! role). Marker grammar + placeholder substitution rules in
|
||||
//! `docs/turn-loop.md::On-boot files` (`claude-system-prompt.md`).
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::mcp::Flavor;
|
||||
|
||||
/// Assemble the system prompt for a given flavor + label + pronouns +
|
||||
/// optional hive / swarm display names. Pure function — no I/O. Splits
|
||||
/// out from [`write_system_prompt`] so the marker logic + substitution
|
||||
/// is unit-testable in isolation. The caller supplies the template body
|
||||
/// so tests can pass an inline fixture and production reads it once at
|
||||
/// harness startup via [`hive_sh4re::assets::prompt_template`]
|
||||
/// Assemble the system prompt for a given label + pronouns + optional hive /
|
||||
/// swarm display names. Pure function — no I/O. Splits out from
|
||||
/// [`write_system_prompt`] so the marker logic + substitution is unit-testable
|
||||
/// in isolation. The caller supplies the template body so tests can pass an
|
||||
/// inline fixture and production reads it once at harness startup via
|
||||
/// [`hive_sh4re::assets::prompt_template`]
|
||||
/// (`$HIVE_ASSETS_DIR/prompts/system.md`). Substitution placeholders +
|
||||
/// marker grammar documented in
|
||||
/// `docs/turn-loop.md::On-boot files` (`claude-system-prompt.md`).
|
||||
#[must_use]
|
||||
pub fn render(
|
||||
template: &str,
|
||||
flavor: Flavor,
|
||||
label: &str,
|
||||
operator_pronouns: &str,
|
||||
hive_name: Option<&str>,
|
||||
swarm_name: Option<&str>,
|
||||
) -> String {
|
||||
let target = match flavor {
|
||||
Flavor::Agent => "agent",
|
||||
Flavor::Manager => "manager",
|
||||
};
|
||||
let body = filter_role_blocks(template, target);
|
||||
let body = filter_role_blocks(template, "agent");
|
||||
let qualified = crate::identity::qualify(label);
|
||||
let hive_identity = hive_name
|
||||
.filter(|n| !n.is_empty())
|
||||
|
|
@ -113,7 +106,7 @@ fn parse_close_marker(line: &str) -> Option<&str> {
|
|||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the system prompt file cannot be written.
|
||||
pub async fn write_system_prompt(_socket: &Path, label: &str, flavor: Flavor) -> Result<PathBuf> {
|
||||
pub async fn write_system_prompt(_socket: &Path, label: &str) -> Result<PathBuf> {
|
||||
let parent = crate::paths::config_dir();
|
||||
tokio::fs::create_dir_all(&parent).await.ok();
|
||||
let pronouns = std::env::var("HIVE_OPERATOR_PRONOUNS").unwrap_or_else(|_| "she/her".to_owned());
|
||||
|
|
@ -133,7 +126,6 @@ pub async fn write_system_prompt(_socket: &Path, label: &str, flavor: Flavor) ->
|
|||
let swarm_name = crate::identity::swarm_name();
|
||||
let body = render(
|
||||
&template,
|
||||
flavor,
|
||||
label,
|
||||
&pronouns,
|
||||
hive_name.as_deref(),
|
||||
|
|
@ -279,7 +271,6 @@ shared closer
|
|||
// harness already relied on.
|
||||
let rendered = render(
|
||||
&PRODUCTION_TEMPLATE,
|
||||
Flavor::Agent,
|
||||
"alice",
|
||||
"they/them",
|
||||
None,
|
||||
|
|
@ -292,66 +283,32 @@ shared closer
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn render_agent_excludes_manager_only_tools() {
|
||||
// Spot-check: the manager-only tool block (request_init_config,
|
||||
// kill, schedule_*) MUST NOT appear in the agent's rendered
|
||||
// prompt. Drift between flavor and tool surface bites every
|
||||
// time it happens.
|
||||
fn render_no_role_markers_in_output() {
|
||||
// No raw role markers should survive into the rendered prompt.
|
||||
let rendered = render(
|
||||
&PRODUCTION_TEMPLATE,
|
||||
Flavor::Agent,
|
||||
"alice",
|
||||
"she/her",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(!rendered.contains("request_init_config"));
|
||||
assert!(!rendered.contains("request_apply_commit"));
|
||||
assert!(!rendered.contains("get_logs"));
|
||||
// Sanity: shared tools DO appear.
|
||||
assert!(!rendered.contains("<!-- role:"));
|
||||
assert!(!rendered.contains("<!-- /role:"));
|
||||
// Shared tools appear.
|
||||
assert!(rendered.contains("mcp__hyperhive__recv"));
|
||||
assert!(rendered.contains("mcp__hyperhive__ask"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_manager_includes_manager_only_tools() {
|
||||
fn render_uses_agent_opener() {
|
||||
let rendered = render(
|
||||
&PRODUCTION_TEMPLATE,
|
||||
Flavor::Manager,
|
||||
"ruth",
|
||||
"she/her",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(rendered.contains("request_init_config"));
|
||||
assert!(rendered.contains("request_apply_commit"));
|
||||
assert!(rendered.contains("get_logs"));
|
||||
assert!(rendered.contains("request_schedule_prompt"));
|
||||
assert!(rendered.contains("cancel_schedule"));
|
||||
// Sub-agent-only sections must NOT appear in manager prompt.
|
||||
assert!(!rendered.contains("request_next_turn"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_uses_correct_role_opener() {
|
||||
let agent = render(
|
||||
&PRODUCTION_TEMPLATE,
|
||||
Flavor::Agent,
|
||||
"alice",
|
||||
"she/her",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(agent.starts_with("You are hyperhive agent"));
|
||||
let manager = render(
|
||||
&PRODUCTION_TEMPLATE,
|
||||
Flavor::Manager,
|
||||
"ruth",
|
||||
"she/her",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(manager.starts_with("You are the hyperhive manager"));
|
||||
assert!(rendered.starts_with("You are hyperhive agent"));
|
||||
}
|
||||
|
||||
// Inline fixture for the {hive_identity} / {swarm_identity}
|
||||
|
|
@ -371,7 +328,6 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
|||
fn render_substitutes_hive_identity_when_set() {
|
||||
let rendered = render(
|
||||
IDENTITY_FIXTURE,
|
||||
Flavor::Agent,
|
||||
"alice",
|
||||
"she/her",
|
||||
Some("pr1ma"),
|
||||
|
|
@ -389,7 +345,6 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
|||
fn render_substitutes_swarm_identity_when_set() {
|
||||
let rendered = render(
|
||||
IDENTITY_FIXTURE,
|
||||
Flavor::Manager,
|
||||
"ruth",
|
||||
"she/her",
|
||||
None,
|
||||
|
|
@ -403,7 +358,6 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
|||
fn render_substitutes_both_when_both_set() {
|
||||
let rendered = render(
|
||||
IDENTITY_FIXTURE,
|
||||
Flavor::Agent,
|
||||
"iris",
|
||||
"she/her",
|
||||
Some("pr1ma"),
|
||||
|
|
@ -418,14 +372,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
|||
fn render_omits_identity_when_unset() {
|
||||
// None / None must round-trip the non-identity opener verbatim
|
||||
// — single-hive deployments see zero diff.
|
||||
let rendered = render(
|
||||
IDENTITY_FIXTURE,
|
||||
Flavor::Agent,
|
||||
"alice",
|
||||
"she/her",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let rendered = render(IDENTITY_FIXTURE, "alice", "she/her", None, None);
|
||||
assert!(!rendered.contains("on hive"));
|
||||
assert!(!rendered.contains("in swarm"));
|
||||
assert!(!rendered.contains("{hive_identity}"));
|
||||
|
|
@ -438,14 +385,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
|||
// through `identity::hive_name()` as None (the accessor
|
||||
// filters empty), but `render` should still no-op on a
|
||||
// direct `Some("")` from a test fixture or a future caller.
|
||||
let rendered = render(
|
||||
IDENTITY_FIXTURE,
|
||||
Flavor::Agent,
|
||||
"alice",
|
||||
"she/her",
|
||||
Some(""),
|
||||
Some(""),
|
||||
);
|
||||
let rendered = render(IDENTITY_FIXTURE, "alice", "she/her", Some(""), Some(""));
|
||||
assert!(!rendered.contains("on hive"));
|
||||
assert!(!rendered.contains("in swarm"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
//! Per-turn claude invocation shared by `hive-ag3nt` and `hive-m1nd`. The
|
||||
//! two binaries differ only in their MCP `Flavor` (agent surface vs.
|
||||
//! manager surface) and their wake-prompt wording; the spawn shape,
|
||||
//! arg-vector, stdin plumbing, and stream-json pumping are identical.
|
||||
//! Per-turn claude invocation. The spawn shape, arg-vector, stdin plumbing,
|
||||
//! and stream-json pumping are shared across all roles (there is only one
|
||||
//! role: agent).
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
@ -107,7 +106,6 @@ pub struct TurnFiles {
|
|||
pub mcp_config: PathBuf,
|
||||
pub settings: PathBuf,
|
||||
pub system_prompt: PathBuf,
|
||||
pub flavor: mcp::Flavor,
|
||||
}
|
||||
|
||||
impl TurnFiles {
|
||||
|
|
@ -117,12 +115,11 @@ impl TurnFiles {
|
|||
/// # Errors
|
||||
///
|
||||
/// Returns an error if any of the config files cannot be written to disk.
|
||||
pub async fn prepare(socket: &Path, label: &str, flavor: mcp::Flavor) -> Result<Self> {
|
||||
pub async fn prepare(socket: &Path, label: &str) -> Result<Self> {
|
||||
Ok(Self {
|
||||
mcp_config: write_mcp_config(socket).await?,
|
||||
settings: write_settings(socket).await?,
|
||||
system_prompt: write_system_prompt(socket, label, flavor).await?,
|
||||
flavor,
|
||||
system_prompt: write_system_prompt(socket, label).await?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -184,12 +181,8 @@ pub async fn write_settings(_socket: &Path) -> Result<PathBuf> {
|
|||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the system prompt file cannot be written.
|
||||
pub async fn write_system_prompt(
|
||||
socket: &Path,
|
||||
label: &str,
|
||||
flavor: mcp::Flavor,
|
||||
) -> Result<PathBuf> {
|
||||
crate::prompt::write_system_prompt(socket, label, flavor).await
|
||||
pub async fn write_system_prompt(socket: &Path, label: &str) -> Result<PathBuf> {
|
||||
crate::prompt::write_system_prompt(socket, label).await
|
||||
}
|
||||
|
||||
/// One claude turn's outcome. The harness uses this to decide whether to
|
||||
|
|
@ -682,9 +675,9 @@ async fn run_claude(prompt: &str, files: &TurnFiles, bus: &Bus) -> Result<(bool,
|
|||
.arg(&files.mcp_config)
|
||||
.arg("--strict-mcp-config")
|
||||
.arg("--tools")
|
||||
.arg(mcp::builtin_tools_arg_for_flavor(files.flavor))
|
||||
.arg(mcp::builtin_tools_arg())
|
||||
.arg("--allowedTools")
|
||||
.arg(mcp::allowed_tools_arg(files.flavor));
|
||||
.arg(mcp::allowed_tools_arg());
|
||||
let mut child = cmd
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ use crate::client;
|
|||
use crate::events::Bus;
|
||||
use crate::login::LoginState;
|
||||
use crate::login_session::{LoginSession, drop_if_finished};
|
||||
use crate::mcp;
|
||||
use crate::turn::TurnFiles;
|
||||
|
||||
/// Live login state for the web UI. The harness updates this in place as it
|
||||
|
|
@ -63,8 +62,6 @@ struct AppState {
|
|||
gui_vnc_port: Option<u16>,
|
||||
}
|
||||
|
||||
/// Re-export so callers in `turn.rs` can name the type via `web_ui::Flavor`.
|
||||
pub type Flavor = mcp::Flavor;
|
||||
|
||||
/// Bind the per-container web listener and serve the SPA.
|
||||
///
|
||||
|
|
|
|||
Loading…
Reference in a new issue