hive-agent-mcp: remove the ask/answer MCP tools

This commit is contained in:
damocles 2026-08-29 20:49:06 +02:00 committed by mara
commit c10e551b2a
2 changed files with 5 additions and 151 deletions

View file

@ -157,47 +157,6 @@ pub struct UpdateArgs {
pub name: String,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct AskArgs {
/// The question to surface.
pub question: String,
/// Optional fixed-choice answers. The dashboard renders these as
/// chips alongside a free-text fallback ("Other…") so the operator
/// is never trapped by an incomplete list; peer-agent recipients
/// see the list in their inbox event and can return any string.
#[serde(default)]
pub options: Vec<String>,
/// When true, options are rendered as checkboxes — the answerer
/// can pick any subset. The answer comes back as a single string
/// with selections joined by ", ". Ignored when `options` is empty.
#[serde(default)]
pub multi: bool,
/// Optional auto-cancel after `ttl_seconds` (capped server-side at
/// 6 hours). On expiry the question resolves with answer
/// `[expired]` and the asker receives the usual
/// `question_answered` system event (with `answerer:
/// "ttl-watchdog"`). `None` (default) = wait indefinitely.
#[serde(default)]
pub ttl_seconds: Option<u64>,
/// Recipient. Omit (or pass `"operator"`) to ask the human
/// operator via the dashboard. Pass another agent's logical name
/// to ask that peer — they receive a `question_asked` event in
/// their inbox and answer via `mcp__hyperhive__answer`.
#[serde(default)]
pub to: Option<String>,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct AnswerArgs {
/// Id of the question being answered — comes from the
/// `question_asked` event in your inbox.
pub id: i64,
/// Free-text answer body. Soft-capped at 4 KiB by the same
/// `MESSAGE_MAX_BYTES` limit as `send`; keep it short or write the
/// detail to a file and pass a path.
pub answer: String,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct CancelLooseEndArgs {
/// Which kind of thread to cancel — `"question"` for an open

View file

@ -24,11 +24,11 @@ mod args;
mod render;
pub use args::{
AckUntilArgs, AgentGetLooseEndsArgs, AnswerArgs, AskArgs, CancelLooseEndArgs,
CancelScheduleArgs, CompactArgs, CreateRepoArgs, EditScheduleArgs, FireScheduleNowArgs,
GetAgentMetaArgs, GetHostJournalArgs, GetLogsArgs, KillArgs, MarkTodosDoneArgs, RecvArgs,
RemindArgs, RequestInitConfigArgs, RequestSchedulePromptArgs, RestartArgs, SendArgs,
SetStatusArgs, StartArgs, UpdateArgs, UpdateMetaInputsArgs,
AckUntilArgs, AgentGetLooseEndsArgs, CancelLooseEndArgs, CancelScheduleArgs, CompactArgs,
CreateRepoArgs, EditScheduleArgs, FireScheduleNowArgs, GetAgentMetaArgs, GetHostJournalArgs,
GetLogsArgs, KillArgs, MarkTodosDoneArgs, RecvArgs, RemindArgs, RequestInitConfigArgs,
RequestSchedulePromptArgs, RestartArgs, SendArgs, SetStatusArgs, StartArgs, UpdateArgs,
UpdateMetaInputsArgs,
};
pub use render::{annotate_retries, format_ack, format_agent_meta, format_recv};
@ -169,111 +169,6 @@ impl AgentServer {
.await
}
#[tool(
description = "Surface a structured question to either the operator OR a peer agent. \
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. \n\n\
Recipient: omit `to` (or set `to: \"operator\"`) for the human operator on the \
dashboard. Set `to: \"<agent-name>\"` to ask a peer agent — they receive a \
`question_asked { id, asker, question, options, multi }` event in their inbox \
and answer via `mcp__hyperhive__answer`. \n\n\
`options` is advisory: pass a short fixed-choice list when applicable, otherwise \
leave empty for free text. Set `multi: true` to let the answerer pick multiple \
options (checkboxes on the dashboard, hint to the agent otherwise) answer comes \
back as a comma-separated string. Set `ttl_seconds` to auto-cancel a \
no-longer-relevant question on expiry the answer is `[expired]` (with \
`answerer: \"ttl-watchdog\"`) and the same `question_answered` event fires."
)]
async fn ask(&self, Parameters(args): Parameters<AskArgs>) -> String {
let log = format!("{args:?}");
run_tool_envelope("ask", log, async move {
let to = match args.to.map(|t| hive_types::Ident::parse(&t)).transpose() {
Ok(to) => to,
Err(reason) => return format!("invalid `to` agent name: {reason}"),
};
let question = args.question;
let target = to
.as_ref()
.map_or_else(|| "operator".to_owned(), std::string::ToString::to_string);
let (resp, retries) = self
.dispatch(hive_core_agent_sock::Request::Ask {
question: question.clone(),
options: args.options,
multi: args.multi,
ttl_seconds: args.ttl_seconds,
to,
})
.await;
let s = match resp {
Ok(hive_core_agent_sock::Response::QuestionQueued { id }) => {
// Best-effort local questions-mirror record — a dial
// failure just means `get_loose_ends` won't show this
// row locally; the actual question is already queued
// in c0re regardless.
let _ = dial_agent_socket(&hive_agent_sock::Request::RecordAskedQuestion {
id,
target,
question,
})
.await;
format!(
"question queued (id={id}); answer will arrive as a system \
`question_answered` event in your inbox"
)
}
other => reply_err(other, "ask"),
};
annotate_retries(s, retries)
})
.await
}
#[tool(
description = "Answer a question that was routed to YOU via a `question_asked` system \
event in your inbox. Pass the `id` from that event and your `answer` string. The \
answer will surface in the asker's inbox as a `question_answered { id, question, \
answer, answerer: <your-name> }` event. \n\n\
Authorisation is strict you can only answer questions where you are the declared \
target (i.e. the asker did `ask(to: \"<your-name>\", ...)`). Trying to answer an \
operator-targeted question or a question addressed to a different agent will fail."
)]
async fn answer(&self, Parameters(args): Parameters<AnswerArgs>) -> String {
let log = format!("{args:?}");
let id = args.id;
run_tool_envelope("answer", log, async move {
let (resp, retries) = self
.dispatch(hive_core_agent_sock::Request::Answer {
id,
answer: args.answer,
})
.await;
// Clear the local `answering` mirror row whenever this agent is
// done owing a reply for `id`: either a genuine success, or
// c0re telling us the question already resolved without us
// (the asker cancelled/answered it first, surfacing as an
// "already answered"/"not found" rejection) — in both cases
// nothing is still owed, so the row would otherwise linger
// stale. Any other rejection (e.g. wrong answerer) means the
// question is still genuinely outstanding, so leave it be.
let should_clear = match &resp {
Ok(hive_core_agent_sock::Response::Ok) => true,
Ok(hive_core_agent_sock::Response::Err { message }) => {
message.contains("not found") || message.contains("already answered")
}
_ => false,
};
if should_clear {
let _ = dial_agent_socket(&hive_agent_sock::Request::ClearQuestion { id }).await;
}
annotate_retries(
format_ack(resp, "answer", format!("answered question {id}")),
retries,
)
})
.await
}
#[tool(
description = "Pop messages from this agent's inbox. Returns one or more messages, or \
an empty marker if nothing is waiting. Always an immediate 'anything pending?' peek \