ruby_llm-ag_ui 0.1.0 → 0.2.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: ab7bc5eeccd9381e134cd795b20b0815aed665ac3f4371d29503bed969811410
4
- data.tar.gz: 5991ee6bec49c64e3dd5ac0f8f9ccd18abe7c5f9aab55cb5493b232cff088cbe
3
+ metadata.gz: d8529d394d1ef5d52bc73952e047ee5303634af7aa7738cc894acc8dde378f8b
4
+ data.tar.gz: ed676da00af762d17fbf6d1a743b995f03d1e7744aba9afb985b95c20bedf93f
5
5
  SHA512:
6
- metadata.gz: 205524fd5c5cbcd214da1f98c2c2b3678e0961b417ee598803741b577a7c7400be2e51b8d78716905cb14b2fb386d3ee46a8a3e157947cbf06afe6eaf8d01669
7
- data.tar.gz: 8680a5a03eacc9cfa8d3920db2be725d5f148befd24125a9b6875312307c4207528f69b527dc294f7e064a2c4f33d0c5bf3666318d3ea81f5927089f6408ae78
6
+ metadata.gz: 34df865e3f9dd71cbf573ad7a89d19259d16931685cb153df6aab6e989d92be68407f6207e654fc44f615b625a840a90a44e6c4e725e5781e976e253d6744457
7
+ data.tar.gz: 40c5c7fd607e2ce6ccd54e53e6e6e8c2482bf7645d3155e4cdf0eedb95c58f3337bb481650a44c902863629a52bc70cd03dd4224ecaad2ab0546a947f633a0e1
data/CHANGELOG.md CHANGED
@@ -6,6 +6,25 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.0] - 2026-10-19
10
+
11
+ ### Added
12
+
13
+ - Opt-in tool progress as redacted `ACTIVITY_SNAPSHOT` events, with stable
14
+ per-call identity and per-run or global configuration.
15
+ - Installed-artifact host checks for generated migrations, RubyLLM model setup,
16
+ actor-scoped chat persistence, and Ruby/Rails run paths.
17
+ - A pinned official AG-UI client demo for Ruby and Rails, with browser workflow
18
+ tests, mobile screenshots, and schema validation of consumed events.
19
+
20
+ ### Fixed
21
+
22
+ - Reconnect an exact completed resume request without redeeming its approval
23
+ again. Changed input still conflicts, and a new run cannot reuse an answered
24
+ interrupt.
25
+ - Supply the required actor scope in the opt-in live-provider check and exercise
26
+ its run setup offline, before provider verification.
27
+
9
28
  ## [0.1.0] - 2026-10-18
10
29
 
11
30
  ### Added
data/CONTRIBUTING.md CHANGED
@@ -7,8 +7,7 @@ with tests are the easiest to accept.
7
7
 
8
8
  ```bash
9
9
  bundle install
10
- bundle exec rspec
11
- bundle exec rubocop
10
+ bin/check
12
11
  ```
13
12
 
14
13
  ## Ground rules
@@ -33,15 +32,40 @@ bundle exec rubocop
33
32
 
34
33
  ## Tests
35
34
 
35
+ Run focused specs first, then `bin/check`. For client or demo changes, also run
36
+ `bin/check-browser` and inspect its screenshots. The browser suite uses synthetic
37
+ fixtures, a real AG-UI client, and a temporary local server. Keep artifacts in
38
+ ignored `tmp/`. Use `bin/check --package --browser` for the full local check,
39
+ or `bin/check-browser --grep "rails: approval"` for one browser workflow.
40
+ The package probe installs the artifact outside the checkout using available
41
+ development dependencies; `bin/verify-package` separately tests clean dependency
42
+ resolution. Shut down all task-owned processes when the check finishes.
43
+
36
44
  Every behavior change needs a test, and lifecycle, identity and security changes
37
45
  need their negative case too. The acceptance IDs (FR-01..FR-08) are referenced
38
46
  in spec descriptions. Offline CI uses fakes; live provider tests are opt-in and
39
47
  never required:
40
48
 
41
49
  ```bash
42
- RUBY_LLM_LIVE=1 OPENAI_API_KEY=... bundle exec rspec spec/live
50
+ RUBY_LLM_LIVE=1 bundle exec rspec spec/live
43
51
  ```
44
52
 
53
+ Load provider credentials through your environment or an untracked env file,
54
+ never through command-line arguments. See the
55
+ [compatibility guide](https://rubyllm-ag-ui.wangyaodi.com/compatibility/) for
56
+ what each check establishes.
57
+
58
+ ## Documentation
59
+
60
+ - Keep README to the project introduction, installation, one example, and links.
61
+ - Keep usage, configuration, compatibility, and API details in `docs/`, the
62
+ source of the documentation site. Link to a guide instead of copying it.
63
+ - Keep contributor rules here, shipped changes in [CHANGELOG.md](CHANGELOG.md),
64
+ and proposed work in [ROADMAP.md](ROADMAP.md).
65
+ - Change a public API's guide in the same commit. Follow [AGENTS.md](AGENTS.md)
66
+ and build with `docs/bin/build.sh` before submitting documentation changes.
67
+ - Keep example READMEs specific to running or adapting that example.
68
+
45
69
  ## Commit style
46
70
 
47
71
  Conventional Commits (`feat:`, `fix:`, `docs:`, `test:`, `chore:`, `refactor:`),
data/README.md CHANGED
@@ -2,145 +2,69 @@
2
2
 
3
3
  [![CI](https://github.com/yorzi/ruby_llm-ag-ui/actions/workflows/ci.yml/badge.svg)](https://github.com/yorzi/ruby_llm-ag-ui/actions/workflows/ci.yml)
4
4
 
5
- Run [RubyLLM](https://rubyllm.com) 2.x chats as [AG-UI](https://github.com/ag-ui-protocol/ag-ui) 1.0 agent runs.
6
-
7
- `ruby_llm-ag_ui` turns one RubyLLM chat turn into one AG-UI run: it validates the
8
- inbound `RunAgentInput`, maps RubyLLM streaming, tool and lifecycle behavior onto
9
- schema-valid AG-UI events, turns `requires_approval` pauses into interrupt
10
- outcomes with host-authorized resume, reports `Chat#cancel` as the cancelled
11
- outcome, and writes the stream as Server-Sent Events. A pure-Ruby core comes
12
- with an optional Rails adapter.
13
-
14
- > **Unofficial community project.** This gem is not affiliated with, endorsed
15
- > by, or maintained by the RubyLLM maintainers or the AG-UI protocol
16
- > maintainers. `RubyLLM::` and "AG-UI" appear here because this gem extends
17
- > those projects, not because it speaks for them.
18
-
19
- ## Why
20
-
21
- If your agent already runs on RubyLLM and you want an AG-UI client (CopilotKit,
22
- `@ag-ui/client`, or another AG-UI consumer) in front of it, you currently have
23
- to hand-map RubyLLM callbacks, approval pauses and cancellation onto the AG-UI
24
- run lifecycle, and you have to parse and authorize `RunAgentInput` yourself:
25
- the community Ruby SDK builds outbound events but has no inbound parser. The
26
- usual results are wrong terminal events, duplicate runs on retry, approvals that
27
- are not bound to the call they approve, and cancellations reported as success.
28
- This gem removes that mapping work.
29
-
30
- ## What it does and does not do
31
-
32
- It owns the RubyLLM → AG-UI mapping, input parsing, event ordering, run identity
33
- and terminal correctness. It is **not** an AG-UI core SDK, a schema fork, a chat
34
- UI, a CopilotKit runtime clone, or a second agent framework. RubyLLM owns the
35
- agent loop, tool execution, approval pauses and cancellation checkpoints. Your
36
- application owns identity, authorization, which tools and model a run may use,
37
- and persistence.
38
-
39
- ## Status
40
-
41
- Version `0.1.0` is the first release. The pure-Ruby core, run store, approval/resume, cancel
42
- handling and the Rails adapter are implemented and covered by an offline test
43
- suite. Compatibility claims for real AG-UI clients and live providers are
44
- tracked in the [compatibility guide](https://rubyllm-ag-ui.wangyaodi.com/compatibility/) and are
45
- **not** asserted until recorded there.
46
-
47
- Read the [documentation site](https://rubyllm-ag-ui.wangyaodi.com/) for the
48
- guides, protocol coverage, compatibility matrix and security model. Its source
49
- is in [docs/](https://github.com/yorzi/ruby_llm-ag-ui/tree/main/docs).
50
-
51
- ## Installation
5
+ Connect [RubyLLM](https://rubyllm.com) chats to [AG-UI](https://docs.ag-ui.com)
6
+ clients over Server-Sent Events. The gem handles streaming text, tool calls, approval and resume,
7
+ cancellation outcomes, and run identity. Use it from plain Ruby or with the
8
+ optional Rails adapter.
52
9
 
53
- ```ruby
54
- # Gemfile
55
- gem 'ruby_llm-ag_ui', '~> 0.1.0'
56
- ```
10
+ Your application provides authentication, tool permissions, chat history, and
11
+ the model. This is an unofficial community project, independent of the
12
+ RubyLLM and AG-UI maintainers.
57
13
 
58
- ```bash
59
- bundle install
14
+ **[Documentation](https://rubyllm-ag-ui.wangyaodi.com/)** ·
15
+ [Getting started](https://rubyllm-ag-ui.wangyaodi.com/getting-started/) ·
16
+ [Rails guide](https://rubyllm-ag-ui.wangyaodi.com/rails-integration/) ·
17
+ [Compatibility](https://rubyllm-ag-ui.wangyaodi.com/compatibility/)
18
+
19
+ ## Install
20
+
21
+ ```ruby
22
+ gem 'ruby_llm-ag_ui', '~> 0.2.0'
60
23
  ```
61
24
 
62
- Requires Ruby >= 3.2, RubyLLM >= 2.1 and `ag-ui-protocol` ~> 0.2.0.
63
- Runtime dependencies also select patched `cgi`, `resolv`, `uri` and `zlib` gems.
25
+ Requires Ruby >= 3.2, RubyLLM >= 2.1 and < 3.0, and `ag-ui-protocol` 0.2.x.
26
+ Run `bundle install` after adding the gem.
64
27
 
65
- ## Quick start (pure Ruby)
28
+ ## Stream a run
66
29
 
67
- The host authenticates, authorizes and builds the chat; the gem writes the
68
- stream.
30
+ Pass a chat authorized for the current actor, a shared run store, and an
31
+ IO-like response stream from your application:
69
32
 
70
33
  ```ruby
71
- require 'ruby_llm'
72
34
  require 'ruby_llm/ag_ui'
73
35
 
74
- input = RubyLLM::AgUI::Input.parse(request_body) # bounds, validates, digests
75
- chat = host_chat_for(actor, input.thread_id) # your RubyLLM::Chat
36
+ input = RubyLLM::AgUI::Input.parse(request_body)
37
+ chat = authorized_chat_for(current_user, input.thread_id)
76
38
 
77
39
  RubyLLM::AgUI::Run.new(
78
40
  chat: chat,
79
41
  input: input,
80
- scope: "tenant:#{tenant.id}:actor:#{actor.id}", # from authenticated identity
42
+ scope: "actor:#{current_user.id}",
81
43
  store: run_store
82
- ).stream(to: sse_io)
83
- ```
84
-
85
- `scope` is what the run is reserved under: it must be a non-empty String the host
86
- builds from authenticated identity, never anything from the request body.
87
-
88
- `examples/pure_ruby/app.rb` is a runnable Rack endpoint that streams a complete
89
- run (a tool call, an approval pause and the run that answers it) with no
90
- provider and no credentials (`rake example`, or
91
- `rackup examples/pure_ruby/config.ru`). Set `RUBY_LLM_AG_UI_EXAMPLE_MODEL` to
92
- drive a real `RubyLLM::Chat` through the same code.
93
-
94
- ## Rails
95
-
96
- The optional adapter provides a controller concern, a durable run store keyed
97
- by the actor's scope, and reconnect snapshots. It loads only when
98
- `Rails::Railtie` is already defined.
99
-
100
- ```ruby
101
- class AgentRunsController < ApplicationController
102
- include RubyLLM::AgUI::Rails::Streaming
103
-
104
- def create
105
- stream_ag_ui_run { |input| current_user.chats.find_or_create_by!(ag_ui_thread_id: input.thread_id) }
106
- end
107
-
108
- def ag_ui_scope
109
- "actor:#{current_user.id}"
110
- end
111
- end
44
+ ).stream(to: response_stream)
112
45
  ```
113
46
 
114
- See the [Rails integration guide](https://rubyllm-ag-ui.wangyaodi.com/rails-integration/) and `examples/rails_8`.
47
+ Build `scope` from authenticated identity. See the
48
+ [streaming guide](https://rubyllm-ag-ui.wangyaodi.com/streaming-runs/) for setup
49
+ and the [examples](examples/) for integration code.
115
50
 
116
51
  ## Development
117
52
 
118
- ```bash
53
+ ```sh
119
54
  bundle install
120
55
  bundle exec rspec
121
56
  bundle exec rubocop
122
- rake example # prints a full AG-UI run as SSE
57
+ bundle exec rake example # offline approval and resume demo, no API key needed
123
58
  ```
124
59
 
125
- The suite is offline: fakes instead of a provider, no network, no credentials.
126
- The only test that talks to a model is opt-in:
127
-
128
- ```bash
129
- RUBY_LLM_LIVE=1 OPENAI_API_KEY=... bundle exec rspec spec/live
130
- ```
131
-
132
- ## Documentation
133
-
134
- - [Architecture](https://rubyllm-ag-ui.wangyaodi.com/overview/) - the components, run, and invariants.
135
- - [Protocol coverage](https://rubyllm-ag-ui.wangyaodi.com/protocol-coverage/) - all 31 AG-UI 1.0 event types
136
- and what this gem does with each.
137
- - [Compatibility](https://rubyllm-ag-ui.wangyaodi.com/compatibility/) - what this repository verifies.
138
- - [Rails](https://rubyllm-ag-ui.wangyaodi.com/rails-integration/) - the controller concern, store, and transport.
139
- - [Security](https://rubyllm-ag-ui.wangyaodi.com/security-and-deployment/) - the threat model and deployment checklist.
140
-
141
- ## License
60
+ The default suite is offline. Real client and provider verification is tracked
61
+ in the [compatibility guide](https://rubyllm-ag-ui.wangyaodi.com/compatibility/).
62
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development and documentation rules,
63
+ [ROADMAP.md](ROADMAP.md) for the proposed path to 1.0, and
64
+ [CHANGELOG.md](CHANGELOG.md) for shipped changes.
142
65
 
143
- MIT. See [LICENSE.txt](LICENSE.txt). Read the [third-party notices](https://rubyllm-ag-ui.wangyaodi.com/third-party-notices/).
66
+ ## License and security
144
67
 
145
- This is an unofficial community project and is not affiliated with the RubyLLM or
146
- AG-UI maintainers. Report security issues privately. See [SECURITY.md](SECURITY.md).
68
+ [MIT](LICENSE.txt). See the
69
+ [third-party notices](https://rubyllm-ag-ui.wangyaodi.com/third-party-notices/)
70
+ and report vulnerabilities through [SECURITY.md](SECURITY.md).
@@ -0,0 +1,96 @@
1
+ ---
2
+ layout: default
3
+ title: Client Example
4
+ nav_order: 3
5
+ description: Run a real AG-UI browser client against offline Ruby and Rails endpoints.
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 offline client example.
16
+ * Which interactions it verifies with the official client.
17
+ * Which responsibilities remain with a real host application.
18
+
19
+ ## Run the Example
20
+
21
+ From the repository, install the development dependencies and build the client:
22
+
23
+ ```sh
24
+ bundle install
25
+ npm ci
26
+ npm run build:demo
27
+ bundle exec ruby examples/client/server.rb
28
+ ```
29
+
30
+ Open `http://127.0.0.1:3199`. Stop the server with `Ctrl-C` when finished.
31
+ The host binds loopback and serves only the demo HTML and its locally bundled
32
+ JavaScript. It creates and removes a temporary SQLite database on each run.
33
+ No CDN, provider account, or API key is needed.
34
+
35
+ Choose **Plain Ruby** or **Rails + SQLite**, then run a turn. The official
36
+ `@ag-ui/client` package consumes the SSE stream. The scripted host retains one
37
+ chat per fixture actor and thread, so approval resumes the original tool call
38
+ and later turns use the same conversation.
39
+
40
+ ## Try the Interactions
41
+
42
+ The tool scenario pauses for approval. Approve it to run a synthetic order
43
+ cancellation once, or deny it to leave the order unchanged. Tool results stay
44
+ redacted by default. Approved tools also publish synthetic progress counts;
45
+ the host policy withholds progress messages. The text, sanitized failure, and cancelled scenarios show
46
+ the other run outcomes.
47
+
48
+ Use **Reconnect completed run** after a successful turn. The client sends the
49
+ original input and receives the recorded outcome and snapshot; no new model
50
+ turn or text delta is produced.
51
+
52
+ ## Check in a Browser
53
+
54
+ ```sh
55
+ bin/check-browser
56
+ ```
57
+
58
+ This runs Playwright with Chrome or its installed Chromium. On Linux, install
59
+ the browser first with `npx playwright install --with-deps chromium`. Set
60
+ `CHROME_BIN` to select another local Chrome executable.
61
+
62
+ The check starts its own server, runs desktop and 390px mobile scenarios, saves
63
+ screenshots and consumed events under ignored `tmp/browser/`, and stops the
64
+ server and browser. It validates the captured events against the pinned JSON
65
+ schema. Failures retain traces and screenshots. Inspect those artifacts before
66
+ retrying; a screenshot alone is not an assertion.
67
+
68
+ ## Verify an Installed Gem
69
+
70
+ ```sh
71
+ bin/check --package
72
+ ```
73
+
74
+ The package probe builds a gem, installs that artifact outside the checkout,
75
+ and checks both run paths, generated migration, approval, progress, and
76
+ reconnect. It also generates RubyLLM's models and tables, applies the Rails
77
+ example migrations in order, and checks actor-scoped chat persistence. It uses
78
+ available development dependencies. Use `bin/verify-package --rails --audit`
79
+ after `bundle exec rake build` to resolve dependencies in a clean gem home.
80
+
81
+ ## Adapt to a Real Host
82
+
83
+ The example's actor header is a synthetic identity selector. It does not
84
+ provide authentication, CSRF protection, or durable chat history. The run store
85
+ uses SQLite, but the scripted chat objects live in one process and disappear
86
+ when that process stops. A real host authorizes the thread and the next user
87
+ turn, persists its RubyLLM chat, and rechecks tool permission before execution.
88
+
89
+ The demo and browser suite make no live provider call. Read
90
+ [Compatibility]({% link _reference/compatibility.md %}) for the exact versions
91
+ and evidence boundaries.
92
+
93
+ ## Next Steps
94
+
95
+ * [Rails Integration]({% link _advanced/rails-integration.md %}) - install the adapter in your application.
96
+ * [Approval and Resume]({% link _core_features/approval-and-resume.md %}) - inspect the resume contract.
@@ -52,7 +52,7 @@ responsibility.
52
52
  - **Redeclare a run id.** The same identity with a *different* input is refused
53
53
  as `RunConflictError` (409 in the Rails adapter) rather than executed
54
54
  ("refuses the same identity with a changed input as a conflict").
55
- - **Run the same tool twice.** A resume is recorded once; a second answer to the
55
+ - **Run the same tool twice.** A resume is recorded once; a new run that answers the
56
56
  same interrupt is refused without executing the tool ("refuses a decision
57
57
  another resume already recorded, without executing the tool").
58
58
  - **Widen what an approval covers.** An interrupt pins the tool call and the
@@ -29,6 +29,12 @@ The store reserves a run before any provider work. It keys the record by authent
29
29
 
30
30
  Reconnect does not replay text or tool deltas. Replaying deltas could display text twice or repeat a side effect. The host can publish a state snapshot for a finished run, but the gem does not reconstruct conversation history.
31
31
 
32
+ The same rule applies to a completed approval-resume run. Resend its exact
33
+ input, including `resume`, with the original run id to receive the recorded
34
+ outcome without applying the decision again. A new run id that tries to answer
35
+ an already answered interrupt is refused. Changing the input under an existing
36
+ run id is a conflict.
37
+
32
38
  ## Choose a Store
33
39
 
34
40
  `MemoryStore` is suitable for tests and a single process. Its records disappear on restart and are not shared between workers. The Rails adapter's `ActiveRecordStore` persists records and redeems interrupts under a database lock.
@@ -45,5 +45,6 @@ The run record reaches a terminal state before the terminal event is written. If
45
45
  ## Next Steps
46
46
 
47
47
  * [Validating Input]({% link _core_features/input-validation.md %}) - parse untrusted request data.
48
+ * [Tool Progress]({% link _core_features/tool-progress.md %}) - publish local tool updates.
48
49
  * [Approval and Resume]({% link _core_features/approval-and-resume.md %}) - answer an interrupt on a new run.
49
50
  * [Reconnects and Run Identity]({% link _core_features/reconnects.md %}) - serve a completed run again.
@@ -0,0 +1,90 @@
1
+ ---
2
+ layout: default
3
+ title: Tool Progress
4
+ nav_order: 5
5
+ description: Publish local tool progress as redacted AG-UI activity snapshots.
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 enable tool progress globally or for one run.
16
+ * How progress snapshots identify a tool call and replace earlier values.
17
+ * How to choose which progress values reach the client.
18
+
19
+ ## Enable Progress
20
+
21
+ Long tools can report their work before the result arrives. Enable progress
22
+ when your client handles AG-UI activity messages:
23
+
24
+ ```ruby
25
+ RubyLLM::AgUI.configure do |config|
26
+ config.emit_tool_progress = true
27
+ end
28
+ ```
29
+
30
+ The default is `false`. You can override it on one run with
31
+ `Run.new(chat: chat, input: input, scope: scope, store: store,
32
+ emit_tool_progress: true)`. Set the per-run option to `false` to disable a global
33
+ opt-in. The Rails concern uses the global setting.
34
+
35
+ The gem listens to RubyLLM's `after_tool_progress(tool_call, progress)` callback.
36
+ Your RubyLLM tool reports progress through its own `progress` method. The gem
37
+ publishes only values that the tool reports; it does not estimate a percentage
38
+ or make up a total.
39
+
40
+ ## Choose a Redaction Policy
41
+
42
+ Progress can include private messages. Its values pass through the configured
43
+ redactor with kind `:tool_progress`. The default replaces each reported value
44
+ with `[redacted]`, including numeric values. The tool call identifier remains
45
+ available so the client can associate the activity with the existing call.
46
+
47
+ For example, a host can publish counts while withholding messages:
48
+
49
+ ```ruby
50
+ RubyLLM::AgUI.configure do |config|
51
+ config.emit_tool_progress = true
52
+ config.redact = lambda do |kind, payload|
53
+ if kind == :tool_progress
54
+ payload.slice('value', 'total')
55
+ else
56
+ RubyLLM::AgUI::Redaction.default(kind, payload)
57
+ end
58
+ end
59
+ end
60
+ ```
61
+
62
+ Use `Redaction.method(:none)` only for host-approved public data. The same
63
+ policy still protects tool results with kind `:tool_result`.
64
+
65
+ ## Read an Activity Snapshot
66
+
67
+ With counts approved by the host, a tool update can produce:
68
+
69
+ ```json
70
+ {"type":"ACTIVITY_SNAPSHOT","messageId":"run-1:progress:call-1","activityType":"TOOL_PROGRESS","content":{"toolCallId":"call-1","progress":{"value":1,"total":3}},"replace":true}
71
+ ```
72
+
73
+ Each tool call has one activity id per run. Later updates replace that
74
+ activity's content. Unknown fields are absent: a message-only update has no
75
+ `value` or `total`. Empty progress updates produce no event.
76
+
77
+ Snapshots do not open another tool call, change an approval, or determine the
78
+ run outcome. Updates for a tool whose result has arrived are ignored, as are
79
+ callbacks after the run closes. Concurrent callbacks and run finalization use
80
+ the same lock so progress cannot be written after the terminal event.
81
+
82
+ Progress is not retained for reconnect. A completed-run reconnect serves its
83
+ recorded outcome and optional state snapshot, without replaying activity,
84
+ text, or tool deltas. No `ACTIVITY_DELTA` events are produced.
85
+
86
+ ## Next Steps
87
+
88
+ * [Client Example]({% link _advanced/client-example.md %}) - try a real client with offline tools.
89
+ * [Configuration]({% link _getting_started/configuration.md %}) - choose run defaults.
90
+ * [Protocol Coverage]({% link _reference/protocol-coverage.md %}) - inspect supported events.
@@ -32,12 +32,13 @@ p RubyLLM::AgUI.config.to_h[:redact]
32
32
 
33
33
  ## Settings
34
34
 
35
- The global configuration supplies defaults. `Run.new` can override the store, logger, redaction policy, clock, and interrupt expiry for one run. `MemoryStore` is process-local, so use a durable store when runs can move between workers.
35
+ The global configuration supplies defaults. `Run.new` can override the store, logger, redaction policy, clock, interrupt expiry, and tool progress for one run. `MemoryStore` is process-local, so use a durable store when runs can move between workers.
36
36
 
37
37
  | Setting | Default | Purpose |
38
38
  | --- | --- | --- |
39
39
  | `run_store` | `nil` | Store run records and interrupts. A run needs a store, supplied here or to `Run.new`. |
40
40
  | `redact` | `Redaction.default` | Transform tool results before they reach the stream. |
41
+ | `emit_tool_progress` | `false` | Publish tool progress as redacted activity snapshots. |
41
42
  | `logger` | `nil` | Receive host-side diagnostics without prompts or raw tool payloads. |
42
43
  | `protocol_version` | `"1.0"` | Value reported by `RUN_STARTED`. |
43
44
  | `max_input_bytes` | `256_000` | Maximum request body size. |
@@ -0,0 +1,53 @@
1
+ ---
2
+ layout: default
3
+ title: "What's New in 0.2"
4
+ nav_order: 0
5
+ description: Verify real AG-UI client workflows and opt in to redacted tool progress.
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 publish tool progress without exposing private messages.
16
+ * How to run the maintained browser client example.
17
+ * Which client checks run offline and which provider checks remain separate.
18
+
19
+ ## Publish Tool Progress
20
+
21
+ Enable activity snapshots when your client supports them:
22
+
23
+ ```ruby
24
+ RubyLLM::AgUI.configure do |config|
25
+ config.emit_tool_progress = true
26
+ end
27
+ ```
28
+
29
+ Progress is off by default. Each tool call has a stable activity id and later
30
+ snapshots replace earlier values. Values pass through the host redaction policy;
31
+ late callbacks cannot write after the run closes. See
32
+ [Tool Progress]({% link _core_features/tool-progress.md %}).
33
+
34
+ ## Run the Official Client Locally
35
+
36
+ ```sh
37
+ bundle install
38
+ npm ci
39
+ bin/check-browser
40
+ ```
41
+
42
+ The maintained example connects a pinned official `@ag-ui/client` to plain Ruby
43
+ and Rails endpoints. It covers text, a second turn, approval/denial, progress,
44
+ completed-run reconnect, sanitized failure, and cancellation outcomes. The Rails
45
+ endpoint uses a generated migration and temporary SQLite database. See
46
+ [Client Example]({% link _advanced/client-example.md %}).
47
+
48
+ Browser screenshots, event traces, and schema validation are local fixture
49
+ evidence. They do not establish live provider behavior, all-client compatibility,
50
+ or remote CI success. The opt-in live spec now shares an actor-scoped setup
51
+ with an offline harness check. See
52
+ [Compatibility]({% link _reference/compatibility.md %}) and
53
+ [Upgrading]({% link _reference/upgrading.md %}) before updating your application.
@@ -21,15 +21,16 @@ dependency does.
21
21
 
22
22
  ## Supported versions
23
23
 
24
- | Component | Range | Verified |
24
+ | Component | Range | Evidence |
25
25
  |---|---|---|
26
- | Ruby | `>= 3.2` | 3.2, 3.3, 3.4 in CI; 3.3.5 locally |
26
+ | Ruby | `>= 3.2` | CI passes on 3.2.11, 3.3.12, and 3.4.11; 3.3.5 checked locally |
27
27
  | RubyLLM | `>= 2.1`, `< 3.0` | 2.1.1 (callback, approval and cancellation behavior read from its source) |
28
28
  | `ag-ui-protocol` | `~> 0.2.0` | 0.2.0 |
29
- | Rails (adapter only) | `railties`, `actionpack`, `activerecord` `>= 8.0` | 8.0.x and 8.1.x in CI |
29
+ | Rails (adapter only) | `railties`, `actionpack`, `activerecord` `>= 8.0` | CI passes on 8.0.5.1 and 8.1.4 with Ruby 3.3/3.4; 8.1.4 checked locally |
30
30
  | AG-UI schema | pinned commit `89cedd3fd549feeee41e50e1d7c6d373e549241a` | vendored, SHA-256 recorded in [AG-UI Protocol Coverage]({% link _reference/protocol-coverage.md %}) |
31
31
 
32
- The pure-Ruby core depends only on `ruby_llm` and `ag-ui-protocol`. Rails,
32
+ The pure-Ruby core uses `ruby_llm` and `ag-ui-protocol`, plus the patched
33
+ standard-library gems required by the gemspec. Rails,
33
34
  `ToolViews`, `OpenUI` and `OddlyDock` are not runtime dependencies, and the Rails
34
35
  adapter is only loaded when `Rails::Railtie` is already defined.
35
36
 
@@ -52,15 +53,40 @@ adapter is only loaded when `Rails::Railtie` is already defined.
52
53
 
53
54
  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.
54
55
 
56
+ ## Recorded Client Verification
57
+
58
+ On 2026-10-19, the local 0.2.0 candidate passes the maintained browser workflows
59
+ with `@ag-ui/client` 1.0.3, Playwright 1.64.1, and Chrome 155.0.8059.82 on macOS.
60
+ Both the plain Ruby and Rails endpoints use scripted chats. Checks cover text,
61
+ a second turn, approval/denial, incremental progress before completion, default
62
+ tool-result redaction, sanitized failure, cancellation, and exact completed-run
63
+ reconnect, including a completed resume run. Captured events also validate
64
+ against the pinned schema. The progress demo publishes synthetic counts through
65
+ a host policy and withholds messages; unit tests check default progress redaction.
66
+
67
+ Run `bin/check-browser` to reproduce and inspect screenshots and events under
68
+ `tmp/browser/`. `bin/check --package` separately installs the built artifact
69
+ outside the checkout, checks both endpoints, and generates RubyLLM models and
70
+ ordered example migrations to verify scoped chat persistence. This package
71
+ probe uses available development dependencies; `bin/verify-package` checks a
72
+ clean gem home with freshly resolved dependencies.
73
+
74
+ The [2026-10-19 CI run](https://github.com/yorzi/ruby_llm-ag-ui/actions/runs/39179834370)
75
+ passes all ten jobs at `4aba0dc`: the Ruby and Rails matrices above, lint,
76
+ installed-host verification, and the browser suite on Ubuntu 24.04.
77
+ The browser job uses Playwright's Chromium 156.0.8078.4 and passes all 12
78
+ scenarios, with 154 consumed events validated against the schema. Its desktop
79
+ progress and mobile denial screenshots were also inspected. These remain
80
+ synthetic checks; no live provider session is recorded.
81
+
55
82
  ## What is not asserted
56
83
 
57
- - **Real AG-UI clients** (CopilotKit, `@ag-ui/client`, any other consumer). No
58
- client has been recorded against this producer. The events are schema-valid and
59
- the ordering rules the protocol documents are enforced, but "a specific client
60
- renders this correctly" is a separate claim, and it is not made here.
84
+ - **Other clients or versions.** CopilotKit and client versions other than the
85
+ recorded `@ag-ui/client` version have not been verified. Schema conformance
86
+ does not establish that every UI renders the events correctly.
61
87
  - **Live providers.** `spec/live/live_run_spec.rb` is the opt-in check
62
- (`RUBY_LLM_LIVE=1`, plus a provider key); it is skipped in the default suite.
63
- Provider behavior in 2.1.1 is derived from its source, not from a recorded
88
+ (`RUBY_LLM_LIVE=1`, plus a provider key); its provider context is skipped in the default suite. The same run setup
89
+ is exercised separately with an offline chat. Provider behavior in 2.1.1 is derived from its source, not from a recorded
64
90
  session.
65
91
  - **Every RubyLLM patch release in the `>= 2.1, < 3.0` range.** The upper bound
66
92
  is enforced by the gemspec; the verified version is 2.1.1.
@@ -83,9 +109,9 @@ These are choices, not gaps, and each has a reason:
83
109
  repeating it would duplicate the call in the client's conversation.
84
110
  - **`MESSAGES_SNAPSHOT` is never produced.** History is the host's, and the gem
85
111
  will not re-id it. `STATE_SNAPSHOT` is produced, on a reconnect.
86
- - **No `ACTIVITY_*`, `REASONING_*`, `RAW` or `SUBAGENT_*` events.** RubyLLM 2.x
87
- exposes no data for them on the supported path, and the gem does not invent
88
- events. A thinking-only chunk produces nothing.
112
+ - **Tool progress is opt-in.** `ACTIVITY_SNAPSHOT` carries local tool progress
113
+ when enabled. `ACTIVITY_DELTA`, `REASONING_*`, `RAW` and `SUBAGENT_*` remain
114
+ unproduced. A thinking-only chunk produces nothing.
89
115
  - **Message ids are the gem's** (`run-<id>:a<n>`), scoped to the run, because
90
116
  RubyLLM streaming chunks carry no message id.
91
117
  - **Steps are per phase** (`completion`, `tool`), named `step-<n>` per run.
@@ -66,11 +66,12 @@ Legend:
66
66
  | `TOOL_CALL_RESULT` | tool | The tool result, after the host redaction policy. |
67
67
  | `STATE_SNAPSHOT` | state | On a reconnect: the state the host gave the run. |
68
68
 
69
- ### Optional (1)
69
+ ### Optional (2)
70
70
 
71
71
  | Event type | Family | When |
72
72
  |---|---|---|
73
73
  | `CUSTOM` | raw/custom | Only safe, documented host extensions. Nothing in the gem emits one today. |
74
+ | `ACTIVITY_SNAPSHOT` | activity | Local tool progress when `emit_tool_progress` is enabled, after host redaction. |
74
75
 
75
76
  ### Host-owned, not produced (2)
76
77
 
@@ -79,14 +80,13 @@ Legend:
79
80
  | `MESSAGES_SNAPSHOT` | state | The conversation belongs to the host. Producing one would mean re-synthesizing message ids the gem did not mint, which a client would then render as new messages. |
80
81
  | `STATE_DELTA` | state | Deltas describe a change the host made. The gem never invents a patch, and it has no API that publishes one. |
81
82
 
82
- ### Not produced (15)
83
+ ### Not produced (14)
83
84
 
84
85
  | Event type | Family | Why |
85
86
  |---|---|---|
86
87
  | `TEXT_MESSAGE_CHUNK` | text | Convenience form of the explicit triad; the gem emits `START`/`CONTENT`/`END`. |
87
88
  | `TOOL_CALL_CHUNK` | tool | Convenience form; the gem emits the explicit triad. |
88
- | `ACTIVITY_SNAPSHOT` | activity | No `ACTIVITY_*` projection yet; RubyLLM's progress signals are not mapped. |
89
- | `ACTIVITY_DELTA` | activity | Same: no progress projection, and no invented patches. |
89
+ | `ACTIVITY_DELTA` | activity | Progress uses full snapshots; no patches are produced. |
90
90
  | `RAW` | raw/custom | The gem never passes provider payloads through. |
91
91
  | `REASONING_START` | reasoning | Private reasoning is not emitted until a host opts in, and there is no opt-in yet. |
92
92
  | `REASONING_MESSAGE_START` | reasoning | A chunk that carries thinking is ignored. |
@@ -99,7 +99,7 @@ Legend:
99
99
  | `SUBAGENT_FINISHED` | subagents | Same. |
100
100
  | `SUBAGENT_ERROR` | subagents | Same. |
101
101
 
102
- Totals: 13 produced + 1 optional + 2 host-owned + 15 not produced = **31**.
102
+ Totals: 13 produced + 2 optional + 2 host-owned + 14 not produced = **31**.
103
103
 
104
104
  ## Terminal events
105
105
 
@@ -172,4 +172,5 @@ must produce no event at all.
172
172
  ## Next Steps
173
173
 
174
174
  * [Streaming Runs]({% link _core_features/streaming-runs.md %}) - follow event order and outcomes.
175
+ * [Tool Progress]({% link _core_features/tool-progress.md %}) - opt in to activity snapshots.
175
176
  * [Compatibility]({% link _reference/compatibility.md %}) - see what this repository verifies.
@@ -11,12 +11,13 @@ description: Review RubyLLM AG-UI releases, newest first, with highlights and up
11
11
  {{ page.description }}
12
12
  {: .fs-6 .fw-300 }
13
13
 
14
- {% if site.docs_unreleased %}This build follows `main`. The latest stable release is `{{ site.version }}`.
14
+ {% if site.docs_unreleased %}This build follows `main` for version `{{ site.version }}` development. Select Stable in the version menu for the published release.
15
15
  {% else %}This documentation describes version `{{ site.version }}`. Use its tag and release notes to identify the exact source.
16
16
  {% endif %}
17
17
 
18
18
  | Release | Date | Highlights |
19
19
  | --- | --- | --- |
20
- | [0.1.0](https://github.com/yorzi/ruby_llm-ag-ui/releases/tag/v0.1.0) | 2026-10-18 | [First release]({% link _getting_started/whats-new-in-0-1.md %}): AG-UI event mapping, approval and resume, cancellation, reconnect, and the optional Rails adapter |
20
+ | [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 _getting_started/whats-new-in-0-2.md %}): opt-in redacted progress, a real-client demo, and completed resume reconnect |
21
+ | [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 |
21
22
 
22
23
  To move an application from one release to the next, follow [Upgrading]({% link _reference/upgrading.md %}).
@@ -20,16 +20,31 @@ After reading this guide, you will know:
20
20
 
21
21
  ```bash
22
22
  bundle exec rake
23
+ npm ci
24
+ bin/check-browser
23
25
  bundle exec bundle-audit check --update
24
26
  bundle exec rake build
27
+ bundle exec ruby scripts/check_package_host.rb --built
25
28
  bundle exec ruby bin/verify-package --rails --audit
26
29
  docs/bin/build.sh
30
+ python3 scripts/check_docs_links.py
27
31
  ```
28
32
 
29
33
  The package check installs dependencies in a fresh temporary gem home and requires the installed artifact from outside the checkout. It verifies the core entrypoint, shipped templates and guides, and input parsing. With `--rails`, it invokes the documented generator and migrates its table up and down in SQLite. With `--audit`, it checks the actually activated core dependencies against the advisory database.
30
34
 
31
35
  These checks make no live provider call. Keep provider and client interoperability evidence in [Compatibility]({% link _reference/compatibility.md %}).
32
36
 
37
+ Install the documentation bundle before building the site: run `bundle install`
38
+ from `docs/`. API HTML and Markdown use the RDoc version locked in that bundle,
39
+ so local and CI builds use the same navigation and parser.
40
+
41
+ ## Review Release Scope
42
+
43
+ Use the repository's [roadmap](https://github.com/yorzi/ruby_llm-ag-ui/blob/main/ROADMAP.md)
44
+ for proposed iterations and the 1.0 release conditions. Guides describe available
45
+ behavior; the roadmap describes planned work. Check the release's conditions
46
+ before preparing its tag.
47
+
33
48
  ## Configure Trusted Publishing
34
49
 
35
50
  Before the first tag, register a pending trusted publisher on RubyGems with gem name `ruby_llm-ag_ui`, GitHub owner `yorzi`, repository `ruby_llm-ag-ui`, workflow `release.yml`, and environment `release`. The workflow uses a short-lived GitHub OIDC identity instead of a stored RubyGems API key.
@@ -15,28 +15,33 @@ After reading this guide, you will know:
15
15
  * How to move between versioned releases.
16
16
  * Where to find changes that need code updates.
17
17
 
18
- ## One Release at a Time
18
+ ## Move from 0.1 to 0.2
19
19
 
20
- Version `0.1.0` is the first release. There is no earlier published version to upgrade from. Start with [Getting Started]({% link _getting_started/getting-started.md %}). For future releases, upgrade one release at a time.
21
-
22
- ## Install Version 0.1
20
+ Change your Gemfile and update the dependency:
23
21
 
24
22
  ```ruby
25
- gem 'ruby_llm-ag_ui', '~> 0.1.0'
23
+ gem 'ruby_llm-ag_ui', '~> 0.2.0'
24
+ ```
25
+
26
+ ```sh
27
+ bundle update ruby_llm-ag_ui
26
28
  ```
27
29
 
28
- If you used an untagged development checkout, review [Configuration]({% link _getting_started/configuration.md %}) and [Rails Integration]({% link _advanced/rails-integration.md %}) before changing the dependency. No compatibility with earlier untagged checkouts is promised.
30
+ The 0.2 release keeps the 0.1 run, store, and Rails APIs. It does not require
31
+ another migration or installer run.
29
32
 
30
- ## Use the Full Generator Command
33
+ Tool progress is off by default. Set `config.emit_tool_progress = true` only
34
+ when your client handles activity snapshots, and review its redaction policy.
35
+ See [Tool Progress]({% link _core_features/tool-progress.md %}).
31
36
 
32
- Untagged development checkouts registered the shorter `ruby_llm:ag_ui` namespace. Version 0.1 registers the documented installation command:
37
+ ## Completed Resume Retries in 0.2
33
38
 
34
- ```bash
35
- # Before, on an untagged checkout
36
- bin/rails generate ruby_llm:ag_ui
39
+ An exact retry of a completed resume run now returns its recorded outcome.
40
+ Keep the original run id and full input, including `resume`. A new run id that
41
+ tries to answer an already answered interrupt is still refused, and changed
42
+ input under the original id is a conflict. No approval or tool is executed again.
37
43
 
38
- # Version 0.1
39
- bin/rails generate ruby_llm:ag_ui:install
40
- ```
44
+ ## Older Development Checkouts
41
45
 
42
- Existing generated tables and initializers do not need to be regenerated for this command-name correction.
46
+ For the initial release's generator namespace correction, read the
47
+ [0.1 upgrading guide](https://github.com/yorzi/ruby_llm-ag-ui/blob/v0.1.0/docs/_reference/upgrading.md).
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  layout: default
3
3
  title: "What's New in 0.1"
4
- nav_order: 0
4
+ nav_order: 1
5
+ parent: Release History
5
6
  description: Use the first RubyLLM AG-UI release to stream, resume, cancel, and reconnect agent runs.
6
7
  ---
7
8
 
@@ -28,6 +28,10 @@ module RubyLLM
28
28
  # RubyLLM::AgUI::Redaction.method(:none), or replace it with your policy.
29
29
  attr_writer :redact
30
30
 
31
+ # Whether runs publish local tool progress as activity snapshots. Off
32
+ # unless the host opts in; progress payloads still pass through redaction.
33
+ attr_accessor :emit_tool_progress
34
+
31
35
  # A Logger used for host-side diagnostics. Never receives prompts, raw
32
36
  # tool payloads or credentials.
33
37
  attr_accessor :logger
@@ -71,6 +75,7 @@ module RubyLLM
71
75
  @interrupt_ttl = 900
72
76
  @clock = -> { Time.now.utc }
73
77
  @allow_client_history = false
78
+ @emit_tool_progress = false
74
79
  end
75
80
 
76
81
  # The redaction policy in force. Never nil: an unset or nil policy falls
@@ -105,7 +110,8 @@ module RubyLLM
105
110
  max_tools: max_tools,
106
111
  max_context: max_context,
107
112
  interrupt_ttl: interrupt_ttl,
108
- allow_client_history: allow_client_history
113
+ allow_client_history: allow_client_history,
114
+ emit_tool_progress: emit_tool_progress
109
115
  }
110
116
  end
111
117
  end
@@ -66,8 +66,8 @@ module RubyLLM
66
66
  'MESSAGES_SNAPSHOT' => [:host_owned, 'Not produced: history is the host\'s; ids are never re-synthesized.'],
67
67
  'STATE_SNAPSHOT' => [:produced, 'Reconnect: the state the host gave the run, never replayed deltas.'],
68
68
  'STATE_DELTA' => [:host_owned, 'Not produced: patches are the host\'s to describe; the gem invents none.'],
69
- 'ACTIVITY_SNAPSHOT' => [:not_produced, 'No ACTIVITY_* projection yet; after_tool_progress is not mapped.'],
70
- 'ACTIVITY_DELTA' => [:not_produced, 'No ACTIVITY_* projection yet; the gem does not invent patches.'],
69
+ 'ACTIVITY_SNAPSHOT' => [:optional, 'Opt-in local tool progress, after host redaction.'],
70
+ 'ACTIVITY_DELTA' => [:not_produced, 'Progress uses full snapshots; the gem does not invent patches.'],
71
71
  'REASONING_START' => [:not_produced, 'Private reasoning is not emitted until a host opts in.'],
72
72
  'REASONING_MESSAGE_START' => [:not_produced, 'Never emitted; a chunk that carries thinking is ignored.'],
73
73
  'REASONING_MESSAGE_CONTENT' => [:not_produced, 'Never emitted; a chunk that carries thinking is ignored.'],
@@ -53,6 +53,7 @@ module RubyLLM
53
53
  @last_message_id = nil
54
54
  @text_emitted = false
55
55
  @calls = {}
56
+ @completed_tools = {}
56
57
  @open_step = nil
57
58
  @step_kind = nil
58
59
  @warned_redaction = false
@@ -95,6 +96,23 @@ module RubyLLM
95
96
  events
96
97
  end
97
98
 
99
+ # Reports a tool's actual progress as a replacing activity snapshot.
100
+ # Each call has one run-scoped activity id. Values pass through the host's
101
+ # :tool_progress redaction policy; the tool call id remains structural.
102
+ # Empty updates and callbacks for finished tools or runs produce nothing.
103
+ def tool_progress(tool_call, progress)
104
+ return [] if @closed || @completed_tools[tool_call.id]
105
+
106
+ payload = { 'value' => progress.value, 'total' => progress.total, 'message' => progress.message }.compact
107
+ return [] if payload.empty?
108
+
109
+ [::AgUiProtocol::Core::Events::ActivitySnapshotEvent.new(
110
+ message_id: "#{@run_id}:progress:#{tool_call.id}", activity_type: 'TOOL_PROGRESS',
111
+ content: { 'toolCallId' => tool_call.id, 'progress' => redact(Redaction::TOOL_PROGRESS, payload) },
112
+ replace: true
113
+ )]
114
+ end
115
+
98
116
  # Closes any open text message, tool call and step, and closes the mapper.
99
117
  #
100
118
  # The caller writes these before the terminal event: AG-UI has no way to
@@ -261,6 +279,7 @@ module RubyLLM
261
279
  # --- tool results --------------------------------------------------------
262
280
 
263
281
  def tool_result_events(message)
282
+ @completed_tools[message.tool_call_id] = true
264
283
  events = close_text_events
265
284
  events.concat(close_tool_calls)
266
285
  events.concat(ensure_step(STEP_TOOL))
@@ -320,7 +339,7 @@ module RubyLLM
320
339
  return if @warned_redaction
321
340
 
322
341
  @warned_redaction = true
323
- @logger&.warn('[ruby_llm_ag_ui] tool results are redacted by default; ' \
342
+ @logger&.warn('[ruby_llm_ag_ui] tool results and progress are redacted by default; ' \
324
343
  'configure RubyLLM::AgUI.config.redact to publish them')
325
344
  end
326
345
 
@@ -25,6 +25,7 @@ module RubyLLM
25
25
 
26
26
  # The kinds a policy is asked about.
27
27
  TOOL_RESULT = :tool_result
28
+ TOOL_PROGRESS = :tool_progress
28
29
 
29
30
  module_function
30
31
 
@@ -32,9 +32,13 @@ module RubyLLM
32
32
  @input.resume?
33
33
  end
34
34
 
35
- # Checks every entry and builds the plan. Writes nothing, so a rejected
36
- # resume is rejected before the run opens a stream.
35
+ # Checks a new resume and builds its plan before the run opens a stream.
36
+ # An existing identity is left to reservation and reconnect: an exact
37
+ # retry reports its recorded outcome, never redeems the decision twice.
37
38
  def plan(now:)
39
+ existing = @store.find(scope: @scope, thread_id: @input.thread_id, run_id: @input.run_id)
40
+ return self if existing
41
+
38
42
  source = interrupted_run
39
43
  @plan = @input.resume.map { |entry| plan_entry(source, entry, now) }
40
44
  self
@@ -25,15 +25,17 @@ module RubyLLM
25
25
 
26
26
  # rubocop:disable-next Metrics/ParameterLists -- one keyword per host-supplied or overridable value
27
27
  def initialize(chat:, input:, scope:, store: nil, logger: nil, redact: nil, clock: nil, expires_in: nil,
28
- state: nil)
28
+ state: nil, emit_tool_progress: RubyLLM::AgUI.config.emit_tool_progress)
29
+ defaults = RubyLLM::AgUI.config
29
30
  @chat = chat
30
31
  @input = input
31
32
  @scope = scope
32
- @store = store || RubyLLM::AgUI.config.run_store!
33
- @logger = logger || RubyLLM::AgUI.config.logger
33
+ @store = store || defaults.run_store!
34
+ @logger = logger || defaults.logger
34
35
  @redact = redact
35
- @clock = clock || RubyLLM::AgUI.config.clock
36
- @expires_in = expires_in || RubyLLM::AgUI.config.interrupt_ttl
36
+ @clock = clock || defaults.clock
37
+ @expires_in = expires_in || defaults.interrupt_ttl
38
+ @emit_tool_progress = emit_tool_progress
37
39
  @mutex = Mutex.new
38
40
  validate!
39
41
  @state = state.nil? ? @input.state : state
@@ -84,16 +86,20 @@ module RubyLLM
84
86
  end
85
87
 
86
88
  def execute(record, to)
87
- @mapper = build_mapper(record)
88
- @emitter = build_emitter(record, to, mapper: @mapper)
89
- @emitter.start(run_started_event(record), state: @state)
89
+ start_execution(record, to)
90
90
  resume.apply(now: @clock.call) if resume.needed?
91
91
  drive
92
- @record = finish
92
+ @record = finalize { finish }
93
93
  rescue RubyLLM::CancelledError
94
- @record = @emitter.cancel
94
+ @record = finalize { @emitter.cancel }
95
95
  rescue StandardError => e
96
- @record = @emitter.fail(e)
96
+ @record = finalize { @emitter.fail(e) }
97
+ end
98
+
99
+ def start_execution(record, to)
100
+ @mapper = build_mapper(record)
101
+ @emitter = build_emitter(record, to, mapper: @mapper)
102
+ @emitter.start(run_started_event(record), state: @state)
97
103
  end
98
104
 
99
105
  def build_emitter(record, to, mapper: build_mapper(record))
@@ -113,9 +119,14 @@ module RubyLLM
113
119
  def drive
114
120
  @chat.before_tool_call { |tool_call| project { @mapper.before_tool_call(tool_call) } }
115
121
  @chat.after_message { |message| project { @mapper.message(message) } }
122
+ if @emit_tool_progress == true
123
+ @chat.after_tool_progress { |call, progress| project { @mapper.tool_progress(call, progress) } }
124
+ end
116
125
  @chat.complete { |chunk| project { @mapper.chunk(chunk) } }
117
126
  end
118
127
 
128
+ def finalize(&) = @mutex.synchronize(&)
129
+
119
130
  def project
120
131
  @mutex.synchronize { @emitter.write(yield) }
121
132
  end
@@ -9,7 +9,7 @@ module RubyLLM
9
9
  # already in progress instead of executing twice, and it keeps the
10
10
  # interrupts that a later resume has to answer.
11
11
  #
12
- # Two stores ship with the gem: Store::MemoryStore, which is the default for
12
+ # Two stores ship with the gem: Store::MemoryStore for
13
13
  # a single process, and the optional Rails adapter's ActiveRecord store. A
14
14
  # host may implement this interface instead; every method must be safe to
15
15
  # call from concurrent threads.
@@ -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.1.0'
6
+ VERSION = '0.2.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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Wang
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-18 00:00:00.000000000 Z
11
+ date: 2026-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ag-ui-protocol
@@ -142,16 +142,18 @@ files:
142
142
  - LICENSE.txt
143
143
  - README.md
144
144
  - SECURITY.md
145
+ - docs/_advanced/client-example.md
145
146
  - docs/_advanced/rails-integration.md
146
147
  - docs/_advanced/security-and-deployment.md
147
148
  - docs/_core_features/approval-and-resume.md
148
149
  - docs/_core_features/input-validation.md
149
150
  - docs/_core_features/reconnects.md
150
151
  - docs/_core_features/streaming-runs.md
152
+ - docs/_core_features/tool-progress.md
151
153
  - docs/_getting_started/configuration.md
152
154
  - docs/_getting_started/getting-started.md
153
155
  - docs/_getting_started/overview.md
154
- - docs/_getting_started/whats-new-in-0-1.md
156
+ - docs/_getting_started/whats-new-in-0-2.md
155
157
  - docs/_reference/api-reference.md
156
158
  - docs/_reference/compatibility.md
157
159
  - docs/_reference/protocol-coverage.md
@@ -159,6 +161,7 @@ files:
159
161
  - docs/_reference/releasing.md
160
162
  - docs/_reference/third-party-notices.md
161
163
  - docs/_reference/upgrading.md
164
+ - docs/_reference/whats-new-in-0-1.md
162
165
  - docs/index.md
163
166
  - lib/generators/ruby_llm/ag_ui/install_generator.rb
164
167
  - lib/generators/ruby_llm/ag_ui/templates/create_ruby_llm_ag_ui_runs.rb.tt