Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "3.5.2"
".": "3.5.3"
}
2 changes: 1 addition & 1 deletion .stats.yml
Original file line number Diff line number Diff line change
@@ -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: 8a36f79075102c63234ed06107deb8c9
config_hash: 7386d24e2f03a3b2a89b3f6881446348
config_hash: 4252fc025e947bc0fd6b2abd91a0cc8e
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 3.5.3 (2026-02-13)

Full Changelog: [v3.5.2...v3.5.3](https://github.com/browserbase/stagehand-ruby/compare/v3.5.2...v3.5.3)

## 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)
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ GIT
PATH
remote: .
specs:
stagehand (3.5.2)
stagehand (3.5.3)
cgi
connection_pool

Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,16 +136,18 @@ bundle install
Remote browser example:

```bash
export BROWSERBASE_API_KEY="your-browserbase-api-key"
export BROWSERBASE_PROJECT_ID="your-browserbase-project-id"
export MODEL_API_KEY="your-openai-api-key"
cp examples/.env.example examples/.env
# Edit examples/.env with your credentials.
bundle exec ruby examples/remote_browser_example.rb
```

The examples load `examples/.env` automatically.

Local mode example (embedded server, local Chrome/Chromium):

```bash
export MODEL_API_KEY="your-openai-api-key"
cp examples/.env.example examples/.env
# Edit examples/.env with your credentials.
bundle exec ruby examples/local_browser_example.rb
```

Expand Down
4 changes: 4 additions & 0 deletions examples/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
STAGEHAND_API_URL=https://api.stagehand.browserbase.com
MODEL_API_KEY=sk-proj-your-llm-api-key-here
BROWSERBASE_API_KEY=bb_live_your_api_key_here
BROWSERBASE_PROJECT_ID=your-bb-project-uuid-here
45 changes: 45 additions & 0 deletions examples/env.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

module ExampleEnv
REQUIRED_KEYS = %w[
STAGEHAND_API_URL
MODEL_API_KEY
BROWSERBASE_API_KEY
BROWSERBASE_PROJECT_ID
].freeze

def self.load!
env_path = find_env_path
raise "Missing examples/.env (expected in repo examples/ directory)." unless env_path

File.readlines(env_path, chomp: true).each do |line|
trimmed = line.strip
next if trimmed.empty? || trimmed.start_with?("#")

key, value = trimmed.split("=", 2)
next if key.nil? || value.nil?

ENV[key] ||= value
end

missing = REQUIRED_KEYS.select { |key| ENV[key].to_s.empty? }
unless missing.empty?
raise "Missing required env vars: #{missing.join(', ')} (from examples/.env)"
end

ENV["STAGEHAND_BASE_URL"] ||= ENV["STAGEHAND_API_URL"]
end

def self.find_env_path
current = File.expand_path(Dir.pwd)
loop do
candidate = File.join(current, "examples", ".env")
return candidate if File.exist?(candidate)

parent = File.dirname(current)
return nil if parent == current

current = parent
end
end
end
68 changes: 56 additions & 12 deletions examples/local_browser_example.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
require "bundler/setup"
require "stagehand"

# Local mode runs the embedded Stagehand server and uses a local Chrome/Chromium.
# Set MODEL_API_KEY before running this script.
require_relative "env"
ExampleEnv.load!
model_key = ENV["MODEL_API_KEY"].to_s
if model_key.empty?
warn "Set MODEL_API_KEY to run the local example."
Expand All @@ -17,6 +17,40 @@
server: "local"
)

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
if event.type == :system && event.data.respond_to?(:status) && event.data.status == :error
error_message = event.data.respond_to?(:error) && event.data.error ? event.data.error : "unknown error"
raise("#{label} stream error: #{error_message}")
end
end
result
end

session_id = nil

begin
Expand All @@ -35,21 +69,25 @@
client.sessions.navigate(session_id, url: "https://example.com")
puts("Navigated to example.com")

observe_response = client.sessions.observe(
observe_stream = client.sessions.observe_streaming(
session_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)
observe_actions = observe_result || []
puts("Found #{observe_actions.length} possible actions")

action = observe_response.data.result.first
action = observe_actions.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,
input: act_input
)
puts("Act completed: #{act_response.data.result[:message]}")
act_result = stream_with_result("Act", act_stream)
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,
instruction: "extract the main heading and any links on this page",
schema: {
Expand All @@ -60,9 +98,10 @@
}
}
)
puts("Extracted: #{extract_response.data.result}")
extract_result = stream_with_result("Extract", extract_stream)
puts("Extracted: #{extract_result}")

execute_response = client.sessions.execute(
execute_stream = client.sessions.execute_streaming(
session_id,
execute_options: {
instruction: "Click on the 'Learn more' link if available and report the page title",
Expand All @@ -76,8 +115,13 @@
cua: false
}
)
puts("Agent completed: #{execute_response.data.result[:message]}")
puts("Agent success: #{execute_response.data.result[:success]}")
execute_result = stream_with_result("Execute", execute_stream)
execute_message = execute_result.is_a?(Hash) ? (execute_result[:message] || execute_result["message"]) : execute_result
execute_success = execute_result.is_a?(Hash) ? (execute_result[:success] || execute_result["success"]) : nil
execute_actions = execute_result.is_a?(Hash) ? (execute_result[:actions] || execute_result["actions"]) : nil
puts("Agent completed: #{execute_message}")
puts("Agent success: #{execute_success}")
puts("Agent actions taken: #{execute_actions&.length || 0}")
ensure
client.sessions.end_(session_id) if session_id
client.close
Expand Down
17 changes: 3 additions & 14 deletions examples/local_browser_playwright_example.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,16 @@
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

require_relative "env"
ExampleEnv.load!
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"]
model_key = ENV["MODEL_API_KEY"]
browserbase_api_key = ENV["BROWSERBASE_API_KEY"].to_s
browserbase_project_id = ENV["BROWSERBASE_PROJECT_ID"].to_s

Expand Down
86 changes: 58 additions & 28 deletions examples/local_playwright_example.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,8 @@
require "net/http"
require "stagehand"

# Example: Using Playwright with Stagehand local mode
#
# This example mirrors the Python Playwright flow: launch the browser with Playwright,
# connect Stagehand to the same browser via CDP, and target the existing page by frame_id.
#
# Prerequisites:
# - Set MODEL_API_KEY or OPENAI_API_KEY environment variable
# - Chrome/Chromium installed locally
# - 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_playwright_example.rb

require_relative "env"
ExampleEnv.load!
begin
require("playwright")
rescue LoadError
Expand Down Expand Up @@ -69,9 +55,43 @@ def fetch_page_target_id(port, page_url)
target_id
end

model_key = ENV["MODEL_API_KEY"] || ENV["OPENAI_API_KEY"]
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
if event.type == :system && event.data.respond_to?(:status) && event.data.status == :error
error_message = event.data.respond_to?(:error) && event.data.error ? event.data.error : "unknown error"
raise("#{label} stream error: #{error_message}")
end
end
result
end

model_key = ENV["MODEL_API_KEY"]
if model_key.to_s.empty?
warn "Set MODEL_API_KEY (or OPENAI_API_KEY) to run the local example."
warn "Set MODEL_API_KEY to run the local example."
exit 1
end

Expand Down Expand Up @@ -110,23 +130,27 @@ def fetch_page_target_id(port, page_url)
session_id = start_response.data.session_id
puts("Session started: #{session_id}")

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)
observe_actions = observe_result || []
puts("Found #{observe_actions.length} possible actions")

action = observe_response.data.result.first
action = observe_actions.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)
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",
Expand All @@ -138,9 +162,10 @@ def fetch_page_target_id(port, page_url)
}
}
)
puts("Extracted: #{extract_response.data.result}")
extract_result = stream_with_result("Extract", extract_stream)
puts("Extracted: #{extract_result}")

execute_response = client.sessions.execute(
execute_stream = client.sessions.execute_streaming(
session_id,
frame_id: page_target_id,
execute_options: {
Expand All @@ -155,8 +180,13 @@ def fetch_page_target_id(port, 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("Execute", execute_stream)
execute_message = execute_result.is_a?(Hash) ? (execute_result[:message] || execute_result["message"]) : execute_result
execute_success = execute_result.is_a?(Hash) ? (execute_result[:success] || execute_result["success"]) : nil
execute_actions = execute_result.is_a?(Hash) ? (execute_result[:actions] || execute_result["actions"]) : nil
puts("Agent completed: #{execute_message}")
puts("Agent success: #{execute_success}")
puts("Agent actions taken: #{execute_actions&.length || 0}")

page.wait_for_load_state(state: "domcontentloaded")
page.screenshot(path: "screenshot_local_playwright.png", fullPage: true)
Expand Down
Loading