use crate::ai_agents::{AiAgentAvailability, AiAgentStreamEvent}; pub use crate::cli_agent_runtime::AgentStreamRequest; use std::path::Path; pub fn check_cli() -> AiAgentAvailability { crate::opencode_discovery::check_cli() } pub fn run_agent_stream(request: AgentStreamRequest, emit: F) -> Result where F: FnMut(AiAgentStreamEvent), { let binary = crate::opencode_discovery::find_binary()?; run_agent_stream_with_binary(&binary, request, emit) } fn run_agent_stream_with_binary( binary: &Path, request: AgentStreamRequest, emit: F, ) -> Result where F: FnMut(AiAgentStreamEvent), { let command = crate::opencode_config::build_command(binary, &request)?; crate::cli_agent_runtime::run_ai_agent_json_stream( command, "opencode", emit, crate::opencode_events::session_id, crate::opencode_events::dispatch_event, crate::opencode_events::format_error, ) } #[cfg(test)] mod tests { use super::*; use crate::ai_agents::AiAgentPermissionMode; #[cfg(unix)] fn executable_script(dir: &Path, body: &str) -> std::path::PathBuf { use std::os::unix::fs::PermissionsExt; let script = dir.join("opencode"); std::fs::write(&script, format!("#!/bin/sh\n{body}")).unwrap(); std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap(); script } fn request(vault_path: String) -> AgentStreamRequest { AgentStreamRequest { message: "Summarize".into(), system_prompt: None, vault_path, permission_mode: AiAgentPermissionMode::Safe, } } #[cfg(unix)] #[test] fn run_agent_stream_maps_opencode_json_events() { let dir = tempfile::tempdir().unwrap(); let vault = tempfile::tempdir().unwrap(); let binary = executable_script( dir.path(), r#"printf '%s\n' '{"type":"session","sessionID":"open_1"}' printf '%s\n' '{"type":"message","text":"Done"}' "#, ); let mut events = Vec::new(); let session_id = run_agent_stream_with_binary( &binary, request(vault.path().to_string_lossy().into_owned()), |event| events.push(event), ) .unwrap(); assert_eq!(session_id, "open_1"); assert!(matches!( &events[0], AiAgentStreamEvent::Init { session_id } if session_id == "open_1" )); assert!(matches!( &events[1], AiAgentStreamEvent::TextDelta { text } if text == "Done" )); assert!(matches!(events.last(), Some(AiAgentStreamEvent::Done))); } #[cfg(unix)] #[test] fn run_agent_stream_reports_opencode_nonzero_exit_errors() { let dir = tempfile::tempdir().unwrap(); let vault = tempfile::tempdir().unwrap(); let binary = executable_script( dir.path(), r#"printf '%s\n' '{"type":"session","sessionID":"open_1"}' printf '%s\n' 'provider login required' >&2 exit 3 "#, ); let mut events = Vec::new(); let session_id = run_agent_stream_with_binary( &binary, request(vault.path().to_string_lossy().into_owned()), |event| events.push(event), ) .unwrap(); assert_eq!(session_id, "open_1"); assert!(events.iter().any(|event| matches!( event, AiAgentStreamEvent::Error { message } if message.contains("provider configured") ))); assert!(matches!(events.last(), Some(AiAgentStreamEvent::Done))); } }