Skip to content
Merged
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.7.0"
".": "3.7.1"
}
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 3.7.1 (2026-02-27)

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

### Bug Fixes

* properly mock time in ruby ci tests ([ed2c2c2](https://github.com/browserbase/stagehand-ruby/commit/ed2c2c28400498332f0742b2616fd14a66646690))

## 3.7.0 (2026-02-25)

Full Changelog: [v3.6.1...v3.7.0](https://github.com/browserbase/stagehand-ruby/compare/v3.6.1...v3.7.0)
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.7.0)
stagehand (3.7.1)
cgi
connection_pool

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ Examples and dependencies:
- `examples/local_playwright_example.rb`: `playwright-ruby-client` + Playwright browsers
- `examples/local_watir_example.rb`: `watir`

Multiregion support: see `examples/local_server_multiregion_browser_example.rb`.

Install dependencies for the example you want to run, then execute it:

```bash
Expand Down
161 changes: 161 additions & 0 deletions examples/local_server_multiregion_browser_example.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require "bundler/setup"
require "stagehand"

require_relative "env"
ExampleEnv.load!
browserbase_api_key = ENV["BROWSERBASE_API_KEY"].to_s
browserbase_project_id = ENV["BROWSERBASE_PROJECT_ID"].to_s
model_key = ENV["MODEL_API_KEY"].to_s

missing = []
missing << "BROWSERBASE_API_KEY" if browserbase_api_key.empty?
missing << "BROWSERBASE_PROJECT_ID" if browserbase_project_id.empty?
missing << "MODEL_API_KEY" if model_key.empty?

unless missing.empty?
warn "Set #{missing.join(', ')} to run the local server + multiregion Browserbase example."
exit 1
end

client = Stagehand::Client.new(
browserbase_api_key: browserbase_api_key,
browserbase_project_id: browserbase_project_id,
model_api_key: model_key,
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
start_response = client.sessions.start(
model_name: "anthropic/claude-sonnet-4-6",
browser: {type: :browserbase},
browserbase_session_create_params: {
region: Stagehand::SessionStartParams::BrowserbaseSessionCreateParams::Region::EU_CENTRAL_1
}
)
session_id = start_response.data.session_id
puts("Session started: #{session_id}")

client.sessions.navigate(session_id, url: "https://news.ycombinator.com")
puts("Navigated to Hacker News")

observe_stream = client.sessions.observe_streaming(
session_id,
instruction: "find the link to view comments for the top post"
)

observe_result = stream_with_result("Observe", observe_stream)
actions = observe_result || []
puts("Found #{actions.length} possible actions")

action = actions.first
unless action
warn("No actions found")
exit(1)
end

puts("Acting on: #{action.description}")

act_stream = client.sessions.act_streaming(
session_id,
input: action.to_h.merge(method: "click")
)
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_stream = client.sessions.extract_streaming(
session_id,
instruction: "extract the text of the top comment on this page",
schema: {
type: "object",
properties: {
comment_text: {
type: "string",
description: "The text content of the top comment"
},
author: {
type: "string",
description: "The username of the comment author"
}
},
required: ["comment_text"]
}
)
extract_result = stream_with_result("Extract", extract_stream)
puts("Extracted data: #{extract_result}")

extracted_data = extract_result
author = extracted_data.is_a?(Hash) ? extracted_data[:author] : nil
author ||= "unknown"
puts("Looking up profile for author: #{author}")

instruction = [
"Find any personal website, GitHub, or LinkedIn for the Hacker News user '#{author}'.",
"Click their username to open the profile and look for shared links."
].join(" ")

execute_stream = client.sessions.execute_streaming(
session_id,
execute_options: {
instruction: instruction,
max_steps: 15
},
agent_config: {
model: Stagehand::ModelConfig.new(
model_name: "anthropic/claude-opus-4-6",
api_key: model_key
),
cua: false
}
)

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
end
2 changes: 1 addition & 1 deletion lib/stagehand/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Stagehand
VERSION = "3.7.0"
VERSION = "3.7.1"
end
8 changes: 5 additions & 3 deletions test/stagehand/client_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,11 @@ def test_client_retry_after_seconds
end

def test_client_retry_after_date
time_now = Time.now

stub_request(:post, "http://localhost/v1/sessions/start").to_return_json(
status: 500,
headers: {"retry-after" => (Time.now + 10).httpdate},
headers: {"retry-after" => (time_now + 10).httpdate},
body: {}
)

Expand All @@ -148,11 +150,11 @@ def test_client_retry_after_date
max_retries: 1
)

Thread.current.thread_variable_set(:time_now, time_now)
assert_raises(Stagehand::Errors::InternalServerError) do
Thread.current.thread_variable_set(:time_now, Time.now)
stagehand.sessions.start(model_name: "openai/gpt-4o")
Thread.current.thread_variable_set(:time_now, nil)
end
Thread.current.thread_variable_set(:time_now, nil)

assert_requested(:any, /./, times: 2)
assert_in_delta(10, Thread.current.thread_variable_get(:mock_sleep).last, 1.0)
Expand Down