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 +4 -4
- data/CHANGELOG.md +27 -0
- data/README.md +1 -1
- data/docs/_advanced/provider-verification.md +70 -0
- data/docs/_advanced/security-and-deployment.md +2 -2
- data/docs/_core_features/approval-and-resume.md +9 -0
- data/docs/_core_features/reconnects.md +15 -0
- data/docs/_getting_started/configuration.md +7 -2
- data/docs/_getting_started/whats-new-in-0-4.md +57 -0
- data/docs/_reference/api-reference.md +2 -1
- data/docs/_reference/compatibility.md +76 -8
- data/docs/_reference/public-api.md +102 -0
- data/docs/_reference/release-history.md +2 -1
- data/docs/_reference/upgrading.md +19 -27
- data/docs/{_getting_started → _reference}/whats-new-in-0-3.md +2 -1
- data/lib/ruby_llm/ag_ui/configuration.rb +14 -11
- data/lib/ruby_llm/ag_ui/emitter.rb +1 -1
- data/lib/ruby_llm/ag_ui/error_sanitizer.rb +1 -1
- data/lib/ruby_llm/ag_ui/immutable.rb +34 -0
- data/lib/ruby_llm/ag_ui/input.rb +5 -4
- data/lib/ruby_llm/ag_ui/interrupt.rb +10 -10
- data/lib/ruby_llm/ag_ui/mapper.rb +8 -0
- data/lib/ruby_llm/ag_ui/message_parser.rb +1 -1
- data/lib/ruby_llm/ag_ui/reconnect.rb +1 -1
- data/lib/ruby_llm/ag_ui/resume.rb +1 -1
- data/lib/ruby_llm/ag_ui/resume_entry.rb +3 -3
- data/lib/ruby_llm/ag_ui/run.rb +7 -6
- data/lib/ruby_llm/ag_ui/run_record.rb +10 -10
- data/lib/ruby_llm/ag_ui/store/memory_store.rb +3 -3
- data/lib/ruby_llm/ag_ui/validation.rb +1 -1
- data/lib/ruby_llm/ag_ui/version.rb +1 -1
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7e2fef2885431f82e496f0e1682bbd6d11ed036a6dcda941353e5f70d22e9589
|
|
4
|
+
data.tar.gz: 06d1631a5e98ecfea25a53dd9ea9f40c235f2fea871fcc7d4fb9e2b707596439
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
@@ -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
|
|
47
|
-
|
|
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"` |
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
27
|
-
| RubyLLM | `>= 2.1`, `< 3.0` | 2.1.1
|
|
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.
|
|
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
|
-
- **
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
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.
|
|
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.
|
|
18
|
+
## Move from 0.3 to 0.4
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Update your Gemfile and dependency:
|
|
21
21
|
|
|
22
22
|
```ruby
|
|
23
|
-
gem 'ruby_llm-ag_ui', '~> 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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
JSON
|
|
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
|
-
|
|
56
|
-
|
|
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.
|
|
@@ -10,8 +10,8 @@ module RubyLLM
|
|
|
10
10
|
# config.logger = Rails.logger
|
|
11
11
|
# end
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
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
|
-
|
|
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 =
|
|
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
|
data/lib/ruby_llm/ag_ui/input.rb
CHANGED
|
@@ -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
|
|
21
|
-
#
|
|
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&.
|
|
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&.
|
|
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
|
|
data/lib/ruby_llm/ag_ui/run.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
56
|
+
@record = reservation.record
|
|
57
|
+
return execute(@record, to) if reservation.created?
|
|
56
58
|
|
|
57
|
-
reconnect(
|
|
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
|
|
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&.
|
|
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
|
|
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: 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-
|
|
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-
|
|
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
|