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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/a2a/agent.rb +98 -241
  3. data/lib/a2a/{faraday → client}/middleware/json_rpc/request.rb +10 -8
  4. data/lib/a2a/{faraday → client}/middleware/json_rpc/response.rb +10 -9
  5. data/lib/a2a/{faraday → client}/middleware/rest/request.rb +24 -18
  6. data/lib/a2a/{faraday → client}/middleware/rest/response.rb +12 -8
  7. data/lib/a2a/{faraday → client}/middleware/schema_request.rb +9 -8
  8. data/lib/a2a/client.rb +40 -39
  9. data/lib/a2a/errors/json_rpc_error.rb +1 -2
  10. data/lib/a2a/errors/rest_error.rb +1 -2
  11. data/lib/a2a/errors.rb +1 -2
  12. data/lib/a2a/protocol/json_schema/definition.rb +276 -0
  13. data/lib/a2a/protocol/json_schema/validation_error.rb +132 -0
  14. data/lib/a2a/{schema.rb → protocol/json_schema.rb} +211 -208
  15. data/lib/a2a/protocol/protobuf.rb +447 -0
  16. data/lib/a2a/protocol.rb +31 -0
  17. data/lib/a2a/server/bindings/grpc.rb +37 -0
  18. data/lib/a2a/server/bindings/json_rpc.rb +29 -9
  19. data/lib/a2a/server/bindings/rest.rb +26 -13
  20. data/lib/a2a/server/middleware/extract_message.rb +145 -0
  21. data/lib/a2a/{middleware → server/middleware}/fetch_task.rb +82 -89
  22. data/lib/a2a/{middleware → server/middleware}/limit_history_length.rb +40 -49
  23. data/lib/a2a/{middleware → server/middleware}/limit_pagination_size.rb +36 -42
  24. data/lib/a2a/server/middleware/sse_stream.rb +236 -0
  25. data/lib/a2a/{sse → server/sse}/event_parser.rb +70 -69
  26. data/lib/a2a/server/sse/json_rpc_stream.rb +89 -0
  27. data/lib/a2a/server/sse/rest_stream.rb +63 -0
  28. data/lib/a2a/server/sse/stream.rb +261 -0
  29. data/lib/a2a/server/triage.rb +35 -4
  30. data/lib/a2a/server/well_known.rb +27 -0
  31. data/lib/a2a/server.rb +44 -58
  32. data/lib/a2a/test_helpers.rb +34 -75
  33. data/lib/a2a/version.rb +1 -1
  34. data/lib/a2a.rb +7 -11
  35. data/lib/traces/provider/a2a/agent.rb +25 -0
  36. data/lib/traces/provider/a2a.rb +1 -1
  37. metadata +39 -37
  38. data/lib/a2a/middleware/extract_message.rb +0 -120
  39. data/lib/a2a/middleware/sse_stream.rb +0 -235
  40. data/lib/a2a/proto.rb +0 -444
  41. data/lib/a2a/schema/definition.rb +0 -148
  42. data/lib/a2a/schema/validation_error.rb +0 -129
  43. data/lib/a2a/server/dispatcher.rb +0 -127
  44. data/lib/a2a/sse/json_rpc_stream.rb +0 -88
  45. data/lib/a2a/sse/rest_stream.rb +0 -62
  46. data/lib/a2a/sse/stream.rb +0 -257
  47. data/lib/traces/provider/a2a/server/dispatcher.rb +0 -26
  48. /data/lib/a2a/{middleware.rb → server/middleware.rb} +0 -0
  49. /data/lib/a2a/{sse.rb → server/sse.rb} +0 -0
data/lib/a2a/proto.rb DELETED
@@ -1,444 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/setup"
4
- require "a2a"
5
-
6
- module A2A
7
- # Parses the A2A protocol's .proto file to extract service operations
8
- # and bridge them to Schema definition classes for request/response
9
- # validation.
10
- #
11
- # The .proto file is the single normative source for the A2A protocol.
12
- # This module extracts the service definition (RPC names, request/response
13
- # types, streaming flags, HTTP bindings) and connects each operation to
14
- # the corresponding Schema[] class so callers can validate data:
15
- #
16
- # op = A2A::Proto.operation("SendMessage")
17
- # op.request_schema.new(params).valid!
18
- # op.response_schema.new(result).valid!
19
- #
20
- # A2A::Proto.operations
21
- # #=> [Operation("SendMessage", ...), Operation("GetTask", ...), ...]
22
- #
23
- module Proto
24
- PROTO_PATH = File.expand_path("../../data/a2a.proto", __dir__).freeze
25
-
26
- # --- Data classes ---------------------------------------------------
27
-
28
- HttpBinding = Data.define(:verb, :path, :body)
29
-
30
- class Operation
31
- attr_reader :name, :request_type, :response_type,
32
- :server_streaming, :http_bindings
33
-
34
- def initialize(name:, request_type:, response_type:,
35
- server_streaming:, http_bindings:)
36
- @name = name
37
- @request_type = request_type
38
- @response_type = response_type
39
- @server_streaming = server_streaming
40
- @http_bindings = http_bindings
41
- end
42
-
43
- def server_streaming? = @server_streaming
44
-
45
- # Bridge to Schema: convert proto PascalCase type name to
46
- # the JSON schema title used by A2A::Schema[].
47
- #
48
- # "SendMessageRequest" => Schema["Send Message Request"]
49
- #
50
- def request_schema
51
- A2A::Schema[pascal_to_title(request_type)]
52
- end
53
-
54
- def response_schema
55
- return nil if response_type.include?(".") # google.protobuf.Empty
56
- A2A::Schema[pascal_to_title(response_type)]
57
- end
58
-
59
- # JSON-RPC and gRPC method names are identical to the RPC name.
60
- def json_rpc_method = name
61
- def grpc_method = name
62
-
63
- # REST binding from the primary (non-tenant) HTTP annotation.
64
- def rest_verb = http_bindings.first.verb
65
- def rest_path = http_bindings.first.path
66
-
67
- def inspect
68
- "#<Proto::Operation #{name} #{rest_verb.upcase} #{rest_path}>"
69
- end
70
-
71
- private
72
-
73
- # "SendMessageRequest" => "Send Message Request"
74
- def pascal_to_title(str)
75
- str.gsub(/([A-Z])/) { " #{$1}" }.strip
76
- end
77
- end
78
-
79
- # --- Module API -----------------------------------------------------
80
-
81
- class << self
82
- def operations
83
- @operations ||= parse_operations
84
- end
85
-
86
- def operation(name)
87
- operations.find { |op| op.name == name }
88
- end
89
-
90
- # Reset cached state (for tests).
91
- def reset!
92
- @operations = nil
93
- end
94
-
95
- private
96
-
97
- def parse_operations
98
- proto_text = File.read(PROTO_PATH)
99
- service_block = extract_service_block(proto_text)
100
- parse_rpcs(service_block)
101
- end
102
-
103
- # Extract the body of `service A2AService { ... }` from the proto text.
104
- def extract_service_block(text)
105
- start = text.index(/^service\s+\w+\s*\{/)
106
- return "" unless start
107
-
108
- depth = 0
109
- pos = text.index("{", start)
110
- body_start = pos + 1
111
-
112
- (pos...text.length).each do |i|
113
- case text[i]
114
- when "{" then depth += 1
115
- when "}"
116
- depth -= 1
117
- return text[body_start...i] if depth == 0
118
- end
119
- end
120
-
121
- ""
122
- end
123
-
124
- # Parse all `rpc` definitions from the service block text.
125
- def parse_rpcs(block)
126
- ops = []
127
-
128
- rpc_chunks = block.split(/(?=^\s*rpc\s)/m).select { |c| c.match?(/^\s*rpc\s/) }
129
-
130
- rpc_chunks.each do |chunk|
131
- sig = chunk.match(
132
- /rpc\s+(\w+)\s*\(\s*(\w[\w.]*)\s*\)\s*returns\s*\(\s*(stream\s+)?(\w[\w.]*)\s*\)/
133
- )
134
- next unless sig
135
-
136
- name = sig[1]
137
- request_type = sig[2]
138
- streaming = !sig[3].nil?
139
- response_type = sig[4]
140
-
141
- http_bindings = parse_http_bindings(chunk)
142
-
143
- ops << Operation.new(
144
- name: name,
145
- request_type: request_type,
146
- response_type: response_type,
147
- server_streaming: streaming,
148
- http_bindings: http_bindings,
149
- )
150
- end
151
-
152
- ops
153
- end
154
-
155
- def parse_http_bindings(chunk)
156
- bindings = []
157
-
158
- http_start = chunk.index("google.api.http")
159
- return bindings unless http_start
160
-
161
- eq_brace = chunk.index("{", http_start)
162
- return bindings unless eq_brace
163
-
164
- block_text = extract_braced_block(chunk, eq_brace)
165
- return bindings unless block_text
166
-
167
- primary = parse_single_binding(block_text)
168
- bindings << primary if primary
169
-
170
- scan_additional_bindings(block_text).each do |ab_text|
171
- binding = parse_single_binding(ab_text)
172
- bindings << binding if binding
173
- end
174
-
175
- bindings
176
- end
177
-
178
- def extract_braced_block(text, open_pos)
179
- depth = 0
180
- body_start = open_pos + 1
181
-
182
- (open_pos...text.length).each do |i|
183
- case text[i]
184
- when "{" then depth += 1
185
- when "}"
186
- depth -= 1
187
- return text[body_start...i] if depth == 0
188
- end
189
- end
190
-
191
- nil
192
- end
193
-
194
- def parse_single_binding(text)
195
- verb_match = text.match(/\b(get|post|put|patch|delete):\s*"([^"]+)"/)
196
- return nil unless verb_match
197
-
198
- verb = verb_match[1]
199
- path = verb_match[2]
200
-
201
- body_match = text.match(/\bbody:\s*"([^"]*)"/)
202
- body = body_match ? body_match[1] : nil
203
-
204
- HttpBinding.new(verb: verb, path: path, body: body)
205
- end
206
-
207
- def scan_additional_bindings(text)
208
- blocks = []
209
- search_from = 0
210
-
211
- while (ab_pos = text.index("additional_bindings", search_from))
212
- brace_pos = text.index("{", ab_pos)
213
- break unless brace_pos
214
-
215
- inner = extract_braced_block(text, brace_pos)
216
- if inner
217
- blocks << inner
218
- search_from = brace_pos + inner.length + 2
219
- else
220
- break
221
- end
222
- end
223
-
224
- blocks
225
- end
226
- end
227
- end
228
- end
229
-
230
- test do
231
- proto = A2A::Proto
232
-
233
- it "finds 11 operations" do
234
- proto.operations.size.should == 11
235
- end
236
-
237
- it "finds all operations by name" do
238
- expected = %w[
239
- SendMessage SendStreamingMessage GetTask ListTasks CancelTask
240
- SubscribeToTask CreateTaskPushNotificationConfig
241
- GetTaskPushNotificationConfig ListTaskPushNotificationConfigs
242
- DeleteTaskPushNotificationConfig GetExtendedAgentCard
243
- ]
244
- proto.operations.map(&:name).sort.should == expected.sort
245
- end
246
-
247
- it "looks up an operation by name" do
248
- proto.operation("SendMessage").should.not.be.nil
249
- proto.operation("SendMessage").name.should == "SendMessage"
250
- end
251
-
252
- it "returns nil for unknown operation" do
253
- proto.operation("NoSuchThing").should.be.nil
254
- end
255
-
256
- it "SendMessage request type" do
257
- proto.operation("SendMessage").request_type.should == "SendMessageRequest"
258
- end
259
-
260
- it "SendMessage response type" do
261
- proto.operation("SendMessage").response_type.should == "SendMessageResponse"
262
- end
263
-
264
- it "GetTask returns Task" do
265
- proto.operation("GetTask").response_type.should == "Task"
266
- end
267
-
268
- it "CancelTask returns Task" do
269
- proto.operation("CancelTask").response_type.should == "Task"
270
- end
271
-
272
- it "GetExtendedAgentCard returns AgentCard" do
273
- proto.operation("GetExtendedAgentCard").response_type.should == "AgentCard"
274
- end
275
-
276
- it "DeleteTaskPushNotificationConfig returns google.protobuf.Empty" do
277
- proto.operation("DeleteTaskPushNotificationConfig")
278
- .response_type.should == "google.protobuf.Empty"
279
- end
280
-
281
- it "CreateTaskPushNotificationConfig request and response are both TaskPushNotificationConfig" do
282
- op = proto.operation("CreateTaskPushNotificationConfig")
283
- op.request_type.should == "TaskPushNotificationConfig"
284
- op.response_type.should == "TaskPushNotificationConfig"
285
- end
286
-
287
- it "SendStreamingMessage is server-streaming" do
288
- proto.operation("SendStreamingMessage").server_streaming?.should == true
289
- end
290
-
291
- it "SubscribeToTask is server-streaming" do
292
- proto.operation("SubscribeToTask").server_streaming?.should == true
293
- end
294
-
295
- it "SendMessage is not server-streaming" do
296
- proto.operation("SendMessage").server_streaming?.should == false
297
- end
298
-
299
- it "GetTask is not server-streaming" do
300
- proto.operation("GetTask").server_streaming?.should == false
301
- end
302
-
303
- it "SendMessage has POST /message:send" do
304
- b = proto.operation("SendMessage").http_bindings.first
305
- b.verb.should == "post"
306
- b.path.should == "/message:send"
307
- b.body.should == "*"
308
- end
309
-
310
- it "GetTask has GET /tasks/{id=*}" do
311
- b = proto.operation("GetTask").http_bindings.first
312
- b.verb.should == "get"
313
- b.path.should == "/tasks/{id=*}"
314
- b.body.should.be.nil
315
- end
316
-
317
- it "ListTasks has GET /tasks" do
318
- b = proto.operation("ListTasks").http_bindings.first
319
- b.verb.should == "get"
320
- b.path.should == "/tasks"
321
- end
322
-
323
- it "CancelTask has POST /tasks/{id=*}:cancel" do
324
- b = proto.operation("CancelTask").http_bindings.first
325
- b.verb.should == "post"
326
- b.path.should == "/tasks/{id=*}:cancel"
327
- b.body.should == "*"
328
- end
329
-
330
- it "DeleteTaskPushNotificationConfig has DELETE verb" do
331
- b = proto.operation("DeleteTaskPushNotificationConfig").http_bindings.first
332
- b.verb.should == "delete"
333
- end
334
-
335
- it "each operation has at least 2 HTTP bindings (primary + tenant)" do
336
- proto.operations.each do |op|
337
- op.http_bindings.size.should >= 2
338
- end
339
- end
340
-
341
- it "second binding is the tenant-prefixed variant" do
342
- b = proto.operation("SendMessage").http_bindings[1]
343
- b.path.should == "/{tenant}/message:send"
344
- b.verb.should == "post"
345
- end
346
-
347
- it "GetTask tenant binding" do
348
- b = proto.operation("GetTask").http_bindings[1]
349
- b.path.should == "/{tenant}/tasks/{id=*}"
350
- end
351
-
352
- it "rest_verb returns the primary verb" do
353
- proto.operation("GetTask").rest_verb.should == "get"
354
- proto.operation("SendMessage").rest_verb.should == "post"
355
- proto.operation("DeleteTaskPushNotificationConfig").rest_verb.should == "delete"
356
- end
357
-
358
- it "rest_path returns the primary path" do
359
- proto.operation("GetTask").rest_path.should == "/tasks/{id=*}"
360
- proto.operation("ListTasks").rest_path.should == "/tasks"
361
- end
362
-
363
- it "json_rpc_method equals the operation name" do
364
- proto.operations.each do |op|
365
- op.json_rpc_method.should == op.name
366
- end
367
- end
368
-
369
- it "grpc_method equals the operation name" do
370
- proto.operations.each do |op|
371
- op.grpc_method.should == op.name
372
- end
373
- end
374
-
375
- it "request_schema returns the matching Schema class" do
376
- op = proto.operation("SendMessage")
377
- op.request_schema.should == A2A::Schema["Send Message Request"]
378
- end
379
-
380
- it "response_schema returns the matching Schema class" do
381
- op = proto.operation("SendMessage")
382
- op.response_schema.should == A2A::Schema["Send Message Response"]
383
- end
384
-
385
- it "response_schema for GetTask returns Task schema" do
386
- op = proto.operation("GetTask")
387
- op.response_schema.should == A2A::Schema["Task"]
388
- end
389
-
390
- it "response_schema returns nil for google.protobuf.Empty" do
391
- op = proto.operation("DeleteTaskPushNotificationConfig")
392
- op.response_schema.should.be.nil
393
- end
394
-
395
- it "every operation has a valid request_schema" do
396
- proto.operations.each do |op|
397
- op.request_schema.should.be.kind_of(Class)
398
- (op.request_schema < A2A::Schema::Definition).should == true
399
- end
400
- end
401
-
402
- it "every non-Empty operation has a valid response_schema" do
403
- proto.operations.each do |op|
404
- next if op.response_type.include?(".")
405
- op.response_schema.should.be.kind_of(Class)
406
- (op.response_schema < A2A::Schema::Definition).should == true
407
- end
408
- end
409
-
410
- it "can build and validate a SendMessage request" do
411
- op = proto.operation("SendMessage")
412
- req = op.request_schema.new(
413
- message: {
414
- "messageId" => "msg-1",
415
- "role" => "ROLE_USER",
416
- "parts" => [{ "text" => "Hello" }]
417
- }
418
- )
419
- req.valid?.should == true
420
- end
421
-
422
- it "can build and validate a GetTask request" do
423
- op = proto.operation("GetTask")
424
- req = op.request_schema.new(id: "task-123")
425
- req.valid?.should == true
426
- end
427
-
428
- it "can build and validate a Task response" do
429
- op = proto.operation("GetTask")
430
- resp = op.response_schema.new(
431
- id: "task-123",
432
- context_id: "ctx-456",
433
- status: { "state" => "TASK_STATE_SUBMITTED" }
434
- )
435
- resp.valid?.should == true
436
- end
437
-
438
- it "has a useful inspect" do
439
- op = proto.operation("SendMessage")
440
- op.inspect.should.include?("SendMessage")
441
- op.inspect.should.include?("POST")
442
- op.inspect.should.include?("/message:send")
443
- end
444
- end
@@ -1,148 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/setup"
4
- require "a2a"
5
-
6
- module A2A
7
- module Schema
8
- # Base class for schema-validated A2A protocol objects.
9
- #
10
- # Each A2A definition type (Agent Card, Agent Capabilities, Task, etc.)
11
- # gets a dynamically-generated subclass of Definition with:
12
- # - A JSONSchemer sub-schema attached (.schema)
13
- # - Reader methods for each property (snake_case)
14
- # - Validation via .valid? / .valid!
15
- #
16
- # caps = A2A::Schema["Agent Capabilities"].new(
17
- # streaming: true,
18
- # push_notifications: false
19
- # )
20
- # caps.valid? #=> true
21
- # caps.streaming #=> true
22
- # caps.push_notifications #=> false
23
- # caps.to_h #=> { "streaming" => true, "pushNotifications" => false }
24
- #
25
- class Definition
26
-
27
- def initialize(hash = {})
28
- props = self.class.schema_properties
29
- snake = self.class.snake_to_camel_map
30
- refs = self.class.property_refs
31
- @data = {}
32
-
33
- hash.each do |key, value|
34
- k = key.to_s
35
-
36
- # Resolve snake_case input to camelCase storage key
37
- camel = snake[k] || k
38
-
39
- if props.include?(camel)
40
- @data[camel] = if value.is_a?(Definition)
41
- value.to_h
42
- elsif (ref_info = refs[camel])
43
- wrap_ref(value, ref_info)
44
- else
45
- value
46
- end
47
- end
48
- end
49
- end
50
-
51
- # --- class methods overridden by the factory -----------------------
52
-
53
- def self.schema
54
- raise "A2A::Schema::Definition should NOT be instantiated directly"
55
- end
56
-
57
- def self.definition_name
58
- raise "A2A::Schema::Definition should NOT be instantiated directly"
59
- end
60
-
61
- def self.schema_properties
62
- raise "A2A::Schema::Definition should NOT be instantiated directly"
63
- end
64
-
65
- def self.snake_to_camel_map
66
- raise "A2A::Schema::Definition should NOT be instantiated directly"
67
- end
68
-
69
- def self.property_refs
70
- raise "A2A::Schema::Definition should NOT be instantiated directly"
71
- end
72
-
73
- # --- validation ----------------------------------------------------
74
-
75
- def valid?
76
- self.class.schema.valid?(to_h)
77
- end
78
-
79
- def valid!
80
- errors = self.class.schema.validate(to_h).to_a
81
- return true if errors.empty?
82
-
83
- raise ValidationError.new(errors,
84
- definition_name: self.class.definition_name,
85
- data: to_h
86
- )
87
- end
88
-
89
- # --- serialization -------------------------------------------------
90
-
91
- # Returns the data as a plain Hash with camelCase string keys,
92
- # matching the JSON wire format. Nested Definition instances
93
- # are auto-coerced via deep_compact.
94
- def to_h
95
- deep_compact(@data)
96
- end
97
-
98
- def ==(other)
99
- other.is_a?(Definition) && to_h == other.to_h
100
- end
101
-
102
- def inspect
103
- "#<#{self.class.definition_name} #{to_h.inspect}>"
104
- end
105
-
106
- private
107
-
108
- def wrap_ref(value, ref_info)
109
- kind, title = ref_info
110
-
111
- case kind
112
- when :object
113
- value.is_a?(Hash) ? A2A::Schema[title].new(value) : value
114
- when :array
115
- if value.is_a?(Array)
116
- value.map { |el| el.is_a?(Hash) ? A2A::Schema[title].new(el) : el }
117
- else
118
- value
119
- end
120
- when :map
121
- if value.is_a?(Hash)
122
- value.transform_values { |v| v.is_a?(Hash) ? A2A::Schema[title].new(v) : v }
123
- else
124
- value
125
- end
126
- else
127
- value
128
- end
129
- end
130
-
131
- def deep_compact(obj)
132
- case obj
133
- when Hash
134
- obj.each_with_object({}) do |(k, v), result|
135
- compacted = deep_compact(v)
136
- result[k] = compacted unless compacted.nil?
137
- end
138
- when Array
139
- obj.map { |v| deep_compact(v) }
140
- when Definition
141
- obj.to_h
142
- else
143
- obj
144
- end
145
- end
146
- end
147
- end
148
- end