ag3nt: manager MCP tools + prompt for scheduled prompts (#444)
This commit is contained in:
parent
002419ba4d
commit
2593896383
3 changed files with 138 additions and 1 deletions
|
|
@ -11,6 +11,9 @@ Tools (hyperhive surface):
|
|||
- `mcp__hyperhive__restart(name)` — stop + start a sub-agent. No approval required.
|
||||
- `mcp__hyperhive__update(name)` — rebuild a sub-agent (re-applies the current hyperhive flake + agent.nix, restarts the container). No approval required — idempotent. Use when you receive a `needs_update` system event.
|
||||
- `mcp__hyperhive__request_update_meta_inputs(inputs?, description?)` — queue an approval for the operator to run `nix flake update [inputs...]` on the meta flake. Pass specific input names (e.g. `["bitburner-agent"]`) or omit / pass `[]` for all inputs. Returns immediately; lock update runs on operator approval. Does NOT trigger rebuilds — call `update(name)` on affected agents after approval resolves.
|
||||
- `mcp__hyperhive__request_schedule_prompt(targets, body, first_fire_at_unix, interval_seconds?, description?)` — queue an approval for the operator to add a scheduled prompt. On approve hive-c0re inserts a schedule row and the worker fans `body` out to each agent in `targets` at `first_fire_at_unix` (recurring every `interval_seconds` if set, one-shot when absent). Even self-targeted schedules go through approval — the existing `remind` tool stays the quick no-approval self-wake path. Catch-up clamp: long downtime fires ONCE per recurring row on resume (skipped count surfaces in per-target `last_result`), not N stacked pulses.
|
||||
- `mcp__hyperhive__cancel_schedule(id, targets?)` — cancel a schedule. Omit `targets` / pass empty to cancel the whole schedule; pass a list to cancel just those recipients (the schedule keeps firing for any remaining active targets, auto-cancels when every target is gone). Authorization: you can cancel schedules you own OR any owned by a sub-agent in your subtree per topology.json.
|
||||
- `mcp__hyperhive__list_schedules()` — snapshot every schedule in the queue (active + cancelled-but-not-reaped). Returns id, owner, body, target set with per-target `last_fired_at` + `last_result`, `next_fire_at_unix`, recurring `interval_seconds`. Use to look up an id before cancelling, or to audit upcoming wake-ups across the swarm.
|
||||
- `mcp__hyperhive__get_logs(agent, lines?)` — fetch recent journal lines for a sub-agent container. Use to diagnose MCP-server registration failures, startup crashes, or harness issues you can't see from inside. Pass the plain logical agent name; `lines` defaults to 50 (capped at 500).
|
||||
- `mcp__hyperhive__ask(question, options?, multi?, ttl_seconds?, to?)` — surface a structured question to the operator (default, or `to: "operator"`) OR a sub-agent (`to: "<agent-name>"`). Returns immediately with a question id; the answer arrives later as a system `question_answered { id, question, answer, answerer }` event in your inbox. Options are advisory: the dashboard always lets the operator type a free-text answer in addition. Set `multi: true` to render options as checkboxes (operator can pick multiple); the answer comes back as `, `-separated. Set `ttl_seconds` to auto-cancel after a deadline (capped at 6h server-side) — on expiry the answer is `[expired]` and `answerer` is `"ttl-watchdog"`. Do not poll inside the same turn — finish the current work and react when the event lands.
|
||||
- `mcp__hyperhive__answer(id, answer)` — answer a question that was routed to YOU (a sub-agent did `ask(to: "manager", ...)`). The triggering event in your inbox is `question_asked { id, asker, question, options, multi }`. The answer surfaces in the asker's inbox as a `question_answered` event.
|
||||
|
|
|
|||
|
|
@ -179,7 +179,8 @@ async fn serve(
|
|||
| ManagerResponse::LooseEnds { .. }
|
||||
| ManagerResponse::PendingRemindersCount { .. }
|
||||
| ManagerResponse::ReminderRollup { .. }
|
||||
| ManagerResponse::AgentMeta { .. },
|
||||
| ManagerResponse::AgentMeta { .. }
|
||||
| ManagerResponse::Schedules { .. },
|
||||
) => {
|
||||
tracing::warn!("recv produced unexpected response kind");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ pub enum SocketReply {
|
|||
QuestionQueued(i64),
|
||||
Recent(Vec<hive_sh4re::InboxRow>),
|
||||
Logs(String),
|
||||
/// `list_schedules` result — used by the manager surface only;
|
||||
/// AgentResponse has no equivalent variant.
|
||||
Schedules(Vec<hive_sh4re::WireSchedule>),
|
||||
LooseEnds(Vec<hive_sh4re::LooseEnd>),
|
||||
PendingRemindersCount(u64),
|
||||
ReminderRollup(hive_sh4re::ReminderStats),
|
||||
|
|
@ -102,6 +105,7 @@ impl From<hive_sh4re::ManagerResponse> for SocketReply {
|
|||
hive_sh4re::ManagerResponse::QuestionQueued { id } => Self::QuestionQueued(id),
|
||||
hive_sh4re::ManagerResponse::Recent { rows } => Self::Recent(rows),
|
||||
hive_sh4re::ManagerResponse::Logs { content } => Self::Logs(content),
|
||||
hive_sh4re::ManagerResponse::Schedules { schedules } => Self::Schedules(schedules),
|
||||
hive_sh4re::ManagerResponse::LooseEnds { loose_ends } => Self::LooseEnds(loose_ends),
|
||||
hive_sh4re::ManagerResponse::PendingRemindersCount { count } => {
|
||||
Self::PendingRemindersCount(count)
|
||||
|
|
@ -929,6 +933,46 @@ pub struct UpdateMetaInputsArgs {
|
|||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct RequestSchedulePromptArgs {
|
||||
/// Recipient agents — one schedule fires to many inboxes at the
|
||||
/// scheduled time. `operator` is a legitimate target (mara: "we
|
||||
/// want to get rid of the manager special case so yes manager
|
||||
/// can be recipient" — the operator slot follows the same rule).
|
||||
pub targets: Vec<String>,
|
||||
/// Message body delivered to each target's inbox at fire time.
|
||||
/// Same size budget as `send` bodies.
|
||||
pub body: String,
|
||||
/// Absolute unix timestamp (seconds) for the FIRST fire. For
|
||||
/// recurring schedules the worker re-arms in
|
||||
/// `interval_seconds` steps from this point on.
|
||||
pub first_fire_at_unix: i64,
|
||||
/// `None` / absent = one-shot. `Some(n > 0)` = recurring every
|
||||
/// `n` seconds. The worker clamps catch-up so a long downtime
|
||||
/// fires ONCE on resume (skipped-cycle count surfaces in the
|
||||
/// per-target last_result), not N delayed pulses in a row.
|
||||
#[serde(default)]
|
||||
pub interval_seconds: Option<u64>,
|
||||
/// Optional description shown on the dashboard approval card +
|
||||
/// preserved on the schedule row for later operator reference.
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct CancelScheduleArgs {
|
||||
/// Schedule id from a prior `list_schedules` call or the
|
||||
/// approval-resolved event for a `request_schedule_prompt`.
|
||||
pub id: i64,
|
||||
/// Optional target list. `None` / empty = cancel the entire
|
||||
/// schedule. `Some(["alice", "bob"])` = cancel just those
|
||||
/// recipients (the schedule keeps firing for any remaining
|
||||
/// active targets, and auto-cancels its parent row when every
|
||||
/// target is gone).
|
||||
#[serde(default)]
|
||||
pub targets: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct GetLogsArgs {
|
||||
/// Logical agent name to fetch logs for (e.g. `gui`, `hm1nd`).
|
||||
|
|
@ -1181,6 +1225,95 @@ impl ManagerServer {
|
|||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Queue an approval to add a scheduled prompt — one body delivered to \
|
||||
N agent inboxes at a target time, optionally recurring every `interval_seconds`. \
|
||||
The operator approves; on approve hive-c0re inserts the schedule and the worker \
|
||||
fans it out. Even self-targeted schedules go through this flow (the operator pays \
|
||||
for the wake-up tokens); the existing `remind` MCP tool stays the quick \
|
||||
no-approval self-wake path. \n\n\
|
||||
Catch-up clamp: if hive-c0re is down across multiple intervals, only ONE delayed \
|
||||
fire happens on resume (per recurring schedule). The skipped-cycle count surfaces \
|
||||
in the per-target `last_result` for the operator's audit trail. \n\n\
|
||||
Per-target failure: a target name that doesn't resolve to a live agent → operator \
|
||||
gets a one-line advisory `Message` from `system`; the schedule keeps firing for \
|
||||
the other (live) targets."
|
||||
)]
|
||||
async fn request_schedule_prompt(
|
||||
&self,
|
||||
Parameters(args): Parameters<RequestSchedulePromptArgs>,
|
||||
) -> String {
|
||||
let log = format!("{args:?}");
|
||||
run_tool_envelope("request_schedule_prompt", log, async move {
|
||||
let target_count = args.targets.len();
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::ManagerRequest::RequestSchedulePrompt(
|
||||
hive_sh4re::SchedulePromptPayload {
|
||||
targets: args.targets,
|
||||
body: args.body,
|
||||
first_fire_at_unix: args.first_fire_at_unix,
|
||||
interval_seconds: args.interval_seconds,
|
||||
description: args.description,
|
||||
},
|
||||
))
|
||||
.await;
|
||||
annotate_retries(
|
||||
format_ack(
|
||||
resp,
|
||||
"request_schedule_prompt",
|
||||
format!("approval queued: {target_count} target(s)"),
|
||||
),
|
||||
retries,
|
||||
)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Cancel a scheduled prompt. With no `targets` field, cancels the \
|
||||
whole schedule (all recipients flipped). With a non-empty `targets` list, cancels \
|
||||
just those recipients; the schedule keeps firing for any remaining active targets \
|
||||
and auto-cancels its parent row when every target is cancelled. \n\n\
|
||||
Authorization: the manager can cancel its own schedules + any schedule whose \
|
||||
owner is one of its sub-agents per topology.json. Other owners are refused."
|
||||
)]
|
||||
async fn cancel_schedule(&self, Parameters(args): Parameters<CancelScheduleArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
run_tool_envelope("cancel_schedule", log, async move {
|
||||
let id = args.id;
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::ManagerRequest::CancelSchedule {
|
||||
id: args.id,
|
||||
targets: args.targets,
|
||||
})
|
||||
.await;
|
||||
annotate_retries(format_ack(resp, "cancel_schedule", format!("cancelled #{id}")), retries)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "List every scheduled prompt in the queue (active + cancelled but \
|
||||
not yet reaped). Returns the full snapshot — schedule id, owner, body, target set \
|
||||
with per-target last_fired_at + last_result, next fire time, recurring interval. \
|
||||
Use this to look up an id before calling `cancel_schedule`, or to audit what \
|
||||
the swarm is going to be woken up about next."
|
||||
)]
|
||||
async fn list_schedules(&self) -> String {
|
||||
run_tool_envelope("list_schedules", String::new(), async move {
|
||||
let (resp, retries) = self.dispatch(hive_sh4re::ManagerRequest::ListSchedules).await;
|
||||
let body = match resp {
|
||||
Ok(SocketReply::Schedules(schedules)) => serde_json::to_string(&schedules)
|
||||
.unwrap_or_else(|e| format!("list_schedules: serialise: {e:#}")),
|
||||
Ok(SocketReply::Err(m)) => format!("list_schedules: {m}"),
|
||||
Ok(other) => format!("list_schedules unexpected response: {other:?}"),
|
||||
Err(e) => format!("list_schedules transport error: {e:#}"),
|
||||
};
|
||||
annotate_retries(body, retries)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Surface a structured question to either the operator OR a sub-agent. \
|
||||
Returns immediately with a question id — do NOT wait inline. When the recipient \
|
||||
|
|
|
|||
Loading…
Reference in a new issue