ag-ui 0.3.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 +89 -62
- 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 +77 -52
- 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
- metadata +30 -9
- data/data/generate-ag-ui-schema.py +0 -50
|
@@ -39,49 +39,49 @@ module AgUi
|
|
|
39
39
|
# The frontend applies the same snapshot/patch to its own store; env[:state]
|
|
40
40
|
# is kept coherent so the agent's own later reads (and further deltas) see
|
|
41
41
|
# the current value.
|
|
42
|
-
class State
|
|
42
|
+
class State < Brute::Middleware::Base
|
|
43
43
|
SNAPSHOT_TOOL = "AGUISendStateSnapshot"
|
|
44
44
|
DELTA_TOOL = "AGUISendStateDelta"
|
|
45
45
|
TOOL_NAMES = [SNAPSHOT_TOOL, DELTA_TOOL].freeze
|
|
46
46
|
|
|
47
47
|
SNAPSHOT_DEFINITION = {
|
|
48
|
-
"name"
|
|
48
|
+
"name" => SNAPSHOT_TOOL,
|
|
49
49
|
"description" =>
|
|
50
|
-
|
|
50
|
+
"Replace the shared application state with a new snapshot; the " \
|
|
51
51
|
"frontend re-renders from it. Send the COMPLETE next state object, " \
|
|
52
52
|
"not a diff. Prefer AGUISendStateDelta for small targeted changes.",
|
|
53
|
-
"parameters"
|
|
54
|
-
"type"
|
|
53
|
+
"parameters" => {
|
|
54
|
+
"type" => "object",
|
|
55
55
|
"properties" => {
|
|
56
56
|
"snapshot" => {
|
|
57
|
-
"type"
|
|
57
|
+
"type" => "object",
|
|
58
58
|
"description" => "The complete new application state.",
|
|
59
59
|
},
|
|
60
60
|
},
|
|
61
|
-
"required"
|
|
61
|
+
"required" => %w[snapshot],
|
|
62
62
|
},
|
|
63
63
|
}.freeze
|
|
64
64
|
|
|
65
65
|
DELTA_DEFINITION = {
|
|
66
|
-
"name"
|
|
66
|
+
"name" => DELTA_TOOL,
|
|
67
67
|
"description" =>
|
|
68
|
-
|
|
68
|
+
"Apply a JSON Patch (RFC 6902) to the shared application state — an " \
|
|
69
69
|
"array of {op, path, value} operations. Use for small, targeted " \
|
|
70
70
|
"changes. Paths are JSON Pointers, e.g. " \
|
|
71
71
|
"\"/documentEditor/activeTabId\". ops: add, replace, remove, move, " \
|
|
72
72
|
"copy, test.",
|
|
73
|
-
"parameters"
|
|
74
|
-
"type"
|
|
73
|
+
"parameters" => {
|
|
74
|
+
"type" => "object",
|
|
75
75
|
"properties" => {
|
|
76
76
|
"delta" => {
|
|
77
|
-
"type"
|
|
77
|
+
"type" => "array",
|
|
78
78
|
"description" =>
|
|
79
|
-
|
|
79
|
+
"JSON Patch operations, e.g. " \
|
|
80
80
|
"[{\"op\":\"replace\",\"path\":\"/theme\",\"value\":\"dark\"}].",
|
|
81
|
-
"items"
|
|
81
|
+
"items" => { "type" => "object" },
|
|
82
82
|
},
|
|
83
83
|
},
|
|
84
|
-
"required"
|
|
84
|
+
"required" => %w[delta],
|
|
85
85
|
},
|
|
86
86
|
}.freeze
|
|
87
87
|
|
|
@@ -153,7 +153,7 @@ module AgUi
|
|
|
153
153
|
snapshot = tool_call.arguments["snapshot"]
|
|
154
154
|
if snapshot.is_a?(Hash)
|
|
155
155
|
env[:state] = snapshot
|
|
156
|
-
env
|
|
156
|
+
env.emit(:state_snapshot, { snapshot: snapshot })
|
|
157
157
|
ack(env, tool_call)
|
|
158
158
|
else
|
|
159
159
|
ack(env, tool_call, error: "snapshot must be an object")
|
|
@@ -178,7 +178,7 @@ module AgUi
|
|
|
178
178
|
ack(env, tool_call, error: "invalid JSON Patch: #{e.message}")
|
|
179
179
|
else
|
|
180
180
|
env[:state] = patched
|
|
181
|
-
env
|
|
181
|
+
env.emit(:state_delta, { delta: delta })
|
|
182
182
|
ack(env, tool_call)
|
|
183
183
|
end
|
|
184
184
|
|
|
@@ -186,17 +186,21 @@ module AgUi
|
|
|
186
186
|
# :tool message and Loop::ToolResult continues) and emit TOOL_CALL_RESULT
|
|
187
187
|
# on the wire — the exact shape server tools produce.
|
|
188
188
|
def ack(env, tool_call, error: nil)
|
|
189
|
-
|
|
189
|
+
if error
|
|
190
|
+
content = { "status" => "error", "error" => error }
|
|
191
|
+
else
|
|
192
|
+
content = { "status" => "ok" }
|
|
193
|
+
end
|
|
190
194
|
json = JSON.generate(content)
|
|
191
195
|
env[:messages].tool(json, tool_call_id: tool_call.id)
|
|
192
|
-
env
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
message_id:
|
|
196
|
+
env.emit(
|
|
197
|
+
:tool_call_result,
|
|
198
|
+
{
|
|
199
|
+
message_id: SecureRandom.uuid,
|
|
196
200
|
tool_call_id: tool_call.id,
|
|
197
|
-
content:
|
|
201
|
+
content: json,
|
|
198
202
|
},
|
|
199
|
-
|
|
203
|
+
)
|
|
200
204
|
end
|
|
201
205
|
|
|
202
206
|
def normalize(state)
|
|
@@ -223,6 +227,20 @@ end
|
|
|
223
227
|
__END__
|
|
224
228
|
|
|
225
229
|
describe "AgUi::Middleware::State" do
|
|
230
|
+
|
|
231
|
+
# Brute 6 replaced the env[:events] sink with the hooks registry, so a test
|
|
232
|
+
# that wants to see what a middleware emitted subscribes instead of reading
|
|
233
|
+
# an array off the env. This records the vocabulary State speaks, in
|
|
234
|
+
# emission order.
|
|
235
|
+
STATE_EVENTS = %i[state_snapshot state_delta tool_call_result].freeze
|
|
236
|
+
|
|
237
|
+
def recording(env, into)
|
|
238
|
+
hooks = Brute::Hooks::Registry.new
|
|
239
|
+
STATE_EVENTS.each do |event|
|
|
240
|
+
hooks.on(event) { |_env, data, _trace| into << { type: event, data: data } }
|
|
241
|
+
end
|
|
242
|
+
Brute::Hooks::Trace.new(env, hooks: hooks)
|
|
243
|
+
end
|
|
226
244
|
snapshot_call = ->(args) do
|
|
227
245
|
Brute::Message.new(
|
|
228
246
|
role: :assistant, content: nil,
|
|
@@ -240,9 +258,9 @@ describe "AgUi::Middleware::State" do
|
|
|
240
258
|
it "advertises both state tools idempotently and seeds env[:state] from input" do
|
|
241
259
|
seen = nil
|
|
242
260
|
mw = AgUi::Middleware::State.new(->(env) { seen = env }, state: { "theme" => "light" })
|
|
243
|
-
env = { messages: Brute.log,
|
|
244
|
-
mw.call(env)
|
|
245
|
-
mw.call(env) # second loop iteration — no dupes
|
|
261
|
+
env = { messages: Brute.log, tools: [{ "name" => "navigate" }] }
|
|
262
|
+
mw.call(recording(env, []))
|
|
263
|
+
mw.call(recording(env, [])) # second loop iteration — no dupes
|
|
246
264
|
|
|
247
265
|
seen[:tools].map { |t| t["name"] }.should ==
|
|
248
266
|
%w[navigate AGUISendStateSnapshot AGUISendStateDelta]
|
|
@@ -254,18 +272,19 @@ describe "AgUi::Middleware::State" do
|
|
|
254
272
|
def definition.to_h = { "count" => 1 }
|
|
255
273
|
seen = nil
|
|
256
274
|
AgUi::Middleware::State.new(->(env) { seen = env }, state: definition)
|
|
257
|
-
.call({ messages: Brute.log,
|
|
275
|
+
.call(recording({ messages: Brute.log, tools: [] }, []))
|
|
258
276
|
seen[:state].should == { "count" => 1 }
|
|
259
277
|
end
|
|
260
278
|
|
|
261
279
|
it "AGUISendStateSnapshot: sets state, emits STATE_SNAPSHOT + result, continues the run" do
|
|
262
280
|
terminal = ->(env) { env[:messages] << snapshot_call.("snapshot" => { "count" => 3 }) }
|
|
263
|
-
env = { messages: Brute.log,
|
|
264
|
-
|
|
281
|
+
env = { messages: Brute.log, tools: [], should_exit: true }
|
|
282
|
+
events = []
|
|
283
|
+
AgUi::Middleware::State.new(terminal).call(recording(env, events))
|
|
265
284
|
|
|
266
285
|
env[:state].should == { "count" => 3 }
|
|
267
|
-
|
|
268
|
-
|
|
286
|
+
events.map { |e| e[:type] }.should == %i[state_snapshot tool_call_result]
|
|
287
|
+
events[0][:data][:snapshot].should == { "count" => 3 }
|
|
269
288
|
env[:messages].last.role.should == :tool
|
|
270
289
|
env[:messages].last.tool_call_id.should == "tc1"
|
|
271
290
|
env[:messages].last.content.should == "{\"status\":\"ok\"}"
|
|
@@ -275,43 +294,47 @@ describe "AgUi::Middleware::State" do
|
|
|
275
294
|
it "AGUISendStateDelta: patches state (Hana), emits STATE_DELTA with the raw ops" do
|
|
276
295
|
delta = [{ "op" => "replace", "path" => "/theme", "value" => "dark" }]
|
|
277
296
|
terminal = ->(env) { env[:messages] << delta_call.("delta" => delta) }
|
|
278
|
-
env = { messages: Brute.log,
|
|
279
|
-
|
|
297
|
+
env = { messages: Brute.log, tools: [], state: { "theme" => "light" }, should_exit: true }
|
|
298
|
+
events = []
|
|
299
|
+
AgUi::Middleware::State.new(terminal).call(recording(env, events))
|
|
280
300
|
|
|
281
301
|
env[:state].should == { "theme" => "dark" }
|
|
282
|
-
|
|
283
|
-
|
|
302
|
+
events.map { |e| e[:type] }.should == %i[state_delta tool_call_result]
|
|
303
|
+
events[0][:data][:delta].should == delta
|
|
284
304
|
env[:should_exit].should == false
|
|
285
305
|
end
|
|
286
306
|
|
|
287
307
|
it "adds a nested key via delta against seeded state" do
|
|
288
308
|
delta = [{ "op" => "add", "path" => "/documentEditor", "value" => { "activeTabId" => "doc-2" } }]
|
|
289
309
|
terminal = ->(env) { env[:messages] << delta_call.("delta" => delta) }
|
|
290
|
-
env = { messages: Brute.log,
|
|
310
|
+
env = { messages: Brute.log, tools: [], should_exit: true }
|
|
311
|
+
events = []
|
|
291
312
|
AgUi::Middleware::State.new(terminal, state: { "documentEditor" => { "activeTabId" => "doc-1" } })
|
|
292
|
-
.call(env)
|
|
313
|
+
.call(recording(env, events))
|
|
293
314
|
|
|
294
315
|
env[:state].should == { "documentEditor" => { "activeTabId" => "doc-2" } }
|
|
295
|
-
|
|
316
|
+
events[0][:type].should == :state_delta
|
|
296
317
|
end
|
|
297
318
|
|
|
298
319
|
it "reports a bad patch back to the model as a tool error, no STATE_DELTA emitted" do
|
|
299
320
|
delta = [{ "op" => "replace", "path" => "/missing/deep", "value" => 1 }]
|
|
300
321
|
terminal = ->(env) { env[:messages] << delta_call.("delta" => delta) }
|
|
301
|
-
env = { messages: Brute.log,
|
|
302
|
-
|
|
322
|
+
env = { messages: Brute.log, tools: [], state: {}, should_exit: true }
|
|
323
|
+
events = []
|
|
324
|
+
AgUi::Middleware::State.new(terminal).call(recording(env, events))
|
|
303
325
|
|
|
304
|
-
|
|
305
|
-
JSON.parse(
|
|
326
|
+
events.map { |e| e[:type] }.should == %i[tool_call_result]
|
|
327
|
+
JSON.parse(events[0][:data][:content])["status"].should == "error"
|
|
306
328
|
env[:messages].last.role.should == :tool
|
|
307
329
|
end
|
|
308
330
|
|
|
309
331
|
it "rejects a non-array delta and a non-object snapshot as tool errors" do
|
|
310
332
|
[delta_call.("delta" => "nope"), snapshot_call.("snapshot" => "nope")].each do |msg|
|
|
311
333
|
terminal = ->(env) { env[:messages] << msg }
|
|
312
|
-
env = { messages: Brute.log,
|
|
313
|
-
|
|
314
|
-
|
|
334
|
+
env = { messages: Brute.log, tools: [] }
|
|
335
|
+
events = []
|
|
336
|
+
AgUi::Middleware::State.new(terminal).call(recording(env, events))
|
|
337
|
+
JSON.parse(events.last[:data][:content])["status"].should == "error"
|
|
315
338
|
end
|
|
316
339
|
end
|
|
317
340
|
|
|
@@ -325,17 +348,19 @@ describe "AgUi::Middleware::State" do
|
|
|
325
348
|
],
|
|
326
349
|
)
|
|
327
350
|
end
|
|
328
|
-
env = { messages: Brute.log,
|
|
329
|
-
|
|
351
|
+
env = { messages: Brute.log, tools: [], should_exit: true }
|
|
352
|
+
events = []
|
|
353
|
+
AgUi::Middleware::State.new(terminal).call(recording(env, events))
|
|
330
354
|
|
|
331
|
-
|
|
355
|
+
events.any? { |e| e[:type] == :state_snapshot }.should == true
|
|
332
356
|
env[:should_exit].should == true # browser still needs to run navigate
|
|
333
357
|
end
|
|
334
358
|
|
|
335
359
|
it "does nothing on a plain text turn" do
|
|
336
|
-
env = { messages: Brute.log,
|
|
337
|
-
|
|
338
|
-
|
|
360
|
+
env = { messages: Brute.log, tools: [] }
|
|
361
|
+
events = []
|
|
362
|
+
AgUi::Middleware::State.new(->(e) { e[:messages].assistant("hi") }).call(recording(env, events))
|
|
363
|
+
events.should == []
|
|
339
364
|
env.key?(:should_exit).should == false
|
|
340
365
|
end
|
|
341
366
|
end
|
|
@@ -11,7 +11,7 @@ module AgUi
|
|
|
11
11
|
#
|
|
12
12
|
# Skips entirely when the history already carries a system message
|
|
13
13
|
# (the client can send its own via system/developer roles).
|
|
14
|
-
class SystemPrompt
|
|
14
|
+
class SystemPrompt < Brute::Middleware::Base
|
|
15
15
|
def initialize(app, prompt: nil, context: nil)
|
|
16
16
|
@app = app
|
|
17
17
|
@prompt = prompt
|
|
@@ -26,7 +26,7 @@ module AgUi
|
|
|
26
26
|
# RUN_FINISHED and the browser executes it (multi-run model)
|
|
27
27
|
# - mixed turns: server tools still execute, but any client call
|
|
28
28
|
# ends the run
|
|
29
|
-
class ToolRouter
|
|
29
|
+
class ToolRouter < Brute::Middleware::Base
|
|
30
30
|
def initialize(app, tools: nil, server_tools: nil)
|
|
31
31
|
@app = app
|
|
32
32
|
@client_tools = tools || []
|
|
@@ -72,9 +72,9 @@ module AgUi
|
|
|
72
72
|
|
|
73
73
|
def server_definition(tool)
|
|
74
74
|
{
|
|
75
|
-
"name"
|
|
75
|
+
"name" => tool[:name].to_s,
|
|
76
76
|
"description" => tool[:description].to_s,
|
|
77
|
-
"parameters"
|
|
77
|
+
"parameters" => tool[:parameters] || { "type" => "object" },
|
|
78
78
|
}
|
|
79
79
|
end
|
|
80
80
|
|
|
@@ -82,7 +82,7 @@ module AgUi
|
|
|
82
82
|
client_called = false
|
|
83
83
|
|
|
84
84
|
tool_calls.each do |tool_call|
|
|
85
|
-
emit_call(env
|
|
85
|
+
emit_call(env, tool_call)
|
|
86
86
|
|
|
87
87
|
server = @server_tools[tool_call.name]
|
|
88
88
|
if server
|
|
@@ -97,19 +97,16 @@ module AgUi
|
|
|
97
97
|
end
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
-
def emit_call(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
type: :tool_call_end,
|
|
111
|
-
data: { tool_call_id: tool_call.id },
|
|
112
|
-
}
|
|
100
|
+
def emit_call(env, tool_call)
|
|
101
|
+
env.emit(
|
|
102
|
+
:tool_call_start,
|
|
103
|
+
{ tool_call_id: tool_call.id, tool_call_name: tool_call.name },
|
|
104
|
+
)
|
|
105
|
+
env.emit(
|
|
106
|
+
:tool_call_args,
|
|
107
|
+
{ tool_call_id: tool_call.id, delta: JSON.generate(tool_call.arguments) },
|
|
108
|
+
)
|
|
109
|
+
env.emit(:tool_call_end, { tool_call_id: tool_call.id })
|
|
113
110
|
end
|
|
114
111
|
|
|
115
112
|
def execute_server_tool(env, tool_call, tool)
|
|
@@ -120,16 +117,20 @@ module AgUi
|
|
|
120
117
|
result = { "error" => e.message }
|
|
121
118
|
end
|
|
122
119
|
|
|
123
|
-
|
|
120
|
+
if result.is_a?(String)
|
|
121
|
+
content = result
|
|
122
|
+
else
|
|
123
|
+
content = JSON.generate(result)
|
|
124
|
+
end
|
|
124
125
|
env[:messages].tool(content, tool_call_id: tool_call.id)
|
|
125
|
-
env
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
message_id:
|
|
126
|
+
env.emit(
|
|
127
|
+
:tool_call_result,
|
|
128
|
+
{
|
|
129
|
+
message_id: SecureRandom.uuid,
|
|
129
130
|
tool_call_id: tool_call.id,
|
|
130
|
-
content:
|
|
131
|
+
content: content,
|
|
131
132
|
},
|
|
132
|
-
|
|
133
|
+
)
|
|
133
134
|
end
|
|
134
135
|
end
|
|
135
136
|
end
|
|
@@ -138,6 +139,20 @@ end
|
|
|
138
139
|
__END__
|
|
139
140
|
|
|
140
141
|
describe "AgUi::Middleware::ToolRouter" do
|
|
142
|
+
# Brute 6 replaced the env[:events] sink with the hooks registry, so a test
|
|
143
|
+
# that wants to see what a middleware emitted subscribes instead of reading
|
|
144
|
+
# an array off the env. This records the AG-UI tool vocabulary in emission
|
|
145
|
+
# order, which keeps the assertions below the shape they always had.
|
|
146
|
+
TOOL_EVENTS = %i[tool_call_start tool_call_args tool_call_end tool_call_result].freeze
|
|
147
|
+
|
|
148
|
+
def recording(env, into)
|
|
149
|
+
hooks = Brute::Hooks::Registry.new
|
|
150
|
+
TOOL_EVENTS.each do |event|
|
|
151
|
+
hooks.on(event) { |_env, data, _trace| into << { type: event, data: data } }
|
|
152
|
+
end
|
|
153
|
+
Brute::Hooks::Trace.new(env, hooks: hooks)
|
|
154
|
+
end
|
|
155
|
+
|
|
141
156
|
it "advertises client and server tools idempotently across iterations" do
|
|
142
157
|
seen = nil
|
|
143
158
|
server_tool = { name: "get_time", description: "Now", handler: -> (_args) { "12:00" } }
|
|
@@ -147,9 +162,9 @@ describe "AgUi::Middleware::ToolRouter" do
|
|
|
147
162
|
server_tools: [server_tool],
|
|
148
163
|
)
|
|
149
164
|
|
|
150
|
-
env = { messages: Brute.log
|
|
151
|
-
mw.call(env)
|
|
152
|
-
mw.call(env) # second loop iteration
|
|
165
|
+
env = { messages: Brute.log }
|
|
166
|
+
mw.call(recording(env, []))
|
|
167
|
+
mw.call(recording(env, [])) # second loop iteration
|
|
153
168
|
|
|
154
169
|
seen.map { |t| t["name"] }.should == %w[navigate get_time]
|
|
155
170
|
seen.last["parameters"].should == { "type" => "object" }
|
|
@@ -169,17 +184,18 @@ describe "AgUi::Middleware::ToolRouter" do
|
|
|
169
184
|
)
|
|
170
185
|
end
|
|
171
186
|
|
|
172
|
-
env = { messages: Brute.log
|
|
173
|
-
|
|
187
|
+
env = { messages: Brute.log }
|
|
188
|
+
events = []
|
|
189
|
+
AgUi::Middleware::ToolRouter.new(terminal, server_tools: [server_tool]).call(recording(env, events))
|
|
174
190
|
|
|
175
191
|
env[:messages].last.role.should == :tool
|
|
176
192
|
env[:messages].last.tool_call_id.should == "tc1"
|
|
177
193
|
env[:messages].last.content.should == "{\"found\":42}"
|
|
178
194
|
|
|
179
|
-
|
|
195
|
+
events.map { |e| e[:type] }.should == %i[
|
|
180
196
|
tool_call_start tool_call_args tool_call_end tool_call_result
|
|
181
197
|
]
|
|
182
|
-
|
|
198
|
+
events.last[:data][:content].should == "{\"found\":42}"
|
|
183
199
|
env.key?(:should_exit).should == false
|
|
184
200
|
end
|
|
185
201
|
|
|
@@ -192,8 +208,8 @@ describe "AgUi::Middleware::ToolRouter" do
|
|
|
192
208
|
)
|
|
193
209
|
end
|
|
194
210
|
|
|
195
|
-
env = { messages: Brute.log
|
|
196
|
-
AgUi::Middleware::ToolRouter.new(terminal, server_tools: [server_tool]).call(env)
|
|
211
|
+
env = { messages: Brute.log }
|
|
212
|
+
AgUi::Middleware::ToolRouter.new(terminal, server_tools: [server_tool]).call(recording(env, []))
|
|
197
213
|
|
|
198
214
|
env[:messages].last.content.should == "{\"error\":\"kaput\"}"
|
|
199
215
|
env.key?(:should_exit).should == false
|
|
@@ -211,11 +227,12 @@ describe "AgUi::Middleware::ToolRouter" do
|
|
|
211
227
|
)
|
|
212
228
|
end
|
|
213
229
|
|
|
214
|
-
env = { messages: Brute.log
|
|
215
|
-
|
|
230
|
+
env = { messages: Brute.log }
|
|
231
|
+
events = []
|
|
232
|
+
AgUi::Middleware::ToolRouter.new(terminal, server_tools: [server_tool]).call(recording(env, events))
|
|
216
233
|
|
|
217
234
|
env[:should_exit].should == true
|
|
218
|
-
|
|
235
|
+
events.count { |e| e[:type] == :tool_call_result }.should == 1
|
|
219
236
|
end
|
|
220
237
|
|
|
221
238
|
it "emits TOOL_CALL events and exits when the turn ends on client tool calls" do
|
|
@@ -229,23 +246,25 @@ describe "AgUi::Middleware::ToolRouter" do
|
|
|
229
246
|
)
|
|
230
247
|
end
|
|
231
248
|
|
|
232
|
-
env = { messages: Brute.log
|
|
233
|
-
|
|
249
|
+
env = { messages: Brute.log }
|
|
250
|
+
events = []
|
|
251
|
+
AgUi::Middleware::ToolRouter.new(terminal).call(recording(env, events))
|
|
234
252
|
|
|
235
|
-
|
|
253
|
+
events.map { |e| e[:type] }.should == %i[
|
|
236
254
|
tool_call_start tool_call_args tool_call_end
|
|
237
255
|
tool_call_start tool_call_args tool_call_end
|
|
238
256
|
]
|
|
239
|
-
|
|
240
|
-
|
|
257
|
+
events[0][:data].should == { tool_call_id: "tc1", tool_call_name: "navigate" }
|
|
258
|
+
events[1][:data][:delta].should == "{\"path\":\"/data\"}"
|
|
241
259
|
env[:should_exit].should == true
|
|
242
260
|
end
|
|
243
261
|
|
|
244
262
|
it "does nothing on the way out for plain text turns" do
|
|
245
|
-
env = { messages: Brute.log
|
|
246
|
-
|
|
263
|
+
env = { messages: Brute.log }
|
|
264
|
+
events = []
|
|
265
|
+
AgUi::Middleware::ToolRouter.new(->(e) { e[:messages].assistant("hi") }).call(recording(env, events))
|
|
247
266
|
|
|
248
|
-
|
|
267
|
+
events.should == []
|
|
249
268
|
env.key?(:should_exit).should == false
|
|
250
269
|
end
|
|
251
270
|
end
|
|
@@ -33,12 +33,12 @@ module AgUi
|
|
|
33
33
|
camel = snake[k] || k
|
|
34
34
|
|
|
35
35
|
if props.include?(camel)
|
|
36
|
-
|
|
37
|
-
value.to_h
|
|
36
|
+
if value.is_a?(Definition)
|
|
37
|
+
@data[camel] = value.to_h
|
|
38
38
|
elsif (ref_info = refs[camel])
|
|
39
|
-
wrap_ref(value, ref_info)
|
|
39
|
+
@data[camel] = wrap_ref(value, ref_info)
|
|
40
40
|
else
|
|
41
|
-
value
|
|
41
|
+
@data[camel] = value
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
44
|
end
|
|
@@ -91,7 +91,7 @@ module AgUi
|
|
|
91
91
|
raise ValidationError.new(
|
|
92
92
|
errors,
|
|
93
93
|
definition_name: self.class.definition_name,
|
|
94
|
-
data:
|
|
94
|
+
data: to_h,
|
|
95
95
|
)
|
|
96
96
|
end
|
|
97
97
|
end
|
|
@@ -129,14 +129,18 @@ module AgUi
|
|
|
129
129
|
properties.each do |camel_key, prop_schema|
|
|
130
130
|
schema = unwrap_optional(prop_schema)
|
|
131
131
|
|
|
132
|
-
kind
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
132
|
+
kind = nil
|
|
133
|
+
ref = nil
|
|
134
|
+
if (r = schema["$ref"])
|
|
135
|
+
kind = :object
|
|
136
|
+
ref = r
|
|
137
|
+
elsif schema["type"] == "array" && (r = schema.dig("items", "$ref"))
|
|
138
|
+
kind = :array
|
|
139
|
+
ref = r
|
|
140
|
+
elsif schema["type"] == "object" && (r = schema.dig("additionalProperties", "$ref"))
|
|
141
|
+
kind = :map
|
|
142
|
+
ref = r
|
|
143
|
+
end
|
|
140
144
|
|
|
141
145
|
if ref
|
|
142
146
|
name = ref_name_for(ref)
|
|
@@ -168,13 +172,13 @@ module AgUi
|
|
|
168
172
|
end
|
|
169
173
|
|
|
170
174
|
def build_snake_to_camel(camel_keys)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
{}.tap do |map|
|
|
176
|
+
camel_keys.each do |camel|
|
|
177
|
+
snake = camel_to_snake(camel)
|
|
178
|
+
map[snake] = camel
|
|
179
|
+
map[camel] = camel
|
|
180
|
+
end
|
|
176
181
|
end
|
|
177
|
-
map
|
|
178
182
|
end
|
|
179
183
|
|
|
180
184
|
def camel_to_snake(str)
|
data/lib/ag_ui/run_input.rb
CHANGED
data/lib/ag_ui/server/info.rb
CHANGED
|
@@ -26,19 +26,19 @@ module AgUi
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
base = {
|
|
29
|
-
"version"
|
|
30
|
-
"agents"
|
|
29
|
+
"version" => VERSION_PARITY,
|
|
30
|
+
"agents" => { agent_id => agent },
|
|
31
31
|
"audioFileTranscriptionEnabled" => false,
|
|
32
|
-
"mode"
|
|
33
|
-
"threadEndpoints"
|
|
34
|
-
"list"
|
|
35
|
-
"inspect"
|
|
36
|
-
"mutations"
|
|
32
|
+
"mode" => "sse",
|
|
33
|
+
"threadEndpoints" => {
|
|
34
|
+
"list" => false,
|
|
35
|
+
"inspect" => false,
|
|
36
|
+
"mutations" => false,
|
|
37
37
|
"realtimeMetadata" => false,
|
|
38
38
|
},
|
|
39
|
-
"a2uiEnabled"
|
|
40
|
-
"openGenerativeUIEnabled"
|
|
41
|
-
"telemetryDisabled"
|
|
39
|
+
"a2uiEnabled" => a2ui_enabled,
|
|
40
|
+
"openGenerativeUIEnabled" => false,
|
|
41
|
+
"telemetryDisabled" => true,
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
if a2ui_enabled
|
|
@@ -67,9 +67,9 @@ module AgUi
|
|
|
67
67
|
on_event: nil, on_finish: nil, on_task: nil, &block)
|
|
68
68
|
stream = SSE::Stream.new(
|
|
69
69
|
thread_id: thread_id,
|
|
70
|
-
run_id:
|
|
71
|
-
validate:
|
|
72
|
-
on_event:
|
|
70
|
+
run_id: run_id,
|
|
71
|
+
validate: validate,
|
|
72
|
+
on_event: on_event,
|
|
73
73
|
)
|
|
74
74
|
|
|
75
75
|
@env["ag_ui.stream"] = stream
|
|
@@ -23,20 +23,20 @@ module AgUi
|
|
|
23
23
|
|
|
24
24
|
private
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
def strip_nils(value)
|
|
27
|
+
case value
|
|
28
|
+
when Hash
|
|
29
|
+
value.each_with_object({}) do |(k, v), out|
|
|
30
|
+
unless v.nil?
|
|
31
|
+
out[k] = strip_nils(v)
|
|
32
|
+
end
|
|
32
33
|
end
|
|
34
|
+
when Array
|
|
35
|
+
value.map { |v| strip_nils(v) }
|
|
36
|
+
else
|
|
37
|
+
value
|
|
33
38
|
end
|
|
34
|
-
when Array
|
|
35
|
-
value.map { |v| strip_nils(v) }
|
|
36
|
-
else
|
|
37
|
-
value
|
|
38
39
|
end
|
|
39
|
-
end
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
end
|
|
@@ -90,7 +90,11 @@ module AgUi
|
|
|
90
90
|
property_defaults = AgUi::Protocol::JsonSchema.raw_schema
|
|
91
91
|
.dig("definitions", definition_name, "properties")
|
|
92
92
|
.each_with_object({}) do |(camel, prop), defaults|
|
|
93
|
-
|
|
93
|
+
if prop["default"].nil?
|
|
94
|
+
value = prop["const"]
|
|
95
|
+
else
|
|
96
|
+
value = prop["default"]
|
|
97
|
+
end
|
|
94
98
|
unless value.nil?
|
|
95
99
|
defaults[camel] = value
|
|
96
100
|
end
|