ag-ui 0.1.0 → 0.2.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/lib/ag_ui/terminals/ruby_llm.rb +20 -5
- data/lib/ag_ui/version.rb +1 -1
- data/lib/ag_ui.rb +0 -1
- metadata +1 -2
- data/lib/ag_ui/run_loop.rb +0 -285
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ba6811e09735cfc95262ba711a5e953a92d9af8d78ea8b13867f1d1c60fbeedc
|
|
4
|
+
data.tar.gz: 75e995ce5c17476566258e05e5b56de4481b3df8539554e91d687363e15ad456
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7e1bfaaa5c833b60ab7ce9a99d069abd3603eaf8366abf355b73688b07c54fb1d76fd280fb46a00e3e0e01a5cbf9de8d7b4ce7e59b581679eff92f7f891842c2
|
|
7
|
+
data.tar.gz: 205c4a2f519bcbfcf1fcb975de4929da568633d1358a793c78b042424c867918cce430dd6c9289b965ead31bb1a218a4d706b14e7476093a2ac42d7a10a1e21b
|
|
@@ -14,8 +14,8 @@ module AgUi
|
|
|
14
14
|
# require "ag_ui/terminals/ruby_llm"
|
|
15
15
|
#
|
|
16
16
|
# RubyLLM.configure { |c| c.anthropic_api_key = ENV["ANTHROPIC_API_KEY"] }
|
|
17
|
-
#
|
|
18
|
-
#
|
|
17
|
+
# terminal = AgUi::Terminals::RubyLLM.new
|
|
18
|
+
# agent = Brute.agent.use(...).run(terminal) # standard Brute agent
|
|
19
19
|
#
|
|
20
20
|
# One assistant turn per call: seeds the chat from env[:messages]
|
|
21
21
|
# (including prior tool-call turns), registers env[:tools] as
|
|
@@ -517,7 +517,7 @@ describe "AgUi::Terminals::RubyLLM" do
|
|
|
517
517
|
captured.should == [[:anthropic, "claude-sonnet-4-5"], [:anthropic, "claude-sonnet-4-5"]]
|
|
518
518
|
end
|
|
519
519
|
|
|
520
|
-
it "drives the full client-tool run through
|
|
520
|
+
it "drives the full client-tool run through a standard Brute agent + ToolRouter" do
|
|
521
521
|
tool_calls = {
|
|
522
522
|
"tc1" => ::RubyLLM::ToolCall.new(id: "tc1", name: "navigate",
|
|
523
523
|
arguments: { "path" => "/data" }),
|
|
@@ -525,8 +525,23 @@ describe "AgUi::Terminals::RubyLLM" do
|
|
|
525
525
|
fake = fake_chat_class.new(final: nil, tool_calls: tool_calls)
|
|
526
526
|
terminal = AgUi::Terminals::RubyLLM.new(chat_factory: ->(**) { fake })
|
|
527
527
|
|
|
528
|
-
|
|
529
|
-
|
|
528
|
+
app = AgUi.agent(agent_id: "default") do |env|
|
|
529
|
+
input = env["ag_ui.input"]
|
|
530
|
+
agent = Brute.agent
|
|
531
|
+
.use(AgUi::Middleware::SystemPrompt, prompt: "Be terse.", context: input.context)
|
|
532
|
+
.use(AgUi::Middleware::ForwardedProps, props: input.forwarded_props)
|
|
533
|
+
.use(Brute::Middleware::Loop::ToolResult)
|
|
534
|
+
.use(Brute::Middleware::MaxIterations, max_iterations: 10)
|
|
535
|
+
.use(AgUi::Middleware::ToolRouter, tools: input.tools, server_tools: [])
|
|
536
|
+
.run(terminal)
|
|
537
|
+
env["ag_ui.stream"].open(thread_id: input.thread_id, run_id: input.run_id) do |stream|
|
|
538
|
+
stream.run_started
|
|
539
|
+
agent.start(AgUi::Messages.to_brute(input.messages), events: AgUi::EventBridge.new(stream))
|
|
540
|
+
stream.run_finished
|
|
541
|
+
rescue => e
|
|
542
|
+
stream.run_error(message: e.message, code: e.class.name)
|
|
543
|
+
end
|
|
544
|
+
end
|
|
530
545
|
|
|
531
546
|
body = JSON.generate({
|
|
532
547
|
"threadId" => "t1", "runId" => "r1", "state" => nil,
|
data/lib/ag_ui/version.rb
CHANGED
data/lib/ag_ui.rb
CHANGED
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.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- AgUi Contributors
|
|
@@ -206,7 +206,6 @@ files:
|
|
|
206
206
|
- lib/ag_ui/protocol/json_schema/definition.rb
|
|
207
207
|
- lib/ag_ui/protocol/json_schema/validation_error.rb
|
|
208
208
|
- lib/ag_ui/run_input.rb
|
|
209
|
-
- lib/ag_ui/run_loop.rb
|
|
210
209
|
- lib/ag_ui/run_store.rb
|
|
211
210
|
- lib/ag_ui/server.rb
|
|
212
211
|
- lib/ag_ui/server/info.rb
|
data/lib/ag_ui/run_loop.rb
DELETED
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "bundler/setup"
|
|
4
|
-
require "console"
|
|
5
|
-
require "ag_ui"
|
|
6
|
-
|
|
7
|
-
module AgUi
|
|
8
|
-
# The run loop: drives one AG-UI run through a brute turn pipeline.
|
|
9
|
-
#
|
|
10
|
-
# The terminal block is the LLM call (ruby_llm or anything else) — it
|
|
11
|
-
# receives brute's env ({messages:, events:, ...}), streams deltas into
|
|
12
|
-
# env[:events] (translated live to SSE by EventBridge) and appends the
|
|
13
|
-
# assistant message to env[:messages]. The gem stays LLM-agnostic.
|
|
14
|
-
#
|
|
15
|
-
# run_loop = AgUi::RunLoop.new(system_prompt: PROMPT) do |env|
|
|
16
|
-
# env[:events] << { type: :text_message_start, data: { message_id: id } }
|
|
17
|
-
# ... stream provider chunks ...
|
|
18
|
-
# env[:events] << { type: :text_message_end, data: { message_id: id } }
|
|
19
|
-
# env[:messages].assistant(full_text)
|
|
20
|
-
# end
|
|
21
|
-
#
|
|
22
|
-
# app = AgUi.agent(agent_id: "default", &run_loop)
|
|
23
|
-
#
|
|
24
|
-
# Lifecycle per run (the AG-UI contract): RUN_STARTED first; then the
|
|
25
|
-
# pipeline streams; then RUN_FINISHED — or RUN_ERROR if the pipeline
|
|
26
|
-
# raised. Schema-validation failures are wire-contract bugs and re-raise
|
|
27
|
-
# after RUN_ERROR so they fail loudly in dev.
|
|
28
|
-
class RunLoop
|
|
29
|
-
# a2ui: nil/false = off; an AgUi::A2ui::Catalog = on with that catalog;
|
|
30
|
-
# true = on degraded (tool injected, no component schema).
|
|
31
|
-
# server_tools: [{name:, description:, parameters:, handler:}] execute
|
|
32
|
-
# inline and the turn loops (Loop::ToolResult) until the model answers
|
|
33
|
-
# in text, a client tool defers to the browser, or max_iterations hits.
|
|
34
|
-
def initialize(system_prompt: nil, validate: true, middleware: [], a2ui: nil,
|
|
35
|
-
server_tools: [], max_iterations: 10, &terminal)
|
|
36
|
-
unless terminal
|
|
37
|
-
raise ArgumentError, "RunLoop requires a terminal block (the LLM call)"
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
@system_prompt = system_prompt
|
|
41
|
-
@validate = validate
|
|
42
|
-
@middleware = middleware
|
|
43
|
-
@a2ui = a2ui
|
|
44
|
-
@server_tools = server_tools
|
|
45
|
-
@max_iterations = max_iterations
|
|
46
|
-
@terminal = terminal
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def a2ui_enabled?
|
|
50
|
-
!(@a2ui.nil? || @a2ui == false)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# AgUi.agent takes a block; RunLoop quacks like one.
|
|
54
|
-
def to_proc
|
|
55
|
-
run_loop = self
|
|
56
|
-
proc { |rack_env| run_loop.handle(rack_env) }
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def handle(rack_env)
|
|
60
|
-
input = rack_env["ag_ui.input"]
|
|
61
|
-
|
|
62
|
-
rack_env["ag_ui.stream"].open(
|
|
63
|
-
thread_id: input.thread_id,
|
|
64
|
-
run_id: input.run_id,
|
|
65
|
-
validate: @validate,
|
|
66
|
-
) do |stream|
|
|
67
|
-
stream.run_started
|
|
68
|
-
run(stream, input)
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
private
|
|
73
|
-
|
|
74
|
-
def run(stream, input)
|
|
75
|
-
pipeline(input).start(
|
|
76
|
-
Messages.to_brute(input.messages),
|
|
77
|
-
events: EventBridge.new(stream),
|
|
78
|
-
)
|
|
79
|
-
stream.run_finished
|
|
80
|
-
rescue Protocol::JsonSchema::ValidationError => e
|
|
81
|
-
Console.error(self, "wire-contract bug: #{e.message}", e)
|
|
82
|
-
stream.run_error(message: "Internal error", code: "validation")
|
|
83
|
-
raise
|
|
84
|
-
rescue => e
|
|
85
|
-
Console.error(self, "run failed: #{e.class}: #{e.message}", e)
|
|
86
|
-
stream.run_error(message: e.message, code: e.class.name)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def pipeline(input)
|
|
90
|
-
agent = Brute.agent
|
|
91
|
-
agent.use(
|
|
92
|
-
AgUi::Middleware::SystemPrompt,
|
|
93
|
-
prompt: @system_prompt,
|
|
94
|
-
context: input.context,
|
|
95
|
-
)
|
|
96
|
-
agent.use(AgUi::Middleware::ForwardedProps, props: input.forwarded_props)
|
|
97
|
-
agent.use(Brute::Middleware::Loop::ToolResult)
|
|
98
|
-
agent.use(Brute::Middleware::MaxIterations, max_iterations: @max_iterations)
|
|
99
|
-
if a2ui_enabled?
|
|
100
|
-
catalog = @a2ui.is_a?(AgUi::A2ui::Catalog) ? @a2ui : nil
|
|
101
|
-
agent.use(AgUi::Middleware::A2ui, catalog: catalog)
|
|
102
|
-
end
|
|
103
|
-
agent.use(AgUi::Middleware::ToolRouter, tools: input.tools, server_tools: @server_tools)
|
|
104
|
-
@middleware.each do |(klass, options)|
|
|
105
|
-
agent.use(klass, **(options || {}))
|
|
106
|
-
end
|
|
107
|
-
agent.run(@terminal)
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
__END__
|
|
113
|
-
|
|
114
|
-
describe "AgUi::RunLoop" do
|
|
115
|
-
minimal_input = JSON.generate({
|
|
116
|
-
"threadId" => "t1", "runId" => "r1", "state" => nil,
|
|
117
|
-
"messages" => [{ "id" => "u1", "role" => "user", "content" => "hi" }],
|
|
118
|
-
"tools" => [], "context" => [{ "description" => "currentPath", "value" => "/data" }],
|
|
119
|
-
"forwardedProps" => nil,
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
request = ->(app, body) do
|
|
123
|
-
app.call({
|
|
124
|
-
"REQUEST_METHOD" => "POST",
|
|
125
|
-
"PATH_INFO" => "/agent/default/run",
|
|
126
|
-
"rack.input" => StringIO.new(body),
|
|
127
|
-
})
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
read_frames = ->(stream) do
|
|
131
|
-
frames = []
|
|
132
|
-
while (chunk = stream.read)
|
|
133
|
-
frames << JSON.parse(chunk.sub(/\Adata: /, "").strip)
|
|
134
|
-
end
|
|
135
|
-
frames
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
it "streams a full run through a brute pipeline" do
|
|
139
|
-
seen_env = nil
|
|
140
|
-
run_loop = AgUi::RunLoop.new(system_prompt: "Be terse.") do |env|
|
|
141
|
-
seen_env = env
|
|
142
|
-
env[:events] << { type: :text_message_start, data: { message_id: "m1" } }
|
|
143
|
-
env[:events] << { type: :text_message_content, data: { message_id: "m1", delta: "Hello" } }
|
|
144
|
-
env[:events] << { type: :text_message_end, data: { message_id: "m1" } }
|
|
145
|
-
env[:messages].assistant("Hello")
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
app = AgUi.agent(agent_id: "default", &run_loop)
|
|
149
|
-
_status, _headers, body = request.(app, minimal_input)
|
|
150
|
-
|
|
151
|
-
frames = read_frames.(body)
|
|
152
|
-
frames.map { |f| f["type"] }.should == %w[
|
|
153
|
-
RUN_STARTED TEXT_MESSAGE_START TEXT_MESSAGE_CONTENT TEXT_MESSAGE_END RUN_FINISHED
|
|
154
|
-
]
|
|
155
|
-
|
|
156
|
-
# The pipeline saw: system prompt (with context addendum) + history.
|
|
157
|
-
seen_env[:messages].map(&:role).should == [:system, :user, :assistant]
|
|
158
|
-
seen_env[:messages].first.content.should.include?("Be terse.")
|
|
159
|
-
seen_env[:messages].first.content.should.include?("currentPath: /data")
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
it "ends the run with RUN_ERROR when the terminal raises" do
|
|
163
|
-
run_loop = AgUi::RunLoop.new { |_env| raise "provider exploded" }
|
|
164
|
-
app = AgUi.agent(agent_id: "default", &run_loop)
|
|
165
|
-
|
|
166
|
-
_status, _headers, body = request.(app, minimal_input)
|
|
167
|
-
frames = read_frames.(body)
|
|
168
|
-
|
|
169
|
-
frames.map { |f| f["type"] }.should == %w[RUN_STARTED RUN_ERROR]
|
|
170
|
-
frames.last["message"].should == "provider exploded"
|
|
171
|
-
frames.last["code"].should == "RuntimeError"
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
it "supports extra brute middleware" do
|
|
175
|
-
marker = Class.new do
|
|
176
|
-
def initialize(app, note:)
|
|
177
|
-
@app = app
|
|
178
|
-
@note = note
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def call(env)
|
|
182
|
-
env[:metadata][:note] = @note
|
|
183
|
-
@app.call(env)
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
seen = nil
|
|
188
|
-
run_loop = AgUi::RunLoop.new(middleware: [[marker, { note: "hi" }]]) do |env|
|
|
189
|
-
seen = env[:metadata][:note]
|
|
190
|
-
env[:messages].assistant("ok")
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
request.(AgUi.agent(&run_loop), minimal_input)
|
|
194
|
-
seen.should == "hi"
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
it "requires a terminal block" do
|
|
198
|
-
lambda { AgUi::RunLoop.new }.should.raise(ArgumentError)
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
it "executes server tools inline and loops the turn to completion" do
|
|
202
|
-
weather_tool = {
|
|
203
|
-
name: "get_weather",
|
|
204
|
-
description: "Weather for a city",
|
|
205
|
-
parameters: { "type" => "object" },
|
|
206
|
-
handler: ->(args) { { "city" => args["city"], "temp" => 21 } },
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
iterations = 0
|
|
210
|
-
terminal = ->(env) do
|
|
211
|
-
iterations += 1
|
|
212
|
-
if iterations == 1
|
|
213
|
-
env[:messages] << Brute::Message.new(
|
|
214
|
-
role: :assistant, content: nil,
|
|
215
|
-
tool_calls: [{ id: "tc1", name: "get_weather", arguments: { "city" => "Lisbon" } }],
|
|
216
|
-
)
|
|
217
|
-
else
|
|
218
|
-
# The model sees its own call + the executed result.
|
|
219
|
-
env[:messages].last.role.should == :tool
|
|
220
|
-
env[:messages].last.content.should == "{\"city\":\"Lisbon\",\"temp\":21}"
|
|
221
|
-
env[:events] << { type: :text_message_start, data: { message_id: "m2" } }
|
|
222
|
-
env[:events] << { type: :text_message_content, data: { message_id: "m2", delta: "21C in Lisbon" } }
|
|
223
|
-
env[:events] << { type: :text_message_end, data: { message_id: "m2" } }
|
|
224
|
-
env[:messages].assistant("21C in Lisbon")
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
run_loop = AgUi::RunLoop.new(server_tools: [weather_tool], &terminal)
|
|
229
|
-
app = AgUi.agent(agent_id: "default", &run_loop)
|
|
230
|
-
|
|
231
|
-
_status, _headers, stream = request.(app, minimal_input)
|
|
232
|
-
frames = read_frames.(stream)
|
|
233
|
-
|
|
234
|
-
iterations.should == 2
|
|
235
|
-
frames.map { |f| f["type"] }.should == %w[
|
|
236
|
-
RUN_STARTED
|
|
237
|
-
TOOL_CALL_START TOOL_CALL_ARGS TOOL_CALL_END TOOL_CALL_RESULT
|
|
238
|
-
TEXT_MESSAGE_START TEXT_MESSAGE_CONTENT TEXT_MESSAGE_END
|
|
239
|
-
RUN_FINISHED
|
|
240
|
-
]
|
|
241
|
-
frames[4]["content"].should == "{\"city\":\"Lisbon\",\"temp\":21}"
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
it "streams the full A2UI sequence when the model renders a surface" do
|
|
245
|
-
catalog = AgUi::A2ui::Catalog.new(
|
|
246
|
-
catalog_id: "host://ai-catalog",
|
|
247
|
-
components: { "Card" => {} },
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
terminal = ->(env) do
|
|
251
|
-
env[:tools].map { |t| t["name"] }.should.include?("render_a2ui")
|
|
252
|
-
env[:messages].first.content.should.include?("A2UI Component Schema")
|
|
253
|
-
|
|
254
|
-
env[:messages] << Brute::Message.new(
|
|
255
|
-
role: :assistant, content: nil,
|
|
256
|
-
tool_calls: [{ id: "tc9", name: "render_a2ui", arguments: {
|
|
257
|
-
"surfaceId" => "s1",
|
|
258
|
-
"components" => [{ "id" => "root", "component" => "Card" }],
|
|
259
|
-
} }],
|
|
260
|
-
)
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
run_loop = AgUi::RunLoop.new(a2ui: catalog, &terminal)
|
|
264
|
-
app = AgUi.agent(agent_id: "default", &run_loop)
|
|
265
|
-
|
|
266
|
-
_status, _headers, stream = request.(app, minimal_input)
|
|
267
|
-
frames = read_frames.(stream)
|
|
268
|
-
|
|
269
|
-
frames.map { |f| f["type"] }.should == %w[
|
|
270
|
-
RUN_STARTED TOOL_CALL_START TOOL_CALL_ARGS TOOL_CALL_END
|
|
271
|
-
ACTIVITY_SNAPSHOT TOOL_CALL_RESULT RUN_FINISHED
|
|
272
|
-
]
|
|
273
|
-
|
|
274
|
-
snapshot = frames[4]
|
|
275
|
-
snapshot["messageId"].should == "a2ui-surface-tc9"
|
|
276
|
-
snapshot["activityType"].should == "a2ui-surface"
|
|
277
|
-
snapshot["replace"].should == true
|
|
278
|
-
snapshot["content"]["a2ui_operations"][0]["createSurface"]["catalogId"]
|
|
279
|
-
.should == "host://ai-catalog"
|
|
280
|
-
|
|
281
|
-
result = frames[5]
|
|
282
|
-
result["toolCallId"].should == "tc9"
|
|
283
|
-
result["content"].should == "{\"status\":\"rendered\"}"
|
|
284
|
-
end
|
|
285
|
-
end
|