127 lines
4.6 KiB
Rust
127 lines
4.6 KiB
Rust
//! Pure helpers factored out of the harness serve loop
|
|
//! (`bin/hive-agent.rs`).
|
|
//! Only functions with no wire-type dependency live here;
|
|
//! request/response-flavored helpers (`requeue_inflight`, `ack_turn`, etc.)
|
|
//! stay in the binary because they touch the request enum variants directly.
|
|
|
|
use crate::events::Bus;
|
|
use crate::turn::{TurnError, 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. `id` is the broker row
|
|
/// id, rendered as a `[msg #<id>]` marker so the agent can reference it in
|
|
/// `ack_until`. `unread` is the inbox depth after this message was popped.
|
|
/// `redelivered` prepends a "may already be handled" banner; `interrupted`
|
|
/// prepends a "previous turn was /cancel'd" banner instead (the two are
|
|
/// mutually exclusive in practice — a redelivered message means the harness
|
|
/// itself restarted, which also clears the in-memory interrupted flag — so
|
|
/// `redelivered` takes priority if both were somehow set).
|
|
#[must_use]
|
|
pub fn format_wake_prompt(
|
|
id: i64,
|
|
from: &str,
|
|
body: &str,
|
|
unread: u64,
|
|
redelivered: bool,
|
|
interrupted: bool,
|
|
) -> String {
|
|
let banner = if redelivered {
|
|
hive_sh4re::REDELIVERY_HINT
|
|
} else if interrupted {
|
|
hive_sh4re::INTERRUPTED_HINT
|
|
} else {
|
|
""
|
|
};
|
|
let tag = if id > 0 {
|
|
format!("[msg #{id}] ")
|
|
} else {
|
|
String::new()
|
|
};
|
|
let pending = hive_sh4re::pending_hint(unread);
|
|
format!("{banner}{tag}Incoming message from `{from}`:\n---\n{body}\n---{pending}")
|
|
}
|
|
|
|
/// Field-named args for [`build_row`]. Mirrors the turn-stats row
|
|
/// columns; `outcome` and `bus` borrow for the duration of the call.
|
|
pub struct TurnRowArgs<'a> {
|
|
pub started_at: i64,
|
|
pub ended_at: i64,
|
|
pub duration_ms: i64,
|
|
pub model: String,
|
|
pub wake_from: String,
|
|
pub outcome: &'a TurnOutcome,
|
|
pub bus: &'a Bus,
|
|
pub open_threads_count: Option<u64>,
|
|
pub open_reminders_count: Option<u64>,
|
|
}
|
|
|
|
/// Assemble a `TurnStatRow` from the harness's per-turn state. Lives here
|
|
/// (rather than inline in the serve loop) so it stays wire-type-free
|
|
/// and unit-testable; the binary just feeds it the post-turn counts.
|
|
#[must_use]
|
|
pub fn build_row(args: TurnRowArgs<'_>) -> TurnStatRow {
|
|
let TurnRowArgs {
|
|
started_at,
|
|
ended_at,
|
|
duration_ms,
|
|
model,
|
|
wake_from,
|
|
outcome,
|
|
bus,
|
|
open_threads_count,
|
|
open_reminders_count,
|
|
} = args;
|
|
// Prefer the API-resolved model id (e.g. `claude-opus-4-8`) captured
|
|
// from this turn's assistant events over the requested `--model`
|
|
// name/alias, so the model-mix + cost rollup label the concrete
|
|
// version that ran. Falls back to the requested name on a degenerate
|
|
// turn that produced no assistant event.
|
|
let model = bus.last_resolved_model().unwrap_or(model);
|
|
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 {
|
|
Ok(false) => ("ok", None),
|
|
Ok(true) => ("compacted", None),
|
|
Err(TurnError::PromptTooLong) => ("prompt_too_long", None),
|
|
Err(TurnError::RateLimited) => ("rate_limited", None),
|
|
Err(TurnError::AuthFailed) => ("auth_failed", None),
|
|
Err(TurnError::SessionNotFound) => ("session_not_found", None),
|
|
Err(TurnError::ApiStall) => ("api_stall", None),
|
|
Err(TurnError::Failed(e)) => ("failed", Some(format!("{e:#}"))),
|
|
};
|
|
let wake_from = if wake_from.starts_with("bash-task-") {
|
|
"bash-task".to_owned()
|
|
} else {
|
|
wake_from
|
|
};
|
|
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,
|
|
session_id: bus.current_session_id(),
|
|
}
|
|
}
|