Wire the service/browser runtime onto the websocket-driven execution path and add the new browser/service modules needed for the submit flow and runtime integration. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
164 lines
5.2 KiB
Rust
164 lines
5.2 KiB
Rust
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<T: Transport>(
|
|
transport: &T,
|
|
browser_tool: &BrowserPipeTool<T>,
|
|
plan: &planner::TaskPlan,
|
|
) -> Result<String, PipeError> {
|
|
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<String> {
|
|
let trimmed = value.trim();
|
|
(!trimmed.is_empty()).then(|| trimmed.to_string())
|
|
}
|
|
|
|
fn browser_backend_for_submit<T: Transport + 'static>(
|
|
browser_tool: &BrowserPipeTool<T>,
|
|
context: &AgentRuntimeContext,
|
|
request: &SubmitTaskRequest,
|
|
) -> Result<Arc<dyn BrowserBackend>, 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<String> {
|
|
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<T: Transport>(transport: &T, state: &str) -> Result<(), PipeError> {
|
|
transport.send(&AgentMessage::StatusChanged {
|
|
state: state.to_string(),
|
|
})
|
|
}
|
|
|
|
pub fn execute_task<T: Transport>(
|
|
transport: &T,
|
|
browser_tool: &BrowserPipeTool<T>,
|
|
instruction: &str,
|
|
) -> Result<String, PipeError> {
|
|
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<T: Transport + 'static>(
|
|
transport: &T,
|
|
browser_tool: &BrowserPipeTool<T>,
|
|
message: BrowserMessage,
|
|
) -> Result<(), PipeError> {
|
|
handle_browser_message_with_context(
|
|
transport,
|
|
browser_tool,
|
|
&AgentRuntimeContext::default(),
|
|
message,
|
|
)
|
|
}
|
|
|
|
pub fn handle_browser_message_with_context<T: Transport + 'static>(
|
|
transport: &T,
|
|
browser_tool: &BrowserPipeTool<T>,
|
|
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())
|
|
);
|
|
}
|
|
}
|