docs: add missing #[must_use], # Errors, # Panics across public api
This commit is contained in:
parent
748536203b
commit
908cadb151
10 changed files with 157 additions and 0 deletions
|
|
@ -19,6 +19,11 @@ const RETRY_BACKOFFS_MS: &[u64] = &[2_000, 4_000, 8_000, 16_000, 30_000];
|
||||||
/// the retry count. Use this from non-tool callers (the harness serve
|
/// the retry count. Use this from non-tool callers (the harness serve
|
||||||
/// loop, web UI, CLI subcommands) where we just want the socket-restart
|
/// loop, web UI, CLI subcommands) where we just want the socket-restart
|
||||||
/// resilience without surfacing the bookkeeping.
|
/// resilience without surfacing the bookkeeping.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the socket is unreachable after all retries, or if
|
||||||
|
/// serialization / deserialization of the request or response fails.
|
||||||
pub async fn request<Req, Resp>(socket: &Path, req: &Req) -> Result<Resp>
|
pub async fn request<Req, Resp>(socket: &Path, req: &Req) -> Result<Resp>
|
||||||
where
|
where
|
||||||
Req: Serialize + ?Sized,
|
Req: Serialize + ?Sized,
|
||||||
|
|
@ -33,6 +38,16 @@ where
|
||||||
/// retries happened — that way claude knows the prior socket flake
|
/// retries happened — that way claude knows the prior socket flake
|
||||||
/// wasn't a content error and shouldn't trigger an LLM-level retry of
|
/// wasn't a content error and shouldn't trigger an LLM-level retry of
|
||||||
/// its own.
|
/// its own.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if all retries are exhausted, or on a fatal protocol
|
||||||
|
/// error (serialization / deserialization failure).
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if `RETRY_BACKOFFS_MS.len()` does not fit in a `u32`, which
|
||||||
|
/// cannot happen with the current compile-time constant.
|
||||||
pub async fn request_retried<Req, Resp>(socket: &Path, req: &Req) -> Result<(Resp, u32)>
|
pub async fn request_retried<Req, Resp>(socket: &Path, req: &Req) -> Result<(Resp, u32)>
|
||||||
where
|
where
|
||||||
Req: Serialize + ?Sized,
|
Req: Serialize + ?Sized,
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,7 @@ pub struct TokenUsage {
|
||||||
|
|
||||||
impl TokenUsage {
|
impl TokenUsage {
|
||||||
/// Total context consumed this turn (input + cache reads + cache writes).
|
/// Total context consumed this turn (input + cache reads + cache writes).
|
||||||
|
#[must_use]
|
||||||
pub fn context_tokens(&self) -> u64 {
|
pub fn context_tokens(&self) -> u64 {
|
||||||
self.input_tokens + self.cache_read_input_tokens + self.cache_creation_input_tokens
|
self.input_tokens + self.cache_read_input_tokens + self.cache_creation_input_tokens
|
||||||
}
|
}
|
||||||
|
|
@ -230,6 +231,7 @@ impl TokenUsage {
|
||||||
/// **cumulative** sum across every inference in the turn — useful as a
|
/// **cumulative** sum across every inference in the turn — useful as a
|
||||||
/// cost signal, but NOT the current context size (a tool-heavy turn
|
/// cost signal, but NOT the current context size (a tool-heavy turn
|
||||||
/// sums per-call cached prompts and easily exceeds the model window).
|
/// sums per-call cached prompts and easily exceeds the model window).
|
||||||
|
#[must_use]
|
||||||
pub fn from_stream_event(v: &serde_json::Value) -> Option<Self> {
|
pub fn from_stream_event(v: &serde_json::Value) -> Option<Self> {
|
||||||
if v.get("type").and_then(|t| t.as_str()) != Some("result") {
|
if v.get("type").and_then(|t| t.as_str()) != Some("result") {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -241,6 +243,7 @@ impl TokenUsage {
|
||||||
/// `.message.usage` block. Each turn fires one of these for every
|
/// `.message.usage` block. Each turn fires one of these for every
|
||||||
/// model call; tracking the LAST one over the turn gives the actual
|
/// model call; tracking the LAST one over the turn gives the actual
|
||||||
/// conversation context size — the number to watch for compaction.
|
/// conversation context size — the number to watch for compaction.
|
||||||
|
#[must_use]
|
||||||
pub fn from_assistant_event(v: &serde_json::Value) -> Option<Self> {
|
pub fn from_assistant_event(v: &serde_json::Value) -> Option<Self> {
|
||||||
if v.get("type").and_then(|t| t.as_str()) != Some("assistant") {
|
if v.get("type").and_then(|t| t.as_str()) != Some("assistant") {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -443,12 +446,17 @@ impl Bus {
|
||||||
|
|
||||||
/// Take + clear the one-shot. Returns true iff the caller should
|
/// Take + clear the one-shot. Returns true iff the caller should
|
||||||
/// run claude without `--continue` for this turn.
|
/// run claude without `--continue` for this turn.
|
||||||
|
#[must_use]
|
||||||
pub fn take_skip_continue(&self) -> bool {
|
pub fn take_skip_continue(&self) -> bool {
|
||||||
self.skip_continue_once.swap(false, Ordering::SeqCst)
|
self.skip_continue_once.swap(false, Ordering::SeqCst)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Currently-selected claude model name. Read on every turn so a
|
/// Currently-selected claude model name. Read on every turn so a
|
||||||
/// `/model <name>` flip takes effect on the next turn.
|
/// `/model <name>` flip takes effect on the next turn.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the internal lock is poisoned.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn model(&self) -> String {
|
pub fn model(&self) -> String {
|
||||||
self.model.lock().unwrap().clone()
|
self.model.lock().unwrap().clone()
|
||||||
|
|
@ -459,6 +467,10 @@ impl Bus {
|
||||||
/// state dir (`hyperhive-model`) so the override survives harness
|
/// state dir (`hyperhive-model`) so the override survives harness
|
||||||
/// restart and container rebuild (gone on `--purge`, matching
|
/// restart and container rebuild (gone on `--purge`, matching
|
||||||
/// every other piece of agent state).
|
/// every other piece of agent state).
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the internal lock is poisoned.
|
||||||
pub fn set_model(&self, name: impl Into<String>) {
|
pub fn set_model(&self, name: impl Into<String>) {
|
||||||
let value: String = name.into();
|
let value: String = name.into();
|
||||||
self.model.lock().unwrap().clone_from(&value);
|
self.model.lock().unwrap().clone_from(&value);
|
||||||
|
|
@ -472,6 +484,10 @@ impl Bus {
|
||||||
/// emitting a SSE event. Used by the bin entrypoints to backfill
|
/// emitting a SSE event. Used by the bin entrypoints to backfill
|
||||||
/// from the most recent `turn_stats` row so the per-agent web UI's
|
/// from the most recent `turn_stats` row so the per-agent web UI's
|
||||||
/// ctx + cost badges paint real numbers on cold load.
|
/// ctx + cost badges paint real numbers on cold load.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if an internal lock is poisoned.
|
||||||
pub fn seed_usage(&self, ctx: Option<TokenUsage>, cost: Option<TokenUsage>) {
|
pub fn seed_usage(&self, ctx: Option<TokenUsage>, cost: Option<TokenUsage>) {
|
||||||
if ctx.is_some() {
|
if ctx.is_some() {
|
||||||
*self.last_ctx_usage.lock().unwrap() = ctx;
|
*self.last_ctx_usage.lock().unwrap() = ctx;
|
||||||
|
|
@ -485,6 +501,10 @@ impl Bus {
|
||||||
/// usage (current context size); `cost` is the cumulative across
|
/// usage (current context size); `cost` is the cumulative across
|
||||||
/// every inference in the turn (cost signal). One SSE event fires
|
/// every inference in the turn (cost signal). One SSE event fires
|
||||||
/// per turn carrying both.
|
/// per turn carrying both.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if an internal lock is poisoned.
|
||||||
pub fn record_turn_usage(&self, ctx: TokenUsage, cost: TokenUsage) {
|
pub fn record_turn_usage(&self, ctx: TokenUsage, cost: TokenUsage) {
|
||||||
*self.last_ctx_usage.lock().unwrap() = Some(ctx);
|
*self.last_ctx_usage.lock().unwrap() = Some(ctx);
|
||||||
*self.last_cost_usage.lock().unwrap() = Some(cost);
|
*self.last_cost_usage.lock().unwrap() = Some(cost);
|
||||||
|
|
@ -503,6 +523,10 @@ impl Bus {
|
||||||
/// per-turn counter for each one we find. Called by the stdout
|
/// per-turn counter for each one we find. Called by the stdout
|
||||||
/// pump on every parsed line. Cheap when the line isn't an
|
/// pump on every parsed line. Cheap when the line isn't an
|
||||||
/// assistant message — the field-check short-circuits.
|
/// assistant message — the field-check short-circuits.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the internal lock is poisoned.
|
||||||
pub fn observe_stream(&self, v: &serde_json::Value) {
|
pub fn observe_stream(&self, v: &serde_json::Value) {
|
||||||
if v.get("type").and_then(|t| t.as_str()) != Some("assistant") {
|
if v.get("type").and_then(|t| t.as_str()) != Some("assistant") {
|
||||||
return;
|
return;
|
||||||
|
|
@ -531,6 +555,10 @@ impl Bus {
|
||||||
/// Snapshot + clear the per-turn tool-call counter. The harness
|
/// Snapshot + clear the per-turn tool-call counter. The harness
|
||||||
/// calls this between turns to fold the breakdown into a
|
/// calls this between turns to fold the breakdown into a
|
||||||
/// `turn_stats` row, then start the next turn with an empty map.
|
/// `turn_stats` row, then start the next turn with an empty map.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the internal lock is poisoned.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn take_tool_calls(&self) -> std::collections::HashMap<String, u64> {
|
pub fn take_tool_calls(&self) -> std::collections::HashMap<String, u64> {
|
||||||
std::mem::take(&mut *self.tool_calls.lock().unwrap())
|
std::mem::take(&mut *self.tool_calls.lock().unwrap())
|
||||||
|
|
@ -538,6 +566,10 @@ impl Bus {
|
||||||
|
|
||||||
/// Last context-size snapshot (last inference of the most recent
|
/// Last context-size snapshot (last inference of the most recent
|
||||||
/// turn), or `None` if no turn has completed yet.
|
/// turn), or `None` if no turn has completed yet.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the internal lock is poisoned.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn last_ctx_usage(&self) -> Option<TokenUsage> {
|
pub fn last_ctx_usage(&self) -> Option<TokenUsage> {
|
||||||
*self.last_ctx_usage.lock().unwrap()
|
*self.last_ctx_usage.lock().unwrap()
|
||||||
|
|
@ -545,6 +577,10 @@ impl Bus {
|
||||||
|
|
||||||
/// Last cumulative cost snapshot (sum across the most recent turn's
|
/// Last cumulative cost snapshot (sum across the most recent turn's
|
||||||
/// inferences), or `None` if no turn has completed yet.
|
/// inferences), or `None` if no turn has completed yet.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the internal lock is poisoned.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn last_cost_usage(&self) -> Option<TokenUsage> {
|
pub fn last_cost_usage(&self) -> Option<TokenUsage> {
|
||||||
*self.last_cost_usage.lock().unwrap()
|
*self.last_cost_usage.lock().unwrap()
|
||||||
|
|
@ -552,6 +588,10 @@ impl Bus {
|
||||||
|
|
||||||
/// Update the harness's authoritative turn-loop state. Records
|
/// Update the harness's authoritative turn-loop state. Records
|
||||||
/// the transition time so `state_snapshot` can return a since-age.
|
/// the transition time so `state_snapshot` can return a since-age.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the internal lock is poisoned.
|
||||||
pub fn set_state(&self, next: TurnState) {
|
pub fn set_state(&self, next: TurnState) {
|
||||||
let since;
|
let since;
|
||||||
{
|
{
|
||||||
|
|
@ -598,6 +638,10 @@ impl Bus {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Current state + since-when (unix seconds). Snapshot copy, no lock held.
|
/// Current state + since-when (unix seconds). Snapshot copy, no lock held.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the internal lock is poisoned.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn state_snapshot(&self) -> (TurnState, i64) {
|
pub fn state_snapshot(&self) -> (TurnState, i64) {
|
||||||
*self.state.lock().unwrap()
|
*self.state.lock().unwrap()
|
||||||
|
|
@ -617,6 +661,7 @@ impl Bus {
|
||||||
let _ = self.tx.send(envelope);
|
let _ = self.tx.send(envelope);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn subscribe(&self) -> broadcast::Receiver<BusEvent> {
|
pub fn subscribe(&self) -> broadcast::Receiver<BusEvent> {
|
||||||
self.tx.subscribe()
|
self.tx.subscribe()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,11 @@ impl LoginSession {
|
||||||
/// `HYPERHIVE_LOGIN_CMD` (single string, shell-split into argv); by
|
/// `HYPERHIVE_LOGIN_CMD` (single string, shell-split into argv); by
|
||||||
/// default we run `claude auth login`. Failing to spawn returns an error
|
/// default we run `claude auth login`. Failing to spawn returns an error
|
||||||
/// before any state is registered.
|
/// before any state is registered.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if spawning the login command fails, or if the child's
|
||||||
|
/// stdio handles cannot be acquired.
|
||||||
pub fn start() -> Result<Self> {
|
pub fn start() -> Result<Self> {
|
||||||
let (cmd, args) = resolve_command();
|
let (cmd, args) = resolve_command();
|
||||||
tracing::info!(%cmd, ?args, "spawning login session");
|
tracing::info!(%cmd, ?args, "spawning login session");
|
||||||
|
|
@ -82,6 +87,11 @@ impl LoginSession {
|
||||||
/// Write `code` (plus a newline) to the child's stdin. Returns an error
|
/// Write `code` (plus a newline) to the child's stdin. Returns an error
|
||||||
/// if the stdin has already been closed (e.g. after the child exited or
|
/// if the stdin has already been closed (e.g. after the child exited or
|
||||||
/// after a prior submission consumed it).
|
/// after a prior submission consumed it).
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the login stdin is already closed, or if writing
|
||||||
|
/// to or flushing the stdin pipe fails.
|
||||||
pub async fn submit_code(&self, code: &str) -> Result<()> {
|
pub async fn submit_code(&self, code: &str) -> Result<()> {
|
||||||
let mut guard = self.stdin.lock().await;
|
let mut guard = self.stdin.lock().await;
|
||||||
let stdin = guard.as_mut().context("login stdin already closed")?;
|
let stdin = guard.as_mut().context("login stdin already closed")?;
|
||||||
|
|
@ -100,18 +110,34 @@ impl LoginSession {
|
||||||
let _ = self.stdin.lock().await.take();
|
let _ = self.stdin.lock().await.take();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the internal lock is poisoned.
|
||||||
|
#[must_use]
|
||||||
pub fn output(&self) -> String {
|
pub fn output(&self) -> String {
|
||||||
self.state.lock().unwrap().output.clone()
|
self.state.lock().unwrap().output.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the internal lock is poisoned.
|
||||||
|
#[must_use]
|
||||||
pub fn url(&self) -> Option<String> {
|
pub fn url(&self) -> Option<String> {
|
||||||
self.state.lock().unwrap().url.clone()
|
self.state.lock().unwrap().url.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the internal lock is poisoned.
|
||||||
|
#[must_use]
|
||||||
pub fn finished(&self) -> bool {
|
pub fn finished(&self) -> bool {
|
||||||
self.state.lock().unwrap().finished
|
self.state.lock().unwrap().finished
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the internal lock is poisoned.
|
||||||
|
#[must_use]
|
||||||
pub fn exit_note(&self) -> Option<String> {
|
pub fn exit_note(&self) -> Option<String> {
|
||||||
self.state.lock().unwrap().exit_note.clone()
|
self.state.lock().unwrap().exit_note.clone()
|
||||||
}
|
}
|
||||||
|
|
@ -119,6 +145,10 @@ impl LoginSession {
|
||||||
/// Best-effort: poll the child once and update `finished`/`exit_note`.
|
/// Best-effort: poll the child once and update `finished`/`exit_note`.
|
||||||
/// Called by the web UI on each render so the state stays fresh without
|
/// Called by the web UI on each render so the state stays fresh without
|
||||||
/// running a dedicated reaper task.
|
/// running a dedicated reaper task.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if an internal lock is poisoned.
|
||||||
pub fn poll(&self) {
|
pub fn poll(&self) {
|
||||||
let mut child = self.child.lock().unwrap();
|
let mut child = self.child.lock().unwrap();
|
||||||
match child.try_wait() {
|
match child.try_wait() {
|
||||||
|
|
@ -137,6 +167,10 @@ impl LoginSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kill the child if it's still running. Idempotent.
|
/// Kill the child if it's still running. Idempotent.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the internal lock is poisoned.
|
||||||
pub fn kill(&self) {
|
pub fn kill(&self) {
|
||||||
if let Err(e) = self.child.lock().unwrap().start_kill() {
|
if let Err(e) = self.child.lock().unwrap().start_kill() {
|
||||||
tracing::warn!(error = ?e, "kill login child");
|
tracing::warn!(error = ?e, "kill login child");
|
||||||
|
|
@ -217,6 +251,10 @@ fn extract_url(line: &str) -> Option<String> {
|
||||||
|
|
||||||
/// Helper used by the web UI to gate "is there a session running right now"
|
/// Helper used by the web UI to gate "is there a session running right now"
|
||||||
/// without holding both this module's mutex and the `AppState`'s at once.
|
/// without holding both this module's mutex and the `AppState`'s at once.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the internal lock is poisoned.
|
||||||
pub fn drop_if_finished(slot: &Mutex<Option<Arc<LoginSession>>>) {
|
pub fn drop_if_finished(slot: &Mutex<Option<Arc<LoginSession>>>) {
|
||||||
let mut guard = slot.lock().unwrap();
|
let mut guard = slot.lock().unwrap();
|
||||||
if let Some(s) = guard.as_ref() {
|
if let Some(s) = guard.as_ref() {
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,7 @@ impl From<hive_sh4re::ManagerResponse> for SocketReply {
|
||||||
/// Format helper for "send-like" tools (anything that expects an `Ok`).
|
/// Format helper for "send-like" tools (anything that expects an `Ok`).
|
||||||
/// `tool` and `ok_msg` only appear in the result string; they don't change
|
/// `tool` and `ok_msg` only appear in the result string; they don't change
|
||||||
/// behavior.
|
/// behavior.
|
||||||
|
#[must_use]
|
||||||
pub fn format_ack(resp: Result<SocketReply, anyhow::Error>, tool: &str, ok_msg: String) -> String {
|
pub fn format_ack(resp: Result<SocketReply, anyhow::Error>, tool: &str, ok_msg: String) -> String {
|
||||||
match resp {
|
match resp {
|
||||||
Ok(SocketReply::Ok) => ok_msg,
|
Ok(SocketReply::Ok) => ok_msg,
|
||||||
|
|
@ -131,6 +132,7 @@ pub fn format_ack(resp: Result<SocketReply, anyhow::Error>, tool: &str, ok_msg:
|
||||||
/// and `---` separators between bodies so the model can tell where
|
/// and `---` separators between bodies so the model can tell where
|
||||||
/// one ends and the next begins; per-message redelivery banners
|
/// one ends and the next begins; per-message redelivery banners
|
||||||
/// included.
|
/// included.
|
||||||
|
#[must_use]
|
||||||
pub fn format_recv(resp: Result<SocketReply, anyhow::Error>) -> String {
|
pub fn format_recv(resp: Result<SocketReply, anyhow::Error>) -> String {
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
let messages = match resp {
|
let messages = match resp {
|
||||||
|
|
@ -171,6 +173,7 @@ pub const REDELIVERY_HINT: &str =
|
||||||
/// of pending approvals + questions + reminders. Empty list collapses
|
/// of pending approvals + questions + reminders. Empty list collapses
|
||||||
/// to a clear marker so claude doesn't go hunting for a payload that
|
/// to a clear marker so claude doesn't go hunting for a payload that
|
||||||
/// isn't there.
|
/// isn't there.
|
||||||
|
#[must_use]
|
||||||
pub fn format_loose_ends(resp: Result<SocketReply, anyhow::Error>) -> String {
|
pub fn format_loose_ends(resp: Result<SocketReply, anyhow::Error>) -> String {
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
let loose_ends = match resp {
|
let loose_ends = match resp {
|
||||||
|
|
@ -259,6 +262,7 @@ fn loose_end_kind_label(kind: hive_sh4re::CancelLooseEndKind) -> &'static str {
|
||||||
/// Format helper for `whoami`: renders the identity block as a short
|
/// Format helper for `whoami`: renders the identity block as a short
|
||||||
/// human-readable string. Skips fields that are `None` so the output
|
/// human-readable string. Skips fields that are `None` so the output
|
||||||
/// doesn't carry dead placeholders.
|
/// doesn't carry dead placeholders.
|
||||||
|
#[must_use]
|
||||||
pub fn format_whoami(resp: Result<SocketReply, anyhow::Error>) -> String {
|
pub fn format_whoami(resp: Result<SocketReply, anyhow::Error>) -> String {
|
||||||
match resp {
|
match resp {
|
||||||
Ok(SocketReply::Whoami {
|
Ok(SocketReply::Whoami {
|
||||||
|
|
@ -294,6 +298,7 @@ where
|
||||||
/// from "c0re flickered and the harness rode it out" — without the
|
/// from "c0re flickered and the harness rode it out" — without the
|
||||||
/// hint, a tool result that took 30s to come back looks identical to a
|
/// hint, a tool result that took 30s to come back looks identical to a
|
||||||
/// content failure and the model would burn a turn retrying it.
|
/// content failure and the model would burn a turn retrying it.
|
||||||
|
#[must_use]
|
||||||
pub fn annotate_retries(mut s: String, retries: u32) -> String {
|
pub fn annotate_retries(mut s: String, retries: u32) -> String {
|
||||||
if retries > 0 {
|
if retries > 0 {
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
|
@ -660,6 +665,11 @@ impl AgentServer {
|
||||||
impl ServerHandler for AgentServer {}
|
impl ServerHandler for AgentServer {}
|
||||||
|
|
||||||
/// Run the agent MCP server over stdio. Returns when the client disconnects.
|
/// Run the agent MCP server over stdio. Returns when the client disconnects.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the MCP server fails to initialize or the transport
|
||||||
|
/// encounters a fatal error.
|
||||||
pub async fn serve_agent_stdio(socket: PathBuf) -> Result<()> {
|
pub async fn serve_agent_stdio(socket: PathBuf) -> Result<()> {
|
||||||
let server = AgentServer::new(socket);
|
let server = AgentServer::new(socket);
|
||||||
let service = server.serve(stdio()).await?;
|
let service = server.serve(stdio()).await?;
|
||||||
|
|
@ -668,6 +678,11 @@ pub async fn serve_agent_stdio(socket: PathBuf) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the manager MCP server over stdio. Same idea, different tool surface.
|
/// Run the manager MCP server over stdio. Same idea, different tool surface.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the MCP server fails to initialize or the transport
|
||||||
|
/// encounters a fatal error.
|
||||||
pub async fn serve_manager_stdio(socket: PathBuf) -> Result<()> {
|
pub async fn serve_manager_stdio(socket: PathBuf) -> Result<()> {
|
||||||
let server = ManagerServer::new(socket);
|
let server = ManagerServer::new(socket);
|
||||||
let service = server.serve(stdio()).await?;
|
let service = server.serve(stdio()).await?;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ use crate::turn_stats::TurnStatRow;
|
||||||
/// system prompt; this is just the wake signal body. `unread` is the inbox
|
/// system prompt; this is just the wake signal body. `unread` is the inbox
|
||||||
/// depth after this message was popped. `redelivered` prepends a "may already
|
/// depth after this message was popped. `redelivered` prepends a "may already
|
||||||
/// be handled" banner.
|
/// be handled" banner.
|
||||||
|
#[must_use]
|
||||||
pub fn format_wake_prompt(from: &str, body: &str, unread: u64, redelivered: bool) -> String {
|
pub fn format_wake_prompt(from: &str, body: &str, unread: u64, redelivered: bool) -> String {
|
||||||
let banner = if redelivered { REDELIVERY_HINT } else { "" };
|
let banner = if redelivered { REDELIVERY_HINT } else { "" };
|
||||||
let pending = if unread == 0 {
|
let pending = if unread == 0 {
|
||||||
|
|
@ -26,6 +27,7 @@ pub fn format_wake_prompt(from: &str, body: &str, unread: u64, redelivered: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Current time as a Unix timestamp (seconds). Returns 0 on any error.
|
/// Current time as a Unix timestamp (seconds). Returns 0 on any error.
|
||||||
|
#[must_use]
|
||||||
pub fn now_unix() -> i64 {
|
pub fn now_unix() -> i64 {
|
||||||
std::time::SystemTime::now()
|
std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
|
@ -37,6 +39,7 @@ pub fn now_unix() -> i64 {
|
||||||
/// Assemble a `TurnStatRow` from the harness's per-turn state. Used by both
|
/// 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
|
/// the agent and manager serve loops — the shape is identical, only the
|
||||||
/// post-turn count fetch helpers differ (and those stay in each binary).
|
/// post-turn count fetch helpers differ (and those stay in each binary).
|
||||||
|
#[must_use]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn build_row(
|
pub fn build_row(
|
||||||
started_at: i64,
|
started_at: i64,
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ pub enum Window {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Window {
|
impl Window {
|
||||||
|
#[must_use]
|
||||||
pub fn parse(s: &str) -> Self {
|
pub fn parse(s: &str) -> Self {
|
||||||
match s {
|
match s {
|
||||||
"1h" => Self::Hour,
|
"1h" => Self::Hour,
|
||||||
|
|
@ -51,6 +52,7 @@ impl Window {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn span_secs(self) -> i64 {
|
pub fn span_secs(self) -> i64 {
|
||||||
match self {
|
match self {
|
||||||
Self::Hour => 3600,
|
Self::Hour => 3600,
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,10 @@ pub struct TurnFiles {
|
||||||
impl TurnFiles {
|
impl TurnFiles {
|
||||||
/// Write all three files into the per-agent runtime dir alongside
|
/// Write all three files into the per-agent runtime dir alongside
|
||||||
/// `socket`. Idempotent — overwrites whatever was there.
|
/// `socket`. Idempotent — overwrites whatever was there.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if any of the config files cannot be written to disk.
|
||||||
pub async fn prepare(socket: &Path, label: &str, flavor: mcp::Flavor) -> Result<Self> {
|
pub async fn prepare(socket: &Path, label: &str, flavor: mcp::Flavor) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
mcp_config: write_mcp_config(socket).await?,
|
mcp_config: write_mcp_config(socket).await?,
|
||||||
|
|
@ -112,6 +116,10 @@ impl TurnFiles {
|
||||||
/// as `--socket <path>`); `binary_subcommand` is e.g. `"mcp"` for sub-agents
|
/// as `--socket <path>`); `binary_subcommand` is e.g. `"mcp"` for sub-agents
|
||||||
/// or `"mcp"` for the manager (both binaries name their MCP subcommand the
|
/// or `"mcp"` for the manager (both binaries name their MCP subcommand the
|
||||||
/// same — the differentiator is which binary `/proc/self/exe` resolves to).
|
/// same — the differentiator is which binary `/proc/self/exe` resolves to).
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the config file cannot be written.
|
||||||
pub async fn write_mcp_config(socket: &Path) -> Result<PathBuf> {
|
pub async fn write_mcp_config(socket: &Path) -> Result<PathBuf> {
|
||||||
let parent = socket.parent().unwrap_or_else(|| Path::new("/run/hive"));
|
let parent = socket.parent().unwrap_or_else(|| Path::new("/run/hive"));
|
||||||
tokio::fs::create_dir_all(parent).await.ok();
|
tokio::fs::create_dir_all(parent).await.ok();
|
||||||
|
|
@ -128,6 +136,10 @@ pub async fn write_mcp_config(socket: &Path) -> Result<PathBuf> {
|
||||||
/// Drop the static `--settings` JSON next to the MCP config so we can
|
/// Drop the static `--settings` JSON next to the MCP config so we can
|
||||||
/// pass a path (`--settings <file>`) instead of an ever-growing inline
|
/// pass a path (`--settings <file>`) instead of an ever-growing inline
|
||||||
/// blob — the CLI argv has a finite length budget.
|
/// blob — the CLI argv has a finite length budget.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the settings file cannot be written.
|
||||||
pub async fn write_settings(socket: &Path) -> Result<PathBuf> {
|
pub async fn write_settings(socket: &Path) -> Result<PathBuf> {
|
||||||
let parent = socket.parent().unwrap_or_else(|| Path::new("/run/hive"));
|
let parent = socket.parent().unwrap_or_else(|| Path::new("/run/hive"));
|
||||||
tokio::fs::create_dir_all(parent).await.ok();
|
tokio::fs::create_dir_all(parent).await.ok();
|
||||||
|
|
@ -142,6 +154,10 @@ pub async fn write_settings(socket: &Path) -> Result<PathBuf> {
|
||||||
/// `--system-prompt-file`, replacing claude's default system prompt with
|
/// `--system-prompt-file`, replacing claude's default system prompt with
|
||||||
/// the role + tools instructions. Per-turn prompts become much smaller
|
/// the role + tools instructions. Per-turn prompts become much smaller
|
||||||
/// (just the wake message body).
|
/// (just the wake message body).
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the system prompt file cannot be written.
|
||||||
pub async fn write_system_prompt(
|
pub async fn write_system_prompt(
|
||||||
socket: &Path,
|
socket: &Path,
|
||||||
label: &str,
|
label: &str,
|
||||||
|
|
@ -399,6 +415,10 @@ pub fn emit_turn_end(bus: &Bus, outcome: &TurnOutcome) {
|
||||||
/// Block until the bound `~/.claude/` dir contains a session, polling
|
/// Block until the bound `~/.claude/` dir contains a session, polling
|
||||||
/// `claude_dir` on a `poll_ms` interval (min 2s). Flips `state` to
|
/// `claude_dir` on a `poll_ms` interval (min 2s). Flips `state` to
|
||||||
/// `Online` when login lands; caller resumes its serve loop.
|
/// `Online` when login lands; caller resumes its serve loop.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the internal login-state lock is poisoned.
|
||||||
pub async fn wait_for_login(
|
pub async fn wait_for_login(
|
||||||
claude_dir: &Path,
|
claude_dir: &Path,
|
||||||
state: Arc<Mutex<LoginState>>,
|
state: Arc<Mutex<LoginState>>,
|
||||||
|
|
@ -442,6 +462,11 @@ pub async fn run_turn(prompt: &str, files: &TurnFiles, bus: &Bus) -> TurnOutcome
|
||||||
/// surface, same system prompt, same allowed-tools — so the post-
|
/// surface, same system prompt, same allowed-tools — so the post-
|
||||||
/// compact state matches a normal turn's. Only the prompt over stdin
|
/// compact state matches a normal turn's. Only the prompt over stdin
|
||||||
/// differs (`/compact` vs the wake-up payload).
|
/// differs (`/compact` vs the wake-up payload).
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the `claude --print /compact` invocation fails
|
||||||
|
/// (non-zero exit or I/O error).
|
||||||
pub async fn compact_session(files: &TurnFiles, bus: &Bus) -> Result<()> {
|
pub async fn compact_session(files: &TurnFiles, bus: &Bus) -> Result<()> {
|
||||||
bus.emit(LiveEvent::Note {
|
bus.emit(LiveEvent::Note {
|
||||||
text: "context overflow — running /compact on the persistent session".into(),
|
text: "context overflow — running /compact on the persistent session".into(),
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,10 @@ impl TurnStats {
|
||||||
|
|
||||||
/// Insert a row. Best-effort — logs + swallows errors so a sqlite
|
/// Insert a row. Best-effort — logs + swallows errors so a sqlite
|
||||||
/// hiccup (locked db, full disk) doesn't crash the harness.
|
/// hiccup (locked db, full disk) doesn't crash the harness.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the internal lock is poisoned.
|
||||||
pub fn record(&self, row: &TurnStatRow) {
|
pub fn record(&self, row: &TurnStatRow) {
|
||||||
let conn = self.inner.lock().unwrap();
|
let conn = self.inner.lock().unwrap();
|
||||||
let res = conn.execute(
|
let res = conn.execute(
|
||||||
|
|
@ -209,6 +213,9 @@ impl TurnStats {
|
||||||
/// have last-inference zeros — those rows yield `ctx = None` so the
|
/// have last-inference zeros — those rows yield `ctx = None` so the
|
||||||
/// badge stays empty until the next real turn rather than showing a
|
/// badge stays empty until the next real turn rather than showing a
|
||||||
/// misleading 0.
|
/// misleading 0.
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the internal lock is poisoned.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn last_usage(
|
pub fn last_usage(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,9 @@ impl AppState {
|
||||||
/// `post_compact`) the allowed-tools surface claude sees.
|
/// `post_compact`) the allowed-tools surface claude sees.
|
||||||
pub type Flavor = mcp::Flavor;
|
pub type Flavor = mcp::Flavor;
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the TCP listener cannot bind to the given port.
|
||||||
pub async fn serve(
|
pub async fn serve(
|
||||||
label: String,
|
label: String,
|
||||||
port: u16,
|
port: u16,
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,7 @@ pub struct ReminderStats {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HostResponse {
|
impl HostResponse {
|
||||||
|
#[must_use]
|
||||||
pub fn success() -> Self {
|
pub fn success() -> Self {
|
||||||
Self {
|
Self {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|
@ -142,6 +143,7 @@ impl HostResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn error(message: impl Into<String>) -> Self {
|
pub fn error(message: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
ok: false,
|
ok: false,
|
||||||
|
|
@ -151,6 +153,7 @@ impl HostResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn list(agents: Vec<String>) -> Self {
|
pub fn list(agents: Vec<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|
@ -160,6 +163,7 @@ impl HostResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn pending(approvals: Vec<Approval>) -> Self {
|
pub fn pending(approvals: Vec<Approval>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue