1use std::sync::{Arc, Mutex};
13
14use bob_rs::{
15 get_readiness, install_bob, spawn_bob, BobApprovalMode, BobChatMode, RunBobOptions,
16 KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE,
17};
18use crate::{
19 normalize_process_event, CredentialSpec, Harness, HarnessCapabilities, HarnessError,
20 HarnessInfo, HarnessReadiness, InstallCallback, RunCallback, RunHandle, RunMode, RunRequest,
21};
22
23pub mod parser;
24
25pub use parser::{normalize_bob_event, parse_bob_line, BobStreamParser};
26
27pub const BOB_HARNESS_ID: &str = "bob";
29
30#[derive(Debug, Default, Clone)]
33pub struct BobHarness;
34
35impl BobHarness {
36 pub fn new() -> Self {
37 Self
38 }
39}
40
41impl Harness for BobHarness {
42 fn info(&self) -> HarnessInfo {
43 HarnessInfo {
44 id: BOB_HARNESS_ID.to_owned(),
45 display_name: "Bob".to_owned(),
46 description: "IBM's bob agent CLI. Runs locally via Node.js.".to_owned(),
47 requires_install: true,
48 capabilities: HarnessCapabilities {
49 credential_required: true,
53 previews_edits: true,
54 models: Vec::new(),
55 allows_custom_model: false,
56 supports_effort: false,
57 supports_max_turns: false,
58 supports_login: false,
59 },
60 }
61 }
62
63 fn readiness(&self) -> HarnessReadiness {
64 let snapshot = get_readiness();
65 let details = serde_json::to_value(&snapshot).unwrap_or(serde_json::Value::Null);
69 HarnessReadiness {
70 harness_id: BOB_HARNESS_ID.to_owned(),
71 ready: snapshot.ready,
72 installed: snapshot.bob.installed,
73 version: snapshot.bob.version.clone(),
74 auth_configured: snapshot.auth.configured,
75 error: snapshot.bob.error.clone(),
76 details,
77 }
78 }
79
80 fn install(&self, on_event: InstallCallback) -> Result<(), HarnessError> {
81 install_bob(move |event| (*on_event)(event)).map_err(HarnessError::install)
86 }
87
88 fn run(&self, request: RunRequest, on_event: RunCallback) -> Result<RunHandle, HarnessError> {
89 let opts = RunBobOptions {
90 prompt: request.prompt,
91 chat_mode: match request.mode {
92 RunMode::Ask => BobChatMode::Ask,
93 RunMode::Edit => BobChatMode::Code,
96 },
97 approval_mode: BobApprovalMode::Default,
100 max_coins: 30,
101 cwd: request.cwd,
102 bob_executable: None,
103 };
104 let parser = Arc::new(Mutex::new(BobStreamParser::default()));
114 let handle = spawn_bob(opts, request.run_id, move |event| {
115 let mut parser = parser.lock().unwrap_or_else(|p| p.into_inner());
118 for normalized in normalize_process_event(event, |line| parser.parse_line(line)) {
119 (*on_event)(normalized);
120 }
121 })
122 .map_err(HarnessError::spawn)?;
123 Ok(Box::new(handle))
124 }
125
126 fn credential(&self) -> CredentialSpec {
127 CredentialSpec {
128 label: "Bob API key".to_owned(),
129 keychain_service: KEYCHAIN_SERVICE.to_owned(),
130 keychain_account: KEYCHAIN_ACCOUNT.to_owned(),
131 required: true,
132 }
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn bob_info_requires_install() {
142 let info = BobHarness::new().info();
143 assert_eq!(info.id, BOB_HARNESS_ID);
144 assert!(info.requires_install);
145 }
146
147 #[test]
148 fn bob_credential_points_at_the_shared_keychain_slot() {
149 let cred = BobHarness::new().credential();
150 assert_eq!(cred.keychain_service, KEYCHAIN_SERVICE);
151 assert_eq!(cred.keychain_account, KEYCHAIN_ACCOUNT);
152 assert!(cred.required);
153 assert_eq!(
156 BobHarness::new().info().capabilities.credential_required,
157 cred.required
158 );
159 }
160
161 #[test]
162 fn bob_default_login_is_unsupported() {
163 let cb: InstallCallback = Arc::new(|_| {});
166 assert!(BobHarness::new().login(cb).is_err());
167 assert!(!BobHarness::new().info().capabilities.supports_login);
168 }
169}