diff --git a/hive-agent-mcp/src/mcp/args.rs b/hive-agent-mcp/src/mcp/args.rs index 1f828e76..f64ac080 100644 --- a/hive-agent-mcp/src/mcp/args.rs +++ b/hive-agent-mcp/src/mcp/args.rs @@ -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, - /// 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, - /// 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, -} - -#[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 diff --git a/hive-agent-mcp/src/mcp/mod.rs b/hive-agent-mcp/src/mcp/mod.rs index b233e03b..cd8a7c0d 100644 --- a/hive-agent-mcp/src/mcp/mod.rs +++ b/hive-agent-mcp/src/mcp/mod.rs @@ -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: \"\"` 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) -> 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: }` event. \n\n\ - Authorisation is strict — you can only answer questions where you are the declared \ - target (i.e. the asker did `ask(to: \"\", ...)`). Trying to answer an \ - operator-targeted question or a question addressed to a different agent will fail." - )] - async fn answer(&self, Parameters(args): Parameters) -> 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 \