diff --git a/docs/turn-loop.md b/docs/turn-loop.md index 492a7fce..88ac9b82 100644 --- a/docs/turn-loop.md +++ b/docs/turn-loop.md @@ -328,8 +328,9 @@ at_unix_timestamp?)`, `request_next_turn()`. scheduled reminders. Each row carries an id + kind for `cancel_loose_end`. - `cancel_loose_end` — withdraw a `question` (posts `[cancelled by - ]`) or hard-delete a `reminder`. Agents may only cancel rows - they own. + ]`), hard-delete a `reminder`, or cancel a pending `approval` + row. Agents may only cancel rows they own; the `approval` kind is + further restricted to the root agent (`ruth`) server-side. - `remind` — schedule a reminder in this agent's own inbox. Large payloads spill to `/agents//state/reminders/`. Pending count capped at 50 per agent (`HIVE_REMIND_MAX_PENDING_PER_AGENT`). diff --git a/hive-ag3nt/prompts/system.md b/hive-ag3nt/prompts/system.md index 273d0f0a..825b9561 100644 --- a/hive-ag3nt/prompts/system.md +++ b/hive-ag3nt/prompts/system.md @@ -8,7 +8,7 @@ Tools (hyperhive surface): - `mcp__hyperhive__ask(question, options?, multi?, ttl_seconds?, to?)` — surface a structured question to the human operator (default, or `to: "operator"`) OR a peer agent (`to: ""`). Returns immediately with a question id — do NOT wait inline. When the recipient answers, a system message with event `question_answered { id, question, answer, answerer }` lands in your inbox; handle it on a future turn. Use this for clarifications, permission for risky actions, choice between options, or peer Q&A without burning regular inbox slots. `options` is advisory: a short fixed-choice list when applicable, otherwise leave empty for free text. `multi: true` lets the answerer pick multiple (checkboxes), answer comes back comma-joined. `ttl_seconds` auto-cancels with answer `[expired]` (and `answerer: "ttl-watchdog"`) when the decision becomes moot. - `mcp__hyperhive__answer(id, answer)` — answer a question that was routed to YOU. You'll see one in your inbox as a `question_asked { id, asker, question, options, multi }` system event when a peer or the operator calls `ask(to: "", ...)`. The answer surfaces in the asker's inbox as a `question_answered` event. Strict authorisation: you can only answer questions where you are the declared target. - `mcp__hyperhive__get_loose_ends()` — list your loose ends: unanswered questions where you're asker (waiting on someone) or target (owing a reply), plus reminders you've scheduled that haven't fired. No args, cheap server-side sweep. Useful at turn start to remember what's outstanding without scanning inbox archaeology. -- `mcp__hyperhive__cancel_loose_end(kind, id)` — cancel one of your own open threads. `kind` is `"question"` (the asker — you, in this case — gets a `[cancelled by ]` answer so the waiter unblocks), `"reminder"` (hard-deleted before it fires), or `"approval"` (withdraws a pending approval you submitted that got superseded — operator-approved path, so requires the `approvals` tool group). `id` from the matching `get_loose_ends` row or the original submission reply. +- `mcp__hyperhive__cancel_loose_end(kind, id)` — cancel one of your own open threads. `kind` is `"question"` (the asker — you, in this case — gets a `[cancelled by ]` answer so the waiter unblocks), `"reminder"` (hard-deleted before it fires), or `"approval"` (withdraws a pending approval you submitted that got superseded — root agent only; the server rejects this kind for all other callers). `id` from the matching `get_loose_ends` row or the original submission reply. - `mcp__hyperhive__remind(message, delay_seconds? | at_unix_timestamp?, file_path?)` — schedule a message to land in your *own* inbox at a future time (sender shows as `reminder`). Set exactly one of `delay_seconds` (relative) or `at_unix_timestamp` (absolute). Use for self-paced follow-ups instead of blocking a whole turn on a long `recv` wait. A large `message` auto-spills to a file under `/agents/{label}/state/reminders/`; pass `file_path` to point at one yourself. Each agent's pending-reminder count is capped (default 50) — the tool will error if the cap is already reached. - `mcp__hyperhive__set_status(text)` — set a free-text status visible on the operator dashboard. **Call this at the start of every task** to say what you're working on (e.g. `"processing matrix messages"`, `"fixing #319 model priority"`, `"idle"`). Single line, ≤200 chars — the dashboard renders this as a short chip, so longer multi-line text is rejected. Pass an empty string to clear. Persists across harness restarts. - `mcp__hyperhive__get_agent_meta(name?)` — fetch identity + status metadata for an agent: canonical `name`, current `hyperhive_rev`, plus self-reported `status` text (set via `set_status`) and how long ago it was set. Also returns the hive + swarm display names (`hive_name`, `swarm_name`) when the operator has configured `services.hyperhive.{hiveName, swarmName}`; both lines omitted when unset. Pass `name` to query a peer (e.g. check whether iris is idle before pinging them). Omit `name` to get your own trustworthy identity stamp — useful for state files, commit messages, cross-agent attribution that won't drift across renames or session-continue boundaries where the system-prompt label could be stale. diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index ed66e7ed..b8cee0b1 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -383,7 +383,7 @@ fn loose_end_kind_label(kind: hive_sh4re::CancelLooseEndKind) -> &'static str { } /// Format helper for `get_agent_meta`: renders an agent's identity + -/// current status as a short human-readable block. `name`, `role`, +/// current status as a short human-readable block. `name`, /// `hyperhive_rev`, and `running` are always shown; `status` only /// appears when one is set, otherwise the line reads `status: `. /// When `running` is false the host has already cleared `status_text` @@ -978,12 +978,10 @@ impl AgentServer { // IMPORTANT: this tool is only available when the `lifecycle` tool group // is granted to this agent. hive-c0re enforces the topology check // server-side: the call is rejected unless `name` is a direct child. - #[tool( - description = "Stop a direct child sub-agent container (graceful). \ + #[tool(description = "Stop a direct child sub-agent container (graceful). \ Only succeeds if `name` is a direct child of this agent in the topology \ tree — the server enforces this. No approval required. \ - State dir is kept; recreating the agent reuses prior config + credentials." - )] + State dir is kept; recreating the agent reuses prior config + credentials.")] async fn kill(&self, Parameters(args): Parameters) -> String { let log = format!("{args:?}"); let name = args.name.clone(); @@ -1012,7 +1010,10 @@ impl AgentServer { let (resp, retries) = self .dispatch(hive_sh4re::Request::Update { name: args.name }) .await; - annotate_retries(format_ack(resp, "update", format!("updated {name}")), retries) + annotate_retries( + format_ack(resp, "update", format!("updated {name}")), + retries, + ) }) .await } @@ -1029,9 +1030,7 @@ impl AgentServer { )] async fn list_containers(&self) -> String { run_tool_envelope("list_containers", String::new(), async move { - let (resp, retries) = self - .dispatch(hive_sh4re::Request::ListDescendants) - .await; + let (resp, retries) = self.dispatch(hive_sh4re::Request::ListDescendants).await; let body = match resp { Ok(SocketReply::Containers(containers)) => { if containers.is_empty() { @@ -1176,11 +1175,9 @@ impl AgentServer { // IMPORTANT: this tool is only available when the `lifecycle` tool group // is granted to this agent. hive-c0re enforces the topology check // server-side: the call is rejected unless `name` is a direct child. - #[tool( - description = "Start a stopped direct child sub-agent container. \ + #[tool(description = "Start a stopped direct child sub-agent container. \ Only succeeds if `name` is a direct child of this agent in the topology \ - tree — the server enforces this. No approval required." - )] + tree — the server enforces this. No approval required.")] async fn start(&self, Parameters(args): Parameters) -> String { let log = format!("{args:?}"); let name = args.name.clone(); @@ -1188,12 +1185,14 @@ impl AgentServer { let (resp, retries) = self .dispatch(hive_sh4re::Request::Start { name: args.name }) .await; - annotate_retries(format_ack(resp, "start", format!("started {name}")), retries) + annotate_retries( + format_ack(resp, "start", format!("started {name}")), + retries, + ) }) .await } - #[tool( description = "Fetch recent journal log lines for a sub-agent container. Useful \ for diagnosing MCP server registration failures, startup crashes, plugin install \ @@ -1406,9 +1405,7 @@ impl AgentServer { )] async fn list_schedules(&self) -> String { run_tool_envelope("list_schedules", String::new(), async move { - let (resp, retries) = self - .dispatch(hive_sh4re::Request::ListSchedules) - .await; + let (resp, retries) = self.dispatch(hive_sh4re::Request::ListSchedules).await; let body = match resp { Ok(SocketReply::Schedules(schedules)) => serde_json::to_string(&schedules) .unwrap_or_else(|e| format!("list_schedules: serialise: {e:#}")),