agent mcp: expose 'remind' tool for self-scheduled wakes
This commit is contained in:
parent
271c524e66
commit
f2484b5e78
2 changed files with 70 additions and 2 deletions
3
TODO.md
3
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.
|
- ~~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.
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue