broker: recv_batch(max) — drain a bursty inbox in one round-trip
This commit is contained in:
parent
96ffb0e39a
commit
77b89bf2c6
9 changed files with 354 additions and 11 deletions
|
|
@ -265,6 +265,7 @@ async fn serve(
|
|||
Ok(
|
||||
AgentResponse::Ok
|
||||
| AgentResponse::Status { .. }
|
||||
| AgentResponse::Batch { .. }
|
||||
| AgentResponse::Recent { .. }
|
||||
| AgentResponse::QuestionQueued { .. }
|
||||
| AgentResponse::LooseEnds { .. }
|
||||
|
|
@ -301,7 +302,8 @@ fn format_wake_prompt(from: &str, body: &str, unread: u64, redelivered: bool) ->
|
|||
String::new()
|
||||
} else {
|
||||
format!(
|
||||
"\n\n({unread} more message(s) pending in your inbox — drain via `mcp__hyperhive__recv` if relevant.)"
|
||||
"\n\n({unread} more message(s) pending in your inbox — call `mcp__hyperhive__recv_batch` \
|
||||
with `max: {unread}` to drain them all in one round-trip before acting.)"
|
||||
)
|
||||
};
|
||||
format!("{banner}Incoming message from `{from}`:\n---\n{body}\n---{pending}")
|
||||
|
|
|
|||
|
|
@ -221,6 +221,7 @@ async fn serve(
|
|||
Ok(
|
||||
ManagerResponse::Ok
|
||||
| ManagerResponse::Status { .. }
|
||||
| ManagerResponse::Batch { .. }
|
||||
| ManagerResponse::QuestionQueued { .. }
|
||||
| ManagerResponse::Recent { .. }
|
||||
| ManagerResponse::Logs { .. }
|
||||
|
|
@ -256,7 +257,8 @@ fn format_wake_prompt(from: &str, body: &str, unread: u64, redelivered: bool) ->
|
|||
String::new()
|
||||
} else {
|
||||
format!(
|
||||
"\n\n({unread} more message(s) pending in your inbox — drain via `mcp__hyperhive__recv` if relevant.)"
|
||||
"\n\n({unread} more message(s) pending in your inbox — call `mcp__hyperhive__recv_batch` \
|
||||
with `max: {unread}` to drain them all in one round-trip before acting.)"
|
||||
)
|
||||
};
|
||||
format!("{banner}Incoming message from `{from}`:\n---\n{body}\n---{pending}")
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ pub enum SocketReply {
|
|||
id: i64,
|
||||
redelivered: bool,
|
||||
},
|
||||
/// Result of `recv_batch`: zero or more messages popped in one
|
||||
/// round-trip. Empty vec is the equivalent of `Empty` for a
|
||||
/// single `recv` — the format helper collapses it to "(empty)".
|
||||
Batch(Vec<hive_sh4re::DeliveredMessage>),
|
||||
Empty,
|
||||
Status(u64),
|
||||
QuestionQueued(i64),
|
||||
|
|
@ -77,6 +81,7 @@ impl From<hive_sh4re::AgentResponse> for SocketReply {
|
|||
redelivered,
|
||||
},
|
||||
hive_sh4re::AgentResponse::Empty => Self::Empty,
|
||||
hive_sh4re::AgentResponse::Batch { messages } => Self::Batch(messages),
|
||||
hive_sh4re::AgentResponse::Status { unread } => Self::Status(unread),
|
||||
hive_sh4re::AgentResponse::Recent { rows } => Self::Recent(rows),
|
||||
hive_sh4re::AgentResponse::QuestionQueued { id } => Self::QuestionQueued(id),
|
||||
|
|
@ -114,6 +119,7 @@ impl From<hive_sh4re::ManagerResponse> for SocketReply {
|
|||
redelivered,
|
||||
},
|
||||
hive_sh4re::ManagerResponse::Empty => Self::Empty,
|
||||
hive_sh4re::ManagerResponse::Batch { messages } => Self::Batch(messages),
|
||||
hive_sh4re::ManagerResponse::Status { unread } => Self::Status(unread),
|
||||
hive_sh4re::ManagerResponse::QuestionQueued { id } => Self::QuestionQueued(id),
|
||||
hive_sh4re::ManagerResponse::Recent { rows } => Self::Recent(rows),
|
||||
|
|
@ -179,6 +185,35 @@ pub fn format_recv(resp: Result<SocketReply, anyhow::Error>) -> String {
|
|||
pub const REDELIVERY_HINT: &str =
|
||||
"[redelivered after harness restart — may already be handled]\n";
|
||||
|
||||
/// Format helper for `recv_batch`: renders zero or more popped
|
||||
/// messages as a single string. Empty batch collapses to "(empty)"
|
||||
/// so claude doesn't go hunting for content. Each message is rendered
|
||||
/// in the same `from: <name>\n\n<body>` shape as `format_recv` —
|
||||
/// per-message redelivery banner included — separated by a thin rule
|
||||
/// so the model can tell where one body ends and the next begins.
|
||||
pub fn format_recv_batch(resp: Result<SocketReply, anyhow::Error>) -> String {
|
||||
use std::fmt::Write as _;
|
||||
let messages = match resp {
|
||||
Ok(SocketReply::Batch(m)) => m,
|
||||
Ok(SocketReply::Err(m)) => return format!("recv_batch failed: {m}"),
|
||||
Ok(other) => return format!("recv_batch unexpected response: {other:?}"),
|
||||
Err(e) => return format!("recv_batch transport error: {e:#}"),
|
||||
};
|
||||
if messages.is_empty() {
|
||||
return "(empty)".to_owned();
|
||||
}
|
||||
let n = messages.len();
|
||||
let mut out = format!("popped {n} message(s):\n\n");
|
||||
for (i, m) in messages.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str("\n---\n\n");
|
||||
}
|
||||
let banner = if m.redelivered { REDELIVERY_HINT } else { "" };
|
||||
let _ = write!(out, "{banner}from: {}\n\n{}", m.from, m.body);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Format helper for `get_loose_ends`: renders a short bulleted list
|
||||
/// of pending approvals + questions + reminders. Empty list collapses
|
||||
/// to a clear marker so claude doesn't go hunting for a payload that
|
||||
|
|
@ -338,6 +373,17 @@ pub struct RecvArgs {
|
|||
pub wait_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct RecvBatchArgs {
|
||||
/// Maximum number of messages to pop in this round-trip. Capped
|
||||
/// at 32 server-side; values above the cap clamp silently.
|
||||
/// Returns whatever is currently pending (possibly zero) without
|
||||
/// long-polling — call when you've been told the inbox is busy
|
||||
/// (e.g. the wake prompt mentioned N pending) and want to drain
|
||||
/// them in one tool call instead of N separate `recv` calls.
|
||||
pub max: u32,
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
|
@ -511,6 +557,26 @@ impl AgentServer {
|
|||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Pop up to `max` messages from this agent's inbox in a single round-trip \
|
||||
(no long-poll — returns whatever's pending immediately, possibly zero). Use this \
|
||||
when the wake prompt tells you the inbox has more messages queued, or any time you \
|
||||
know you'll be draining several at once: one tool call beats N consecutive `recv`s. \
|
||||
Per-message redelivery banners + ack bookkeeping work the same as `recv`. `max` \
|
||||
caps at 32 server-side; pass whatever you reasonably expect to handle this turn."
|
||||
)]
|
||||
async fn recv_batch(&self, Parameters(args): Parameters<RecvBatchArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
let max = args.max;
|
||||
run_tool_envelope("recv_batch", log, async move {
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::AgentRequest::RecvBatch { max })
|
||||
.await;
|
||||
annotate_retries(format_recv_batch(resp), retries)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "List loose ends pending against this agent: unanswered questions \
|
||||
where you are the asker (waiting on someone) or the target (someone's waiting on \
|
||||
|
|
@ -819,6 +885,26 @@ impl ManagerServer {
|
|||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Pop up to `max` messages from the manager inbox in a single round-trip \
|
||||
(no long-poll — returns whatever's pending immediately, possibly zero). Use this \
|
||||
when the wake prompt tells you the inbox has more messages queued, or any time you \
|
||||
know you'll be draining several at once: one tool call beats N consecutive `recv`s. \
|
||||
Per-message redelivery banners + ack bookkeeping work the same as `recv`. `max` \
|
||||
caps at 32 server-side; pass whatever you reasonably expect to handle this turn."
|
||||
)]
|
||||
async fn recv_batch(&self, Parameters(args): Parameters<RecvBatchArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
let max = args.max;
|
||||
run_tool_envelope("recv_batch", log, async move {
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::ManagerRequest::RecvBatch { max })
|
||||
.await;
|
||||
annotate_retries(format_recv_batch(resp), retries)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Queue a Spawn approval for a brand-new sub-agent. The operator \
|
||||
approves on the dashboard before the container is actually created."
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue