From 8498bdd34e0288b8a1c28e789d35dcecf91cd8b7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 05:37:49 +0000 Subject: [PATCH 1/7] chore(docs): remove www prefix --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 96554b4..c2d5f73 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,13 +43,13 @@ If you’d like to use the repository from source, you can either install from g To install via git in your `Gemfile`: ```ruby -gem "stagehand", git: "https://www.github.com/browserbase/stagehand-ruby" +gem "stagehand", git: "https://github.com/browserbase/stagehand-ruby" ``` Alternatively, reference local copy of the repo: ```bash -$ git clone -- 'https://www.github.com/browserbase/stagehand-ruby' '' +$ git clone -- 'https://github.com/browserbase/stagehand-ruby' '' ``` ```ruby From 3eb6444c0a39cf65df3e95a8660b39a5c30e2d8f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:52:28 +0000 Subject: [PATCH 2/7] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 9738f87..a0da57a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 8 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-8fbb3fa8f3a37c1c7408de427fe125aadec49f705e8e30d191601a9b69c4cc41.yml -openapi_spec_hash: 48b4dfac35a842d7fb0d228caf87544e +openapi_spec_hash: 8a36f79075102c63234ed06107deb8c9 config_hash: 7386d24e2f03a3b2a89b3f6881446348 From d6c2d22e8ee78f90ea1bcce21616c4cf9412084f Mon Sep 17 00:00:00 2001 From: monadoid Date: Thu, 5 Feb 2026 15:09:33 -0700 Subject: [PATCH 3/7] Add remote Browserbase Playwright SSE example. --- README.md | 2 +- ...b => remote_browser_playwright_example.rb} | 114 ++++++++++++++++-- 2 files changed, 102 insertions(+), 14 deletions(-) rename examples/{remote_playwright_example.rb => remote_browser_playwright_example.rb} (53%) diff --git a/README.md b/README.md index 2216409..5bada98 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ npm install playwright export BROWSERBASE_API_KEY="your-browserbase-api-key" export BROWSERBASE_PROJECT_ID="your-browserbase-project-id" export MODEL_API_KEY="your-openai-api-key" -bundle exec ruby examples/remote_playwright_example.rb +bundle exec ruby examples/remote_browser_playwright_example.rb ``` Watir local example: diff --git a/examples/remote_playwright_example.rb b/examples/remote_browser_playwright_example.rb similarity index 53% rename from examples/remote_playwright_example.rb rename to examples/remote_browser_playwright_example.rb index 9b4375a..89c2fda 100755 --- a/examples/remote_playwright_example.rb +++ b/examples/remote_browser_playwright_example.rb @@ -16,7 +16,7 @@ # ./node_modules/.bin/playwright install chromium # # Run: -# bundle exec ruby examples/remote_playwright_example.rb +# bundle exec ruby examples/remote_browser_playwright_example.rb begin require("playwright") @@ -62,6 +62,36 @@ def resolve_page_target_id(cdp_session, page_url) target && target["targetId"] end +def print_stream_event(label, event) + case event.type + when :log + puts("[#{label}] log: #{event.data.message}") + when :system + status = event.data.status + if event.data.respond_to?(:error) && event.data.error + puts("[#{label}] system #{status}: #{event.data.error}") + elsif event.data.respond_to?(:result) && !event.data.result.nil? + puts("[#{label}] system #{status}: #{event.data.result}") + else + puts("[#{label}] system #{status}") + end + else + puts("[#{label}] event: #{event.inspect}") + end +end + +def stream_with_result(label, stream) + puts("#{label} stream:") + result = nil + stream.each do |event| + print_stream_event(label, event) + if event.type == :system && event.data.respond_to?(:result) && !event.data.result.nil? + result = event.data.result + end + end + result +end + client = Stagehand::Client.new( browserbase_api_key: browserbase_api_key, browserbase_project_id: browserbase_project_id, @@ -102,23 +132,43 @@ def resolve_page_target_id(cdp_session, page_url) raise "Page target id not found for page target" end - observe_response = client.sessions.observe( + observe_stream = client.sessions.observe_streaming( session_id, frame_id: page_target_id, instruction: "Find all clickable links on this page" ) - puts("Found #{observe_response.data.result.length} possible actions") + observe_result = stream_with_result("Observe", observe_stream) + if observe_result.nil? + observe_response = client.sessions.observe( + session_id, + frame_id: page_target_id, + instruction: "Find all clickable links on this page" + ) + observe_result = observe_response.data.result + end + puts("Found #{observe_result.length} possible actions") - action = observe_response.data.result.first + action = observe_result.first act_input = action ? action.to_h.merge(method: "click") : "Click the 'Learn more' link" - act_response = client.sessions.act( + + act_stream = client.sessions.act_streaming( session_id, frame_id: page_target_id, input: act_input ) - puts("Act completed: #{act_response.data.result[:message]}") + act_result = stream_with_result("Act", act_stream) + if act_result.nil? + act_response = client.sessions.act( + session_id, + frame_id: page_target_id, + input: act_input + ) + act_result = act_response.data.result + end + act_message = act_result.is_a?(Hash) ? (act_result[:message] || act_result["message"]) : act_result + puts("Act completed: #{act_message}") - extract_response = client.sessions.extract( + extract_stream = client.sessions.extract_streaming( session_id, frame_id: page_target_id, instruction: "Extract the main heading and any links on this page", @@ -130,9 +180,25 @@ def resolve_page_target_id(cdp_session, page_url) } } ) - puts("Extracted: #{extract_response.data.result}") + extract_result = stream_with_result("Extract", extract_stream) + if extract_result.nil? + extract_response = client.sessions.extract( + session_id, + frame_id: page_target_id, + instruction: "Extract the main heading and any links on this page", + schema: { + type: "object", + properties: { + heading: {type: "string"}, + links: {type: "array", items: {type: "string"}} + } + } + ) + extract_result = extract_response.data.result + end + puts("Extracted: #{extract_result}") - execute_response = client.sessions.execute( + execute_stream = client.sessions.execute_streaming( session_id, frame_id: page_target_id, execute_options: { @@ -147,12 +213,34 @@ def resolve_page_target_id(cdp_session, page_url) cua: false } ) - puts("Agent completed: #{execute_response.data.result[:message]}") - puts("Agent success: #{execute_response.data.result[:success]}") + execute_result = stream_with_result("Agent", execute_stream) + if execute_result.nil? + execute_response = client.sessions.execute( + session_id, + frame_id: page_target_id, + execute_options: { + instruction: "Click on the 'Learn more' link if available", + max_steps: 3 + }, + agent_config: { + model: Stagehand::ModelConfig.new( + model_name: "openai/gpt-5-nano", + api_key: model_key + ), + cua: false + } + ) + execute_result = execute_response.data.result + end + agent_message = + execute_result.is_a?(Hash) ? (execute_result[:message] || execute_result["message"]) : execute_result + agent_success = execute_result.is_a?(Hash) ? (execute_result[:success] || execute_result["success"]) : nil + puts("Agent completed: #{agent_message}") + puts("Agent success: #{agent_success}") page.wait_for_load_state(state: "domcontentloaded") - page.screenshot(path: "screenshot_remote_playwright.png", fullPage: true) - puts("Screenshot saved to: screenshot_remote_playwright.png") + page.screenshot(path: "screenshot_remote_browser_playwright.png", fullPage: true) + puts("Screenshot saved to: screenshot_remote_browser_playwright.png") ensure browser.close end From 34fb71424404a5afbe760da87746d4dd2f71b5a0 Mon Sep 17 00:00:00 2001 From: monadoid Date: Fri, 6 Feb 2026 14:22:23 -0700 Subject: [PATCH 4/7] Add local browser SSE example --- README.md | 4 +- examples/local_browser_playwright_example.rb | 207 +++++++++++++++++++ 2 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 examples/local_browser_playwright_example.rb diff --git a/README.md b/README.md index 5bada98..4089a35 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ export MODEL_API_KEY="your-openai-api-key" bundle exec ruby examples/local_browser_example.rb ``` -Playwright local example: +Playwright local example (SSE streaming): ```bash gem install playwright-ruby-client @@ -157,6 +157,8 @@ npm install playwright ./node_modules/.bin/playwright install chromium export MODEL_API_KEY="your-openai-api-key" bundle exec ruby examples/local_playwright_example.rb + +bundle exec ruby examples/local_browser_playwright_example.rb ``` Playwright remote example: diff --git a/examples/local_browser_playwright_example.rb b/examples/local_browser_playwright_example.rb new file mode 100644 index 0000000..587a8bf --- /dev/null +++ b/examples/local_browser_playwright_example.rb @@ -0,0 +1,207 @@ +#!/usr/bin/env ruby +# typed: ignore +# frozen_string_literal: true + +require "bundler/setup" +require "stagehand" + +# Example: Using Playwright with Stagehand local mode (local browser). +# +# Prerequisites: +# - Set MODEL_API_KEY or OPENAI_API_KEY environment variable +# - Set BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID (can be any value in local mode) +# - Install Playwright (outside this gem): +# gem install playwright-ruby-client +# npm install playwright +# ./node_modules/.bin/playwright install chromium +# +# Run: +# bundle exec ruby examples/local_browser_playwright_example.rb + +begin + require("playwright") +rescue LoadError + warn("Playwright is not installed. Run: gem install playwright-ruby-client") + exit(1) +end + +model_key = ENV["MODEL_API_KEY"] || ENV["OPENAI_API_KEY"] +browserbase_api_key = ENV["BROWSERBASE_API_KEY"].to_s +browserbase_project_id = ENV["BROWSERBASE_PROJECT_ID"].to_s + +missing = [] +missing << "MODEL_API_KEY" if model_key.to_s.empty? +missing << "BROWSERBASE_API_KEY" if browserbase_api_key.empty? +missing << "BROWSERBASE_PROJECT_ID" if browserbase_project_id.empty? + +unless missing.empty? + warn("Set #{missing.join(', ')} to run the local Playwright example.") + exit(1) +end + +def print_stream_event(label, event) + case event.type + when :log + puts("[#{label}] log: #{event.data.message}") + when :system + status = event.data.status + if event.data.respond_to?(:error) && event.data.error + puts("[#{label}] system #{status}: #{event.data.error}") + elsif event.data.respond_to?(:result) && !event.data.result.nil? + puts("[#{label}] system #{status}: #{event.data.result}") + else + puts("[#{label}] system #{status}") + end + else + puts("[#{label}] event: #{event.inspect}") + end +end + +def stream_with_result(label, stream) + puts("#{label} stream:") + result = nil + stream.each do |event| + print_stream_event(label, event) + if event.type == :system && event.data.respond_to?(:result) && !event.data.result.nil? + result = event.data.result + end + end + result +end + +client = Stagehand::Client.new( + browserbase_api_key: browserbase_api_key, + browserbase_project_id: browserbase_project_id, + model_api_key: model_key, + server: "local" +) + +session_id = nil + +begin + # rubocop:disable Metrics/BlockLength + Playwright.create(playwright_cli_executable_path: "./node_modules/.bin/playwright") do |playwright| + browser_server = playwright.chromium.launch_server(headless: true) + cdp_url = browser_server.ws_endpoint + + start_response = client.sessions.start( + model_name: "openai/gpt-5-nano", + browser: { + type: :local, + launch_options: { + cdp_url: cdp_url + } + } + ) + session_id = start_response.data.session_id + + puts("Session started: #{session_id}") + puts("Connecting Playwright over CDP...") + + browser = playwright.chromium.connect_over_cdp(cdp_url) + begin + context = browser.contexts.first || browser.new_context + page = context.pages.first || context.new_page + page.goto("https://example.com") + page.wait_for_load_state(state: "domcontentloaded") + + observe_stream = client.sessions.observe_streaming( + session_id, + instruction: "Find all clickable links on this page" + ) + observe_result = stream_with_result("Observe", observe_stream) + if observe_result.nil? + observe_response = client.sessions.observe( + session_id, + instruction: "Find all clickable links on this page" + ) + observe_result = observe_response.data.result + end + puts("Found #{observe_result.length} possible actions") + + act_input = "Click the 'Learn more' link" + act_stream = client.sessions.act_streaming( + session_id, + input: act_input + ) + act_result = stream_with_result("Act", act_stream) + if act_result.nil? + act_response = client.sessions.act( + session_id, + input: act_input + ) + act_result = act_response.data.result + end + act_message = act_result.is_a?(Hash) ? (act_result[:message] || act_result["message"]) : act_result + puts("Act completed: #{act_message}") + + extract_stream = client.sessions.extract_streaming( + session_id, + instruction: "Extract the main heading and any links on this page", + schema: { + type: "object", + properties: { + heading: {type: "string"}, + links: {type: "array", items: {type: "string"}} + } + } + ) + extract_result = stream_with_result("Extract", extract_stream) + if extract_result.nil? + extract_response = client.sessions.extract( + session_id, + instruction: "Extract the main heading and any links on this page", + schema: { + type: "object", + properties: { + heading: {type: "string"}, + links: {type: "array", items: {type: "string"}} + } + } + ) + extract_result = extract_response.data.result + end + puts("Extracted: #{extract_result}") + + execute_stream = client.sessions.execute_streaming( + session_id, + execute_options: { + instruction: "Click the 'Learn more' link if available and summarize the destination.", + max_steps: 5 + }, + agent_config: { + model: { + model_name: "openai/gpt-5-nano", + api_key: model_key + }, + cua: false + } + ) + execute_result = stream_with_result("Execute", execute_stream) + if execute_result.nil? + execute_response = client.sessions.execute( + session_id, + execute_options: { + instruction: "Click the 'Learn more' link if available and summarize the destination.", + max_steps: 5 + }, + agent_config: { + model: { + model_name: "openai/gpt-5-nano", + api_key: model_key + }, + cua: false + } + ) + execute_result = execute_response.data.result + end + puts("Execute result: #{execute_result}") + ensure + browser.close + browser_server.close + end + end + # rubocop:enable Metrics/BlockLength +ensure + client.sessions.end(session_id) if session_id +end From 1f060e94155bec7ecdedecaaf36ea5ed5b9dddd9 Mon Sep 17 00:00:00 2001 From: monadoid Date: Fri, 6 Feb 2026 14:41:28 -0700 Subject: [PATCH 5/7] Fix Ruby example file permissions --- examples/local_browser_playwright_example.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 examples/local_browser_playwright_example.rb diff --git a/examples/local_browser_playwright_example.rb b/examples/local_browser_playwright_example.rb old mode 100644 new mode 100755 From ea4142cc5973c9207a81c7336c8425c9dbe63a30 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 07:04:34 +0000 Subject: [PATCH 6/7] fix(client): loosen json header parsing --- lib/stagehand/internal/util.rb | 2 +- rbi/stagehand/internal/util.rbi | 2 +- test/stagehand/internal/util_test.rb | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/stagehand/internal/util.rb b/lib/stagehand/internal/util.rb index 1698a6f..1b87520 100644 --- a/lib/stagehand/internal/util.rb +++ b/lib/stagehand/internal/util.rb @@ -485,7 +485,7 @@ def writable_enum(&blk) end # @type [Regexp] - JSON_CONTENT = %r{^application/(?:vnd(?:\.[^.]+)*\+)?json(?!l)} + JSON_CONTENT = %r{^application/(?:[a-zA-Z0-9.-]+\+)?json(?!l)} # @type [Regexp] JSONL_CONTENT = %r{^application/(:?x-(?:n|l)djson)|(:?(?:x-)?jsonl)} diff --git a/rbi/stagehand/internal/util.rbi b/rbi/stagehand/internal/util.rbi index 2f93054..2e2e702 100644 --- a/rbi/stagehand/internal/util.rbi +++ b/rbi/stagehand/internal/util.rbi @@ -296,7 +296,7 @@ module Stagehand end JSON_CONTENT = - T.let(%r{^application/(?:vnd(?:\.[^.]+)*\+)?json(?!l)}, Regexp) + T.let(%r{^application/(?:[a-zA-Z0-9.-]+\+)?json(?!l)}, Regexp) JSONL_CONTENT = T.let(%r{^application/(:?x-(?:n|l)djson)|(:?(?:x-)?jsonl)}, Regexp) diff --git a/test/stagehand/internal/util_test.rb b/test/stagehand/internal/util_test.rb index 7efd7e5..571c403 100644 --- a/test/stagehand/internal/util_test.rb +++ b/test/stagehand/internal/util_test.rb @@ -171,6 +171,8 @@ def test_json_content cases = { "application/json" => true, "application/jsonl" => false, + "application/arbitrary+json" => true, + "application/ARBITRARY+json" => true, "application/vnd.github.v3+json" => true, "application/vnd.api+json" => true } From f9cd6fc37444beab2d3d7d91f7e52accbf674e9b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 07:04:48 +0000 Subject: [PATCH 7/7] release: 3.5.2 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 13 +++++++++++++ Gemfile.lock | 2 +- lib/stagehand/version.rb | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3549461..c6a6955 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.5.1" + ".": "3.5.2" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c0ee849..819a91e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 3.5.2 (2026-02-07) + +Full Changelog: [v3.5.1...v3.5.2](https://github.com/browserbase/stagehand-ruby/compare/v3.5.1...v3.5.2) + +### Bug Fixes + +* **client:** loosen json header parsing ([ea4142c](https://github.com/browserbase/stagehand-ruby/commit/ea4142cc5973c9207a81c7336c8425c9dbe63a30)) + + +### Chores + +* **docs:** remove www prefix ([8498bdd](https://github.com/browserbase/stagehand-ruby/commit/8498bdd34e0288b8a1c28e789d35dcecf91cd8b7)) + ## 3.5.1 (2026-02-03) Full Changelog: [v3.5.0...v3.5.1](https://github.com/browserbase/stagehand-ruby/compare/v3.5.0...v3.5.1) diff --git a/Gemfile.lock b/Gemfile.lock index c29ea56..d78caf4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,7 +11,7 @@ GIT PATH remote: . specs: - stagehand (3.5.1) + stagehand (3.5.2) cgi connection_pool diff --git a/lib/stagehand/version.rb b/lib/stagehand/version.rb index 23c52d4..6872959 100644 --- a/lib/stagehand/version.rb +++ b/lib/stagehand/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Stagehand - VERSION = "3.5.1" + VERSION = "3.5.2" end