hyperhive/hive-claude
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-07-07 22:45:18 +02:00
..
src feat(#2109): harness-side idle watchdog to bail on anthropic api stall storms 2026-07-07 22:45:18 +02:00
Cargo.toml fix(hive-claude): match session by parsed top-level customTitle, not transcript substring 2026-07-06 00:09:36 +02:00
README.md feat(hive-claude): parse turn telemetry in the lib, return it from run 2026-07-05 20:06:30 +02:00

hive-claude

A small, reusable async driver for headless claude --print (Claude Code CLI) sessions. It spawns the CLI, streams and classifies the stream-json output, and reports the result. It knows only about the Claude Code CLI — no hyperhive types, policy, watermarks, or logging. Compaction, retry, and reset policy belong to the caller.

When to use it

Reach for this crate whenever you need to run the claude CLI from Rust and react to how a turn ended. It is the shared substrate under hive-ag3nt's turn loop; new callers (tools, tests, other agents) should build on it rather than shelling out to claude by hand.

Shape

Two layers — reach for the high-level one:

  • InfiniteSession { name, store, policy } — a durable session that keeps itself alive across the context window. .run(&config, prompt, &sink)Result<Progress, Error> does resume-or-create, compacts reactively on overflow (compact + retry once), and proactively after a clean turn when the policy says so (optional checkpoint turn, then compact). .compact(…) forces one. Progress { created, compacted, telemetry } reports what happened — including the turn's parsed Telemetry, finalised at the result event.
  • Claude::run(&config, &attach, prompt, &sink)Result<(), Error> — the low-level driver: one turn, one Attach target (Resume / Create / Continue / OneOff). A clean turn is Ok(()); every other state is an Error variant.

Supporting pieces:

  • CompactionPolicy — decides when to compact. PercentPolicy (percent, default_window, checkpoint_prompt) compacts at a percent of the model window; NeverCompact never does.
  • Config — the invocation (model, effort, cwd, prompt/MCP files, tools, extra args).
  • Sink — a trait with no-op defaults; implement what you care about to observe stream events, non-JSON stdout, and stderr. NoopSink ignores all.
  • SessionStore — locate and archive on-disk sessions by title.
  • Telemetry (context, cost, context_window, model) — everything the driver parses from a turn's stream, returned in Progress. Usage is the minimal slice (context_tokens, context_window) the policy sees.

Error unifies the two things that can stop a turn: recognized sentinels (PromptTooLong, RateLimited, AuthFailed, SessionNotFound) and hard failures (Spawn, Stdin, Wait, Exit, Io). Sentinels are expected control-flow, not crashes — the caller compacts, parks, re-auths, or creates a session in response.

use hive_claude::{Claude, Config, Error, NoopSink, Session};

let config = Config { model: "haiku".into(), ..Default::default() };
match Claude::run(&config, &Session::Resume("my-session".into()), "hello", &NoopSink).await {
    Ok(()) => {}
    Err(Error::PromptTooLong) => { /* compact + retry */ }
    Err(Error::RateLimited)   => { /* park + retry */ }
    Err(other)                => eprintln!("claude: {other}"),
}

thiserror here, anyhow in the apps

This is a library, so it returns a concrete, matchable Error enum built with thiserror: callers can tell a rate-limit from a spawn failure and act accordingly. Libraries should never force their callers into anyhow's type-erased error.

The applications (the hive-* binaries) use anyhow instead — at the top level you usually only want to add context and bubble a failure up, not match on it. A hive_claude::Error converts into an anyhow::Error for free at the ? boundary. Rule of thumb: libraries return thiserror enums, binaries consume them with anyhow.