ruby_llm-browser 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20b730312da9ae14b8e3a17d386ecaa0034a38b1ac87515e89d2c6a4aa29e8d7
4
- data.tar.gz: 7f27467a067bbf8e8712cfffe1a855ce85a21e563765658a142ca7c468297929
3
+ metadata.gz: 6c1e32269fa76e278df596a6e63e30d28506a9e7e3ada269703bad06ba870fef
4
+ data.tar.gz: d103e32333352508883d3794dd6e37a4aa3058d4f2e5842ccd174c6b2b43b11e
5
5
  SHA512:
6
- metadata.gz: ec5d0517a3b4fa4b4fbd2a440fd52d59b173b2f2adb4674c96f9e667bbaa57276984f09f1292865adb662fc1595f9f73b63c32509b9bd9269a058366bfa24ce0
7
- data.tar.gz: 13fea67455f9bdd9b49864752e69731b527958fbf06bd7ca0325a167345eaf115190cff9778247e8e526518fdef0b17c1fcb46f60346ab5c09508ca490f8cb35
6
+ metadata.gz: 39769537e3c05f25231657f2b56ab72bda7690ac2b7e144149cac85a8d2d7de7b29e73f45a4dd41fe8e6718aa0f1535d6c2d77d5f4203469474b4a64e018dc4d
7
+ data.tar.gz: 1ff61fb1a7fe9017be814634d6763938e6a6652814df76386b43ca90217f2ffad2f51429a5edec1855a431300802182c8d2ad7c9a9eb00890a6612ba9ccd7da4
data/CHANGELOG.md CHANGED
@@ -4,6 +4,25 @@ All notable changes to this project are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project aims to
5
5
  follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.3.0] - 2026-10-19
8
+
9
+ ### Added
10
+
11
+ - Opt-in `Rails.result_delivery` receives a sanitized `Result` and its operation
12
+ after execution and cleanup. Hosts own persistence, authorization and recovery.
13
+ - `Rails::ResultDeliveryError` reports failed delivery with retained in-process
14
+ operation/result objects. Callback errors do not leak through exception causes.
15
+ - A runnable host-owned SQLite result store and separate-process Active Job example,
16
+ with scoped reads, idempotent writes, failure recovery and worker-crash tests.
17
+ - Upgrade coverage against the frozen 0.2.0 migration with existing records retained.
18
+
19
+ ### Fixed
20
+
21
+ - Copy and freeze `Result` identity and error strings so constructor-input mutation
22
+ cannot alter a retained result. Mutating these result attributes raises `FrozenError`.
23
+ - Suppress result delivery from cancelled or fenced workers. A delivery failure
24
+ preserves session state; duplicate jobs never repeat browser work or delivery.
25
+
7
26
  ## [0.2.0] - 2026-10-19
8
27
 
9
28
  ### Added
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  An unofficial [RubyLLM](https://rubyllm.com) extension for read-only browser inspection
4
4
  and schema-checked extraction, with an optional Rails 8 adapter.
5
5
 
6
- **0.2.0 supports offline use only.** `FakeBackend` runs without credentials or network
6
+ **0.3.0 supports offline use only.** `FakeBackend` runs without credentials or network
7
7
  requests. The experimental Stagehand adapter refuses live operations; `Act` is an
8
8
  offline simulation. This project is not maintained or endorsed by RubyLLM or Browserbase.
9
9
 
@@ -16,7 +16,7 @@ offline simulation. This project is not maintained or endorsed by RubyLLM or Bro
16
16
  Use Ruby 3.2+ and RubyLLM 2.x. Add to your Gemfile, then run `bundle install`:
17
17
 
18
18
  ```ruby
19
- gem "ruby_llm-browser", "~> 0.2.0"
19
+ gem "ruby_llm-browser", "~> 0.3.0"
20
20
  ```
21
21
 
22
22
  ## Try it offline
data/docs/README.md CHANGED
@@ -8,6 +8,7 @@ the gem.
8
8
  - [Getting started](_getting_started/getting-started.md): installation and an offline example.
9
9
  - [Configuration](_getting_started/configuration.md): host inputs and process defaults.
10
10
  - [Inspect and Extract](_core_features/inspect-extract.md): tools and result handling.
11
+ - [Rails result delivery](_advanced/result-delivery.md): host storage, access checks, and delivery recovery.
11
12
  - [Rails jobs and recovery](_advanced/rails-integration.md): installation, workers, and cleanup.
12
13
  - [API and configuration reference](_reference/api-and-configuration.md): parameters and errors.
13
14
  - [Security model](SECURITY_MODEL.md): guarantees and limits.
data/docs/RELEASING.md CHANGED
@@ -17,6 +17,10 @@ After reading this guide, you will know:
17
17
 
18
18
  ## Compatibility evidence
19
19
 
20
+ The source repository's `docs/evidence/V0_3_VALIDATION.md` records local 0.3.0 checks.
21
+ `docs/evidence/V0_3_RELEASE.md` records the exact remote CI, published artifact and
22
+ hosted documentation results. The earlier audit below is historical evidence.
23
+
20
24
  The 2026-10-18 audit runs on Ruby **3.3.5**, arm64 macOS, with Stagehand **3.21.0**,
21
25
  JSONSchemer **2.5.0**, and sqlite3 **2.10.0**. Full offline suites cover:
22
26
 
@@ -40,6 +44,7 @@ bundle exec rake
40
44
  bundle exec bundle-audit check --update
41
45
  bundle exec ruby examples/plain_ruby.rb
42
46
  bundle exec ruby examples/rails_adapter.rb
47
+ bundle exec ruby examples/rails_results.rb
43
48
 
44
49
  BUNDLE_GEMFILE=gemfiles/ruby_llm_2_0.gemfile bundle install
45
50
  BUNDLE_GEMFILE=gemfiles/ruby_llm_2_0.gemfile bundle exec rspec
@@ -77,10 +82,10 @@ Verify all of the following:
77
82
  - Core require loads no Rails, Active Record, Active Job, or Stagehand client.
78
83
  - The optional Rails require loads on demand.
79
84
  - The generated migration and all README-linked guides are shipped.
80
- - Both examples run using the installed artifact.
85
+ - All shipped examples run using the installed artifact.
81
86
  - The artifact contains no keys, cookies, dumps, databases, lockfiles, or test fixtures.
82
87
  - Dependency/license inventory and known-advisory check match the resolved versions.
83
- - Record the artifact SHA-256, exact versions, commands and date in the WORK ledger.
88
+ - Record the artifact SHA-256, exact versions, commands and date in the candidate validation note.
84
89
 
85
90
  ## Release boundaries
86
91
 
@@ -114,12 +119,12 @@ section and run `bin/verify-release` without that flag. The tag workflow require
114
119
  released metadata; local candidate verification cannot enable publication.
115
120
 
116
121
  Run the checks above, review the package contents and push the completed commit to
117
- `main`. Then create and push a matching tag, for example `v0.2.0` for gem version
118
- `0.2.0`:
122
+ `main`. Then create and push a matching tag, for example `v{{ site.version }}` for gem version
123
+ `{{ site.version }}`:
119
124
 
120
125
  ```bash
121
- git tag -a v0.2.0 -m "Release 0.2.0"
122
- git push origin v0.2.0
126
+ git tag -a v{{ site.version }} -m "Release {{ site.version }}"
127
+ git push origin v{{ site.version }}
123
128
  ```
124
129
 
125
130
  The `Release Gem` workflow rejects a tag that does not match both the gem and docs
@@ -28,6 +28,13 @@ the RubyLLM 2.0 and Rails 8.0 alternate bundles, and the plain-Ruby and Rails ex
28
28
  Install each alternate bundle first if needed. Database suites run sequentially.
29
29
  RuboCop uses the writable `tmp/rubocop` cache. Default tests block unmocked HTTP.
30
30
 
31
+ The Rails result suite executes serialized jobs in separate processes against temporary
32
+ SQLite files. It verifies reads after worker exit, actor/run isolation, conflicting
33
+ delivery rejection, persistence failures before and after commit, and the crash gap
34
+ between browser execution and host storage. HTTP stays blocked in those subprocesses.
35
+ A frozen 0.2.0 migration verifies preservation of existing rows and indexes. These
36
+ checks exercise host result delivery without adding a second browser fixture.
37
+
31
38
  Commit each completed change with its docs and checks. Keep the next work item in the
32
39
  [roadmap]({% link _reference/roadmap.md %}); do not create a second task ledger.
33
40
 
@@ -15,6 +15,7 @@ After reading this guide, you will know:
15
15
 
16
16
  - How to install the adapter's tables.
17
17
  - How to build a freshly authorized job context with matching budgets.
18
+ - How to deliver results to host storage.
18
19
  - How to inspect status, cancel work, and recover orphaned sessions.
19
20
 
20
21
  ## Installing the adapter
@@ -103,9 +104,12 @@ summary = operation.summary
103
104
 
104
105
  `summary` omits provider session IDs and budget internals. It reports state, steps,
105
106
  bytes, fence, timestamps, error code and cancellation flag. It is coarse progress,
106
- not a percentage-complete promise. No result body is durably stored by the job; the
107
- host must explicitly implement authorized result storage/delivery if needed. Do not
108
- rely on an Active Job return value after asynchronous execution.
107
+ not a percentage-complete promise. To persist a result body, opt in to
108
+ `RubyLLM::Browser::Rails.result_delivery`. The callback receives `operation:` and
109
+ `result:` after execution and cleanup. Your application owns storage and authorized
110
+ retrieval. Read [Rails result delivery]({% link _advanced/result-delivery.md %}) for
111
+ failure handling and recovery. Do not rely on an Active Job return value after
112
+ asynchronous execution.
109
113
 
110
114
  The stores are trusted server APIs, not tenant-safe controllers. They expose unscoped
111
115
  fetch/sweep methods for workers; never expose them directly to model arguments or HTTP.
@@ -0,0 +1,119 @@
1
+ ---
2
+ layout: default
3
+ title: Rails Result Delivery
4
+ nav_order: 2
5
+ description: Persist sanitized background results in your application and recover delivery separately from browser cleanup.
6
+ ---
7
+
8
+ # {{ page.title }}
9
+
10
+ {{ page.description }}
11
+
12
+ After reading this guide, you will know:
13
+
14
+ - How to receive a background operation's bounded result.
15
+ - How to store and authorize results in your application.
16
+ - How to recover delivery without repeating browser work.
17
+
18
+ ## Persist a Result
19
+
20
+ Configure the optional Rails adapter and its context factory first. See
21
+ [Rails jobs and recovery]({% link _advanced/rails-integration.md %}). Register a trusted
22
+ callback in your initializer:
23
+
24
+ ```ruby
25
+ RubyLLM::Browser::Rails.result_delivery = lambda do |operation:, result:|
26
+ # Your host service persists result.to_h with operation.id, actor_id and run_id.
27
+ BrowserResults.persist(operation: operation, payload: result.to_h)
28
+ end
29
+ ```
30
+
31
+ `BrowserResults` is your application service. It is not a gem class. The callback
32
+ receives an immutable `Operation` snapshot and the core's sanitized, bounded `Result`.
33
+ Persist `result.to_h` or `result.to_json`, rather than the operation object, which
34
+ contains a provider session ID for trusted bookkeeping.
35
+
36
+ The callback runs synchronously in the worker after the engine attempts cleanup and
37
+ before the job returns. It receives successful, truncated, error, unknown, and
38
+ cleanup-pending results. Preflight exceptions have no `Result` and do not invoke it.
39
+ Cancellation or a changed fence detected after execution suppresses delivery. A
40
+ missing, already started, or terminal operation does not invoke it either.
41
+
42
+ The default is `nil`, which preserves the existing job return behavior. Setting the
43
+ hook to a non-callable value raises `InvalidInput`. Callback return values are ignored;
44
+ raise an exception if persistence fails.
45
+
46
+ ## Keep Storage and Access in the Host
47
+
48
+ Run the disposable, offline host example:
49
+
50
+ ```sh
51
+ bundle exec ruby examples/rails_results.rb
52
+ # RESULT_AFTER_WORKER_EXIT_PASS
53
+ # SCOPED_READS_PASS
54
+ # IDEMPOTENT_WRITE_PASS
55
+ ```
56
+
57
+ It writes a serialized Active Job to a temporary file, executes it in a separate
58
+ worker, waits for exit, and reads the schema-checked extraction from SQLite. Repeating
59
+ the job in another worker starts no browser session. The example's host table accepts
60
+ identical repeated writes and rejects conflicting bodies or actor/run identities.
61
+ Its file queue holds one job and is for demonstration; use a durable queue in your
62
+ application. The temporary database and queue file are removed on completion. Read
63
+ [the example source](https://github.com/yorzi/ruby_llm-browser/blob/main/examples/rails_results.rb).
64
+
65
+ Use a unique operation ID for idempotent writes. Repeating a write of the same result
66
+ should succeed; reject a conflicting body or actor/run identity. Retrieve with the
67
+ authenticated actor and run in the database query. Never use model-supplied actor IDs
68
+ as authorization. Recheck host revocation and retention rules when serving a result.
69
+
70
+ The gem creates no result table and adds no migrations. Keep results in your host's
71
+ database or durable storage. Do not rely on an Active Job return value or in-memory
72
+ callback state after a worker exits.
73
+
74
+ An `ok` extraction passed the pinned schema before redaction and bounding. A truncated
75
+ result can contain a preview instead of the original schema shape. Treat all page
76
+ content as untrusted. Store failures too, without interpreting `cleanup_pending` or
77
+ `unknown` as success. A stored result describes that attempt; a later reaper changes
78
+ session state, not the historical result body.
79
+
80
+ ## Recover Delivery Separately
81
+
82
+ If the callback raises, the job raises `RubyLLM::Browser::Rails::ResultDeliveryError`
83
+ with code `RESULT_DELIVERY_FAILED`. The original callback exception is not chained,
84
+ so automatic job logs cannot print its potentially sensitive message. The error
85
+ exposes `operation` and `result` to trusted, in-process host recovery code:
86
+
87
+ ```ruby
88
+ # Inside host code that catches ResultDeliveryError while these objects still exist:
89
+ RubyLLM::Browser::Rails.result_delivery.call(
90
+ operation: error.operation, result: error.result
91
+ )
92
+ ```
93
+
94
+ This retries only your callback. Reauthorize recovery in the host. Keep external side
95
+ effects idempotent and persist the result before sending notifications. For durable
96
+ notification retries, write the result and a host outbox entry in one transaction.
97
+ An exception object does not survive worker exit as durable result storage.
98
+
99
+ A failed callback does not change the operation's session state or error code. Queue
100
+ redelivery of `OperationJob` does not repeat a started browser operation and does not
101
+ redeliver its result. Keep delivery failures visible in your job monitoring; retrying
102
+ the browser job is not a result-recovery mechanism.
103
+
104
+ There is no atomic transaction across provider execution, operation bookkeeping, and
105
+ host persistence. A process crash after execution but before persistence can lose the
106
+ result. The hook offers one automatic delivery attempt, not guaranteed or exactly-once
107
+ delivery. If host storage is unavailable, a recovery handler must save the retained
108
+ result to a separate durable destination before it exits. Never recover a lost result
109
+ by replaying an uncertain browser operation automatically.
110
+
111
+ Cancellation can race after the worker's final check. If your host forbids storing a
112
+ result after revocation, enforce that rule in the persistence transaction as well as
113
+ at read time. A callback failure must not clear cancellation, pending cleanup, or quota.
114
+
115
+ ## Next Steps
116
+
117
+ Read [the API reference]({% link _reference/api-and-configuration.md %}) for result
118
+ fields and [Rails jobs and recovery]({% link _advanced/rails-integration.md %}) for
119
+ cancellation and reaper behavior.
data/docs/_config.yml CHANGED
@@ -108,6 +108,6 @@ code_language: ruby
108
108
  # and {{ site.examples.NAME }}. Candidates remain unreleased; the docs builder
109
109
  # reads stable docs from the latest release tag and main under /next/.
110
110
  released: true
111
- version: "0.2.0"
112
- stable_version: "0.2.0"
111
+ version: "0.3.0"
112
+ stable_version: "0.3.0"
113
113
  examples: {}
@@ -9,6 +9,6 @@
9
9
  collections:
10
10
  - reference
11
11
  - title: "What's New"
12
- url: /whats-new-in-0-2/
12
+ url: /whats-new-in-0-3/
13
13
  - title: Releases
14
14
  url: https://github.com/yorzi/ruby_llm-browser/releases
@@ -50,3 +50,6 @@ does not click or submit forms.
50
50
  * [Getting Started]({% link _getting_started/getting-started.md %}) - install and run the offline example.
51
51
  * [Configuration]({% link _getting_started/configuration.md %}) - choose host-owned defaults.
52
52
  * [API and configuration reference]({% link _reference/api-reference.md %}) - see options, statuses, and result fields.
53
+
54
+ Read [Rails Result Delivery]({% link _advanced/result-delivery.md %}) to persist a
55
+ background result in host storage and recover delivery separately from session cleanup.
@@ -0,0 +1,48 @@
1
+ ---
2
+ layout: default
3
+ title: What's New in 0.3
4
+ redirect_from:
5
+ - /whats-new/
6
+ nav_order: 4
7
+ description: Deliver Rails background results to host storage without repeating browser work on delivery failure.
8
+ ---
9
+
10
+ # {{ page.title }}
11
+
12
+ {{ page.description }}
13
+
14
+ After reading this guide, you will know:
15
+
16
+ - How to persist the result of a background operation.
17
+ - Which delivery failures your application must recover.
18
+ - Which capabilities remain unsupported.
19
+
20
+ ## Rails Result Delivery
21
+
22
+ Register a host callback to receive each eligible job's sanitized `Result` after
23
+ execution and cleanup:
24
+
25
+ ```ruby
26
+ RubyLLM::Browser::Rails.result_delivery = lambda do |operation:, result:|
27
+ BrowserResults.persist(operation: operation, payload: result.to_h)
28
+ end
29
+ ```
30
+
31
+ Your application supplies `BrowserResults`, storage, access checks, retention, and
32
+ idempotent recovery. The gem adds no database tables or runtime dependencies. Read
33
+ [Rails Result Delivery]({% link _advanced/result-delivery.md %}) for the contract.
34
+
35
+ Run `bundle exec ruby examples/rails_results.rb` for a disposable SQLite host example.
36
+ It executes a serialized job in a separate process, waits for the worker to exit,
37
+ then verifies result retrieval, actor/run isolation, and idempotent persistence.
38
+
39
+ ## Delivery Failure Is Separate from Cleanup
40
+
41
+ A callback failure raises `Rails::ResultDeliveryError` with the retained operation and
42
+ result. It does not change session state. A duplicate browser job does not rerun the
43
+ operation or redeliver the result. Recover host delivery using the retained result;
44
+ do not enqueue a new browser operation to recover persistence.
45
+
46
+ The hook cannot guarantee delivery across worker crashes. Stagehand and live Act remain
47
+ unsupported. Review [the roadmap]({% link _reference/roadmap.md %}) for live-backend
48
+ qualification and the remaining 1.0 requirements.
@@ -95,6 +95,10 @@ or redacted field need not retain the original schema's type. Always check the s
95
95
 
96
96
  ## Results and errors
97
97
 
98
+ `Result` copies and freezes its strings and nested data. This includes operation ID,
99
+ error code and error message; changes to constructor inputs cannot alter a retained
100
+ result. Duplicate a returned string if host code needs to edit it.
101
+
98
102
  `Result#to_h` contains symbol keys: `status`, `operation_id`, `final_url`,
99
103
  `captured_at` (UTC ISO 8601), `locator`, `digest`, `truncated`, `data`, and optional
100
104
  `error: { code:, message: }`. Locator/data objects use string keys. JSON encodes all
@@ -182,6 +186,17 @@ is deferred to a later version. The Engine refuses all non-fake Act calls before
182
186
  a session. Selector/description keyword filters are defense in depth, not semantic proof
183
187
  that a click is harmless. The Stagehand adapter does not support Act in this release.
184
188
 
189
+ ## Rails Result Delivery
190
+
191
+ After `require "ruby_llm/browser/rails"`, set `Rails.result_delivery` to a callable
192
+ accepting `operation:` and `result:`, or `nil` to disable it. `Rails.reset!` clears the
193
+ hook. The callback runs synchronously after execution and cleanup. Return values are
194
+ ignored; exceptions become `Rails::ResultDeliveryError` (`RESULT_DELIVERY_FAILED`),
195
+ which exposes `operation` and `result` to trusted recovery code. These objects are
196
+ not automatically persisted. Duplicate browser jobs do not retry delivery. Read
197
+ [Rails Result Delivery]({% link _advanced/result-delivery.md %}) for storage, access,
198
+ cancellation, and crash boundaries.
199
+
185
200
  ## Next steps
186
201
 
187
202
  - Read [Rails integration]({% link _advanced/rails-integration.md %}) for durable workers and recovery.
@@ -18,7 +18,8 @@ release notes to identify the exact source for this version.
18
18
 
19
19
  | Release | Date | Highlights |
20
20
  | --- | --- | --- |
21
- | [0.2.0]({% link _getting_started/whats-new-in-0-2.md %}) | 2026-10-19 | Read-only tool factory, backend-aware Doctor, and agent development/browser verification |
21
+ | [0.3.0]({% link _getting_started/whats-new-in-0-3.md %}) | 2026-10-19 | Opt-in Rails result delivery, durable host example, recovery and upgrade verification |
22
+ | [0.2.0]({% link _reference/whats-new-in-0-2.md %}) | 2026-10-19 | Read-only tool factory, backend-aware Doctor, and agent development/browser verification |
22
23
  | [0.1.0](https://github.com/yorzi/ruby_llm-browser/releases/tag/v0.1.0) | 2026-10-18 | Offline read-only Inspect and Extract, Rails adapter, and Act simulation; live Stagehand and live Act are unsupported |
23
24
 
24
25
  For changes to existing applications, follow [Upgrading]({% link _reference/upgrading.md %}).
@@ -24,8 +24,9 @@ Ruby and through the optional Rails adapter.
24
24
 
25
25
  Version 0.1.0 introduced the offline foundation: Inspect, Extract, URL policy, bounded
26
26
  results, operation bookkeeping, Rails jobs, and Act simulation. It does not provide a
27
- supported live backend. The current Stagehand adapter refuses live operations. Rails
28
- jobs track state but do not durably deliver result bodies.
27
+ supported live backend. The current Stagehand adapter refuses live operations. The
28
+ 0.3.0 release adds opt-in Rails result delivery to host-owned storage;
29
+ it does not add a gem-managed results database or guaranteed delivery.
29
30
 
30
31
  **This is a proposed development sequence, not a list of available features or release
31
32
  dates.** Future API names below are proposals. Keep each release small; use patch
@@ -48,8 +49,8 @@ feedback to settle the public API.
48
49
 
49
50
  ### 0.2.0: Two Small Features
50
51
 
51
- Version `0.2.0` adds both features below. Later milestones remain proposed scope. See
52
- [What's New]({% link _getting_started/whats-new-in-0-2.md %}) for usage and
52
+ Version `0.2.0` adds both features below. Milestones after 0.3 remain proposed scope. See
53
+ [What's New]({% link _reference/whats-new-in-0-2.md %}) for usage and
53
54
  [development and testing]({% link _advanced/development-and-testing.md %}) for validation.
54
55
 
55
56
  1. Add the read-only tool factory `RubyLLM::Browser.tools(context:, schema:)`.
@@ -72,8 +73,15 @@ validation precede the release tag and publication.
72
73
 
73
74
  ### 0.3.0: Complete the Rails Result Path
74
75
 
75
- Add an opt-in result-delivery hook that lets the host persist the sanitized `Result`
76
- against its operation ID. Provide a runnable host example that stores and retrieves
76
+ Version `0.3.0` implements `Rails.result_delivery`, typed
77
+ host-delivery failures, and a runnable SQLite host example with separate workers.
78
+ Read [What's New in 0.3]({% link _getting_started/whats-new-in-0-3.md %}) and
79
+ [Rails Result Delivery]({% link _advanced/result-delivery.md %}) for the current API.
80
+ The source repository's `docs/evidence/V0_3_VALIDATION.md` records local candidate
81
+ checks. `docs/evidence/V0_3_RELEASE.md` records remote verification and publication.
82
+
83
+ The result-delivery hook lets the host persist the sanitized `Result`
84
+ against its operation ID. Its runnable host example stores and retrieves
77
85
  the result after an asynchronous job finishes. The host owns access checks, retention,
78
86
  and storage; the gem does not need an admin interface or a general results database.
79
87
 
@@ -2,7 +2,7 @@
2
2
  layout: default
3
3
  title: Upgrading
4
4
  nav_order: 2
5
- description: Upgrade from 0.1.0 while preserving tool registration, diagnostics, and operation data.
5
+ description: Upgrade from 0.2.0 while preserving operation data and configuring optional Rails result delivery.
6
6
  ---
7
7
 
8
8
  # {{ page.title }}
@@ -11,11 +11,11 @@ description: Upgrade from 0.1.0 while preserving tool registration, diagnostics,
11
11
 
12
12
  After reading this guide, you will know:
13
13
 
14
- - How to update an application from 0.1.0.
15
- - Which tool and diagnostic contracts remain compatible.
16
- - How to preserve data from earlier development snapshots.
14
+ - How to update an application from 0.2.0.
15
+ - How to preserve existing data and enable result delivery.
16
+ - Which result and worker behaviors change.
17
17
 
18
- ## Upgrade from 0.1.0
18
+ ## Upgrade from 0.2.0
19
19
 
20
20
  Update your Gemfile and bundle:
21
21
 
@@ -27,38 +27,41 @@ gem "ruby_llm-browser", "~> {{ site.version }}"
27
27
  bundle update ruby_llm-browser
28
28
  ```
29
29
 
30
- There is no database migration or new runtime dependency. Keep existing operation and
31
- approval records. The individual Inspect and Extract constructors remain supported.
32
- You can optionally replace their registration with:
30
+ This release adds no gem-managed database migration or runtime dependency. Preserve all
31
+ operation, approval, and actor-lock rows. Do not rerun the install generator on an
32
+ existing installation. Result storage belongs to your application.
33
33
 
34
- ```ruby
35
- chat.with_tools(*RubyLLM::Browser.tools(context: context, schema: schema))
36
- ```
34
+ ## Enable Result Delivery When Needed
37
35
 
38
- The host still supplies the context and extraction schema. The factory never adds Act.
36
+ `RubyLLM::Browser::Rails.result_delivery` defaults to `nil`. Existing job return values
37
+ remain supported. To retain results after a worker exits, follow
38
+ [Rails Result Delivery]({% link _advanced/result-delivery.md %}). Configure persistence,
39
+ authorized reads, idempotent writes, and recovery before enabling the callback.
39
40
 
40
- ## Read Doctor Reports by Name
41
+ A callback failure raises `Rails::ResultDeliveryError`. Queue retries do not redeliver
42
+ a result. The error retains the result only in process; it is not durable storage.
43
+ Recover delivery separately from browser execution. A crash before host persistence
44
+ can still lose the result, so do not treat this hook as guaranteed delivery.
41
45
 
42
- Doctor adds `mode` while retaining `ok`, `summary`, and `checks`. Check names now depend
43
- on the backend. Do not require Stagehand or credential checks in every report, and do
44
- not rely on check order. FakeBackend diagnostics skip provider environment and SDK
45
- checks. Stagehand still reports a failure; `mode: :live` does not enable it.
46
+ ## Account for Immutable Results and Fenced Workers
46
47
 
47
- Read [Checking Configuration]({% link _core_features/diagnostics.md %}) for the report
48
- fields. No live browser provider is supported by this release.
48
+ `Result` now also copies and freezes operation ID, error code and error message strings,
49
+ consistent with its other fields. Mutating these attributes raises `FrozenError`;
50
+ duplicate a string before changing it in host code. Caller-owned inputs stay mutable.
49
51
 
50
- ## Earlier Development Snapshots
52
+ A worker fenced during browser execution now returns `:fenced_out` and cannot deliver
53
+ its stale result. Cancelled work never invokes the delivery hook, including while
54
+ cleanup remains pending. Delivery failures do not change the session state or free
55
+ quota. Stagehand and live Act remain unsupported.
51
56
 
52
- If you used an earlier development Rails generator, preserve operation data and follow
53
- the additive migration instructions in the
54
- [Rails guide]({% link _advanced/rails-integration.md %}). Drain workers during migration;
55
- do not reuse idempotency keys for legacy rows without request digests.
57
+ ## Earlier Versions
56
58
 
57
- For the previous version's instructions, read the
58
- [0.1.0 upgrading guide](https://github.com/yorzi/ruby_llm-browser/blob/v0.1.0/docs/_reference/upgrading.md).
59
+ For upgrades from 0.1.0 and earlier development snapshots, first follow the
60
+ [0.2.0 upgrading guide](https://github.com/yorzi/ruby_llm-browser/blob/v0.2.0/docs/_reference/upgrading.md).
61
+ Preserve operation data and use additive migrations where required.
59
62
 
60
63
  ## Next Steps
61
64
 
62
- - Read [What's New in 0.2]({% link _getting_started/whats-new-in-0-2.md %}).
65
+ - Read [What's New in 0.3]({% link _getting_started/whats-new-in-0-3.md %}).
63
66
  - Review [release history]({% link _reference/release-history.md %}) and
64
67
  [live-backend qualification]({% link _advanced/live-backend-qualification.md %}).
@@ -1,9 +1,8 @@
1
1
  ---
2
2
  layout: default
3
3
  title: What's New in 0.2
4
- redirect_from:
5
- - /whats-new/
6
- nav_order: 4
4
+ parent: Release History
5
+ nav_order: 1
7
6
  description: Use the read-only tool factory and backend-aware diagnostics in this release.
8
7
  ---
9
8
 
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A host-owned result table and a single-job file queue, using only FakeBackend.
4
+ # This disposable demonstration is not a production queue, controller or migration.
5
+ require "ruby_llm/browser/rails"
6
+ require "logger"
7
+ require "open3"
8
+ require "tmpdir"
9
+
10
+ module BrowserResultsExample
11
+ SCHEMA = { "type" => "object", "properties" => { "title" => { "type" => "string" } },
12
+ "required" => [ "title" ], "additionalProperties" => false }.freeze
13
+ RESOLVER = Object.new
14
+ def RESOLVER.getaddresses(_host) = [ "93.184.216.34" ]
15
+
16
+ class ResultRecord < ActiveRecord::Base
17
+ self.table_name = "example_browser_results"
18
+ self.primary_key = "operation_id"
19
+ end
20
+
21
+ class FileQueue
22
+ def initialize(path) = @path = path
23
+ def enqueue(job) = File.write(@path, JSON.generate(job.serialize))
24
+ def enqueue_at(*) = raise(ArgumentError, "demo queue supports immediate jobs only")
25
+ end
26
+
27
+ module_function
28
+
29
+ def connect(database)
30
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: database, timeout: 5000)
31
+ ActiveRecord::Base.logger = Logger.new(IO::NULL)
32
+ ActiveJob::Base.logger = Logger.new(IO::NULL)
33
+ RubyLLM::Browser::Rails::Schema.create!
34
+ connection = ActiveRecord::Base.connection
35
+ return if connection.table_exists?(ResultRecord.table_name)
36
+
37
+ connection.create_table(ResultRecord.table_name, id: false) do |table|
38
+ table.string :operation_id, primary_key: true, null: false
39
+ table.string :actor_id, null: false
40
+ table.string :run_id, null: false
41
+ table.text :body, null: false
42
+ table.datetime :created_at, null: false
43
+ end
44
+ end
45
+
46
+ def configure(backend: RubyLLM::Browser::FakeBackend.new)
47
+ RubyLLM::Browser::Rails.context_factory = lambda do |actor_id:, run_id:|
48
+ RubyLLM::Browser::Context.new(
49
+ actor_id: actor_id, run_id: run_id, backend: backend,
50
+ source_policy: RubyLLM::Browser::Policy.new(allowed_hosts: [ "docs.example.com" ], resolver: RESOLVER),
51
+ operation_store: RubyLLM::Browser::Rails.operation_store
52
+ )
53
+ end
54
+ RubyLLM::Browser::Rails.result_delivery = lambda do |operation:, result:|
55
+ store_result(operation: operation, payload: result.to_h)
56
+ end
57
+ end
58
+
59
+ # Trusted server API. The worker supplies identity, never a model or request body.
60
+ def store_result(operation:, payload:)
61
+ body = RubyLLM::Browser::Redactor.canonical_json(payload)
62
+ ResultRecord.transaction do
63
+ record = ResultRecord.create_or_find_by!(operation_id: operation.id) do |row|
64
+ row.actor_id = operation.actor_id
65
+ row.run_id = operation.run_id
66
+ row.body = body
67
+ end
68
+ unless [ record.actor_id, record.run_id, record.body ] == [ operation.actor_id, operation.run_id, body ]
69
+ raise RubyLLM::Browser::InvalidInput, "conflicting result delivery"
70
+ end
71
+ end
72
+ end
73
+
74
+ # In an HTTP host, derive actor/run from authenticated records and recheck access.
75
+ # Use the same response for a missing result and an inaccessible result.
76
+ def read_result(operation_id:, actor_id:, run_id:)
77
+ record = ResultRecord.find_by(operation_id: operation_id, actor_id: actor_id, run_id: run_id)
78
+ raise RubyLLM::Browser::NotAuthorized, "result unavailable" unless record
79
+
80
+ JSON.parse(record.body)
81
+ end
82
+
83
+ def worker(database, queue_file)
84
+ connect(database)
85
+ backend = RubyLLM::Browser::FakeBackend.new
86
+ configure(backend: backend)
87
+ ActiveJob::Base.execute(JSON.parse(File.read(queue_file)))
88
+ puts JSON.generate(starts: backend.call_count(:start), closes: backend.call_count(:end_session))
89
+ end
90
+
91
+ def run_demo
92
+ Dir.mktmpdir("browser-results-") do |directory|
93
+ database = File.join(directory, "host.sqlite3")
94
+ queue_file = File.join(directory, "job.json")
95
+ connect(database)
96
+ ActiveJob::Base.queue_adapter = FileQueue.new(queue_file)
97
+ operation = RubyLLM::Browser::Rails::OperationJob.enqueue(
98
+ actor_id: "demo-actor", run_id: "demo-run", kind: :extract,
99
+ url: "https://docs.example.com/", schema: SCHEMA,
100
+ budget: RubyLLM::Browser::Budget.new, idempotency_key: "demo-result"
101
+ )
102
+ # capture3 waits for exit. Execute the serialized job twice in separate workers.
103
+ [ 1, 0 ].each do |expected_starts|
104
+ stdout, stderr, status = Open3.capture3(RbConfig.ruby, "-I", File.expand_path("../lib", __dir__),
105
+ __FILE__, "worker", database, queue_file)
106
+ raise "worker failed: #{stderr}" unless status.success?
107
+ unless JSON.parse(stdout) == { "starts" => expected_starts, "closes" => expected_starts }
108
+ raise "unexpected browser dispatch"
109
+ end
110
+ end
111
+
112
+ payload = read_result(operation_id: operation.id, actor_id: "demo-actor", run_id: "demo-run")
113
+ raise "lost extraction" unless payload.fetch("data") == { "title" => "Example" } && payload.fetch("status") == "ok"
114
+ raise "lost provenance" unless payload.fetch("locator").fetch("schema_digest") == RubyLLM::Browser::Redactor.digest(SCHEMA)
115
+ puts "RESULT_AFTER_WORKER_EXIT_PASS"
116
+
117
+ [ [ "other-actor", "demo-run" ], [ "demo-actor", "other-run" ] ].each do |actor, run|
118
+ begin
119
+ read_result(operation_id: operation.id, actor_id: actor, run_id: run)
120
+ raise "cross-scope result leak"
121
+ rescue RubyLLM::Browser::NotAuthorized
122
+ # Expected refusal, including after the worker has exited.
123
+ end
124
+ end
125
+ puts "SCOPED_READS_PASS"
126
+ 2.times { store_result(operation: operation, payload: payload) }
127
+ raise "duplicate result rows" unless ResultRecord.count == 1
128
+ puts "IDEMPOTENT_WRITE_PASS"
129
+ ensure
130
+ ActiveRecord::Base.connection_pool.disconnect!
131
+ end
132
+ end
133
+ end
134
+
135
+ if $PROGRAM_NAME == __FILE__
136
+ if ARGV.first == "worker"
137
+ BrowserResultsExample.worker(*ARGV.drop(1))
138
+ else
139
+ BrowserResultsExample.run_demo
140
+ end
141
+ end
@@ -91,8 +91,14 @@ module RubyLLM
91
91
  max_bytes: max_bytes)
92
92
 
93
93
  # Keep unknown action/start results, even when cleanup later closes a session.
94
- final = settled(operation_id)
94
+ completed = store.fetch!(operation_id)
95
+ final = settled(completed)
95
96
  return :cancelled if final == :cancelled
97
+ # Cancellation can still await cleanup. A reaper or newer worker can also
98
+ # fence this job while the browser call is in flight.
99
+ return :fenced_out unless completed.cancel_requested? || completed.fence == leased.fence
100
+
101
+ deliver(completed, result) unless completed.cancel_requested?
96
102
 
97
103
  result.ok? ? (final || result.to_h) : result.to_h
98
104
  rescue InvalidInput => e
@@ -106,15 +112,21 @@ module RubyLLM
106
112
  # Reports the durable state if the host cancelled or another worker fenced this
107
113
  # one while the operation was running; otherwise returns nil so the engine's own
108
114
  # result is used.
109
- def settled(operation_id)
110
- final = store.fetch(operation_id)
111
- case final&.state
115
+ def settled(operation)
116
+ case operation.state
112
117
  when :cancelled then :cancelled
113
118
  when :cleanup_pending then :cleanup_pending
114
119
  when :unknown then :unknown
115
120
  end
116
121
  end
117
122
 
123
+ def deliver(operation, result)
124
+ RubyLLM::Browser::Rails.result_delivery&.call(operation: operation, result: result)
125
+ rescue StandardError
126
+ # Queue logs may print chained exceptions containing host credentials.
127
+ raise ResultDeliveryError.new(operation: operation, result: result), cause: nil
128
+ end
129
+
118
130
  def run(kind, operation, url:, instruction:, schema:, max_bytes:)
119
131
  context = build_context(operation)
120
132
  unless context.is_a?(Context) && context.operation_store.equal?(store)
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Browser
5
+ module Rails
6
+ # The host callback failed after browser execution and cleanup were attempted.
7
+ # Retain these values in host storage to retry delivery without rerunning a job.
8
+ # These are in-process objects, not a durable queue or automatic retry policy.
9
+ class ResultDeliveryError < Error
10
+ CODE = "RESULT_DELIVERY_FAILED"
11
+
12
+ attr_reader :operation, :result
13
+
14
+ def initialize(operation:, result:)
15
+ @operation = operation
16
+ @result = result
17
+ super("host result delivery failed; retry delivery without repeating browser work")
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -27,6 +27,7 @@ require_relative "rails/operation_record"
27
27
  require_relative "rails/approval_record"
28
28
  require_relative "rails/active_record_operation_store"
29
29
  require_relative "rails/active_record_approval_store"
30
+ require_relative "rails/result_delivery_error"
30
31
  require_relative "rails/operation_job"
31
32
  require_relative "rails/reaper"
32
33
 
@@ -41,6 +42,20 @@ module RubyLLM
41
42
  # ->(actor_id:, run_id:) { RubyLLM::Browser::Context }, supplied by the host.
42
43
  attr_accessor :context_factory
43
44
 
45
+ # Optional host callback: ->(operation:, result:) { persist_result }.
46
+ # Runs synchronously after execution and cleanup, before the job returns.
47
+ # Raise to report failure; return values are ignored. Duplicate jobs do not
48
+ # redeliver. The host owns persistence, idempotency, access and recovery.
49
+ attr_reader :result_delivery
50
+
51
+ def result_delivery=(callback)
52
+ unless callback.nil? || callback.respond_to?(:call)
53
+ raise InvalidInput, "result_delivery must be callable or nil"
54
+ end
55
+
56
+ @result_delivery = callback
57
+ end
58
+
44
59
  # Returns the current UTC time used by the adapter.
45
60
  def clock
46
61
  @clock ||= -> { Time.now.utc }
@@ -68,6 +83,7 @@ module RubyLLM
68
83
  # Restores process defaults. Primarily a test helper.
69
84
  def reset!
70
85
  @context_factory = nil
86
+ @result_delivery = nil
71
87
  @clock = nil
72
88
  @job_queue = nil
73
89
  @operation_store = nil
@@ -27,15 +27,15 @@ module RubyLLM
27
27
  def initialize(status:, operation_id: nil, final_url: nil, captured_at: nil, locator: nil,
28
28
  digest: nil, data: nil, truncated: false, error_code: nil, error_message: nil)
29
29
  @status = normalize_status(status)
30
- @operation_id = operation_id
30
+ @operation_id = Immutable.copy(operation_id)
31
31
  @final_url = Immutable.copy(final_url)
32
32
  @captured_at = Immutable.copy(captured_at)
33
33
  @locator = Immutable.copy(locator)
34
34
  @digest = Immutable.copy(digest)
35
35
  @data = Immutable.copy(data)
36
36
  @truncated = truncated ? true : false
37
- @error_code = error_code
38
- @error_message = error_message
37
+ @error_code = Immutable.copy(error_code)
38
+ @error_message = Immutable.copy(error_message)
39
39
  freeze
40
40
  end
41
41
 
@@ -4,6 +4,6 @@ module RubyLLM
4
4
  module Browser
5
5
  # The Gem version. Development happens ahead of any public release: the packaged
6
6
  # version is not a RubyGems publication claim.
7
- VERSION = "0.2.0"
7
+ VERSION = "0.3.0"
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm-browser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Wang
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-18 00:00:00.000000000 Z
11
+ date: 2026-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby_llm
@@ -174,6 +174,7 @@ files:
174
174
  - docs/_advanced/development-and-testing.md
175
175
  - docs/_advanced/live-backend-qualification.md
176
176
  - docs/_advanced/rails-integration.md
177
+ - docs/_advanced/result-delivery.md
177
178
  - docs/_config.yml
178
179
  - docs/_core_features/diagnostics.md
179
180
  - docs/_core_features/inspect-extract.md
@@ -186,13 +187,14 @@ files:
186
187
  - docs/_getting_started/configuration.md
187
188
  - docs/_getting_started/getting-started.md
188
189
  - docs/_getting_started/overview.md
189
- - docs/_getting_started/whats-new-in-0-2.md
190
+ - docs/_getting_started/whats-new-in-0-3.md
190
191
  - docs/_plugins/versioned_docs.rb
191
192
  - docs/_reference/api-and-configuration.md
192
193
  - docs/_reference/api-reference.md
193
194
  - docs/_reference/release-history.md
194
195
  - docs/_reference/roadmap.md
195
196
  - docs/_reference/upgrading.md
197
+ - docs/_reference/whats-new-in-0-2.md
196
198
  - docs/bin/build-api.sh
197
199
  - docs/bin/build.sh
198
200
  - docs/bin/export-api-markdown.rb
@@ -204,6 +206,7 @@ files:
204
206
  - docs/index.md
205
207
  - examples/plain_ruby.rb
206
208
  - examples/rails_adapter.rb
209
+ - examples/rails_results.rb
207
210
  - lib/generators/ruby_llm/browser/install_generator.rb
208
211
  - lib/generators/ruby_llm/browser/templates/create_ruby_llm_browser_tables.rb.tt
209
212
  - lib/ruby_llm/browser.rb
@@ -232,6 +235,7 @@ files:
232
235
  - lib/ruby_llm/browser/rails/operation_job.rb
233
236
  - lib/ruby_llm/browser/rails/operation_record.rb
234
237
  - lib/ruby_llm/browser/rails/reaper.rb
238
+ - lib/ruby_llm/browser/rails/result_delivery_error.rb
235
239
  - lib/ruby_llm/browser/rails/schema.rb
236
240
  - lib/ruby_llm/browser/redactor.rb
237
241
  - lib/ruby_llm/browser/result.rb