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.
@@ -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" => SNAPSHOT_TOOL,
48
+ "name" => SNAPSHOT_TOOL,
49
49
  "description" =>
50
- "Replace the shared application state with a new snapshot; the " \
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" => "object",
53
+ "parameters" => {
54
+ "type" => "object",
55
55
  "properties" => {
56
56
  "snapshot" => {
57
- "type" => "object",
57
+ "type" => "object",
58
58
  "description" => "The complete new application state.",
59
59
  },
60
60
  },
61
- "required" => %w[snapshot],
61
+ "required" => %w[snapshot],
62
62
  },
63
63
  }.freeze
64
64
 
65
65
  DELTA_DEFINITION = {
66
- "name" => DELTA_TOOL,
66
+ "name" => DELTA_TOOL,
67
67
  "description" =>
68
- "Apply a JSON Patch (RFC 6902) to the shared application state — an " \
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" => "object",
73
+ "parameters" => {
74
+ "type" => "object",
75
75
  "properties" => {
76
76
  "delta" => {
77
- "type" => "array",
77
+ "type" => "array",
78
78
  "description" =>
79
- "JSON Patch operations, e.g. " \
79
+ "JSON Patch operations, e.g. " \
80
80
  "[{\"op\":\"replace\",\"path\":\"/theme\",\"value\":\"dark\"}].",
81
- "items" => { "type" => "object" },
81
+ "items" => { "type" => "object" },
82
82
  },
83
83
  },
84
- "required" => %w[delta],
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[:events] << { type: :state_snapshot, data: { snapshot: snapshot } }
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[:events] << { type: :state_delta, data: { delta: delta } }
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
- content = error ? { "status" => "error", "error" => error } : { "status" => "ok" }
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[:events] << {
193
- type: :tool_call_result,
194
- data: {
195
- message_id: SecureRandom.uuid,
196
+ env.emit(
197
+ :tool_call_result,
198
+ {
199
+ message_id: SecureRandom.uuid,
196
200
  tool_call_id: tool_call.id,
197
- content: json,
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, events: [], tools: [{ "name" => "navigate" }] }
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, events: [], tools: [] })
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, events: [], tools: [], should_exit: true }
264
- AgUi::Middleware::State.new(terminal).call(env)
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
- env[:events].map { |e| e[:type] }.should == %i[state_snapshot tool_call_result]
268
- env[:events][0][:data][:snapshot].should == { "count" => 3 }
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, events: [], tools: [], state: { "theme" => "light" }, should_exit: true }
279
- AgUi::Middleware::State.new(terminal).call(env)
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
- env[:events].map { |e| e[:type] }.should == %i[state_delta tool_call_result]
283
- env[:events][0][:data][:delta].should == delta
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, events: [], tools: [], should_exit: true }
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
- env[:events][0][:type].should == :state_delta
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, events: [], tools: [], state: {}, should_exit: true }
302
- AgUi::Middleware::State.new(terminal).call(env)
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
- env[:events].map { |e| e[:type] }.should == %i[tool_call_result]
305
- JSON.parse(env[:events][0][:data][:content])["status"].should == "error"
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, events: [], tools: [] }
313
- AgUi::Middleware::State.new(terminal).call(env)
314
- JSON.parse(env[:events].last[:data][:content])["status"].should == "error"
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, events: [], tools: [], should_exit: true }
329
- AgUi::Middleware::State.new(terminal).call(env)
351
+ env = { messages: Brute.log, tools: [], should_exit: true }
352
+ events = []
353
+ AgUi::Middleware::State.new(terminal).call(recording(env, events))
330
354
 
331
- env[:events].any? { |e| e[:type] == :state_snapshot }.should == true
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, events: [], tools: [] }
337
- AgUi::Middleware::State.new(->(e) { e[:messages].assistant("hi") }).call(env)
338
- env[:events].should == []
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" => tool[:name].to_s,
75
+ "name" => tool[:name].to_s,
76
76
  "description" => tool[:description].to_s,
77
- "parameters" => tool[:parameters] || { "type" => "object" },
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[:events], tool_call)
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(events, tool_call)
101
- events << {
102
- type: :tool_call_start,
103
- data: { tool_call_id: tool_call.id, tool_call_name: tool_call.name },
104
- }
105
- events << {
106
- type: :tool_call_args,
107
- data: { tool_call_id: tool_call.id, delta: JSON.generate(tool_call.arguments) },
108
- }
109
- events << {
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
- content = result.is_a?(String) ? result : JSON.generate(result)
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[:events] << {
126
- type: :tool_call_result,
127
- data: {
128
- message_id: SecureRandom.uuid,
126
+ env.emit(
127
+ :tool_call_result,
128
+ {
129
+ message_id: SecureRandom.uuid,
129
130
  tool_call_id: tool_call.id,
130
- content: 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, events: [] }
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, events: [] }
173
- AgUi::Middleware::ToolRouter.new(terminal, server_tools: [server_tool]).call(env)
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
- env[:events].map { |e| e[:type] }.should == %i[
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
- env[:events].last[:data][:content].should == "{\"found\":42}"
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, events: [] }
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, events: [] }
215
- AgUi::Middleware::ToolRouter.new(terminal, server_tools: [server_tool]).call(env)
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
- env[:events].count { |e| e[:type] == :tool_call_result }.should == 1
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, events: [] }
233
- AgUi::Middleware::ToolRouter.new(terminal).call(env)
249
+ env = { messages: Brute.log }
250
+ events = []
251
+ AgUi::Middleware::ToolRouter.new(terminal).call(recording(env, events))
234
252
 
235
- env[:events].map { |e| e[:type] }.should == %i[
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
- env[:events][0][:data].should == { tool_call_id: "tc1", tool_call_name: "navigate" }
240
- env[:events][1][:data][:delta].should == "{\"path\":\"/data\"}"
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, events: [] }
246
- AgUi::Middleware::ToolRouter.new(->(e) { e[:messages].assistant("hi") }).call(env)
263
+ env = { messages: Brute.log }
264
+ events = []
265
+ AgUi::Middleware::ToolRouter.new(->(e) { e[:messages].assistant("hi") }).call(recording(env, events))
247
266
 
248
- env[:events].should == []
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
- @data[camel] = if value.is_a?(Definition)
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: to_h,
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, ref =
133
- if (r = schema["$ref"])
134
- [:object, r]
135
- elsif schema["type"] == "array" && (r = schema.dig("items", "$ref"))
136
- [:array, r]
137
- elsif schema["type"] == "object" && (r = schema.dig("additionalProperties", "$ref"))
138
- [:map, r]
139
- end
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
- map = {}
172
- camel_keys.each do |camel|
173
- snake = camel_to_snake(camel)
174
- map[snake] = camel
175
- map[camel] = camel
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)
@@ -42,7 +42,7 @@ module AgUi
42
42
  validation_error = Protocol::JsonSchema::ValidationError.new(
43
43
  errors,
44
44
  definition_name: "RunAgentInput",
45
- data: raw,
45
+ data: raw,
46
46
  )
47
47
  raise InvalidError, validation_error.message
48
48
  end
@@ -26,19 +26,19 @@ module AgUi
26
26
  end
27
27
 
28
28
  base = {
29
- "version" => VERSION_PARITY,
30
- "agents" => { agent_id => agent },
29
+ "version" => VERSION_PARITY,
30
+ "agents" => { agent_id => agent },
31
31
  "audioFileTranscriptionEnabled" => false,
32
- "mode" => "sse",
33
- "threadEndpoints" => {
34
- "list" => false,
35
- "inspect" => false,
36
- "mutations" => false,
32
+ "mode" => "sse",
33
+ "threadEndpoints" => {
34
+ "list" => false,
35
+ "inspect" => false,
36
+ "mutations" => false,
37
37
  "realtimeMetadata" => false,
38
38
  },
39
- "a2uiEnabled" => a2ui_enabled,
40
- "openGenerativeUIEnabled" => false,
41
- "telemetryDisabled" => true,
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: run_id,
71
- validate: validate,
72
- on_event: 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
- 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)
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
- value = prop["default"].nil? ? prop["const"] : prop["default"]
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