ruby_llm-ag_ui 0.3.0 → 0.4.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: fd1d89205c88b89562445e5f654f2f2f931753ac4ff4e2ed4434031ad65251d6
4
- data.tar.gz: ec70b5e27313a50c8596beaa33c2de9f3b8fc46fab5968b309cc8f626b4a70a0
3
+ metadata.gz: 7e2fef2885431f82e496f0e1682bbd6d11ed036a6dcda941353e5f70d22e9589
4
+ data.tar.gz: 06d1631a5e98ecfea25a53dd9ea9f40c235f2fea871fcc7d4fb9e2b707596439
5
5
  SHA512:
6
- metadata.gz: 7526b6e10f776726e8647efa1a47819d4958b0f0e54cb57f65c81968c4929bbf7330a326197adcbc869f1cc3333921d37f8eb74eb088da9f71690cea7a2c6715
7
- data.tar.gz: ec8fd2a7271e5aa28f8084716a75ebd8d5067a2d7d4e164aa521e8ceeea4b24897397242f1110ef0cd6cd4b5588dd0fb7c25e732c137ea196c5249ef5ece7f28
6
+ metadata.gz: bc183b5b76a8f3f92a27feb731a879780b454425a95c024bfc5ebba2c23f006ec2da5c2682c1c2074dcb4a946650d3941aa0482915c3987873c5bcc420613f3e
7
+ data.tar.gz: 19da3e02abe3806802bbe4efee68138a3a8b7e31477ae43a7085511f748dd88df59c71ba12398bf84310a74c7e57fca8ee25581e26d9ce797816ca95f5b5f3db
data/CHANGELOG.md CHANGED
@@ -6,6 +6,33 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.0] - 2026-10-21
10
+
11
+ ### Added
12
+
13
+ - A proposed public application and custom-store contract, with orchestration
14
+ internals excluded from generated API documentation.
15
+ - Explicit RubyLLM 2.1.0 and 2.1.1 CI matrices, and a shared intercepted/live
16
+ OpenRouter harness for text, approval, result association, and reconnect.
17
+ - Client-state assertions for one original tool call and one associated result.
18
+
19
+ ### Changed
20
+
21
+ - Remove the unused `allow_client_history` configuration and diagnostic key.
22
+ Client messages remain untrusted data under host-controlled history policy.
23
+ - Reject unsupported `protocol_version` configuration instead of accepting a
24
+ setting that cannot change the producer's fixed 1.0 output.
25
+
26
+ ### Fixed
27
+
28
+ - Expose the returned reconnect outcome through `Run#record`, without executing
29
+ the chat again. Verify real RubyLLM denial and installed result association.
30
+ - Resume real RubyLLM approval without repeating the original tool-call start
31
+ and arguments. Its result keeps the original call id; new calls still stream.
32
+ - Isolate and freeze nested input, identity, interrupt, and JSON record values.
33
+ Editing caller-owned data cannot mutate memory-store indexes or bypass a
34
+ record revision; editing published values raises `FrozenError`.
35
+
9
36
  ## [0.3.0] - 2026-10-20
10
37
 
11
38
  ### Added
data/README.md CHANGED
@@ -19,7 +19,7 @@ RubyLLM and AG-UI maintainers.
19
19
  ## Install
20
20
 
21
21
  ```ruby
22
- gem 'ruby_llm-ag_ui', '~> 0.3.0'
22
+ gem 'ruby_llm-ag_ui', '~> 0.4.0'
23
23
  ```
24
24
 
25
25
  Requires Ruby >= 3.2, RubyLLM >= 2.1 and < 3.0, and `ag-ui-protocol` 0.2.x.
@@ -0,0 +1,70 @@
1
+ ---
2
+ layout: default
3
+ title: Provider Verification
4
+ nav_order: 6
5
+ description: Run opt-in provider checks separately from offline client and protocol tests.
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
+ * How to run the maintained live checks with synthetic input.
16
+ * What the checks verify and which calls can reach a provider.
17
+ * How to keep credentials and provider results out of shared evidence.
18
+
19
+ ## Run the Opt-in Checks
20
+
21
+ From the repository, with `OPENROUTER_API_KEY` already configured:
22
+
23
+ ```sh
24
+ RUBY_LLM_LIVE=1 bundle exec rspec spec/live
25
+ ```
26
+
27
+ The harness uses OpenRouter with `openai/gpt-4.1-mini`. Set
28
+ `RUBY_LLM_LIVE_MODEL` to choose another OpenRouter model; record its name and
29
+ result separately. A successful model does not verify every model on the gateway.
30
+ These calls can incur provider charges. Default offline tests never run them.
31
+
32
+ If you use an environment file, keep it untracked and load it through dotenv:
33
+
34
+ ```sh
35
+ RUBY_LLM_LIVE=1 bundle exec dotenv -f .env.live rspec spec/live
36
+ ```
37
+
38
+ Set the key in that file or your existing credential manager. Do not put keys
39
+ in command arguments, inline exports, screenshots, or committed fixtures.
40
+ The repository ignores `.env*` files except `.env.example`.
41
+
42
+ ## Understand the Checks
43
+
44
+ One example streams a short text response. Another forces a synthetic lookup
45
+ that requires approval, verifies no tool execution before approval, resumes the
46
+ original call, and reconnects the completed resume. The tool only returns a
47
+ fixture value; it does not access a database, file, or business service.
48
+ Its result is redacted and retains the original tool-call id.
49
+
50
+ The provider configuration disables retries, caps output at 128 tokens, uses
51
+ a 30-second request timeout, and discards provider logs. The live network
52
+ allowlist permits only the OpenRouter HTTPS chat-completions endpoint. This
53
+ allowlist is a test guard, not an operating-system firewall.
54
+
55
+ An offline test drives the same setup through intercepted HTTP. It verifies
56
+ the bounded request and reproduces the real RubyLLM approval callbacks without
57
+ credentials. Browser tests continue to use the maintained synthetic host; they
58
+ verify the official client's message and tool-result association separately.
59
+
60
+ ## Record Evidence Without Secrets
61
+
62
+ Record the date, Ruby and RubyLLM versions, provider route, model, pass/fail
63
+ result, and any fixed regression. Keep generated evidence under ignored `tmp/`.
64
+ Do not save raw HTTP headers or provider traces. A failed assertion, intercepted
65
+ request, or schema-valid event is not proof that a live request completed.
66
+
67
+ For the recorded results and unverified paths, read
68
+ [Compatibility]({% link _reference/compatibility.md %}). Live cancellation,
69
+ timeouts, and network-loss timing require separate provider evidence; offline
70
+ fault injection does not establish those provider behaviors.
@@ -43,8 +43,8 @@ responsibility.
43
43
  - **Choose the model, tools or system prompt.** `input.tools` is informational:
44
44
  `Input#tool_names` and `Input#client_tools?` report it, and nothing in the gem
45
45
  registers a client-supplied tool on the chat. `input.messages` is parsed and
46
- echoed, never appended to the chat as trusted turns; `config.allow_client_history`
47
- is off and documented as off.
46
+ echoed, never appended to the chat as trusted turns. The host owns history
47
+ reconciliation; this gem has no switch that imports client history.
48
48
  - **Reach another actor's run.** Every lookup is bounded by the scope the host
49
49
  supplies. The same `(threadId, runId)` under a different scope is a different
50
50
  run (`spec/ruby_llm/ag_ui/run_spec.rb`: "does not let one scope attach to
@@ -60,3 +60,12 @@ The interrupt must still be pending and unexpired. The tool call id and argument
60
60
  * [Streaming Runs]({% link _core_features/streaming-runs.md %}) - follow the event and outcome sequence.
61
61
  * [Reconnects and Run Identity]({% link _core_features/reconnects.md %}) - understand stored run identity.
62
62
  * [Security and Deployment]({% link _advanced/security-and-deployment.md %}) - keep approval separate from authorization.
63
+
64
+
65
+ ## Keep the Original Tool Call
66
+
67
+ RubyLLM fires its before-tool callback when an approved call executes. The
68
+ resume run does not repeat the original `TOOL_CALL_START`, arguments, or end.
69
+ Its `TOOL_CALL_RESULT` uses the original `toolCallId`. A later new tool call
70
+ still emits its own start and arguments. Keep the client conversation across
71
+ both runs so the result can attach to the call shown before the interrupt.
@@ -52,3 +52,18 @@ stuck record; deleting its reservation can repeat unknown tool effects.
52
52
  * [Configuration]({% link _getting_started/configuration.md %}) - set the default store.
53
53
  * [Rails Integration]({% link _advanced/rails-integration.md %}) - install the durable ActiveRecord store.
54
54
  * [Protocol Coverage]({% link _reference/protocol-coverage.md %}) - inspect reconnect events.
55
+
56
+
57
+ ## Keep Request and Record Values Read-only
58
+
59
+ Parsed input, run identity, and JSON snapshots are isolated from the objects
60
+ supplied by the caller. Nested strings, arrays, hashes, and parsed SDK message
61
+ models are frozen. Editing them raises `FrozenError`; it cannot change the
62
+ reserved digest or bypass a store revision check. Constructors copy JSON values
63
+ rather than freezing the host's original state. Keep snapshots JSON-compatible.
64
+
65
+ Create a new input with `Input.parse` for a new request. Use `RunRecord`
66
+ transitions and retain `store.save`'s returned record when updating storage.
67
+
68
+ After `Run#stream` returns a recorded outcome, `Run#record` exposes that same
69
+ record, including on a reconnect. Reading it never executes the chat.
@@ -40,14 +40,19 @@ The global configuration supplies defaults. `Run.new` can override the store, lo
40
40
  | `redact` | `Redaction.default` | Transform tool results before they reach the stream. |
41
41
  | `emit_tool_progress` | `false` | Publish tool progress as redacted activity snapshots. |
42
42
  | `logger` | `nil` | Receive host-side diagnostics without prompts or raw tool payloads. |
43
- | `protocol_version` | `"1.0"` | Value reported by `RUN_STARTED`. |
43
+ | `protocol_version` | `"1.0"` | Fixed supported version reported by `RUN_STARTED`; setting another value raises `ConfigurationError`. |
44
44
  | `max_input_bytes` | `256_000` | Maximum request body size. |
45
45
  | `max_messages` | `500` | Maximum messages in one request. |
46
46
  | `max_tools` | `64` | Maximum tool definitions in one request. |
47
47
  | `max_context` | `64` | Maximum context entries in one request. |
48
48
  | `interrupt_ttl` | `900` seconds | How long an approval interrupt can be answered. |
49
49
  | `clock` | `Time.now.utc` | Clock used to set interrupt expiry. |
50
- | `allow_client_history` | `false` | Reserved for future history behavior; client history is not trusted in this release. |
50
+
51
+ `Configuration#protocol_version=` accepts only `"1.0"`. Its value describes the
52
+ producer; it does not select another event schema.
53
+
54
+ Client history stays under host control. There is no configuration switch that
55
+ trusts or imports it into a chat.
51
56
 
52
57
  Provider keys and model selection are configured through RubyLLM and the host app. This gem does not read credentials when it loads.
53
58
 
@@ -0,0 +1,57 @@
1
+ ---
2
+ layout: default
3
+ title: "What's New in 0.4"
4
+ nav_order: -1
5
+ description: Review the public integration contract and verified compatibility boundaries.
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
+ * Which configuration changes need an application update.
16
+ * Where to find the proposed public API and compatibility evidence.
17
+
18
+ ## Use Effective Configuration
19
+
20
+ ```ruby
21
+ RubyLLM::AgUI.configure do |config|
22
+ config.protocol_version = '1.0'
23
+ end
24
+ ```
25
+
26
+ Other protocol versions raise `ConfigurationError`. The unused
27
+ `allow_client_history` setting is removed. Client history remains the host's
28
+ responsibility. See [Upgrading]({% link _reference/upgrading.md %}).
29
+
30
+ ## Preserve Immutable Request and Store Values
31
+
32
+ Parsed input and JSON record values now copy and freeze their nested members.
33
+ Changing a caller's original state cannot change a stored snapshot or identity.
34
+ Use new inputs and record transitions instead of editing nested values. See
35
+ [Reconnects]({% link _core_features/reconnects.md %}).
36
+
37
+ The [Public API Contract]({% link _reference/public-api.md %}) identifies
38
+ application and custom-store APIs and separates orchestration internals.
39
+
40
+ ## Associate Resumed Tool Results Once
41
+
42
+ Real RubyLLM approval resumes now emit the result of the original call without
43
+ repeating its start and arguments. The offline real-chat and opt-in provider
44
+ checks use the same harness. `Run#record` also exposes the recorded outcome
45
+ after a reconnect, matching the return value from `stream`. See
46
+ [Approval and Resume]({% link _core_features/approval-and-resume.md %}).
47
+
48
+ ## Verify the Provider Separately
49
+
50
+ The opt-in harness checks OpenRouter text and a synthetic approval/resume
51
+ without external business effects. The same real RubyLLM chat setup has offline
52
+ HTTP tests. See [Provider Verification]({% link _advanced/provider-verification.md %})
53
+ for credential handling, request limits, and the scope of the results.
54
+
55
+ Read
56
+ [Compatibility]({% link _reference/compatibility.md %}) for recorded results
57
+ and limits; an intended matrix is not a completed verification.
@@ -11,7 +11,8 @@ description: Browse RDoc pages generated from classes and methods in the gem sou
11
11
  {{ page.description }}
12
12
  {: .fs-6 .fw-300 }
13
13
 
14
- The [API reference]({{ '/api/' | relative_url }}) opens on the `RubyLLM` module. The generated pages follow the RDoc comments in `lib/`.
14
+ Read the [Public API Contract]({% link _reference/public-api.md %}) before
15
+ depending on a class. The [API reference]({{ '/api/' | relative_url }}) opens on the `RubyLLM` module. The generated pages follow the RDoc comments in `lib/`.
15
16
 
16
17
  | Build with | Reference |
17
18
  | --- | --- |
@@ -23,11 +23,11 @@ dependency does.
23
23
 
24
24
  | Component | Range | Evidence |
25
25
  |---|---|---|
26
- | Ruby | `>= 3.2` | Recorded 0.3 CI passes on 3.2.11, 3.3.12, and 3.4.11 |
27
- | RubyLLM | `>= 2.1`, `< 3.0` | 2.1.1 (callback, approval and cancellation behavior read from its source) |
26
+ | Ruby | `>= 3.2` | Recorded 0.4 CI passes on 3.2.11, 3.3.12, and 3.4.11 |
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.3 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 |
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
@@ -54,6 +54,66 @@ adapter is only loaded when `Rails::Railtie` is already defined.
54
54
 
55
55
  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
56
 
57
+ ## 0.4 Release Acceptance
58
+
59
+ The [2026-10-21 acceptance CI](https://github.com/yorzi/ruby_llm-ag-ui/actions/runs/39518735488)
60
+ passes all 17 jobs at `e09b81b`. Each test job reports 296 examples, zero failures,
61
+ and two opt-in provider skips across the Ruby, Rails, and RubyLLM matrix below.
62
+ Local checks also pass 296 examples and lint on 95 files. Both installed
63
+ endpoints verify one original tool call and one result with its original id.
64
+
65
+ The added regressions cover `Run#record` on reconnect and real RubyLLM denial
66
+ without tool execution. Denial uses intercepted HTTP. The local OpenRouter
67
+ text and approval/resume examples are separately rerun on the release code:
68
+ two live cases plus one offline harness example pass with zero failures.
69
+
70
+ The browser suite again passes 18 workflows and validates all 242 consumed
71
+ events. Desktop approval progress and 390px mobile denial screenshots were
72
+ inspected. The earlier candidate record below retains its own commit and counts.
73
+
74
+ ## Local 0.4 Development Verification
75
+
76
+ On 2026-10-21, the candidate passes 294 offline examples with zero failures
77
+ and two opt-in provider checks skipped. Separate runs use RubyLLM 2.1.0 and
78
+ 2.1.1 on Ruby 3.3.5 with Rails 8.1.4 and JSON 2.21.2. The [0.4 candidate CI](https://github.com/yorzi/ruby_llm-ag-ui/actions/runs/39515383346)
79
+ passes all 17 jobs at `14b3c15`: six Ruby jobs across 3.2.11, 3.3.12, and
80
+ 3.4.11 with both RubyLLM versions; eight Rails jobs across 8.0.5.1 and 8.1.4
81
+ on Ruby 3.3/3.4 with both versions; lint, installed-host, and browser checks.
82
+ Each suite reports 294 examples, zero failures, and two opt-in provider skips.
83
+
84
+ The maintained client passes 18 workflows and validates all 242 consumed
85
+ events. Approval assertions inspect the official client's messages as well as
86
+ the wire: one original call, one associated result, no duplicate visible text
87
+ after reconnect. The remote browser uses Chromium 156.0.8078.4; local and remote
88
+ desktop progress and 390px denial screenshots were inspected. The fixture now fires the same resumed before-tool callback
89
+ as a real RubyLLM chat. Installed-host and generated Rails model checks pass. A fixed local 66-file
90
+ artifact independently passes clean core loading, activated dependency audit,
91
+ and Rails generator migration up/down. The isolated core uses JSON 2.7.1.
92
+ The root and activated-core audit reports no findings against the recorded
93
+ advisory database. These are development artifact checks, not public downloads.
94
+
95
+ ### Recorded Live Provider Session
96
+
97
+ The 2026-10-21 local session uses OpenRouter with `openai/gpt-4.1-mini`,
98
+ RubyLLM 2.1.1, and Ruby 3.3.5. Both live examples pass: streamed text, then a
99
+ synthetic tool approval/resume with its original call id, redaction, and completed
100
+ resume reconnect. The tool is not executed before approval and executes once
101
+ after it. The same command includes one offline harness example, for three
102
+ examples total with zero failures. It verifies the gem's Ruby path, not a live
103
+ provider session through the browser or a deployed Rails application.
104
+
105
+ This session first exposed duplicate tool-call starts on a real resume.
106
+ An intercepted-HTTP real-chat test reproduces that defect before the fix and
107
+ passes afterward. Text, tool results, and approval are live evidence for this
108
+ route and model only. Live cancellation, timeout timing, network loss, denial,
109
+ and other providers remain unverified. Follow
110
+ [Provider Verification]({% link _advanced/provider-verification.md %}) to reproduce.
111
+
112
+ The checked current `ag-ui-protocol` remains 0.2.0. Its missing cancelled
113
+ outcome and `RUN_STARTED.protocolVersion` still require the local protocol
114
+ shims. Their encoded events pass the pinned schema checks; no workaround is
115
+ removed solely because a new version was expected.
116
+
57
117
  ## Local 0.3 Development Verification
58
118
 
59
119
  On 2026-10-19, Ruby 3.3.5, RubyLLM 2.1.1, and Rails 8.1.4 pass 284 examples
@@ -89,6 +149,14 @@ chat. Version 0.3 bounds JSON to 2.x; the passing matrix uses 2.21.2. Intercepte
89
149
  HTTP tests reproduce the failure before the bound and pass after it. These
90
150
  checks make no live provider call.
91
151
 
152
+ The [0.3.0 release workflow](https://github.com/yorzi/ruby_llm-ag-ui/actions/runs/39376258843)
153
+ passes offline tests, browser workflows, advisory checks, and installed-host
154
+ plus clean core/Rails checks. After publication, the actual GitHub and RubyGems
155
+ downloads have matching SHA-256 hashes and all 62 packaged files match the tag.
156
+ The downloaded artifact independently passes both installed endpoints and fresh
157
+ core, advisory, and Rails generator migration checks. The isolated macOS core
158
+ activates JSON 2.7.1; the remote test matrix uses 2.21.2.
159
+
92
160
  ## Recorded 0.2 Client Verification
93
161
 
94
162
  On 2026-10-19, the local 0.2.0 candidate passes the maintained browser workflows
@@ -126,12 +194,12 @@ checks after download.
126
194
  - **Other clients or versions.** CopilotKit and client versions other than the
127
195
  recorded `@ag-ui/client` version have not been verified. Schema conformance
128
196
  does not establish that every UI renders the events correctly.
129
- - **Live providers.** `spec/live/live_run_spec.rb` is the opt-in check
130
- (`RUBY_LLM_LIVE=1`, plus a provider key); its provider context is skipped in the default suite. The same run setup
131
- is exercised separately with an offline chat. Provider behavior in 2.1.1 is derived from its source, not from a recorded
132
- session.
197
+ - **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.
133
200
  - **Every RubyLLM patch release in the `>= 2.1, < 3.0` range.** The upper bound
134
- is enforced by the gemspec; the verified version is 2.1.1.
201
+ is a dependency resolver bound; verified versions are 2.1.0 and 2.1.1. Future
202
+ 2.x releases need the same checks before a compatibility claim.
135
203
  - **Rails versions before 8.0.** The adapter uses `Rails.setup!` and an
136
204
  initializer, and its specs run on 8.0/8.1. It may work on 7.2 and it is not
137
205
  tested there.
@@ -0,0 +1,102 @@
1
+ ---
2
+ layout: default
3
+ title: Public API Contract
4
+ nav_order: 3
5
+ description: Build against the application API and custom-store contract proposed for 1.0.
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
+ * Which entry points applications can depend on.
16
+ * Which values and failures cross the integration boundary.
17
+ * How to implement a custom store without depending on orchestration internals.
18
+
19
+ ## Run One Authorized Turn
20
+
21
+ ```ruby
22
+ input = RubyLLM::AgUI::Input.parse(request_body)
23
+ record = RubyLLM::AgUI::Run.new(
24
+ chat: host_chat, input: input, scope: authenticated_scope, store: run_store
25
+ ).stream(to: response_stream)
26
+ record.status
27
+ ```
28
+
29
+ The host owns its chat, authentication, authorization, history, model, and tools.
30
+ Parsing client data never adds messages or tools to that chat. A completed run
31
+ returns its stored outcome on an exact retry; it never repeats the turn.
32
+ An active duplicate is refused. See
33
+ [Reconnects]({% link _core_features/reconnects.md %}).
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.
37
+
38
+ | Entry point | Application contract |
39
+ | --- | --- |
40
+ | `RubyLLM::AgUI.configure`, `.config`, `.reset_configuration!` | Application defaults listed in [Configuration]({% link _getting_started/configuration.md %}). |
41
+ | `Input.parse(raw, max_bytes: nil, strict: false, logger: nil)` | Accept a Hash, JSON String, or readable body; return parsed identity, data, digest, warnings, and `to_h`. |
42
+ | `Run.new(...).stream(to:)` | Reserve before execution; return a `RunRecord`; serialize callbacks and detach a failed transport. `Run#record` exposes the reserved record and, after completion or reconnect, the returned outcome. |
43
+ | `SSEWriter#write`, `#flush`, `#close`, `#closed?`, `.headers` | Encode schema-valid events on an IO-like stream; direct write failures raise `TransportError`. |
44
+ | `RunRecord`, `Interrupt`, `ResumeEntry` | Read-only identity and JSON data, predicates, documented transitions, and serialization. Transitions return new values. |
45
+ | `Redaction.default`, `.none` and a host callable | Transform tool output and opt-in progress; nil uses the default redactor. |
46
+ | `Coverage` | Inspect produced and unsupported event types for the pinned schema. |
47
+
48
+ `Mapper` is an advanced event projection API. Its documented methods accept
49
+ RubyLLM chunks, messages, and tool calls; they return events and do not persist
50
+ or execute work. Prefer `Run` when you need identity and terminal guarantees.
51
+ The protocol shim's concrete model classes and constructors are implementation
52
+ details. Depend on encoded AG-UI events, not on a shim class name.
53
+
54
+ ## Implement a Store
55
+
56
+ Implement the public methods on `Store::Base`: `reserve`, `find`, `latest_run`,
57
+ `latest_interrupted`, `save`, `find_interrupt`, and `redeem_interrupt`.
58
+ `recover` is supplied by the base class, or you can implement the same conditional
59
+ contract. Return `Reservation` and `Redemption` values where the interface
60
+ requires them. Their constructors and predicates are extension API.
61
+
62
+ Keep scope and run identity isolated. Reserve identities and redeem approvals
63
+ atomically. `save(record)` compares the observed revision and returns a new
64
+ record with a higher revision. Retain the return value. A stale write raises
65
+ `StaleRecordError`. A recovery never supersedes a terminal or newer outcome.
66
+ Read [Worker Recovery]({% link _advanced/worker-recovery.md %}) before using it.
67
+
68
+ Use `MemoryStore` within one process, or `Rails::ActiveRecordStore` with the
69
+ shipped migration. The verified database is SQLite. Other databases require
70
+ independent concurrency evidence.
71
+
72
+ ## Handle Errors at the Right Boundary
73
+
74
+ `InputError#http_status` is 400 for malformed or invalid input, 413 for size
75
+ limits, and 409 for identity conflicts or rejected resumes. `ConfigurationError`
76
+ is a host setup error. An active duplicate raises `StreamError`; the Rails
77
+ adapter refuses it before committing the stream with 409.
78
+
79
+ A provider or tool failure becomes a sanitized `RUN_ERROR` and failed record.
80
+ A cooperative cancellation becomes a cancelled outcome. A `PersistenceError`
81
+ propagates because acknowledgement may be uncertain. Inspect storage before
82
+ recovery; do not infer whether an external tool effect happened from that error.
83
+ A `RunConflictError`, including `StaleRecordError`, also propagates.
84
+
85
+ The Rails entry point is `stream_ag_ui_run { |input| authorized_chat }`.
86
+ Override `ag_ui_scope` and optionally `ag_ui_state`. Configure storage with
87
+ `config.run_store`. Private concern helpers are not extension API. Follow the
88
+ [Rails guide]({% link _advanced/rails-integration.md %}) for their signatures.
89
+
90
+ ## Avoid Orchestration Internals
91
+
92
+ `Emitter`, `Resume`, `Reconnect`, `MessageParser`, input parser helpers,
93
+ `Validation`, `ErrorSanitizer`, `Persistence`, and `Immutable` are internal.
94
+ Their names, constructors, and methods can change without an application API
95
+ migration. Store private helpers and generated Rails model internals have the
96
+ same boundary.
97
+
98
+ ## Next Steps
99
+
100
+ * [API Reference]({% link _reference/api-reference.md %}) - inspect source signatures.
101
+ * [Compatibility]({% link _reference/compatibility.md %}) - read tested versions and limits.
102
+ * [Upgrading]({% link _reference/upgrading.md %}) - adapt older applications.
@@ -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.3.0](https://github.com/yorzi/ruby_llm-ag-ui/releases/tag/v0.3.0) | 2026-10-20 | [Lifecycle and worker recovery]({% link _getting_started/whats-new-in-0-3.md %}): disconnect handling, scoped cancellation, and conditional durable writes |
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 |
21
+ | [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 |
21
22
  | [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 |
22
23
  | [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 |
23
24
 
@@ -15,42 +15,34 @@ 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.2 to 0.3
18
+ ## Move from 0.3 to 0.4
19
19
 
20
- Drain your old workers, change your Gemfile, and update the dependency:
20
+ Update your Gemfile and dependency:
21
21
 
22
22
  ```ruby
23
- gem 'ruby_llm-ag_ui', '~> 0.3.0'
23
+ gem 'ruby_llm-ag_ui', '~> 0.4.0'
24
24
  ```
25
25
 
26
26
  ```sh
27
27
  bundle update ruby_llm-ag_ui
28
28
  ```
29
29
 
30
- Version 0.3 requires JSON 2.x. Rails 8.0 passes an option removed in JSON 3
31
- when RubyLLM loads its chat class. If your Gemfile pins JSON 3, remove that pin
32
- or select JSON 2.x before updating. This bound also applies to plain Ruby hosts.
33
-
34
- `Run` now continues after a response write or flush fails. It saves the real
35
- outcome instead of reporting the disconnected client as a failed run. Hosts
36
- that previously relied on disconnects to stop work must explicitly request
37
- cancellation on the authorized chat. `SSEWriter` still raises a `StreamError`
38
- subclass when used directly. See
39
- [Streaming Runs]({% link _core_features/streaming-runs.md %}).
40
-
41
- Store writes now use an atomic revision check. Always retain the record returned
42
- by `save`. Custom stores must implement this contract before upgrading. Existing
43
- JSON payloads load with revision zero and the shipped table needs no migration.
44
- Drain old workers first, since 0.2 writers do not enforce revision checks.
45
- `PersistenceError` now propagates storage failures without guessing a terminal
46
- outcome. Read [Worker Recovery]({% link _advanced/worker-recovery.md %}) for
47
- explicit recovery and external-effect reconciliation.
48
-
49
- Faraday request timeouts now use `transport_timeout` instead of the generic
50
- `transport_error` code. Both remain failed outcomes. See
51
- [Cancellation and Timeouts]({% link _core_features/cancellation.md %}).
30
+ This release keeps JSON 2.x and the 0.3 store revision contract. It needs no
31
+ additional SQL migration. Drain old workers before replacing their code.
32
+
33
+ Remove assignments to `config.allow_client_history`. The unused setting is
34
+ removed, including its diagnostic key. The gem never imports client messages
35
+ into the host chat; reconcile history in your host application.
36
+
37
+ `config.protocol_version` remains `"1.0"`. Assigning any other value now raises
38
+ `ConfigurationError` instead of accepting a setting the emitted events ignore.
39
+
40
+ Nested request and JSON record values are now read-only. In-place edits raise
41
+ `FrozenError`. Build a new input, or use record transitions and `store.save`,
42
+ instead of mutating ids, snapshots, failure hashes, or interrupt values.
43
+ Caller-owned JSON objects remain mutable and do not alias the stored record.
52
44
 
53
45
  ## Older Releases
54
46
 
55
- For earlier upgrade steps, read the
56
- [0.2 upgrading guide](https://github.com/yorzi/ruby_llm-ag-ui/blob/v0.2.0/docs/_reference/upgrading.md).
47
+ Read the [0.3 upgrading guide](https://github.com/yorzi/ruby_llm-ag-ui/blob/v0.3.0/docs/_reference/upgrading.md)
48
+ for JSON 2.x, worker draining, and the custom-store revision contract.
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  layout: default
3
3
  title: "What's New in 0.3"
4
- nav_order: -1
4
+ nav_order: 30
5
+ parent: Release History
5
6
  description: Verify transport failure, scoped cancellation, and explicit worker recovery.
6
7
  ---
7
8
 
@@ -10,8 +10,8 @@ module RubyLLM
10
10
  # config.logger = Rails.logger
11
11
  # end
12
12
  #
13
- # Every value can be overridden per run by passing the matching keyword to
14
- # RubyLLM::AgUI::Run.new.
13
+ # Run.new can override storage, logging, redaction, clock, interrupt expiry,
14
+ # and tool progress. Input.parse uses the configured request limits.
15
15
  class Configuration
16
16
  # The object that reserves run identity and stores interrupts. Defaults
17
17
  # to nil; set a RubyLLM::AgUI::Store::MemoryStore for single-process use,
@@ -38,7 +38,17 @@ module RubyLLM
38
38
 
39
39
  # The AG-UI protocol version this producer speaks, emitted on
40
40
  # RUN_STARTED so a consumer can detect a downgrade.
41
- attr_accessor :protocol_version
41
+ attr_reader :protocol_version
42
+
43
+ # Accepts only the protocol implemented by this producer. A different
44
+ # version is refused instead of silently reporting a misleading setting.
45
+ def protocol_version=(value)
46
+ unless value == Protocol::PROTOCOL_VERSION
47
+ raise ConfigurationError, "This producer supports protocol #{Protocol::PROTOCOL_VERSION} only."
48
+ end
49
+
50
+ @protocol_version = value
51
+ end
42
52
 
43
53
  # Maximum accepted request body size in bytes.
44
54
  attr_accessor :max_input_bytes
@@ -58,23 +68,17 @@ module RubyLLM
58
68
  # A callable returning the current UTC Time. Injected in tests.
59
69
  attr_accessor :clock
60
70
 
61
- # Whether client-supplied message history is reconciled against
62
- # host-stored history. Always off in the first release: client messages
63
- # are never trusted as system or tool turns.
64
- attr_accessor :allow_client_history
65
-
66
71
  def initialize
67
72
  @run_store = nil
68
73
  @redact = Redaction.method(:default)
69
74
  @logger = nil
70
- @protocol_version = '1.0'
75
+ @protocol_version = Protocol::PROTOCOL_VERSION
71
76
  @max_input_bytes = 256_000
72
77
  @max_messages = 500
73
78
  @max_tools = 64
74
79
  @max_context = 64
75
80
  @interrupt_ttl = 900
76
81
  @clock = -> { Time.now.utc }
77
- @allow_client_history = false
78
82
  @emit_tool_progress = false
79
83
  end
80
84
 
@@ -110,7 +114,6 @@ module RubyLLM
110
114
  max_tools: max_tools,
111
115
  max_context: max_context,
112
116
  interrupt_ttl: interrupt_ttl,
113
- allow_client_history: allow_client_history,
114
117
  emit_tool_progress: emit_tool_progress
115
118
  }
116
119
  end
@@ -14,7 +14,7 @@ module RubyLLM
14
14
  # The emitter decides nothing about the outcome and executes nothing. What it
15
15
  # owns is the one ordering rule a caller should never have to remember: a
16
16
  # run's open text, tool calls and step are closed before the terminal event.
17
- class Emitter
17
+ class Emitter # :nodoc:
18
18
  # The run record, including the sequence numbers written so far.
19
19
  attr_reader :record
20
20
 
@@ -9,7 +9,7 @@ module RubyLLM
9
9
  # module never does that: it classifies the error by class and returns a
10
10
  # fixed, public message. The raw exception goes to the host logger, if the
11
11
  # host configured one, and nowhere else.
12
- module ErrorSanitizer
12
+ module ErrorSanitizer # :nodoc:
13
13
  # Ordered by specificity. The first ancestor of the error whose name is a
14
14
  # key wins, so subclass relationships are respected without loading
15
15
  # optional error classes eagerly.
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module AgUI
5
+ # Isolated JSON-shaped values for request and storage boundaries.
6
+ module Immutable # :nodoc:
7
+ module_function
8
+
9
+ def copy(value)
10
+ case value
11
+ when Hash
12
+ value.to_h { |key, item| [copy(key), copy(item)] }.freeze
13
+ when Array
14
+ value.map { |item| copy(item) }.freeze
15
+ when String, Time
16
+ value.dup.freeze
17
+ when ::AgUiProtocol::Core::Types::Model
18
+ copy_model(value)
19
+ else
20
+ value
21
+ end
22
+ end
23
+
24
+ def copy_model(value)
25
+ value.dup.tap do |model|
26
+ model.instance_variables.each do |name|
27
+ model.instance_variable_set(name, copy(model.instance_variable_get(name)))
28
+ end
29
+ end.freeze
30
+ end
31
+ private_class_method :copy_model
32
+ end
33
+ end
34
+ end
@@ -17,8 +17,8 @@ module RubyLLM
17
17
  #
18
18
  # Nothing here grants trust. Client messages, state, tools, context and
19
19
  # forwardedProps are accepted as data only; the host decides which tools
20
- # and model a run may use, and client history is reconciled by id against
21
- # host-stored history rather than appended as trusted system or tool turns.
20
+ # and model a run may use. This gem never reconciles or appends client
21
+ # history to the host chat. The host owns that policy.
22
22
  class Input
23
23
  # The protocol versions this gem can act on.
24
24
  SUPPORTED_PROTOCOL_VERSIONS = [Protocol::PROTOCOL_VERSION].freeze
@@ -71,6 +71,7 @@ module RubyLLM
71
71
  end
72
72
 
73
73
  def initialize(attributes)
74
+ attributes = Immutable.copy(attributes)
74
75
  @thread_id = attributes[:thread_id]
75
76
  @run_id = attributes[:run_id]
76
77
  @parent_run_id = attributes[:parent_run_id]
@@ -118,7 +119,7 @@ module RubyLLM
118
119
  #
119
120
  # Kept separate from Input so the value object stays immutable and every
120
121
  # failure message stays in one place.
121
- class Parser
122
+ class Parser # :nodoc:
122
123
  include Validation
123
124
 
124
125
  MAX_NESTING = 32
@@ -246,7 +247,7 @@ module RubyLLM
246
247
 
247
248
  # Builds the composite members of a RunAgentInput: the client tool
248
249
  # definitions, the context entries and the resume entries.
249
- class PartBuilder
250
+ class PartBuilder # :nodoc:
250
251
  include Validation
251
252
 
252
253
  def tools(values)
@@ -69,15 +69,15 @@ module RubyLLM
69
69
  # rubocop:disable Metrics/ParameterLists -- one keyword per field the value object owns
70
70
  def initialize(id:, tool_call_id:, arguments_digest:, expires_at:, reason: REASON_TOOL_CALL,
71
71
  message: nil, status: PENDING, decided_at: nil, created_at: nil)
72
- @id = id
73
- @tool_call_id = tool_call_id
74
- @arguments_digest = arguments_digest
75
- @expires_at = expires_at
76
- @reason = reason
77
- @message = message
72
+ @id = Immutable.copy(id)
73
+ @tool_call_id = Immutable.copy(tool_call_id)
74
+ @arguments_digest = Immutable.copy(arguments_digest)
75
+ @expires_at = Immutable.copy(expires_at)
76
+ @reason = Immutable.copy(reason)
77
+ @message = Immutable.copy(message)
78
78
  @status = assert_status!(status)
79
- @decided_at = decided_at
80
- @created_at = created_at
79
+ @decided_at = Immutable.copy(decided_at)
80
+ @created_at = Immutable.copy(created_at)
81
81
  freeze
82
82
  end
83
83
 
@@ -146,7 +146,7 @@ module RubyLLM
146
146
  reason: reason,
147
147
  message: message,
148
148
  tool_call_id: tool_call_id,
149
- expires_at: expires_at&.utc&.iso8601
149
+ expires_at: expires_at&.getutc&.iso8601
150
150
  )
151
151
  end
152
152
 
@@ -194,7 +194,7 @@ module RubyLLM
194
194
  private
195
195
 
196
196
  def iso8601(time)
197
- time&.utc&.iso8601
197
+ time&.getutc&.iso8601
198
198
  end
199
199
 
200
200
  def assert_status!(value)
@@ -60,6 +60,12 @@ module RubyLLM
60
60
  @closed = false
61
61
  end
62
62
 
63
+ # Internal resume bookkeeping: these calls were already reported by the
64
+ # interrupted run. Only their result and progress belong to this run.
65
+ def continue_tool_calls(ids) # :nodoc:
66
+ @continued_tool_call_ids = ids.dup.freeze
67
+ end
68
+
63
69
  # One streamed Chunk from Chat#complete.
64
70
  def chunk(chunk)
65
71
  return [] if @closed
@@ -185,6 +191,8 @@ module RubyLLM
185
191
  return [] if tool_calls.nil? || tool_calls.empty?
186
192
 
187
193
  tool_calls.flat_map do |key, tool_call|
194
+ next [] if @continued_tool_call_ids&.include?(tool_call.id)
195
+
188
196
  record = track(key, tool_call, streamed: streamed)
189
197
  open_tool_call(record) + argument_events(record, tool_call, streamed: streamed)
190
198
  end
@@ -9,7 +9,7 @@ module RubyLLM
9
9
  # gem never trusts a client message as a system instruction or a tool
10
10
  # result. This parser only guarantees the shapes the pinned schema defines,
11
11
  # so a mismatch is a clear InputError instead of a downstream surprise.
12
- class MessageParser
12
+ class MessageParser # :nodoc:
13
13
  include Validation
14
14
 
15
15
  # The roles the pinned schema defines.
@@ -12,7 +12,7 @@ module RubyLLM
12
12
  # What cannot be served this way is a run that has not ended: attaching a
13
13
  # second stream to a live run needs a relay the gem does not own, and a
14
14
  # silent second stream would duplicate the work. Those are refused.
15
- class Reconnect
15
+ class Reconnect # :nodoc:
16
16
  # The failure reported for a run that failed before its sanitized error
17
17
  # was recorded — a record from an older release, or one whose write was
18
18
  # interrupted.
@@ -15,7 +15,7 @@ module RubyLLM
15
15
  #
16
16
  # An approval answers an interrupt. The host still re-authorizes the actor
17
17
  # before the tool executes.
18
- class Resume
18
+ class Resume # :nodoc:
19
19
  # One entry's effect: the interrupt it answers and the decision to record.
20
20
  Decision = Struct.new(:interrupt, :decision, keyword_init: true)
21
21
 
@@ -25,9 +25,9 @@ module RubyLLM
25
25
  attr_reader :payload
26
26
 
27
27
  def initialize(interrupt_id:, status:, payload: nil)
28
- @interrupt_id = interrupt_id
29
- @status = status
30
- @payload = payload
28
+ @interrupt_id = Immutable.copy(interrupt_id)
29
+ @status = Immutable.copy(status)
30
+ @payload = Immutable.copy(payload)
31
31
  freeze
32
32
  end
33
33
 
@@ -20,7 +20,8 @@ module RubyLLM
20
20
  # input, because a scope derived from anything the client sent would let one
21
21
  # client attach to another's run.
22
22
  class Run
23
- # The run's authoritative record, once it has been reserved.
23
+ # The reserved record or final outcome observed by this run, including a
24
+ # reconnect. Nil before the run obtains its reservation.
24
25
  attr_reader :record
25
26
 
26
27
  # rubocop:disable-next Metrics/ParameterLists -- one keyword per host-supplied or overridable value
@@ -52,16 +53,15 @@ module RubyLLM
52
53
  def stream(to:)
53
54
  resume.plan(now: @clock.call) if resume.needed?
54
55
  reservation = reserve
55
- return execute(reservation.record, to) if reservation.created?
56
+ @record = reservation.record
57
+ return execute(@record, to) if reservation.created?
56
58
 
57
- reconnect(reservation.record, to)
59
+ reconnect(@record, to)
58
60
  end
59
61
 
60
62
  private
61
63
 
62
- def resume
63
- @resume ||= Resume.new(input: @input, store: @store, scope: @scope, chat: @chat)
64
- end
64
+ def resume = @resume ||= Resume.new(input: @input, store: @store, scope: @scope, chat: @chat)
65
65
 
66
66
  def reserve
67
67
  Persistence.write do
@@ -102,6 +102,7 @@ module RubyLLM
102
102
 
103
103
  def start_execution(record, to)
104
104
  @mapper = build_mapper(record)
105
+ @mapper.continue_tool_calls(@chat.pending_approvals.map(&:id)) if resume.needed?
105
106
  @emitter = build_emitter(record, to, mapper: @mapper)
106
107
  @emitter.start(run_started_event(record), state: @state)
107
108
  end
@@ -72,18 +72,18 @@ module RubyLLM
72
72
  # rubocop:disable-next Metrics/ParameterLists -- one keyword per field the record owns
73
73
  def initialize(scope:, thread_id:, run_id:, digest:, created_at:, status: RESERVED, sequence: 0,
74
74
  snapshot: nil, interrupts: [], failure: nil, updated_at: nil, revision: 0)
75
- @scope = scope
76
- @thread_id = thread_id
77
- @run_id = run_id
78
- @digest = digest
75
+ @scope = Immutable.copy(scope)
76
+ @thread_id = Immutable.copy(thread_id)
77
+ @run_id = Immutable.copy(run_id)
78
+ @digest = Immutable.copy(digest)
79
79
  @status = assert_status!(status)
80
80
  @sequence = sequence
81
81
  @revision = revision
82
- @snapshot = snapshot
83
- @interrupts = interrupts.freeze
84
- @failure = failure
85
- @created_at = created_at
86
- @updated_at = updated_at || created_at
82
+ @snapshot = Immutable.copy(snapshot)
83
+ @interrupts = Immutable.copy(interrupts)
84
+ @failure = Immutable.copy(failure)
85
+ @created_at = Immutable.copy(created_at)
86
+ @updated_at = Immutable.copy(updated_at || created_at)
87
87
  freeze
88
88
  end
89
89
 
@@ -229,7 +229,7 @@ module RubyLLM
229
229
  end
230
230
 
231
231
  def iso8601(time)
232
- time&.utc&.iso8601
232
+ time&.getutc&.iso8601
233
233
  end
234
234
 
235
235
  def assert_status!(value)
@@ -34,7 +34,7 @@ module RubyLLM
34
34
  )
35
35
  @records[key] = record
36
36
  thread = thread_key(scope, thread_id)
37
- @threads[thread] = (@threads[thread] || []) + [run_id]
37
+ @threads[thread] = (@threads[thread] || []) + [record.run_id]
38
38
  Reservation.new(record: record, status: Reservation::CREATED)
39
39
  end
40
40
  end
@@ -107,11 +107,11 @@ module RubyLLM
107
107
  end
108
108
 
109
109
  def key_for(scope, thread_id, run_id)
110
- [scope_key(scope), thread_id, run_id]
110
+ Immutable.copy([scope_key(scope), thread_id, run_id])
111
111
  end
112
112
 
113
113
  def thread_key(scope, thread_id)
114
- [scope_key(scope), thread_id]
114
+ Immutable.copy([scope_key(scope), thread_id])
115
115
  end
116
116
  end
117
117
  end
@@ -9,7 +9,7 @@ module RubyLLM
9
9
  # the client without echoing request bytes back.
10
10
  #
11
11
  # Included by RubyLLM::AgUI::Input::Parser and RubyLLM::AgUI::MessageParser.
12
- module Validation
12
+ module Validation # :nodoc:
13
13
  # The longest accepted id (threadId, runId, message id, interrupt id).
14
14
  MAX_ID_LENGTH = 255
15
15
 
@@ -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.3.0'
6
+ VERSION = '0.4.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.3.0
4
+ version: 0.4.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-20 00:00:00.000000000 Z
11
+ date: 2026-10-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ag-ui-protocol
@@ -163,6 +163,7 @@ files:
163
163
  - README.md
164
164
  - SECURITY.md
165
165
  - docs/_advanced/client-example.md
166
+ - docs/_advanced/provider-verification.md
166
167
  - docs/_advanced/rails-integration.md
167
168
  - docs/_advanced/security-and-deployment.md
168
169
  - docs/_advanced/worker-recovery.md
@@ -175,16 +176,18 @@ files:
175
176
  - docs/_getting_started/configuration.md
176
177
  - docs/_getting_started/getting-started.md
177
178
  - docs/_getting_started/overview.md
178
- - docs/_getting_started/whats-new-in-0-3.md
179
+ - docs/_getting_started/whats-new-in-0-4.md
179
180
  - docs/_reference/api-reference.md
180
181
  - docs/_reference/compatibility.md
181
182
  - docs/_reference/protocol-coverage.md
183
+ - docs/_reference/public-api.md
182
184
  - docs/_reference/release-history.md
183
185
  - docs/_reference/releasing.md
184
186
  - docs/_reference/third-party-notices.md
185
187
  - docs/_reference/upgrading.md
186
188
  - docs/_reference/whats-new-in-0-1.md
187
189
  - docs/_reference/whats-new-in-0-2.md
190
+ - docs/_reference/whats-new-in-0-3.md
188
191
  - docs/index.md
189
192
  - lib/generators/ruby_llm/ag_ui/install_generator.rb
190
193
  - lib/generators/ruby_llm/ag_ui/templates/create_ruby_llm_ag_ui_runs.rb.tt
@@ -196,6 +199,7 @@ files:
196
199
  - lib/ruby_llm/ag_ui/emitter.rb
197
200
  - lib/ruby_llm/ag_ui/error.rb
198
201
  - lib/ruby_llm/ag_ui/error_sanitizer.rb
202
+ - lib/ruby_llm/ag_ui/immutable.rb
199
203
  - lib/ruby_llm/ag_ui/input.rb
200
204
  - lib/ruby_llm/ag_ui/interrupt.rb
201
205
  - lib/ruby_llm/ag_ui/mapper.rb