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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +48 -0
  3. data/CONTRIBUTING.md +56 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +146 -0
  6. data/SECURITY.md +30 -0
  7. data/docs/_advanced/rails-integration.md +199 -0
  8. data/docs/_advanced/security-and-deployment.md +204 -0
  9. data/docs/_core_features/approval-and-resume.md +62 -0
  10. data/docs/_core_features/input-validation.md +64 -0
  11. data/docs/_core_features/reconnects.md +42 -0
  12. data/docs/_core_features/streaming-runs.md +49 -0
  13. data/docs/_getting_started/configuration.md +57 -0
  14. data/docs/_getting_started/getting-started.md +52 -0
  15. data/docs/_getting_started/overview.md +189 -0
  16. data/docs/_getting_started/whats-new-in-0-1.md +41 -0
  17. data/docs/_reference/api-reference.md +26 -0
  18. data/docs/_reference/compatibility.md +130 -0
  19. data/docs/_reference/protocol-coverage.md +175 -0
  20. data/docs/_reference/release-history.md +22 -0
  21. data/docs/_reference/releasing.md +54 -0
  22. data/docs/_reference/third-party-notices.md +82 -0
  23. data/docs/_reference/upgrading.md +42 -0
  24. data/docs/index.md +53 -0
  25. data/lib/generators/ruby_llm/ag_ui/install_generator.rb +37 -0
  26. data/lib/generators/ruby_llm/ag_ui/templates/create_ruby_llm_ag_ui_runs.rb.tt +24 -0
  27. data/lib/generators/ruby_llm/ag_ui/templates/initializer.rb.tt +24 -0
  28. data/lib/ruby_llm/ag_ui/canonical.rb +40 -0
  29. data/lib/ruby_llm/ag_ui/configuration.rb +113 -0
  30. data/lib/ruby_llm/ag_ui/coverage.rb +163 -0
  31. data/lib/ruby_llm/ag_ui/emitter.rb +119 -0
  32. data/lib/ruby_llm/ag_ui/error.rb +69 -0
  33. data/lib/ruby_llm/ag_ui/error_sanitizer.rb +69 -0
  34. data/lib/ruby_llm/ag_ui/input.rb +299 -0
  35. data/lib/ruby_llm/ag_ui/interrupt.rb +207 -0
  36. data/lib/ruby_llm/ag_ui/mapper.rb +333 -0
  37. data/lib/ruby_llm/ag_ui/message_parser.rb +127 -0
  38. data/lib/ruby_llm/ag_ui/protocol.rb +130 -0
  39. data/lib/ruby_llm/ag_ui/rails/active_record_store.rb +119 -0
  40. data/lib/ruby_llm/ag_ui/rails/railtie.rb +29 -0
  41. data/lib/ruby_llm/ag_ui/rails/record.rb +51 -0
  42. data/lib/ruby_llm/ag_ui/rails/streaming.rb +166 -0
  43. data/lib/ruby_llm/ag_ui/reconnect.rb +86 -0
  44. data/lib/ruby_llm/ag_ui/redaction.rb +58 -0
  45. data/lib/ruby_llm/ag_ui/redemption.rb +62 -0
  46. data/lib/ruby_llm/ag_ui/reservation.rb +56 -0
  47. data/lib/ruby_llm/ag_ui/resume.rb +108 -0
  48. data/lib/ruby_llm/ag_ui/resume_entry.rb +49 -0
  49. data/lib/ruby_llm/ag_ui/run.rb +177 -0
  50. data/lib/ruby_llm/ag_ui/run_record.rb +229 -0
  51. data/lib/ruby_llm/ag_ui/sse_writer.rb +119 -0
  52. data/lib/ruby_llm/ag_ui/store/base.rb +123 -0
  53. data/lib/ruby_llm/ag_ui/store/memory_store.rb +123 -0
  54. data/lib/ruby_llm/ag_ui/validation.rb +63 -0
  55. data/lib/ruby_llm/ag_ui/version.rb +8 -0
  56. data/lib/ruby_llm/ag_ui.rb +65 -0
  57. 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