refactor(agent): extract claude driver into hive-claude crate

This commit is contained in:
müde 2026-07-05 18:50:12 +02:00
commit a3b66241d1
13 changed files with 907 additions and 442 deletions

61
hive-claude/src/lib.rs Normal file
View file

@ -0,0 +1,61 @@
//! `hive-claude` — a small, reusable async driver for headless
//! `claude --print` (Claude Code CLI) sessions.
//!
//! It spawns the CLI, streams and classifies its `stream-json` output, and
//! reports the result as a `Result<(), Error>`: a clean turn is `Ok(())`, and
//! every non-completion state — both recognized sentinels (rate-limit,
//! prompt-too-long, …) and hard failures (spawn, non-zero exit) — is a variant
//! of the single [`Error`] enum, so callers branch with one `match`. The crate
//! also locates and archives on-disk sessions by title ([`SessionStore`]). It
//! knows only about the Claude Code CLI — no application types, policy,
//! watermarks, or logging. Callers wire streaming output through a [`Sink`] and
//! layer their own compaction / retry / reset policy on top.
//!
//! # `thiserror` here, `anyhow` in the apps
//!
//! This is a **library**, so it exposes a concrete, matchable error enum built
//! with [`thiserror`](https://docs.rs/thiserror): a caller can distinguish
//! `Error::Exit { status, .. }` from `Error::Spawn { .. }` and branch on it.
//! A library should never force its callers to reach into `anyhow`'s
//! type-erased error to find out what went wrong.
//!
//! The **applications** in this workspace (the `hive-*` binaries) use
//! [`anyhow`](https://docs.rs/anyhow) instead. At the top level you usually
//! only want to attach context and log or bubble a failure up — not match on
//! it — and `anyhow::Result` + `?` + `.context()` is the ergonomic fit.
//! `anyhow::Error` implements `From<E>` for any `std::error::Error`, so 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`.**
//!
//! # Example
//!
//! ```no_run
//! # async fn ex() {
//! 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) => { /* caller compacts + retries */ }
//! Err(Error::RateLimited) => { /* caller parks + retries */ }
//! Err(other) => eprintln!("claude: {other}"),
//! }
//! # }
//! ```
mod classify;
mod config;
mod driver;
mod error;
mod sink;
mod store;
pub use config::{Config, Session};
pub use driver::Claude;
pub use error::{Error, Result};
pub use sink::{NoopSink, Sink};
pub use store::SessionStore;