agent2agent 1.0.8 → 1.1.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 +165 -117
- data/lib/a2a/client.rb +470 -51
- data/lib/a2a/errors/json_rpc_error.rb +71 -0
- data/lib/a2a/errors/rest_error.rb +68 -0
- data/lib/a2a/errors.rb +535 -0
- data/lib/a2a/faraday/middleware/json_rpc/request.rb +96 -0
- data/lib/a2a/faraday/middleware/json_rpc/response.rb +131 -0
- data/lib/a2a/faraday/middleware/rest/request.rb +166 -0
- data/lib/a2a/faraday/middleware/rest/response.rb +144 -0
- data/lib/a2a/faraday/middleware/schema_request.rb +69 -0
- data/lib/a2a/middleware/extract_message.rb +120 -0
- data/lib/a2a/middleware/fetch_task.rb +228 -0
- data/lib/a2a/middleware/limit_history_length.rb +123 -0
- data/lib/a2a/middleware/limit_pagination_size.rb +133 -0
- data/lib/a2a/middleware/sse_stream.rb +235 -0
- data/lib/a2a/middleware.rb +7 -0
- data/lib/a2a/schema/definition.rb +35 -1
- data/lib/a2a/schema.rb +126 -0
- data/lib/a2a/{bindings → server/bindings}/json_rpc.rb +12 -8
- data/lib/a2a/{bindings → server/bindings}/rest.rb +12 -8
- data/lib/a2a/server/dispatcher.rb +52 -54
- data/lib/a2a/server/env.rb +4 -6
- data/lib/a2a/server/triage.rb +1 -1
- data/lib/a2a/server.rb +10 -10
- data/lib/a2a/sse/event_parser.rb +202 -0
- data/lib/a2a/sse/json_rpc_stream.rb +27 -5
- data/lib/a2a/sse/rest_stream.rb +17 -5
- data/lib/a2a/sse/stream.rb +135 -7
- data/lib/a2a/sse.rb +1 -0
- data/lib/a2a/test_helpers.rb +89 -0
- data/lib/a2a/version.rb +1 -1
- data/lib/a2a.rb +6 -2
- data/lib/traces/provider/a2a/{bindings → server/bindings}/json_rpc.rb +2 -2
- data/lib/traces/provider/a2a/{bindings → server/bindings}/rest.rb +2 -2
- data/lib/traces/provider/a2a.rb +2 -2
- metadata +49 -22
- data/lib/a2a/server/cancel_task.rb +0 -14
- data/lib/a2a/server/create_task_push_notification_config.rb +0 -14
- data/lib/a2a/server/delete_task_push_notification_config.rb +0 -14
- data/lib/a2a/server/get_extended_agent_card.rb +0 -15
- data/lib/a2a/server/get_task.rb +0 -14
- data/lib/a2a/server/get_task_push_notification_config.rb +0 -14
- data/lib/a2a/server/list_task_push_notification_configs.rb +0 -14
- data/lib/a2a/server/list_tasks.rb +0 -14
- data/lib/a2a/server/send_message.rb +0 -14
- data/lib/a2a/server/send_streaming_message.rb +0 -14
- data/lib/a2a/server/subscribe_to_task.rb +0 -14
- data/lib/a2a/store/processor.rb +0 -136
- data/lib/a2a/store/pub_sub.rb +0 -149
- data/lib/a2a/store/sqlite.rb +0 -533
- data/lib/a2a/store/webhooks.rb +0 -94
- data/lib/a2a/store.rb +0 -6
- data/lib/a2a/task_store.rb +0 -315
data/lib/a2a/client.rb
CHANGED
|
@@ -2,89 +2,508 @@
|
|
|
2
2
|
|
|
3
3
|
require "bundler/setup"
|
|
4
4
|
require "a2a"
|
|
5
|
+
require "faraday"
|
|
6
|
+
require "async/http/faraday"
|
|
5
7
|
require "console"
|
|
6
8
|
|
|
7
9
|
module A2A
|
|
8
|
-
#
|
|
10
|
+
# Faraday-based A2A protocol client.
|
|
9
11
|
#
|
|
10
|
-
#
|
|
12
|
+
# Supports both protocol bindings: JSON-RPC 2.0 and HTTP+JSON/REST.
|
|
13
|
+
# Uses the async-http-faraday adapter for fiber-based non-blocking I/O
|
|
14
|
+
# and supports SSE streaming for long-lived connections.
|
|
11
15
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
16
|
+
# Request params are validated against the operation's request schema
|
|
17
|
+
# before sending. Responses are returned as Schema::Definition objects.
|
|
18
|
+
#
|
|
19
|
+
# # JSON-RPC (default)
|
|
20
|
+
# client = A2A::Client.new("http://localhost:9292")
|
|
21
|
+
#
|
|
22
|
+
# # REST
|
|
23
|
+
# client = A2A::Client.new("http://localhost:9292", binding: :rest)
|
|
18
24
|
#
|
|
19
25
|
class Client
|
|
20
|
-
def initialize(url)
|
|
21
|
-
@url
|
|
26
|
+
def initialize(url, binding: :json_rpc, &block)
|
|
27
|
+
@url = url.chomp("/")
|
|
28
|
+
@binding = binding
|
|
29
|
+
@conn = build_connection(&block)
|
|
22
30
|
end
|
|
23
31
|
|
|
24
32
|
# GET /.well-known/agent-card.json
|
|
33
|
+
#
|
|
34
|
+
# Returns an A2A::Schema["Agent Card"] instance.
|
|
25
35
|
def agent_card
|
|
26
|
-
get("/.well-known/agent-card.json")
|
|
36
|
+
response = @conn.get("/.well-known/agent-card.json")
|
|
37
|
+
parsed = response.body
|
|
38
|
+
A2A::Schema["Agent Card"].new(parsed)
|
|
27
39
|
end
|
|
28
40
|
|
|
29
|
-
#
|
|
41
|
+
# Operations — each maps to a Proto operation name.
|
|
30
42
|
Proto.operations.each do |op|
|
|
31
43
|
method_name = op.name.gsub(/([A-Z])/) { "_#{$1.downcase}" }.sub(/^_/, "")
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
45
|
+
if op.server_streaming?
|
|
46
|
+
define_method(method_name) do |params = {}, &block|
|
|
47
|
+
Console.info(self) { "Client #{op.name}: #{params}" }
|
|
48
|
+
invoke_streaming(op, params, &block)
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
define_method(method_name) do |params = {}|
|
|
52
|
+
Console.info(self) { "Client #{op.name}: #{params}" }
|
|
53
|
+
invoke(op, params)
|
|
54
|
+
end
|
|
36
55
|
end
|
|
37
56
|
end
|
|
38
57
|
|
|
39
58
|
private
|
|
40
59
|
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
60
|
+
def build_connection(&block)
|
|
61
|
+
case @binding
|
|
62
|
+
when :json_rpc then build_json_rpc_connection(&block)
|
|
63
|
+
when :rest then build_rest_connection(&block)
|
|
64
|
+
else raise ArgumentError, "Unknown binding: #{@binding}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def build_json_rpc_connection(&block)
|
|
69
|
+
::Faraday.new(url: @url) do |f|
|
|
70
|
+
f.request :a2a_schema
|
|
71
|
+
f.request :a2a_json_rpc
|
|
72
|
+
f.request :json
|
|
73
|
+
|
|
74
|
+
f.response :a2a_json_rpc
|
|
75
|
+
f.response :json
|
|
48
76
|
|
|
49
|
-
|
|
50
|
-
parsed = JSON.parse(response)
|
|
77
|
+
f.adapter :async_http
|
|
51
78
|
|
|
52
|
-
|
|
79
|
+
block&.call(f)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def build_rest_connection(&block)
|
|
84
|
+
::Faraday.new(url: @url) do |f|
|
|
85
|
+
f.request :a2a_schema
|
|
86
|
+
f.request :a2a_rest
|
|
87
|
+
f.request :json
|
|
88
|
+
|
|
89
|
+
f.response :a2a_rest
|
|
90
|
+
f.response :json
|
|
53
91
|
|
|
54
|
-
|
|
55
|
-
|
|
92
|
+
f.adapter :async_http
|
|
93
|
+
|
|
94
|
+
block&.call(f)
|
|
56
95
|
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def invoke(operation, params)
|
|
99
|
+
request = operation.request_schema.new(params)
|
|
100
|
+
request.valid!
|
|
101
|
+
|
|
102
|
+
response = @conn.post("/") do |req|
|
|
103
|
+
req.options.context = { a2a_operation: operation }
|
|
104
|
+
req.body = request
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
response.body
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def invoke_streaming(operation, params, &block)
|
|
111
|
+
request = operation.request_schema.new(params)
|
|
112
|
+
request.valid!
|
|
113
|
+
|
|
114
|
+
parser = A2A::SSE::EventParser.new(binding: @binding)
|
|
115
|
+
|
|
116
|
+
@conn.post("/") do |req|
|
|
117
|
+
req.options.context = { a2a_operation: operation }
|
|
118
|
+
req.body = request
|
|
119
|
+
req.headers["Accept"] = "text/event-stream"
|
|
120
|
+
|
|
121
|
+
if block
|
|
122
|
+
req.options.on_data = proc do |chunk, _size, _env|
|
|
123
|
+
parser.feed(chunk) { |event| block.call(event) }
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
test do
|
|
132
|
+
# --- JSON-RPC binding (default) ---
|
|
133
|
+
|
|
134
|
+
it "generates methods for all Proto operations" do
|
|
135
|
+
client = A2A::Client.new("http://localhost:9292") do |f|
|
|
136
|
+
f.adapter :test do |stub|
|
|
137
|
+
stub.post("/a2a") { |env|
|
|
138
|
+
[200, { "content-type" => "application/json" }, JSON.generate({ "jsonrpc" => "2.0", "id" => 1, "result" => {} })]
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
end
|
|
57
142
|
|
|
58
|
-
|
|
143
|
+
expected = %w[
|
|
144
|
+
send_message
|
|
145
|
+
send_streaming_message
|
|
146
|
+
get_task
|
|
147
|
+
list_tasks
|
|
148
|
+
cancel_task
|
|
149
|
+
subscribe_to_task
|
|
150
|
+
create_task_push_notification_config
|
|
151
|
+
get_task_push_notification_config
|
|
152
|
+
list_task_push_notification_configs
|
|
153
|
+
delete_task_push_notification_config
|
|
154
|
+
get_extended_agent_card
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
expected.each do |name|
|
|
158
|
+
client.respond_to?(name).should == true
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it "agent_card returns a Schema object" do
|
|
163
|
+
client = A2A::Client.new("http://localhost:9292") do |f|
|
|
164
|
+
f.adapter :test do |stub|
|
|
165
|
+
stub.get("/.well-known/agent-card.json") { |env|
|
|
166
|
+
[200, { "content-type" => "application/json" }, JSON.generate({
|
|
167
|
+
"name" => "Test Agent",
|
|
168
|
+
"version" => "1.0.0",
|
|
169
|
+
"capabilities" => { "streaming" => true }
|
|
170
|
+
})]
|
|
171
|
+
}
|
|
59
172
|
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
card = client.agent_card
|
|
176
|
+
card.should.be.kind_of(A2A::Schema::Definition)
|
|
177
|
+
card.name.should == "Test Agent"
|
|
178
|
+
card.version.should == "1.0.0"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it "json_rpc: send_message validates, wraps in JSON-RPC, returns Schema" do
|
|
182
|
+
client = A2A::Client.new("http://localhost:9292") do |f|
|
|
183
|
+
f.adapter :test do |stub|
|
|
184
|
+
stub.post("/a2a") { |env|
|
|
185
|
+
parsed = JSON.parse(env.body)
|
|
186
|
+
parsed["method"].should == "SendMessage"
|
|
187
|
+
parsed["params"]["message"]["role"].should == "ROLE_USER"
|
|
60
188
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
189
|
+
[200, { "content-type" => "application/json" }, JSON.generate({
|
|
190
|
+
"jsonrpc" => "2.0", "id" => parsed["id"],
|
|
191
|
+
"result" => {
|
|
192
|
+
"task" => {
|
|
193
|
+
"id" => "task-1",
|
|
194
|
+
"contextId" => "ctx-1",
|
|
195
|
+
"status" => { "state" => "TASK_STATE_COMPLETED" },
|
|
196
|
+
"artifacts" => [{
|
|
197
|
+
"artifactId" => "a-1",
|
|
198
|
+
"parts" => [{ "text" => "Echo: Hello" }]
|
|
199
|
+
}]
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
})]
|
|
203
|
+
}
|
|
69
204
|
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
result = client.send_message(
|
|
208
|
+
message: {
|
|
209
|
+
message_id: "msg-1",
|
|
210
|
+
role: "ROLE_USER",
|
|
211
|
+
parts: [{ text: "Hello" }]
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
result.should.be.kind_of(A2A::Schema::Definition)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it "json_rpc: get_task returns a Task" do
|
|
218
|
+
client = A2A::Client.new("http://localhost:9292") do |f|
|
|
219
|
+
f.adapter :test do |stub|
|
|
220
|
+
stub.post("/a2a") { |env|
|
|
221
|
+
parsed = JSON.parse(env.body)
|
|
222
|
+
parsed["method"].should == "GetTask"
|
|
223
|
+
parsed["params"]["id"].should == "task-123"
|
|
70
224
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
result
|
|
83
|
-
end.wait
|
|
225
|
+
[200, { "content-type" => "application/json" }, JSON.generate({
|
|
226
|
+
"jsonrpc" => "2.0", "id" => parsed["id"],
|
|
227
|
+
"result" => {
|
|
228
|
+
"id" => "task-123",
|
|
229
|
+
"contextId" => "ctx-456",
|
|
230
|
+
"status" => {
|
|
231
|
+
"state" => "TASK_STATE_SUBMITTED"
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
})]
|
|
235
|
+
}
|
|
84
236
|
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
result = client.get_task(id: "task-123")
|
|
240
|
+
result.should.be.kind_of(A2A::Schema::Definition)
|
|
241
|
+
result.id.should == "task-123"
|
|
242
|
+
result.context_id.should == "ctx-456"
|
|
243
|
+
end
|
|
85
244
|
|
|
86
|
-
|
|
87
|
-
|
|
245
|
+
it "json_rpc: raises on JSON-RPC error" do
|
|
246
|
+
client = A2A::Client.new("http://localhost:9292") do |f|
|
|
247
|
+
f.adapter :test do |stub|
|
|
248
|
+
stub.post("/a2a") { |env|
|
|
249
|
+
[200, { "content-type" => "application/json" }, JSON.generate({
|
|
250
|
+
"jsonrpc" => "2.0",
|
|
251
|
+
"id" => 1,
|
|
252
|
+
"error" => {
|
|
253
|
+
"code" => -32600,
|
|
254
|
+
"message" => "Invalid Request"
|
|
255
|
+
}
|
|
256
|
+
})]
|
|
257
|
+
}
|
|
88
258
|
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
lambda { client.get_task(id: "task-123") }.should.raise(A2A::JsonRpcError)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
it "json_rpc: raises ValidationError on invalid params" do
|
|
265
|
+
client = A2A::Client.new("http://localhost:9292") do |f|
|
|
266
|
+
f.adapter :test do |stub|
|
|
267
|
+
stub.post("/a2a") { |env| [200, { "content-type" => "application/json" }, "{}"] }
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
lambda { client.send_message(message: "not_a_hash") }.should.raise(A2A::Schema::ValidationError)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
it "json_rpc: send_streaming_message sends correct method and Accept header" do
|
|
275
|
+
captured_env = nil
|
|
276
|
+
client = A2A::Client.new("http://localhost:9292") do |f|
|
|
277
|
+
f.adapter :test do |stub|
|
|
278
|
+
stub.post("/a2a") { |env|
|
|
279
|
+
captured_env = env
|
|
280
|
+
[200, { "content-type" => "text/event-stream" }, ""]
|
|
281
|
+
}
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
client.send_streaming_message(
|
|
286
|
+
message: {
|
|
287
|
+
message_id: "msg-1",
|
|
288
|
+
role: "ROLE_USER",
|
|
289
|
+
parts: [{ text: "Hello" }]
|
|
290
|
+
}
|
|
291
|
+
) do |event|
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
parsed = JSON.parse(captured_env.request_body)
|
|
295
|
+
parsed["method"].should == "SendStreamingMessage"
|
|
296
|
+
captured_env.request_headers["Accept"].should == "text/event-stream"
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# --- REST binding ---
|
|
300
|
+
|
|
301
|
+
it "rest: send_message posts to /message:send with application/a2a+json" do
|
|
302
|
+
captured_env = nil
|
|
303
|
+
client = A2A::Client.new("http://localhost:9292", binding: :rest) do |f|
|
|
304
|
+
f.adapter :test do |stub|
|
|
305
|
+
stub.post("/message:send") { |env|
|
|
306
|
+
captured_env = env
|
|
307
|
+
[200, { "content-type" => "application/a2a+json" }, JSON.generate({
|
|
308
|
+
"task" => {
|
|
309
|
+
"id" => "task-1",
|
|
310
|
+
"contextId" => "ctx-1",
|
|
311
|
+
"status" => {
|
|
312
|
+
"state" => "TASK_STATE_COMPLETED"
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
})]
|
|
316
|
+
}
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
result = client.send_message(
|
|
321
|
+
message: {
|
|
322
|
+
message_id: "msg-1",
|
|
323
|
+
role: "ROLE_USER",
|
|
324
|
+
parts: [{ text: "Hello" }]
|
|
325
|
+
}
|
|
326
|
+
)
|
|
327
|
+
result.should.be.kind_of(A2A::Schema::Definition)
|
|
328
|
+
captured_env.request_headers["content-type"].should == "application/a2a+json"
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
it "rest: get_task uses GET /tasks/{id}" do
|
|
332
|
+
client = A2A::Client.new("http://localhost:9292", binding: :rest) do |f|
|
|
333
|
+
f.adapter :test do |stub|
|
|
334
|
+
stub.get("/tasks/task-123") { |env|
|
|
335
|
+
[200, { "content-type" => "application/a2a+json" }, JSON.generate({
|
|
336
|
+
"id" => "task-123",
|
|
337
|
+
"contextId" => "ctx-456",
|
|
338
|
+
"status" => {
|
|
339
|
+
"state" => "TASK_STATE_SUBMITTED"
|
|
340
|
+
}
|
|
341
|
+
})]
|
|
342
|
+
}
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
result = client.get_task(id: "task-123")
|
|
347
|
+
result.should.be.kind_of(A2A::Schema::Definition)
|
|
348
|
+
result.id.should == "task-123"
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
it "rest: list_tasks uses GET /tasks" do
|
|
352
|
+
client = A2A::Client.new("http://localhost:9292", binding: :rest) do |f|
|
|
353
|
+
f.adapter :test do |stub|
|
|
354
|
+
stub.get("/tasks") { |env|
|
|
355
|
+
[200, { "content-type" => "application/a2a+json" }, JSON.generate({
|
|
356
|
+
"tasks" => [
|
|
357
|
+
{ "id" => "t-1", "contextId" => "c-1", "status" => { "state" => "TASK_STATE_COMPLETED" } }
|
|
358
|
+
]
|
|
359
|
+
})]
|
|
360
|
+
}
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
result = client.list_tasks
|
|
365
|
+
result.should.be.kind_of(A2A::Schema::Definition)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
it "rest: cancel_task uses POST /tasks/{id}:cancel" do
|
|
369
|
+
client = A2A::Client.new("http://localhost:9292", binding: :rest) do |f|
|
|
370
|
+
f.adapter :test do |stub|
|
|
371
|
+
stub.post("/tasks/task-123:cancel") { |env|
|
|
372
|
+
[200, { "content-type" => "application/a2a+json" }, JSON.generate({
|
|
373
|
+
"id" => "task-123", "contextId" => "ctx-456",
|
|
374
|
+
"status" => { "state" => "TASK_STATE_CANCELED" }
|
|
375
|
+
})]
|
|
376
|
+
}
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
result = client.cancel_task(id: "task-123")
|
|
381
|
+
result.should.be.kind_of(A2A::Schema::Definition)
|
|
382
|
+
result.id.should == "task-123"
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
it "rest: create_task_push_notification_config uses POST /tasks/{task_id}/pushNotificationConfigs" do
|
|
386
|
+
client = A2A::Client.new("http://localhost:9292", binding: :rest) do |f|
|
|
387
|
+
f.adapter :test do |stub|
|
|
388
|
+
stub.post("/tasks/task-123/pushNotificationConfigs") { |env|
|
|
389
|
+
[200, { "content-type" => "application/a2a+json" }, JSON.generate({
|
|
390
|
+
"id" => "config-1", "taskId" => "task-123",
|
|
391
|
+
"url" => "https://example.com/webhook"
|
|
392
|
+
})]
|
|
393
|
+
}
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
result = client.create_task_push_notification_config(
|
|
398
|
+
task_id: "task-123", url: "https://example.com/webhook"
|
|
399
|
+
)
|
|
400
|
+
result.should.be.kind_of(A2A::Schema::Definition)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
it "rest: get_task_push_notification_config uses GET /tasks/{task_id}/pushNotificationConfigs/{id}" do
|
|
404
|
+
client = A2A::Client.new("http://localhost:9292", binding: :rest) do |f|
|
|
405
|
+
f.adapter :test do |stub|
|
|
406
|
+
stub.get("/tasks/task-123/pushNotificationConfigs/config-1") { |env|
|
|
407
|
+
[200, { "content-type" => "application/a2a+json" }, JSON.generate({
|
|
408
|
+
"id" => "config-1", "taskId" => "task-123",
|
|
409
|
+
"url" => "https://example.com/webhook"
|
|
410
|
+
})]
|
|
411
|
+
}
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
result = client.get_task_push_notification_config(id: "config-1", task_id: "task-123")
|
|
416
|
+
result.should.be.kind_of(A2A::Schema::Definition)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
it "rest: list_task_push_notification_configs uses GET /tasks/{task_id}/pushNotificationConfigs" do
|
|
420
|
+
client = A2A::Client.new("http://localhost:9292", binding: :rest) do |f|
|
|
421
|
+
f.adapter :test do |stub|
|
|
422
|
+
stub.get("/tasks/task-123/pushNotificationConfigs") { |env|
|
|
423
|
+
[200, { "content-type" => "application/a2a+json" }, JSON.generate({
|
|
424
|
+
"pushNotificationConfigs" => []
|
|
425
|
+
})]
|
|
426
|
+
}
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
result = client.list_task_push_notification_configs(task_id: "task-123")
|
|
431
|
+
result.should.be.kind_of(A2A::Schema::Definition)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
it "rest: delete_task_push_notification_config uses DELETE /tasks/{task_id}/pushNotificationConfigs/{id}" do
|
|
435
|
+
client = A2A::Client.new("http://localhost:9292", binding: :rest) do |f|
|
|
436
|
+
f.adapter :test do |stub|
|
|
437
|
+
stub.delete("/tasks/task-123/pushNotificationConfigs/config-1") { |env|
|
|
438
|
+
[200, { "content-type" => "application/a2a+json" }, JSON.generate({})]
|
|
439
|
+
}
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
result = client.delete_task_push_notification_config(id: "config-1", task_id: "task-123")
|
|
444
|
+
result.should == {}
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
it "rest: get_extended_agent_card uses GET /extendedAgentCard" do
|
|
448
|
+
client = A2A::Client.new("http://localhost:9292", binding: :rest) do |f|
|
|
449
|
+
f.adapter :test do |stub|
|
|
450
|
+
stub.get("/extendedAgentCard") { |env|
|
|
451
|
+
[200, { "content-type" => "application/a2a+json" }, JSON.generate({
|
|
452
|
+
"name" => "Extended Agent",
|
|
453
|
+
"version" => "2.0.0",
|
|
454
|
+
"capabilities" => {
|
|
455
|
+
"streaming" => true,
|
|
456
|
+
"extendedAgentCard" => true
|
|
457
|
+
}
|
|
458
|
+
})]
|
|
459
|
+
}
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
result = client.get_extended_agent_card
|
|
464
|
+
result.should.be.kind_of(A2A::Schema::Definition)
|
|
465
|
+
result.name.should == "Extended Agent"
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
it "rest: subscribe_to_task uses GET /tasks/{id}:subscribe with Accept header" do
|
|
469
|
+
captured_env = nil
|
|
470
|
+
client = A2A::Client.new("http://localhost:9292", binding: :rest) do |f|
|
|
471
|
+
f.adapter :test do |stub|
|
|
472
|
+
stub.get("/tasks/task-1:subscribe") { |env|
|
|
473
|
+
captured_env = env
|
|
474
|
+
[200, { "content-type" => "text/event-stream" }, ""]
|
|
475
|
+
}
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
client.subscribe_to_task(id: "task-1") do |event|
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
captured_env.request_headers["Accept"].should == "text/event-stream"
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
it "rest: raises on HTTP 400 error" do
|
|
486
|
+
client = A2A::Client.new("http://localhost:9292", binding: :rest) do |f|
|
|
487
|
+
f.adapter :test do |stub|
|
|
488
|
+
stub.get("/tasks/bad-id") { |env|
|
|
489
|
+
[400, { "content-type" => "application/problem+json" }, JSON.generate({
|
|
490
|
+
"type" => "error",
|
|
491
|
+
"title" => "Bad Request","status" => 400
|
|
492
|
+
})]
|
|
493
|
+
}
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
lambda { client.get_task(id: "bad-id") }.should.raise(A2A::RestError)
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
it "rest: raises ValidationError on invalid params" do
|
|
501
|
+
client = A2A::Client.new("http://localhost:9292", binding: :rest) do |f|
|
|
502
|
+
f.adapter :test do |stub|
|
|
503
|
+
stub.post("/message:send") { |env| [200, { "content-type" => "application/a2a+json" }, "{}"] }
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
lambda { client.send_message(message: "not_a_hash") }.should.raise(A2A::Schema::ValidationError)
|
|
89
508
|
end
|
|
90
509
|
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "a2a"
|
|
5
|
+
|
|
6
|
+
module A2A
|
|
7
|
+
# Raised by the JSON-RPC response middleware when the server returns
|
|
8
|
+
# a JSON-RPC 2.0 error envelope. Preserves the wire code, message,
|
|
9
|
+
# and optional structured data array.
|
|
10
|
+
#
|
|
11
|
+
# JSON-RPC errors are always delivered over HTTP 200 — the error
|
|
12
|
+
# lives inside the JSON-RPC envelope, not the HTTP status.
|
|
13
|
+
#
|
|
14
|
+
class JsonRpcError < Error
|
|
15
|
+
def initialize(message, code:, data: nil)
|
|
16
|
+
@wire_data = data
|
|
17
|
+
super(message, code: code, http_status: 200)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def error_data
|
|
21
|
+
@wire_data
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
test do
|
|
27
|
+
describe "A2A::JsonRpcError" do
|
|
28
|
+
it "has correct code and message" do
|
|
29
|
+
err = A2A::JsonRpcError.new("Task not found", code: -32001)
|
|
30
|
+
err.code.should == -32001
|
|
31
|
+
err.message.should == "Task not found"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "always has http_status 200" do
|
|
35
|
+
err = A2A::JsonRpcError.new("fail", code: -32001)
|
|
36
|
+
err.http_status.should == 200
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "preserves wire data" do
|
|
40
|
+
data = [{
|
|
41
|
+
"@type" => "type.googleapis.com/google.rpc.ErrorInfo",
|
|
42
|
+
"reason" => "TASK_NOT_FOUND",
|
|
43
|
+
"domain" => "a2a-protocol.org",
|
|
44
|
+
"metadata" => { "taskId" => "t-1" },
|
|
45
|
+
}]
|
|
46
|
+
err = A2A::JsonRpcError.new("Task not found", code: -32001, data: data)
|
|
47
|
+
err.error_data.should == data
|
|
48
|
+
err.error_data.first["reason"].should == "TASK_NOT_FOUND"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "returns nil error_data when no data provided" do
|
|
52
|
+
err = A2A::JsonRpcError.new("fail", code: -32600)
|
|
53
|
+
err.error_data.should.be.nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "is a subclass of A2A::Error" do
|
|
57
|
+
err = A2A::JsonRpcError.new("fail", code: -32001)
|
|
58
|
+
err.is_a?(A2A::Error).should == true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "serializes to_h with wire data" do
|
|
62
|
+
data = [{ "reason" => "TASK_NOT_FOUND" }]
|
|
63
|
+
err = A2A::JsonRpcError.new("Task not found", code: -32001, data: data)
|
|
64
|
+
h = err.to_h
|
|
65
|
+
h[:code].should == -32001
|
|
66
|
+
h[:http_status].should == 200
|
|
67
|
+
h[:message].should == "Task not found"
|
|
68
|
+
h[:data].should == data
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|