Skip to content
Draft
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
44 changes: 22 additions & 22 deletions packages/sdk-ruby/ESTIMATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,34 @@ Python and Go SDKs are built.

## What the spike proved (de-risked)

| Risk | Outcome |
|---|---|
| No JSON-Schema→Ruby codegen exists | Custom stdlib-only generator, 274 LOC, covers all 234 defs + method registries, `--check` drift mode wired into `just generate`/`just check` |
| Wire casing | Free — the schema is already snake_case; opaque containers pass through verbatim (fixture round-trip tested) |
| Sync Ruby ↔ bidirectional RPC | Background-reader-thread model (Go-style) works; `websocket-driver` + own TCP/SSL socket, no reactor framework needed |
| Chrome without Playwright | Hand-rolled launcher ported from Python's `browser.py` works headless + headed on macOS |
| Extension packaging | Ruby zip is **byte-identical** to the Python SDK's deterministic archive (same SHA-256) |
| No Browserbase Ruby SDK exists | Hand-rolled 4-endpoint REST client (~130 LOC) suffices, mirroring Go |
| End-to-end | `goto → observe → act → extract` verified live on a local Chrome (transport) and on a Browserbase session (full AI loop incl. extension upload + Model Gateway + session release) |
| Risk | Outcome |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| No JSON-Schema→Ruby codegen exists | Custom stdlib-only generator, 274 LOC, covers all 234 defs + method registries, `--check` drift mode wired into `just generate`/`just check` |
| Wire casing | Free — the schema is already snake_case; opaque containers pass through verbatim (fixture round-trip tested) |
| Sync Ruby ↔ bidirectional RPC | Background-reader-thread model (Go-style) works; `websocket-driver` + own TCP/SSL socket, no reactor framework needed |
| Chrome without Playwright | Hand-rolled launcher ported from Python's `browser.py` works headless + headed on macOS |
| Extension packaging | Ruby zip is **byte-identical** to the Python SDK's deterministic archive (same SHA-256) |
| No Browserbase Ruby SDK exists | Hand-rolled 4-endpoint REST client (~130 LOC) suffices, mirroring Go |
| End-to-end | `goto → observe → act → extract` verified live on a local Chrome (transport) and on a Browserbase session (full AI loop incl. extension upload + Model Gateway + session release) |

Spike size: ~2,400 hand-written LOC + ~2,000 generated + ~800 tests. Python comparison:
~4,900 hand-written + ~3,600 generated — a fair proxy for the finished Ruby size.

## Work breakdown to production parity

| # | Item | Weeks |
|---|---|---|
| A | Walking skeleton (this spike) | 2.5–3 *(sunk)* |
| B | Full method surface (~62 remaining: context 14, page ~26, locator 17, response 6, clipboard, webmcp, file upload, callback_batch passthrough) — mechanical wrapper + tests per method, batched by namespace | 3–4 |
| C | Client-side LLM (`llm.generate` inbound handler, message/tool unions, custom-LLM example) | 1 |
| D | Validation hardening: strict unions, input ergonomics, RBS signatures, thread-safety soak | 1–1.5 |
| E | 11-example set + `example-parity` compliance | 0.5–1 |
| F | Parity tooling: ast-grep Ruby lane (`@ast-grep/lang-ruby` availability is the biggest unknown; prism-based fallback +0.5–1 wk) + extend `rules/ast-grep/*` | 1–1.5 |
| G | Docs: Ruby tabs across `docs/v4/reference/*` + guides + `sdk-reference.test.ts` | 1 |
| H | Release + CI: turbo task, changesets version proxy, `sync-ruby-version.ts`, extension embedding in the gem, RubyGems trusted publishing + alpha lane, CI matrix (Ruby 3.2–3.4 × macOS/Linux), **Windows launcher** | 1.5–2 |
| I | Beta hardening buffer (real-world sites, large payloads, memory/soak) | 1–1.5 |
| | **Total beyond spike** | **10–13.5** |
| | **Total including spike** | **~12.5–16.5** |
| # | Item | Weeks |
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------- |
| A | Walking skeleton (this spike) | 2.5–3 _(sunk)_ |
| B | Full method surface (~62 remaining: context 14, page ~26, locator 17, response 6, clipboard, webmcp, file upload, callback_batch passthrough) — mechanical wrapper + tests per method, batched by namespace | 3–4 |
| C | Client-side LLM (`llm.generate` inbound handler, message/tool unions, custom-LLM example) | 1 |
| D | Validation hardening: strict unions, input ergonomics, RBS signatures, thread-safety soak | 1–1.5 |
| E | 11-example set + `example-parity` compliance | 0.5–1 |
| F | Parity tooling: ast-grep Ruby lane (`@ast-grep/lang-ruby` availability is the biggest unknown; prism-based fallback +0.5–1 wk) + extend `rules/ast-grep/*` | 1–1.5 |
| G | Docs: Ruby tabs across `docs/v4/reference/*` + guides + `sdk-reference.test.ts` | 1 |
| H | Release + CI: turbo task, changesets version proxy, `sync-ruby-version.ts`, extension embedding in the gem, RubyGems trusted publishing + alpha lane, CI matrix (Ruby 3.2–3.4 × macOS/Linux), **Windows launcher** | 1.5–2 |
| I | Beta hardening buffer (real-world sites, large payloads, memory/soak) | 1–1.5 |
| | **Total beyond spike** | **10–13.5** |
| | **Total including spike** | **~12.5–16.5** |

## Ongoing cost

Expand Down
40 changes: 20 additions & 20 deletions packages/sdk-ruby/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ end to end but deliberately implements only a sliver of the API surface.
and WebMCP (`tools` → invoke/result/cancel).
- **Locator** — `page.locator(selector)` → all 17 locator methods
(`click/fill/type/hover/scroll_to/count/text_content/inner_text/inner_html/
input_value/visible?/checked?/centroid/highlight/send_click_event/
select_option/set_input_files` + `first`/`nth`; file uploads take paths or
input_value/visible?/checked?/centroid/highlight/send_click_event/
select_option/set_input_files` + `first`/`nth`; file uploads take paths or
`Stagehand::FilePayload`, 50 MiB/file).
- **Context** — `pages/new_page/active_page/set_active_page/close`, cookies
(`cookies/add_cookies/clear_cookies` with String/Regexp filters), clipboard
Expand Down Expand Up @@ -90,24 +90,24 @@ Browserbase sessions.
Ruby ports of the canonical example set (`packages/sdk-{ts,python,go}/examples`),
each runnable as `bundle exec ruby examples/<name>.rb [--browserbase]`:

| Example | Notes |
|---|---|
| `act.rb`, `observe.rb`, `extract.rb` | example.com, mirror the Python examples |
| `model_gateway.rb` | Browserbase-only; no model configured (Gateway picks one) |
| `caching.rb` | Browserbase-only; `cache: true` + `metadata.cache` round-trip |
| `custom_logging.rb` | `on_log:` callback appending JSONL to `stagehand.jsonl` |
| `file_upload.rb` | mirrors the Python example; no LLM needed (runs local or Browserbase) |
| `page_interactions.rb` | locator fill/type/click/readers, evaluate, screenshot, history; no LLM needed |
| `hybrid_news.rb` | AI extract + deterministic locators/pagination/screenshot on Hacker News |
| `search_flow.rb` | locator-driven search on DuckDuckGo + AI extraction of the results |
| `batch.rb` | mirrors the Python example; callback batch, no LLM needed |
| `page_events.rb` | mirrors the Python example; page.on console events + AI extract |
| `context_and_response.rb` | Response/cookies/clipboard/viewport/snapshot tour; no LLM needed |
| `custom_llm.rb` | mirrors the Python example; bring-your-own-LLM via `llm.generate` (OPENAI_API_KEY or AI_GATEWAY_API_KEY) |
| `demo.rb`, `arctic_observe.rb` | spike walkthroughs (not part of the canonical set) |

The one remaining canonical example is `webmcp`, which needs a WebMCP-enabled
target page (the `page.webmcp_*` methods themselves are wrapped and tested).
| Example | Notes |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------- |
| `act.rb`, `observe.rb`, `extract.rb` | example.com, mirror the Python examples |
| `model_gateway.rb` | Browserbase-only; no model configured (Gateway picks one) |
| `caching.rb` | Browserbase-only; `cache: true` + `metadata.cache` round-trip |
| `custom_logging.rb` | `on_log:` callback appending JSONL to `stagehand.jsonl` |
| `file_upload.rb` | mirrors the Python example; no LLM needed (runs local or Browserbase) |
| `page_interactions.rb` | locator fill/type/click/readers, evaluate, screenshot, history; no LLM needed |
| `hybrid_news.rb` | AI extract + deterministic locators/pagination/screenshot on Hacker News |
| `search_flow.rb` | locator-driven search on DuckDuckGo + AI extraction of the results |
| `batch.rb` | mirrors the Python example; callback batch, no LLM needed |
| `page_events.rb` | mirrors the Python example; page.on console events + AI extract |
| `context_and_response.rb` | Response/cookies/clipboard/viewport/snapshot tour; no LLM needed |
| `custom_llm.rb` | mirrors the Python example; bring-your-own-LLM via `llm.generate` (OPENAI_API_KEY or AI_GATEWAY_API_KEY) |
| `webmcp.rb` | mirrors the Python example; page-registered tools via `page.tools` → invoke → result; no LLM needed |
| `demo.rb`, `arctic_observe.rb` | spike walkthroughs (not part of the canonical set) |

That completes the canonical example set from `packages/sdk-{ts,python,go}/examples`.

## Development

Expand Down
49 changes: 49 additions & 0 deletions packages/sdk-ruby/examples/webmcp.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

# WebMCP: pages can register tools (navigator.modelContext); Stagehand lists
# them via page.tools and invokes them. Ruby port of
# packages/sdk-python/examples/webmcp.py — no LLM needed.
#
# ruby examples/webmcp.rb # local Chrome (headed, like the sibling SDKs)
# ruby examples/webmcp.rb --browserbase # Browserbase session

require "json"

require_relative "../lib/stagehand"
require_relative "example_helpers"

WEBMCP_TEST_SITE = "https://browserbase.github.io/stagehand-eval-sites/sites/webmcp-test/"

browser =
if ARGV.include?("--browserbase")
api_key = ExampleHelpers.env("BROWSERBASE_API_KEY") or abort "Set BROWSERBASE_API_KEY."
puts "Creating a Browserbase session..."
Stagehand::Browserbase.launch(api_key: api_key)
else
puts "Launching local Chrome..."
Stagehand::LocalBrowser.launch(headless: false)
end

begin
stagehand = Stagehand.create(browser: browser, log_level: ENV.fetch("STAGEHAND_LOG_LEVEL", "warn"))
begin
page = browser.context.pages.first
raise "Stagehand initialized without an active page" if page.nil?
page.goto(WEBMCP_TEST_SITE)

tools = page.tools(timeout: 5_000)
puts "tools -> #{tools.map(&:name).join(", ")}"
calculate_sum = tools.find { |tool| tool.name == "calculateSum" }
raise "calculateSum was not registered by the page" if calculate_sum.nil?

invocation = calculate_sum.invoke(input: { a: 19, b: 23 })
result = invocation.result

puts JSON.pretty_generate(result.to_wire)
ensure
stagehand.close
end
ensure
browser.close
end
puts "Closed."