use std::sync::{Arc, Mutex};
use bob_rs::{
get_readiness, install_bob, spawn_bob, BobApprovalMode, BobChatMode, RunBobOptions,
KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE,
};
use crate::{
normalize_process_event, CredentialSpec, Harness, HarnessCapabilities, HarnessError,
HarnessInfo, HarnessReadiness, InstallCallback, RunCallback, RunHandle, RunMode, RunRequest,
};
pub mod parser;
pub use parser::{normalize_bob_event, parse_bob_line, BobStreamParser};
pub const BOB_HARNESS_ID: &str = "bob";
#[derive(Debug, Default, Clone)]
pub struct BobHarness;
impl BobHarness {
pub fn new() -> Self {
Self
}
}
impl Harness for BobHarness {
fn info(&self) -> HarnessInfo {
HarnessInfo {
id: BOB_HARNESS_ID.to_owned(),
display_name: "Bob".to_owned(),
description: "IBM's bob agent CLI. Runs locally via Node.js.".to_owned(),
requires_install: true,
capabilities: HarnessCapabilities {
credential_required: true,
previews_edits: true,
models: Vec::new(),
allows_custom_model: false,
supports_effort: false,
supports_max_turns: false,
supports_login: false,
},
}
}
fn readiness(&self) -> HarnessReadiness {
let snapshot = get_readiness();
let details = serde_json::to_value(&snapshot).unwrap_or(serde_json::Value::Null);
HarnessReadiness {
harness_id: BOB_HARNESS_ID.to_owned(),
ready: snapshot.ready,
installed: snapshot.bob.installed,
version: snapshot.bob.version.clone(),
auth_configured: snapshot.auth.configured,
error: snapshot.bob.error.clone(),
details,
}
}
fn install(&self, on_event: InstallCallback) -> Result<(), HarnessError> {
install_bob(move |event| (*on_event)(event)).map_err(HarnessError::install)
}
fn run(&self, request: RunRequest, on_event: RunCallback) -> Result<RunHandle, HarnessError> {
let opts = RunBobOptions {
prompt: request.prompt,
chat_mode: match request.mode {
RunMode::Ask => BobChatMode::Ask,
RunMode::Edit => BobChatMode::Code,
},
approval_mode: BobApprovalMode::Default,
max_coins: 30,
cwd: request.cwd,
bob_executable: None,
};
let parser = Arc::new(Mutex::new(BobStreamParser::default()));
let handle = spawn_bob(opts, request.run_id, move |event| {
let mut parser = parser.lock().unwrap_or_else(|p| p.into_inner());
for normalized in normalize_process_event(event, |line| parser.parse_line(line)) {
(*on_event)(normalized);
}
})
.map_err(HarnessError::spawn)?;
Ok(Box::new(handle))
}
fn credential(&self) -> CredentialSpec {
CredentialSpec {
label: "Bob API key".to_owned(),
keychain_service: KEYCHAIN_SERVICE.to_owned(),
keychain_account: KEYCHAIN_ACCOUNT.to_owned(),
required: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bob_info_requires_install() {
let info = BobHarness::new().info();
assert_eq!(info.id, BOB_HARNESS_ID);
assert!(info.requires_install);
}
#[test]
fn bob_credential_points_at_the_shared_keychain_slot() {
let cred = BobHarness::new().credential();
assert_eq!(cred.keychain_service, KEYCHAIN_SERVICE);
assert_eq!(cred.keychain_account, KEYCHAIN_ACCOUNT);
assert!(cred.required);
assert_eq!(
BobHarness::new().info().capabilities.credential_required,
cred.required
);
}
#[test]
fn bob_default_login_is_unsupported() {
let cb: InstallCallback = Arc::new(|_| {});
assert!(BobHarness::new().login(cb).is_err());
assert!(!BobHarness::new().info().capabilities.supports_login);
}
}