From da791460fd228f465d8b25143ce3285f1c0c670f Mon Sep 17 00:00:00 2001 From: monadoid Date: Wed, 14 Jan 2026 19:30:32 -0700 Subject: [PATCH 1/3] Added page helper --- Cargo.lock | 2 +- Cargo.toml | 13 ++- examples/chromiumoxide_page_example.rs | 150 +++++++++++++++++++++++++ src/lib.rs | 32 ++++++ tests/chromiumoxide_integration.rs | 13 ++- 5 files changed, 204 insertions(+), 6 deletions(-) create mode 100644 examples/chromiumoxide_page_example.rs diff --git a/Cargo.lock b/Cargo.lock index e7289ed..0763f63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1731,7 +1731,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stagehand_sdk" -version = "0.3.2" +version = "0.4.0" dependencies = [ "async-channel 2.5.0", "async-std", diff --git a/Cargo.toml b/Cargo.toml index a64eac5..97fb0fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ readme = "README.md" default = ["tokio-runtime"] tokio-runtime = ["tokio", "tokio-stream"] async-std-runtime = ["async-std"] +chromiumoxide-page = ["chromiumoxide"] [dependencies] # Runtime-agnostic @@ -26,6 +27,7 @@ bytes = "1" eventsource-client = "0.15.1" dotenvy = "0.15" async-trait = "0.1.89" +chromiumoxide = { version = "0.8.0", features = ["tokio-runtime"], optional = true } # Tokio runtime (optional, default) tokio = { version = "1", features = ["sync", "rt", "macros", "rt-multi-thread"], optional = true } @@ -35,6 +37,15 @@ tokio-stream = { version = "0.1", optional = true } async-std = { version = "1", features = ["attributes"], optional = true } [dev-dependencies] -chromiumoxide = { version = "0.8.0", features = ["tokio-runtime"] } # TLS support for WSS connections to Browserbase async-tungstenite = { version = "0.32", features = ["tokio-native-tls", "tokio-runtime"] } + +[[example]] +name = "chromiumoxide_page_example" +path = "examples/chromiumoxide_page_example.rs" +required-features = ["chromiumoxide-page"] + +[[test]] +name = "chromiumoxide_integration" +path = "tests/chromiumoxide_integration.rs" +required-features = ["chromiumoxide-page"] diff --git a/examples/chromiumoxide_page_example.rs b/examples/chromiumoxide_page_example.rs new file mode 100644 index 0000000..1112c88 --- /dev/null +++ b/examples/chromiumoxide_page_example.rs @@ -0,0 +1,150 @@ +//! Example: use a chromiumoxide `Page` with the Stagehand Rust SDK. +//! +//! What this demonstrates: +//! - Start a Stagehand session (remote Stagehand API / Browserbase browser) +//! - Attach chromiumoxide to the same browser via CDP (`browserbase_cdp_url`) +//! - Use a helper to convert a chromiumoxide `Page` into the Stagehand `frame_id` +//! so Stagehand uses the correct page in `observe/act/extract`. +//! +//! Environment variables required: +//! - MODEL_API_KEY (or another supported model provider API key) +//! - BROWSERBASE_API_KEY +//! - BROWSERBASE_PROJECT_ID +//! +//! Optional: +//! - STAGEHAND_BASE_URL (defaults to https://api.stagehand.browserbase.com/v1) + +use chromiumoxide::browser::Browser; +use chromiumoxide::cdp::browser_protocol::page::NavigateParams; +use futures::StreamExt; +use stagehand_sdk::{ + ActResponseEvent, Env, ExtractResponseEvent, Model, ObserveResponseEvent, Stagehand, + TransportChoice, V3Options, +}; +use std::collections::HashMap; + +#[tokio::main] +async fn main() -> Result<(), Box> { + dotenvy::dotenv().ok(); + + println!("=== Stagehand Rust SDK + chromiumoxide Page Example ===\n"); + + println!("1. Connecting to Stagehand..."); + let mut stagehand = Stagehand::connect(TransportChoice::default_rest()).await?; + println!(" Connected!\n"); + + println!("2. Starting browser session..."); + let opts = V3Options { + env: Some(Env::Browserbase), + model: Some(Model::String("openai/gpt-5-nano".into())), + verbose: Some(1), + ..Default::default() + }; + stagehand.start(opts).await?; + println!(" Session ID: {:?}\n", stagehand.session_id()); + + println!("3. Fetching Browserbase CDP URL..."); + let cdp_url = stagehand.browserbase_cdp_url().await?; + println!(" CDP URL: {cdp_url}\n"); + + println!("4. Connecting chromiumoxide over CDP..."); + let (browser, mut handler) = Browser::connect(&cdp_url).await?; + let handler_task = tokio::spawn(async move { + while let Some(event) = handler.next().await { + if event.is_err() { + break; + } + } + }); + println!(" Connected!\n"); + + println!("5. Getting a page and navigating with chromiumoxide..."); + let pages = browser.pages().await?; + let page = if pages.is_empty() { + browser.new_page("about:blank").await? + } else { + pages.into_iter().next().unwrap() + }; + + page.execute(NavigateParams::builder().url("https://example.com").build()?) + .await?; + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + println!(" Chromiumoxide navigation complete.\n"); + + println!("6. Resolving Stagehand frame_id from chromiumoxide page..."); + let frame_id = stagehand_sdk::rust_chromeoxide_page_to_target_id(&page).await?; + println!(" frame_id: {frame_id}\n"); + + println!("7. Stagehand.observe(frame_id=...) ..."); + let mut observe_stream = stagehand + .observe( + Some("Find the most relevant click target on this page".to_string()), + None, + Some(30_000), + None, + Some(frame_id.clone()), + ) + .await?; + + while let Some(msg) = observe_stream.next().await { + if let Ok(event) = msg { + if let Some(ObserveResponseEvent::ElementsJson(json)) = event.event { + println!(" Observed elements JSON: {json}"); + } + } + } + + println!("\n8. Stagehand.extract(frame_id=...) ..."); + let schema = serde_json::json!({ + "type": "object", + "properties": { + "title": { "type": "string" }, + "url": { "type": "string" } + } + }); + + let mut extract_stream = stagehand + .extract( + "Extract the page title and current URL", + schema, + None, + Some(30_000), + None, + Some(frame_id.clone()), + ) + .await?; + + while let Some(msg) = extract_stream.next().await { + if let Ok(event) = msg { + if let Some(ExtractResponseEvent::DataJson(json)) = event.event { + println!(" Extracted: {json}"); + } + } + } + + println!("\n9. Stagehand.act(frame_id=...) ..."); + let mut act_stream = stagehand + .act( + "Click on the 'More information...' link", + None, + HashMap::new(), + Some(30_000), + Some(frame_id.clone()), + ) + .await?; + + while let Some(msg) = act_stream.next().await { + if let Ok(event) = msg { + if let Some(ActResponseEvent::Success(success)) = event.event { + println!(" Act success: {success}"); + } + } + } + + println!("\n10. Cleaning up..."); + handler_task.abort(); + stagehand.end().await?; + println!(" Done."); + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index dd7972f..d825672 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -342,6 +342,38 @@ pub trait Transport: Send + Sync { async fn end(&mut self, session_id: &str) -> Result<(), StagehandError>; } +// ============================================================================= +// Frame ID Helpers (chromiumoxide integration) +// ============================================================================= + +/// Convert a chromiumoxide `Page` to the Stagehand `frame_id` (CDP frame id). +/// +/// This calls `Page.getFrameTree` over CDP and returns `frameTree.frame.id`. +#[cfg(feature = "chromiumoxide-page")] +pub async fn chromiumoxide_page_to_frame_id( + page: &chromiumoxide::page::Page, +) -> Result { + use chromiumoxide::cdp::browser_protocol::page::GetFrameTreeParams; + + let resp = page + .execute(GetFrameTreeParams::default()) + .await + .map_err(|e| StagehandError::Api(format!("Failed to call Page.getFrameTree via chromiumoxide: {e}")))?; + + Ok(resp.result.frame_tree.frame.id.inner().clone()) +} + +/// Backwards-compatible naming for the chromiumoxide helper. +/// +/// Despite the name, this returns the Stagehand `frame_id` (CDP frame id), which is +/// what Stagehand methods expect as their `frame_id` parameter. +#[cfg(feature = "chromiumoxide-page")] +pub async fn rust_chromeoxide_page_to_target_id( + page: &chromiumoxide::page::Page, +) -> Result { + chromiumoxide_page_to_frame_id(page).await +} + // ============================================================================= // REST Transport Implementation // ============================================================================= diff --git a/tests/chromiumoxide_integration.rs b/tests/chromiumoxide_integration.rs index b2e3731..1064cdc 100644 --- a/tests/chromiumoxide_integration.rs +++ b/tests/chromiumoxide_integration.rs @@ -101,7 +101,12 @@ async fn test_chromiumoxide_browserbase_connection( .await?; println!(" Page loaded successfully!"); - // 5. Now use Stagehand's AI-powered methods on the same browser session + // 5. Resolve the Stagehand `frame_id` for the chromiumoxide `Page` + println!("5. Resolving Stagehand frame_id from chromiumoxide page..."); + let frame_id = stagehand_sdk::rust_chromeoxide_page_to_target_id(&page).await?; + println!(" frame_id: {}", frame_id); + + // 6. Now use Stagehand's AI-powered methods on the same browser session println!("5. Using Stagehand AI to extract data from the same session..."); // Schema must be in JSON Schema format @@ -120,7 +125,7 @@ async fn test_chromiumoxide_browserbase_connection( None, Some(30_000), None, - None, + Some(frame_id.clone()), ) .await?; @@ -135,7 +140,7 @@ async fn test_chromiumoxide_browserbase_connection( } } - // 6. Use Stagehand to click the "More information..." link + // 7. Use Stagehand to click the "More information..." link println!("6. Using Stagehand AI to click the link..."); let mut act_stream = stagehand @@ -144,7 +149,7 @@ async fn test_chromiumoxide_browserbase_connection( None, HashMap::new(), Some(30_000), - None, + Some(frame_id.clone()), ) .await?; From f3c1a3edc3c95ca8e3ae75b6f6e5380fe9f10772 Mon Sep 17 00:00:00 2001 From: monadoid Date: Wed, 14 Jan 2026 19:34:35 -0700 Subject: [PATCH 2/3] Removed redundant helper --- examples/chromiumoxide_page_example.rs | 2 +- src/lib.rs | 11 ----------- tests/chromiumoxide_integration.rs | 2 +- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/examples/chromiumoxide_page_example.rs b/examples/chromiumoxide_page_example.rs index 1112c88..7ebc004 100644 --- a/examples/chromiumoxide_page_example.rs +++ b/examples/chromiumoxide_page_example.rs @@ -72,7 +72,7 @@ async fn main() -> Result<(), Box> { println!(" Chromiumoxide navigation complete.\n"); println!("6. Resolving Stagehand frame_id from chromiumoxide page..."); - let frame_id = stagehand_sdk::rust_chromeoxide_page_to_target_id(&page).await?; + let frame_id = stagehand_sdk::chromiumoxide_page_to_frame_id(&page).await?; println!(" frame_id: {frame_id}\n"); println!("7. Stagehand.observe(frame_id=...) ..."); diff --git a/src/lib.rs b/src/lib.rs index d825672..5bc264c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -363,17 +363,6 @@ pub async fn chromiumoxide_page_to_frame_id( Ok(resp.result.frame_tree.frame.id.inner().clone()) } -/// Backwards-compatible naming for the chromiumoxide helper. -/// -/// Despite the name, this returns the Stagehand `frame_id` (CDP frame id), which is -/// what Stagehand methods expect as their `frame_id` parameter. -#[cfg(feature = "chromiumoxide-page")] -pub async fn rust_chromeoxide_page_to_target_id( - page: &chromiumoxide::page::Page, -) -> Result { - chromiumoxide_page_to_frame_id(page).await -} - // ============================================================================= // REST Transport Implementation // ============================================================================= diff --git a/tests/chromiumoxide_integration.rs b/tests/chromiumoxide_integration.rs index 1064cdc..bb649ea 100644 --- a/tests/chromiumoxide_integration.rs +++ b/tests/chromiumoxide_integration.rs @@ -103,7 +103,7 @@ async fn test_chromiumoxide_browserbase_connection( // 5. Resolve the Stagehand `frame_id` for the chromiumoxide `Page` println!("5. Resolving Stagehand frame_id from chromiumoxide page..."); - let frame_id = stagehand_sdk::rust_chromeoxide_page_to_target_id(&page).await?; + let frame_id = stagehand_sdk::chromiumoxide_page_to_frame_id(&page).await?; println!(" frame_id: {}", frame_id); // 6. Now use Stagehand's AI-powered methods on the same browser session From 393037d2efe28485bf2e7670bb58c5096aa61bbf Mon Sep 17 00:00:00 2001 From: monadoid Date: Thu, 15 Jan 2026 11:08:20 -0700 Subject: [PATCH 3/3] Removed helper - using chromiumoxide built in helper for getting frameid --- Cargo.toml | 14 ++------------ examples/chromiumoxide_page_example.rs | 7 ++++--- src/lib.rs | 21 --------------------- tests/chromiumoxide_integration.rs | 6 +++--- 4 files changed, 9 insertions(+), 39 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 97fb0fc..c2d4155 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ readme = "README.md" default = ["tokio-runtime"] tokio-runtime = ["tokio", "tokio-stream"] async-std-runtime = ["async-std"] -chromiumoxide-page = ["chromiumoxide"] [dependencies] # Runtime-agnostic @@ -27,7 +26,6 @@ bytes = "1" eventsource-client = "0.15.1" dotenvy = "0.15" async-trait = "0.1.89" -chromiumoxide = { version = "0.8.0", features = ["tokio-runtime"], optional = true } # Tokio runtime (optional, default) tokio = { version = "1", features = ["sync", "rt", "macros", "rt-multi-thread"], optional = true } @@ -37,15 +35,7 @@ tokio-stream = { version = "0.1", optional = true } async-std = { version = "1", features = ["attributes"], optional = true } [dev-dependencies] +# Used only by opt-in examples / integration tests. +chromiumoxide = { version = "0.8.0", features = ["tokio-runtime"] } # TLS support for WSS connections to Browserbase async-tungstenite = { version = "0.32", features = ["tokio-native-tls", "tokio-runtime"] } - -[[example]] -name = "chromiumoxide_page_example" -path = "examples/chromiumoxide_page_example.rs" -required-features = ["chromiumoxide-page"] - -[[test]] -name = "chromiumoxide_integration" -path = "tests/chromiumoxide_integration.rs" -required-features = ["chromiumoxide-page"] diff --git a/examples/chromiumoxide_page_example.rs b/examples/chromiumoxide_page_example.rs index 7ebc004..9d796de 100644 --- a/examples/chromiumoxide_page_example.rs +++ b/examples/chromiumoxide_page_example.rs @@ -15,7 +15,7 @@ //! - STAGEHAND_BASE_URL (defaults to https://api.stagehand.browserbase.com/v1) use chromiumoxide::browser::Browser; -use chromiumoxide::cdp::browser_protocol::page::NavigateParams; +use chromiumoxide::cdp::browser_protocol::page::{GetFrameTreeParams, NavigateParams}; use futures::StreamExt; use stagehand_sdk::{ ActResponseEvent, Env, ExtractResponseEvent, Model, ObserveResponseEvent, Stagehand, @@ -71,8 +71,9 @@ async fn main() -> Result<(), Box> { tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; println!(" Chromiumoxide navigation complete.\n"); - println!("6. Resolving Stagehand frame_id from chromiumoxide page..."); - let frame_id = stagehand_sdk::chromiumoxide_page_to_frame_id(&page).await?; + println!("6. Resolving Stagehand frame_id from chromiumoxide page via Page.getFrameTree..."); + let frame_tree = page.execute(GetFrameTreeParams::default()).await?.result.frame_tree; + let frame_id = frame_tree.frame.id.inner().clone(); println!(" frame_id: {frame_id}\n"); println!("7. Stagehand.observe(frame_id=...) ..."); diff --git a/src/lib.rs b/src/lib.rs index 5bc264c..dd7972f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -342,27 +342,6 @@ pub trait Transport: Send + Sync { async fn end(&mut self, session_id: &str) -> Result<(), StagehandError>; } -// ============================================================================= -// Frame ID Helpers (chromiumoxide integration) -// ============================================================================= - -/// Convert a chromiumoxide `Page` to the Stagehand `frame_id` (CDP frame id). -/// -/// This calls `Page.getFrameTree` over CDP and returns `frameTree.frame.id`. -#[cfg(feature = "chromiumoxide-page")] -pub async fn chromiumoxide_page_to_frame_id( - page: &chromiumoxide::page::Page, -) -> Result { - use chromiumoxide::cdp::browser_protocol::page::GetFrameTreeParams; - - let resp = page - .execute(GetFrameTreeParams::default()) - .await - .map_err(|e| StagehandError::Api(format!("Failed to call Page.getFrameTree via chromiumoxide: {e}")))?; - - Ok(resp.result.frame_tree.frame.id.inner().clone()) -} - // ============================================================================= // REST Transport Implementation // ============================================================================= diff --git a/tests/chromiumoxide_integration.rs b/tests/chromiumoxide_integration.rs index bb649ea..d354d85 100644 --- a/tests/chromiumoxide_integration.rs +++ b/tests/chromiumoxide_integration.rs @@ -8,7 +8,7 @@ //! 5. Optionally uses Stagehand's AI-powered methods alongside direct CDP control use chromiumoxide::browser::Browser; -use chromiumoxide::cdp::browser_protocol::page::NavigateParams; +use chromiumoxide::cdp::browser_protocol::page::{GetFrameTreeParams, NavigateParams}; use futures::StreamExt; use stagehand_sdk::{ActResponseEvent, ExtractResponseEvent}; use stagehand_sdk::{Env, Model, Stagehand, TransportChoice, V3Options}; @@ -101,9 +101,9 @@ async fn test_chromiumoxide_browserbase_connection( .await?; println!(" Page loaded successfully!"); - // 5. Resolve the Stagehand `frame_id` for the chromiumoxide `Page` + // 5. Resolve the Stagehand `frame_id` for the chromiumoxide `Page` (CDP Page.getFrameTree) println!("5. Resolving Stagehand frame_id from chromiumoxide page..."); - let frame_id = stagehand_sdk::chromiumoxide_page_to_frame_id(&page).await?; + let frame_id = page.execute(GetFrameTreeParams::default()).await?.result.frame_tree.frame.id.inner().clone(); println!(" frame_id: {}", frame_id); // 6. Now use Stagehand's AI-powered methods on the same browser session