ag-ui 0.2.0 → 1.0.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 +4 -4
- data/data/ag_ui.json +1 -1
- data/lib/ag_ui/a2ui/recovery.rb +19 -12
- data/lib/ag_ui/a2ui/validate.rb +150 -89
- data/lib/ag_ui/event_bridge.rb +144 -44
- data/lib/ag_ui/messages.rb +20 -22
- data/lib/ag_ui/middleware/a2ui.rb +109 -82
- data/lib/ag_ui/middleware/forwarded_props.rb +1 -1
- data/lib/ag_ui/middleware/state.rb +366 -0
- data/lib/ag_ui/middleware/system_prompt.rb +1 -1
- data/lib/ag_ui/middleware/tool_router.rb +63 -44
- data/lib/ag_ui/protocol/json_schema/definition.rb +5 -5
- data/lib/ag_ui/protocol/json_schema.rb +18 -14
- data/lib/ag_ui/run_input.rb +1 -1
- data/lib/ag_ui/server/info.rb +10 -10
- data/lib/ag_ui/server/middleware/sse_stream.rb +3 -3
- data/lib/ag_ui/server/sse/event_encoder.rb +11 -11
- data/lib/ag_ui/server/sse/stream.rb +5 -1
- data/lib/ag_ui/server/triage.rb +11 -8
- data/lib/ag_ui/server.rb +14 -10
- data/lib/ag_ui/terminals/ruby_llm.rb +68 -39
- data/lib/ag_ui/version.rb +1 -1
- data/lib/ag_ui.rb +1 -0
- metadata +45 -9
- data/data/generate-ag-ui-schema.py +0 -50
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 226774b66734c0e15d3668f1333c061ef41caee6fd160a3d74227ccb0766b43e
|
|
4
|
+
data.tar.gz: 48926b867c6e753d0a6f4865b8636704b826add52510522afee69726d05de4d7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 144142fdc3950ec5925887614db47e416e3bc92a6810fb3cd7bea146de1a3b54de3655d110fe6bba4246761a70a88f9d26c9c2d7101657a3848c52c86dff5fcb
|
|
7
|
+
data.tar.gz: 8b62bdfd10acb644e867aa8f0ac71e1c4fbb1897644f43c71642c05076792fe32a030244c0f7d4aa2451774f9a357a226e60fa784bffa0c53db6c147f8fa0034
|
data/data/ag_ui.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$comment": "Generated by
|
|
2
|
+
"$comment": "Generated by bin/generate-ag-ui-schema from ag-ui sdks/python (77 models). Do not edit.",
|
|
3
3
|
"definitions": {
|
|
4
4
|
"ActivityDeltaEvent": {
|
|
5
5
|
"additionalProperties": true,
|
data/lib/ag_ui/a2ui/recovery.rb
CHANGED
|
@@ -23,8 +23,8 @@ module AgUi
|
|
|
23
23
|
A2UI_RECOVERY_ACTIVITY_TYPE = "a2ui_recovery"
|
|
24
24
|
|
|
25
25
|
NO_TOOL_CALL_ERROR = {
|
|
26
|
-
"code"
|
|
27
|
-
"path"
|
|
26
|
+
"code" => "empty_components",
|
|
27
|
+
"path" => "components",
|
|
28
28
|
"message" => "Sub-agent did not call render_a2ui",
|
|
29
29
|
}.freeze
|
|
30
30
|
|
|
@@ -57,15 +57,22 @@ module AgUi
|
|
|
57
57
|
prompt = augment_prompt_with_validation_errors(base_prompt, last_errors)
|
|
58
58
|
args = invoke_subagent.call(prompt, attempt)
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
if args.nil? || args.empty?
|
|
61
|
+
record = { "attempt" => attempt, "ok" => false, "errors" => [NO_TOOL_CALL_ERROR] }
|
|
62
|
+
else
|
|
63
|
+
if args["components"].is_a?(Array)
|
|
64
|
+
components = args["components"]
|
|
65
|
+
else
|
|
66
|
+
components = []
|
|
67
|
+
end
|
|
68
|
+
if args["data"].is_a?(Hash)
|
|
69
|
+
data = args["data"]
|
|
63
70
|
else
|
|
64
|
-
|
|
65
|
-
data = args["data"].is_a?(Hash) ? args["data"] : {}
|
|
66
|
-
result = Validate.validate_components(components: components, data: data, catalog: catalog)
|
|
67
|
-
{ "attempt" => attempt, "ok" => result["valid"], "errors" => result["errors"] }
|
|
71
|
+
data = {}
|
|
68
72
|
end
|
|
73
|
+
result = Validate.validate_components(components: components, data: data, catalog: catalog)
|
|
74
|
+
record = { "attempt" => attempt, "ok" => result["valid"], "errors" => result["errors"] }
|
|
75
|
+
end
|
|
69
76
|
|
|
70
77
|
attempts << record
|
|
71
78
|
on_attempt&.call(record)
|
|
@@ -82,7 +89,7 @@ module AgUi
|
|
|
82
89
|
{
|
|
83
90
|
"envelope" => exhausted_envelope(max_attempts, attempts),
|
|
84
91
|
"attempts" => attempts,
|
|
85
|
-
"ok"
|
|
92
|
+
"ok" => false,
|
|
86
93
|
}
|
|
87
94
|
end
|
|
88
95
|
end
|
|
@@ -91,8 +98,8 @@ module AgUi
|
|
|
91
98
|
def exhausted_envelope(max_attempts, attempts)
|
|
92
99
|
JSON.generate(
|
|
93
100
|
{
|
|
94
|
-
"error"
|
|
95
|
-
"code"
|
|
101
|
+
"error" => "Failed to generate valid A2UI after #{max_attempts} attempt(s)",
|
|
102
|
+
"code" => "a2ui_recovery_exhausted",
|
|
96
103
|
"attempts" => attempts,
|
|
97
104
|
},
|
|
98
105
|
)
|
data/lib/ag_ui/a2ui/validate.rb
CHANGED
|
@@ -26,42 +26,66 @@ module AgUi
|
|
|
26
26
|
def validate_components(components:, data: nil, catalog: nil, validate_bindings: true)
|
|
27
27
|
if !components.is_a?(Array) || components.empty?
|
|
28
28
|
{
|
|
29
|
-
"valid"
|
|
29
|
+
"valid" => false,
|
|
30
30
|
"errors" => [{
|
|
31
|
-
"code"
|
|
32
|
-
"path"
|
|
31
|
+
"code" => "empty_components",
|
|
32
|
+
"path" => "components",
|
|
33
33
|
"message" => "A2UI components must be a non-empty array",
|
|
34
|
-
}
|
|
34
|
+
}
|
|
35
|
+
],
|
|
35
36
|
}
|
|
36
37
|
else
|
|
37
|
-
validate_populated(
|
|
38
|
+
validate_populated(
|
|
39
|
+
components,
|
|
40
|
+
data,
|
|
41
|
+
catalog,
|
|
42
|
+
validate_bindings,
|
|
43
|
+
)
|
|
38
44
|
end
|
|
39
45
|
end
|
|
40
46
|
|
|
41
47
|
def validate_populated(components, data, catalog, validate_bindings)
|
|
42
48
|
errors = []
|
|
43
49
|
ids = collect_ids(components, errors)
|
|
44
|
-
|
|
50
|
+
if catalog.is_a?(Hash)
|
|
51
|
+
catalog_components = (catalog["components"] || {})
|
|
52
|
+
else
|
|
53
|
+
catalog_components = {}
|
|
54
|
+
end
|
|
45
55
|
|
|
46
56
|
components.each_with_index do |comp, i|
|
|
47
57
|
check_identity(comp, i, errors)
|
|
48
|
-
check_catalog(
|
|
49
|
-
|
|
58
|
+
check_catalog(
|
|
59
|
+
comp,
|
|
60
|
+
i,
|
|
61
|
+
catalog,
|
|
62
|
+
catalog_components,
|
|
63
|
+
errors,
|
|
64
|
+
)
|
|
65
|
+
check_refs_and_bindings(
|
|
66
|
+
comp,
|
|
67
|
+
i,
|
|
68
|
+
ids,
|
|
69
|
+
catalog_components,
|
|
70
|
+
data,
|
|
71
|
+
validate_bindings,
|
|
72
|
+
errors,
|
|
73
|
+
)
|
|
50
74
|
end
|
|
51
75
|
|
|
52
76
|
find_child_cycles(components, catalog_components).each do |cycle|
|
|
53
77
|
chain = (cycle + [cycle.first]).join(" -> ")
|
|
54
78
|
errors << {
|
|
55
|
-
"code"
|
|
56
|
-
"path"
|
|
79
|
+
"code" => "child_cycle",
|
|
80
|
+
"path" => "components[id=#{cycle.first}]",
|
|
57
81
|
"message" => "Child reference cycle detected: #{chain}",
|
|
58
82
|
}
|
|
59
83
|
end
|
|
60
84
|
|
|
61
85
|
unless components.any? { |c| c.is_a?(Hash) && c["id"] == "root" }
|
|
62
86
|
errors << {
|
|
63
|
-
"code"
|
|
64
|
-
"path"
|
|
87
|
+
"code" => "no_root",
|
|
88
|
+
"path" => "components",
|
|
65
89
|
"message" => "No component has id 'root'",
|
|
66
90
|
}
|
|
67
91
|
end
|
|
@@ -70,61 +94,77 @@ module AgUi
|
|
|
70
94
|
end
|
|
71
95
|
|
|
72
96
|
def collect_ids(components, errors)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
97
|
+
Set.new.tap do |ids|
|
|
98
|
+
seen = Set.new
|
|
99
|
+
components.each do |comp|
|
|
100
|
+
if comp.is_a?(Hash)
|
|
101
|
+
cid = comp["id"]
|
|
102
|
+
else
|
|
103
|
+
cid = nil
|
|
104
|
+
end
|
|
105
|
+
if cid.is_a?(String)
|
|
106
|
+
if seen.include?(cid)
|
|
107
|
+
errors << {
|
|
108
|
+
"code" => "duplicate_id",
|
|
109
|
+
"path" => "components[id=#{cid}]",
|
|
110
|
+
"message" => "Duplicate component id '#{cid}'",
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
seen << cid
|
|
114
|
+
ids << cid
|
|
84
115
|
end
|
|
85
|
-
seen << cid
|
|
86
|
-
ids << cid
|
|
87
116
|
end
|
|
88
117
|
end
|
|
89
|
-
ids
|
|
90
118
|
end
|
|
91
119
|
|
|
92
120
|
def check_identity(comp, index, errors)
|
|
93
|
-
|
|
94
|
-
|
|
121
|
+
if comp.is_a?(Hash)
|
|
122
|
+
cid = comp["id"]
|
|
123
|
+
else
|
|
124
|
+
cid = nil
|
|
125
|
+
end
|
|
126
|
+
if comp.is_a?(Hash)
|
|
127
|
+
ctype = comp["component"]
|
|
128
|
+
else
|
|
129
|
+
ctype = nil
|
|
130
|
+
end
|
|
95
131
|
|
|
96
132
|
unless cid.is_a?(String) && !cid.empty?
|
|
97
133
|
errors << {
|
|
98
|
-
"code"
|
|
99
|
-
"path"
|
|
134
|
+
"code" => "missing_id",
|
|
135
|
+
"path" => "components[#{index}].id",
|
|
100
136
|
"message" => "Component at index #{index} is missing a string 'id'",
|
|
101
137
|
}
|
|
102
138
|
end
|
|
103
139
|
unless ctype.is_a?(String) && !ctype.empty?
|
|
104
140
|
errors << {
|
|
105
|
-
"code"
|
|
106
|
-
"path"
|
|
141
|
+
"code" => "missing_component_type",
|
|
142
|
+
"path" => "components[#{index}].component",
|
|
107
143
|
"message" => "Component at index #{index} is missing a string 'component' type",
|
|
108
144
|
}
|
|
109
145
|
end
|
|
110
146
|
end
|
|
111
147
|
|
|
112
148
|
def check_catalog(comp, index, catalog, catalog_components, errors)
|
|
113
|
-
|
|
149
|
+
if comp.is_a?(Hash)
|
|
150
|
+
ctype = comp["component"]
|
|
151
|
+
else
|
|
152
|
+
ctype = nil
|
|
153
|
+
end
|
|
114
154
|
if catalog && ctype.is_a?(String)
|
|
115
155
|
schema = catalog_components[ctype]
|
|
116
156
|
if schema.nil?
|
|
117
157
|
errors << {
|
|
118
|
-
"code"
|
|
119
|
-
"path"
|
|
158
|
+
"code" => "unknown_component",
|
|
159
|
+
"path" => "components[#{index}].component",
|
|
120
160
|
"message" => "Component type '#{ctype}' is not in the catalog",
|
|
121
161
|
}
|
|
122
162
|
else
|
|
123
163
|
(schema["required"] || []).each do |req|
|
|
124
164
|
unless comp.is_a?(Hash) && comp.key?(req)
|
|
125
165
|
errors << {
|
|
126
|
-
"code"
|
|
127
|
-
"path"
|
|
166
|
+
"code" => "missing_required_prop",
|
|
167
|
+
"path" => "components[#{index}].#{req}",
|
|
128
168
|
"message" => "Component '#{ctype}' (index #{index}) is missing required prop '#{req}'",
|
|
129
169
|
}
|
|
130
170
|
end
|
|
@@ -136,13 +176,17 @@ module AgUi
|
|
|
136
176
|
def check_refs_and_bindings(comp, index, ids, catalog_components, data, validate_bindings, errors)
|
|
137
177
|
if comp.is_a?(Hash)
|
|
138
178
|
ctype = comp["component"]
|
|
139
|
-
|
|
179
|
+
if ctype.is_a?(String)
|
|
180
|
+
schema = catalog_components[ctype]
|
|
181
|
+
else
|
|
182
|
+
schema = nil
|
|
183
|
+
end
|
|
140
184
|
|
|
141
185
|
collect_component_ref_edges(comp, schema).each do |(ref_path, ref)|
|
|
142
186
|
unless ids.include?(ref)
|
|
143
187
|
errors << {
|
|
144
|
-
"code"
|
|
145
|
-
"path"
|
|
188
|
+
"code" => "unresolved_child",
|
|
189
|
+
"path" => "components[#{index}].#{ref_path}",
|
|
146
190
|
"message" => "Child reference '#{ref}' does not match any component id",
|
|
147
191
|
}
|
|
148
192
|
end
|
|
@@ -152,8 +196,8 @@ module AgUi
|
|
|
152
196
|
collect_absolute_binding_paths(comp, []).each do |path|
|
|
153
197
|
unless absolute_path_resolves?(path, data || {})
|
|
154
198
|
errors << {
|
|
155
|
-
"code"
|
|
156
|
-
"path"
|
|
199
|
+
"code" => "unresolved_binding",
|
|
200
|
+
"path" => "components[#{index}]",
|
|
157
201
|
"message" => "Binding path '#{path}' does not resolve in the data model",
|
|
158
202
|
}
|
|
159
203
|
end
|
|
@@ -193,21 +237,21 @@ module AgUi
|
|
|
193
237
|
# A bare string id, or a {componentId: ...} template — the two child
|
|
194
238
|
# reference shapes.
|
|
195
239
|
def collect_child_refs(children)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
240
|
+
[].tap do |refs|
|
|
241
|
+
push = ->(v) do
|
|
242
|
+
if v.is_a?(String)
|
|
243
|
+
refs << v
|
|
244
|
+
elsif v.is_a?(Hash) && v["componentId"].is_a?(String)
|
|
245
|
+
refs << v["componentId"]
|
|
246
|
+
end
|
|
202
247
|
end
|
|
203
|
-
end
|
|
204
248
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
249
|
+
if children.is_a?(Array)
|
|
250
|
+
children.each { |v| push.(v) }
|
|
251
|
+
else
|
|
252
|
+
push.(children)
|
|
253
|
+
end
|
|
209
254
|
end
|
|
210
|
-
refs
|
|
211
255
|
end
|
|
212
256
|
|
|
213
257
|
# (path_suffix, ref_id) pairs for every child reference a component
|
|
@@ -217,47 +261,60 @@ module AgUi
|
|
|
217
261
|
# array-of-object item schemas honoured per element — finds Tabs
|
|
218
262
|
# tabItems[].child). Unmarked props are data, never refs.
|
|
219
263
|
def collect_component_ref_edges(comp, schema)
|
|
220
|
-
|
|
264
|
+
[].tap do |edges|
|
|
221
265
|
|
|
222
|
-
|
|
223
|
-
collect_child_refs(value).each { |ref| edges << [field, ref] }
|
|
224
|
-
end
|
|
225
|
-
push_list = ->(field, value) do
|
|
226
|
-
if value.is_a?(Array)
|
|
227
|
-
value.each_with_index do |item, k|
|
|
228
|
-
collect_child_refs(item).each { |ref| edges << ["#{field}[#{k}]", ref] }
|
|
229
|
-
end
|
|
230
|
-
else
|
|
266
|
+
push_single = ->(field, value) do
|
|
231
267
|
collect_child_refs(value).each { |ref| edges << [field, ref] }
|
|
232
268
|
end
|
|
233
|
-
|
|
269
|
+
push_list = ->(field, value) do
|
|
270
|
+
if value.is_a?(Array)
|
|
271
|
+
value.each_with_index do |item, k|
|
|
272
|
+
collect_child_refs(item).each { |ref| edges << ["#{field}[#{k}]", ref] }
|
|
273
|
+
end
|
|
274
|
+
else
|
|
275
|
+
collect_child_refs(value).each { |ref| edges << [field, ref] }
|
|
276
|
+
end
|
|
277
|
+
end
|
|
234
278
|
|
|
235
|
-
|
|
236
|
-
|
|
279
|
+
push_single.("child", comp["child"])
|
|
280
|
+
push_list.("children", comp["children"])
|
|
237
281
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
282
|
+
if schema.is_a?(Hash)
|
|
283
|
+
props = schema["properties"]
|
|
284
|
+
else
|
|
285
|
+
props = nil
|
|
286
|
+
end
|
|
287
|
+
if props.is_a?(Hash)
|
|
288
|
+
props.each do |field, prop_schema|
|
|
289
|
+
if %w[child children].include?(field) || !prop_schema.is_a?(Hash)
|
|
290
|
+
next
|
|
291
|
+
end
|
|
244
292
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
293
|
+
case prop_schema["format"]
|
|
294
|
+
when "componentRef"
|
|
295
|
+
push_single.(field, comp[field])
|
|
296
|
+
when "componentRefList"
|
|
297
|
+
push_list.(field, comp[field])
|
|
298
|
+
else
|
|
299
|
+
collect_array_item_edges(
|
|
300
|
+
comp,
|
|
301
|
+
field,
|
|
302
|
+
prop_schema,
|
|
303
|
+
edges,
|
|
304
|
+
)
|
|
305
|
+
end
|
|
252
306
|
end
|
|
253
307
|
end
|
|
254
308
|
end
|
|
255
|
-
edges
|
|
256
309
|
end
|
|
257
310
|
|
|
258
311
|
def collect_array_item_edges(comp, field, prop_schema, edges)
|
|
259
312
|
items = prop_schema["items"]
|
|
260
|
-
|
|
313
|
+
if items.is_a?(Hash)
|
|
314
|
+
item_props = items["properties"]
|
|
315
|
+
else
|
|
316
|
+
item_props = nil
|
|
317
|
+
end
|
|
261
318
|
arr_val = comp[field]
|
|
262
319
|
if prop_schema["type"] == "array" && item_props.is_a?(Hash) && arr_val.is_a?(Array)
|
|
263
320
|
arr_val.each_with_index do |item, k|
|
|
@@ -289,15 +346,19 @@ module AgUi
|
|
|
289
346
|
end
|
|
290
347
|
|
|
291
348
|
def child_adjacency(components, catalog_components)
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
349
|
+
{}.tap do |adj|
|
|
350
|
+
components.each do |comp|
|
|
351
|
+
if comp.is_a?(Hash) && comp["id"].is_a?(String)
|
|
352
|
+
ctype = comp["component"]
|
|
353
|
+
if ctype.is_a?(String)
|
|
354
|
+
schema = catalog_components[ctype]
|
|
355
|
+
else
|
|
356
|
+
schema = nil
|
|
357
|
+
end
|
|
358
|
+
adj[comp["id"]] = collect_component_ref_edges(comp, schema).map { |(_p, ref)| ref }
|
|
359
|
+
end
|
|
298
360
|
end
|
|
299
361
|
end
|
|
300
|
-
adj
|
|
301
362
|
end
|
|
302
363
|
|
|
303
364
|
# Unique child-reference cycles via ITERATIVE DFS (explicit frame
|