Skip to main content

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}