pub mod planner; pub mod runtime; pub mod task_runner; use std::sync::Arc; use crate::browser::ws_backend::WsBrowserBackend; use crate::browser::{BrowserBackend, PipeBrowserBackend}; use crate::pipe::{AgentMessage, BrowserMessage, BrowserPipeTool, PipeError, Transport}; pub use task_runner::{ run_submit_task, run_submit_task_with_browser_backend, AgentEventSink, AgentRuntimeContext, SubmitTaskRequest, }; fn execute_plan( transport: &T, browser_tool: &BrowserPipeTool, plan: &planner::TaskPlan, ) -> Result { for step in &plan.steps { transport.send(&AgentMessage::LogEntry { level: "info".to_string(), message: step.log_message.clone(), })?; let result = browser_tool.invoke( step.action.clone(), step.params.clone(), &step.expected_domain, )?; if !result.success { return Err(PipeError::Protocol(format!( "browser action failed: {}", result.data ))); } } Ok(plan.summary.clone()) } fn normalize_optional_submit_field(value: String) -> Option { let trimmed = value.trim(); (!trimmed.is_empty()).then(|| trimmed.to_string()) } fn browser_backend_for_submit( browser_tool: &BrowserPipeTool, context: &AgentRuntimeContext, request: &SubmitTaskRequest, ) -> Result, PipeError> { if let Some(browser_ws_url) = configured_browser_ws_url(context) { return Ok(Arc::new( WsBrowserBackend::new( Arc::new(crate::service::browser_ws_client::ServiceWsClient::connect( &browser_ws_url, )?), browser_tool.mac_policy().clone(), crate::service::browser_ws_client::initial_request_url_for_submit_task(request), ) .with_response_timeout(browser_tool.response_timeout()), )); } Ok(Arc::new(PipeBrowserBackend::from_inner(browser_tool.clone()))) } fn configured_browser_ws_url(context: &AgentRuntimeContext) -> Option { std::env::var("SGCLAW_BROWSER_WS_URL") .ok() .filter(|value| !value.trim().is_empty()) .or_else(|| { context .load_sgclaw_settings() .ok() .flatten() .and_then(|settings| settings.browser_ws_url) .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) }) } fn send_status_changed(transport: &T, state: &str) -> Result<(), PipeError> { transport.send(&AgentMessage::StatusChanged { state: state.to_string(), }) } pub fn execute_task( transport: &T, browser_tool: &BrowserPipeTool, instruction: &str, ) -> Result { let plan = planner::plan_instruction(instruction) .map_err(|err| PipeError::Protocol(err.to_string()))?; execute_plan(transport, browser_tool, &plan) } pub fn handle_browser_message( transport: &T, browser_tool: &BrowserPipeTool, message: BrowserMessage, ) -> Result<(), PipeError> { handle_browser_message_with_context( transport, browser_tool, &AgentRuntimeContext::default(), message, ) } pub fn handle_browser_message_with_context( transport: &T, browser_tool: &BrowserPipeTool, context: &AgentRuntimeContext, message: BrowserMessage, ) -> Result<(), PipeError> { match message { BrowserMessage::Connect => send_status_changed(transport, "connected"), BrowserMessage::Start => send_status_changed(transport, "started"), BrowserMessage::Stop => send_status_changed(transport, "stopped"), BrowserMessage::SubmitTask { instruction, conversation_id, messages, page_url, page_title, } => { let request = SubmitTaskRequest { instruction, conversation_id: normalize_optional_submit_field(conversation_id), messages, page_url: normalize_optional_submit_field(page_url), page_title: normalize_optional_submit_field(page_title), }; let browser_backend = browser_backend_for_submit(browser_tool, context, &request)?; run_submit_task_with_browser_backend(transport, transport, browser_backend, context, request) } BrowserMessage::Init { .. } => { eprintln!("ignoring duplicate init after handshake"); Ok(()) } BrowserMessage::Response { seq, .. } => { eprintln!("ignoring unsolicited response: seq={seq}"); Ok(()) } } } #[cfg(test)] mod tests { use super::normalize_optional_submit_field; #[test] fn normalize_optional_submit_field_trims_and_drops_blank_values() { assert_eq!(normalize_optional_submit_field(" \n\t ".to_string()), None); assert_eq!( normalize_optional_submit_field(" https://example.com/page ".to_string()), Some("https://example.com/page".to_string()) ); } }