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
@@ -0,0 +1,276 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "a2a"
5
+
6
+ module A2A
7
+ module Protocol
8
+ module JsonSchema
9
+ # Base class for schema-validated A2A protocol objects.
10
+ #
11
+ # Each A2A definition type (Agent Card, Agent Capabilities, Task, etc.)
12
+ # gets a dynamically-generated subclass of Definition with:
13
+ # - A JSONSchemer sub-schema attached (.schema)
14
+ # - Reader methods for each property (snake_case)
15
+ # - Validation via .valid? / .valid!
16
+ #
17
+ # caps = A2A::Protocol::JsonSchema["Agent Capabilities"].new(
18
+ # streaming: true,
19
+ # push_notifications: false
20
+ # )
21
+ # caps.valid? #=> true
22
+ # caps.streaming #=> true
23
+ # caps.push_notifications #=> false
24
+ # caps.to_h #=> { "streaming" => true, "pushNotifications" => false }
25
+ #
26
+ class Definition
27
+
28
+ def initialize(hash = {})
29
+ props = self.class.schema_properties
30
+ snake = self.class.snake_to_camel_map
31
+ refs = self.class.property_refs
32
+ @data = {}
33
+
34
+ hash.each do |key, value|
35
+ k = key.to_s
36
+
37
+ # Resolve snake_case input to camelCase storage key
38
+ camel = snake[k] || k
39
+
40
+ if props.include?(camel)
41
+ @data[camel] = if value.is_a?(Definition)
42
+ value.to_h
43
+ elsif (ref_info = refs[camel])
44
+ wrap_ref(value, ref_info)
45
+ else
46
+ value
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ # --- class methods overridden by the factory -----------------------
53
+
54
+ def self.schema
55
+ raise "A2A::Protocol::JsonSchema::Definition should NOT be instantiated directly"
56
+ end
57
+
58
+ def self.definition_name
59
+ raise "A2A::Protocol::JsonSchema::Definition should NOT be instantiated directly"
60
+ end
61
+
62
+ def self.schema_properties
63
+ raise "A2A::Protocol::JsonSchema::Definition should NOT be instantiated directly"
64
+ end
65
+
66
+ def self.snake_to_camel_map
67
+ raise "A2A::Protocol::JsonSchema::Definition should NOT be instantiated directly"
68
+ end
69
+
70
+ def self.property_refs
71
+ raise "A2A::Protocol::JsonSchema::Definition should NOT be instantiated directly"
72
+ end
73
+
74
+ # Reverse of snake_to_camel_map. Built first-entry-wins: the
75
+ # factory inserts the snake_case key before the camelCase
76
+ # identity key, so camelCase storage keys map back to their
77
+ # snake_case reader names.
78
+ def self.camel_to_snake_map
79
+ @camel_to_snake_map ||= snake_to_camel_map.each_with_object({}) do |(key, camel), map|
80
+ map[camel] ||= key
81
+ end
82
+ end
83
+
84
+ # --- validation ----------------------------------------------------
85
+
86
+ def valid?
87
+ self.class.schema.valid?(to_h)
88
+ end
89
+
90
+ def valid!
91
+ errors = self.class.schema.validate(to_h).to_a
92
+ return true if errors.empty?
93
+
94
+ raise ValidationError.new(errors,
95
+ definition_name: self.class.definition_name,
96
+ data: to_h
97
+ )
98
+ end
99
+
100
+ # --- serialization -------------------------------------------------
101
+
102
+ # Returns the data as a plain Hash with camelCase string keys,
103
+ # matching the JSON wire format. Nested Definition instances
104
+ # are auto-coerced via deep_compact.
105
+ def to_h
106
+ deep_compact(@data)
107
+ end
108
+
109
+ def ==(other)
110
+ other.is_a?(Definition) && to_h == other.to_h
111
+ end
112
+
113
+ # --- pattern matching ------------------------------------------------
114
+
115
+ # Supports Ruby hash pattern matching:
116
+ #
117
+ # case env["a2a.request"].message
118
+ # in { task_id: String => id, parts: }
119
+ # ...
120
+ # end
121
+ #
122
+ # Keys are the snake_case property names, same as the reader
123
+ # methods (camelCase symbols also work). Absent properties are
124
+ # omitted from the result so patterns that require them fail to
125
+ # match. Nested Definition values are returned as-is, so
126
+ # patterns can destructure recursively.
127
+ def deconstruct_keys(keys)
128
+ snake = self.class.snake_to_camel_map
129
+
130
+ if keys
131
+ keys.each_with_object({}) do |key, result|
132
+ camel = snake[key.to_s] || key.to_s
133
+ result[key] = @data[camel] if @data.key?(camel)
134
+ end
135
+ else
136
+ camel_to_snake = self.class.camel_to_snake_map
137
+ @data.each_with_object({}) do |(camel, value), result|
138
+ result[(camel_to_snake[camel] || camel).to_sym] = value
139
+ end
140
+ end
141
+ end
142
+
143
+ def inspect
144
+ "#<#{self.class.definition_name} #{to_h.inspect}>"
145
+ end
146
+
147
+ private
148
+
149
+ def wrap_ref(value, ref_info)
150
+ kind, title = ref_info
151
+
152
+ case kind
153
+ when :object
154
+ value.is_a?(Hash) ? A2A::Protocol::JsonSchema[title].new(value) : value
155
+ when :array
156
+ if value.is_a?(Array)
157
+ value.map { |el| el.is_a?(Hash) ? A2A::Protocol::JsonSchema[title].new(el) : el }
158
+ else
159
+ value
160
+ end
161
+ when :map
162
+ if value.is_a?(Hash)
163
+ value.transform_values { |v| v.is_a?(Hash) ? A2A::Protocol::JsonSchema[title].new(v) : v }
164
+ else
165
+ value
166
+ end
167
+ else
168
+ value
169
+ end
170
+ end
171
+
172
+ def deep_compact(obj)
173
+ case obj
174
+ when Hash
175
+ obj.each_with_object({}) do |(k, v), result|
176
+ compacted = deep_compact(v)
177
+ result[k] = compacted unless compacted.nil?
178
+ end
179
+ when Array
180
+ obj.map { |v| deep_compact(v) }
181
+ when Definition
182
+ obj.to_h
183
+ else
184
+ obj
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ __END__
193
+ describe "A2A::Protocol::JsonSchema::Definition#deconstruct_keys" do
194
+ schema = A2A::Protocol::JsonSchema
195
+
196
+ it "matches snake_case keys" do
197
+ msg = schema["Message"].new(role: "ROLE_USER", message_id: "m-1", task_id: "t-1")
198
+
199
+ case msg
200
+ in { task_id: String => id, role: }
201
+ id.should == "t-1"
202
+ role.should == "ROLE_USER"
203
+ end
204
+ end
205
+
206
+ it "matches camelCase keys" do
207
+ msg = schema["Message"].new(role: "ROLE_USER", task_id: "t-1")
208
+
209
+ case msg
210
+ in { taskId: String => id }
211
+ id.should == "t-1"
212
+ end
213
+ end
214
+
215
+ it "omits absent properties so patterns requiring them fail" do
216
+ msg = schema["Message"].new(role: "ROLE_USER")
217
+
218
+ matched = case msg
219
+ in { task_id: String }
220
+ true
221
+ else
222
+ false
223
+ end
224
+
225
+ matched.should == false
226
+ end
227
+
228
+ it "omits absent properties rather than yielding nil" do
229
+ msg = schema["Message"].new(role: "ROLE_USER")
230
+ msg.deconstruct_keys([:task_id]).key?(:task_id).should == false
231
+ end
232
+
233
+ it "destructures nested Definitions recursively" do
234
+ task = schema["Task"].new(
235
+ id: "t-1",
236
+ context_id: "c-1",
237
+ status: { "state" => "TASK_STATE_WORKING", "timestamp" => "2025-01-01T00:00:00.000Z" },
238
+ )
239
+
240
+ case task
241
+ in { status: { state: } }
242
+ state.should == "TASK_STATE_WORKING"
243
+ end
244
+ end
245
+
246
+ it "destructures Definitions inside arrays" do
247
+ msg = schema["Message"].new(
248
+ role: "ROLE_USER",
249
+ message_id: "m-1",
250
+ parts: [{ "text" => "hello" }],
251
+ )
252
+
253
+ case msg
254
+ in { parts: [{ text: }] }
255
+ text.should == "hello"
256
+ end
257
+ end
258
+
259
+ it "returns all properties as snake_case symbols for a nil key filter" do
260
+ msg = schema["Message"].new(role: "ROLE_USER", message_id: "m-1")
261
+ msg.deconstruct_keys(nil).should == { role: "ROLE_USER", message_id: "m-1" }
262
+ end
263
+
264
+ it "supports guarded operation-style matching" do
265
+ msg = schema["Message"].new(role: "ROLE_USER", message_id: "m-1", task_id: "")
266
+
267
+ matched = case msg
268
+ in { task_id: String => id } if !id.empty?
269
+ :continuation
270
+ else
271
+ :new_task
272
+ end
273
+
274
+ matched.should == :new_task
275
+ end
276
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "a2a"
5
+
6
+ module A2A
7
+ module Protocol
8
+ module JsonSchema
9
+ # Raised when a Definition instance fails schema validation.
10
+ #
11
+ # Collects JSONSchemer error details and formats them as a
12
+ # human-readable list with dot-notation field paths.
13
+ #
14
+ # Agent Card validation failed:
15
+ # - name is required but missing
16
+ # - capabilities.streaming must be boolean, got string
17
+ #
18
+ class ValidationError < StandardError
19
+ attr_reader :errors, :definition_name, :data
20
+
21
+ def initialize(errors, definition_name:, data: nil)
22
+ @errors = errors
23
+ @definition_name = definition_name
24
+ @data = data
25
+
26
+ super(build_message)
27
+ end
28
+
29
+ private
30
+
31
+ def build_message
32
+ lines = errors.map { |e| " - #{format_error(e)}" }
33
+ "#{definition_name} validation failed:\n#{lines.join("\n")}"
34
+ end
35
+
36
+ def format_error(error)
37
+ path = format_path(error)
38
+ type = error["type"]
39
+
40
+ case type
41
+ when "required"
42
+ missing = error.dig("details", "missing_keys")&.join(", ") || "unknown"
43
+ if path.empty?
44
+ "#{missing} is required but missing"
45
+ else
46
+ "#{path}.#{missing} is required but missing"
47
+ end
48
+ when "type"
49
+ expected = Array(error.dig("schema", "type")).join(" or ")
50
+ "#{path.empty? ? "(root)" : path} must be #{expected}"
51
+ when "enum"
52
+ allowed = error.dig("schema", "enum")&.join(", ") || "?"
53
+ "#{path.empty? ? "(root)" : path} must be one of: #{allowed}"
54
+ when "pattern"
55
+ pattern = error.dig("schema", "pattern")
56
+ "#{path.empty? ? "(root)" : path} must match pattern #{pattern}"
57
+ when "format"
58
+ fmt = error.dig("schema", "format")
59
+ "#{path.empty? ? "(root)" : path} must be a valid #{fmt}"
60
+ when "minimum", "maximum"
61
+ "#{path.empty? ? "(root)" : path} #{error["error"]}"
62
+ when "additionalProperties"
63
+ "#{path.empty? ? "(root)" : path} has unknown properties"
64
+ else
65
+ detail = error["error"] || error["type"] || "invalid"
66
+ "#{path.empty? ? "(root)" : path} #{detail}"
67
+ end
68
+ end
69
+
70
+ # Convert JSON pointer like "/properties/capabilities/streaming"
71
+ # to dot notation like "capabilities.streaming"
72
+ def format_path(error)
73
+ pointer = error["data_pointer"].to_s
74
+ return "" if pointer.empty? || pointer == "/"
75
+
76
+ pointer.delete_prefix("/").gsub("/", ".")
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ __END__
84
+ describe "A2A::Protocol::JsonSchema::ValidationError" do
85
+ error_data = [
86
+ {
87
+ "data_pointer" => "",
88
+ "type" => "required",
89
+ "details" => { "missing_keys" => ["name"] },
90
+ "schema" => {},
91
+ "error" => "missing keys: name"
92
+ }
93
+ ]
94
+
95
+ err = A2A::Protocol::JsonSchema::ValidationError.new(error_data, definition_name: "Agent Card")
96
+
97
+ it "includes the definition name" do
98
+ err.message.should.include?("Agent Card")
99
+ end
100
+
101
+ it "formats required errors" do
102
+ err.message.should.include?("name is required but missing")
103
+ end
104
+
105
+ it "stores the errors array" do
106
+ err.errors.should == error_data
107
+ end
108
+
109
+ it "stores the definition name" do
110
+ err.definition_name.should == "Agent Card"
111
+ end
112
+
113
+ nested = A2A::Protocol::JsonSchema::ValidationError.new(
114
+ [{ "data_pointer" => "/capabilities/streaming", "type" => "type",
115
+ "schema" => { "type" => "boolean" }, "error" => "wrong type" }],
116
+ definition_name: "Agent Card"
117
+ )
118
+
119
+ it "formats nested paths with dot notation" do
120
+ nested.message.should.include?("capabilities.streaming must be boolean")
121
+ end
122
+
123
+ additional = A2A::Protocol::JsonSchema::ValidationError.new(
124
+ [{ "data_pointer" => "/foo", "type" => "additionalProperties",
125
+ "schema" => {}, "error" => "unexpected" }],
126
+ definition_name: "Test"
127
+ )
128
+
129
+ it "formats additionalProperties errors" do
130
+ additional.message.should.include?("foo has unknown properties")
131
+ end
132
+ end