oxios_kernel/kernel_handle/browser_api.rs
1//! Browser API — headless browser engine facade (RFC: browser-migration).
2//!
3//! Wraps the SDK's pure-Rust `oxibrowser-core` engine behind a lazily
4//! initialized [`BrowserEngine`]. The engine is created on first use and
5//! shared across every agent run that holds the same [`KernelHandle`].
6//!
7//! The engine is only available with the `native-browser` feature. Without
8//! it, [`BrowserApi::try_engine`] always returns `None` and no browse tools
9//! are registered — the capability degrades gracefully to a no-op.
10//!
11//! [`BrowserEngine`]: oxi_sdk::BrowserEngine
12
13use std::sync::Arc;
14
15use crate::config::OxiosConfig;
16
17/// Headless browser facade.
18///
19/// Holds the SDK [`BrowseConfig`] and a lazily-initialized engine cell. The
20/// concrete `OxiBrowserEngine` (pure-Rust: boa JS + html5ever + wreq) is
21/// constructed only when [`engine`](Self::engine) is first awaited.
22///
23/// [`BrowseConfig`]: oxi_sdk::BrowseConfig
24pub struct BrowserApi {
25 /// Lazily-initialized shared engine. `None` until first `engine().await`.
26 #[cfg(feature = "native-browser")]
27 engine: tokio::sync::OnceCell<Arc<dyn oxi_sdk::BrowserEngine>>,
28 /// Engine configuration (propagated to the backend on init).
29 config: oxi_sdk::BrowseConfig,
30}
31
32impl std::fmt::Debug for BrowserApi {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 f.debug_struct("BrowserApi")
35 .field("config", &self.config)
36 .finish()
37 }
38}
39
40impl BrowserApi {
41 /// Create a new browser facade with the given configuration.
42 pub fn new(config: oxi_sdk::BrowseConfig) -> Self {
43 Self {
44 #[cfg(feature = "native-browser")]
45 engine: tokio::sync::OnceCell::new(),
46 config,
47 }
48 }
49
50 /// Build a [`BrowserApi`] from the kernel config, honoring `[browser].enabled`.
51 ///
52 /// Returns `None` when browser integration is disabled, so callers can
53 /// short-circuit engine construction entirely.
54 pub fn from_config(config: &OxiosConfig) -> Option<Self> {
55 if config.browser.enabled {
56 Some(Self::new(config.browser.engine.clone()))
57 } else {
58 None
59 }
60 }
61
62 /// Lazily initialize and return the shared browser engine.
63 ///
64 /// The underlying `OxiBrowserEngine` (pure-Rust `oxibrowser-core`) is
65 /// created exactly once; subsequent calls return the cached handle.
66 /// A failed init is retryable — the cell stays empty on error.
67 ///
68 /// Only available with the `native-browser` feature.
69 #[cfg(feature = "native-browser")]
70 pub async fn engine(&self) -> anyhow::Result<Arc<dyn oxi_sdk::BrowserEngine>> {
71 self.engine
72 .get_or_try_init(|| async {
73 let backend = oxi_sdk::OxiBrowserEngine::with_config(self.config.clone())
74 .await
75 .map_err(|e| anyhow::anyhow!("browser engine init failed: {e}"))?;
76 Ok(Arc::new(backend) as Arc<dyn oxi_sdk::BrowserEngine>)
77 })
78 .await
79 .map(Arc::clone)
80 }
81
82 /// Synchronous accessor — returns the engine only if already initialized.
83 ///
84 /// Used by the (synchronous) tool registration path, which relies on the
85 /// agent runtime having awaited [`engine`](Self::engine) first. Returns
86 /// `None` if the engine has not been initialized (or the feature is off).
87 #[cfg(feature = "native-browser")]
88 pub fn try_engine(&self) -> Option<Arc<dyn oxi_sdk::BrowserEngine>> {
89 self.engine.get().cloned()
90 }
91
92 /// Without `native-browser`, no engine is ever available.
93 #[cfg(not(feature = "native-browser"))]
94 pub fn try_engine(&self) -> Option<Arc<dyn oxi_sdk::BrowserEngine>> {
95 None
96 }
97}