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 +4 -4
- data/CHANGELOG.md +43 -0
- data/CONTRIBUTING.md +4 -0
- data/README.md +2 -2
- data/docs/_advanced/client-example.md +7 -0
- data/docs/_advanced/rails-integration.md +9 -1
- data/docs/_core_features/streaming-runs.md +9 -1
- data/docs/_getting_started/getting-started.md +1 -1
- data/docs/_getting_started/overview.md +2 -1
- data/docs/_getting_started/whats-new-in-1-0.md +103 -0
- data/docs/_reference/compatibility.md +139 -4
- data/docs/_reference/protocol-coverage.md +1 -1
- data/docs/_reference/public-api.md +4 -3
- data/docs/_reference/release-history.md +2 -1
- data/docs/_reference/releasing.md +5 -0
- data/docs/_reference/upgrading.md +27 -0
- data/docs/{_getting_started → _reference}/whats-new-in-0-4.md +2 -1
- data/lib/ruby_llm/ag_ui/configuration.rb +2 -1
- data/lib/ruby_llm/ag_ui/input.rb +7 -2
- data/lib/ruby_llm/ag_ui/mapper.rb +52 -15
- data/lib/ruby_llm/ag_ui/rails/active_record_store.rb +18 -9
- data/lib/ruby_llm/ag_ui/rails/streaming.rb +8 -2
- data/lib/ruby_llm/ag_ui/redaction.rb +3 -1
- data/lib/ruby_llm/ag_ui/run.rb +11 -7
- data/lib/ruby_llm/ag_ui/sse_writer.rb +20 -3
- data/lib/ruby_llm/ag_ui/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: faaf47d4aefadd16b5b9f9ee3a069efca39ada7c5e6935306879d82fcbe6094f
|
|
4
|
+
data.tar.gz: 2d81db9c7f7429a33391eb485d791160b5cf363b3313a0c3e65807a72b85fb16
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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', '~>
|
|
22
|
+
gem 'ruby_llm-ag_ui', '~> 1.0'
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
Requires Ruby >= 3.
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
199
|
-
|
|
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 |
|
|
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
|
|
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.
|
|
36
|
-
|
|
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.
|
|
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:
|
|
@@ -96,7 +96,8 @@ module RubyLLM
|
|
|
96
96
|
:custom
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
-
# The run store
|
|
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.')
|
data/lib/ruby_llm/ag_ui/input.rb
CHANGED
|
@@ -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.
|
|
103
|
-
#
|
|
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
|
|
204
|
-
# fragment; the complete message is keyed by id. Both
|
|
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 =
|
|
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
|
|
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,
|
|
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
|
|
290
|
+
# the stream delivered no argument bytes for the call.
|
|
264
291
|
def argument_delta(record, tool_call, streamed:)
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
|
|
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
|
-
|
|
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
|
|
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
|
data/lib/ruby_llm/ag_ui/run.rb
CHANGED
|
@@ -97,7 +97,7 @@ module RubyLLM
|
|
|
97
97
|
rescue StandardError => e
|
|
98
98
|
@record = finalize { @emitter.fail(e) }
|
|
99
99
|
ensure
|
|
100
|
-
finalize {
|
|
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
|
-
|
|
136
|
-
|
|
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(',').
|
|
56
|
-
|
|
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
|
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
|
+
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-
|
|
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
|
|
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.
|
|
244
|
+
version: 3.3.0
|
|
244
245
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
245
246
|
requirements:
|
|
246
247
|
- - ">="
|