use async_trait::async_trait;
use serde_json::json;
use tracing::{debug, warn};
use crate::actions::{ActionResult, BrowserAction};
use crate::backend::BrowserBackend;
#[derive(Debug, Clone)]
pub struct AgentBrowserConfig {
pub binary_path: String,
pub timeout_seconds: u64,
}
impl Default for AgentBrowserConfig {
fn default() -> Self {
Self {
binary_path: "agent-browser".into(),
timeout_seconds: 30,
}
}
}
pub struct AgentBrowserBackend {
config: AgentBrowserConfig,
available: bool,
}
impl AgentBrowserBackend {
pub fn new(config: AgentBrowserConfig) -> Self {
let available = std::process::Command::new("which")
.arg(&config.binary_path)
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if !available {
warn!(
binary = %config.binary_path,
"agent-browser binary not found — backend will be unavailable"
);
}
Self { config, available }
}
fn action_to_args(action: &BrowserAction) -> Vec<String> {
match action {
BrowserAction::Navigate { url } => vec!["navigate".into(), url.clone()],
BrowserAction::Click { selector } => vec!["click".into(), selector.clone()],
BrowserAction::Type { selector, text } => {
vec!["type".into(), selector.clone(), text.clone()]
}
BrowserAction::Screenshot => vec!["screenshot".into()],
BrowserAction::Pdf => vec!["pdf".into()],
BrowserAction::Evaluate { expression } => {
vec!["evaluate".into(), expression.clone()]
}
BrowserAction::GetCookies => vec!["get-cookies".into()],
BrowserAction::ClearCookies => vec!["clear-cookies".into()],
BrowserAction::ReadPage => vec!["read-page".into()],
BrowserAction::GoBack => vec!["go-back".into()],
BrowserAction::GoForward => vec!["go-forward".into()],
BrowserAction::Reload => vec!["reload".into()],
}
}
}
#[async_trait]
impl BrowserBackend for AgentBrowserBackend {
async fn execute(&self, action: &BrowserAction) -> ActionResult {
if !self.available {
return ActionResult::err("agent-browser", "agent-browser binary not available".into());
}
let args = Self::action_to_args(action);
let action_name = args.first().cloned().unwrap_or_default();
debug!(backend = "agent-browser", action = %action_name, "executing");
let timeout = std::time::Duration::from_secs(self.config.timeout_seconds);
let result = tokio::time::timeout(timeout, async {
tokio::process::Command::new(&self.config.binary_path)
.args(&args)
.arg("--json")
.output()
.await
})
.await;
match result {
Ok(Ok(output)) => {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
match serde_json::from_str::<serde_json::Value>(&stdout) {
Ok(data) => ActionResult::ok(&action_name, data),
Err(_) => ActionResult::ok(&action_name, json!({ "raw": stdout.trim() })),
}
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
ActionResult::err(
&action_name,
format!("exit code {}: {}", output.status, stderr.trim()),
)
}
}
Ok(Err(e)) => ActionResult::err(&action_name, format!("spawn failed: {e}")),
Err(_) => ActionResult::err(
&action_name,
format!("timeout after {}s", self.config.timeout_seconds),
),
}
}
fn name(&self) -> &str {
"agent-browser"
}
fn is_available(&self) -> bool {
self.available
}
}