recv: fold batch drain into recv(max) — one tool, uniform list response
This commit is contained in:
parent
77b89bf2c6
commit
5d27ae3048
8 changed files with 271 additions and 417 deletions
|
|
@ -34,23 +34,14 @@ use crate::client;
|
|||
pub enum SocketReply {
|
||||
Ok,
|
||||
Err(String),
|
||||
/// `id` is the broker's row id — not surfaced to claude but
|
||||
/// useful for harness-side bookkeeping (not used in this module
|
||||
/// today; the bin loops drive ack via `AckTurn` instead of
|
||||
/// per-id). `redelivered` triggers the "may already be handled"
|
||||
/// hint in `format_recv` so claude sees it when draining the
|
||||
/// inbox in-turn.
|
||||
Message {
|
||||
from: String,
|
||||
body: String,
|
||||
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,
|
||||
/// Unified `recv` result: zero or more messages popped in one
|
||||
/// round-trip. Empty vec = "(empty)" path; single-message = the
|
||||
/// standard wake body; multi = batch render with per-message
|
||||
/// separators. Per-row `id` is opaque to claude (the bin loops
|
||||
/// drive ack via `AckTurn`, not per-id); `redelivered` triggers
|
||||
/// the "may already be handled" banner in `format_recv` for that
|
||||
/// specific row.
|
||||
Messages(Vec<hive_sh4re::DeliveredMessage>),
|
||||
Status(u64),
|
||||
QuestionQueued(i64),
|
||||
Recent(Vec<hive_sh4re::InboxRow>),
|
||||
|
|
@ -69,19 +60,7 @@ impl From<hive_sh4re::AgentResponse> for SocketReply {
|
|||
match r {
|
||||
hive_sh4re::AgentResponse::Ok => Self::Ok,
|
||||
hive_sh4re::AgentResponse::Err { message } => Self::Err(message),
|
||||
hive_sh4re::AgentResponse::Message {
|
||||
from,
|
||||
body,
|
||||
id,
|
||||
redelivered,
|
||||
} => Self::Message {
|
||||
from,
|
||||
body,
|
||||
id,
|
||||
redelivered,
|
||||
},
|
||||
hive_sh4re::AgentResponse::Empty => Self::Empty,
|
||||
hive_sh4re::AgentResponse::Batch { messages } => Self::Batch(messages),
|
||||
hive_sh4re::AgentResponse::Messages { messages } => Self::Messages(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),
|
||||
|
|
@ -107,19 +86,7 @@ impl From<hive_sh4re::ManagerResponse> for SocketReply {
|
|||
match r {
|
||||
hive_sh4re::ManagerResponse::Ok => Self::Ok,
|
||||
hive_sh4re::ManagerResponse::Err { message } => Self::Err(message),
|
||||
hive_sh4re::ManagerResponse::Message {
|
||||
from,
|
||||
body,
|
||||
id,
|
||||
redelivered,
|
||||
} => Self::Message {
|
||||
from,
|
||||
body,
|
||||
id,
|
||||
redelivered,
|
||||
},
|
||||
hive_sh4re::ManagerResponse::Empty => Self::Empty,
|
||||
hive_sh4re::ManagerResponse::Batch { messages } => Self::Batch(messages),
|
||||
hive_sh4re::ManagerResponse::Messages { messages } => Self::Messages(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),
|
||||
|
|
@ -153,55 +120,30 @@ pub fn format_ack(resp: Result<SocketReply, anyhow::Error>, tool: &str, ok_msg:
|
|||
}
|
||||
}
|
||||
|
||||
/// Format helper for `recv` tools: `Message` → from + body block;
|
||||
/// `Empty` → marker; anything else surfaces as an error. When the
|
||||
/// broker tags the row as `redelivered` (popped before, never acked,
|
||||
/// resurfaced after a harness restart) a short banner is prepended
|
||||
/// so claude knows the side-effects of any previous handling may
|
||||
/// already have happened.
|
||||
/// 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.
|
||||
pub fn format_recv(resp: Result<SocketReply, anyhow::Error>) -> String {
|
||||
match resp {
|
||||
Ok(SocketReply::Message {
|
||||
from,
|
||||
body,
|
||||
redelivered,
|
||||
..
|
||||
}) => {
|
||||
let banner = if redelivered { REDELIVERY_HINT } else { "" };
|
||||
format!("{banner}from: {from}\n\n{body}")
|
||||
}
|
||||
Ok(SocketReply::Empty) => "(empty)".into(),
|
||||
Ok(SocketReply::Err(m)) => format!("recv failed: {m}"),
|
||||
Ok(other) => format!("recv unexpected response: {other:?}"),
|
||||
Err(e) => format!("recv transport error: {e:#}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Header prepended to message bodies that were popped by a prior
|
||||
/// harness session, never acked (turn crash / OOM / restart), and
|
||||
/// resurfaced by `RequeueInflight` on this session's boot. Same
|
||||
/// string surfaces in the wake prompt (see the bin loops) and the
|
||||
/// 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";
|
||||
|
||||
/// 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:#}"),
|
||||
Ok(SocketReply::Messages(m)) => m,
|
||||
Ok(SocketReply::Err(m)) => return format!("recv failed: {m}"),
|
||||
Ok(other) => return format!("recv unexpected response: {other:?}"),
|
||||
Err(e) => return format!("recv transport error: {e:#}"),
|
||||
};
|
||||
if messages.is_empty() {
|
||||
return "(empty)".to_owned();
|
||||
}
|
||||
if messages.len() == 1 {
|
||||
let m = &messages[0];
|
||||
let banner = if m.redelivered { REDELIVERY_HINT } else { "" };
|
||||
return format!("{banner}from: {}\n\n{}", m.from, m.body);
|
||||
}
|
||||
let n = messages.len();
|
||||
let mut out = format!("popped {n} message(s):\n\n");
|
||||
for (i, m) in messages.iter().enumerate() {
|
||||
|
|
@ -214,6 +156,14 @@ pub fn format_recv_batch(resp: Result<SocketReply, anyhow::Error>) -> String {
|
|||
out
|
||||
}
|
||||
|
||||
/// Header prepended to message bodies that were popped by a prior
|
||||
/// harness session, never acked (turn crash / OOM / restart), and
|
||||
/// resurfaced by `RequeueInflight` on this session's boot. Same
|
||||
/// string surfaces in the wake prompt (see the bin loops) and the
|
||||
/// 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";
|
||||
|
||||
/// 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
|
||||
|
|
@ -365,23 +315,22 @@ pub struct SendArgs {
|
|||
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct RecvArgs {
|
||||
/// How long to long-poll for a new message before returning the
|
||||
/// empty marker. Capped at 60s server-side. Default (None) is
|
||||
/// 30s. Useful when an agent wants to throttle wakes without
|
||||
/// actually napping — pick a longer wait to coalesce bursts.
|
||||
/// How long to long-poll for the FIRST message before returning
|
||||
/// the empty marker. Capped at 60s server-side. Default (None)
|
||||
/// is 30s. Useful when an agent wants to park its turn waiting
|
||||
/// for any new work — pick a longer wait to coalesce bursts.
|
||||
#[serde(default)]
|
||||
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,
|
||||
/// Maximum number of messages to pop in this round-trip. Default
|
||||
/// (None) is 1 (single-message behaviour — exactly what you want
|
||||
/// when you're called to drive a turn off the first wake). Pass
|
||||
/// a higher value (capped at 32 server-side) when you've been
|
||||
/// told the inbox has more queued (the wake prompt mentions
|
||||
/// pending count) and want to drain everything in one tool call.
|
||||
/// Once the long-poll wakes up, the call drains up to `max` in
|
||||
/// total before returning — no extra round-trip needed.
|
||||
#[serde(default)]
|
||||
pub max: Option<u32>,
|
||||
}
|
||||
|
||||
/// MCP tool args for `remind`. Exactly one of `delay_seconds` or
|
||||
|
|
@ -535,14 +484,21 @@ impl AgentServer {
|
|||
}
|
||||
|
||||
#[tool(
|
||||
description = "Pop one message from this agent's inbox. Returns the sender and body, \
|
||||
or an empty marker if nothing is waiting. Without `wait_seconds` (or with 0) the \
|
||||
call returns immediately — a cheap 'anything pending?' peek. Pass a positive \
|
||||
description = "Pop messages from this agent's inbox. Returns one or more messages, or \
|
||||
an empty marker if nothing is waiting. \n\n\
|
||||
**Single-message default**: with no args (or `max: 1`) you get the next message — \
|
||||
same behaviour the harness uses to drive a turn. Without `wait_seconds` (or with 0) \
|
||||
the call returns immediately — a cheap 'anything pending?' peek. Pass a positive \
|
||||
`wait_seconds` (capped at 180) to park the turn waiting for new work — incoming \
|
||||
messages wake you instantly, otherwise the call returns empty at the timeout. \
|
||||
That's strictly better than a fixed shell `sleep`. Typical pattern: when you have \
|
||||
nothing else useful to do, call `recv(wait_seconds: 180)` to park until \
|
||||
something arrives."
|
||||
That's strictly better than a fixed shell `sleep`. \n\n\
|
||||
**Batch drain**: pass `max: N` (capped at 32) to drain up to N messages in one \
|
||||
round-trip. Use this when the wake prompt told you the inbox has more queued, or \
|
||||
any time you expect a burst — one tool call beats N consecutive single recvs. \
|
||||
`wait_seconds` still applies to the FIRST message; once one arrives the call drains \
|
||||
up to `max` in total. Empty result reported the same way regardless of `max`. \n\n\
|
||||
Typical pattern: when you have nothing else useful to do, call \
|
||||
`recv(wait_seconds: 180)` to park until something arrives."
|
||||
)]
|
||||
async fn recv(&self, Parameters(args): Parameters<RecvArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
|
|
@ -550,6 +506,7 @@ impl AgentServer {
|
|||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::AgentRequest::Recv {
|
||||
wait_seconds: args.wait_seconds,
|
||||
max: args.max,
|
||||
})
|
||||
.await;
|
||||
annotate_retries(format_recv(resp), retries)
|
||||
|
|
@ -557,26 +514,6 @@ 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 \
|
||||
|
|
@ -866,11 +803,14 @@ impl ManagerServer {
|
|||
}
|
||||
|
||||
#[tool(
|
||||
description = "Pop one message from the manager inbox. Returns sender + body, or \
|
||||
empty. Without `wait_seconds` (or 0) returns immediately — a cheap inbox peek. \
|
||||
Pass a positive value (capped at 180) to park until either a message arrives \
|
||||
or the timeout fires; prefer a long wait (120 or 180) over ending a turn \
|
||||
early when you have nothing else to do."
|
||||
description = "Pop messages from the manager inbox. Default returns one (sender + \
|
||||
body) or empty. Without `wait_seconds` (or 0) returns immediately — a cheap inbox \
|
||||
peek. Pass a positive value (capped at 180) to park until either a message arrives \
|
||||
or the timeout fires; prefer a long wait (120 or 180) over ending a turn early \
|
||||
when you have nothing else to do. \n\n\
|
||||
Pass `max: N` (capped at 32) to drain up to N messages in one round-trip — useful \
|
||||
when the wake prompt tells you the inbox has more queued. `wait_seconds` still \
|
||||
applies to the FIRST message; once one lands the call drains up to `max` in total."
|
||||
)]
|
||||
async fn recv(&self, Parameters(args): Parameters<RecvArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
|
|
@ -878,6 +818,7 @@ impl ManagerServer {
|
|||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::ManagerRequest::Recv {
|
||||
wait_seconds: args.wait_seconds,
|
||||
max: args.max,
|
||||
})
|
||||
.await;
|
||||
annotate_retries(format_recv(resp), retries)
|
||||
|
|
@ -885,26 +826,6 @@ 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