harness: add request_next_turn MCP tool — immediate self-continuation (closes #216)

This commit is contained in:
damocles 2026-05-22 00:35:13 +02:00
parent de6ff3da29
commit c99261b042
3 changed files with 68 additions and 0 deletions

View file

@ -11,6 +11,7 @@ Tools (hyperhive surface):
- `mcp__hyperhive__cancel_loose_end(kind, id)` — cancel one of your own open threads. `kind` is `"question"` (the asker — you, in this case — gets a `[cancelled by <you>]` answer so the waiter unblocks) or `"reminder"` (hard-deleted before it fires). `id` from the matching `get_loose_ends` row or the original submission reply. - `mcp__hyperhive__cancel_loose_end(kind, id)` — cancel one of your own open threads. `kind` is `"question"` (the asker — you, in this case — gets a `[cancelled by <you>]` answer so the waiter unblocks) or `"reminder"` (hard-deleted before it fires). `id` from the matching `get_loose_ends` row or the original submission reply.
- `mcp__hyperhive__remind(message, delay_seconds? | at_unix_timestamp?, file_path?)` — schedule a message to land in your *own* inbox at a future time (sender shows as `reminder`). Set exactly one of `delay_seconds` (relative) or `at_unix_timestamp` (absolute). Use for self-paced follow-ups instead of blocking a whole turn on a long `recv` wait. A large `message` auto-spills to a file under `/agents/{label}/state/reminders/`; pass `file_path` to point at one yourself. Each agent's pending-reminder count is capped (default 50) — the tool will error if the cap is already reached. - `mcp__hyperhive__remind(message, delay_seconds? | at_unix_timestamp?, file_path?)` — schedule a message to land in your *own* inbox at a future time (sender shows as `reminder`). Set exactly one of `delay_seconds` (relative) or `at_unix_timestamp` (absolute). Use for self-paced follow-ups instead of blocking a whole turn on a long `recv` wait. A large `message` auto-spills to a file under `/agents/{label}/state/reminders/`; pass `file_path` to point at one yourself. Each agent's pending-reminder count is capped (default 50) — the tool will error if the cap is already reached.
- `mcp__hyperhive__whoami()` — self-introspection: returns your canonical agent name (from socket identity, not the prompt-substituted label), role, and current hyperhive rev. No args. Use it when you want a trustworthy identity stamp for state files, commit messages, or cross-agent attribution that won't drift across renames. - `mcp__hyperhive__whoami()` — self-introspection: returns your canonical agent name (from socket identity, not the prompt-substituted label), role, and current hyperhive rev. No args. Use it when you want a trustworthy identity stamp for state files, commit messages, or cross-agent attribution that won't drift across renames.
- `mcp__hyperhive__request_next_turn()` — ask the harness to start another turn immediately after this one ends, even if the inbox is empty. Use for multi-turn tasks (long builds, sequential steps) where you want to continue without waiting for an external message. The next turn starts with `from: "self"` and `body: "continue"`. No-op if new inbox messages arrive before this turn ends (the harness already loops immediately on pending messages). No args.
Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — message the manager (recipient `manager`) describing what you need + why. The manager evaluates the request (it doesn't rubber-stamp), edits `/agents/{label}/config/agent.nix` on your behalf, commits, and submits an approval that the operator can accept on the dashboard; on approve hive-c0re rebuilds your container with the new config. Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — message the manager (recipient `manager`) describing what you need + why. The manager evaluates the request (it doesn't rubber-stamp), edits `/agents/{label}/config/agent.nix` on your behalf, commits, and submits an approval that the operator can accept on the dashboard; on approve hive-c0re rebuilds your container with the new config.

View file

@ -273,6 +273,11 @@ async fn serve(
if pending > 0 { if pending > 0 {
tracing::info!(%pending, "pending messages after turn; fetching next"); tracing::info!(%pending, "pending messages after turn; fetching next");
} }
// `request_next_turn` MCP tool: agent wrote a sentinel
// requesting an immediate self-continuation turn. Clear
// the file and inject a synthetic wake so the outer loop
// fires a bare turn even if the inbox is empty.
check_and_inject_continue(socket, label).await;
} }
Ok(AgentResponse::Messages { .. }) => { Ok(AgentResponse::Messages { .. }) => {
// Idle: empty list = nothing pending. Brief sleep // Idle: empty list = nothing pending. Brief sleep
@ -408,3 +413,41 @@ async fn fetch_agent_post_turn_counts(socket: &Path) -> (Option<u64>, Option<u64
(threads, reminders) (threads, reminders)
} }
/// Check for the `request_next_turn` sentinel file. If present, remove it
/// and inject a synthetic `from: "self", body: "continue"` message so the
/// serve loop fires an immediate follow-up turn even when the inbox is empty.
/// Best-effort: any I/O error is logged and ignored (the agent just waits
/// for a real message as normal).
async fn check_and_inject_continue(socket: &Path, label: &str) {
let sentinel = hive_ag3nt::paths::state_dir().join("hyperhive-continue");
if !sentinel.exists() {
return;
}
if let Err(e) = std::fs::remove_file(&sentinel) {
tracing::warn!(error = %e, "check_and_inject_continue: remove sentinel failed");
return;
}
// Sentinel was present: inject a wake so the outer loop fires immediately.
// Route through the `Wake` request which is already wired in agent_server.
let res = client::request::<_, AgentResponse>(
socket,
&AgentRequest::Wake {
from: "self".into(),
body: "continue".into(),
},
)
.await;
match res {
Ok(AgentResponse::Ok) => {
tracing::info!(%label, "request_next_turn: injected self-continue wake");
}
Ok(AgentResponse::Err { message }) => {
tracing::warn!(%message, "check_and_inject_continue: wake rejected");
}
Err(e) => {
tracing::warn!(error = ?e, "check_and_inject_continue: wake transport error");
}
_ => {}
}
}

View file

@ -626,6 +626,30 @@ impl AgentServer {
}) })
.await .await
} }
#[tool(
description = "Ask the harness to start another turn immediately after this one \
completes, even if the inbox is empty. Use this when you have ongoing work that \
spans multiple turns (long builds, multi-step tasks) and you want to continue \
without waiting for an external message. The next turn will start with \
`from: \"self\"` and `body: \"continue\"`. Has no effect if a new inbox message \
arrives before this turn ends the harness already loops immediately on pending \
messages. No args."
)]
async fn request_next_turn(&self) -> String {
run_tool_envelope("request_next_turn", String::new(), async move {
let sentinel = crate::paths::state_dir().join("hyperhive-continue");
match std::fs::write(&sentinel, b"") {
Ok(()) => "ok — harness will start another turn immediately after this one",
Err(e) => {
tracing::warn!(error = %e, path = %sentinel.display(), "request_next_turn: write failed");
return format!("request_next_turn failed: {e}");
}
}
.to_string()
})
.await
}
} }
#[tool_handler( #[tool_handler(