ruby_llm-ag_ui 0.4.0 → 1.0.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: 7e2fef2885431f82e496f0e1682bbd6d11ed036a6dcda941353e5f70d22e9589
4
- data.tar.gz: 06d1631a5e98ecfea25a53dd9ea9f40c235f2fea871fcc7d4fb9e2b707596439
3
+ metadata.gz: faaf47d4aefadd16b5b9f9ee3a069efca39ada7c5e6935306879d82fcbe6094f
4
+ data.tar.gz: 2d81db9c7f7429a33391eb485d791160b5cf363b3313a0c3e65807a72b85fb16
5
5
  SHA512:
6
- metadata.gz: bc183b5b76a8f3f92a27feb731a879780b454425a95c024bfc5ebba2c23f006ec2da5c2682c1c2074dcb4a946650d3941aa0482915c3987873c5bcc420613f3e
7
- data.tar.gz: 19da3e02abe3806802bbe4efee68138a3a8b7e31477ae43a7085511f748dd88df59c71ba12398bf84310a74c7e57fca8ee25581e26d9ce797816ca95f5b5f3db
6
+ metadata.gz: 4ae655ed6335e4ca391c2caf310567d0ca471d4d2cc7594cae3c0d631456d28627bf3ac2828f895f59e742bc9ea2e9cd94c7e8ea6549fb4db724cadb1506e05f
7
+ data.tar.gz: 15b3541966a7a4fc2baa342525da774e1adff49929c4d5e334ea8c420ae14a75a5c62bcb574f28a0b69b282865e355c5395391d3cd193ca160dc4a93fe095d26
data/CHANGELOG.md CHANGED
@@ -6,6 +6,49 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.0.0] - 2026-11-02
10
+
11
+ The stable integration contract. The public API, configuration, and store
12
+ contract proposed in 0.4 are now covered by Semantic Versioning.
13
+
14
+ ### Removed
15
+
16
+ - Support for Ruby 3.2, which reached end of life in March 2026. The gem now
17
+ requires Ruby 3.3 or newer, and CI tests Ruby 3.3, 3.4, and 4.0.
18
+
19
+ ### Changed
20
+
21
+ - The durable store finds interrupted runs through the shipped status index,
22
+ instead of loading and parsing every run on the thread for each resume.
23
+
24
+ ### Fixed
25
+
26
+ - Open a new tool call when a streamed fragment carries a new id under a stream
27
+ index already in use, as RubyLLM does. An OpenAI-compatible server that sends
28
+ each parallel call whole at index 0 no longer has a second call's arguments
29
+ appended to the first call.
30
+ - Handle stream refusals in Rails hosts without Active Record, preserving
31
+ unrelated host errors instead of replacing them with a missing constant.
32
+ - Honor zero quality and media-range precedence in `SSEWriter.accepts?`.
33
+ Rails refuses an unacceptable stream with 406 before resolving a chat or
34
+ reserving a run, even when a wildcard would otherwise allow streaming.
35
+ - Open each streamed tool call of a later tool round in the same turn. Providers
36
+ restart stream indexes in every response, so a second round's arguments were
37
+ sent to the first round's ended call before the new call opened.
38
+ - Send streamed tool arguments that concatenate to valid JSON for Anthropic,
39
+ Bedrock, and Gemini. The empty input object that opens an Anthropic or
40
+ Bedrock call is no longer sent before its partial JSON, and a whole Gemini
41
+ call is serialized as JSON instead of Ruby `Hash#to_s` output.
42
+ - Defer loading `ActiveRecord::Base` until the host uses the durable store.
43
+ Requiring the gem during a Rails boot no longer loads Active Record before
44
+ the application's configuration.
45
+ - Accept a redaction policy object that defines only `#call`, instead of
46
+ failing the run when a tool result or progress update is redacted.
47
+ - Refuse input with text that is not UTF-8, or a number JSON cannot represent,
48
+ as `MalformedInputError` (400) instead of raising `JSON::GeneratorError`.
49
+ - Release a finished run's response stream. RubyLLM cannot remove callbacks,
50
+ so a chat reused across turns kept every earlier run's stream reachable.
51
+
9
52
  ## [0.4.0] - 2026-10-21
10
53
 
11
54
  ### Added
data/CONTRIBUTING.md CHANGED
@@ -41,6 +41,10 @@ The package probe installs the artifact outside the checkout using available
41
41
  development dependencies; `bin/verify-package` separately tests clean dependency
42
42
  resolution. Shut down all task-owned processes when the check finishes.
43
43
 
44
+ Before a release, build the gem and run `bundle exec ruby bin/verify-upgrade`
45
+ with the downloaded previous release's gem path. It checks installed artifacts in
46
+ separate processes against one temporary SQLite database, without a provider.
47
+
44
48
  Every behavior change needs a test, and lifecycle, identity and security changes
45
49
  need their negative case too. The acceptance IDs (FR-01..FR-08) are referenced
46
50
  in spec descriptions. Offline CI uses fakes; live provider tests are opt-in and
data/README.md CHANGED
@@ -19,10 +19,10 @@ RubyLLM and AG-UI maintainers.
19
19
  ## Install
20
20
 
21
21
  ```ruby
22
- gem 'ruby_llm-ag_ui', '~> 0.4.0'
22
+ gem 'ruby_llm-ag_ui', '~> 1.0'
23
23
  ```
24
24
 
25
- Requires Ruby >= 3.2, RubyLLM >= 2.1 and < 3.0, and `ag-ui-protocol` 0.2.x.
25
+ Requires Ruby >= 3.3, RubyLLM >= 2.1 and < 3.0, and `ag-ui-protocol` 0.2.x.
26
26
  Run `bundle install` after adding the gem.
27
27
 
28
28
  ## Stream a run
@@ -37,6 +37,13 @@ Choose **Plain Ruby** or **Rails + SQLite**, then run a turn. The official
37
37
  chat per fixture actor and thread, so approval resumes the original tool call
38
38
  and later turns use the same conversation.
39
39
 
40
+ The development bundle pins `@ag-ui/client` 1.0.3 and overrides Zod to 4.6.6
41
+ for [GHSA-mrhx-c7xc-m3w5](https://github.com/advisories/GHSA-mrhx-c7xc-m3w5).
42
+ The client and core schema code import `zod/v4`; browser workflows verify the
43
+ override with the pinned client. Run `npm audit` alongside the browser checks.
44
+ These JavaScript packages are development dependencies and are not shipped in
45
+ the Ruby gem.
46
+
40
47
  ## Try the Interactions
41
48
 
42
49
  The tool scenario pauses for approval. Approve it to run a synthetic order
@@ -85,7 +85,7 @@ app knows what the client is looking at.
85
85
  | Request | Response |
86
86
  |---|---|
87
87
  | Valid input, `Accept: text/event-stream` | `200`, SSE, one `data:` line per AG-UI event |
88
- | `Accept` that cannot read SSE | `406` before any event |
88
+ | `Accept` that cannot read SSE, including an effective `q=0` | `406` before resolving the chat or reserving a run |
89
89
  | Empty, non-JSON, or schema-invalid body | `400` |
90
90
  | Body past `config.max_input_bytes` | `413` |
91
91
  | `threadId` the actor does not own, when the block raises `ActiveRecord::RecordNotFound` | `404` |
@@ -195,6 +195,14 @@ what `spec/support/fake_chat.rb` in this repository does.
195
195
  | Tool results are `[redacted]` | Redaction is the default; configure `config.redact` |
196
196
  | A run succeeds but the client shows nothing | The client was not sent the interrupt/resume flow it expected, or it re-sent a `runId` whose run had already ended |
197
197
 
198
+ ## Use Rails Without Active Record
199
+
200
+ You can use the streaming concern without Active Record. In that setup,
201
+ configure a host-supplied store and resolve the chat from your own persistence.
202
+ Stream conflicts still return 409. When Active Record is loaded, a missing
203
+ record returns 404. Other errors raised while resolving a chat remain host
204
+ errors and propagate to your application's error handling.
205
+
198
206
  ## Next Steps
199
207
 
200
208
  * [Approval and Resume]({% link _core_features/approval-and-resume.md %}) - answer a tool approval pause.
@@ -30,7 +30,7 @@ In an application, parse the request first, then pass the host-selected chat, pa
30
30
 
31
31
  The mapper turns RubyLLM callbacks into AG-UI events. The emitter assigns each event a sequence, persists run state, closes open text or tool events, and writes one JSON event per `data:` line through the SSE writer.
32
32
 
33
- An assistant message opens on its first non-empty chunk and closes before a tool call, phase change, or terminal event. A tool call starts when its id and name are known, emits the arguments as received, and ends before the call runs. One run ends with one terminal result.
33
+ An assistant message opens on its first non-empty chunk and closes before a tool call, phase change, or terminal event. A tool call starts when its id and name are known and ends before the call runs. Its argument deltas concatenate to the JSON arguments the tool receives: streamed fragments go out unchanged, a call a provider sends whole is serialized once, and the empty object some providers open a call with is not sent. Calls from one response can be open at the same time, so assemble arguments by `toolCallId`. One run ends with one terminal result.
34
34
 
35
35
  ## Outcomes
36
36
 
@@ -55,6 +55,14 @@ failure cannot replace an outcome that is already persisted. `SSEWriter` used
55
55
  directly raises `TransportError`, a subclass of `StreamError`; `Run` handles
56
56
  that transport error. Mapping and persistence errors are separate failures.
57
57
 
58
+ ## Check the Client's Accept Header
59
+
60
+ `SSEWriter.accepts?` checks whether an `Accept` header permits an event stream.
61
+ An explicit `text/event-stream;q=0` refuses streaming even with `*/*;q=1`.
62
+ A specific media range takes precedence over its wildcard, following
63
+ [HTTP quality and media-range rules](https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.1).
64
+ The Rails adapter answers 406 before choosing a chat or reserving a run.
65
+
58
66
  ## Next Steps
59
67
 
60
68
  * [Validating Input]({% link _core_features/input-validation.md %}) - parse untrusted request data.
@@ -22,7 +22,7 @@ After reading this guide, you will know:
22
22
  {% if site.docs_unreleased %}bundle add ruby_llm-ag_ui --git https://github.com/yorzi/ruby_llm-ag-ui --branch main{% else %}bundle add ruby_llm-ag_ui --version {{ site.version }}{% endif %}
23
23
  ```
24
24
 
25
- The gem requires Ruby 3.2 or newer, RubyLLM 2.1 or newer, and `ag-ui-protocol` 0.2.x. The Rails adapter is optional. Provider credentials belong to your RubyLLM application; the parser example below needs none.
25
+ The gem requires Ruby 3.3 or newer, RubyLLM 2.1 or newer, and `ag-ui-protocol` 0.2.x. The Rails adapter is optional. Provider credentials belong to your RubyLLM application; the parser example below needs none.
26
26
 
27
27
  The runtime dependencies select patched `cgi`, `resolv`, `uri`, and `zlib` gems. An older Ruby installation's default library versions do not override those dependency requirements.
28
28
 
@@ -111,7 +111,8 @@ reason.
111
111
  the events to write. The mapper is a state machine: a text message opens on
112
112
  its first non-empty chunk and closes before a tool call, a phase change or the
113
113
  terminal event; a tool call opens when its id and name are both known, streams
114
- its argument deltas as received, and closes before it runs; one step per phase
114
+ argument deltas that concatenate to its JSON arguments, and closes before it
115
+ runs; one step per phase
115
116
  (`completion`, `tool`) so consecutive steps never overlap.
116
117
  6. **Write.** The emitter writes the events as SSE, advancing the run record's
117
118
  sequence by one per event.
@@ -0,0 +1,103 @@
1
+ ---
2
+ layout: default
3
+ title: "What's New in 1.0"
4
+ nav_order: -1
5
+ description: Install the stable 1.0 release, with streamed tool calls fixed across provider protocols, Ruby 3.3 or newer, and a rehearsed upgrade from 0.4.
6
+ ---
7
+
8
+ # {{ page.title }}
9
+
10
+ {{ page.description }}
11
+ {: .fs-6 .fw-300 }
12
+
13
+ After reading this guide, you will know:
14
+
15
+ * What the 1.0 stability promise covers.
16
+ * Which streamed tool-call fixes reach your client.
17
+ * What changed for Rails boots, malformed input, and long-lived chats.
18
+
19
+ ## Install 1.0
20
+
21
+ ```ruby
22
+ gem 'ruby_llm-ag_ui', '~> 1.0'
23
+ ```
24
+
25
+ Version 1.0.0 keeps the public API, configuration, and store contract that 0.4
26
+ proposed, with no new migration. From 1.0 on, that contract follows Semantic
27
+ Versioning: a change that breaks it waits for 2.0. See the
28
+ [Public API Contract]({% link _reference/public-api.md %}) for what it covers.
29
+ Report a problem with the versions from `RubyLLM::AgUI.config.to_h`, as
30
+ [Compatibility]({% link _reference/compatibility.md %}) describes.
31
+
32
+ ## Use Ruby 3.3 or Newer
33
+
34
+ Ruby 3.2 reached end of life in March 2026, and 1.0 requires Ruby 3.3 or newer.
35
+ CI tests Ruby 3.3, 3.4, and 4.0 with RubyLLM 2.1.0 and 2.1.1.
36
+
37
+ ## Stream Tool Calls Correctly Across Protocols
38
+
39
+ A client assembles a tool call's arguments by joining its `TOOL_CALL_ARGS`
40
+ deltas. Before this release, four provider shapes produced arguments the client
41
+ could not parse or attribute:
42
+
43
+ * A second tool round in one turn sent its arguments to the first round's ended
44
+ call. Chat Completions, OpenAI Responses, and Anthropic restart stream indexes
45
+ in every response.
46
+ * Anthropic and Bedrock open a call with an empty input object before streaming
47
+ partial JSON, so the client received `{}{"query":"one"}`.
48
+ * Gemini sends a whole call as a parsed object, which went out as Ruby
49
+ `Hash#to_s` output instead of JSON.
50
+ * Some OpenAI-compatible servers send each parallel call whole at index 0. The
51
+ second call's arguments were appended to the first call. RubyLLM starts a new
52
+ call for each fragment with a new id, and so does the mapper now.
53
+
54
+ Each call now opens once, and its deltas join to the JSON arguments the tool
55
+ received. Intercepted-HTTP specs run a real RubyLLM chat for Chat Completions,
56
+ Responses, Anthropic, and Gemini. Bedrock uses the same empty-object rule
57
+ without its own HTTP spec. See [Streaming Runs]({% link _core_features/streaming-runs.md %}).
58
+
59
+ ## Boot Rails Without Loading Active Record Early
60
+
61
+ Requiring the gem during `Bundler.require` no longer loads `ActiveRecord::Base`.
62
+ The durable store's row class loads on first use, after your application's
63
+ configuration. The generated initializer still assigns
64
+ `RubyLLM::AgUI::Rails::ActiveRecordStore.new` as before.
65
+
66
+ ## Refuse an Unacceptable Stream Before Provider Work
67
+
68
+ `SSEWriter.accepts?` honors quality values and media-range precedence. An
69
+ explicit `text/event-stream;q=0` refuses SSE even with `*/*;q=1`. Empty media
70
+ parameters do not raise a server error. Rails returns 406 before resolving a
71
+ chat or reserving a run. Stream conflicts also return 409 in Rails hosts that
72
+ do not load Active Record; unrelated host errors still propagate.
73
+
74
+ ## Refuse Malformed Text as a Client Error
75
+
76
+ ```ruby
77
+ RubyLLM::AgUI::Input.parse(body)
78
+ # => raises RubyLLM::AgUI::MalformedInputError (400)
79
+ ```
80
+
81
+ A body whose text is not UTF-8, or that holds a number JSON cannot represent,
82
+ is now refused as malformed input. The Rails adapter answers 400 instead of
83
+ raising `JSON::GeneratorError` as a server error.
84
+
85
+ ## Reuse a Chat Across Turns
86
+
87
+ RubyLLM cannot remove a callback, so each run's callbacks stay on a chat you
88
+ reuse. A finished run now releases its response stream, and its callbacks map
89
+ and write nothing. A redaction policy object that defines only `#call` also
90
+ works now, alongside lambdas and methods.
91
+
92
+ ## Upgrade From 0.4
93
+
94
+ Change the version in your Gemfile and run `bundle update ruby_llm-ag_ui`.
95
+ Records written by 0.4 stay readable. The release rehearsal reconnects to a 0.4
96
+ run and resumes a 0.4 interrupt with 1.0. Follow
97
+ [Upgrading]({% link _reference/upgrading.md %}) for the steps.
98
+
99
+ ## Next Steps
100
+
101
+ * [Compatibility]({% link _reference/compatibility.md %}) - read the 1.0 release evidence.
102
+ * [Public API Contract]({% link _reference/public-api.md %}) - review the API 1.0 stabilizes.
103
+ * [Release History]({% link _reference/release-history.md %}) - compare earlier releases.
@@ -23,11 +23,11 @@ dependency does.
23
23
 
24
24
  | Component | Range | Evidence |
25
25
  |---|---|---|
26
- | Ruby | `>= 3.2` | Recorded 0.4 CI passes on 3.2.11, 3.3.12, and 3.4.11 |
26
+ | Ruby | `>= 3.3` | Recorded 0.4 CI passes on 3.3.12 and 3.4.11; 4.0.9 passes locally, and CI now includes 4.0. Ruby 3.2 is no longer supported |
27
27
  | RubyLLM | `>= 2.1`, `< 3.0` | 2.1.0 and 2.1.1 pass the 0.4 CI matrix; the recorded live session uses 2.1.1 |
28
28
  | `ag-ui-protocol` | `~> 0.2.0` | 0.2.0 |
29
29
  | JSON | `>= 2.0`, `< 3` | 2.21.2 passes both Rails lines; JSON 3 removes the `quirks_mode` option used by Rails 8.0 |
30
- | Rails (adapter only) | `railties`, `actionpack`, `activerecord` `>= 8.0` | Recorded 0.4 CI passes on 8.0.5.1 and 8.1.4 with Ruby 3.3/3.4 |
30
+ | Rails (adapter only) | `railties`, `actionpack`, `activerecord` `>= 8.0` | Recorded 0.4 CI passes on 8.0.5.1 and 8.1.4 with Ruby 3.3/3.4; both pass locally on Ruby 4.0.9 |
31
31
  | AG-UI schema | pinned commit `89cedd3fd549feeee41e50e1d7c6d373e549241a` | vendored, SHA-256 recorded in [AG-UI Protocol Coverage]({% link _reference/protocol-coverage.md %}) |
32
32
 
33
33
  The pure-Ruby core uses `ruby_llm` and `ag-ui-protocol`, plus the patched
@@ -44,6 +44,8 @@ adapter is only loaded when `Rails::Railtie` is already defined.
44
44
  | The 31-type coverage map is the schema's own union | `spec/ruby_llm/ag_ui/coverage_spec.rb` compares the pinned list with `Coverage`, and the SDK enum at runtime |
45
45
  | The SDK's five legacy `THINKING_*` constants are never counted as 1.0 | the same spec asserts `LEGACY_SDK_ONLY` is absent from the map |
46
46
  | Text, tool and lifecycle sequences are ordered and closed | mapper specs, including partial arguments, a nil chunk, a tool that raises, and a thinking-only chunk |
47
+ | Streamed tool arguments join to valid JSON for each call | `spec/integration/streamed_tool_calls_spec.rb` runs a real RubyLLM chat over intercepted Chat Completions, Responses, Anthropic, and Gemini streams, including two tool rounds in one turn |
48
+ | A Rails boot does not load Active Record through the gem | `spec/integration/core_load_spec.rb` requires the gem after the Active Record railtie in a subprocess |
47
49
  | One outcome per run, and the outcome matches what happened | run specs: success, interrupt, cancelled, sanitized failure, unsupported pause |
48
50
  | `requires_approval` → interrupt → resume executes the tool once | run specs drive a fake chat through the pause, approve, deny, replay and expiry cases |
49
51
  | Identity is reserved, replayed and never re-executed | store specs (memory and ActiveRecord), run specs for conflict, reconnect and live-run refusal |
@@ -54,6 +56,120 @@ adapter is only loaded when `Rails::Railtie` is already defined.
54
56
 
55
57
  The RSpec suite runs offline with fakes and no provider credentials. Package verification uses the network only to install dependencies, then runs its core and Rails checks without a provider. Its advisory check includes the actual default-library gem versions activated by the isolated core install.
56
58
 
59
+ ## 1.0.0 Release Evidence
60
+
61
+ Version 1.0.0 was rehearsed as the local, unpublished candidate 1.0.0.rc.1 and
62
+ published after the maintainer's acceptance. The dated reviews below record that
63
+ candidate. The 2026-10-21 evidence records the earlier rehearsal.
64
+
65
+ ### Release Verification on 2026-11-02
66
+
67
+ The 1.0.0 release commit passes 321 offline examples with zero failures and two
68
+ opt-in live checks skipped on Ruby 3.3.5, and lint passes on 100 files. The
69
+ browser suite passes 18 workflows with all 242 consumed events schema-valid. The
70
+ built gem passes both installed endpoints, clean core and Rails installation with
71
+ the generator migrated up and down, and the activated-dependency advisory check.
72
+ The upgrade from the published 0.4.0 artifact keeps the generated files,
73
+ reconnects, runs an approval once, and denies with no effect.
74
+
75
+ The OpenRouter rerun passes live streamed text and approval with reconnect, plus
76
+ the offline and intercepted harness cases: six examples, zero failures.
77
+
78
+ The development bundle audit uses 1,307 advisories at
79
+ `79c0e052ade15d29d3ecc1e8d4fa59c337a86383`. Advisories published on 2026-11-01
80
+ affected `addressable`, `rack`, and `sqlite3` in the development and test bundle
81
+ only; after updating them to 2.9.2, 3.2.8, and 2.10.2 the audit reports no
82
+ findings. None is a runtime dependency of the gem. The npm audit reports zero
83
+ findings.
84
+
85
+ ### Candidate Review on 2026-11-02
86
+
87
+ A review of every change since 0.4.0 found one more streamed tool-call defect.
88
+ RubyLLM starts a new call for each fragment that carries an id, but the mapper
89
+ continued the call already at that stream index. An OpenAI-compatible server
90
+ that sends each parallel call whole at index 0 had the second call's arguments
91
+ appended to the first. A real-chat intercepted-HTTP spec fails before the fix.
92
+
93
+ The offline suite passes 321 examples with zero failures and two opt-in live
94
+ checks skipped on Ruby 3.3.5, and lint passes on 100 files. Ruby 4.0.9 passes the
95
+ same 321 examples twice: with RubyLLM 2.1.1 and Rails 8.1.4, and with RubyLLM
96
+ 2.1.0 and Rails 8.0.5.1. Malformed-input refusal holds on JSON 2.7.1, 2.9.1, and
97
+ 2.21.2. CI adds Ruby 4.0 to the core and Rails matrices; its remote results are
98
+ separate evidence.
99
+
100
+ The fix changes a packaged file, so the 2026-10-26 artifact below is superseded.
101
+ The rebuilt candidate passes the installed-host checks for both endpoints and the
102
+ generated RubyLLM models. Its upgrade from the published 0.4.0 artifact keeps the
103
+ generated files, reconnects, runs an approval once, and denies with no effect.
104
+ The browser suite passes 18 workflows with all 242 consumed events schema-valid;
105
+ desktop approval progress and 390px mobile denial screenshots were inspected.
106
+
107
+ ### Final Local Review on 2026-10-26
108
+
109
+ The offline suite passes 320 examples with zero failures and two opt-in live
110
+ checks skipped, on Ruby 3.3.5 with RubyLLM 2.1.1 and Rails 8.1.4. Line coverage
111
+ is 97.58 percent; this measures executed lines rather than complete behavior.
112
+ The same 320 examples pass separately on the minimum RubyLLM 2.1.0.
113
+ Lint passes on 100 files. Added regressions cover SSE quality and wildcard
114
+ precedence, empty media parameters, and actual Rails requests without Active
115
+ Record. Known missing-record errors still return 404; unrelated host errors
116
+ propagate. The focused tests reproduce the defects before their fixes.
117
+
118
+ The client still uses `@ag-ui/client` 1.0.3. After overriding Zod to 4.6.6,
119
+ 18 browser workflows pass with all 242 consumed events schema-valid. Desktop
120
+ approval progress and 390px mobile denial screenshots were inspected. The
121
+ 2026-10-26 npm audit reports zero findings. Before the override, the audit
122
+ reported four high findings from one transitive Zod advisory. This is a
123
+ development bundle, not a JavaScript dependency shipped with the Ruby gem.
124
+
125
+ The Ruby audit uses 1,269 advisories at
126
+ `03da937a9e171e3fedebde6620e203559b7a60c0` and reports no findings after updating
127
+ the local Rails dependency `crass` from 1.0.7 to 1.0.8. Existing applications
128
+ need their own lockfile audit; installing this gem does not audit a host's
129
+ Rails dependencies. CI now audits both the Ruby development bundle and npm.
130
+
131
+ `bin/verify-upgrade` preserves this rehearsal as a repeatable command. It
132
+ installs the published 0.4.0 artifact and the built candidate, then uses separate
133
+ processes against one temporary SQLite database. Generated files are unchanged;
134
+ 0.4 completed outcomes reconnect, old approvals execute once, denial executes
135
+ no effect, completed retries do not execute again, and scope isolation holds.
136
+ Its scripted chat and existing development dependencies are separate evidence
137
+ from clean installation and real-provider tests.
138
+
139
+ The bounded OpenRouter rerun passes both live text and approval/resume checks
140
+ with `openai/gpt-4.1-mini`, plus one offline harness case. No live browser,
141
+ Rails/provider, denial, cancellation-timing, or additional-provider claim is made.
142
+
143
+ ### Earlier Local Rehearsal on 2026-10-21
144
+
145
+ On 2026-10-21, the candidate passes 313 offline examples with zero failures
146
+ and two opt-in provider skips on Ruby 3.3.5, RubyLLM 2.1.1, and Rails 8.1.4.
147
+ Lint passes on 97 files. The browser suite passes 18 workflows with
148
+ `@ag-ui/client` 1.0.3 and validates all 242 consumed events. Desktop approval
149
+ and 390px mobile denial screenshots were inspected.
150
+
151
+ The built artifact passes both installed endpoints, the generated RubyLLM
152
+ models, a clean gem home with the activated-dependency audit, and Rails
153
+ generator migration up and down. The root advisory audit reports no findings.
154
+
155
+ An upgrade rehearsal installs the published 0.4.0 gem in an isolated gem home,
156
+ generates and migrates its table in SQLite, and records a succeeded run and an
157
+ interrupted run. The candidate artifact then reconnects to the 0.4 run without
158
+ executing it, resumes the 0.4 interrupt with the original call id, runs the
159
+ tool once, and reconnects without running it again. Its generated migration
160
+ and initializer match 0.4.0.
161
+
162
+ The rerun OpenRouter session uses `openai/gpt-4.1-mini` with RubyLLM 2.1.1.
163
+ Both live examples pass: streamed text, and the synthetic approval/resume with
164
+ reconnect. The same command runs four offline and intercepted harness examples,
165
+ for six examples and zero failures.
166
+
167
+ The audit for this candidate found streamed tool-call defects that the 0.4
168
+ evidence did not exercise: a second tool round in one turn, Anthropic and
169
+ Bedrock empty opening input, and Gemini whole-call arguments. Each defect has a
170
+ real-chat intercepted-HTTP spec that fails before its fix. Remote CI is separate
171
+ from that local record. No RC tag or publication is planned.
172
+
57
173
  ## 0.4 Release Acceptance
58
174
 
59
175
  The [2026-10-21 acceptance CI](https://github.com/yorzi/ruby_llm-ag-ui/actions/runs/39518735488)
@@ -71,6 +187,23 @@ The browser suite again passes 18 workflows and validates all 242 consumed
71
187
  events. Desktop approval progress and 390px mobile denial screenshots were
72
188
  inspected. The earlier candidate record below retains its own commit and counts.
73
189
 
190
+ ### Published 0.4.0 Artifact
191
+
192
+ The [tag-source CI](https://github.com/yorzi/ruby_llm-ag-ui/actions/runs/39518971268)
193
+ passes all 17 jobs at `23d9020`. The
194
+ [0.4.0 release workflow](https://github.com/yorzi/ruby_llm-ag-ui/actions/runs/39519175592)
195
+ passes its offline, browser, advisory, installed-host, and clean-install checks.
196
+ The actual GitHub and RubyGems downloads have matching SHA-256 hashes, including
197
+ the release checksum and RubyGems metadata. All 66 packaged files match the tag.
198
+ The downloaded artifact independently passes both installed endpoints and fresh
199
+ core, activated-dependency audit, and Rails generator migration up/down checks.
200
+
201
+ The [stable documentation deployment](https://github.com/yorzi/ruby_llm-ag-ui/actions/runs/39519565224)
202
+ passes. HTTP checks verify the stable guides, generated API, release history,
203
+ and stable/Next installation commands and `llms.txt`. A fresh browser session
204
+ shows 0.4.0 and switches to Next through the version menu; its screenshot was
205
+ inspected. Publication checks do not expand the provider or database claims below.
206
+
74
207
  ## Local 0.4 Development Verification
75
208
 
76
209
  On 2026-10-21, the candidate passes 294 offline examples with zero failures
@@ -195,8 +328,10 @@ checks after download.
195
328
  recorded `@ag-ui/client` version have not been verified. Schema conformance
196
329
  does not establish that every UI renders the events correctly.
197
330
  - **Other live paths.** Only the recorded OpenRouter model's text and approval
198
- workflow is verified live. Browser provider sessions, other providers,
199
- cancellation timing, timeouts, and network-loss recovery need separate evidence.
331
+ workflow is verified live. Anthropic, Gemini, and OpenAI Responses streams are
332
+ checked with intercepted HTTP only. Browser provider sessions, other
333
+ providers, cancellation timing, timeouts, and network-loss recovery need
334
+ separate evidence.
200
335
  - **Every RubyLLM patch release in the `>= 2.1, < 3.0` range.** The upper bound
201
336
  is a dependency resolver bound; verified versions are 2.1.0 and 2.1.1. Future
202
337
  2.x releases need the same checks before a compatibility claim.
@@ -61,7 +61,7 @@ Legend:
61
61
  | `TEXT_MESSAGE_CONTENT` | text | One delta per non-empty `chunk.content`, in order. |
62
62
  | `TEXT_MESSAGE_END` | text | Closed before a tool call, a phase change or the terminal event. |
63
63
  | `TOOL_CALL_START` | tool | Emitted when a call is observed before it runs. |
64
- | `TOOL_CALL_ARGS` | tool | Arguments as received; never re-synthesized. |
64
+ | `TOOL_CALL_ARGS` | tool | Deltas that concatenate to the call's JSON arguments; streamed fragments unchanged. |
65
65
  | `TOOL_CALL_END` | tool | Closed before the call runs or the run ends. |
66
66
  | `TOOL_CALL_RESULT` | tool | The tool result, after the host redaction policy. |
67
67
  | `STATE_SNAPSHOT` | state | On a reconnect: the state the host gave the run. |
@@ -2,7 +2,7 @@
2
2
  layout: default
3
3
  title: Public API Contract
4
4
  nav_order: 3
5
- description: Build against the application API and custom-store contract proposed for 1.0.
5
+ description: Build against the application API and custom-store contract that 1.0 stabilizes.
6
6
  ---
7
7
 
8
8
  # {{ page.title }}
@@ -32,8 +32,9 @@ returns its stored outcome on an exact retry; it never repeats the turn.
32
32
  An active duplicate is refused. See
33
33
  [Reconnects]({% link _core_features/reconnects.md %}).
34
34
 
35
- Version 0.4 proposes the following application contract for 1.0. It is not a
36
- 1.0 stability promise yet. Breaking changes before 1.0 have upgrade steps.
35
+ Version 1.0.0 makes the following application contract a stability promise.
36
+ It follows Semantic Versioning: a change that breaks it waits for 2.0 and ships
37
+ with upgrade steps.
37
38
 
38
39
  | Entry point | Application contract |
39
40
  | --- | --- |
@@ -17,7 +17,8 @@ description: Review RubyLLM AG-UI releases, newest first, with highlights and up
17
17
 
18
18
  | Release | Date | Highlights |
19
19
  | --- | --- | --- |
20
- | [0.4.0](https://github.com/yorzi/ruby_llm-ag-ui/releases/tag/v0.4.0) | 2026-10-21 | [API and compatibility]({% link _getting_started/whats-new-in-0-4.md %}): effective configuration, immutable records, and real-chat approval verification |
20
+ | [1.0.0](https://github.com/yorzi/ruby_llm-ag-ui/releases/tag/v1.0.0) | 2026-11-02 | [Stable integration contract]({% link _getting_started/whats-new-in-1-0.md %}): streamed tool calls fixed across provider protocols, Ruby 3.3 or newer, and a rehearsed upgrade from 0.4 |
21
+ | [0.4.0](https://github.com/yorzi/ruby_llm-ag-ui/releases/tag/v0.4.0) | 2026-10-21 | [API and compatibility]({% link _reference/whats-new-in-0-4.md %}): effective configuration, immutable records, and real-chat approval verification |
21
22
  | [0.3.0](https://github.com/yorzi/ruby_llm-ag-ui/releases/tag/v0.3.0) | 2026-10-20 | [Lifecycle and worker recovery]({% link _reference/whats-new-in-0-3.md %}): disconnect handling, scoped cancellation, and conditional durable writes |
22
23
  | [0.2.0](https://github.com/yorzi/ruby_llm-ag-ui/releases/tag/v0.2.0) | 2026-10-19 | [Client workflows and tool progress]({% link _reference/whats-new-in-0-2.md %}): opt-in redacted progress, a real-client demo, and completed resume reconnect |
23
24
  | [0.1.0](https://github.com/yorzi/ruby_llm-ag-ui/releases/tag/v0.1.0) | 2026-10-18 | [First release]({% link _reference/whats-new-in-0-1.md %}): AG-UI event mapping, approval and resume, cancellation, reconnect, and the optional Rails adapter |
@@ -40,6 +40,11 @@ so local and CI builds use the same navigation and parser.
40
40
 
41
41
  ## Review Release Scope
42
42
 
43
+ Keep `released: false` on `main` between releases. For a release that changes
44
+ the public contract or its upgrade path, build the artifact, run
45
+ `bundle exec ruby bin/verify-upgrade` with the previous published gem, and
46
+ install it in a real application before you tag.
47
+
43
48
  Use the repository's [roadmap](https://github.com/yorzi/ruby_llm-ag-ui/blob/main/ROADMAP.md)
44
49
  for proposed iterations and the 1.0 release conditions. Guides describe available
45
50
  behavior; the roadmap describes planned work. Check the release's conditions
@@ -15,6 +15,33 @@ After reading this guide, you will know:
15
15
  * How to move between versioned releases.
16
16
  * Where to find changes that need code updates.
17
17
 
18
+ ## Move from 0.4 to 1.0
19
+
20
+ Update your Gemfile and dependency:
21
+
22
+ ```ruby
23
+ gem 'ruby_llm-ag_ui', '~> 1.0'
24
+ ```
25
+
26
+ ```sh
27
+ bundle update ruby_llm-ag_ui
28
+ ```
29
+
30
+ 1.0 keeps the 0.4 public API, configuration, and store
31
+ contract. It needs no SQL migration, and the generated migration and
32
+ initializer are unchanged. Records written by 0.4 stay readable: an exact retry
33
+ reconnects to a 0.4 run, and a 0.4 interrupt can be resumed after the upgrade.
34
+
35
+ Clients now receive tool argument deltas that join to valid JSON for every
36
+ streamed call. If your client worked around the old `{}` prefix or Ruby
37
+ `Hash#to_s` arguments, remove that workaround.
38
+
39
+ Requiring the gem no longer loads `ActiveRecord::Base` during a Rails boot.
40
+ Code that relied on the gem to load Active Record must load it itself.
41
+
42
+ 1.0 requires Ruby 3.3 or newer. Upgrade an application on Ruby 3.2 before you
43
+ update the gem; Bundler refuses to install 1.0 on Ruby 3.2.
44
+
18
45
  ## Move from 0.3 to 0.4
19
46
 
20
47
  Update your Gemfile and dependency:
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  layout: default
3
3
  title: "What's New in 0.4"
4
- nav_order: -1
4
+ nav_order: 40
5
+ parent: Release History
5
6
  description: Review the public integration contract and verified compatibility boundaries.
6
7
  ---
7
8
 
@@ -96,7 +96,8 @@ module RubyLLM
96
96
  :custom
97
97
  end
98
98
 
99
- # The run store, or nil when the host has not configured one.
99
+ # The configured run store. Raises ConfigurationError when the host has
100
+ # not configured one.
100
101
  def run_store!
101
102
  @run_store || raise(ConfigurationError, 'No run_store is configured. ' \
102
103
  'Set RubyLLM::AgUI.config.run_store or pass store: to Run.new.')
@@ -99,8 +99,8 @@ module RubyLLM
99
99
  @tools.filter_map { |tool| tool['name'] }
100
100
  end
101
101
 
102
- # Whether the client supplied tool definitions. The first release never
103
- # grants them; a run reports them and stops.
102
+ # Whether the client supplied tool definitions. Informational only: the
103
+ # gem never registers a client tool on the host's chat.
104
104
  def client_tools?
105
105
  !@tools.empty?
106
106
  end
@@ -133,10 +133,15 @@ module RubyLLM
133
133
  @parts = PartBuilder.new
134
134
  end
135
135
 
136
+ # The digest re-encodes the accepted values, so text that is not UTF-8
137
+ # or a number that overflowed to Infinity is refused here as malformed
138
+ # input rather than escaping as an encoder error.
136
139
  def parse
137
140
  document = strip_unknown_members(decode(read_body))
138
141
  validate_common(document)
139
142
  build(document)
143
+ rescue JSON::GeneratorError
144
+ raise MalformedInputError, 'The request body must be valid UTF-8 JSON.'
140
145
  end
141
146
 
142
147
  private
@@ -47,12 +47,12 @@ module RubyLLM
47
47
  @run_id = run_id
48
48
  @redact = redact || RubyLLM::AgUI.config.redact
49
49
  @logger = logger || RubyLLM::AgUI.config.logger
50
- @message_count = 0
51
- @step_count = 0
50
+ @message_count = @step_count = 0
52
51
  @open_text_id = nil
53
52
  @last_message_id = nil
54
53
  @text_emitted = false
55
54
  @calls = {}
55
+ end_stream_keys
56
56
  @completed_tools = {}
57
57
  @open_step = nil
58
58
  @step_kind = nil
@@ -182,6 +182,7 @@ module RubyLLM
182
182
  events.concat(close_step)
183
183
  @last_message_id = nil
184
184
  @text_emitted = false
185
+ end_stream_keys
185
186
  events
186
187
  end
187
188
 
@@ -200,21 +201,47 @@ module RubyLLM
200
201
 
201
202
  # The record for one tool call, created on first sight.
202
203
  #
203
- # A streamed delta is keyed by index and carries the id only on its first
204
- # fragment; the complete message is keyed by id. Both keys point at the
205
- # same record, so the call is opened once.
204
+ # A streamed delta is keyed by its provider stream index and carries the
205
+ # id only on its first fragment; the complete message is keyed by id. Both
206
+ # keys point at the same record, so the call is opened once. Providers
207
+ # restart stream indexes in every response, so an index names a call only
208
+ # until its completion ends. An unkeyed fragment continues the latest
209
+ # streamed call, and a fragment with a different id starts a new call
210
+ # under the same key, as RubyLLM's own accumulator does.
206
211
  def track(key, tool_call, streamed:)
207
- record = @calls[key] || find_call(tool_call.id) || new_call
212
+ record = lookup_call(key, tool_call, streamed: streamed)
208
213
  record[:id] ||= tool_call.id
209
214
  record[:name] ||= tool_call.name
210
- record[:streamed] ||= streamed
211
- @calls[key] = record
215
+ remember_stream_call(key, record) if streamed
212
216
  @calls[record[:id]] = record if record[:id]
213
217
  record
214
218
  end
215
219
 
220
+ def lookup_call(key, tool_call, streamed:)
221
+ record = stream_call(key) if streamed
222
+ return record if record && continues?(record, tool_call)
223
+
224
+ find_call(tool_call.id) || new_call
225
+ end
226
+
227
+ def continues?(record, tool_call)
228
+ tool_call.id.nil? || record[:id].nil? || record[:id] == tool_call.id
229
+ end
230
+
231
+ def stream_call(key) = key.nil? ? @latest_stream_call : @stream_calls[key]
232
+
233
+ def remember_stream_call(key, record)
234
+ @stream_calls[key] = record unless key.nil?
235
+ @latest_stream_call = record
236
+ end
237
+
238
+ def end_stream_keys
239
+ @stream_calls = {}
240
+ @latest_stream_call = nil
241
+ end
242
+
216
243
  def new_call
217
- { id: nil, name: nil, streamed: false, args_emitted: false, started: false, closed: false, buffered: nil }
244
+ { id: nil, name: nil, args_emitted: false, started: false, closed: false, buffered: nil }
218
245
  end
219
246
 
220
247
  def find_call(id)
@@ -260,19 +287,29 @@ module RubyLLM
260
287
  # A streamed chunk carries a fragment of the provider's own argument bytes
261
288
  # and goes out unchanged. A complete message carries the Hash RubyLLM
262
289
  # reconstructed from those bytes, so it is serialized once and only when
263
- # the stream did not already deliver the fragments.
290
+ # the stream delivered no argument bytes for the call.
264
291
  def argument_delta(record, tool_call, streamed:)
265
- return streamed_delta(tool_call) if streamed
266
- return nil if record[:streamed] || record[:args_emitted]
292
+ delta = streamed ? streamed_delta(tool_call) : complete_delta(record, tool_call)
293
+ record[:args_emitted] = true unless delta.nil? || delta.empty?
294
+ delta
295
+ end
267
296
 
268
- record[:args_emitted] = true
297
+ # Like RubyLLM's stream accumulator, an empty value carries no bytes:
298
+ # Anthropic and Bedrock open a call with an empty input object before
299
+ # streaming its partial JSON. Gemini streams a whole call as a parsed
300
+ # object, which is serialized as JSON.
301
+ def streamed_delta(tool_call)
269
302
  arguments = tool_call.arguments
303
+ return nil if arguments.nil? || (arguments.respond_to?(:empty?) && arguments.empty?)
304
+
270
305
  arguments.is_a?(String) ? arguments : Canonical.dump(arguments)
271
306
  end
272
307
 
273
- def streamed_delta(tool_call)
308
+ def complete_delta(record, tool_call)
309
+ return nil if record[:args_emitted]
310
+
274
311
  arguments = tool_call.arguments
275
- arguments.is_a?(String) ? arguments : arguments.to_s
312
+ arguments.is_a?(String) ? arguments : Canonical.dump(arguments)
276
313
  end
277
314
 
278
315
  def close_tool_calls
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'record'
4
-
5
3
  module RubyLLM
6
4
  module AgUI
7
5
  module Rails
6
+ # Defining the row class loads ActiveRecord::Base. Defer it to first use,
7
+ # so requiring the gem during a Rails boot does not load Active Record
8
+ # before the host's configuration.
9
+ autoload :Record, File.expand_path('record', __dir__)
10
+
8
11
  # The durable run store: run identity in a table instead of a process.
9
12
  #
10
13
  # The in-process store is correct for one process only. A host that runs
@@ -43,8 +46,7 @@ module RubyLLM
43
46
  end
44
47
 
45
48
  def latest_interrupted(scope:, thread_id:)
46
- interrupted = RunRecord::INTERRUPTED.to_s
47
- thread_rows(scope, thread_id).reverse.find { |row| row.status == interrupted }&.as_run_record
49
+ interrupted_rows(scope, thread_id).last&.as_run_record
48
50
  end
49
51
 
50
52
  def save(record)
@@ -116,12 +118,19 @@ module RubyLLM
116
118
  Record.where(scope: scope, thread_id: thread_id).order(:id).to_a
117
119
  end
118
120
 
119
- # An interrupt id is unique among a thread's runs, so the thread is
120
- # searched newest first and the first row that knows the id wins. A
121
- # thread holds few runs, and the alternative is a JSON query that would
122
- # bind this store to one database.
121
+ # Only an interrupted run carries interrupts, and it keeps that status
122
+ # after they are answered. The status index finds those rows without
123
+ # loading every run on the thread.
124
+ def interrupted_rows(scope, thread_id)
125
+ Record.where(scope: scope, thread_id: thread_id, status: RunRecord::INTERRUPTED.to_s).order(:id).to_a
126
+ end
127
+
128
+ # An interrupt id is unique among a thread's runs, so the interrupted
129
+ # runs are searched newest first and the first row that knows the id
130
+ # wins. The alternative is a JSON query that would bind this store to
131
+ # one database.
123
132
  def row_for_interrupt(scope, thread_id, interrupt_id)
124
- thread_rows(scope, thread_id).reverse.find { |row| row.as_run_record.interrupt(interrupt_id) }
133
+ interrupted_rows(scope, thread_id).reverse.find { |row| row.as_run_record.interrupt(interrupt_id) }
125
134
  end
126
135
  end
127
136
  end
@@ -66,10 +66,10 @@ module RubyLLM
66
66
  # identity claimed with different input or a resume that answers
67
67
  # nothing, 413 for a body past the configured bound.
68
68
  reject_ag_ui_request(e.http_status, e.message)
69
- rescue ::ActiveRecord::RecordNotFound
70
- reject_ag_ui_request(NOT_FOUND, 'There is no such thread.')
71
69
  rescue ::RubyLLM::AgUI::StreamError => e
72
70
  reject_ag_ui_request(CONFLICT, e.message)
71
+ rescue StandardError => e
72
+ reject_ag_ui_host_error(e)
73
73
  end
74
74
 
75
75
  # The tenant/actor key every reservation is bounded by.
@@ -89,6 +89,12 @@ module RubyLLM
89
89
 
90
90
  private
91
91
 
92
+ def reject_ag_ui_host_error(error)
93
+ raise error unless defined?(::ActiveRecord::RecordNotFound) && error.is_a?(::ActiveRecord::RecordNotFound)
94
+
95
+ reject_ag_ui_request(NOT_FOUND, 'There is no such thread.')
96
+ end
97
+
92
98
  # Whether this client can read an event stream. An AG-UI client asks for
93
99
  # `text/event-stream`; a request that asks for something else is told so
94
100
  # instead of being streamed at.
@@ -51,8 +51,10 @@ module RubyLLM
51
51
 
52
52
  # Calls +callable+ with the arguments its arity takes, so a host can write
53
53
  # either `->(kind, payload) { ... }` or the terse `->(payload) { ... }`.
54
+ # A policy object that only defines #call is read the same way.
54
55
  def apply(callable, kind, payload)
55
- callable.arity == 1 ? callable.call(payload) : callable.call(kind, payload)
56
+ arity = callable.respond_to?(:arity) ? callable.arity : callable.method(:call).arity
57
+ arity == 1 ? callable.call(payload) : callable.call(kind, payload)
56
58
  end
57
59
  end
58
60
  end
@@ -97,7 +97,7 @@ module RubyLLM
97
97
  rescue StandardError => e
98
98
  @record = finalize { @emitter.fail(e) }
99
99
  ensure
100
- finalize { @mapper&.close }
100
+ finalize { release }
101
101
  end
102
102
 
103
103
  def start_execution(record, to)
@@ -132,8 +132,15 @@ module RubyLLM
132
132
 
133
133
  def finalize(&) = @mutex.synchronize(&)
134
134
 
135
- def project
136
- @mutex.synchronize { @emitter.write(yield) }
135
+ # After release, a stale callback maps and writes nothing.
136
+ def project = @mutex.synchronize { @emitter&.write(yield) }
137
+
138
+ # The callbacks this run registered stay on the chat after it ends, and
139
+ # RubyLLM cannot remove them. The closed mapper maps nothing further, and
140
+ # dropping the emitter lets the response stream it writes to be freed.
141
+ def release
142
+ @mapper&.close
143
+ @emitter = nil
137
144
  end
138
145
 
139
146
  # A run that stopped for a human is an interrupt, never a success. Any
@@ -178,10 +185,7 @@ module RubyLLM
178
185
  raise ConfigurationError, 'chat must be a RubyLLM::Chat or act like one (it does not respond to #complete)'
179
186
  end
180
187
  raise ConfigurationError, 'input must be a RubyLLM::AgUI::Input' unless @input.is_a?(Input)
181
-
182
- return if @scope.is_a?(String) && !@scope.empty?
183
-
184
- raise ConfigurationError, Store::Base::SCOPE_MESSAGE
188
+ raise ConfigurationError, Store::Base::SCOPE_MESSAGE unless @scope.is_a?(String) && !@scope.empty?
185
189
  end
186
190
  end
187
191
  end
@@ -48,14 +48,31 @@ module RubyLLM
48
48
  #
49
49
  # A host calls this before opening the stream. An explicit header that
50
50
  # cannot accept `text/event-stream` is a client that will not understand
51
- # the response, so the host should answer 406 instead of streaming.
51
+ # the response, so the host should answer 406 instead of streaming. A zero
52
+ # quality refuses that range; a more specific range overrides a wildcard.
52
53
  def self.accepts?(accept)
53
54
  return true if accept.nil? || accept.strip.empty?
54
55
 
55
- accept.split(',').any? do |range|
56
- %w[text/event-stream text/* */*].include?(range.split(';').first.to_s.strip.downcase)
56
+ matches = accept.split(',').filter_map do |range|
57
+ media_type, *parameters = range.downcase.split(';').map(&:strip)
58
+ specificity = %w[*/* text/* text/event-stream].index(media_type)
59
+ next if specificity.nil?
60
+
61
+ [specificity, accept_quality(parameters)]
57
62
  end
63
+ return false if matches.empty?
64
+
65
+ matches.max.last.positive?
66
+ end
67
+
68
+ def self.accept_quality(parameters)
69
+ quality = parameters.find { |parameter| parameter.split('=', 2).first.to_s.strip == 'q' }
70
+ return 1.0 unless quality
71
+
72
+ value = quality.split('=', 2).last.to_s.strip
73
+ value.match?(/\A(?:0(?:\.\d{0,3})?|1(?:\.0{0,3})?)\z/) ? value.to_f : 0.0
58
74
  end
75
+ private_class_method :accept_quality
59
76
 
60
77
  # The Content-Type for the response the host is about to open.
61
78
  def content_type
@@ -3,6 +3,6 @@
3
3
  module RubyLLM
4
4
  module AgUI
5
5
  # The version of the ruby_llm-ag_ui gem, as a string.
6
- VERSION = '0.4.0'
6
+ VERSION = '1.0.0'
7
7
  end
8
8
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm-ag_ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 1.0.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-21 00:00:00.000000000 Z
11
+ date: 2026-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ag-ui-protocol
@@ -176,7 +176,7 @@ files:
176
176
  - docs/_getting_started/configuration.md
177
177
  - docs/_getting_started/getting-started.md
178
178
  - docs/_getting_started/overview.md
179
- - docs/_getting_started/whats-new-in-0-4.md
179
+ - docs/_getting_started/whats-new-in-1-0.md
180
180
  - docs/_reference/api-reference.md
181
181
  - docs/_reference/compatibility.md
182
182
  - docs/_reference/protocol-coverage.md
@@ -188,6 +188,7 @@ files:
188
188
  - docs/_reference/whats-new-in-0-1.md
189
189
  - docs/_reference/whats-new-in-0-2.md
190
190
  - docs/_reference/whats-new-in-0-3.md
191
+ - docs/_reference/whats-new-in-0-4.md
191
192
  - docs/index.md
192
193
  - lib/generators/ruby_llm/ag_ui/install_generator.rb
193
194
  - lib/generators/ruby_llm/ag_ui/templates/create_ruby_llm_ag_ui_runs.rb.tt
@@ -240,7 +241,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
240
241
  requirements:
241
242
  - - ">="
242
243
  - !ruby/object:Gem::Version
243
- version: 3.2.0
244
+ version: 3.3.0
244
245
  required_rubygems_version: !ruby/object:Gem::Requirement
245
246
  requirements:
246
247
  - - ">="