From f2484b5e780706ae70a979934dfbfa97b7506d28 Mon Sep 17 00:00:00 2001 From: damocles Date: Sun, 17 May 2026 10:54:36 +0200 Subject: [PATCH] agent mcp: expose 'remind' tool for self-scheduled wakes --- TODO.md | 3 +- hive-ag3nt/src/mcp.rs | 69 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index f7a3c6c..04aa69f 100644 --- a/TODO.md +++ b/TODO.md @@ -13,7 +13,8 @@ - ~~Handle text overflow → suggest file_path option for long messages~~ ✓ fixed — Remind dispatch rejects `message.len() > 4096` (when no `file_path` was supplied) with an error pointing at the `file_path` escape hatch. - Per-agent reminder limits (burst capacity, rate limiting) -- **Expose `remind` MCP tool**: wire protocol exists (`AgentRequest::Remind`) and the broker handles it, but no `#[tool]` method on `AgentServer` actually surfaces it to claude. Until that lands, the Remind path is unreachable from agent turns. +- ~~**Expose `remind` MCP tool**~~ ✓ fixed — `mcp__hyperhive__remind` now on `AgentServer`; takes `message`, exactly one of `delay_seconds` / `at_unix_timestamp`, optional `file_path`. Manager surface still missing (no `ManagerRequest::Remind` variant) — separate item below. +- **Manager-side `remind`**: mirror of the agent tool but on `ManagerServer`. Needs `ManagerRequest::Remind` variant in hive-sh4re, dispatch in manager_server.rs, MCP tool wiring. - **File path delivery**: currently unused in scheduler delivery loop — implement file write/delivery to /state//reminders/ or similar (also needed for the overflow-check escape hatch above to actually do anything useful). - ~~**Orphan reminders**~~ ✓ fixed — `Broker::deliver_reminder` wraps the inbox INSERT + reminders UPDATE in one sqlite transaction; partial failure can no longer cause duplicate delivery on the next tick. - ~~**Unbounded batches**~~ ✓ fixed — scheduler now calls `get_due_reminders(REMINDER_BATCH_LIMIT)` (cap = 100/tick); overflow stays due and gets picked up next cycle. diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index 542abfb..384a126 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -144,6 +144,34 @@ pub struct RecvArgs { pub wait_seconds: Option, } +/// MCP tool args for `remind`. Exactly one of `delay_seconds` or +/// `at_unix_timestamp` must be set; both / neither is a tool-side error. +/// Hides the tagged `ReminderTiming` enum behind a flatter schema so the +/// model picks one field instead of building `{"timing_type": "in_seconds", +/// "seconds": 60}` shaped objects. +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +pub struct RemindArgs { + /// Body that lands in your inbox when the reminder fires (sender + /// will appear as `reminder`). Capped at 4096 bytes when + /// `file_path` is unset — anything bigger should be persisted to + /// disk and pointed at via `file_path`. + pub message: String, + /// Fire `delay_seconds` from now (relative). Set this OR + /// `at_unix_timestamp`, not both. + #[serde(default)] + pub delay_seconds: Option, + /// Fire at this absolute unix timestamp (seconds since epoch). Set + /// this OR `delay_seconds`, not both. + #[serde(default)] + pub at_unix_timestamp: Option, + /// Optional path to a file the scheduler should reference instead of + /// inlining a long `message`. Use this for large payloads (research + /// notes, file lists, intermediate state). Path must be reachable from + /// the agent's container — typically under `/agents//state/`. + #[serde(default)] + pub file_path: Option, +} + /// Per-agent tool surface. Holds the socket path so each tool call doesn't /// re-derive it; the socket itself is the per-container `/run/hive/mcp.sock`. #[derive(Debug, Clone)] @@ -254,12 +282,51 @@ impl AgentServer { }) .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 \ + status in 60s', 'retry failed deploy at 14:00 UTC', 'nudge me when the operator's \ + deploy window opens'. Set EXACTLY ONE of `delay_seconds` (fire N seconds from now) \ + or `at_unix_timestamp` (fire at absolute epoch second). Body is capped at 4096 bytes \ + when `file_path` is unset; for larger payloads write them to a file under your \ + `/agents//state/` dir and pass the path in `file_path`. Returns immediately — \ + the reminder lives in the broker until due." + )] + async fn remind(&self, Parameters(args): Parameters) -> String { + let log = format!("{args:?}"); + run_tool_envelope("remind", log, async move { + let timing = match (args.delay_seconds, args.at_unix_timestamp) { + (Some(_), Some(_)) => { + return "remind failed: pass exactly one of `delay_seconds` or \ + `at_unix_timestamp`, not both" + .to_string(); + } + (None, None) => { + return "remind failed: pass exactly one of `delay_seconds` or \ + `at_unix_timestamp`" + .to_string(); + } + (Some(s), None) => hive_sh4re::ReminderTiming::InSeconds { seconds: s }, + (None, Some(t)) => hive_sh4re::ReminderTiming::At { unix_timestamp: t }, + }; + let (resp, retries) = self + .dispatch(hive_sh4re::AgentRequest::Remind { + message: args.message, + timing, + file_path: args.file_path, + }) + .await; + annotate_retries(format_ack(resp, "remind", "reminder scheduled".to_string()), retries) + }) + .await + } } #[tool_handler( instructions = "You are a hyperhive agent. Use `send` to talk to peers (by their logical \ name) or to the operator (recipient `operator`). Use `recv` to drain your inbox one \ - message at a time." + message at a time. Use `remind` to schedule a future wake-up message for yourself." )] impl ServerHandler for AgentServer {}