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.
@@ -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
@@ -45,7 +45,7 @@ module AgUi
45
45
  @app.call(
46
46
  env.merge(
47
47
  "ag_ui.operation" => "stop",
48
- "ag_ui.agent_id" => m[:agent_id],
48
+ "ag_ui.agent_id" => m[:agent_id],
49
49
  "ag_ui.thread_id" => m[:thread_id],
50
50
  ),
51
51
  )
@@ -66,8 +66,8 @@ module AgUi
66
66
  @app.call(
67
67
  env.merge(
68
68
  "ag_ui.operation" => "run",
69
- "ag_ui.agent_id" => "default",
70
- "ag_ui.input" => input,
69
+ "ag_ui.agent_id" => "default",
70
+ "ag_ui.input" => input,
71
71
  ),
72
72
  )
73
73
  rescue RunInput::InvalidError => e
@@ -80,8 +80,8 @@ module AgUi
80
80
  @app.call(
81
81
  env.merge(
82
82
  "ag_ui.operation" => match[:action],
83
- "ag_ui.agent_id" => match[:agent_id],
84
- "ag_ui.input" => input,
83
+ "ag_ui.agent_id" => match[:agent_id],
84
+ "ag_ui.input" => input,
85
85
  ),
86
86
  )
87
87
  rescue RunInput::InvalidError => e
@@ -109,17 +109,20 @@ module AgUi
109
109
  "access-control-allow-origin" => "*",
110
110
  "access-control-allow-methods" => "GET, POST, OPTIONS",
111
111
  "access-control-allow-headers" => "*",
112
- }, []]
112
+ }, []
113
+ ]
113
114
  end
114
115
 
115
116
  def bad_request(details)
116
117
  [400, { "content-type" => "application/json" },
117
- [JSON.generate({ "error" => "Invalid request body", "details" => details })]]
118
+ [JSON.generate({ "error" => "Invalid request body", "details" => details })]
119
+ ]
118
120
  end
119
121
 
120
122
  def not_found
121
123
  [404, { "content-type" => "application/json" },
122
- [JSON.generate({ "error" => "Not found" })]]
124
+ [JSON.generate({ "error" => "Not found" })]
125
+ ]
123
126
  end
124
127
  end
125
128
  end
data/lib/ag_ui/server.rb CHANGED
@@ -45,10 +45,10 @@ module AgUi
45
45
 
46
46
  @agent_id = agent_id
47
47
  @info = Info.payload(
48
- agent_id: agent_id,
49
- description: description,
48
+ agent_id: agent_id,
49
+ description: description,
50
50
  a2ui_enabled: a2ui_enabled,
51
- overrides: info_overrides,
51
+ overrides: info_overrides,
52
52
  )
53
53
  @validate = validate
54
54
  @store = store
@@ -116,8 +116,8 @@ module AgUi
116
116
  thread_id = input&.thread_id.to_s
117
117
  stream = SSE::Stream.new(
118
118
  thread_id: thread_id,
119
- run_id: input&.run_id.to_s,
120
- validate: false,
119
+ run_id: input&.run_id.to_s,
120
+ validate: false,
121
121
  )
122
122
 
123
123
  if @store
@@ -150,7 +150,11 @@ module AgUi
150
150
  # Cancel the thread's in-flight run (the store stops its Async
151
151
  # task; the stream closes via the builder's ensure) and ack.
152
152
  def respond_stop(env)
153
- stopped = @store ? @store.stop(env["ag_ui.thread_id"].to_s) : false
153
+ if @store
154
+ stopped = @store.stop(env["ag_ui.thread_id"].to_s)
155
+ else
156
+ stopped = false
157
+ end
154
158
  [200, JSON_HEADERS.dup, [JSON.generate({ "stopped" => stopped })]]
155
159
  end
156
160
 
@@ -166,11 +170,11 @@ module AgUi
166
170
 
167
171
  @builder.open(
168
172
  thread_id: thread_id,
169
- run_id: run_id,
170
- validate: validate,
171
- on_event: ->(payload) { @store.record(thread_id, payload) },
173
+ run_id: run_id,
174
+ validate: validate,
175
+ on_event: ->(payload) { @store.record(thread_id, payload) },
172
176
  on_finish: -> { @store.finish_run(thread_id) },
173
- on_task: ->(task) { @store.attach_task(thread_id, task) },
177
+ on_task: ->(task) { @store.attach_task(thread_id, task) },
174
178
  &block
175
179
  )
176
180
  end
@@ -20,7 +20,7 @@ module AgUi
20
20
  # One assistant turn per call: seeds the chat from env[:messages]
21
21
  # (including prior tool-call turns), registers env[:tools] as
22
22
  # schema-only CLIENT tools (they halt — never execute server-side),
23
- # streams text deltas into env[:events], then appends either the
23
+ # streams text deltas as AG-UI events through the env, then appends either the
24
24
  # assistant text or the assistant tool-call message to env[:messages]
25
25
  # for the ToolRouter to route.
26
26
  #
@@ -44,7 +44,7 @@ module AgUi
44
44
  apply_tool_choice(chat, env)
45
45
  seed(chat, env[:messages])
46
46
 
47
- emitter = TurnEmitter.new(env[:events])
47
+ emitter = TurnEmitter.new(env)
48
48
  chat.complete do |chunk|
49
49
  thinking = chunk.thinking
50
50
  if thinking&.text
@@ -89,9 +89,13 @@ module AgUi
89
89
  # until its first non-empty delta (tool-call-only turns produce no
90
90
  # empty bubbles), and reasoning closes as soon as text begins —
91
91
  # providers stream thinking strictly before the answer.
92
+ # Turns a provider's chunk stream into the AG-UI text/reasoning
93
+ # vocabulary, emitted through the env's hooks: a reasoning phase opens
94
+ # lazily and closes before any text, and the text phase starts on its
95
+ # first non-empty delta.
92
96
  class TurnEmitter
93
- def initialize(events)
94
- @events = events
97
+ def initialize(env)
98
+ @env = env
95
99
  @text_id = SecureRandom.uuid
96
100
  @reasoning_id = SecureRandom.uuid
97
101
  @text_started = false
@@ -101,10 +105,10 @@ module AgUi
101
105
  def thinking_delta(text)
102
106
  unless text.empty?
103
107
  open_reasoning
104
- @events << {
105
- type: :reasoning_message_content,
106
- data: { message_id: @reasoning_id, delta: text },
107
- }
108
+ @env.emit(
109
+ :reasoning_message_content,
110
+ { message_id: @reasoning_id, delta: text },
111
+ )
108
112
  end
109
113
  end
110
114
 
@@ -112,20 +116,17 @@ module AgUi
112
116
  unless text.empty?
113
117
  close_reasoning
114
118
  unless @text_started
115
- @events << { type: :text_message_start, data: { message_id: @text_id } }
119
+ @env.emit(:text_message_start, { message_id: @text_id })
116
120
  @text_started = true
117
121
  end
118
- @events << {
119
- type: :text_message_content,
120
- data: { message_id: @text_id, delta: text },
121
- }
122
+ @env.emit(:text_message_content, { message_id: @text_id, delta: text })
122
123
  end
123
124
  end
124
125
 
125
126
  def finish
126
127
  close_reasoning
127
128
  if @text_started
128
- @events << { type: :text_message_end, data: { message_id: @text_id } }
129
+ @env.emit(:text_message_end, { message_id: @text_id })
129
130
  end
130
131
  end
131
132
 
@@ -133,16 +134,16 @@ module AgUi
133
134
 
134
135
  def open_reasoning
135
136
  unless @reasoning_open
136
- @events << { type: :reasoning_start, data: { message_id: @reasoning_id } }
137
- @events << { type: :reasoning_message_start, data: { message_id: @reasoning_id } }
137
+ @env.emit(:reasoning_start, { message_id: @reasoning_id })
138
+ @env.emit(:reasoning_message_start, { message_id: @reasoning_id })
138
139
  @reasoning_open = true
139
140
  end
140
141
  end
141
142
 
142
143
  def close_reasoning
143
144
  if @reasoning_open
144
- @events << { type: :reasoning_message_end, data: { message_id: @reasoning_id } }
145
- @events << { type: :reasoning_end, data: { message_id: @reasoning_id } }
145
+ @env.emit(:reasoning_message_end, { message_id: @reasoning_id })
146
+ @env.emit(:reasoning_end, { message_id: @reasoning_id })
146
147
  @reasoning_open = false
147
148
  end
148
149
  end
@@ -168,8 +169,16 @@ module AgUi
168
169
  # force copilotkitSuggest.
169
170
  def apply_tool_choice(chat, env)
170
171
  props = env[:forwarded_props]
171
- choice = props.is_a?(Hash) ? props["toolChoice"] : nil
172
- name = choice.is_a?(Hash) ? choice.dig("function", "name") : nil
172
+ if props.is_a?(Hash)
173
+ choice = props["toolChoice"]
174
+ else
175
+ choice = nil
176
+ end
177
+ if choice.is_a?(Hash)
178
+ name = choice.dig("function", "name")
179
+ else
180
+ name = nil
181
+ end
173
182
  if name
174
183
  chat.with_tools(choice: name)
175
184
  end
@@ -179,9 +188,9 @@ module AgUi
179
188
  (tools || []).each do |tool|
180
189
  chat.with_tool(
181
190
  ClientTool.new(
182
- name: dig_tool(tool, "name"),
191
+ name: dig_tool(tool, "name"),
183
192
  description: dig_tool(tool, "description"),
184
- parameters: dig_tool(tool, "parameters"),
193
+ parameters: dig_tool(tool, "parameters"),
185
194
  ),
186
195
  )
187
196
  end
@@ -212,8 +221,8 @@ module AgUi
212
221
  seed_assistant(chat, message)
213
222
  when :tool
214
223
  chat.add_message(
215
- role: :tool,
216
- content: message.content.to_s,
224
+ role: :tool,
225
+ content: message.content.to_s,
217
226
  tool_call_id: message.tool_call_id,
218
227
  )
219
228
  end
@@ -283,8 +292,8 @@ module AgUi
283
292
 
284
293
  if assistant&.tool_call?
285
294
  env[:messages] << Brute::Message.new(
286
- role: :assistant,
287
- content: assistant.content.to_s,
295
+ role: :assistant,
296
+ content: assistant.content.to_s,
288
297
  tool_calls: assistant.tool_calls.each_value.map do |tc|
289
298
  { id: tc.id, name: tc.name, arguments: tc.arguments }
290
299
  end,
@@ -366,21 +375,38 @@ describe "AgUi::Terminals::RubyLLM" do
366
375
  end
367
376
  end
368
377
 
369
- build_env = -> do
370
- { messages: Brute.log, events: [], metadata: {}, current_iteration: 1 }
378
+ # Brute 6 replaced the env[:events] sink with the hooks registry, so a test
379
+ # that wants to see what the terminal streamed subscribes instead of reading
380
+ # an array off the env. `events` is where the turn's deltas land.
381
+ TURN_EVENTS = %i[
382
+ text_message_start text_message_content text_message_end
383
+ reasoning_start reasoning_message_start reasoning_message_content
384
+ reasoning_message_end reasoning_end
385
+ ].freeze
386
+
387
+ build_env = ->(into = []) do
388
+ hooks = Brute::Hooks::Registry.new
389
+ TURN_EVENTS.each do |event|
390
+ hooks.on(event) { |_env, data, _trace| into << { type: event, data: data } }
391
+ end
392
+ Brute::Hooks::Trace.new(
393
+ { messages: Brute.log, metadata: {}, current_iteration: 1 },
394
+ hooks: hooks,
395
+ )
371
396
  end
372
397
 
373
- it "streams deltas into env[:events] and appends the assistant message" do
398
+ it "streams deltas as AG-UI events and appends the assistant message" do
374
399
  fake = fake_chat_class.new(chunks: ["Hel", "", "lo"], final: "Hello")
375
400
  terminal = AgUi::Terminals::RubyLLM.new(chat_factory: ->(**) { fake })
376
401
 
377
- env = build_env.()
402
+ events = []
403
+ env = build_env.(events)
378
404
  env[:messages].user("hi")
379
405
  terminal.call(env)
380
406
 
381
- types = env[:events].map { |e| e[:type] }
407
+ types = events.map { |e| e[:type] }
382
408
  types.should == [:text_message_start, :text_message_content, :text_message_content, :text_message_end]
383
- env[:events][1][:data][:delta].should == "Hel"
409
+ events[1][:data][:delta].should == "Hel"
384
410
 
385
411
  env[:messages].last.role.should == :assistant
386
412
  env[:messages].last.content.should == "Hello"
@@ -410,12 +436,13 @@ describe "AgUi::Terminals::RubyLLM" do
410
436
  fake = fake_chat_class.new(final: nil, tool_calls: tool_calls)
411
437
  terminal = AgUi::Terminals::RubyLLM.new(chat_factory: ->(**) { fake })
412
438
 
413
- env = build_env.()
439
+ events = []
440
+ env = build_env.(events)
414
441
  env[:messages].user("go to data")
415
442
  terminal.call(env)
416
443
 
417
444
  # No text events for a tool-only turn (lazy start).
418
- env[:events].should == []
445
+ events.should == []
419
446
 
420
447
  last = env[:messages].last
421
448
  last.tool_call?.should.be.true
@@ -466,21 +493,22 @@ describe "AgUi::Terminals::RubyLLM" do
466
493
  )
467
494
  terminal = AgUi::Terminals::RubyLLM.new(thinking: { budget: 1024 }, chat_factory: ->(**) { fake })
468
495
 
469
- env = build_env.()
496
+ events = []
497
+ env = build_env.(events)
470
498
  env[:messages].user("why?")
471
499
  terminal.call(env)
472
500
 
473
501
  fake.thinking_config.should == { budget: 1024 }
474
- env[:events].map { |e| e[:type] }.should == %i[
502
+ events.map { |e| e[:type] }.should == %i[
475
503
  reasoning_start reasoning_message_start
476
504
  reasoning_message_content reasoning_message_content
477
505
  reasoning_message_end reasoning_end
478
506
  text_message_start text_message_content text_message_end
479
507
  ]
480
508
 
481
- reasoning_ids = env[:events].first(6).map { |e| e[:data][:message_id] }.uniq
509
+ reasoning_ids = events.first(6).map { |e| e[:data][:message_id] }.uniq
482
510
  reasoning_ids.length.should == 1
483
- env[:events].last[:data][:message_id].should.not == reasoning_ids.first
511
+ events.last[:data][:message_id].should.not == reasoning_ids.first
484
512
  end
485
513
 
486
514
  it "seeds multimodal user content as ruby_llm Content with attachments" do
@@ -536,7 +564,8 @@ describe "AgUi::Terminals::RubyLLM" do
536
564
  .run(terminal)
537
565
  env["ag_ui.stream"].open(thread_id: input.thread_id, run_id: input.run_id) do |stream|
538
566
  stream.run_started
539
- agent.start(AgUi::Messages.to_brute(input.messages), events: AgUi::EventBridge.new(stream))
567
+ AgUi::EventBridge.new(stream).subscribe(agent)
568
+ agent.start(AgUi::Messages.to_brute(input.messages))
540
569
  stream.run_finished
541
570
  rescue => e
542
571
  stream.run_error(message: e.message, code: e.class.name)
data/lib/ag_ui/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgUi
4
- VERSION = "0.2.0"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/ag_ui.rb CHANGED
@@ -25,6 +25,7 @@ require "ag_ui/event_bridge"
25
25
  require "ag_ui/middleware/system_prompt"
26
26
  require "ag_ui/middleware/forwarded_props"
27
27
  require "ag_ui/middleware/tool_router"
28
+ require "ag_ui/middleware/state"
28
29
  require "ag_ui/a2ui/catalog"
29
30
  require "ag_ui/a2ui/validate"
30
31
  require "ag_ui/a2ui/recovery"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ag-ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - AgUi Contributors
@@ -66,25 +66,47 @@ dependencies:
66
66
  - !ruby/object:Gem::Version
67
67
  version: '2.5'
68
68
  - !ruby/object:Gem::Dependency
69
- name: brute
69
+ name: hana
70
70
  requirement: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '3.0'
75
- - - ">="
74
+ version: '1.3'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
76
80
  - !ruby/object:Gem::Version
77
- version: 3.0.1
81
+ version: '1.3'
82
+ - !ruby/object:Gem::Dependency
83
+ name: brute
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '6.0'
78
89
  type: :runtime
79
90
  prerelease: false
80
91
  version_requirements: !ruby/object:Gem::Requirement
81
92
  requirements:
82
93
  - - "~>"
83
94
  - !ruby/object:Gem::Version
84
- version: '3.0'
85
- - - ">="
95
+ version: '6.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: gem_kit
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
86
101
  - !ruby/object:Gem::Version
87
- version: 3.0.1
102
+ version: '0.2'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.2'
88
110
  - !ruby/object:Gem::Dependency
89
111
  name: ruby_llm
90
112
  requirement: !ruby/object:Gem::Requirement
@@ -183,6 +205,20 @@ dependencies:
183
205
  - - "~>"
184
206
  - !ruby/object:Gem::Version
185
207
  version: '2.1'
208
+ - !ruby/object:Gem::Dependency
209
+ name: gem_kit-release
210
+ requirement: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - "~>"
213
+ - !ruby/object:Gem::Version
214
+ version: '0.3'
215
+ type: :development
216
+ prerelease: false
217
+ version_requirements: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - "~>"
220
+ - !ruby/object:Gem::Version
221
+ version: '0.3'
186
222
  description: Ruby server implementation of the AG-UI protocol (SSE event streaming,
187
223
  run loop, client-tool round-trips, A2UI) for driving CopilotKit frontends from Rack/Falcon.
188
224
  executables: []
@@ -191,7 +227,6 @@ extra_rdoc_files: []
191
227
  files:
192
228
  - LICENSE
193
229
  - data/ag_ui.json
194
- - data/generate-ag-ui-schema.py
195
230
  - lib/ag_ui.rb
196
231
  - lib/ag_ui/a2ui/catalog.rb
197
232
  - lib/ag_ui/a2ui/recovery.rb
@@ -200,6 +235,7 @@ files:
200
235
  - lib/ag_ui/messages.rb
201
236
  - lib/ag_ui/middleware/a2ui.rb
202
237
  - lib/ag_ui/middleware/forwarded_props.rb
238
+ - lib/ag_ui/middleware/state.rb
203
239
  - lib/ag_ui/middleware/system_prompt.rb
204
240
  - lib/ag_ui/middleware/tool_router.rb
205
241
  - lib/ag_ui/protocol/json_schema.rb
@@ -1,50 +0,0 @@
1
- # Generates data/ag_ui.json from the reference Python SDK's pydantic models
2
- # (clone github.com/ag-ui-protocol/ag-ui and point AG_UI_SDK at sdks/python).
3
- #
4
- # Run: uv run --with pydantic data/generate-ag-ui-schema.py [/path/to/ag-ui/sdks/python]
5
- #
6
- # Output shape mirrors a2a's data/a2a.json: a single top-level "definitions"
7
- # map keyed by model class name, refs rewritten to #/definitions/<Model>.
8
- # Wire keys are camelCase (by_alias), matching @ag-ui/core exactly.
9
-
10
- import inspect
11
- import json
12
- import os
13
- import sys
14
-
15
- sdk = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("AG_UI_SDK", "/tmp/ag-ui/sdks/python")
16
- sys.path.insert(0, sdk)
17
-
18
- from pydantic import BaseModel # noqa: E402
19
- from pydantic.json_schema import models_json_schema # noqa: E402
20
-
21
- from ag_ui.core import capabilities, events, types # noqa: E402
22
-
23
- models = []
24
- for mod in (types, events, capabilities):
25
- for _name, obj in sorted(vars(mod).items()):
26
- if (
27
- inspect.isclass(obj)
28
- and issubclass(obj, BaseModel)
29
- and obj.__module__ == mod.__name__
30
- and obj.__name__ != "ConfiguredBaseModel"
31
- ):
32
- models.append(obj)
33
-
34
- _, top = models_json_schema(
35
- [(m, "validation") for m in models],
36
- by_alias=True,
37
- ref_template="#/definitions/{model}",
38
- )
39
-
40
- out = {
41
- "$comment": f"Generated by data/generate-ag-ui-schema.py from ag-ui sdks/python ({len(models)} models). Do not edit.",
42
- "definitions": top["$defs"],
43
- }
44
-
45
- dest = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ag_ui.json")
46
- with open(dest, "w") as f:
47
- json.dump(out, f, indent=2, sort_keys=True)
48
- f.write("\n")
49
-
50
- print(f"wrote {dest}: {len(out['definitions'])} definitions")