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 +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +32 -1
- data/lib/ask/ag_ui/emitter.rb +55 -7
- data/lib/ask/ag_ui/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed8c9330466744997f9faf115cfe0ef641b191144a6d114b73e086c6c1129f35
|
|
4
|
+
data.tar.gz: 39fb54b8eb708a4e2b60154891529e92063ff8c46ef0f6d30cb27ae1d0c689dc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/ask/ag_ui/emitter.rb
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
data/lib/ask/ag_ui/version.rb
CHANGED