ruby_llm-ag_ui 0.1.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 +7 -0
- data/CHANGELOG.md +48 -0
- data/CONTRIBUTING.md +56 -0
- data/LICENSE.txt +21 -0
- data/README.md +146 -0
- data/SECURITY.md +30 -0
- data/docs/_advanced/rails-integration.md +199 -0
- data/docs/_advanced/security-and-deployment.md +204 -0
- data/docs/_core_features/approval-and-resume.md +62 -0
- data/docs/_core_features/input-validation.md +64 -0
- data/docs/_core_features/reconnects.md +42 -0
- data/docs/_core_features/streaming-runs.md +49 -0
- data/docs/_getting_started/configuration.md +57 -0
- data/docs/_getting_started/getting-started.md +52 -0
- data/docs/_getting_started/overview.md +189 -0
- data/docs/_getting_started/whats-new-in-0-1.md +41 -0
- data/docs/_reference/api-reference.md +26 -0
- data/docs/_reference/compatibility.md +130 -0
- data/docs/_reference/protocol-coverage.md +175 -0
- data/docs/_reference/release-history.md +22 -0
- data/docs/_reference/releasing.md +54 -0
- data/docs/_reference/third-party-notices.md +82 -0
- data/docs/_reference/upgrading.md +42 -0
- data/docs/index.md +53 -0
- data/lib/generators/ruby_llm/ag_ui/install_generator.rb +37 -0
- data/lib/generators/ruby_llm/ag_ui/templates/create_ruby_llm_ag_ui_runs.rb.tt +24 -0
- data/lib/generators/ruby_llm/ag_ui/templates/initializer.rb.tt +24 -0
- data/lib/ruby_llm/ag_ui/canonical.rb +40 -0
- data/lib/ruby_llm/ag_ui/configuration.rb +113 -0
- data/lib/ruby_llm/ag_ui/coverage.rb +163 -0
- data/lib/ruby_llm/ag_ui/emitter.rb +119 -0
- data/lib/ruby_llm/ag_ui/error.rb +69 -0
- data/lib/ruby_llm/ag_ui/error_sanitizer.rb +69 -0
- data/lib/ruby_llm/ag_ui/input.rb +299 -0
- data/lib/ruby_llm/ag_ui/interrupt.rb +207 -0
- data/lib/ruby_llm/ag_ui/mapper.rb +333 -0
- data/lib/ruby_llm/ag_ui/message_parser.rb +127 -0
- data/lib/ruby_llm/ag_ui/protocol.rb +130 -0
- data/lib/ruby_llm/ag_ui/rails/active_record_store.rb +119 -0
- data/lib/ruby_llm/ag_ui/rails/railtie.rb +29 -0
- data/lib/ruby_llm/ag_ui/rails/record.rb +51 -0
- data/lib/ruby_llm/ag_ui/rails/streaming.rb +166 -0
- data/lib/ruby_llm/ag_ui/reconnect.rb +86 -0
- data/lib/ruby_llm/ag_ui/redaction.rb +58 -0
- data/lib/ruby_llm/ag_ui/redemption.rb +62 -0
- data/lib/ruby_llm/ag_ui/reservation.rb +56 -0
- data/lib/ruby_llm/ag_ui/resume.rb +108 -0
- data/lib/ruby_llm/ag_ui/resume_entry.rb +49 -0
- data/lib/ruby_llm/ag_ui/run.rb +177 -0
- data/lib/ruby_llm/ag_ui/run_record.rb +229 -0
- data/lib/ruby_llm/ag_ui/sse_writer.rb +119 -0
- data/lib/ruby_llm/ag_ui/store/base.rb +123 -0
- data/lib/ruby_llm/ag_ui/store/memory_store.rb +123 -0
- data/lib/ruby_llm/ag_ui/validation.rb +63 -0
- data/lib/ruby_llm/ag_ui/version.rb +8 -0
- data/lib/ruby_llm/ag_ui.rb +65 -0
- metadata +223 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: "Third-Party Notices"
|
|
4
|
+
nav_order: 5
|
|
5
|
+
guide: false
|
|
6
|
+
description: Review licenses and provenance for runtime, optional, and development dependencies.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# {{ page.title }}
|
|
10
|
+
|
|
11
|
+
{{ page.description }}
|
|
12
|
+
{: .fs-6 .fw-300 }
|
|
13
|
+
|
|
14
|
+
`ruby_llm-ag_ui` is released under the MIT License. See
|
|
15
|
+
[the MIT license in the repository](https://github.com/yorzi/ruby_llm-ag-ui/blob/main/LICENSE.txt).
|
|
16
|
+
|
|
17
|
+
It is an unofficial community project. It is not affiliated with, endorsed by, or
|
|
18
|
+
maintained by the RubyLLM maintainers or the AG-UI protocol maintainers.
|
|
19
|
+
|
|
20
|
+
## Runtime dependencies
|
|
21
|
+
|
|
22
|
+
| Project | License | How this gem uses it |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| [RubyLLM](https://github.com/crmne/ruby_llm) | MIT | The agent loop this gem projects: `Chat#complete`, the streaming block, `before_tool_call`, `after_message`, approval state and cancellation. |
|
|
25
|
+
| [ag-ui-protocol](https://github.com/ag-ui-protocol/ag-ui) (Ruby SDK, `~> 0.2.0`) | MIT | Event models, `BaseEvent`, the encoder and the event-type constants. |
|
|
26
|
+
| [Zeitwerk](https://github.com/fxn/zeitwerk) | MIT | Autoloading the gem's own files. |
|
|
27
|
+
| Ruby standard-library gems `cgi`, `resolv`, `uri`, `zlib` | Ruby or BSD-2-Clause | Patched CGI, DNS resolution, URL, and compression dependencies used by the runtime dependency chain. |
|
|
28
|
+
|
|
29
|
+
The gem's direct runtime dependencies are `ruby_llm`, `ag-ui-protocol`, `cgi`, `resolv`, `uri`, and `zlib`.
|
|
30
|
+
Zeitwerk arrives through RubyLLM and is used directly.
|
|
31
|
+
|
|
32
|
+
## Optional: Rails
|
|
33
|
+
|
|
34
|
+
Rails is **not** a runtime dependency. When a host already has it, the adapter
|
|
35
|
+
uses:
|
|
36
|
+
|
|
37
|
+
| Project | License |
|
|
38
|
+
|---|---|
|
|
39
|
+
| [Rails](https://github.com/rails/rails) (`railties`, `actionpack`, `activerecord`) | MIT |
|
|
40
|
+
| [SQLite3](https://github.com/sparklemotion/sqlite3-ruby) (development/test only) | BSD-3-Clause |
|
|
41
|
+
|
|
42
|
+
## Vendored file
|
|
43
|
+
|
|
44
|
+
`spec/fixtures/schema/ag-ui-1.0.schema.json` is a verbatim copy of
|
|
45
|
+
`docs/spec/1.0/schema.json` from the AG-UI repository at commit
|
|
46
|
+
`89cedd3fd549feeee41e50e1d7c6d373e549241a`.
|
|
47
|
+
|
|
48
|
+
- Upstream: <https://github.com/ag-ui-protocol/ag-ui/blob/89cedd3fd549feeee41e50e1d7c6d373e549241a/docs/spec/1.0/schema.json>
|
|
49
|
+
- License: MIT (the AG-UI repository's `LICENSE` at the same commit)
|
|
50
|
+
- SHA-256: `4b5c93226838a0e72d88e6c5df20633c686c49fcb75d9be2815c6cbf9e48e71a`
|
|
51
|
+
- Provenance note: [the vendored schema README](https://github.com/yorzi/ruby_llm-ag-ui/blob/main/spec/fixtures/schema/README.md)
|
|
52
|
+
|
|
53
|
+
It is used only by the test suite and is never served, re-published or modified.
|
|
54
|
+
|
|
55
|
+
## Development dependencies
|
|
56
|
+
|
|
57
|
+
None of these are shipped to users.
|
|
58
|
+
|
|
59
|
+
| Project | License |
|
|
60
|
+
|---|---|
|
|
61
|
+
| RSpec | MIT |
|
|
62
|
+
| RuboCop, `rubocop-rake`, `rubocop-rspec` | MIT |
|
|
63
|
+
| `json_schemer` | MIT |
|
|
64
|
+
| WebMock | MIT |
|
|
65
|
+
| SimpleCov | MIT |
|
|
66
|
+
| dotenv | MIT |
|
|
67
|
+
| Rake | MIT |
|
|
68
|
+
| bundler-audit | GPL-3.0-or-later |
|
|
69
|
+
|
|
70
|
+
## What this gem does not include
|
|
71
|
+
|
|
72
|
+
No code, templates, assets or schema from ToolViews, OpenUI, OddlyDock or any
|
|
73
|
+
AG-UI client is bundled, copied or required. No provider SDK is vendored: RubyLLM
|
|
74
|
+
supplies provider access, and this gem never talks to a provider directly.
|
|
75
|
+
|
|
76
|
+
## Names and trademarks
|
|
77
|
+
|
|
78
|
+
"RubyLLM" and "AG-UI" are used only to describe what this gem extends. Neither
|
|
79
|
+
project's name, logo or maintainers are used to imply endorsement. The gem is
|
|
80
|
+
published as `ruby_llm-ag_ui` under the author's own name and namespace
|
|
81
|
+
(`RubyLLM::AgUI`), and it does not define or reopen the top-level `AgUi`
|
|
82
|
+
namespace owned by the `ag-ui` gem.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Upgrading
|
|
4
|
+
nav_order: 2
|
|
5
|
+
description: Move a RubyLLM AG-UI application to the next release, one release at a time.
|
|
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 move between versioned releases.
|
|
16
|
+
* Where to find changes that need code updates.
|
|
17
|
+
|
|
18
|
+
## One Release at a Time
|
|
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
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
gem 'ruby_llm-ag_ui', '~> 0.1.0'
|
|
26
|
+
```
|
|
27
|
+
|
|
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.
|
|
29
|
+
|
|
30
|
+
## Use the Full Generator Command
|
|
31
|
+
|
|
32
|
+
Untagged development checkouts registered the shorter `ruby_llm:ag_ui` namespace. Version 0.1 registers the documented installation command:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Before, on an untagged checkout
|
|
36
|
+
bin/rails generate ruby_llm:ag_ui
|
|
37
|
+
|
|
38
|
+
# Version 0.1
|
|
39
|
+
bin/rails generate ruby_llm:ag_ui:install
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Existing generated tables and initializers do not need to be regenerated for this command-name correction.
|
data/docs/index.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: home
|
|
3
|
+
title: "RubyLLM AG-UI"
|
|
4
|
+
description: "Run RubyLLM chats as AG-UI 1.0 streams with validated input, approval pauses, cancellation, and optional Rails integration."
|
|
5
|
+
permalink: /
|
|
6
|
+
hero:
|
|
7
|
+
name: "RubyLLM AG-UI"
|
|
8
|
+
text: "Run RubyLLM chats as AG-UI agent runs"
|
|
9
|
+
tagline: "An unofficial Ruby gem for connecting RubyLLM applications to AG-UI clients."
|
|
10
|
+
actions:
|
|
11
|
+
- theme: brand
|
|
12
|
+
text: Get started
|
|
13
|
+
link: /getting-started/
|
|
14
|
+
- theme: alt
|
|
15
|
+
text: View on GitHub
|
|
16
|
+
link: https://github.com/yorzi/ruby_llm-ag-ui
|
|
17
|
+
features:
|
|
18
|
+
- title: Ordered event streams
|
|
19
|
+
details: Map one RubyLLM chat turn to one AG-UI run with a single recorded outcome.
|
|
20
|
+
link: /streaming-runs/
|
|
21
|
+
link_text: Stream a run
|
|
22
|
+
- title: Safe input and approval
|
|
23
|
+
details: Validate RunAgentInput and resume approved tool calls on a new run.
|
|
24
|
+
link: /input-validation/
|
|
25
|
+
link_text: Validate input
|
|
26
|
+
- title: Optional Rails adapter
|
|
27
|
+
details: Add a controller concern and a durable ActiveRecord store to a Rails app.
|
|
28
|
+
link: /rails-integration/
|
|
29
|
+
link_text: Use with Rails
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
{% if site.docs_unreleased %}bundle add ruby_llm-ag_ui --git https://github.com/yorzi/ruby_llm-ag-ui --branch main{% else %}bundle add ruby_llm-ag_ui --version {{ site.version }}{% endif %}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Parse a request
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
require 'ruby_llm/ag_ui'
|
|
42
|
+
|
|
43
|
+
input = RubyLLM::AgUI::Input.parse({
|
|
44
|
+
'threadId' => 'thread-1',
|
|
45
|
+
'runId' => 'run-1',
|
|
46
|
+
'messages' => [{ 'id' => 'm1', 'role' => 'user', 'content' => 'Hello' }]
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
puts "#{input.thread_id}/#{input.run_id}"
|
|
50
|
+
# => thread-1/run-1
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
[Get started]({% link _getting_started/getting-started.md %}) or read [how a run works]({% link _getting_started/overview.md %}).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'rails/generators/migration'
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
module AgUI
|
|
8
|
+
module Generators
|
|
9
|
+
# `bin/rails generate ruby_llm:ag_ui:install`
|
|
10
|
+
#
|
|
11
|
+
# Creates the table the durable run store keeps run identity in, and an
|
|
12
|
+
# initializer that points the gem at it. Running it twice does nothing the
|
|
13
|
+
# second time, because a migration and an initializer are files, not
|
|
14
|
+
# state.
|
|
15
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
16
|
+
include ::Rails::Generators::Migration
|
|
17
|
+
|
|
18
|
+
namespace 'ruby_llm:ag_ui:install'
|
|
19
|
+
desc 'Create the ruby_llm-ag_ui run store table and its initializer.'
|
|
20
|
+
source_root File.expand_path('templates', __dir__)
|
|
21
|
+
|
|
22
|
+
def self.next_migration_number(dirname)
|
|
23
|
+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def create_migration_file
|
|
27
|
+
migration_template 'create_ruby_llm_ag_ui_runs.rb.tt',
|
|
28
|
+
'db/migrate/create_ruby_llm_ag_ui_runs.rb'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def create_initializer
|
|
32
|
+
template 'initializer.rb.tt', 'config/initializers/ruby_llm_ag_ui.rb'
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateRubyLlmAgUiRuns < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
4
|
+
def change
|
|
5
|
+
create_table :ruby_llm_ag_ui_runs do |t|
|
|
6
|
+
t.string :scope, null: false
|
|
7
|
+
t.string :thread_id, null: false
|
|
8
|
+
t.string :run_id, null: false
|
|
9
|
+
t.string :digest, null: false
|
|
10
|
+
t.string :status, null: false
|
|
11
|
+
t.integer :sequence, null: false, default: 0
|
|
12
|
+
# The whole core record as JSON text: it is immutable (there is no partial
|
|
13
|
+
# update), and the gem encodes it, so the stored shape does not depend on
|
|
14
|
+
# the database's JSON type or the host's JSON encoder.
|
|
15
|
+
t.text :payload, null: false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The identity a run is reserved under, and the guard that stops two workers
|
|
19
|
+
# from reserving it at once.
|
|
20
|
+
add_index :ruby_llm_ag_ui_runs, %i[scope thread_id run_id], unique: true
|
|
21
|
+
# Listing one thread's runs, newest first.
|
|
22
|
+
add_index :ruby_llm_ag_ui_runs, %i[scope thread_id status]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# An unofficial RubyLLM ecosystem extension. Rails is optional and the core is
|
|
4
|
+
# pure Ruby, so this initializer is only about the choices a Rails host makes.
|
|
5
|
+
RubyLLM::AgUI.configure do |config|
|
|
6
|
+
# Run identity must outlive the worker that reserved it: one run per
|
|
7
|
+
# (scope, thread_id, run_id), reserved before any provider work happens.
|
|
8
|
+
# Replace the in-process store, which is correct for one process only.
|
|
9
|
+
config.run_store = RubyLLM::AgUI::Rails::ActiveRecordStore.new
|
|
10
|
+
|
|
11
|
+
# Redaction is on by default and withholds tool result content until a host
|
|
12
|
+
# says what is safe to publish. Replace it with your own policy, or opt out
|
|
13
|
+
# with RubyLLM::AgUI::Redaction.method(:none).
|
|
14
|
+
# config.redact = RubyLLM::AgUI::Redaction.method(:none)
|
|
15
|
+
|
|
16
|
+
# How long an interrupt stays answerable, in seconds.
|
|
17
|
+
# config.interrupt_ttl = 900
|
|
18
|
+
|
|
19
|
+
# The largest request body the adapter will read, in bytes.
|
|
20
|
+
# config.max_input_bytes = 256_000
|
|
21
|
+
|
|
22
|
+
# Host-side diagnostics. Never receives prompts, tool payloads or credentials.
|
|
23
|
+
# config.logger = Rails.logger
|
|
24
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module AgUI
|
|
5
|
+
# Deterministic serialization and hashing.
|
|
6
|
+
#
|
|
7
|
+
# Run identity depends on comparing an input with the one a retry sends.
|
|
8
|
+
# JSON object member order is not significant, so a naive hash of the raw
|
|
9
|
+
# bytes treats the same request as two. Canonical.dump sorts object keys
|
|
10
|
+
# recursively (like RubyLLM's ToolCall#arguments, so a call reloaded from a
|
|
11
|
+
# database hashes the same), producing a stable string to digest.
|
|
12
|
+
module Canonical
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# A deterministic JSON string: object keys sorted by their byte length
|
|
16
|
+
# then value, arrays kept in order.
|
|
17
|
+
def dump(value)
|
|
18
|
+
JSON.generate(sort(value))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The SHA-256 hex digest of the canonical form.
|
|
22
|
+
def digest(value)
|
|
23
|
+
Digest::SHA256.hexdigest(dump(value))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# A recursively key-sorted copy of +value+.
|
|
27
|
+
def sort(value)
|
|
28
|
+
case value
|
|
29
|
+
when Hash
|
|
30
|
+
value.sort_by { |key, _| [key.to_s.bytesize, key.to_s] }
|
|
31
|
+
.to_h { |key, nested| [key.to_s, sort(nested)] }
|
|
32
|
+
when Array
|
|
33
|
+
value.map { |item| sort(item) }
|
|
34
|
+
else
|
|
35
|
+
value
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module AgUI
|
|
5
|
+
# Global configuration for the gem. Hosts usually set it once at boot:
|
|
6
|
+
#
|
|
7
|
+
# RubyLLM::AgUI.configure do |config|
|
|
8
|
+
# config.run_store = MyRunStore.new
|
|
9
|
+
# config.redact = ->(kind, payload) { MyRedactor.call(kind, payload) }
|
|
10
|
+
# config.logger = Rails.logger
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# Every value can be overridden per run by passing the matching keyword to
|
|
14
|
+
# RubyLLM::AgUI::Run.new.
|
|
15
|
+
class Configuration
|
|
16
|
+
# The object that reserves run identity and stores interrupts. Defaults
|
|
17
|
+
# to nil; set a RubyLLM::AgUI::Store::MemoryStore for single-process use,
|
|
18
|
+
# or a durable store for multi-process deployment.
|
|
19
|
+
attr_accessor :run_store
|
|
20
|
+
|
|
21
|
+
# A callable (kind, payload) — or (payload) — invoked before a tool result
|
|
22
|
+
# or activity payload reaches the wire. Return the value to publish.
|
|
23
|
+
#
|
|
24
|
+
# It defaults to RubyLLM::AgUI::Redaction.default, which keeps the
|
|
25
|
+
# payload's shape and withholds its content: the gem cannot know which
|
|
26
|
+
# part of a tool result is a secret, so it does not publish the raw value
|
|
27
|
+
# until a host says it is safe. Opt out with
|
|
28
|
+
# RubyLLM::AgUI::Redaction.method(:none), or replace it with your policy.
|
|
29
|
+
attr_writer :redact
|
|
30
|
+
|
|
31
|
+
# A Logger used for host-side diagnostics. Never receives prompts, raw
|
|
32
|
+
# tool payloads or credentials.
|
|
33
|
+
attr_accessor :logger
|
|
34
|
+
|
|
35
|
+
# The AG-UI protocol version this producer speaks, emitted on
|
|
36
|
+
# RUN_STARTED so a consumer can detect a downgrade.
|
|
37
|
+
attr_accessor :protocol_version
|
|
38
|
+
|
|
39
|
+
# Maximum accepted request body size in bytes.
|
|
40
|
+
attr_accessor :max_input_bytes
|
|
41
|
+
|
|
42
|
+
# Maximum number of messages accepted in RunAgentInput.
|
|
43
|
+
attr_accessor :max_messages
|
|
44
|
+
|
|
45
|
+
# Maximum number of tools accepted in RunAgentInput.
|
|
46
|
+
attr_accessor :max_tools
|
|
47
|
+
|
|
48
|
+
# Maximum number of context entries accepted in RunAgentInput.
|
|
49
|
+
attr_accessor :max_context
|
|
50
|
+
|
|
51
|
+
# How long an interrupt stays answerable, in seconds.
|
|
52
|
+
attr_accessor :interrupt_ttl
|
|
53
|
+
|
|
54
|
+
# A callable returning the current UTC Time. Injected in tests.
|
|
55
|
+
attr_accessor :clock
|
|
56
|
+
|
|
57
|
+
# Whether client-supplied message history is reconciled against
|
|
58
|
+
# host-stored history. Always off in the first release: client messages
|
|
59
|
+
# are never trusted as system or tool turns.
|
|
60
|
+
attr_accessor :allow_client_history
|
|
61
|
+
|
|
62
|
+
def initialize
|
|
63
|
+
@run_store = nil
|
|
64
|
+
@redact = Redaction.method(:default)
|
|
65
|
+
@logger = nil
|
|
66
|
+
@protocol_version = '1.0'
|
|
67
|
+
@max_input_bytes = 256_000
|
|
68
|
+
@max_messages = 500
|
|
69
|
+
@max_tools = 64
|
|
70
|
+
@max_context = 64
|
|
71
|
+
@interrupt_ttl = 900
|
|
72
|
+
@clock = -> { Time.now.utc }
|
|
73
|
+
@allow_client_history = false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The redaction policy in force. Never nil: an unset or nil policy falls
|
|
77
|
+
# back to the default rather than publishing raw payloads.
|
|
78
|
+
def redact
|
|
79
|
+
@redact || Redaction.method(:default)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Which policy is in force, for diagnostics: :default, :none or :custom.
|
|
83
|
+
def redaction
|
|
84
|
+
return :default if redact == Redaction.method(:default)
|
|
85
|
+
return :none if redact == Redaction.method(:none)
|
|
86
|
+
|
|
87
|
+
:custom
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The run store, or nil when the host has not configured one.
|
|
91
|
+
def run_store!
|
|
92
|
+
@run_store || raise(ConfigurationError, 'No run_store is configured. ' \
|
|
93
|
+
'Set RubyLLM::AgUI.config.run_store or pass store: to Run.new.')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# A snapshot of the configuration, for diagnostics and support reports.
|
|
97
|
+
# Never includes secrets.
|
|
98
|
+
def to_h
|
|
99
|
+
{
|
|
100
|
+
run_store: run_store&.class&.name,
|
|
101
|
+
redact: redaction,
|
|
102
|
+
protocol_version: protocol_version,
|
|
103
|
+
max_input_bytes: max_input_bytes,
|
|
104
|
+
max_messages: max_messages,
|
|
105
|
+
max_tools: max_tools,
|
|
106
|
+
max_context: max_context,
|
|
107
|
+
interrupt_ttl: interrupt_ttl,
|
|
108
|
+
allow_client_history: allow_client_history
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module AgUI
|
|
5
|
+
# The AG-UI 1.0 producer coverage map (FR-02).
|
|
6
|
+
#
|
|
7
|
+
# Every one of the 31 event types the pinned 1.0 schema defines is listed
|
|
8
|
+
# with the status this gem gives it. A producer is not required to
|
|
9
|
+
# manufacture events it has no data for; what it must never do is emit an
|
|
10
|
+
# event that the schema does not define, or misreport one it does.
|
|
11
|
+
#
|
|
12
|
+
# This is the gem's own audit against the pinned schema commit
|
|
13
|
+
# (RubyLLM::AgUI::Protocol::SCHEMA_COMMIT). It is not a claim about
|
|
14
|
+
# `ag-ui-protocol`, whose enum is compared separately below.
|
|
15
|
+
module Coverage
|
|
16
|
+
# The statuses an entry may have.
|
|
17
|
+
# :produced — emitted from real RubyLLM data on the supported path
|
|
18
|
+
# :optional — emitted only when the host opts in and real data exists
|
|
19
|
+
# :host_owned — the host owns the payload; the gem does not produce it
|
|
20
|
+
# :not_produced — the producer has no data for it and never fabricates it
|
|
21
|
+
STATUSES = %i[produced optional host_owned not_produced].freeze
|
|
22
|
+
|
|
23
|
+
# Event types listed in the pinned docs/spec/1.0/schema.json Event union.
|
|
24
|
+
SCHEMA_EVENT_TYPES = %w[
|
|
25
|
+
TEXT_MESSAGE_START TEXT_MESSAGE_CONTENT TEXT_MESSAGE_END TEXT_MESSAGE_CHUNK
|
|
26
|
+
TOOL_CALL_START TOOL_CALL_ARGS TOOL_CALL_END TOOL_CALL_CHUNK TOOL_CALL_RESULT
|
|
27
|
+
STATE_SNAPSHOT STATE_DELTA MESSAGES_SNAPSHOT
|
|
28
|
+
ACTIVITY_SNAPSHOT ACTIVITY_DELTA
|
|
29
|
+
RAW CUSTOM
|
|
30
|
+
RUN_STARTED RUN_FINISHED RUN_ERROR STEP_STARTED STEP_FINISHED
|
|
31
|
+
REASONING_START REASONING_MESSAGE_START REASONING_MESSAGE_CONTENT
|
|
32
|
+
REASONING_MESSAGE_END REASONING_MESSAGE_CHUNK REASONING_END REASONING_ENCRYPTED_VALUE
|
|
33
|
+
SUBAGENT_STARTED SUBAGENT_FINISHED SUBAGENT_ERROR
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
FAMILIES = {
|
|
37
|
+
'TEXT_MESSAGE' => :text_messages,
|
|
38
|
+
'TOOL_CALL' => :tool_calls,
|
|
39
|
+
'STATE' => :state,
|
|
40
|
+
'MESSAGES' => :state,
|
|
41
|
+
'ACTIVITY' => :activity,
|
|
42
|
+
'RAW' => :raw_custom,
|
|
43
|
+
'CUSTOM' => :raw_custom,
|
|
44
|
+
'RUN' => :run_lifecycle,
|
|
45
|
+
'STEP' => :run_lifecycle,
|
|
46
|
+
'REASONING' => :reasoning,
|
|
47
|
+
'SUBAGENT' => :subagents
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
50
|
+
# Producer status and a one-line reason, keyed by event type.
|
|
51
|
+
STATUS = {
|
|
52
|
+
'RUN_STARTED' => [:produced, 'Opens a reserved run; declares protocolVersion.'],
|
|
53
|
+
'RUN_FINISHED' => [:produced, 'Success, interrupt or cancelled outcome; never a raw partial.'],
|
|
54
|
+
'RUN_ERROR' => [:produced, 'Sanitized message and code; never raw exception text.'],
|
|
55
|
+
'STEP_STARTED' => [:produced, 'One step per RubyLLM completion or tool round.'],
|
|
56
|
+
'STEP_FINISHED' => [:produced, 'Closed before the terminal event.'],
|
|
57
|
+
'TEXT_MESSAGE_START' => [:produced, 'Opened on the first streamed assistant text chunk.'],
|
|
58
|
+
'TEXT_MESSAGE_CONTENT' => [:produced, 'One delta per non-empty chunk.content.'],
|
|
59
|
+
'TEXT_MESSAGE_END' => [:produced, 'Closed before a tool round or the terminal event.'],
|
|
60
|
+
'TEXT_MESSAGE_CHUNK' => [:not_produced, 'Convenience form; the gem emits the explicit triad.'],
|
|
61
|
+
'TOOL_CALL_START' => [:produced, 'Emitted when a call is observed before it runs.'],
|
|
62
|
+
'TOOL_CALL_ARGS' => [:produced, 'Arguments as received; never re-synthesized.'],
|
|
63
|
+
'TOOL_CALL_END' => [:produced, 'Closed before the call runs or the run ends.'],
|
|
64
|
+
'TOOL_CALL_RESULT' => [:produced, 'Tool result after the host redaction policy.'],
|
|
65
|
+
'TOOL_CALL_CHUNK' => [:not_produced, 'Convenience form; the gem emits the explicit triad.'],
|
|
66
|
+
'MESSAGES_SNAPSHOT' => [:host_owned, 'Not produced: history is the host\'s; ids are never re-synthesized.'],
|
|
67
|
+
'STATE_SNAPSHOT' => [:produced, 'Reconnect: the state the host gave the run, never replayed deltas.'],
|
|
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.'],
|
|
71
|
+
'REASONING_START' => [:not_produced, 'Private reasoning is not emitted until a host opts in.'],
|
|
72
|
+
'REASONING_MESSAGE_START' => [:not_produced, 'Never emitted; a chunk that carries thinking is ignored.'],
|
|
73
|
+
'REASONING_MESSAGE_CONTENT' => [:not_produced, 'Never emitted; a chunk that carries thinking is ignored.'],
|
|
74
|
+
'REASONING_MESSAGE_END' => [:not_produced, 'Never emitted; nothing opens a reasoning block to close.'],
|
|
75
|
+
'REASONING_MESSAGE_CHUNK' => [:not_produced, 'Convenience form; not emitted.'],
|
|
76
|
+
'REASONING_END' => [:not_produced, 'Never emitted; nothing opens a reasoning block to close.'],
|
|
77
|
+
'REASONING_ENCRYPTED_VALUE' => [:not_produced, 'No encrypted reasoning source in RubyLLM 2.x.'],
|
|
78
|
+
'RAW' => [:not_produced, 'The gem never passes provider payloads through.'],
|
|
79
|
+
'CUSTOM' => [:optional, 'Only safe, documented host extensions.'],
|
|
80
|
+
'SUBAGENT_STARTED' => [:not_produced, 'RubyLLM 2.x runs one chat; no subagent data exists.'],
|
|
81
|
+
'SUBAGENT_FINISHED' => [:not_produced, 'RubyLLM 2.x runs one chat; no subagent data exists.'],
|
|
82
|
+
'SUBAGENT_ERROR' => [:not_produced, 'RubyLLM 2.x runs one chat; no subagent data exists.']
|
|
83
|
+
}.freeze
|
|
84
|
+
|
|
85
|
+
# The 1.0 shapes `ag-ui-protocol` 0.2.0 cannot express, with the gem's
|
|
86
|
+
# handling. Upstream issues are drafted in docs/.internal/upstream.
|
|
87
|
+
GAPS = [
|
|
88
|
+
{
|
|
89
|
+
id: 'SDK-GAP-01',
|
|
90
|
+
shape: 'RunFinishedCancelledOutcome',
|
|
91
|
+
detail: 'ag-ui-protocol 0.2.0 has success and interrupt outcomes only.',
|
|
92
|
+
gem_handling: 'RubyLLM::AgUI::Protocol::CancelledOutcome, a schema-valid "cancelled" outcome.',
|
|
93
|
+
upstream: 'https://github.com/ag-ui-protocol/ag-ui/issues'
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: 'SDK-GAP-02',
|
|
97
|
+
shape: 'RUN_STARTED.protocolVersion',
|
|
98
|
+
detail: "0.2.0's RunStartedEvent has no protocolVersion field.",
|
|
99
|
+
gem_handling: 'RubyLLM::AgUI::Protocol::RunStartedEvent adds it, defaulting to "1.0".',
|
|
100
|
+
upstream: 'https://github.com/ag-ui-protocol/ag-ui/issues'
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 'SDK-GAP-03',
|
|
104
|
+
shape: 'JSON to RunAgentInput parser',
|
|
105
|
+
detail: '0.2.0 models have constructors and to_h only; there is no inbound parser.',
|
|
106
|
+
gem_handling: 'RubyLLM::AgUI::Input.parse with size and schema bounds.',
|
|
107
|
+
upstream: 'https://github.com/ag-ui-protocol/ag-ui/issues'
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'SDK-GAP-04',
|
|
111
|
+
shape: 'SUBAGENT_STARTED / _FINISHED / _ERROR',
|
|
112
|
+
detail: 'The five legacy THINKING constants are present; the three SUBAGENT types are not.',
|
|
113
|
+
gem_handling: 'Not produced: RubyLLM 2.x runs a single chat with no subagent data.',
|
|
114
|
+
upstream: 'https://github.com/ag-ui-protocol/ag-ui/issues'
|
|
115
|
+
}
|
|
116
|
+
].freeze
|
|
117
|
+
|
|
118
|
+
# Event types present in the SDK enum but not in the 1.0 schema (the five
|
|
119
|
+
# legacy THINKING constants). Emitting one would masquerade as 1.0.
|
|
120
|
+
LEGACY_SDK_ONLY = %w[
|
|
121
|
+
THINKING_TEXT_MESSAGE_START THINKING_TEXT_MESSAGE_CONTENT THINKING_TEXT_MESSAGE_END
|
|
122
|
+
THINKING_START THINKING_END
|
|
123
|
+
].freeze
|
|
124
|
+
|
|
125
|
+
module_function
|
|
126
|
+
|
|
127
|
+
# Every schema event type with its family and status, as an array of
|
|
128
|
+
# hashes suitable for docs generation.
|
|
129
|
+
def entries
|
|
130
|
+
SCHEMA_EVENT_TYPES.map do |type|
|
|
131
|
+
status, note = STATUS.fetch(type)
|
|
132
|
+
{ type: type, family: family_for(type), status: status, note: note }
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The family an event type belongs to.
|
|
137
|
+
def family_for(type)
|
|
138
|
+
prefix = FAMILIES.keys.find { |key| type.start_with?(key) }
|
|
139
|
+
FAMILIES.fetch(prefix)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Event types grouped by status.
|
|
143
|
+
def report
|
|
144
|
+
entries.group_by { |entry| entry[:status] }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Event type constants the SDK defines (read at runtime, not hardcoded).
|
|
148
|
+
def sdk_event_types
|
|
149
|
+
::AgUiProtocol::Core::Events::EventType.constants.map { |name| ::AgUiProtocol::Core::Events::EventType.const_get(name) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# SDK constants with no 1.0 schema counterpart.
|
|
153
|
+
def sdk_only_event_types
|
|
154
|
+
sdk_event_types - SCHEMA_EVENT_TYPES
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# 1.0 schema types with no SDK constant.
|
|
158
|
+
def schema_only_event_types
|
|
159
|
+
SCHEMA_EVENT_TYPES - sdk_event_types
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|