feat: hint to do other work when recv/bash_status waits time out
When an agent parks on a long-poll that expires with nothing to show, nudge it to spend the idle time on other useful work instead of immediately re-blocking on the same call. - recv: when wait_seconds > 0 and the inbox is empty at timeout, the '(empty)' result now carries IDLE_WAIT_HINT. Immediate peeks (no/zero wait) are unchanged. - bash_status: when wait_seconds > 0 and the task is still pending/running at timeout, append BASH_IDLE_WAIT_HINT. Finished tasks and no-wait calls are unchanged. Both thread a 'waited' flag into the formatter so the hint only fires on an actual wait-timeout. Unit tests cover both. Implements #1411.
This commit is contained in:
parent
0749cb37ab
commit
d34c594f2b
2 changed files with 94 additions and 13 deletions
|
|
@ -160,14 +160,16 @@ pub fn format_ack(resp: Result<SocketReply, anyhow::Error>, tool: &str, ok_msg:
|
|||
|
||||
/// Format helper for `recv`: renders zero, one, or many popped
|
||||
/// messages. Empty list collapses to "(empty)" so claude doesn't go
|
||||
/// hunting for content. A single message renders as the historical
|
||||
/// `from: X\n\nbody` block (banner first if `redelivered`). A
|
||||
/// multi-message batch renders with a `popped N message(s):` header
|
||||
/// and `---` separators between bodies so the model can tell where
|
||||
/// one ends and the next begins; per-message redelivery banners
|
||||
/// included.
|
||||
/// hunting for content; when `waited` is set (the call parked on a
|
||||
/// long-poll that timed out) the empty result also carries
|
||||
/// [`IDLE_WAIT_HINT`] nudging the model toward other work. A single
|
||||
/// message renders as the historical `from: X\n\nbody` block (banner
|
||||
/// first if `redelivered`). A multi-message batch renders with a
|
||||
/// `popped N message(s):` header and `---` separators between bodies
|
||||
/// so the model can tell where one ends and the next begins;
|
||||
/// per-message redelivery banners included.
|
||||
#[must_use]
|
||||
pub fn format_recv(resp: Result<SocketReply, anyhow::Error>) -> String {
|
||||
pub fn format_recv(resp: Result<SocketReply, anyhow::Error>, waited: bool) -> String {
|
||||
use std::fmt::Write as _;
|
||||
let messages = match resp {
|
||||
Ok(SocketReply::Messages(m)) => m,
|
||||
|
|
@ -176,7 +178,11 @@ pub fn format_recv(resp: Result<SocketReply, anyhow::Error>) -> String {
|
|||
Err(e) => return format!("recv transport error: {e:#}"),
|
||||
};
|
||||
if messages.is_empty() {
|
||||
return "(empty)".to_owned();
|
||||
return if waited {
|
||||
format!("(empty){IDLE_WAIT_HINT}")
|
||||
} else {
|
||||
"(empty)".to_owned()
|
||||
};
|
||||
}
|
||||
if messages.len() == 1 {
|
||||
let m = &messages[0];
|
||||
|
|
@ -202,6 +208,14 @@ pub fn format_recv(resp: Result<SocketReply, anyhow::Error>) -> String {
|
|||
/// in-turn `recv` tool result so claude sees the warning either way.
|
||||
pub const REDELIVERY_HINT: &str = "[redelivered after harness restart — may already be handled]\n";
|
||||
|
||||
/// Appended to the `recv` empty result when the agent parked on a
|
||||
/// long-poll (`wait_seconds > 0`) that timed out with nothing new.
|
||||
/// Nudges the model to spend the idle time on other useful work
|
||||
/// instead of immediately re-blocking on `recv`.
|
||||
pub const IDLE_WAIT_HINT: &str = " — nothing arrived before the wait timed out. \
|
||||
If you have other useful work (assigned issues, in-flight PRs, a docs sweep, \
|
||||
notes to update), do that now rather than immediately parking on recv again.";
|
||||
|
||||
/// Inner renderer for a `Vec<LooseEnd>` already extracted from the
|
||||
/// socket reply. Called by both `format_loose_ends` (which handles the
|
||||
/// `Result<SocketReply>` wrapper) and the augmented `get_loose_ends`
|
||||
|
|
@ -723,13 +737,14 @@ impl AgentServer {
|
|||
async fn recv(&self, Parameters(args): Parameters<RecvArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
run_tool_envelope("recv", log, async move {
|
||||
let waited = args.wait_seconds.is_some_and(|w| w > 0);
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::Recv {
|
||||
wait_seconds: args.wait_seconds,
|
||||
max: args.max,
|
||||
})
|
||||
.await;
|
||||
annotate_retries(format_recv(resp), retries)
|
||||
annotate_retries(format_recv(resp, waited), retries)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
|
@ -2043,3 +2058,21 @@ pub fn render_claude_config(agent_binary: &str, socket: &std::path::Path) -> Str
|
|||
let config = serde_json::json!({ "mcpServers": servers });
|
||||
serde_json::to_string_pretty(&config).unwrap_or_else(|_| "{}".into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod recv_hint_tests {
|
||||
use super::{IDLE_WAIT_HINT, SocketReply, format_recv};
|
||||
|
||||
#[test]
|
||||
fn empty_recv_after_wait_appends_idle_hint() {
|
||||
let out = format_recv(Ok(SocketReply::Messages(vec![])), true);
|
||||
assert!(out.starts_with("(empty)"));
|
||||
assert!(out.contains(IDLE_WAIT_HINT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_recv_without_wait_has_no_hint() {
|
||||
let out = format_recv(Ok(SocketReply::Messages(vec![])), false);
|
||||
assert_eq!(out, "(empty)");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,11 +119,27 @@ fn render_bash_run(id: &str, resp: Result<DaemonResponse>) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
/// Appended to a `status` result when the caller parked on a
|
||||
/// `wait_seconds` poll that expired while the task was still running —
|
||||
/// nudges the model to spend the idle time on other work instead of
|
||||
/// immediately re-waiting on the same task.
|
||||
const BASH_IDLE_WAIT_HINT: &str = "\n\nThe task is still running — your wait timed out before it \
|
||||
finished. If you have other useful work, do that and check back later (the task keeps running, and \
|
||||
a wake fires when it completes) rather than immediately re-waiting.";
|
||||
|
||||
/// Turn a `DaemonResponse` from a `BashStatus` call into the string
|
||||
/// claude sees as the tool result.
|
||||
fn render_bash_status(id: &str, resp: Result<DaemonResponse>) -> String {
|
||||
/// claude sees as the tool result. When `waited` is set (the call
|
||||
/// parked on a `wait_seconds` poll) and the task is still non-terminal,
|
||||
/// [`BASH_IDLE_WAIT_HINT`] is appended.
|
||||
fn render_bash_status(id: &str, resp: Result<DaemonResponse>, waited: bool) -> String {
|
||||
match resp {
|
||||
Ok(DaemonResponse::Ok { payload }) => format_task(id, &payload),
|
||||
Ok(DaemonResponse::Ok { payload }) => {
|
||||
let mut out = format_task(id, &payload);
|
||||
if waited && matches!(payload["status"].as_str(), Some("pending" | "running")) {
|
||||
out.push_str(BASH_IDLE_WAIT_HINT);
|
||||
}
|
||||
out
|
||||
}
|
||||
Ok(DaemonResponse::Error { message }) => message,
|
||||
Err(e) => format!("bash bridge error: {e:#}"),
|
||||
}
|
||||
|
|
@ -215,11 +231,12 @@ impl BashMcp {
|
|||
)]
|
||||
async fn status(&self, Parameters(args): Parameters<BashStatusArgs>) -> String {
|
||||
let id = args.id.clone();
|
||||
let waited = args.wait_seconds.is_some_and(|w| w > 0);
|
||||
let req = DaemonRequest::BashStatus {
|
||||
id: args.id,
|
||||
wait_seconds: args.wait_seconds,
|
||||
};
|
||||
render_bash_status(&id, round_trip(req).await)
|
||||
render_bash_status(&id, round_trip(req).await, waited)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -240,3 +257,34 @@ async fn main() -> Result<()> {
|
|||
service.waiting().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod status_hint_tests {
|
||||
use super::{BASH_IDLE_WAIT_HINT, render_bash_status};
|
||||
use anyhow::Result;
|
||||
use hive_bash_mcp::protocol::DaemonResponse;
|
||||
|
||||
fn ok(status: &str) -> Result<DaemonResponse> {
|
||||
Ok(DaemonResponse::Ok {
|
||||
payload: serde_json::json!({ "status": status, "started_at": 1 }),
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn running_task_after_wait_appends_idle_hint() {
|
||||
let out = render_bash_status("t1", ok("running"), true);
|
||||
assert!(out.contains(BASH_IDLE_WAIT_HINT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn running_task_without_wait_has_no_hint() {
|
||||
let out = render_bash_status("t1", ok("running"), false);
|
||||
assert!(!out.contains(BASH_IDLE_WAIT_HINT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finished_task_after_wait_has_no_hint() {
|
||||
let out = render_bash_status("t1", ok("done"), true);
|
||||
assert!(!out.contains(BASH_IDLE_WAIT_HINT));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue