Skip to main content

harness/
lib.rs

1//! Unified coding agent harness — run Claude Code, OpenCode, Codex, or Cursor
2//! through a single Rust API.
3//!
4//! # Quick Start
5//!
6//! ```rust,no_run
7//! use harness::{AgentKind, TaskConfig};
8//!
9//! # #[tokio::main]
10//! # async fn main() -> harness::Result<()> {
11//! let config = TaskConfig::new("fix the bug", AgentKind::Claude);
12//! let mut stream = harness::run_task(&config).await?;
13//! # Ok(())
14//! # }
15//! ```
16
17pub mod agents;
18pub mod config;
19pub mod error;
20pub mod event;
21pub mod logger;
22pub mod models;
23pub mod normalize;
24pub mod process;
25pub mod registry;
26pub mod runner;
27pub mod settings;
28
29pub use config::{AgentKind, OutputFormat, PermissionMode, TaskConfig, TaskConfigBuilder};
30pub use error::{Error, Result};
31pub use event::{Event, UsageData};
32pub use models::{ModelEntry, ModelRegistry, ModelResolution};
33pub use normalize::NormalizeConfig;
34pub use process::StreamHandle;
35pub use runner::{AgentCapabilities, AgentRunner, EventStream};
36
37/// Re-export the cancel token type for convenience.
38pub use tokio_util::sync::CancellationToken;
39
40/// Create a runner for the given agent kind and execute the task, returning
41/// a stream of unified events.
42///
43/// This is the simple API — for cancellation support, use `run_task_with_cancel`.
44pub async fn run_task(config: &TaskConfig) -> Result<EventStream> {
45    let handle = run_task_with_cancel(config, None).await?;
46    Ok(handle.stream)
47}
48
49/// Run a task with an optional cancellation token.
50///
51/// Returns a `StreamHandle` containing the event stream and the cancel token.
52/// If no token is provided, a new one is created internally.
53pub async fn run_task_with_cancel(
54    config: &TaskConfig,
55    cancel_token: Option<tokio_util::sync::CancellationToken>,
56) -> Result<StreamHandle> {
57    let runner = agents::create_runner(config.agent);
58
59    // If the user provided a custom binary path, skip the availability check
60    // (the path will be validated at spawn time). Otherwise, check PATH.
61    if config.binary_path.is_none() && !runner.is_available() {
62        return Err(Error::BinaryNotFound {
63            agent: config.agent.display_name().to_string(),
64            binary: config.agent.default_binary().to_string(),
65        });
66    }
67
68    let mut handle = runner.run(config, cancel_token).await?;
69
70    let norm_config = NormalizeConfig {
71        cwd: config
72            .cwd
73            .as_ref()
74            .map(|p| p.display().to_string())
75            .or_else(|| std::env::current_dir().ok().map(|p| p.display().to_string())),
76        model: config.model.clone(),
77        prompt: Some(config.prompt.clone()),
78    };
79    handle.stream = normalize::normalize_stream(handle.stream, norm_config);
80
81    Ok(handle)
82}
83
84/// List which agents are currently available on this system.
85pub fn available_agents() -> Vec<AgentKind> {
86    use AgentKind::*;
87    [Claude, OpenCode, Codex, Cursor]
88        .into_iter()
89        .filter(|kind| {
90            let runner = agents::create_runner(*kind);
91            runner.is_available()
92        })
93        .collect()
94}