Watch
0
0
Fork
You've already forked hyperhive
0

the swarm controller streams every inter-agent message from the agents' terminal rows

This commit is contained in:
atlas 2026-10-18 17:27:32 +02:00
commit c32906b893
11 changed files with 520 additions and 12 deletions

View file

@ -15,7 +15,7 @@
//! `mcp__hyperhive__send`'s row say, which tools get an expandable body)
//! didn't change; only the shape it gets packed into did.
use crate::term_msg::{BodyFormat, ClassifyCtx, Level, TermMsg};
use crate::term_msg::{BodyFormat, ClassifyCtx, Level, SEND_ICON, SEND_SUMMARY_PREFIX, TermMsg};
use serde_json::{Value, json};
/// Classify one raw claude stream-json line into zero or more terminal rows.
@ -490,7 +490,7 @@ fn rich_tool_body(name: &str, input: &Value) -> Option<(String, &'static str)> {
fn tool_icon(name: &str) -> &'static str {
// Exact-match table first, then prefix/contains fallbacks.
match name {
"mcp__hyperhive__send" => "📤",
"mcp__hyperhive__send" => SEND_ICON,
"mcp__hyperhive__recv" => "📥",
"mcp__hyperhive__remind" => "⏰",
"mcp__hyperhive__set_status" => "🏷️",
@ -610,9 +610,9 @@ fn fmt_hyperhive_message_tool(name: &str, short: &str, input: &Value) -> String
let to = sv(input, "to");
let lines = sv(input, "body").lines().count();
if lines > 1 {
format!("{short} → {to} · {lines}L")
format!("{SEND_SUMMARY_PREFIX}{to} · {lines}L")
} else {
format!("{short} → {to}")
format!("{SEND_SUMMARY_PREFIX}{to}")
}
}
_ => fmt_args_generic(short, input),
@ -1331,4 +1331,29 @@ mod tests {
assert_eq!(fmt_room("!ÄÖÜ:server"), "!ÄÖÜ");
assert_eq!(fmt_room("!ÄÖÜ"), "!ÄÖÜ");
}
/// The send row the classifier builds is the shape `send_recipient`
/// reads back, with and without the line-count suffix.
#[test]
fn a_send_row_names_its_recipient_through_the_shared_parser() {
for (body, lines) in [("hi", false), ("one\ntwo\nthree", true)] {
let row = one(&json!({
"type": "assistant",
"message": { "content": [{
"type": "tool_use", "id": "s1", "name": "mcp__hyperhive__send",
"input": { "to": "iris", "body": body },
}]},
}));
assert_eq!(row.icon.as_deref(), Some(SEND_ICON));
assert_eq!(row.summary.contains(" · 3L"), lines, "{}", row.summary);
assert_eq!(crate::term_msg::send_recipient(&row.summary), Some("iris"));
}
assert_eq!(crate::term_msg::send_recipient("send* →"), None);
assert_eq!(crate::term_msg::send_recipient("Read(x)"), None);
// A suffix lookalike in the name is not stripped.
assert_eq!(
crate::term_msg::send_recipient("send* → a · xL"),
Some("a · xL")
);
}
}

View file

@ -128,6 +128,41 @@ pub fn iso8601_utc(unix_seconds: i64) -> String {
.to_rfc3339_opts(SecondsFormat::Secs, true)
}
/// Icon of the row an agent's `send` tool call makes.
pub const SEND_ICON: &str = "📤";
/// What a `send` row's summary starts with; the recipient follows. A
/// multi-line body appends ` · <N>L`, which [`send_recipient`] drops.
pub const SEND_SUMMARY_PREFIX: &str = "send* → ";
/// Icon of the row a turn started by a message makes.
pub const TURN_ICON: &str = "◆";
/// What a turn-start row's summary starts with; the sender follows.
pub const TURN_SUMMARY_PREFIX: &str = "TURN ← ";
/// The recipient a `send` row's summary names, without the line-count suffix.
#[must_use]
pub fn send_recipient(summary: &str) -> Option<&str> {
let rest = summary.strip_prefix(SEND_SUMMARY_PREFIX)?;
Some(match rest.rsplit_once(" · ") {
Some((to, lines))
if lines
.strip_suffix('L')
.is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())) =>
{
to
}
_ => rest,
})
}
/// The sender a turn-start row's summary names.
#[must_use]
pub fn turn_sender(summary: &str) -> Option<&str> {
summary.strip_prefix(TURN_SUMMARY_PREFIX)
}
/// Per-connection/per-request classification state. A live SSE stream keeps
/// one of these alive for the connection's lifetime — `tool_use` id → name
/// correlation, so a `tool_result` can tell it's answering a `recv` call and