open_threads: new get_open_threads MCP tool on agent + manager surfaces
This commit is contained in:
parent
9ec0d60308
commit
dc1ce1f236
11 changed files with 305 additions and 9 deletions
|
|
@ -7,6 +7,7 @@ Tools (hyperhive surface):
|
|||
- (some agents only) **extra MCP tools** surfaced as `mcp__<server>__<tool>` — these are agent-specific (matrix client, scraper, db connector, etc.) declared in your `agent.nix` under `hyperhive.extraMcpServers`. Treat them as first-class tools alongside the hyperhive surface; the operator already auto-approved them at deploy time.
|
||||
- `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: "<agent-name>"`). 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 manager calls `ask(to: "<your-name>", ...)`. 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_open_threads()` — list your loose ends: unanswered questions where you're asker (waiting on someone) or target (owing a reply). No args, cheap server-side sweep. Useful at turn start to remember what's outstanding without scanning inbox archaeology.
|
||||
|
||||
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 + why. The manager evaluates the request (it doesn't rubber-stamp), 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ Tools (hyperhive surface):
|
|||
- `mcp__hyperhive__request_apply_commit(agent, commit_ref, description?)` — submit a config change for any agent (`hm1nd` for self) for operator approval. Pass an optional `description` and it appears on the dashboard approval card so the operator knows what changed without opening the diff. At submit time hive-c0re fetches your commit into the agent's applied repo and pins it as `proposal/<id>`; from that moment your proposed-side commit can be amended or force-pushed freely without changing what the operator will build.
|
||||
- `mcp__hyperhive__ask(question, options?, multi?, ttl_seconds?, to?)` — surface a structured question to the operator (default, or `to: "operator"`) OR a sub-agent (`to: "<agent-name>"`). Returns immediately with a question id; the answer arrives later as a system `question_answered { id, question, answer, answerer }` event in your inbox. Options are advisory: the dashboard always lets the operator type a free-text answer in addition. Set `multi: true` to render options as checkboxes (operator can pick multiple); the answer comes back as `, `-separated. Set `ttl_seconds` to auto-cancel after a deadline (capped at 6h server-side) — on expiry the answer is `[expired]` and `answerer` is `"ttl-watchdog"`. Do not poll inside the same turn — finish the current work and react when the event lands.
|
||||
- `mcp__hyperhive__answer(id, answer)` — answer a question that was routed to YOU (a sub-agent did `ask(to: "manager", ...)`). The triggering event in your inbox is `question_asked { id, asker, question, options, multi }`. The answer surfaces in the asker's inbox as a `question_answered` event.
|
||||
- `mcp__hyperhive__get_open_threads()` — hive-wide loose ends: every pending approval + every unanswered question across the swarm. Cheap server-side sweep, no args. Use to find stalled threads (sub-agent A asked B something three days ago and B never answered) before they rot.
|
||||
|
||||
Approval boundary: lifecycle ops on *existing* sub-agents (`kill`, `start`, `restart`) are at your discretion — no operator approval. *Creating* a new agent (`request_spawn`) and *changing* any agent's config (`request_apply_commit`) still go through the approval queue. The operator only signs off on changes; you run the day-to-day.
|
||||
|
||||
|
|
|
|||
|
|
@ -208,7 +208,8 @@ async fn serve(
|
|||
AgentResponse::Ok
|
||||
| AgentResponse::Status { .. }
|
||||
| AgentResponse::Recent { .. }
|
||||
| AgentResponse::QuestionQueued { .. },
|
||||
| AgentResponse::QuestionQueued { .. }
|
||||
| AgentResponse::OpenThreads { .. },
|
||||
) => {
|
||||
tracing::warn!("recv produced unexpected response kind");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,7 +175,8 @@ async fn serve(
|
|||
| ManagerResponse::Status { .. }
|
||||
| ManagerResponse::QuestionQueued { .. }
|
||||
| ManagerResponse::Recent { .. }
|
||||
| ManagerResponse::Logs { .. },
|
||||
| ManagerResponse::Logs { .. }
|
||||
| ManagerResponse::OpenThreads { .. },
|
||||
) => {
|
||||
tracing::warn!("recv produced unexpected response kind");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ pub enum SocketReply {
|
|||
QuestionQueued(i64),
|
||||
Recent(Vec<hive_sh4re::InboxRow>),
|
||||
Logs(String),
|
||||
OpenThreads(Vec<hive_sh4re::OpenThread>),
|
||||
}
|
||||
|
||||
impl From<hive_sh4re::AgentResponse> for SocketReply {
|
||||
|
|
@ -52,6 +53,7 @@ impl From<hive_sh4re::AgentResponse> for SocketReply {
|
|||
hive_sh4re::AgentResponse::Status { unread } => Self::Status(unread),
|
||||
hive_sh4re::AgentResponse::Recent { rows } => Self::Recent(rows),
|
||||
hive_sh4re::AgentResponse::QuestionQueued { id } => Self::QuestionQueued(id),
|
||||
hive_sh4re::AgentResponse::OpenThreads { threads } => Self::OpenThreads(threads),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -67,6 +69,7 @@ impl From<hive_sh4re::ManagerResponse> for SocketReply {
|
|||
hive_sh4re::ManagerResponse::QuestionQueued { id } => Self::QuestionQueued(id),
|
||||
hive_sh4re::ManagerResponse::Recent { rows } => Self::Recent(rows),
|
||||
hive_sh4re::ManagerResponse::Logs { content } => Self::Logs(content),
|
||||
hive_sh4re::ManagerResponse::OpenThreads { threads } => Self::OpenThreads(threads),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -95,6 +98,57 @@ pub fn format_recv(resp: Result<SocketReply, anyhow::Error>) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
/// Format helper for `get_open_threads`: renders a short bulleted list
|
||||
/// of pending approvals + questions. Empty list collapses to a clear
|
||||
/// marker so claude doesn't go hunting for a payload that isn't there.
|
||||
pub fn format_open_threads(resp: Result<SocketReply, anyhow::Error>) -> String {
|
||||
use std::fmt::Write as _;
|
||||
let threads = match resp {
|
||||
Ok(SocketReply::OpenThreads(t)) => t,
|
||||
Ok(SocketReply::Err(m)) => return format!("get_open_threads failed: {m}"),
|
||||
Ok(other) => return format!("get_open_threads unexpected response: {other:?}"),
|
||||
Err(e) => return format!("get_open_threads transport error: {e:#}"),
|
||||
};
|
||||
if threads.is_empty() {
|
||||
return "(no open threads)".to_owned();
|
||||
}
|
||||
let mut out = format!("{} open thread(s):\n", threads.len());
|
||||
for t in &threads {
|
||||
match t {
|
||||
hive_sh4re::OpenThread::Approval {
|
||||
id,
|
||||
agent,
|
||||
commit_ref,
|
||||
description,
|
||||
age_seconds,
|
||||
} => {
|
||||
let desc = description
|
||||
.as_deref()
|
||||
.map(|d| format!(" — {d}"))
|
||||
.unwrap_or_default();
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"- approval #{id} ({agent} @ {commit_ref}, {age_seconds}s old){desc}"
|
||||
);
|
||||
}
|
||||
hive_sh4re::OpenThread::Question {
|
||||
id,
|
||||
asker,
|
||||
target,
|
||||
question,
|
||||
age_seconds,
|
||||
} => {
|
||||
let to = target.as_deref().unwrap_or("operator");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"- question #{id} ({asker} → {to}, {age_seconds}s old): {question}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Common envelope around every MCP tool handler: pre-log → run →
|
||||
/// post-log. The inbox-status hint used to be appended to every tool
|
||||
/// result; that lives in the wake prompt + UI header now, so tool
|
||||
|
|
@ -317,6 +371,23 @@ impl AgentServer {
|
|||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "List loose ends pending against this agent: unanswered questions \
|
||||
where you are the asker (waiting on someone) or the target (someone's waiting on \
|
||||
you), plus — for the manager only — pending approvals you submitted that the \
|
||||
operator hasn't acted on yet. Cheap server-side sweep, no args. Useful at turn \
|
||||
start to remember what you owe / what's owed to you without scrolling inbox \
|
||||
history. Output is a short bulleted list with ids, ages in seconds, and the \
|
||||
relevant context. Empty result is reported clearly."
|
||||
)]
|
||||
async fn get_open_threads(&self) -> String {
|
||||
run_tool_envelope("get_open_threads", String::new(), async move {
|
||||
let (resp, retries) = self.dispatch(hive_sh4re::AgentRequest::GetOpenThreads).await;
|
||||
annotate_retries(format_open_threads(resp), retries)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Schedule a reminder that lands in this agent's own inbox at a future \
|
||||
time (sender will appear as `reminder`). Use for self-paced follow-ups: 'check task \
|
||||
|
|
@ -784,6 +855,23 @@ impl ManagerServer {
|
|||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Hive-wide loose ends: EVERY pending approval + EVERY unanswered \
|
||||
question across the swarm. Use to scan for stalled coordination — questions \
|
||||
sub-agents asked each other that nobody's answering, approvals stuck waiting on \
|
||||
the operator, etc. No args. The sub-agent flavour of this tool only returns the \
|
||||
agent's own threads; the manager flavour is unfiltered."
|
||||
)]
|
||||
async fn get_open_threads(&self) -> String {
|
||||
run_tool_envelope("get_open_threads", String::new(), async move {
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::ManagerRequest::GetOpenThreads)
|
||||
.await;
|
||||
annotate_retries(format_open_threads(resp), 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 \
|
||||
|
|
@ -826,8 +914,10 @@ impl ManagerServer {
|
|||
approval), `kill` (graceful stop), `request_apply_commit` (config change for \
|
||||
any agent including yourself), `ask` (structured question to the operator or a \
|
||||
sub-agent — non-blocking, answer arrives later as a `question_answered` event), \
|
||||
`answer` (respond to a `question_asked` event directed at you). The manager's own \
|
||||
config lives at `/agents/hm1nd/config/agent.nix`."
|
||||
`answer` (respond to a `question_asked` event directed at you), \
|
||||
`get_open_threads` (hive-wide loose ends — pending approvals + unanswered \
|
||||
questions across the swarm). The manager's own config lives at \
|
||||
`/agents/hm1nd/config/agent.nix`."
|
||||
)]
|
||||
impl ServerHandler for ManagerServer {}
|
||||
|
||||
|
|
@ -861,7 +951,7 @@ pub enum Flavor {
|
|||
#[must_use]
|
||||
pub fn allowed_mcp_tools(flavor: Flavor) -> Vec<String> {
|
||||
let names: &[&str] = match flavor {
|
||||
Flavor::Agent => &["send", "recv", "ask", "answer", "remind"],
|
||||
Flavor::Agent => &["send", "recv", "ask", "answer", "remind", "get_open_threads"],
|
||||
Flavor::Manager => &[
|
||||
"send",
|
||||
"recv",
|
||||
|
|
@ -874,6 +964,7 @@ pub fn allowed_mcp_tools(flavor: Flavor) -> Vec<String> {
|
|||
"ask",
|
||||
"answer",
|
||||
"get_logs",
|
||||
"get_open_threads",
|
||||
"remind",
|
||||
],
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue