//! Helpers shared between `hive-ag3nt` (agent) and `hive-m1nd` (manager) //! serve loops. Only pure functions with no wire-type dependency live here; //! request/response-flavored helpers (`requeue_inflight`, `ack_turn`, etc.) //! stay in each binary because they use different request enum variants. use crate::events::Bus; use crate::mcp::REDELIVERY_HINT; use crate::turn::TurnOutcome; use crate::turn_stats::TurnStatRow; /// Assemble the per-turn wake prompt string. The role/tools/etc. live in the /// system prompt; this is just the wake signal body. `unread` is the inbox /// depth after this message was popped. `redelivered` prepends a "may already /// be handled" banner. #[must_use] pub fn format_wake_prompt(from: &str, body: &str, unread: u64, redelivered: bool) -> String { let banner = if redelivered { REDELIVERY_HINT } else { "" }; let pending = if unread == 0 { String::new() } else { format!( "\n\n({unread} more message(s) pending in your inbox — call `mcp__hyperhive__recv` \ with `max: {unread}` to drain them all in one round-trip before acting.)" ) }; format!("{banner}Incoming message from `{from}`:\n---\n{body}\n---{pending}") } /// Current time as a Unix timestamp (seconds). Returns 0 on any error. #[must_use] pub fn now_unix() -> i64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .ok() .and_then(|d| i64::try_from(d.as_secs()).ok()) .unwrap_or(0) } /// Assemble a `TurnStatRow` from the harness's per-turn state. Used by both /// the agent and manager serve loops — the shape is identical, only the /// post-turn count fetch helpers differ (and those stay in each binary). #[must_use] #[allow(clippy::too_many_arguments)] pub fn build_row( started_at: i64, ended_at: i64, duration_ms: i64, model: String, wake_from: String, outcome: &TurnOutcome, bus: &Bus, open_threads_count: Option, open_reminders_count: Option, ) -> TurnStatRow { let cost = bus.last_cost_usage().unwrap_or_default(); let ctx = bus.last_ctx_usage().unwrap_or(cost); let tool_calls = bus.take_tool_calls(); let tool_call_count: u64 = tool_calls.values().copied().sum(); let tool_call_breakdown_json = if tool_calls.is_empty() { None } else { serde_json::to_string(&tool_calls).ok() }; let (result_kind, note) = match outcome { TurnOutcome::Ok => ("ok", None), TurnOutcome::Compacted => ("compacted", None), TurnOutcome::PromptTooLong => ("prompt_too_long", None), TurnOutcome::RateLimited => ("rate_limited", None), TurnOutcome::Failed(e) => ("failed", Some(format!("{e:#}"))), }; TurnStatRow { started_at, ended_at, duration_ms, model, wake_from, input_tokens: cost.input_tokens, output_tokens: cost.output_tokens, cache_read_input_tokens: cost.cache_read_input_tokens, cache_creation_input_tokens: cost.cache_creation_input_tokens, last_input_tokens: ctx.input_tokens, last_output_tokens: ctx.output_tokens, last_cache_read_input_tokens: ctx.cache_read_input_tokens, last_cache_creation_input_tokens: ctx.cache_creation_input_tokens, tool_call_count, tool_call_breakdown_json, open_threads_count, open_reminders_count, result_kind, note, } }