agent mcp: expose 'remind' tool for self-scheduled wakes

This commit is contained in:
damocles 2026-05-17 10:54:36 +02:00
parent 271c524e66
commit f2484b5e78
2 changed files with 70 additions and 2 deletions

View file

@ -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. - ~~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) - 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/<agent>/reminders/ or similar (also needed for the overflow-check escape hatch above to actually do anything useful). - **File path delivery**: currently unused in scheduler delivery loop — implement file write/delivery to /state/<agent>/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. - ~~**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. - ~~**Unbounded batches**~~ ✓ fixed — scheduler now calls `get_due_reminders(REMINDER_BATCH_LIMIT)` (cap = 100/tick); overflow stays due and gets picked up next cycle.

View file

@ -144,6 +144,34 @@ pub struct RecvArgs {
pub wait_seconds: Option<u64>, pub wait_seconds: Option<u64>,
} }
/// 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<u64>,
/// Fire at this absolute unix timestamp (seconds since epoch). Set
/// this OR `delay_seconds`, not both.
#[serde(default)]
pub at_unix_timestamp: Option<i64>,
/// 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/<you>/state/`.
#[serde(default)]
pub file_path: Option<String>,
}
/// Per-agent tool surface. Holds the socket path so each tool call doesn't /// 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`. /// re-derive it; the socket itself is the per-container `/run/hive/mcp.sock`.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -254,12 +282,51 @@ impl AgentServer {
}) })
.await .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/<you>/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<RemindArgs>) -> 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( #[tool_handler(
instructions = "You are a hyperhive agent. Use `send` to talk to peers (by their logical \ 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 \ 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 {} impl ServerHandler for AgentServer {}