Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
144912f8e0 | ||
|
|
d0beec8a40 |
29 changed files with 763 additions and 748 deletions
14
Cargo.lock
generated
14
Cargo.lock
generated
|
|
@ -1523,6 +1523,7 @@ dependencies = [
|
|||
"clap",
|
||||
"forgejo-api",
|
||||
"futures-util",
|
||||
"hive-agent-sock",
|
||||
"hive-claude",
|
||||
"hive-sh4re",
|
||||
"http-body-util",
|
||||
|
|
@ -1551,6 +1552,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"axum",
|
||||
"clap",
|
||||
"hive-agent-sock",
|
||||
"hive-sh4re",
|
||||
"rmcp",
|
||||
"serde",
|
||||
|
|
@ -1560,13 +1562,21 @@ dependencies = [
|
|||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hive-agent-sock"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"hive-sh4re",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hive-agent-wake"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"hive-sh4re",
|
||||
"hive-agent-sock",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
|
|
@ -1579,6 +1589,7 @@ name = "hive-bash-mcp"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"hive-agent-sock",
|
||||
"hive-sh4re",
|
||||
"libc",
|
||||
"rmcp",
|
||||
|
|
@ -1604,6 +1615,7 @@ dependencies = [
|
|||
"clap-markdown",
|
||||
"clap_complete",
|
||||
"forgejo-api",
|
||||
"hive-agent-sock",
|
||||
"hive-host-sock",
|
||||
"hive-priv-sock",
|
||||
"hive-sh4re",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ resolver = "3"
|
|||
members = [
|
||||
"hive-agent",
|
||||
"hive-agent-mcp",
|
||||
"hive-agent-sock",
|
||||
"hive-agent-wake",
|
||||
"hive-bash-mcp",
|
||||
"hive-c0re",
|
||||
|
|
@ -48,6 +49,7 @@ clap = { version = "4", features = ["derive"] }
|
|||
clap_complete = "4"
|
||||
indicatif = "0.18"
|
||||
hive-sh4re = { path = "hive-sh4re" }
|
||||
hive-agent-sock = { path = "hive-agent-sock" }
|
||||
hive-claude = { path = "hive-claude" }
|
||||
hive-host-sock = { path = "hive-host-sock" }
|
||||
hive-priv-sock = { path = "hive-priv-sock" }
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ workspace = true
|
|||
anyhow.workspace = true
|
||||
axum.workspace = true
|
||||
clap.workspace = true
|
||||
hive-agent-sock.workspace = true
|
||||
hive-sh4re.workspace = true
|
||||
rmcp.workspace = true
|
||||
serde.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! Embedded MCP server. Claude Code (running inside the agent container)
|
||||
//! connects to this over streamable-HTTP via `--mcp-config` (the long-lived
|
||||
//! `hive-mcp-http` daemon); tool calls land here and are translated to
|
||||
//! `AgentRequest::*` / `ManagerRequest::*` against hyperhive's own
|
||||
//! `Request::*` against hyperhive's own
|
||||
//! per-container unix socket at `/run/hive/mcp.sock`.
|
||||
//!
|
||||
//! Two protocols, two surfaces:
|
||||
|
|
@ -99,11 +99,9 @@ where
|
|||
|
||||
/// Unified MCP tool surface for both sub-agent and manager roles.
|
||||
///
|
||||
/// `AgentRequest = ManagerRequest = Request` and `AgentResponse =
|
||||
/// ManagerResponse = Response` are type aliases in hive-sh4re, so a single
|
||||
/// Both sockets speak the same `Request` / `Response` wire, so a single
|
||||
/// `dispatch` call covers both sockets — the only real difference is which
|
||||
/// socket path is used and which tools the flavor enables.
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AgentServer {
|
||||
socket: PathBuf,
|
||||
|
|
@ -119,13 +117,13 @@ impl AgentServer {
|
|||
/// `Response` plus the retry count so tool handlers can annotate their
|
||||
/// result (see `annotate_retries`).
|
||||
///
|
||||
/// `AgentRequest` / `ManagerRequest` / `Request` are all the same type
|
||||
/// (hive-sh4re type aliases), so this single method covers both sockets.
|
||||
/// Both sockets speak the same `Request` type, so this single method
|
||||
/// covers both.
|
||||
async fn dispatch(
|
||||
&self,
|
||||
req: hive_sh4re::Request,
|
||||
) -> (Result<hive_sh4re::Response, anyhow::Error>, u32) {
|
||||
match client::request_retried::<_, hive_sh4re::Response>(&self.socket, &req).await {
|
||||
req: hive_agent_sock::Request,
|
||||
) -> (Result<hive_agent_sock::Response, anyhow::Error>, u32) {
|
||||
match client::request_retried::<_, hive_agent_sock::Response>(&self.socket, &req).await {
|
||||
Ok((r, n)) => (Ok(r), n),
|
||||
Err(e) => (Err(e), 0),
|
||||
}
|
||||
|
|
@ -152,7 +150,7 @@ impl AgentServer {
|
|||
}
|
||||
run_tool_envelope("send", log, async move {
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::Send {
|
||||
.dispatch(hive_agent_sock::Request::Send {
|
||||
to: args.to,
|
||||
body: args.body,
|
||||
in_reply_to: args.in_reply_to,
|
||||
|
|
@ -183,7 +181,7 @@ impl AgentServer {
|
|||
let log = format!("{args:?}");
|
||||
run_tool_envelope("ask", log, async move {
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::Ask {
|
||||
.dispatch(hive_agent_sock::Request::Ask {
|
||||
question: args.question,
|
||||
options: args.options,
|
||||
multi: args.multi,
|
||||
|
|
@ -192,7 +190,7 @@ impl AgentServer {
|
|||
})
|
||||
.await;
|
||||
let s = match resp {
|
||||
Ok(hive_sh4re::Response::QuestionQueued { id }) => format!(
|
||||
Ok(hive_agent_sock::Response::QuestionQueued { id }) => format!(
|
||||
"question queued (id={id}); answer will arrive as a system \
|
||||
`question_answered` event in your inbox"
|
||||
),
|
||||
|
|
@ -217,7 +215,7 @@ impl AgentServer {
|
|||
let id = args.id;
|
||||
run_tool_envelope("answer", log, async move {
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::Answer {
|
||||
.dispatch(hive_agent_sock::Request::Answer {
|
||||
id,
|
||||
answer: args.answer,
|
||||
})
|
||||
|
|
@ -255,7 +253,7 @@ impl AgentServer {
|
|||
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 {
|
||||
.dispatch(hive_agent_sock::Request::Recv {
|
||||
wait_seconds: args.wait_seconds,
|
||||
max: args.max,
|
||||
})
|
||||
|
|
@ -280,10 +278,10 @@ impl AgentServer {
|
|||
let log = format!("{args:?}");
|
||||
run_tool_envelope("ack_until", log, async move {
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::AckUntil { up_to: args.up_to })
|
||||
.dispatch(hive_agent_sock::Request::AckUntil { up_to: args.up_to })
|
||||
.await;
|
||||
let rendered = match resp {
|
||||
Ok(hive_sh4re::Response::Acked { count }) => {
|
||||
Ok(hive_agent_sock::Response::Acked { count }) => {
|
||||
format!("acked {count} message(s) up to id {}", args.up_to)
|
||||
}
|
||||
other => reply_err(other, "ack_until"),
|
||||
|
|
@ -312,11 +310,11 @@ impl AgentServer {
|
|||
run_tool_envelope("get_loose_ends", String::new(), async move {
|
||||
let is_self_query = args.agent.is_none();
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::GetLooseEnds { agent: args.agent })
|
||||
.dispatch(hive_agent_sock::Request::GetLooseEnds { agent: args.agent })
|
||||
.await;
|
||||
// Extract the vec so we can augment before rendering.
|
||||
let mut loose_ends = match resp {
|
||||
Ok(hive_sh4re::Response::LooseEnds { loose_ends }) => loose_ends,
|
||||
Ok(hive_agent_sock::Response::LooseEnds { loose_ends }) => loose_ends,
|
||||
other => return annotate_retries(reply_err(other, "get_loose_ends"), retries),
|
||||
};
|
||||
// Prepend matrix unread entry for self-queries only (can't
|
||||
|
|
@ -365,7 +363,7 @@ impl AgentServer {
|
|||
return e;
|
||||
}
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::SetStatus { text: args.text })
|
||||
.dispatch(hive_agent_sock::Request::SetStatus { text: args.text })
|
||||
.await;
|
||||
annotate_retries(
|
||||
format_ack(resp, "set_status", "status updated".to_owned()),
|
||||
|
|
@ -397,7 +395,7 @@ impl AgentServer {
|
|||
let log = args.name.clone().unwrap_or_else(|| "<self>".to_owned());
|
||||
run_tool_envelope("get_agent_meta", log, async move {
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::GetAgentMeta { name: args.name })
|
||||
.dispatch(hive_agent_sock::Request::GetAgentMeta { name: args.name })
|
||||
.await;
|
||||
annotate_retries(format_agent_meta(resp), retries)
|
||||
})
|
||||
|
|
@ -425,7 +423,7 @@ impl AgentServer {
|
|||
};
|
||||
let kind_label = loose_end_kind_label(kind);
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::CancelLooseEnd { kind, id })
|
||||
.dispatch(hive_agent_sock::Request::CancelLooseEnd { kind, id })
|
||||
.await;
|
||||
annotate_retries(
|
||||
format_ack(
|
||||
|
|
@ -452,10 +450,10 @@ impl AgentServer {
|
|||
let log = format!("{args:?}");
|
||||
run_tool_envelope("create_repo", log, async move {
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::CreateRepo { repo: args.repo })
|
||||
.dispatch(hive_agent_sock::Request::CreateRepo { repo: args.repo })
|
||||
.await;
|
||||
let s = match resp {
|
||||
Ok(hive_sh4re::Response::RepoCreated {
|
||||
Ok(hive_agent_sock::Response::RepoCreated {
|
||||
full_name,
|
||||
clone_url,
|
||||
}) => format!("created repo {full_name} — clone: {clone_url}"),
|
||||
|
|
@ -495,7 +493,7 @@ impl AgentServer {
|
|||
(None, Some(t)) => hive_sh4re::ReminderTiming::At { unix_timestamp: t },
|
||||
};
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::Remind {
|
||||
.dispatch(hive_agent_sock::Request::Remind {
|
||||
message: args.message,
|
||||
timing,
|
||||
file_path: args.file_path,
|
||||
|
|
@ -549,7 +547,7 @@ impl AgentServer {
|
|||
let name = args.name.clone();
|
||||
run_tool_envelope("restart", log, async move {
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::Restart { name: args.name })
|
||||
.dispatch(hive_agent_sock::Request::Restart { name: args.name })
|
||||
.await;
|
||||
annotate_retries(
|
||||
format_ack(resp, "restart", format!("restarted {name}")),
|
||||
|
|
@ -571,7 +569,7 @@ impl AgentServer {
|
|||
let name = args.name.clone();
|
||||
run_tool_envelope("kill", log, async move {
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::Kill { name: args.name })
|
||||
.dispatch(hive_agent_sock::Request::Kill { name: args.name })
|
||||
.await;
|
||||
annotate_retries(format_ack(resp, "kill", format!("killed {name}")), retries)
|
||||
})
|
||||
|
|
@ -592,7 +590,7 @@ impl AgentServer {
|
|||
let name = args.name.clone();
|
||||
run_tool_envelope("update", log, async move {
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::Update { name: args.name })
|
||||
.dispatch(hive_agent_sock::Request::Update { name: args.name })
|
||||
.await;
|
||||
annotate_retries(
|
||||
format_ack(resp, "update", format!("updated {name}")),
|
||||
|
|
@ -614,9 +612,11 @@ impl AgentServer {
|
|||
)]
|
||||
async fn list_containers(&self) -> String {
|
||||
run_tool_envelope("list_containers", String::new(), async move {
|
||||
let (resp, retries) = self.dispatch(hive_sh4re::Request::ListDescendants).await;
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_agent_sock::Request::ListDescendants)
|
||||
.await;
|
||||
let body = match resp {
|
||||
Ok(hive_sh4re::Response::Containers { containers }) => {
|
||||
Ok(hive_agent_sock::Response::Containers { containers }) => {
|
||||
if containers.is_empty() {
|
||||
"no descendant containers".to_owned()
|
||||
} else {
|
||||
|
|
@ -659,7 +659,7 @@ impl AgentServer {
|
|||
let log = format!("{args:?}");
|
||||
run_tool_envelope("get_host_journal", log, async move {
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::GetHostJournal {
|
||||
.dispatch(hive_agent_sock::Request::GetHostJournal {
|
||||
unit: args.unit,
|
||||
container: args.container,
|
||||
lines: args.lines,
|
||||
|
|
@ -670,7 +670,7 @@ impl AgentServer {
|
|||
})
|
||||
.await;
|
||||
let result = match resp {
|
||||
Ok(hive_sh4re::Response::HostJournal { content }) => content,
|
||||
Ok(hive_agent_sock::Response::HostJournal { content }) => content,
|
||||
other => reply_err(other, "get_host_journal"),
|
||||
};
|
||||
annotate_retries(result, retries)
|
||||
|
|
@ -699,7 +699,7 @@ impl AgentServer {
|
|||
let name = args.name.clone();
|
||||
run_tool_envelope("request_init_config", log, async move {
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::RequestInitConfig {
|
||||
.dispatch(hive_agent_sock::Request::RequestInitConfig {
|
||||
name: args.name,
|
||||
description: args.description,
|
||||
})
|
||||
|
|
@ -727,7 +727,7 @@ impl AgentServer {
|
|||
let name = args.name.clone();
|
||||
run_tool_envelope("start", log, async move {
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::Start { name: args.name })
|
||||
.dispatch(hive_agent_sock::Request::Start { name: args.name })
|
||||
.await;
|
||||
annotate_retries(
|
||||
format_ack(resp, "start", format!("started {name}")),
|
||||
|
|
@ -750,13 +750,13 @@ impl AgentServer {
|
|||
run_tool_envelope("get_logs", log, async move {
|
||||
let lines = args.lines.map(|n| n.min(500));
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::GetLogs {
|
||||
.dispatch(hive_agent_sock::Request::GetLogs {
|
||||
agent: agent.clone(),
|
||||
lines,
|
||||
})
|
||||
.await;
|
||||
let s = match resp {
|
||||
Ok(hive_sh4re::Response::Logs { content }) => {
|
||||
Ok(hive_agent_sock::Response::Logs { content }) => {
|
||||
if content.is_empty() {
|
||||
format!("(no journal output for {agent})")
|
||||
} else {
|
||||
|
|
@ -790,7 +790,7 @@ impl AgentServer {
|
|||
args.inputs.join(", ")
|
||||
};
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::RequestUpdateMetaInputs {
|
||||
.dispatch(hive_agent_sock::Request::RequestUpdateMetaInputs {
|
||||
inputs: args.inputs,
|
||||
description: args.description,
|
||||
})
|
||||
|
|
@ -829,7 +829,7 @@ impl AgentServer {
|
|||
run_tool_envelope("request_schedule_prompt", log, async move {
|
||||
let target_count = args.targets.len();
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::RequestSchedulePrompt(
|
||||
.dispatch(hive_agent_sock::Request::RequestSchedulePrompt(
|
||||
hive_sh4re::SchedulePromptPayload {
|
||||
targets: args.targets,
|
||||
body: args.body,
|
||||
|
|
@ -865,7 +865,7 @@ impl AgentServer {
|
|||
run_tool_envelope("fire_schedule_now", log, async move {
|
||||
let id = args.id;
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::FireScheduleNow { id })
|
||||
.dispatch(hive_agent_sock::Request::FireScheduleNow { id })
|
||||
.await;
|
||||
annotate_retries(
|
||||
format_ack(resp, "fire_schedule_now", format!("fired #{id} now")),
|
||||
|
|
@ -888,7 +888,7 @@ impl AgentServer {
|
|||
run_tool_envelope("cancel_schedule", log, async move {
|
||||
let id = args.id;
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::CancelSchedule {
|
||||
.dispatch(hive_agent_sock::Request::CancelSchedule {
|
||||
id: args.id,
|
||||
targets: args.targets,
|
||||
})
|
||||
|
|
@ -920,7 +920,7 @@ impl AgentServer {
|
|||
run_tool_envelope("edit_schedule", log, async move {
|
||||
let id = args.id;
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::Request::EditSchedule {
|
||||
.dispatch(hive_agent_sock::Request::EditSchedule {
|
||||
id: args.id,
|
||||
body: args.body,
|
||||
description: args.description.map(Some),
|
||||
|
|
@ -947,9 +947,9 @@ impl AgentServer {
|
|||
)]
|
||||
async fn list_schedules(&self) -> String {
|
||||
run_tool_envelope("list_schedules", String::new(), async move {
|
||||
let (resp, retries) = self.dispatch(hive_sh4re::Request::ListSchedules).await;
|
||||
let (resp, retries) = self.dispatch(hive_agent_sock::Request::ListSchedules).await;
|
||||
let body = match resp {
|
||||
Ok(hive_sh4re::Response::Schedules { schedules }) => {
|
||||
Ok(hive_agent_sock::Response::Schedules { schedules }) => {
|
||||
serde_json::to_string(&schedules)
|
||||
.unwrap_or_else(|e| format!("list_schedules: serialise: {e:#}"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,12 @@
|
|||
/// transport error: …"`. Handlers match their own happy-path variant and route
|
||||
/// everything else here via a catch-all arm (`other => reply_err(other, tool)`),
|
||||
/// so the triplet lives in exactly one place.
|
||||
pub(super) fn reply_err(resp: Result<hive_sh4re::Response, anyhow::Error>, tool: &str) -> String {
|
||||
pub(super) fn reply_err(
|
||||
resp: Result<hive_agent_sock::Response, anyhow::Error>,
|
||||
tool: &str,
|
||||
) -> String {
|
||||
match resp {
|
||||
Ok(hive_sh4re::Response::Err { message }) => format!("{tool} failed: {message}"),
|
||||
Ok(hive_agent_sock::Response::Err { message }) => format!("{tool} failed: {message}"),
|
||||
Ok(other) => format!("{tool} unexpected response: {other:?}"),
|
||||
Err(e) => format!("{tool} transport error: {e:#}"),
|
||||
}
|
||||
|
|
@ -23,12 +26,12 @@ pub(super) fn reply_err(resp: Result<hive_sh4re::Response, anyhow::Error>, tool:
|
|||
/// behavior.
|
||||
#[must_use]
|
||||
pub fn format_ack(
|
||||
resp: Result<hive_sh4re::Response, anyhow::Error>,
|
||||
resp: Result<hive_agent_sock::Response, anyhow::Error>,
|
||||
tool: &str,
|
||||
ok_msg: String,
|
||||
) -> String {
|
||||
match resp {
|
||||
Ok(hive_sh4re::Response::Ok) => ok_msg,
|
||||
Ok(hive_agent_sock::Response::Ok) => ok_msg,
|
||||
other => reply_err(other, tool),
|
||||
}
|
||||
}
|
||||
|
|
@ -44,9 +47,9 @@ pub fn format_ack(
|
|||
/// 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<hive_sh4re::Response, anyhow::Error>, waited: bool) -> String {
|
||||
pub fn format_recv(resp: Result<hive_agent_sock::Response, anyhow::Error>, waited: bool) -> String {
|
||||
match resp {
|
||||
Ok(hive_sh4re::Response::Messages {
|
||||
Ok(hive_agent_sock::Response::Messages {
|
||||
messages,
|
||||
remaining,
|
||||
}) => render_recv_messages(&messages, remaining, waited),
|
||||
|
|
@ -57,7 +60,7 @@ pub fn format_recv(resp: Result<hive_sh4re::Response, anyhow::Error>, waited: bo
|
|||
// stop unmissably tells claude to flush + end. `remaining` is forced
|
||||
// to 0 — the inbox is fenced, so a "N more pending" hint would be
|
||||
// misleading.
|
||||
Ok(hive_sh4re::Response::GracefulStop) => {
|
||||
Ok(hive_agent_sock::Response::GracefulStop) => {
|
||||
render_recv_messages(&[graceful_stop_message()], 0, waited)
|
||||
}
|
||||
other => reply_err(other, "recv"),
|
||||
|
|
@ -354,9 +357,9 @@ pub(super) fn loose_end_kind_label(kind: hive_sh4re::CancelLooseEndKind) -> &'st
|
|||
/// `running: no` line tells the caller WHY. See
|
||||
/// `docs/turn-loop/mcp.md::Core tools` (`get_agent_meta`).
|
||||
#[must_use]
|
||||
pub fn format_agent_meta(resp: Result<hive_sh4re::Response, anyhow::Error>) -> String {
|
||||
pub fn format_agent_meta(resp: Result<hive_agent_sock::Response, anyhow::Error>) -> String {
|
||||
match resp {
|
||||
Ok(hive_sh4re::Response::AgentMeta {
|
||||
Ok(hive_agent_sock::Response::AgentMeta {
|
||||
name,
|
||||
running,
|
||||
hyperhive_rev,
|
||||
|
|
@ -477,7 +480,7 @@ mod tests {
|
|||
#[test]
|
||||
fn empty_recv_after_wait_appends_idle_hint() {
|
||||
let out = format_recv(
|
||||
Ok(hive_sh4re::Response::Messages {
|
||||
Ok(hive_agent_sock::Response::Messages {
|
||||
messages: vec![],
|
||||
remaining: 0,
|
||||
}),
|
||||
|
|
@ -490,7 +493,7 @@ mod tests {
|
|||
#[test]
|
||||
fn empty_recv_without_wait_has_no_hint() {
|
||||
let out = format_recv(
|
||||
Ok(hive_sh4re::Response::Messages {
|
||||
Ok(hive_agent_sock::Response::Messages {
|
||||
messages: vec![],
|
||||
remaining: 0,
|
||||
}),
|
||||
|
|
@ -502,7 +505,7 @@ mod tests {
|
|||
#[test]
|
||||
fn single_recv_with_remaining_appends_pending_hint() {
|
||||
let out = format_recv(
|
||||
Ok(hive_sh4re::Response::Messages {
|
||||
Ok(hive_agent_sock::Response::Messages {
|
||||
messages: vec![msg(7, "alice", "hi")],
|
||||
remaining: 3,
|
||||
}),
|
||||
|
|
@ -516,7 +519,7 @@ mod tests {
|
|||
#[test]
|
||||
fn single_recv_no_remaining_has_no_pending_hint() {
|
||||
let out = format_recv(
|
||||
Ok(hive_sh4re::Response::Messages {
|
||||
Ok(hive_agent_sock::Response::Messages {
|
||||
messages: vec![msg(7, "alice", "hi")],
|
||||
remaining: 0,
|
||||
}),
|
||||
|
|
@ -528,7 +531,7 @@ mod tests {
|
|||
#[test]
|
||||
fn batch_recv_with_remaining_appends_pending_hint_once() {
|
||||
let out = format_recv(
|
||||
Ok(hive_sh4re::Response::Messages {
|
||||
Ok(hive_agent_sock::Response::Messages {
|
||||
messages: vec![msg(7, "alice", "hi"), msg(8, "bob", "yo")],
|
||||
remaining: 9,
|
||||
}),
|
||||
|
|
|
|||
11
hive-agent-sock/Cargo.toml
Normal file
11
hive-agent-sock/Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "hive-agent-sock"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
hive-sh4re.workspace = true
|
||||
serde.workspace = true
|
||||
399
hive-agent-sock/src/lib.rs
Normal file
399
hive-agent-sock/src/lib.rs
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
//! Per-agent + manager socket wire types (`/run/hive/mcp.sock`).
|
||||
//!
|
||||
//! The unified `Request` / `Response` protocol spoken between an agent's
|
||||
//! in-container harness (and the manager) and the `hive-c0re` daemon over the
|
||||
//! per-agent mcp socket. Re-homed out of `hive-sh4re` so this one socket owns
|
||||
//! its own protocol crate, mirroring `hive-host-sock` / `hive-priv-sock`. The
|
||||
//! shared payload types it references (`Message`, `LooseEnd`, `Approval`, …)
|
||||
//! stay in `hive-sh4re`, which this crate depends on.
|
||||
|
||||
use hive_sh4re::{
|
||||
CancelLooseEndKind, ContainerInfo, DeliveredMessage, InboxRow, JournalPriority, LooseEnd,
|
||||
MatrixIdentity, ReminderStats, ReminderTiming, SchedulePromptPayload, WireSchedule,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// serde `default` helper for `Response::AgentMeta::running` (absent = true).
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Unified request enum for both agent and manager sockets. The agent's
|
||||
/// identity is the socket it arrived on. Privileged variants are marked
|
||||
/// `*(privileged)*` — an agent socket returns `Err` for them server-side.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "cmd", rename_all = "snake_case")]
|
||||
pub enum Request {
|
||||
/// Send a message to another agent.
|
||||
Send {
|
||||
to: String,
|
||||
body: String,
|
||||
/// Optional id of the message being replied to. Stored in the
|
||||
/// broker DB and returned on `Recv` so the dashboard can render
|
||||
/// threads. Ignored if the id is unknown or out of retention.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
in_reply_to: Option<i64>,
|
||||
},
|
||||
/// Pop pending messages from this agent's inbox.
|
||||
/// Delivery + ack cycle: see
|
||||
/// `docs/conventions.md::Broker delivery + ack cycle`.
|
||||
Recv {
|
||||
#[serde(default)]
|
||||
wait_seconds: Option<u64>,
|
||||
#[serde(default)]
|
||||
max: Option<u32>,
|
||||
},
|
||||
/// Non-mutating: how many pending messages are addressed to me?
|
||||
/// Used by the harness to render a status line after each tool call.
|
||||
Status,
|
||||
/// Operator-injected message TO this agent (from this agent's own web
|
||||
/// UI). Recipient is implicit — `from` is `"operator"`. Effectively the
|
||||
/// per-agent equivalent of the old dashboard T4LK form, but scoped to
|
||||
/// the agent whose page the operator is on.
|
||||
OperatorMsg { body: String },
|
||||
/// Wake-up event injected from inside the container. Recipient is
|
||||
/// implicit (this agent); `from` is caller-chosen. See
|
||||
/// `docs/conventions.md::Wake injection` for the trust model and
|
||||
/// typical callers. The wake is persisted in the sqlite broker
|
||||
/// like any other message — the agent can ack it via `AckUntil`.
|
||||
Wake { from: String, body: String },
|
||||
/// Last `limit` messages addressed to this agent, newest-first.
|
||||
/// Non-mutating — pulls from the broker without delivering. The
|
||||
/// per-agent web UI uses this to render its own inbox section.
|
||||
Recent { limit: u64 },
|
||||
/// Surface a question to either the operator or another agent.
|
||||
/// Routing + shape: see
|
||||
/// `docs/conventions.md::Question routing (Ask / Answer)`.
|
||||
Ask {
|
||||
question: String,
|
||||
#[serde(default)]
|
||||
options: Vec<String>,
|
||||
#[serde(default)]
|
||||
multi: bool,
|
||||
#[serde(default)]
|
||||
ttl_seconds: Option<u64>,
|
||||
#[serde(default)]
|
||||
to: Option<String>,
|
||||
},
|
||||
/// Answer a question previously routed to this agent via
|
||||
/// `HelperEvent::QuestionAsked`. Authorised callers + threading
|
||||
/// back via `HelperEvent::QuestionAnswered`: see
|
||||
/// `docs/conventions.md::Question routing (Ask / Answer)`.
|
||||
Answer { id: i64, answer: String },
|
||||
/// Schedule a reminder message to be delivered to this agent at a
|
||||
/// future time. The reminder lands in the agent's inbox as an auto-sent
|
||||
/// message from `"reminder"`. Use for agent follow-ups (e.g. check task
|
||||
/// status, retry failed operation). Message length is limited; pass
|
||||
/// `file_path` to store in a file and get a path-reference message
|
||||
/// instead.
|
||||
Remind {
|
||||
message: String,
|
||||
timing: ReminderTiming,
|
||||
#[serde(default)]
|
||||
file_path: Option<String>,
|
||||
},
|
||||
/// Loose-ends view. On the agent socket: `None` = self; direct
|
||||
/// children are always accessible; non-children require the
|
||||
/// `query_agent_state` capability — rejected with an error otherwise;
|
||||
/// `"*"` is always rejected (use the manager socket). On the manager
|
||||
/// socket: `None` = manager self, `"*"` = hive-wide, any name =
|
||||
/// that agent. See `docs/conventions.md::Loose-ends wire shape`.
|
||||
GetLooseEnds {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
agent: Option<String>,
|
||||
},
|
||||
/// Upsert a *todo* (loose-ends v2) from an in-container
|
||||
/// subsystem (matrix / forge / bash). `subsystem` is the producer
|
||||
/// marker; `key` is the optional subsystem-specific dedup key (a
|
||||
/// matrix room id, a bash task id). Re-pushing an identical keyed
|
||||
/// todo is a no-op; a new-or-changed one coalesces a wake to the
|
||||
/// agent. Keyless todos always insert as one-offs.
|
||||
UpsertTodo {
|
||||
subsystem: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
key: Option<String>,
|
||||
summary: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
source: Option<String>,
|
||||
},
|
||||
/// Clear producer-resolved todo(s) by `(subsystem, key)`. `key =
|
||||
/// Some(k)` clears the one keyed row; `key = None` clears **all** of
|
||||
/// the subsystem's keyless todos (rows with no key can't be told
|
||||
/// apart — clear a specific one via `MarkTodoDone` by id). `all =
|
||||
/// true` wipes the producer's whole set (cancel-and-recreate on
|
||||
/// daemon restart).
|
||||
ClearTodo {
|
||||
subsystem: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
key: Option<String>,
|
||||
#[serde(default)]
|
||||
all: bool,
|
||||
},
|
||||
/// List todos, optionally filtered to one `subsystem` (a producer
|
||||
/// enumerating its own set). `None` = all.
|
||||
ListTodos {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
subsystem: Option<String>,
|
||||
},
|
||||
/// The agent marks one of its own todos done, by id.
|
||||
MarkTodoDone { id: i64 },
|
||||
/// Count of pending (un-delivered) reminders. On the agent socket:
|
||||
/// same target rules as `GetLooseEnds` (self/children free;
|
||||
/// non-children require `query_agent_state`; `"*"` rejected).
|
||||
/// On the manager socket: `None` = self, any name = that agent.
|
||||
/// Used by the harness's per-turn stats sink.
|
||||
CountPendingReminders {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
agent: Option<String>,
|
||||
},
|
||||
/// Reminder statistics: counts of scheduled, delivered, and pending
|
||||
/// reminders over a time window. `since_secs` filters to reminders
|
||||
/// created in the last N seconds (0 = all). On the agent socket:
|
||||
/// same target rules as `GetLooseEnds` (self/children free;
|
||||
/// non-children require `query_agent_state`; `"*"` rejected).
|
||||
/// On the manager socket: `None` = self, any name = that agent.
|
||||
ReminderRollup {
|
||||
/// Only count reminders created in the last N seconds from now.
|
||||
/// Pass 0 to include all reminders.
|
||||
#[serde(default)]
|
||||
since_secs: u64,
|
||||
/// Whose reminders to roll up. `None` = the caller's own.
|
||||
/// `Some("<name>")` = that agent's (requires `query_agent_state`
|
||||
/// capability on the agent socket; always available on the manager
|
||||
/// socket).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
agent: Option<String>,
|
||||
},
|
||||
/// Set a free-text status string visible on the dashboard. The harness
|
||||
/// writes `{state_dir}/hyperhive-status` locally before sending this
|
||||
/// request; hive-c0re just triggers a dashboard rescan on receipt.
|
||||
/// Pass an empty string to clear the status.
|
||||
SetStatus { text: String },
|
||||
/// Fetch identity + status for an agent. `name = None` =
|
||||
/// self-introspection; `Some(<agent>)` = target query. See
|
||||
/// `docs/conventions.md::Agent metadata`.
|
||||
GetAgentMeta {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
name: Option<String>,
|
||||
},
|
||||
/// Cancel an open thread the agent owns. Authorisation +
|
||||
/// per-kind semantics in
|
||||
/// `docs/conventions.md::Loose-ends wire shape`.
|
||||
CancelLooseEnd { kind: CancelLooseEndKind, id: i64 },
|
||||
/// Create a git repo *through hive-c0re*. Agents can't create
|
||||
/// repos with their own forge token (`max_repo_creation = 0`); this is
|
||||
/// the sanctioned path. hive-c0re creates `repo` in the c0re-owned
|
||||
/// `agents` org, adds the calling agent as a write collaborator (not
|
||||
/// owner), and applies operator-team branch protection so the author
|
||||
/// can't merge its own PRs. Returns the new repo's full name.
|
||||
CreateRepo { repo: String },
|
||||
/// Mark every message popped since the last `AckTurn` as handled.
|
||||
/// Harness↔broker pairing fired after `TurnOutcome::Ok`. See
|
||||
/// `docs/conventions.md::Broker delivery + ack cycle`.
|
||||
AckTurn,
|
||||
/// Mark every inbox message with broker row id `<= up_to` as
|
||||
/// handled (`acked_at` set), whether still pending or already
|
||||
/// delivered. The agent-facing bulk-triage escape hatch for a
|
||||
/// redelivered / accumulated backlog: instead of popping and
|
||||
/// re-reading dozens of already-handled messages one turn at a
|
||||
/// time, the agent acks everything up to the id it has seen.
|
||||
/// Recipient-scoped — an agent can only ack its own rows. See
|
||||
/// `docs/conventions.md::Broker delivery + ack cycle`.
|
||||
AckUntil { up_to: i64 },
|
||||
/// Requeue every popped-but-unacked message back into the inbox.
|
||||
/// Harness fires this once at boot to recover from
|
||||
/// crashed-mid-turn sessions. See
|
||||
/// `docs/conventions.md::Broker delivery + ack cycle`.
|
||||
RequeueInflight,
|
||||
/// Harness → c0re: "I saw the `GracefulStop` signal, ran my
|
||||
/// stop-checkpoint turn (durable `/state` flushed) and am exiting my
|
||||
/// serve loop now." Lets the `GracefulStop` orchestration stop the
|
||||
/// container immediately instead of waiting out its timeout fallback.
|
||||
GracefulStopComplete,
|
||||
/// *(capability-gated: `read_host_journal`)* Fetch recent lines
|
||||
/// from the host journal. Filters are all optional; omitting all
|
||||
/// returns the last `lines` entries from the global journal.
|
||||
GetHostJournal {
|
||||
/// Filter to a specific systemd unit (e.g. `hive-c0re.service`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
unit: Option<String>,
|
||||
/// Machine name to pass to journalctl `-M` verbatim (e.g. `h-iris`).
|
||||
/// The caller is responsible for the correct nspawn machine name.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
container: Option<String>,
|
||||
/// Number of journal lines to return (default 30, max 100).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
lines: Option<u32>,
|
||||
/// Minimum syslog priority level.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
priority: Option<JournalPriority>,
|
||||
/// Regex to match against log message fields (journalctl `--grep`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
grep: Option<String>,
|
||||
/// Show entries on or newer than this timestamp (journalctl `--since`).
|
||||
/// ISO 8601 or journalctl-accepted relative strings (e.g. `"-1h"`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
since: Option<String>,
|
||||
/// Show entries on or older than this timestamp (journalctl `--until`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
until: Option<String>,
|
||||
},
|
||||
|
||||
// ---- privileged (manager socket only for now) ---------------------------
|
||||
/// *(privileged)* Initialise a brand-new agent's proposed config repo
|
||||
/// and queue an approval for the operator to review.
|
||||
RequestInitConfig {
|
||||
name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
description: Option<String>,
|
||||
},
|
||||
/// *(privileged)* Stop a sub-agent (graceful).
|
||||
Kill { name: String },
|
||||
/// *(privileged)* Start a previously-stopped sub-agent container.
|
||||
Start { name: String },
|
||||
/// *(privileged)* Restart a sub-agent container (stop + start).
|
||||
Restart { name: String },
|
||||
/// *(privileged)* Rebuild a sub-agent against the current hyperhive
|
||||
/// flake + agent.nix. No approval required.
|
||||
Update { name: String },
|
||||
/// *(privileged)* Fetch recent journal lines for a sub-agent container.
|
||||
GetLogs {
|
||||
agent: String,
|
||||
#[serde(default)]
|
||||
lines: Option<u32>,
|
||||
},
|
||||
/// *(privileged)* Queue an approval to run `nix flake update [inputs...]`.
|
||||
RequestUpdateMetaInputs {
|
||||
#[serde(default)]
|
||||
inputs: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
description: Option<String>,
|
||||
},
|
||||
/// *(privileged)* Queue an approval to add a scheduled prompt.
|
||||
RequestSchedulePrompt(SchedulePromptPayload),
|
||||
/// *(privileged)* Cancel a scheduled prompt.
|
||||
CancelSchedule {
|
||||
id: i64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
targets: Option<Vec<String>>,
|
||||
},
|
||||
/// *(privileged)* List every schedule in the queue.
|
||||
ListSchedules,
|
||||
/// List all containers that are topological descendants of the calling
|
||||
/// agent (direct children + their subtrees). Scoped to the caller's
|
||||
/// subtree; gated by the `lifecycle` tool group. The result includes all
|
||||
/// known descendants regardless of whether the container is currently
|
||||
/// running — use `running` to distinguish.
|
||||
ListDescendants,
|
||||
/// *(privileged)* Fire a scheduled prompt out of band immediately.
|
||||
FireScheduleNow { id: i64 },
|
||||
/// *(privileged)* Edit an existing schedule's mutable fields.
|
||||
EditSchedule {
|
||||
id: i64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
body: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
description: Option<Option<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
interval_seconds: Option<Option<u64>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
next_fire_at_unix: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
targets_add: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
targets_remove: Option<Vec<String>>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Unified response enum for both agent and manager sockets. Privileged
|
||||
/// variants (`Logs`, `Schedules`) are never returned on agent sockets.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum Response {
|
||||
/// `Send` succeeded.
|
||||
Ok,
|
||||
/// Either `Send` failed or `Recv` errored.
|
||||
Err { message: String },
|
||||
/// `Recv` result: zero or more messages, FIFO-ordered, never
|
||||
/// longer than the `max` the caller passed. Empty vec = nothing
|
||||
/// pending (the "(empty)" path for the formatter). Per-row `id` +
|
||||
/// `redelivered` carry the broker's row id (tracked by the harness
|
||||
/// for `AckTurn`, and surfaced to claude as a `[msg #<id>]` marker
|
||||
/// so `AckUntil` has something to reference) and the "previously
|
||||
/// popped, not acked" flag — see `DeliveredMessage` for details.
|
||||
/// `remaining` is the inbox depth *after* this batch was popped —
|
||||
/// how many still-pending messages the caller could drain next. The
|
||||
/// harness surfaces it to claude ("N more pending") so an in-turn
|
||||
/// `recv` learns whether the inbox is drained, mirroring the count
|
||||
/// the wake prompt already carries.
|
||||
Messages {
|
||||
messages: Vec<DeliveredMessage>,
|
||||
remaining: u64,
|
||||
},
|
||||
/// `Status` result: how many pending messages are in this agent's inbox.
|
||||
Status { unread: u64 },
|
||||
/// `AckUntil` result: how many rows were newly marked handled.
|
||||
Acked { count: u64 },
|
||||
/// `Recent` result: newest-first inbox rows.
|
||||
Recent { rows: Vec<InboxRow> },
|
||||
/// `Ask` result: the queued question id. The answer lands later
|
||||
/// as `HelperEvent::QuestionAnswered` in this agent's inbox.
|
||||
QuestionQueued { id: i64 },
|
||||
/// `GetLooseEnds` result: list of loose ends pending against
|
||||
/// this agent. Ordered newest-first within each kind.
|
||||
LooseEnds { loose_ends: Vec<LooseEnd> },
|
||||
/// `CountPendingReminders` result.
|
||||
PendingRemindersCount { count: u64 },
|
||||
/// `ReminderRollup` result: reminder activity stats for the agent.
|
||||
ReminderRollup(ReminderStats),
|
||||
/// `GetAgentMeta` result. Per-field semantics + serde defaults
|
||||
/// live in `docs/conventions.md::Agent metadata`.
|
||||
AgentMeta {
|
||||
name: String,
|
||||
#[serde(default = "default_true")]
|
||||
running: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
hyperhive_rev: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
status_text: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
status_set_at: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
hive_name: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
swarm_name: Option<String>,
|
||||
/// Matrix identities this agent can act as (one per configured +
|
||||
/// live account). Empty for agents with no matrix provisioning.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
matrix_accounts: Vec<MatrixIdentity>,
|
||||
},
|
||||
/// `GetLogs` result: journal lines for the requested container.
|
||||
/// Returned on the manager socket only.
|
||||
Logs { content: String },
|
||||
/// `GetHostJournal` result: host journal lines matching the
|
||||
/// requested filters. Returned on the agent socket when the agent
|
||||
/// holds the `read_host_journal` capability.
|
||||
HostJournal { content: String },
|
||||
/// `ListSchedules` result. Snapshot of every schedule.
|
||||
/// Returned on the manager socket only.
|
||||
Schedules { schedules: Vec<WireSchedule> },
|
||||
/// `CreateRepo` result: the new repo's full name (`agents/<repo>`)
|
||||
/// and clone URL, so the agent can immediately `git clone` it.
|
||||
RepoCreated {
|
||||
full_name: String,
|
||||
clone_url: String,
|
||||
},
|
||||
/// `ListDescendants` result: all descendant containers, with running
|
||||
/// status. Ordered by topology depth (parents before children), then
|
||||
/// alphabetically within each depth tier.
|
||||
Containers { containers: Vec<ContainerInfo> },
|
||||
/// `Recv` result when a graceful stop is pending for this agent
|
||||
/// (set by hive-c0re's `GracefulStop` orchestration). Returned in
|
||||
/// place of `Messages` — it doubles as the inbound fence: the harness
|
||||
/// stops consuming normal inbox messages and instead runs one
|
||||
/// stop-checkpoint turn (flush durable `/state`), then reports
|
||||
/// `GracefulStopComplete` and exits its serve loop so the container
|
||||
/// can be stopped cleanly. New sends keep queueing in the broker for
|
||||
/// the agent's next start.
|
||||
GracefulStop,
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ workspace = true
|
|||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
hive-sh4re.workspace = true
|
||||
hive-agent-sock.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use std::path::PathBuf;
|
|||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use hive_sh4re::{AgentRequest, AgentResponse};
|
||||
use hive_agent_sock::{Request, Response};
|
||||
|
||||
/// Per-agent MCP socket, bind-mounted from the host into every container.
|
||||
const DEFAULT_SOCKET: &str = "/run/hive/mcp.sock";
|
||||
|
|
@ -50,17 +50,17 @@ async fn main() -> Result<()> {
|
|||
} else {
|
||||
cli.body
|
||||
};
|
||||
let resp: AgentResponse = client::request(
|
||||
let resp: Response = client::request(
|
||||
&cli.socket,
|
||||
&AgentRequest::Wake {
|
||||
&Request::Wake {
|
||||
from: cli.from,
|
||||
body,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
match resp {
|
||||
AgentResponse::Ok => Ok(()),
|
||||
AgentResponse::Err { message } => anyhow::bail!("wake: {message}"),
|
||||
Response::Ok => Ok(()),
|
||||
Response::Err { message } => anyhow::bail!("wake: {message}"),
|
||||
other => anyhow::bail!("wake: unexpected response {other:?}"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ time.workspace = true
|
|||
futures-util = "0.3"
|
||||
clap.workspace = true
|
||||
hive-claude.workspace = true
|
||||
hive-agent-sock.workspace = true
|
||||
hive-sh4re.workspace = true
|
||||
rmcp.workspace = true
|
||||
rusqlite.workspace = true
|
||||
|
|
|
|||
|
|
@ -1083,11 +1083,11 @@ async fn poll_once(
|
|||
continue;
|
||||
};
|
||||
|
||||
let req = hive_sh4re::Request::Wake {
|
||||
let req = hive_agent_sock::Request::Wake {
|
||||
from: "forge".to_owned(),
|
||||
body,
|
||||
};
|
||||
let deliver_result = crate::client::request::<_, hive_sh4re::Response>(socket, &req)
|
||||
let deliver_result = crate::client::request::<_, hive_agent_sock::Response>(socket, &req)
|
||||
.await
|
||||
.map(|_| ());
|
||||
match deliver_result {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ use crate::login::LoginState;
|
|||
use crate::turn_stats::TurnStats;
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use hive_sh4re::{AgentRequest, AgentResponse, HelperEvent, SYSTEM_SENDER};
|
||||
use hive_agent_sock::{Request, Response};
|
||||
use hive_sh4re::{HelperEvent, SYSTEM_SENDER};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "hive-agent", about = "hyperhive harness serve loop")]
|
||||
|
|
@ -190,9 +191,8 @@ fn graceful_stop_message() -> hive_sh4re::DeliveredMessage {
|
|||
|
||||
// ---------- surface trait ----------
|
||||
|
||||
/// What a `Recv` long-poll returned. Decoupled from the per-role
|
||||
/// Response enum so `serve_loop` can pattern-match without seeing
|
||||
/// either `AgentResponse` or `ManagerResponse` directly.
|
||||
/// What a `Recv` long-poll returned. Decoupled from the `Response`
|
||||
/// enum so `serve_loop` can pattern-match without seeing it directly.
|
||||
enum RecvOutcome {
|
||||
/// Long-poll returned at least one message; first one is detached.
|
||||
Message(hive_sh4re::DeliveredMessage),
|
||||
|
|
@ -212,7 +212,7 @@ enum RecvOutcome {
|
|||
/// Wire surface abstraction. `AgentSurface` is the only impl — the trait
|
||||
/// exists to keep the turn loop generic and testable. Every function that
|
||||
/// talks to the broker goes through this so there are zero hard-coded
|
||||
/// `AgentRequest` / `AgentResponse` references in the turn loop itself.
|
||||
/// `Request` / `Response` references in the turn loop itself.
|
||||
trait Surface {
|
||||
/// Ack the in-flight turn. Logs warnings on transport/broker
|
||||
/// errors but never propagates — turn loop continues either way.
|
||||
|
|
@ -251,17 +251,17 @@ trait Surface {
|
|||
// ---------- AgentSurface ----------
|
||||
|
||||
/// Zero-sized type tag for the agent wire surface.
|
||||
/// Talks `AgentRequest` / `AgentResponse`.
|
||||
/// Talks `Request` / `Response`.
|
||||
struct AgentSurface;
|
||||
|
||||
/// Issue an `Ok`-expecting fire-and-forget broker request, logging any
|
||||
/// rejection / unexpected response / transport error under `label`. Shared by
|
||||
/// the `Surface` methods that don't need the reply (`ack_turn`,
|
||||
/// `requeue_inflight`, `graceful_stop_complete`).
|
||||
async fn fire_and_forget(socket: &Path, req: AgentRequest, label: &str) {
|
||||
match client::request::<_, AgentResponse>(socket, &req).await {
|
||||
Ok(AgentResponse::Ok) => {}
|
||||
Ok(AgentResponse::Err { message }) => {
|
||||
async fn fire_and_forget(socket: &Path, req: Request, label: &str) {
|
||||
match client::request::<_, Response>(socket, &req).await {
|
||||
Ok(Response::Ok) => {}
|
||||
Ok(Response::Err { message }) => {
|
||||
tracing::warn!(%message, "{label} rejected by broker");
|
||||
}
|
||||
Ok(other) => tracing::warn!(?other, "{label} unexpected response"),
|
||||
|
|
@ -271,55 +271,53 @@ async fn fire_and_forget(socket: &Path, req: AgentRequest, label: &str) {
|
|||
|
||||
impl Surface for AgentSurface {
|
||||
async fn ack_turn(socket: &Path) {
|
||||
fire_and_forget(socket, AgentRequest::AckTurn, "ack_turn").await;
|
||||
fire_and_forget(socket, Request::AckTurn, "ack_turn").await;
|
||||
}
|
||||
|
||||
async fn requeue_inflight(socket: &Path) {
|
||||
fire_and_forget(socket, AgentRequest::RequeueInflight, "requeue_inflight").await;
|
||||
fire_and_forget(socket, Request::RequeueInflight, "requeue_inflight").await;
|
||||
}
|
||||
|
||||
async fn graceful_stop_complete(socket: &Path) {
|
||||
fire_and_forget(
|
||||
socket,
|
||||
AgentRequest::GracefulStopComplete,
|
||||
Request::GracefulStopComplete,
|
||||
"graceful_stop_complete",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn inbox_unread(socket: &Path) -> u64 {
|
||||
match client::request::<_, AgentResponse>(socket, &AgentRequest::Status).await {
|
||||
Ok(AgentResponse::Status { unread }) => unread,
|
||||
match client::request::<_, Response>(socket, &Request::Status).await {
|
||||
Ok(Response::Status { unread }) => unread,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
async fn post_turn_counts(socket: &Path) -> (Option<u64>, Option<u64>) {
|
||||
let threads = match client::request::<_, AgentResponse>(
|
||||
let threads =
|
||||
match client::request::<_, Response>(socket, &Request::GetLooseEnds { agent: None })
|
||||
.await
|
||||
{
|
||||
Ok(Response::LooseEnds { loose_ends }) => u64::try_from(loose_ends.len()).ok(),
|
||||
_ => None,
|
||||
};
|
||||
let reminders = match client::request::<_, Response>(
|
||||
socket,
|
||||
&AgentRequest::GetLooseEnds { agent: None },
|
||||
&Request::CountPendingReminders { agent: None },
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(AgentResponse::LooseEnds { loose_ends }) => u64::try_from(loose_ends.len()).ok(),
|
||||
_ => None,
|
||||
};
|
||||
let reminders = match client::request::<_, AgentResponse>(
|
||||
socket,
|
||||
&AgentRequest::CountPendingReminders { agent: None },
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(AgentResponse::PendingRemindersCount { count }) => Some(count),
|
||||
Ok(Response::PendingRemindersCount { count }) => Some(count),
|
||||
_ => None,
|
||||
};
|
||||
(threads, reminders)
|
||||
}
|
||||
|
||||
async fn send_to_parent(socket: &Path, body: String) {
|
||||
let res = client::request::<_, AgentResponse>(
|
||||
let res = client::request::<_, Response>(
|
||||
socket,
|
||||
&AgentRequest::Send {
|
||||
&Request::Send {
|
||||
to: hive_sh4re::PARENT_RECIPIENT.into(),
|
||||
body,
|
||||
in_reply_to: None,
|
||||
|
|
@ -332,22 +330,22 @@ impl Surface for AgentSurface {
|
|||
}
|
||||
|
||||
async fn recv_next(socket: &Path) -> RecvOutcome {
|
||||
let recv: Result<AgentResponse> = client::request(
|
||||
let recv: Result<Response> = client::request(
|
||||
socket,
|
||||
&AgentRequest::Recv {
|
||||
&Request::Recv {
|
||||
wait_seconds: Some(180),
|
||||
max: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
match recv {
|
||||
Ok(AgentResponse::Messages { messages, .. }) if !messages.is_empty() => {
|
||||
Ok(Response::Messages { messages, .. }) if !messages.is_empty() => {
|
||||
let first = messages.into_iter().next().expect("checked non-empty");
|
||||
RecvOutcome::Message(first)
|
||||
}
|
||||
Ok(AgentResponse::Messages { .. }) => RecvOutcome::Empty,
|
||||
Ok(AgentResponse::GracefulStop) => RecvOutcome::GracefulStop,
|
||||
Ok(AgentResponse::Err { message }) => {
|
||||
Ok(Response::Messages { .. }) => RecvOutcome::Empty,
|
||||
Ok(Response::GracefulStop) => RecvOutcome::GracefulStop,
|
||||
Ok(Response::Err { message }) => {
|
||||
tracing::warn!(%message, "recv error");
|
||||
RecvOutcome::TransportError
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,15 +23,20 @@ pub(super) async fn post_send(
|
|||
if body.is_empty() {
|
||||
return error_response(StatusCode::BAD_REQUEST, "send: `body` required");
|
||||
}
|
||||
match super::broker_request(&state.socket, &hive_sh4re::Request::OperatorMsg { body }).await {
|
||||
match super::broker_request(
|
||||
&state.socket,
|
||||
&hive_agent_sock::Request::OperatorMsg { body },
|
||||
)
|
||||
.await
|
||||
{
|
||||
// 200 instead of 303 → the client doesn't refetch /api/state.
|
||||
// The operator message becomes a broker `Sent` (already shown
|
||||
// server-side in the dashboard); on the agent side, the
|
||||
// resulting `TurnStart` SSE event drives the terminal + the
|
||||
// inbox row gets consumed by the time `TurnEnd` fires the
|
||||
// existing turn-end refresh.
|
||||
Ok(hive_sh4re::Response::Ok) => (axum::http::StatusCode::OK, "ok").into_response(),
|
||||
Ok(hive_sh4re::Response::Err { message }) => error_response(
|
||||
Ok(hive_agent_sock::Response::Ok) => (axum::http::StatusCode::OK, "ok").into_response(),
|
||||
Ok(hive_agent_sock::Response::Err { message }) => error_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&format!("send failed: {message}"),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -302,11 +302,11 @@ enum BrokerError {
|
|||
/// web-UI handler that talks to the broker goes through it.
|
||||
async fn broker_request(
|
||||
socket: &Path,
|
||||
req: &hive_sh4re::Request,
|
||||
) -> std::result::Result<hive_sh4re::Response, BrokerError> {
|
||||
req: &hive_agent_sock::Request,
|
||||
) -> std::result::Result<hive_agent_sock::Response, BrokerError> {
|
||||
match tokio::time::timeout(
|
||||
SOCKET_FETCH_TIMEOUT,
|
||||
crate::client::request::<_, hive_sh4re::Response>(socket, req),
|
||||
crate::client::request::<_, hive_agent_sock::Response>(socket, req),
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
|
|
|||
|
|
@ -366,8 +366,8 @@ async fn recent_inbox(socket: &std::path::Path) -> Vec<hive_sh4re::InboxRow> {
|
|||
const LIMIT: u64 = 30;
|
||||
// Deadline-bounded (via `broker_request`): `/api/state` must render even
|
||||
// when hive-c0re is busy — an empty inbox section beats a hung snapshot.
|
||||
match super::broker_request(socket, &hive_sh4re::Request::Recent { limit: LIMIT }).await {
|
||||
Ok(hive_sh4re::Response::Recent { rows }) => rows,
|
||||
match super::broker_request(socket, &hive_agent_sock::Request::Recent { limit: LIMIT }).await {
|
||||
Ok(hive_agent_sock::Response::Recent { rows }) => rows,
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,14 +35,14 @@ async fn fetch_reminder_stats(
|
|||
) -> Option<hive_sh4re::ReminderStats> {
|
||||
match super::broker_request(
|
||||
socket,
|
||||
&hive_sh4re::Request::ReminderRollup {
|
||||
&hive_agent_sock::Request::ReminderRollup {
|
||||
since_secs: window_secs,
|
||||
agent: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(hive_sh4re::Response::ReminderRollup(stats)) => Some(stats),
|
||||
Ok(hive_agent_sock::Response::ReminderRollup(stats)) => Some(stats),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -57,14 +57,14 @@ async fn fetch_reminder_stats(
|
|||
pub(super) async fn api_loose_ends(State(state): State<AppState>) -> Response {
|
||||
match super::broker_request(
|
||||
&state.socket,
|
||||
&hive_sh4re::Request::GetLooseEnds { agent: None },
|
||||
&hive_agent_sock::Request::GetLooseEnds { agent: None },
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(hive_sh4re::Response::LooseEnds { loose_ends }) => {
|
||||
Ok(hive_agent_sock::Response::LooseEnds { loose_ends }) => {
|
||||
axum::Json(serde_json::json!({ "loose_ends": loose_ends })).into_response()
|
||||
}
|
||||
Ok(hive_sh4re::Response::Err { message }) => error_response(
|
||||
Ok(hive_agent_sock::Response::Err { message }) => error_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&format!("get_loose_ends: {message}"),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ workspace = true
|
|||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
hive-agent-sock.workspace = true
|
||||
hive-sh4re.workspace = true
|
||||
libc.workspace = true
|
||||
rmcp.workspace = true
|
||||
|
|
|
|||
|
|
@ -760,7 +760,7 @@ pub(crate) async fn send_wake(
|
|||
);
|
||||
}
|
||||
}
|
||||
let req = hive_sh4re::AgentRequest::Wake {
|
||||
let req = hive_agent_sock::Request::Wake {
|
||||
from: format!("bash-task-{id}"),
|
||||
body,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ opentelemetry-otlp = { version = "0.32", default-features = false, features = [
|
|||
"reqwest-rustls",
|
||||
] }
|
||||
indicatif.workspace = true
|
||||
hive-agent-sock.workspace = true
|
||||
hive-sh4re.workspace = true
|
||||
hive-host-sock.workspace = true
|
||||
hive-priv-sock.workspace = true
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ pub const MESSAGE_MAX_BYTES: usize = 4096;
|
|||
|
||||
/// Validate that `body` fits under [`MESSAGE_MAX_BYTES`]. Returns a
|
||||
/// caller-ready error string (caller wraps in
|
||||
/// `AgentResponse::Err`/`ManagerResponse::Err`) on failure.
|
||||
/// `Response::Err`) on failure.
|
||||
///
|
||||
/// `label` shows up in the error message verbatim — pass a short
|
||||
/// noun like `"send"`, `"question"`, `"broadcast"` so the model can
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue