ask-ag-ui 0.1.0 → 0.1.1

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: a496b953fbbbf38bde6bfb2cc1771fc67d8b47a8ed6789ef453e18b8a7d68161
4
- data.tar.gz: 94edd256f17e7b5de58a3de085d2d857adc06ec35bb58e83d1e9f92feac457bc
3
+ metadata.gz: ed8c9330466744997f9faf115cfe0ef641b191144a6d114b73e086c6c1129f35
4
+ data.tar.gz: 39fb54b8eb708a4e2b60154891529e92063ff8c46ef0f6d30cb27ae1d0c689dc
5
5
  SHA512:
6
- metadata.gz: 711674f104ef94ed5c9e44293781b74970beaf08222cec1e42865bf304b70e8a94ce80849c0f1884f5d4be254d841dfb85547d610d15892d23c61f17bbe76496
7
- data.tar.gz: cafe0507a38288621887eb3d31cb663676326d4da15814fd4716cd16fd2c045d9587347ed97c421cb71ecf979a6add14775b5bdc52a34f829f5ddca3f1a923af
6
+ metadata.gz: 423f30841ee3a31742b667fd617525e6896ca2b36809e7ba6a6136a49c27b033c78117b94b6efdb15acec10d7b972781aadf965b2f05cb9a5201814539cb4690
7
+ data.tar.gz: 0bf9fc79975ee1a28c49c73cb18f7116395e16a6d24c6d05f343d0a12611050e831629a03537146387da90c35ba86d5056d86aed94deeabc15a68de31cf5b54f
data/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ the keep-a-changelog format.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.1.1] — 2026-09-29
9
+
10
+ ### Added
11
+
12
+ - `Ask::AGUI::Emitter#custom_name` — the public seam for a host that names
13
+ its own `CUSTOM` frames. Pass `custom_names:` to the constructor
14
+ (`{ "VisitorAway" => "resting" }`) or override the method; either way the
15
+ frame's `name` changes and the event still rides one `CUSTOM` frame with
16
+ its `to_h` as the value. The default is unchanged — a `CUSTOM` frame
17
+ named after the event's class — and events nobody named keep riding that
18
+ default path. A host no longer has to subclass the emitter and override
19
+ the private `event_name` method to get there.
20
+
8
21
  ## [0.1.0] — 2026-09-28
9
22
 
10
23
  ### Added
data/README.md CHANGED
@@ -49,11 +49,42 @@ ask-agent internals:
49
49
  | `ToolCallDelta`, `ToolExecutionStart`, `ToolExecutionEnd` | `TOOL_CALL_START` → `TOOL_CALL_ARGS` → `TOOL_CALL_END` → `TOOL_CALL_RESULT` (empty args deltas dropped) |
50
50
  | `SessionEnd` / `#finish` | `RUN_FINISHED` |
51
51
  | `Error` / `#fail` | `RUN_ERROR` |
52
- | anything else | one generic `CUSTOM` passthrough (`name` + `value`) |
52
+ | anything else | one generic `CUSTOM` passthrough (`name` + `value`), named after the event's class — [rename it](#naming-your-own-custom-frames) |
53
53
 
54
54
  Every frame is built with `AgUiProtocol::Core::Events::*` and encoded with
55
55
  `AgUiProtocol::Encoder::EventEncoder` — event JSON is never hand-rolled.
56
56
 
57
+ ### Naming your own CUSTOM frames
58
+
59
+ A host whose states are not event class names — a chat page reading
60
+ `resting`, `done`, `visitor_spent` — says so through the public
61
+ `#custom_name` hook, never by reaching into a private method. Name the
62
+ kinds you know with the `custom_names:` constructor option:
63
+
64
+ ```ruby
65
+ emitter = Ask::AGUI::Emitter.new(
66
+ thread_id: "t1", run_id: "r1",
67
+ custom_names: { "VisitorAway" => "resting", "CheckoutClosed" => "visitor_spent" }
68
+ )
69
+ ```
70
+
71
+ or override the method when the name is computed:
72
+
73
+ ```ruby
74
+ class ChatEmitter < Ask::AGUI::Emitter
75
+ NAMES = { "VisitorAway" => "resting" }
76
+
77
+ def custom_name(event)
78
+ NAMES.fetch(event.class.name.split("::").last) { super }
79
+ end
80
+ end
81
+ ```
82
+
83
+ Either way only the frame's `name` changes: the event still rides one
84
+ `CUSTOM` frame with its `to_h` as the value, and the vocabulary above is
85
+ untouched. Events nobody names keep the class-name default, so the
86
+ passthrough is exactly what it is without any of this.
87
+
57
88
  ## Mounting: `Ask::AGUI::Server`
58
89
 
59
90
  The server is the conventional Rack surface AG-UI clients expect. The host
@@ -39,7 +39,9 @@ module Ask
39
39
  # `RUN_ERROR`. {#finish} and {#fail} drive the same endings manually.
40
40
  # * Anything else rides one generic `CUSTOM` passthrough
41
41
  # (`name` = event class name, `value` = its `to_h`): the emitter knows
42
- # nothing about any chat application's specific states.
42
+ # nothing about any chat application's specific states. A host with
43
+ # its own state vocabulary renames those frames by overriding
44
+ # {#custom_name} — no private method to reach for.
43
45
  #
44
46
  # `MessageEnd` and `TurnEnd` carry no AG-UI counterpart of their own —
45
47
  # they only close whatever text, reasoning, or tool call is still open.
@@ -60,7 +62,10 @@ module Ask
60
62
  # @param messages [Array<AgUiProtocol::Core::Types::BaseMessage,
61
63
  # AgUiProtocol::Core::Types::ActivityMessage>] input messages carried
62
64
  # on the RUN_STARTED event.
63
- def initialize(thread_id:, run_id:, messages: [])
65
+ # @param custom_names [Hash] the host's own `CUSTOM` frame names, keyed
66
+ # by the event's class name (`"VisitorAway" => "resting"`). Events
67
+ # left out keep the default. See {#custom_name}.
68
+ def initialize(thread_id:, run_id:, messages: [], custom_names: {})
64
69
  @thread_id = thread_id
65
70
  @run_id = run_id
66
71
  @input = AgUiProtocol::Core::Types::RunAgentInput.new(
@@ -72,6 +77,7 @@ module Ask
72
77
  context: [],
73
78
  forwarded_props: {}
74
79
  )
80
+ @custom_names = normalize_custom_names(custom_names)
75
81
  @encoder = AgUiProtocol::Encoder::EventEncoder.new
76
82
  @started = false
77
83
  @terminal = false
@@ -147,8 +153,54 @@ module Ask
147
153
  fail_with(reason)
148
154
  end
149
155
 
156
+ # The `CUSTOM` frame name an app-defined event rides as.
157
+ #
158
+ # This is the seam for a host that has a state vocabulary of its own —
159
+ # a chat page reading "resting", "done", "visitor_spent" instead of
160
+ # the event classes. Two ways in, both public:
161
+ #
162
+ # # 1. name the kinds you know, at construction:
163
+ # emitter = Ask::AGUI::Emitter.new(thread_id: "t1", run_id: "r1",
164
+ # custom_names: { "VisitorAway" => "resting", "CheckoutClosed" => "visitor_spent" })
165
+ #
166
+ # # 2. override this method when the name is computed:
167
+ # class ChatEmitter < Ask::AGUI::Emitter
168
+ # NAMES = { "VisitorAway" => "resting" }
169
+ #
170
+ # def custom_name(event)
171
+ # NAMES.fetch(event.class.name.split("::").last) { super }
172
+ # end
173
+ # end
174
+ #
175
+ # Either way only the frame's `name` changes: the event still rides
176
+ # one `CUSTOM` frame with its `to_h` as the value, and the rest of
177
+ # the vocabulary is untouched. An event nobody names — or a name that
178
+ # comes back nil or empty — falls back to the event's class name, so
179
+ # the default is exactly what it is without any of this.
180
+ #
181
+ # @param event [Object] the app-defined event about to ride a frame.
182
+ # @return [String] the `name` that frame carries.
183
+ def custom_name(event)
184
+ name = event_name(event)
185
+ custom = @custom_names[name]
186
+ custom.to_s.empty? ? name : custom.to_s
187
+ end
188
+
150
189
  private
151
190
 
191
+ # The host's names, keyed by event class name, normalized once so
192
+ # `{ VisitorAway: :resting }` reads like `{ "VisitorAway" => "resting" }`.
193
+ def normalize_custom_names(custom_names)
194
+ custom_names.to_h { |kind, name| [kind.to_s, name.to_s] }.freeze
195
+ end
196
+
197
+ # The key the emitter matches an event by — the event's demodulized
198
+ # class name. Internal: renaming a `CUSTOM` frame is {#custom_name}'s
199
+ # job, and renaming the vocabulary would move the mapping itself.
200
+ def event_name(event)
201
+ event.class.name.to_s.split("::").last
202
+ end
203
+
152
204
  def fail_with(reason)
153
205
  frames = close_open_messages
154
206
  return frames if @terminal
@@ -229,7 +281,7 @@ module Ask
229
281
  def handle_custom(event)
230
282
  value = event.respond_to?(:to_h) ? event.to_h : {}
231
283
  value = {} if value.nil?
232
- [encode(AgUiProtocol::Core::Events::CustomEvent.new(name: event_name(event), value: value))]
284
+ [encode(AgUiProtocol::Core::Events::CustomEvent.new(name: custom_name(event), value: value))]
233
285
  end
234
286
 
235
287
  def close_open_messages
@@ -264,10 +316,6 @@ module Ask
264
316
  @encoder.encode(event)
265
317
  end
266
318
 
267
- def event_name(event)
268
- event.class.name.to_s.split("::").last
269
- end
270
-
271
319
  def event_id(event)
272
320
  event.respond_to?(:id) ? event.id.to_s : SecureRandom.uuid
273
321
  end
@@ -3,6 +3,6 @@
3
3
  module Ask
4
4
  module AGUI
5
5
  # Gem version, following Semantic Versioning.
6
- VERSION = "0.1.0"
6
+ VERSION = "0.1.1"
7
7
  end
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-ag-ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto