agent2agent 1.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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/data/a2a.json +1961 -0
  3. data/data/a2a.proto +796 -0
  4. data/data/download-a2a-resources +4 -0
  5. data/data/spec.txt +3611 -0
  6. data/lib/a2a/agent.rb +243 -0
  7. data/lib/a2a/bindings/json_rpc.rb +110 -0
  8. data/lib/a2a/bindings/rest.rb +106 -0
  9. data/lib/a2a/client.rb +85 -0
  10. data/lib/a2a/proto.rb +444 -0
  11. data/lib/a2a/schema/definition.rb +114 -0
  12. data/lib/a2a/schema/validation_error.rb +129 -0
  13. data/lib/a2a/schema.rb +429 -0
  14. data/lib/a2a/server/cancel_task.rb +14 -0
  15. data/lib/a2a/server/create_task_push_notification_config.rb +14 -0
  16. data/lib/a2a/server/delete_task_push_notification_config.rb +14 -0
  17. data/lib/a2a/server/dispatcher.rb +127 -0
  18. data/lib/a2a/server/env.rb +28 -0
  19. data/lib/a2a/server/get_extended_agent_card.rb +15 -0
  20. data/lib/a2a/server/get_task.rb +14 -0
  21. data/lib/a2a/server/get_task_push_notification_config.rb +14 -0
  22. data/lib/a2a/server/list_task_push_notification_configs.rb +14 -0
  23. data/lib/a2a/server/list_tasks.rb +14 -0
  24. data/lib/a2a/server/send_message.rb +14 -0
  25. data/lib/a2a/server/send_streaming_message.rb +14 -0
  26. data/lib/a2a/server/subscribe_to_task.rb +14 -0
  27. data/lib/a2a/server/triage.rb +96 -0
  28. data/lib/a2a/server.rb +98 -0
  29. data/lib/a2a/sse/json_rpc_stream.rb +66 -0
  30. data/lib/a2a/sse/rest_stream.rb +50 -0
  31. data/lib/a2a/sse/stream.rb +129 -0
  32. data/lib/a2a/sse.rb +5 -0
  33. data/lib/a2a/store/processor.rb +136 -0
  34. data/lib/a2a/store/pub_sub.rb +149 -0
  35. data/lib/a2a/store/sqlite.rb +533 -0
  36. data/lib/a2a/store/webhooks.rb +94 -0
  37. data/lib/a2a/store.rb +6 -0
  38. data/lib/a2a/task_store.rb +315 -0
  39. data/lib/a2a/version.rb +5 -0
  40. data/lib/a2a.rb +26 -0
  41. metadata +216 -0
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async"
4
+ require "async/http/internet"
5
+ require "json"
6
+ require "console"
7
+
8
+ module A2A
9
+ module Store
10
+ # Async webhook delivery for A2A push notifications.
11
+ #
12
+ # Following the gospel (async-http):
13
+ # - Uses Async::HTTP::Internet for non-blocking HTTP requests
14
+ # - Each delivery runs in its own Async task (fiber, not thread)
15
+ # - Failures are logged but do not propagate
16
+ #
17
+ # The A2A spec says:
18
+ # - Push notification payloads use StreamResponse format
19
+ # - Content-Type: application/a2a+json
20
+ # - Authentication via scheme + credentials from the config
21
+ # - Token header: X-A2A-Notification-Token
22
+ # - Delivery is at-least-once with possible retries
23
+ #
24
+ class Webhooks
25
+ def initialize
26
+ @internet = nil
27
+ end
28
+
29
+ # Deliver a payload to all push notification configs for a task.
30
+ #
31
+ # @param configs [Array<Hash>] push notification configs
32
+ # @param payload [Hash] the StreamResponse payload
33
+ #
34
+ def deliver(configs, payload)
35
+ return if configs.nil? || configs.empty?
36
+
37
+ configs.each do |config|
38
+ Async do
39
+ deliver_single(config, payload)
40
+ rescue => e
41
+ Console.error(self) { "Webhook delivery failed for #{config["url"]}: #{e.message}" }
42
+ end
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def internet
49
+ @internet ||= Async::HTTP::Internet.new
50
+ end
51
+
52
+ def deliver_single(config, payload)
53
+ url = config["url"]
54
+ return unless url && !url.empty?
55
+
56
+ headers = {
57
+ "content-type" => "application/a2a+json",
58
+ }
59
+
60
+ # Authentication
61
+ if (auth = config["authentication"])
62
+ scheme = auth["scheme"] || "Bearer"
63
+ creds = auth["credentials"] || ""
64
+ headers["authorization"] = "#{scheme} #{creds}"
65
+ end
66
+
67
+ # Token header
68
+ if (token = config["token"])
69
+ headers["x-a2a-notification-token"] = token
70
+ end
71
+
72
+ body = JSON.generate(payload)
73
+
74
+ internet.post(url, headers, [body])
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ test do
81
+ describe "A2A::Store::Webhooks" do
82
+ it "can be instantiated" do
83
+ wh = A2A::Store::Webhooks.new
84
+ wh.should.not.be.nil
85
+ end
86
+
87
+ it "silently skips empty config lists" do
88
+ wh = A2A::Store::Webhooks.new
89
+ # Should not raise — returns nil for empty/nil configs
90
+ wh.deliver([], { "statusUpdate" => {} }).should.be.nil
91
+ wh.deliver(nil, { "statusUpdate" => {} }).should.be.nil
92
+ end
93
+ end
94
+ end
data/lib/a2a/store.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "store/pub_sub"
4
+ require_relative "store/webhooks"
5
+ require_relative "store/sqlite"
6
+ require_relative "store/processor"
@@ -0,0 +1,315 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "a2a"
5
+ require "securerandom"
6
+ require "net/http"
7
+ require "uri"
8
+ require "json"
9
+
10
+ module A2A
11
+ Task = Struct.new(
12
+ :id, :context_id, :state, :result, :artifacts, :history,
13
+ :push_configs, :subscribers, :created_at, :updated_at,
14
+ keyword_init: true
15
+ )
16
+
17
+ # In-memory task registry with pub/sub for streaming and webhook delivery.
18
+ # Swap for a DB-backed implementation in production --
19
+ # if the server crashes, in-memory tasks vanish and the client never gets notified.
20
+ class TaskStore
21
+ TERMINAL_STATES = %w[
22
+ TASK_STATE_COMPLETED TASK_STATE_FAILED
23
+ TASK_STATE_CANCELED TASK_STATE_REJECTED
24
+ ].freeze
25
+
26
+ def initialize
27
+ @tasks = {}
28
+ @mutex = Mutex.new
29
+ end
30
+
31
+ # ── Task CRUD ──────────────────────────────────────────────────────
32
+
33
+ def create(id, context_id, push_config = nil)
34
+ @mutex.synchronize do
35
+ now = Time.now.utc
36
+ configs = {}
37
+ if push_config
38
+ cfg_id = push_config["id"] || SecureRandom.uuid
39
+ push_config["id"] = cfg_id
40
+ configs[cfg_id] = push_config
41
+ end
42
+
43
+ @tasks[id] = Task.new(
44
+ id: id,
45
+ context_id: context_id,
46
+ state: "TASK_STATE_SUBMITTED",
47
+ result: nil,
48
+ artifacts: [],
49
+ history: [],
50
+ push_configs: configs,
51
+ subscribers: [],
52
+ created_at: now,
53
+ updated_at: now,
54
+ )
55
+ end
56
+ end
57
+
58
+ def get(id)
59
+ @mutex.synchronize { @tasks[id] }
60
+ end
61
+
62
+ def update_state(id, state, message: nil)
63
+ task = nil
64
+ @mutex.synchronize do
65
+ task = @tasks[id]
66
+ return nil unless task
67
+ task.state = state
68
+ task.updated_at = Time.now.utc
69
+ end
70
+ if task
71
+ event = build_status_event(task, message)
72
+ notify_subscribers(task, event)
73
+ deliver_webhooks(task, { "statusUpdate" => event })
74
+ end
75
+ task
76
+ end
77
+
78
+ def complete(id, result)
79
+ task = nil
80
+ @mutex.synchronize do
81
+ task = @tasks[id]
82
+ return nil unless task
83
+ task.state = "TASK_STATE_COMPLETED"
84
+ task.result = result
85
+ task.updated_at = Time.now.utc
86
+ end
87
+ if task
88
+ event = build_status_event(task)
89
+ notify_subscribers(task, event)
90
+ deliver_webhooks(task, { "statusUpdate" => event })
91
+ close_subscribers(task)
92
+ end
93
+ task
94
+ end
95
+
96
+ def fail(id, msg)
97
+ task = nil
98
+ @mutex.synchronize do
99
+ task = @tasks[id]
100
+ return nil unless task
101
+ task.state = "TASK_STATE_FAILED"
102
+ task.result = msg
103
+ task.updated_at = Time.now.utc
104
+ end
105
+ if task
106
+ event = build_status_event(task)
107
+ notify_subscribers(task, event)
108
+ deliver_webhooks(task, { "statusUpdate" => event })
109
+ close_subscribers(task)
110
+ end
111
+ task
112
+ end
113
+
114
+ def cancel(id)
115
+ update_state(id, "TASK_STATE_CANCELED")
116
+ task = get(id)
117
+ close_subscribers(task) if task
118
+ task
119
+ end
120
+
121
+ def terminal?(id)
122
+ task = get(id)
123
+ task && TERMINAL_STATES.include?(task.state)
124
+ end
125
+
126
+ # ── Artifacts ──────────────────────────────────────────────────────
127
+
128
+ def add_artifact(id, artifact)
129
+ task = nil
130
+ @mutex.synchronize do
131
+ task = @tasks[id]
132
+ return nil unless task
133
+ task.artifacts << artifact
134
+ task.updated_at = Time.now.utc
135
+ end
136
+ if task
137
+ event = {
138
+ "taskId" => task.id,
139
+ "contextId" => task.context_id,
140
+ "artifact" => artifact,
141
+ "append" => false,
142
+ "lastChunk" => true,
143
+ }
144
+ notify_subscribers(task, event, type: :artifact)
145
+ deliver_webhooks(task, { "artifactUpdate" => event })
146
+ end
147
+ task
148
+ end
149
+
150
+ # ── History ────────────────────────────────────────────────────────
151
+
152
+ def add_message(id, msg)
153
+ @mutex.synchronize do
154
+ task = @tasks[id]
155
+ return nil unless task
156
+ task.history << msg
157
+ task.updated_at = Time.now.utc
158
+ task
159
+ end
160
+ end
161
+
162
+ # ── Listing ────────────────────────────────────────────────────────
163
+
164
+ def list(context_id: nil, state: nil)
165
+ @mutex.synchronize do
166
+ @tasks.values
167
+ .select { |t| context_id.nil? || t.context_id == context_id }
168
+ .select { |t| state.nil? || t.state == state }
169
+ .sort_by { |t| t.updated_at }
170
+ .reverse
171
+ end
172
+ end
173
+
174
+ # ── Push Notification Config CRUD ──────────────────────────────────
175
+
176
+ def create_push_config(task_id, config)
177
+ @mutex.synchronize do
178
+ task = @tasks[task_id]
179
+ return nil unless task
180
+ cfg_id = config["id"] || SecureRandom.uuid
181
+ config["id"] = cfg_id
182
+ config["taskId"] = task_id
183
+ task.push_configs[cfg_id] = config
184
+ task.updated_at = Time.now.utc
185
+ config
186
+ end
187
+ end
188
+
189
+ def get_push_config(task_id, config_id)
190
+ @mutex.synchronize do
191
+ task = @tasks[task_id]
192
+ return nil unless task
193
+ task.push_configs[config_id]
194
+ end
195
+ end
196
+
197
+ def list_push_configs(task_id)
198
+ @mutex.synchronize do
199
+ task = @tasks[task_id]
200
+ return [] unless task
201
+ task.push_configs.values
202
+ end
203
+ end
204
+
205
+ def delete_push_config(task_id, config_id)
206
+ @mutex.synchronize do
207
+ task = @tasks[task_id]
208
+ return nil unless task
209
+ task.push_configs.delete(config_id)
210
+ task.updated_at = Time.now.utc
211
+ end
212
+ end
213
+
214
+ # ── Streaming / Pub-Sub ────────────────────────────────────────────
215
+
216
+ # Subscribe to task updates. Returns a Queue that will receive events.
217
+ # Each event is a Hash: { type: :status|:artifact, data: Hash }
218
+ # A nil sentinel signals stream end.
219
+ def subscribe(task_id)
220
+ queue = Thread::Queue.new
221
+ @mutex.synchronize do
222
+ task = @tasks[task_id]
223
+ return nil unless task
224
+ task.subscribers << queue
225
+ end
226
+ queue
227
+ end
228
+
229
+ def unsubscribe(task_id, queue)
230
+ @mutex.synchronize do
231
+ task = @tasks[task_id]
232
+ return unless task
233
+ task.subscribers.delete(queue)
234
+ end
235
+ end
236
+
237
+ private
238
+
239
+ def build_status_event(task, message = nil)
240
+ event = {
241
+ "taskId" => task.id,
242
+ "contextId" => task.context_id,
243
+ "status" => {
244
+ "state" => task.state,
245
+ "timestamp" => task.updated_at.strftime("%Y-%m-%dT%H:%M:%S.%3NZ"),
246
+ },
247
+ }
248
+ event["status"]["message"] = message if message
249
+ event
250
+ end
251
+
252
+ def notify_subscribers(task, event, type: :status)
253
+ subs = @mutex.synchronize { task.subscribers.dup }
254
+ subs.each do |queue|
255
+ queue << { type: type, data: event }
256
+ rescue ClosedQueueError
257
+ # subscriber disconnected
258
+ end
259
+ end
260
+
261
+ def close_subscribers(task)
262
+ subs = @mutex.synchronize { task.subscribers.dup }
263
+ subs.each do |queue|
264
+ queue << nil # sentinel
265
+ queue.close
266
+ rescue ClosedQueueError
267
+ # already closed
268
+ end
269
+ @mutex.synchronize { task.subscribers.clear }
270
+ end
271
+
272
+ def deliver_webhooks(task, payload)
273
+ configs = @mutex.synchronize { task.push_configs.values.dup }
274
+ return if configs.empty?
275
+
276
+ configs.each do |config|
277
+ Thread.new do
278
+ deliver_single_webhook(config, payload)
279
+ rescue => e
280
+ # Log but don't fail
281
+ $stderr.puts "[A2A::TaskStore] Webhook delivery failed for #{config["url"]}: #{e.message}"
282
+ end
283
+ end
284
+ end
285
+
286
+ def deliver_single_webhook(config, payload)
287
+ url = config["url"]
288
+ return unless url && !url.empty?
289
+
290
+ uri = URI.parse(url)
291
+ http = Net::HTTP.new(uri.host, uri.port)
292
+ http.use_ssl = (uri.scheme == "https")
293
+ http.open_timeout = 10
294
+ http.read_timeout = 30
295
+
296
+ request = Net::HTTP::Post.new(uri.request_uri)
297
+ request["Content-Type"] = "application/a2a+json"
298
+
299
+ # Authentication
300
+ if (auth = config["authentication"])
301
+ scheme = auth["scheme"] || "Bearer"
302
+ creds = auth["credentials"] || ""
303
+ request["Authorization"] = "#{scheme} #{creds}"
304
+ end
305
+
306
+ # Token header
307
+ if (token = config["token"])
308
+ request["X-A2A-Notification-Token"] = token
309
+ end
310
+
311
+ request.body = JSON.generate(payload)
312
+ http.request(request)
313
+ end
314
+ end
315
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module A2A
4
+ VERSION = "1.0.0"
5
+ end
data/lib/a2a.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "scampi"
5
+ require "json"
6
+ require "json_schemer"
7
+ require "async"
8
+ require "async/http/internet"
9
+ require "uri"
10
+ require "rack"
11
+
12
+ module A2A
13
+ end
14
+
15
+ require "a2a/proto"
16
+ require "a2a/schema"
17
+ require "a2a/schema/definition"
18
+ require "a2a/schema/validation_error"
19
+ require "a2a/task_store"
20
+ require "a2a/store"
21
+ require "a2a/sse"
22
+ require "a2a/agent"
23
+
24
+ require "a2a/server"
25
+
26
+ require "a2a/client"
metadata ADDED
@@ -0,0 +1,216 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: agent2agent
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - A2A Contributors
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-01 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: async
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: async-http
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.95'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.95'
40
+ - !ruby/object:Gem::Dependency
41
+ name: protocol-http
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.62'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.62'
54
+ - !ruby/object:Gem::Dependency
55
+ name: scampi
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rack
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: json_schemer
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.5'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2.5'
96
+ - !ruby/object:Gem::Dependency
97
+ name: google-protobuf
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '4.34'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '4.34'
110
+ - !ruby/object:Gem::Dependency
111
+ name: sqlite3
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: rake
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '13.0'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '13.0'
138
+ - !ruby/object:Gem::Dependency
139
+ name: falcon
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '0.55'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '0.55'
152
+ description: Abstraction to help work with A2A protocol
153
+ executables: []
154
+ extensions: []
155
+ extra_rdoc_files: []
156
+ files:
157
+ - data/a2a.json
158
+ - data/a2a.proto
159
+ - data/download-a2a-resources
160
+ - data/spec.txt
161
+ - lib/a2a.rb
162
+ - lib/a2a/agent.rb
163
+ - lib/a2a/bindings/json_rpc.rb
164
+ - lib/a2a/bindings/rest.rb
165
+ - lib/a2a/client.rb
166
+ - lib/a2a/proto.rb
167
+ - lib/a2a/schema.rb
168
+ - lib/a2a/schema/definition.rb
169
+ - lib/a2a/schema/validation_error.rb
170
+ - lib/a2a/server.rb
171
+ - lib/a2a/server/cancel_task.rb
172
+ - lib/a2a/server/create_task_push_notification_config.rb
173
+ - lib/a2a/server/delete_task_push_notification_config.rb
174
+ - lib/a2a/server/dispatcher.rb
175
+ - lib/a2a/server/env.rb
176
+ - lib/a2a/server/get_extended_agent_card.rb
177
+ - lib/a2a/server/get_task.rb
178
+ - lib/a2a/server/get_task_push_notification_config.rb
179
+ - lib/a2a/server/list_task_push_notification_configs.rb
180
+ - lib/a2a/server/list_tasks.rb
181
+ - lib/a2a/server/send_message.rb
182
+ - lib/a2a/server/send_streaming_message.rb
183
+ - lib/a2a/server/subscribe_to_task.rb
184
+ - lib/a2a/server/triage.rb
185
+ - lib/a2a/sse.rb
186
+ - lib/a2a/sse/json_rpc_stream.rb
187
+ - lib/a2a/sse/rest_stream.rb
188
+ - lib/a2a/sse/stream.rb
189
+ - lib/a2a/store.rb
190
+ - lib/a2a/store/processor.rb
191
+ - lib/a2a/store/pub_sub.rb
192
+ - lib/a2a/store/sqlite.rb
193
+ - lib/a2a/store/webhooks.rb
194
+ - lib/a2a/task_store.rb
195
+ - lib/a2a/version.rb
196
+ licenses:
197
+ - MIT
198
+ metadata: {}
199
+ rdoc_options: []
200
+ require_paths:
201
+ - lib
202
+ required_ruby_version: !ruby/object:Gem::Requirement
203
+ requirements:
204
+ - - ">="
205
+ - !ruby/object:Gem::Version
206
+ version: '3.2'
207
+ required_rubygems_version: !ruby/object:Gem::Requirement
208
+ requirements:
209
+ - - ">="
210
+ - !ruby/object:Gem::Version
211
+ version: '0'
212
+ requirements: []
213
+ rubygems_version: 3.7.2
214
+ specification_version: 4
215
+ summary: Agent2Agent protocol
216
+ test_files: []