Replace C browser client with headless Chrome
This commit is contained in:
168
src/agent.rs
168
src/agent.rs
@@ -1,12 +1,14 @@
|
||||
mod web;
|
||||
|
||||
use self::web::Browser;
|
||||
use crate::model::ModelChoice;
|
||||
use serde_json::{Map, Value};
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{CStr, CString, c_char, c_int, c_void};
|
||||
use std::fs::{self, File};
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender, TryRecvError};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
@@ -30,158 +32,8 @@ const SHELL_ENV_ALLOWLIST: &[&str] = &[
|
||||
];
|
||||
pub(crate) const COMPACTION_OBSERVATION_PREFIX: &str = "Bash job update after context compaction.";
|
||||
|
||||
#[repr(C)]
|
||||
struct WebConfig {
|
||||
profile_dir: *const c_char,
|
||||
port: c_int,
|
||||
confirm: Option<unsafe extern "C" fn(*mut c_void, *const c_char, *mut c_char, usize) -> c_int>,
|
||||
confirm_data: *mut c_void,
|
||||
log: Option<unsafe extern "C" fn(*mut c_void, *const c_char)>,
|
||||
log_data: *mut c_void,
|
||||
cancel: Option<unsafe extern "C" fn(*mut c_void) -> bool>,
|
||||
cancel_data: *mut c_void,
|
||||
}
|
||||
|
||||
unsafe extern "C" {
|
||||
fn ds4_web_create(config: *const WebConfig) -> *mut c_void;
|
||||
fn ds4_web_free(web: *mut c_void);
|
||||
fn ds4_web_google_search(
|
||||
web: *mut c_void,
|
||||
query: *const c_char,
|
||||
error: *mut c_char,
|
||||
error_len: usize,
|
||||
) -> *mut c_char;
|
||||
fn ds4_web_visit_page(
|
||||
web: *mut c_void,
|
||||
url: *const c_char,
|
||||
error: *mut c_char,
|
||||
error_len: usize,
|
||||
) -> *mut c_char;
|
||||
fn free(pointer: *mut c_void);
|
||||
}
|
||||
|
||||
struct WebCallbacks {
|
||||
cancel: AtomicPtr<AtomicBool>,
|
||||
}
|
||||
|
||||
struct Browser {
|
||||
web: *mut c_void,
|
||||
callbacks: Box<WebCallbacks>,
|
||||
}
|
||||
|
||||
// The C browser is used by one agent-tools worker at a time under Tools' mutex.
|
||||
unsafe impl Send for Browser {}
|
||||
|
||||
impl Browser {
|
||||
fn new() -> Result<Self, String> {
|
||||
let mut callbacks = Box::new(WebCallbacks {
|
||||
cancel: AtomicPtr::new(std::ptr::null_mut()),
|
||||
});
|
||||
let profile = CString::new(
|
||||
crate::app::browser_profile_path()
|
||||
.to_string_lossy()
|
||||
.as_bytes(),
|
||||
)
|
||||
.map_err(|_| "The browser profile path contains a NUL byte.".to_owned())?;
|
||||
let data = (&mut *callbacks) as *mut WebCallbacks as *mut c_void;
|
||||
let config = WebConfig {
|
||||
profile_dir: profile.as_ptr(),
|
||||
port: 9333,
|
||||
confirm: Some(web_confirm),
|
||||
confirm_data: data,
|
||||
log: None,
|
||||
log_data: std::ptr::null_mut(),
|
||||
cancel: Some(web_cancel),
|
||||
cancel_data: data,
|
||||
};
|
||||
let web = unsafe { ds4_web_create(&config) };
|
||||
if web.is_null() {
|
||||
Err("Could not initialize browser tools.".into())
|
||||
} else {
|
||||
Ok(Self { web, callbacks })
|
||||
}
|
||||
}
|
||||
|
||||
fn google_search(&self, query: &str, cancel: &AtomicBool) -> Result<String, String> {
|
||||
self.call(query, cancel, ds4_web_google_search)
|
||||
}
|
||||
|
||||
fn visit_page(&self, url: &str, cancel: &AtomicBool) -> Result<String, String> {
|
||||
self.call(url, cancel, ds4_web_visit_page)
|
||||
}
|
||||
|
||||
fn call(
|
||||
&self,
|
||||
value: &str,
|
||||
cancel: &AtomicBool,
|
||||
operation: unsafe extern "C" fn(
|
||||
*mut c_void,
|
||||
*const c_char,
|
||||
*mut c_char,
|
||||
usize,
|
||||
) -> *mut c_char,
|
||||
) -> Result<String, String> {
|
||||
let value = CString::new(value).map_err(|_| "web input contains a NUL byte".to_owned())?;
|
||||
let mut error = [0 as c_char; 256];
|
||||
self.callbacks.cancel.store(
|
||||
cancel as *const AtomicBool as *mut AtomicBool,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
let result =
|
||||
unsafe { operation(self.web, value.as_ptr(), error.as_mut_ptr(), error.len()) };
|
||||
self.callbacks
|
||||
.cancel
|
||||
.store(std::ptr::null_mut(), Ordering::Relaxed);
|
||||
if result.is_null() {
|
||||
let error = unsafe { CStr::from_ptr(error.as_ptr()) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
Err(if error.is_empty() {
|
||||
"browser tool failed".into()
|
||||
} else {
|
||||
error
|
||||
})
|
||||
} else {
|
||||
let output = unsafe { CStr::from_ptr(result) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
unsafe { free(result.cast()) };
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Browser {
|
||||
fn drop(&mut self) {
|
||||
unsafe { ds4_web_free(self.web) };
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn web_confirm(
|
||||
_data: *mut c_void,
|
||||
message: *const c_char,
|
||||
error: *mut c_char,
|
||||
error_len: usize,
|
||||
) -> c_int {
|
||||
let _ = message;
|
||||
let _ = error;
|
||||
let _ = error_len;
|
||||
// Approval is obtained by the shared tool worker before entering C. Keeping
|
||||
// this callback lets the reference browser retain its launch guard.
|
||||
1
|
||||
}
|
||||
|
||||
unsafe extern "C" fn web_cancel(data: *mut c_void) -> bool {
|
||||
if data.is_null() {
|
||||
return false;
|
||||
}
|
||||
let callbacks = unsafe { &*(data.cast::<WebCallbacks>()) };
|
||||
let cancel = callbacks.cancel.load(Ordering::Relaxed);
|
||||
!cancel.is_null() && unsafe { (*cancel).load(Ordering::Relaxed) }
|
||||
}
|
||||
|
||||
const TOOL_SCHEMAS: &str = r#"{"type":"function","function":{"name":"google_search","description":"Search Google in a visible browser and return compact Markdown links.","parameters":{"type":"object","properties":{"query":{"type":"string"}},"required":["query"]}}}
|
||||
{"type":"function","function":{"name":"visit_page","description":"Open a URL in a visible browser and return rendered page text.","parameters":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]}}}
|
||||
const TOOL_SCHEMAS: &str = r#"{"type":"function","function":{"name":"google_search","description":"Search Google in a browser and return compact Markdown links.","parameters":{"type":"object","properties":{"query":{"type":"string"}},"required":["query"]}}}
|
||||
{"type":"function","function":{"name":"visit_page","description":"Open a URL in a browser and return rendered page text.","parameters":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]}}}
|
||||
{"type":"function","function":{"name":"bash","description":"Run a shell command.","parameters":{"type":"object","properties":{"command":{"type":"string"},"timeout_sec":{"type":"number"},"refresh_sec":{"type":"number"}},"required":["command"]}}}
|
||||
{"type":"function","function":{"name":"bash_status","description":"Report current status and new output for a bash job.","parameters":{"type":"object","properties":{"job":{"type":"number"},"pid":{"type":"number"},"refresh_sec":{"type":"number"}},"required":["job"]}}}
|
||||
{"type":"function","function":{"name":"bash_stop","description":"Terminate a running bash job and report its final output.","parameters":{"type":"object","properties":{"job":{"type":"number"},"pid":{"type":"number"},"refresh_sec":{"type":"number"}},"required":["job"]}}}
|
||||
@@ -315,7 +167,7 @@ impl Tools {
|
||||
more_text: None,
|
||||
jobs: HashMap::new(),
|
||||
next_job: 1,
|
||||
browser: Browser::new()?,
|
||||
browser: Browser::new(),
|
||||
dev_brain: None,
|
||||
})
|
||||
}
|
||||
@@ -944,8 +796,8 @@ impl Tools {
|
||||
fn approval(&self, call: &ToolCall) -> Option<ApprovalPrompt> {
|
||||
if matches!(call.name.as_str(), "google_search" | "visit_page") {
|
||||
return Some(ApprovalPrompt {
|
||||
title: "Allow visible browser?".into(),
|
||||
detail: "Start a visible Chrome browser for this tool call.".into(),
|
||||
title: "Allow browser?".into(),
|
||||
detail: "Start headless Chrome for this tool call.".into(),
|
||||
working_directory: self.root.clone(),
|
||||
});
|
||||
}
|
||||
@@ -1345,7 +1197,7 @@ pub(crate) fn system_prompt(model: ModelChoice, extra: &str, dev_brain: bool) ->
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"You are a coding agent running in a local workspace. Use tools for local file and system work. Avoid printing large file contents or large code blocks as answers; create or edit files with tools, then summarize results briefly.\n\n## Tools\n\nInvoke native DSML tools exactly as:\n<|DSML|tool_calls>\n<|DSML|invoke name=\"$TOOL_NAME\">\n<|DSML|parameter name=\"$PARAMETER_NAME\" string=\"true|false\">$PARAMETER_VALUE</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|tool_calls>\n\nTool calls are not allowed inside <think></think>. String parameters use raw text and string=\"true\"; numbers and booleans use JSON text and string=\"false\". Read defaults to a bounded chunk; use more to continue and whole=true only when needed. Use write for new files or whole-file replacement. Use edit with path first and exact unique old text; old may contain one [upto] marker between unique head and tail anchors. For long bash commands pass refresh_sec, then use bash_status or bash_stop. The first web call asks permission to start visible Chrome.\n\n### Available Tool Schemas\n\n{schemas}\n\n# Rules\n- Always use strict DSML syntax.\n- Use read/search to get anchors before editing.\n- Preserve the current system configuration unless explicitly asked otherwise."
|
||||
"You are a coding agent running in a local workspace. Use tools for local file and system work. Avoid printing large file contents or large code blocks as answers; create or edit files with tools, then summarize results briefly.\n\n## Tools\n\nInvoke native DSML tools exactly as:\n<|DSML|tool_calls>\n<|DSML|invoke name=\"$TOOL_NAME\">\n<|DSML|parameter name=\"$PARAMETER_NAME\" string=\"true|false\">$PARAMETER_VALUE</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|tool_calls>\n\nTool calls are not allowed inside <think></think>. String parameters use raw text and string=\"true\"; numbers and booleans use JSON text and string=\"false\". Read defaults to a bounded chunk; use more to continue and whole=true only when needed. Use write for new files or whole-file replacement. Use edit with path first and exact unique old text; old may contain one [upto] marker between unique head and tail anchors. For long bash commands pass refresh_sec, then use bash_status or bash_stop. The first web call asks permission to start headless Chrome.\n\n### Available Tool Schemas\n\n{schemas}\n\n# Rules\n- Always use strict DSML syntax.\n- Use read/search to get anchors before editing.\n- Preserve the current system configuration unless explicitly asked otherwise."
|
||||
)
|
||||
};
|
||||
let tools = if dev_brain {
|
||||
|
||||
Reference in New Issue
Block a user