agent2agent 1.1.0 → 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 +30 -20
  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 -40
  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
@@ -4,249 +4,252 @@ require "bundler/setup"
4
4
  require "a2a"
5
5
 
6
6
  module A2A
7
- # Schema-validated A2A protocol objects, powered by json_schemer.
8
- #
9
- # Loads the bundled data/a2a.json schema, rewrites external $ref
10
- # values to internal #/definitions/... pointers, and dynamically
11
- # generates Definition subclasses for each type.
12
- #
13
- # A2A::Schema["Agent Capabilities"]
14
- # #=> Class < Definition with .schema, .valid?, reader methods
15
- #
16
- # A2A::Schema["Agent Card"]
17
- # #=> Class < Definition
18
- #
19
- # A2A::Schema.list_definitions
20
- # #=> ["API Key Security Scheme", "Agent Capabilities", ...]
21
- #
22
- module Schema
23
- DATA_PATH = File.expand_path("../../data/a2a.json", __dir__).freeze
24
-
25
- @definition_classes = {}
26
- @schemer = nil
27
- @raw_schema = nil
28
- @ref_map = nil
29
-
30
- class << self
31
- # Look up a definition by title.
32
- #
33
- # A2A::Schema["Agent Capabilities"]
34
- # #=> Class < Definition
35
- #
36
- def [](name)
37
- @definition_classes[name] ||= begin
38
- definitions = raw_schema.fetch("definitions", {})
39
-
40
- unless definitions.key?(name)
41
- raise "No A2A definition found for #{name.inspect}!" \
42
- "\nAvailable: #{list_definitions.join(", ")}"
43
- end
44
-
45
- encoded = URI::DEFAULT_PARSER.escape(name)
46
- ref_schema = schemer.ref("#/definitions/#{encoded}")
47
- build_definition_class(ref_schema, name, definitions[name])
48
- end
49
- end
50
-
51
- # All available definition titles, sorted.
52
- #
53
- # A2A::Schema.list_definitions
54
- # #=> ["API Key Security Scheme", "Agent Capabilities", ...]
55
- #
56
- def list_definitions
57
- raw_schema.fetch("definitions", {}).keys.sort
58
- end
7
+ module Protocol
8
+ # Schema-validated A2A protocol objects, powered by json_schemer.
9
+ #
10
+ # Loads the bundled data/a2a.json schema, rewrites external $ref
11
+ # values to internal #/definitions/... pointers, and dynamically
12
+ # generates Definition subclasses for each type.
13
+ #
14
+ # A2A::Protocol::JsonSchema["Agent Capabilities"]
15
+ # #=> Class < Definition with .schema, .valid?, reader methods
16
+ #
17
+ # A2A::Protocol::JsonSchema["Agent Card"]
18
+ # #=> Class < Definition
19
+ #
20
+ # A2A::Protocol::JsonSchema.list_definitions
21
+ # #=> ["API Key Security Scheme", "Agent Capabilities", ...]
22
+ #
23
+ module JsonSchema
24
+ DATA_PATH = File.expand_path("../../../data/a2a.json", __dir__).freeze
25
+
26
+ @definition_classes = {}
27
+ @schemer = nil
28
+ @raw_schema = nil
29
+ @ref_map = nil
30
+
31
+ class << self
32
+ # Look up a definition by title.
33
+ #
34
+ # A2A::Protocol::JsonSchema["Agent Capabilities"]
35
+ # #=> Class < Definition
36
+ #
37
+ def [](name)
38
+ @definition_classes[name] ||= begin
39
+ definitions = raw_schema.fetch("definitions", {})
59
40
 
60
- # The JSONSchemer instance for the full A2A schema bundle.
61
- # Cached after first access.
62
- def schemer
63
- @schemer ||= JSONSchemer.schema(raw_schema)
64
- end
41
+ unless definitions.key?(name)
42
+ raise "No A2A definition found for #{name.inspect}!" \
43
+ "\nAvailable: #{list_definitions.join(", ")}"
44
+ end
65
45
 
66
- # The parsed + ref-rewritten JSON schema hash.
67
- def raw_schema
68
- @raw_schema ||= load_and_rewrite_schema
69
- end
46
+ encoded = URI::DEFAULT_PARSER.escape(name)
47
+ ref_schema = schemer.ref("#/definitions/#{encoded}")
48
+ build_definition_class(ref_schema, name, definitions[name])
49
+ end
50
+ end
70
51
 
71
- # Reset all cached state (useful for tests).
72
- def reset!
73
- @definition_classes.clear
74
- @schemer = nil
75
- @raw_schema = nil
76
- @ref_map = nil
77
- end
52
+ # All available definition titles, sorted.
53
+ #
54
+ # A2A::Protocol::JsonSchema.list_definitions
55
+ # #=> ["API Key Security Scheme", "Agent Capabilities", ...]
56
+ #
57
+ def list_definitions
58
+ raw_schema.fetch("definitions", {}).keys.sort
59
+ end
78
60
 
79
- private
61
+ # The JSONSchemer instance for the full A2A schema bundle.
62
+ # Cached after first access.
63
+ def schemer
64
+ @schemer ||= JSONSchemer.schema(raw_schema)
65
+ end
80
66
 
81
- # Load data/a2a.json, build the $ref rewrite map, and
82
- # walk the entire tree replacing external refs with internal ones.
83
- def load_and_rewrite_schema
84
- schema = JSON.parse(File.read(DATA_PATH))
85
- map = build_ref_map(schema)
86
- rewrite_refs!(schema, map)
87
- schema
67
+ # The parsed + ref-rewritten JSON schema hash.
68
+ def raw_schema
69
+ @raw_schema ||= load_and_rewrite_schema
88
70
  end
89
71
 
90
- # Build a map from external $ref strings to internal
91
- # #/definitions/Title pointers.
92
- def build_ref_map(schema)
93
- return @ref_map if @ref_map
72
+ # Reset all cached state (useful for tests).
73
+ def reset!
74
+ @definition_classes.clear
75
+ @schemer = nil
76
+ @raw_schema = nil
77
+ @ref_map = nil
78
+ end
94
79
 
95
- definitions = schema.fetch("definitions", {})
80
+ private
96
81
 
97
- pascal_to_title = {}
98
- definitions.each_key do |title|
99
- pascal = title.gsub(/\s+/, "")
100
- pascal_to_title[pascal] = title
82
+ # Load data/a2a.json, build the $ref rewrite map, and
83
+ # walk the entire tree replacing external refs with internal ones.
84
+ def load_and_rewrite_schema
85
+ schema = JSON.parse(File.read(DATA_PATH))
86
+ map = build_ref_map(schema)
87
+ rewrite_refs!(schema, map)
88
+ schema
101
89
  end
102
90
 
103
- refs = collect_refs(schema)
91
+ # Build a map from external $ref strings to internal
92
+ # #/definitions/Title pointers.
93
+ def build_ref_map(schema)
94
+ return @ref_map if @ref_map
104
95
 
105
- map = {}
106
- refs.each do |ref_str|
107
- type_name = ref_str
108
- .sub(/\.jsonschema\.json\z/, "")
109
- .split(".")
110
- .last
96
+ definitions = schema.fetch("definitions", {})
111
97
 
112
- if (title = pascal_to_title[type_name])
113
- encoded = URI::DEFAULT_PARSER.escape(title)
114
- map[ref_str] = "#/definitions/#{encoded}"
98
+ pascal_to_title = {}
99
+ definitions.each_key do |title|
100
+ pascal = title.gsub(/\s+/, "")
101
+ pascal_to_title[pascal] = title
115
102
  end
116
- end
117
103
 
118
- @ref_map = map
119
- end
104
+ refs = collect_refs(schema)
105
+
106
+ map = {}
107
+ refs.each do |ref_str|
108
+ type_name = ref_str
109
+ .sub(/\.jsonschema\.json\z/, "")
110
+ .split(".")
111
+ .last
120
112
 
121
- # Recursively collect all $ref string values from a JSON tree.
122
- def collect_refs(obj, refs = Set.new)
123
- case obj
124
- when Hash
125
- obj.each do |k, v|
126
- if k == "$ref" && v.is_a?(String) && !v.start_with?("#")
127
- refs << v
128
- else
129
- collect_refs(v, refs)
113
+ if (title = pascal_to_title[type_name])
114
+ encoded = URI::DEFAULT_PARSER.escape(title)
115
+ map[ref_str] = "#/definitions/#{encoded}"
130
116
  end
131
117
  end
132
- when Array
133
- obj.each { |v| collect_refs(v, refs) }
118
+
119
+ @ref_map = map
134
120
  end
135
- refs
136
- end
137
121
 
138
- # Walk the schema tree and replace all external $ref values
139
- # with their internal #/definitions/... equivalents.
140
- def rewrite_refs!(obj, map)
141
- case obj
142
- when Hash
143
- obj.each do |k, v|
144
- if k == "$ref" && v.is_a?(String) && map.key?(v)
145
- obj[k] = map[v]
146
- else
147
- rewrite_refs!(v, map)
122
+ # Recursively collect all $ref string values from a JSON tree.
123
+ def collect_refs(obj, refs = Set.new)
124
+ case obj
125
+ when Hash
126
+ obj.each do |k, v|
127
+ if k == "$ref" && v.is_a?(String) && !v.start_with?("#")
128
+ refs << v
129
+ else
130
+ collect_refs(v, refs)
131
+ end
148
132
  end
133
+ when Array
134
+ obj.each { |v| collect_refs(v, refs) }
149
135
  end
150
- when Array
151
- obj.each { |v| rewrite_refs!(v, map) }
136
+ refs
152
137
  end
153
- end
154
138
 
155
- # Build a Definition subclass for a specific A2A type.
156
- def build_definition_class(schema_instance, definition_name, raw_definition)
157
- properties = raw_definition.fetch("properties", {})
158
- camel_keys = properties.keys
159
- snake_to_camel = build_snake_to_camel(camel_keys)
160
- prop_refs = build_property_refs(properties)
161
-
162
- reader_pairs = camel_keys.map { |ck| [camel_to_snake(ck).to_sym, ck] }
163
-
164
- Class.new(Definition) do
165
- @schema = schema_instance
166
- @definition_name = definition_name
167
- @schema_properties = camel_keys
168
- @snake_to_camel = snake_to_camel
169
- @property_refs = prop_refs
170
-
171
- class << self
172
- def schema = @schema
173
- def definition_name = @definition_name
174
- def schema_properties = @schema_properties
175
- def snake_to_camel_map = @snake_to_camel
176
- def property_refs = @property_refs
177
- end
178
-
179
- reader_pairs.each do |snake_sym, camel_key|
180
- define_method(snake_sym) { @data[camel_key] }
139
+ # Walk the schema tree and replace all external $ref values
140
+ # with their internal #/definitions/... equivalents.
141
+ def rewrite_refs!(obj, map)
142
+ case obj
143
+ when Hash
144
+ obj.each do |k, v|
145
+ if k == "$ref" && v.is_a?(String) && map.key?(v)
146
+ obj[k] = map[v]
147
+ else
148
+ rewrite_refs!(v, map)
149
+ end
150
+ end
151
+ when Array
152
+ obj.each { |v| rewrite_refs!(v, map) }
181
153
  end
182
154
  end
183
- end
184
155
 
185
- # Inspect each property's raw schema for $ref pointers and build
186
- # a map of { camelKey => [:kind, "Definition Title"] } so that
187
- # Definition#initialize can auto-wrap nested Hashes.
188
- #
189
- # Three patterns:
190
- # :object — direct $ref (e.g. task → Task)
191
- # :array — items.$ref (e.g. artifacts → [Artifact, ...])
192
- # :map — additionalProperties.$ref (e.g. securitySchemes {k => SecurityScheme})
193
- def build_property_refs(properties)
194
- definitions = raw_schema.fetch("definitions", {})
195
- refs = {}
196
-
197
- properties.each do |camel_key, prop_schema|
198
- if (ref = prop_schema["$ref"])
199
- # Direct $ref — singular nested object
200
- title = ref_title_for(ref)
201
- if title && definitions.dig(title, "properties")
202
- refs[camel_key] = [:object, title]
203
- end
204
- elsif prop_schema["type"] == "array" && (ref = prop_schema.dig("items", "$ref"))
205
- # Array with $ref items
206
- title = ref_title_for(ref)
207
- if title && definitions.dig(title, "properties")
208
- refs[camel_key] = [:array, title]
156
+ # Build a Definition subclass for a specific A2A type.
157
+ def build_definition_class(schema_instance, definition_name, raw_definition)
158
+ properties = raw_definition.fetch("properties", {})
159
+ camel_keys = properties.keys
160
+ snake_to_camel = build_snake_to_camel(camel_keys)
161
+ prop_refs = build_property_refs(properties)
162
+
163
+ reader_pairs = camel_keys.map { |ck| [camel_to_snake(ck).to_sym, ck] }
164
+
165
+ Class.new(Definition) do
166
+ @schema = schema_instance
167
+ @definition_name = definition_name
168
+ @schema_properties = camel_keys
169
+ @snake_to_camel = snake_to_camel
170
+ @property_refs = prop_refs
171
+
172
+ class << self
173
+ def schema = @schema
174
+ def definition_name = @definition_name
175
+ def schema_properties = @schema_properties
176
+ def snake_to_camel_map = @snake_to_camel
177
+ def property_refs = @property_refs
209
178
  end
210
- elsif prop_schema["type"] == "object" && (ref = prop_schema.dig("additionalProperties", "$ref"))
211
- # Map with $ref additionalProperties
212
- title = ref_title_for(ref)
213
- if title && definitions.dig(title, "properties")
214
- refs[camel_key] = [:map, title]
179
+
180
+ reader_pairs.each do |snake_sym, camel_key|
181
+ define_method(snake_sym) { @data[camel_key] }
215
182
  end
216
183
  end
217
184
  end
218
185
 
219
- refs
220
- end
186
+ # Inspect each property's raw schema for $ref pointers and build
187
+ # a map of { camelKey => [:kind, "Definition Title"] } so that
188
+ # Definition#initialize can auto-wrap nested Hashes.
189
+ #
190
+ # Three patterns:
191
+ # :object — direct $ref (e.g. task → Task)
192
+ # :array — items.$ref (e.g. artifacts → [Artifact, ...])
193
+ # :map — additionalProperties.$ref (e.g. securitySchemes → {k => SecurityScheme})
194
+ def build_property_refs(properties)
195
+ definitions = raw_schema.fetch("definitions", {})
196
+ refs = {}
197
+
198
+ properties.each do |camel_key, prop_schema|
199
+ if (ref = prop_schema["$ref"])
200
+ # Direct $ref — singular nested object
201
+ title = ref_title_for(ref)
202
+ if title && definitions.dig(title, "properties")
203
+ refs[camel_key] = [:object, title]
204
+ end
205
+ elsif prop_schema["type"] == "array" && (ref = prop_schema.dig("items", "$ref"))
206
+ # Array with $ref items
207
+ title = ref_title_for(ref)
208
+ if title && definitions.dig(title, "properties")
209
+ refs[camel_key] = [:array, title]
210
+ end
211
+ elsif prop_schema["type"] == "object" && (ref = prop_schema.dig("additionalProperties", "$ref"))
212
+ # Map with $ref additionalProperties
213
+ title = ref_title_for(ref)
214
+ if title && definitions.dig(title, "properties")
215
+ refs[camel_key] = [:map, title]
216
+ end
217
+ end
218
+ end
221
219
 
222
- # Extract the definition title from an internal $ref pointer.
223
- # e.g. "#/definitions/Task%20Status" => "Task Status"
224
- def ref_title_for(ref)
225
- return nil unless ref.start_with?("#/definitions/")
220
+ refs
221
+ end
226
222
 
227
- URI::DEFAULT_PARSER.unescape(ref.sub("#/definitions/", ""))
228
- end
223
+ # Extract the definition title from an internal $ref pointer.
224
+ # e.g. "#/definitions/Task%20Status" => "Task Status"
225
+ def ref_title_for(ref)
226
+ return nil unless ref.start_with?("#/definitions/")
229
227
 
230
- def build_snake_to_camel(camel_keys)
231
- map = {}
232
- camel_keys.each do |camel|
233
- snake = camel_to_snake(camel)
234
- map[snake] = camel
235
- map[camel] = camel
228
+ URI::DEFAULT_PARSER.unescape(ref.sub("#/definitions/", ""))
236
229
  end
237
- map
238
- end
239
230
 
240
- def camel_to_snake(str)
241
- str.gsub(/([A-Z])/) { "_#{$1.downcase}" }
242
- .delete_prefix("_")
243
- end
231
+ def build_snake_to_camel(camel_keys)
232
+ map = {}
233
+ camel_keys.each do |camel|
234
+ snake = camel_to_snake(camel)
235
+ map[snake] = camel
236
+ map[camel] = camel
237
+ end
238
+ map
239
+ end
240
+
241
+ def camel_to_snake(str)
242
+ str.gsub(/([A-Z])/) { "_#{$1.downcase}" }
243
+ .delete_prefix("_")
244
+ end
245
+ end
244
246
  end
245
247
  end
246
248
  end
247
249
 
248
- test do
249
- schema = A2A::Schema
250
+ __END__
251
+ describe "A2A::Protocol::JsonSchema" do
252
+ schema = A2A::Protocol::JsonSchema
250
253
 
251
254
  it "loads the raw schema" do
252
255
  schema.raw_schema.should.be.kind_of(Hash)
@@ -291,7 +294,7 @@ test do
291
294
  it "returns a Class that subclasses Definition" do
292
295
  klass = schema["Agent Capabilities"]
293
296
  klass.should.be.kind_of(Class)
294
- (klass < A2A::Schema::Definition).should == true
297
+ (klass < A2A::Protocol::JsonSchema::Definition).should == true
295
298
  end
296
299
 
297
300
  it "caches definition classes" do
@@ -375,7 +378,7 @@ test do
375
378
 
376
379
  it "valid! raises ValidationError for invalid data" do
377
380
  caps = schema["Agent Capabilities"].new(streaming: "not_a_bool")
378
- lambda { caps.valid! }.should.raise(A2A::Schema::ValidationError)
381
+ lambda { caps.valid! }.should.raise(A2A::Protocol::JsonSchema::ValidationError)
379
382
  end
380
383
 
381
384
  it "detects additionalProperties violations" do
@@ -471,7 +474,7 @@ test do
471
474
  schema.list_definitions.each do |name|
472
475
  klass = schema[name]
473
476
  instance = klass.new
474
- instance.is_a?(A2A::Schema::Definition).should == true
477
+ instance.is_a?(A2A::Protocol::JsonSchema::Definition).should == true
475
478
  end
476
479
  end
477
480
 
@@ -486,11 +489,11 @@ test do
486
489
  }
487
490
  }
488
491
  )
489
- response.task.should.be.kind_of(A2A::Schema::Definition)
492
+ response.task.should.be.kind_of(A2A::Protocol::JsonSchema::Definition)
490
493
  response.task.id.should == "task-123"
491
494
  response.task.context_id.should == "ctx-456"
492
495
  # Deeply nested: Task.status is also auto-wrapped
493
- response.task.status.should.be.kind_of(A2A::Schema::Definition)
496
+ response.task.status.should.be.kind_of(A2A::Protocol::JsonSchema::Definition)
494
497
  response.task.status.state.should == "TASK_STATE_SUBMITTED"
495
498
  end
496
499
 
@@ -505,7 +508,7 @@ test do
505
508
  )
506
509
  task.history.should.be.kind_of(Array)
507
510
  task.history.length.should == 2
508
- task.history[0].should.be.kind_of(A2A::Schema::Definition)
511
+ task.history[0].should.be.kind_of(A2A::Protocol::JsonSchema::Definition)
509
512
  task.history[0].role.should == "ROLE_USER"
510
513
  task.history[1].role.should == "ROLE_AGENT"
511
514
  end