agent2agent 1.1.1 → 2.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/lib/a2a/agent.rb +98 -241
- data/lib/a2a/{faraday → client}/middleware/json_rpc/request.rb +10 -8
- data/lib/a2a/{faraday → client}/middleware/json_rpc/response.rb +10 -9
- data/lib/a2a/{faraday → client}/middleware/rest/request.rb +24 -18
- data/lib/a2a/{faraday → client}/middleware/rest/response.rb +12 -8
- data/lib/a2a/{faraday → client}/middleware/schema_request.rb +9 -8
- data/lib/a2a/client.rb +40 -39
- data/lib/a2a/errors/json_rpc_error.rb +1 -2
- data/lib/a2a/errors/rest_error.rb +1 -2
- data/lib/a2a/errors.rb +1 -2
- data/lib/a2a/protocol/json_schema/definition.rb +276 -0
- data/lib/a2a/protocol/json_schema/validation_error.rb +132 -0
- data/lib/a2a/{schema.rb → protocol/json_schema.rb} +211 -208
- data/lib/a2a/protocol/protobuf.rb +447 -0
- data/lib/a2a/protocol.rb +31 -0
- data/lib/a2a/server/bindings/grpc.rb +37 -0
- data/lib/a2a/server/bindings/json_rpc.rb +29 -9
- data/lib/a2a/server/bindings/rest.rb +26 -13
- data/lib/a2a/server/middleware/extract_message.rb +145 -0
- data/lib/a2a/{middleware → server/middleware}/fetch_task.rb +82 -89
- data/lib/a2a/{middleware → server/middleware}/limit_history_length.rb +40 -49
- data/lib/a2a/{middleware → server/middleware}/limit_pagination_size.rb +36 -42
- data/lib/a2a/server/middleware/sse_stream.rb +236 -0
- data/lib/a2a/{sse → server/sse}/event_parser.rb +70 -69
- data/lib/a2a/server/sse/json_rpc_stream.rb +89 -0
- data/lib/a2a/server/sse/rest_stream.rb +63 -0
- data/lib/a2a/server/sse/stream.rb +261 -0
- data/lib/a2a/server/triage.rb +35 -4
- data/lib/a2a/server/well_known.rb +27 -0
- data/lib/a2a/server.rb +44 -58
- data/lib/a2a/test_helpers.rb +34 -75
- data/lib/a2a/version.rb +1 -1
- data/lib/a2a.rb +7 -11
- data/lib/traces/provider/a2a/agent.rb +25 -0
- data/lib/traces/provider/a2a.rb +1 -1
- metadata +39 -37
- data/lib/a2a/middleware/extract_message.rb +0 -120
- data/lib/a2a/middleware/sse_stream.rb +0 -235
- data/lib/a2a/proto.rb +0 -444
- data/lib/a2a/schema/definition.rb +0 -148
- data/lib/a2a/schema/validation_error.rb +0 -129
- data/lib/a2a/server/dispatcher.rb +0 -127
- data/lib/a2a/sse/json_rpc_stream.rb +0 -88
- data/lib/a2a/sse/rest_stream.rb +0 -62
- data/lib/a2a/sse/stream.rb +0 -257
- data/lib/traces/provider/a2a/server/dispatcher.rb +0 -26
- /data/lib/a2a/{middleware.rb → server/middleware.rb} +0 -0
- /data/lib/a2a/{sse.rb → server/sse.rb} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dcbe8b4376828c18b601b4ed92a16afc7adc63db6ebb3354b144d1510c0a91bb
|
|
4
|
+
data.tar.gz: '096dd66beddb0b01c21fb8fa631318cb202a8abde4cc475de1c05fd3327cdbb3'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d5ff820324edc012ed3c6f417187afe73381572cfa078a226bebe770a88d21330eec4df404d4f0080f340900e51044999ec0c685ae278f259d38e6da80e51223
|
|
7
|
+
data.tar.gz: 4c2fb80eb26b98ad58003599e3f8d87fc864876d10f4c007b6c1fb473225883b3d2a288a806ebd7fd5a3f1917fe193dec8f0cf6682cfd0334261c140ed384a7f
|
data/lib/a2a/agent.rb
CHANGED
|
@@ -1,291 +1,148 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "bundler/setup"
|
|
4
|
+
require "console"
|
|
4
5
|
require "a2a"
|
|
5
6
|
|
|
6
7
|
module A2A
|
|
7
|
-
#
|
|
8
|
+
# Builds a complete A2A server (a rack app) from a handler block —
|
|
9
|
+
# the one-call entrypoint.
|
|
8
10
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
11
|
+
# The block receives the rack env and returns a domain object
|
|
12
|
+
# (A2A::Protocol::JsonSchema::Definition) — the binding layer formats
|
|
13
|
+
# it into HTTP. Route operations with pattern matching:
|
|
12
14
|
#
|
|
13
|
-
# agent
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
15
|
+
# run A2A.agent(agent_card: { "name" => "My Agent" }) do |env|
|
|
16
|
+
# case env["a2a.operation"]
|
|
17
|
+
# in "SendMessage"
|
|
18
|
+
# A2A::Protocol::JsonSchema["Send Message Response"].new({})
|
|
19
|
+
# in "GetTask"
|
|
20
|
+
# task = Task.find_by(id: env["a2a.request"].id)
|
|
21
|
+
# raise A2A::TaskNotFoundError.new(env["a2a.request"].id) unless task
|
|
22
|
+
# task.to_a2a
|
|
18
23
|
# end
|
|
24
|
+
# end
|
|
19
25
|
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
26
|
+
# To route on request contents as well, match a tuple — Definition
|
|
27
|
+
# implements deconstruct_keys, so patterns destructure recursively:
|
|
28
|
+
#
|
|
29
|
+
# case [env["a2a.operation"], env["a2a.request"]]
|
|
30
|
+
# in ["SendMessage", { message: { task_id: String => id } }] if !id.empty?
|
|
31
|
+
# # continuation of task `id`
|
|
32
|
+
# in ["SendMessage", _]
|
|
33
|
+
# # new task
|
|
27
34
|
# end
|
|
28
35
|
#
|
|
29
|
-
|
|
36
|
+
def self.agent(**options, &block) = Server.new(**options, &block)
|
|
37
|
+
|
|
38
|
+
# Wraps the agent block with the server's error boundary:
|
|
39
|
+
#
|
|
40
|
+
# NoMatchingPatternError -> A2A::UnsupportedOperationError, so an
|
|
41
|
+
# unhandled operation falls out of `case ... in` with no else branch
|
|
42
|
+
# A2A::Error -> returned for the binding layer to format
|
|
43
|
+
# anything else -> logged, returned as a JSON-RPC internal error
|
|
30
44
|
#
|
|
31
45
|
class Agent
|
|
32
|
-
attr_reader :handlers
|
|
33
|
-
|
|
34
46
|
def initialize(&block)
|
|
35
|
-
|
|
36
|
-
instance_eval(&block) if block
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Define a handler stack for one or more A2A operations.
|
|
40
|
-
#
|
|
41
|
-
# The block is evaluated at definition time to build the middleware
|
|
42
|
-
# stack. Use `use` to add middleware, `respond_with` to set the
|
|
43
|
-
# terminal handler.
|
|
44
|
-
#
|
|
45
|
-
# on "SendMessage" do
|
|
46
|
-
# use SomeMiddleware
|
|
47
|
-
# respond_with -> (env) { ... }
|
|
48
|
-
# end
|
|
49
|
-
#
|
|
50
|
-
def on(*operations, &block)
|
|
51
|
-
raise ArgumentError, "on requires at least one operation" if operations.empty?
|
|
52
|
-
raise ArgumentError, "on requires a block" unless block
|
|
53
|
-
|
|
54
|
-
builder = StackBuilder.new
|
|
55
|
-
builder.instance_eval(&block)
|
|
56
|
-
|
|
57
|
-
handler = Handler.new(
|
|
58
|
-
operations: operations.flatten,
|
|
59
|
-
app: builder.to_app
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
@handlers << handler
|
|
63
|
-
handler
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Builds a per-operation middleware stack.
|
|
67
|
-
# Collects `use` and `respond_with` calls, compiles into a callable app.
|
|
68
|
-
class StackBuilder
|
|
69
|
-
def initialize
|
|
70
|
-
@middleware = []
|
|
71
|
-
@terminal = nil
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def use(middleware, *args, &block)
|
|
75
|
-
@middleware << [middleware, args, block]
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def respond_with(callable)
|
|
79
|
-
@terminal = callable
|
|
80
|
-
end
|
|
47
|
+
raise ArgumentError, "agent requires a block" unless block
|
|
81
48
|
|
|
82
|
-
|
|
83
|
-
raise ArgumentError, "respond_with is required" unless @terminal
|
|
84
|
-
|
|
85
|
-
app = Terminal.new(@terminal)
|
|
86
|
-
|
|
87
|
-
@middleware.reverse_each do |klass, args, block|
|
|
88
|
-
app = klass.new(app, *args, &block)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
app
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Wraps the respond_with lambda as a callable.
|
|
96
|
-
# Returns whatever the lambda returns.
|
|
97
|
-
class Terminal
|
|
98
|
-
def initialize(callable)
|
|
99
|
-
@callable = callable
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def call(env)
|
|
103
|
-
@callable.call(env)
|
|
104
|
-
end
|
|
49
|
+
@block = block
|
|
105
50
|
end
|
|
106
51
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
@app.call(env)
|
|
119
|
-
end
|
|
52
|
+
def call(env)
|
|
53
|
+
@block.call(env)
|
|
54
|
+
rescue NoMatchingPatternError
|
|
55
|
+
A2A::UnsupportedOperationError.new(
|
|
56
|
+
message: "Operation not supported: #{env["a2a.operation"]}"
|
|
57
|
+
)
|
|
58
|
+
rescue A2A::Error => e
|
|
59
|
+
e
|
|
60
|
+
rescue => e
|
|
61
|
+
Console.error(self, "Agent raised #{e.class}: #{e.message}", e)
|
|
62
|
+
A2A::Error.new("Internal error", code: -32603, http_status: 500)
|
|
120
63
|
end
|
|
121
64
|
end
|
|
122
65
|
end
|
|
123
66
|
|
|
124
|
-
|
|
125
|
-
describe "A2A
|
|
126
|
-
it "
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
respond_with -> (env) { :ok }
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
agent.handlers.length.should == 1
|
|
134
|
-
agent.handlers.first.operations.should == ["SendMessage"]
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
it "registers multiple operations on a single handler" do
|
|
138
|
-
agent = A2A::Agent.new do
|
|
139
|
-
on "SendMessage", "GetTask" do
|
|
140
|
-
respond_with -> (env) { :ok }
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
agent.handlers.first.operations.should == ["SendMessage", "GetTask"]
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
it "raises if on is called without operations" do
|
|
148
|
-
lambda {
|
|
149
|
-
A2A::Agent.new do
|
|
150
|
-
on do
|
|
151
|
-
respond_with -> (env) { :ok }
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
}.should.raise(ArgumentError)
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
it "raises if on is called without a block" do
|
|
158
|
-
lambda {
|
|
159
|
-
agent = A2A::Agent.new
|
|
160
|
-
agent.on "SendMessage"
|
|
161
|
-
}.should.raise(ArgumentError)
|
|
67
|
+
__END__
|
|
68
|
+
describe "A2A.agent" do
|
|
69
|
+
it "returns an A2A::Server" do
|
|
70
|
+
server = A2A.agent(agent_card: { "name" => "Test" }) { |env| nil }
|
|
71
|
+
server.should.be.is_a(A2A::Server)
|
|
162
72
|
end
|
|
73
|
+
end
|
|
163
74
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
on "SendMessage" do
|
|
168
|
-
use Class.new
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
}.should.raise(ArgumentError)
|
|
75
|
+
describe "A2A::Agent" do
|
|
76
|
+
it "requires a block" do
|
|
77
|
+
lambda { A2A::Agent.new }.should.raise(ArgumentError)
|
|
172
78
|
end
|
|
173
79
|
|
|
174
|
-
it "
|
|
175
|
-
agent = A2A::Agent.new
|
|
176
|
-
|
|
177
|
-
respond_with -> (env) { { "echo" => true } }
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
env = { "a2a.request" => {} }
|
|
182
|
-
result = agent.handlers.first.call(env)
|
|
183
|
-
result.should == { "echo" => true }
|
|
80
|
+
it "calls the block with env and returns its result" do
|
|
81
|
+
agent = A2A::Agent.new { |env| env["a2a.operation"] }
|
|
82
|
+
agent.call({ "a2a.operation" => "SendMessage" }).should == "SendMessage"
|
|
184
83
|
end
|
|
185
84
|
|
|
186
|
-
it "
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
"
|
|
190
|
-
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
agent = A2A::Agent.new do
|
|
194
|
-
on "GetTask" do
|
|
195
|
-
respond_with -> (env) { schema_obj }
|
|
85
|
+
it "routes operations with pattern matching" do
|
|
86
|
+
agent = A2A::Agent.new do |env|
|
|
87
|
+
case env["a2a.operation"]
|
|
88
|
+
in "SendMessage" then :sent
|
|
89
|
+
in "GetTask" then :got
|
|
196
90
|
end
|
|
197
91
|
end
|
|
198
92
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
result.should == schema_obj
|
|
93
|
+
agent.call({ "a2a.operation" => "SendMessage" }).should == :sent
|
|
94
|
+
agent.call({ "a2a.operation" => "GetTask" }).should == :got
|
|
202
95
|
end
|
|
203
96
|
|
|
204
|
-
it "
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
on "SendMessage" do
|
|
209
|
-
respond_with -> (env) { seen_env = env; :ok }
|
|
97
|
+
it "returns UnsupportedOperationError when no pattern matches" do
|
|
98
|
+
agent = A2A::Agent.new do |env|
|
|
99
|
+
case env["a2a.operation"]
|
|
100
|
+
in "SendMessage" then :sent
|
|
210
101
|
end
|
|
211
102
|
end
|
|
212
103
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
104
|
+
result = agent.call({ "a2a.operation" => "UnknownOp" })
|
|
105
|
+
result.should.be.is_a(A2A::UnsupportedOperationError)
|
|
106
|
+
result.code.should == -32004
|
|
216
107
|
end
|
|
217
108
|
|
|
218
|
-
it "
|
|
219
|
-
|
|
109
|
+
it "returns A2A::Error when the block raises one" do
|
|
110
|
+
agent = A2A::Agent.new { |env| raise A2A::TaskNotFoundError.new("t-1") }
|
|
220
111
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
mw2 = Class.new do
|
|
227
|
-
define_method(:initialize) { |app| @app = app }
|
|
228
|
-
define_method(:call) { |env| order << :mw2; @app.call(env) }
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
agent = A2A::Agent.new do
|
|
232
|
-
on "SendMessage" do
|
|
233
|
-
use mw1
|
|
234
|
-
use mw2
|
|
235
|
-
respond_with -> (env) { order << :terminal; :done }
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
env = { "a2a.request" => {} }
|
|
240
|
-
agent.handlers.first.call(env)
|
|
241
|
-
order.should == [:mw1, :mw2, :terminal]
|
|
112
|
+
result = agent.call({})
|
|
113
|
+
result.should.be.is_a(A2A::TaskNotFoundError)
|
|
114
|
+
result.code.should == -32001
|
|
242
115
|
end
|
|
243
116
|
|
|
244
|
-
it "
|
|
245
|
-
|
|
246
|
-
define_method(:initialize) { |app| @app = app }
|
|
247
|
-
define_method(:call) { |env| :blocked }
|
|
248
|
-
end
|
|
117
|
+
it "returns internal error when the block raises unexpectedly" do
|
|
118
|
+
agent = A2A::Agent.new { |env| raise "boom" }
|
|
249
119
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
env = { "a2a.request" => {} }
|
|
258
|
-
result = agent.handlers.first.call(env)
|
|
259
|
-
result.should == :blocked
|
|
120
|
+
result = agent.call({})
|
|
121
|
+
result.should.be.is_a(A2A::Error)
|
|
122
|
+
result.code.should == -32603
|
|
123
|
+
result.http_status.should == 500
|
|
260
124
|
end
|
|
261
125
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
raise A2A::TaskNotFoundError.new("task-abc")
|
|
270
|
-
}
|
|
126
|
+
it "matches request contents via deconstruct_keys" do
|
|
127
|
+
agent = A2A::Agent.new do |env|
|
|
128
|
+
case [env["a2a.operation"], env["a2a.request"]]
|
|
129
|
+
in ["SendMessage", { message: { task_id: String => id } }] if !id.empty?
|
|
130
|
+
[:continuation, id]
|
|
131
|
+
in ["SendMessage", _]
|
|
132
|
+
:new_task
|
|
271
133
|
end
|
|
272
134
|
end
|
|
273
135
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
agent = A2A::Agent.new do
|
|
280
|
-
on "SendMessage" do
|
|
281
|
-
respond_with -> (env) {
|
|
282
|
-
raise RuntimeError, "unexpected bug"
|
|
283
|
-
}
|
|
284
|
-
end
|
|
285
|
-
end
|
|
136
|
+
request = A2A::Protocol::JsonSchema["Send Message Request"].new(
|
|
137
|
+
message: { "taskId" => "t-1", "role" => "ROLE_USER" }
|
|
138
|
+
)
|
|
139
|
+
agent.call({ "a2a.operation" => "SendMessage", "a2a.request" => request })
|
|
140
|
+
.should == [:continuation, "t-1"]
|
|
286
141
|
|
|
287
|
-
|
|
288
|
-
|
|
142
|
+
request = A2A::Protocol::JsonSchema["Send Message Request"].new(
|
|
143
|
+
message: { "role" => "ROLE_USER" }
|
|
144
|
+
)
|
|
145
|
+
agent.call({ "a2a.operation" => "SendMessage", "a2a.request" => request })
|
|
146
|
+
.should == :new_task
|
|
289
147
|
end
|
|
290
148
|
end
|
|
291
|
-
end
|
|
@@ -5,7 +5,7 @@ require "a2a"
|
|
|
5
5
|
require "faraday"
|
|
6
6
|
|
|
7
7
|
module A2A
|
|
8
|
-
|
|
8
|
+
class Client
|
|
9
9
|
module Middleware
|
|
10
10
|
module JsonRpc
|
|
11
11
|
# Faraday request middleware that wraps the request body in a
|
|
@@ -19,7 +19,8 @@ module A2A
|
|
|
19
19
|
operation = env.request.context&.dig(:a2a_operation)
|
|
20
20
|
return unless operation
|
|
21
21
|
|
|
22
|
-
env.url
|
|
22
|
+
# POST to the server's mount root — env.url already carries the
|
|
23
|
+
# base path (e.g. "/agent1" when mounted at a subpath).
|
|
23
24
|
env.method = :post
|
|
24
25
|
|
|
25
26
|
env.body = {
|
|
@@ -41,13 +42,14 @@ module A2A
|
|
|
41
42
|
end
|
|
42
43
|
end
|
|
43
44
|
|
|
44
|
-
::Faraday::Request.register_middleware(a2a_json_rpc: A2A::
|
|
45
|
+
::Faraday::Request.register_middleware(a2a_json_rpc: A2A::Client::Middleware::JsonRpc::Request)
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
__END__
|
|
48
|
+
describe "A2A::Client::Middleware::JsonRpc::Request" do
|
|
49
|
+
middleware = A2A::Client::Middleware::JsonRpc::Request
|
|
50
|
+
operation = A2A::Protocol::Protobuf.operation("SendMessage")
|
|
49
51
|
|
|
50
|
-
it "wraps body in JSON-RPC 2.0 envelope and sets path to /
|
|
52
|
+
it "wraps body in JSON-RPC 2.0 envelope and sets path to /" do
|
|
51
53
|
env = ::Faraday::Env.new
|
|
52
54
|
env.url = URI.parse("http://localhost:9292/")
|
|
53
55
|
env.body = { "message" => { "role" => "ROLE_USER" } }
|
|
@@ -56,7 +58,7 @@ test do
|
|
|
56
58
|
|
|
57
59
|
middleware.new(nil).on_request(env)
|
|
58
60
|
|
|
59
|
-
env.url.path.should == "/
|
|
61
|
+
env.url.path.should == "/"
|
|
60
62
|
env.method.should == :post
|
|
61
63
|
env.body[:jsonrpc].should == "2.0"
|
|
62
64
|
env.body[:id].should == 1
|
|
@@ -6,11 +6,11 @@ require "faraday"
|
|
|
6
6
|
require "json"
|
|
7
7
|
|
|
8
8
|
module A2A
|
|
9
|
-
|
|
9
|
+
class Client
|
|
10
10
|
module Middleware
|
|
11
11
|
module JsonRpc
|
|
12
12
|
# Faraday response middleware that unwraps JSON-RPC 2.0 responses
|
|
13
|
-
# and converts the result into A2A::
|
|
13
|
+
# and converts the result into A2A::Protocol::JsonSchema::Definition objects.
|
|
14
14
|
#
|
|
15
15
|
# Reads env.request.context[:a2a_operation] to determine which
|
|
16
16
|
# Schema class to instantiate. If no operation is set, passes through.
|
|
@@ -55,11 +55,12 @@ module A2A
|
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
::Faraday::Response.register_middleware(a2a_json_rpc: A2A::
|
|
58
|
+
::Faraday::Response.register_middleware(a2a_json_rpc: A2A::Client::Middleware::JsonRpc::Response)
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
__END__
|
|
61
|
+
describe "A2A::Client::Middleware::JsonRpc::Response" do
|
|
62
|
+
middleware = A2A::Client::Middleware::JsonRpc::Response
|
|
63
|
+
operation = A2A::Protocol::Protobuf.operation("GetTask")
|
|
63
64
|
|
|
64
65
|
it "unwraps JSON-RPC result into Schema object" do
|
|
65
66
|
env = ::Faraday::Env.new
|
|
@@ -75,7 +76,7 @@ test do
|
|
|
75
76
|
|
|
76
77
|
middleware.new(nil).on_complete(env)
|
|
77
78
|
|
|
78
|
-
env.body.should.be.kind_of(A2A::
|
|
79
|
+
env.body.should.be.kind_of(A2A::Protocol::JsonSchema::Definition)
|
|
79
80
|
env.body.id.should == "task-123"
|
|
80
81
|
env.body.context_id.should == "ctx-456"
|
|
81
82
|
end
|
|
@@ -93,7 +94,7 @@ test do
|
|
|
93
94
|
end
|
|
94
95
|
|
|
95
96
|
it "returns raw hash when response_schema is nil" do
|
|
96
|
-
op = A2A::
|
|
97
|
+
op = A2A::Protocol::Protobuf.operation("DeleteTaskPushNotificationConfig")
|
|
97
98
|
env = ::Faraday::Env.new
|
|
98
99
|
env.body = { "jsonrpc" => "2.0", "id" => 1, "result" => {} }
|
|
99
100
|
env.request = ::Faraday::RequestOptions.new
|
|
@@ -125,7 +126,7 @@ test do
|
|
|
125
126
|
|
|
126
127
|
middleware.new(nil).on_complete(env)
|
|
127
128
|
|
|
128
|
-
env.body.should.be.kind_of(A2A::
|
|
129
|
+
env.body.should.be.kind_of(A2A::Protocol::JsonSchema::Definition)
|
|
129
130
|
env.body.id.should == "task-1"
|
|
130
131
|
end
|
|
131
132
|
end
|
|
@@ -5,7 +5,7 @@ require "a2a"
|
|
|
5
5
|
require "faraday"
|
|
6
6
|
|
|
7
7
|
module A2A
|
|
8
|
-
|
|
8
|
+
class Client
|
|
9
9
|
module Middleware
|
|
10
10
|
module REST
|
|
11
11
|
# Faraday request middleware that rewrites the request for the
|
|
@@ -13,8 +13,9 @@ module A2A
|
|
|
13
13
|
#
|
|
14
14
|
# Reads env.request.context[:a2a_operation] to determine the
|
|
15
15
|
# HTTP verb and path. Interpolates path parameters from the
|
|
16
|
-
# request body (e.g. {id=*} placeholders)
|
|
17
|
-
#
|
|
16
|
+
# request body (e.g. {id=*} placeholders) and prefixes the
|
|
17
|
+
# /rest mount point. For GET/DELETE operations, remaining
|
|
18
|
+
# params become query string parameters.
|
|
18
19
|
#
|
|
19
20
|
# Sets Content-Type to application/a2a+json for POST requests.
|
|
20
21
|
#
|
|
@@ -27,12 +28,16 @@ module A2A
|
|
|
27
28
|
path = interpolate_path(operation.rest_path, params)
|
|
28
29
|
remaining = remove_path_params(operation.rest_path, params)
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
# Prefix /rest under the base path, so servers mounted at a
|
|
32
|
+
# subpath (e.g. "/agent1") keep working.
|
|
33
|
+
env.url.path = "#{env.url.path.chomp("/")}/rest#{path}"
|
|
31
34
|
env.method = operation.rest_verb.to_sym
|
|
32
35
|
|
|
33
36
|
if [:get, :delete].include?(env.method)
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
# The URL is already built by the time request middleware runs,
|
|
38
|
+
# so query params must be encoded into env.url directly —
|
|
39
|
+
# env.params is not consulted after this point.
|
|
40
|
+
env.url.query = URI.encode_www_form(remaining) unless remaining.empty?
|
|
36
41
|
env.body = nil
|
|
37
42
|
else
|
|
38
43
|
# Serialize to JSON here rather than relying on Faraday's
|
|
@@ -80,13 +85,14 @@ module A2A
|
|
|
80
85
|
end
|
|
81
86
|
end
|
|
82
87
|
|
|
83
|
-
::Faraday::Request.register_middleware(a2a_rest: A2A::
|
|
88
|
+
::Faraday::Request.register_middleware(a2a_rest: A2A::Client::Middleware::REST::Request)
|
|
84
89
|
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
__END__
|
|
91
|
+
describe "A2A::Client::Middleware::REST::Request" do
|
|
92
|
+
middleware = A2A::Client::Middleware::REST::Request
|
|
87
93
|
|
|
88
94
|
it "rewrites POST operation with correct path, verb, and content-type" do
|
|
89
|
-
operation = A2A::
|
|
95
|
+
operation = A2A::Protocol::Protobuf.operation("SendMessage")
|
|
90
96
|
env = ::Faraday::Env.new
|
|
91
97
|
env.url = URI.parse("http://localhost:9292/")
|
|
92
98
|
env.body = { "message" => { "role" => "ROLE_USER" } }
|
|
@@ -97,14 +103,14 @@ test do
|
|
|
97
103
|
|
|
98
104
|
middleware.new(nil).on_request(env)
|
|
99
105
|
|
|
100
|
-
env.url.path.should == "/message:send"
|
|
106
|
+
env.url.path.should == "/rest/message:send"
|
|
101
107
|
env.method.should == :post
|
|
102
108
|
JSON.parse(env.body).should == { "message" => { "role" => "ROLE_USER" } }
|
|
103
109
|
env.request_headers["content-type"].should == "application/a2a+json"
|
|
104
110
|
end
|
|
105
111
|
|
|
106
112
|
it "rewrites GET operation with path interpolation and query params" do
|
|
107
|
-
operation = A2A::
|
|
113
|
+
operation = A2A::Protocol::Protobuf.operation("GetTask")
|
|
108
114
|
env = ::Faraday::Env.new
|
|
109
115
|
env.url = URI.parse("http://localhost:9292/")
|
|
110
116
|
env.body = { "id" => "task-123", "historyLength" => "5" }
|
|
@@ -115,14 +121,14 @@ test do
|
|
|
115
121
|
|
|
116
122
|
middleware.new(nil).on_request(env)
|
|
117
123
|
|
|
118
|
-
env.url.path.should == "/tasks/task-123"
|
|
124
|
+
env.url.path.should == "/rest/tasks/task-123"
|
|
119
125
|
env.method.should == :get
|
|
120
126
|
env.body.should.be.nil
|
|
121
|
-
env.
|
|
127
|
+
env.url.query.should == "historyLength=5"
|
|
122
128
|
end
|
|
123
129
|
|
|
124
130
|
it "rewrites DELETE operation with multi-param path interpolation" do
|
|
125
|
-
operation = A2A::
|
|
131
|
+
operation = A2A::Protocol::Protobuf.operation("DeleteTaskPushNotificationConfig")
|
|
126
132
|
env = ::Faraday::Env.new
|
|
127
133
|
env.url = URI.parse("http://localhost:9292/")
|
|
128
134
|
env.body = { "taskId" => "task-123", "id" => "config-1" }
|
|
@@ -133,13 +139,13 @@ test do
|
|
|
133
139
|
|
|
134
140
|
middleware.new(nil).on_request(env)
|
|
135
141
|
|
|
136
|
-
env.url.path.should == "/tasks/task-123/pushNotificationConfigs/config-1"
|
|
142
|
+
env.url.path.should == "/rest/tasks/task-123/pushNotificationConfigs/config-1"
|
|
137
143
|
env.method.should == :delete
|
|
138
144
|
env.body.should.be.nil
|
|
139
145
|
end
|
|
140
146
|
|
|
141
147
|
it "rewrites POST cancel with path interpolation" do
|
|
142
|
-
operation = A2A::
|
|
148
|
+
operation = A2A::Protocol::Protobuf.operation("CancelTask")
|
|
143
149
|
env = ::Faraday::Env.new
|
|
144
150
|
env.url = URI.parse("http://localhost:9292/")
|
|
145
151
|
env.body = { "id" => "task-123", "metadata" => { "key" => "val" } }
|
|
@@ -150,7 +156,7 @@ test do
|
|
|
150
156
|
|
|
151
157
|
middleware.new(nil).on_request(env)
|
|
152
158
|
|
|
153
|
-
env.url.path.should == "/tasks/task-123:cancel"
|
|
159
|
+
env.url.path.should == "/rest/tasks/task-123:cancel"
|
|
154
160
|
env.method.should == :post
|
|
155
161
|
JSON.parse(env.body).should == { "metadata" => { "key" => "val" } }
|
|
156
162
|
end
|