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

@ -273,6 +273,11 @@ async fn serve(
if pending > 0 {
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 { .. }) => {
// 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)
}
/// 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
}
#[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(