a2a-ruby 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 (128) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +137 -0
  4. data/.simplecov +46 -0
  5. data/.yardopts +10 -0
  6. data/CHANGELOG.md +33 -0
  7. data/CODE_OF_CONDUCT.md +128 -0
  8. data/CONTRIBUTING.md +165 -0
  9. data/Gemfile +43 -0
  10. data/Guardfile +34 -0
  11. data/LICENSE.txt +21 -0
  12. data/PUBLISHING_CHECKLIST.md +214 -0
  13. data/README.md +171 -0
  14. data/Rakefile +165 -0
  15. data/docs/agent_execution.md +309 -0
  16. data/docs/api_reference.md +792 -0
  17. data/docs/configuration.md +780 -0
  18. data/docs/events.md +475 -0
  19. data/docs/getting_started.md +668 -0
  20. data/docs/integration.md +262 -0
  21. data/docs/server_apps.md +621 -0
  22. data/docs/troubleshooting.md +765 -0
  23. data/lib/a2a/client/api_methods.rb +263 -0
  24. data/lib/a2a/client/auth/api_key.rb +161 -0
  25. data/lib/a2a/client/auth/interceptor.rb +288 -0
  26. data/lib/a2a/client/auth/jwt.rb +189 -0
  27. data/lib/a2a/client/auth/oauth2.rb +146 -0
  28. data/lib/a2a/client/auth.rb +137 -0
  29. data/lib/a2a/client/base.rb +316 -0
  30. data/lib/a2a/client/config.rb +210 -0
  31. data/lib/a2a/client/connection_pool.rb +233 -0
  32. data/lib/a2a/client/http_client.rb +524 -0
  33. data/lib/a2a/client/json_rpc_handler.rb +136 -0
  34. data/lib/a2a/client/middleware/circuit_breaker_interceptor.rb +245 -0
  35. data/lib/a2a/client/middleware/logging_interceptor.rb +371 -0
  36. data/lib/a2a/client/middleware/rate_limit_interceptor.rb +142 -0
  37. data/lib/a2a/client/middleware/retry_interceptor.rb +161 -0
  38. data/lib/a2a/client/middleware.rb +116 -0
  39. data/lib/a2a/client/performance_tracker.rb +60 -0
  40. data/lib/a2a/configuration/defaults.rb +34 -0
  41. data/lib/a2a/configuration/environment_loader.rb +76 -0
  42. data/lib/a2a/configuration/file_loader.rb +115 -0
  43. data/lib/a2a/configuration/inheritance.rb +101 -0
  44. data/lib/a2a/configuration/validator.rb +180 -0
  45. data/lib/a2a/configuration.rb +201 -0
  46. data/lib/a2a/errors.rb +291 -0
  47. data/lib/a2a/modules.rb +50 -0
  48. data/lib/a2a/monitoring/alerting.rb +490 -0
  49. data/lib/a2a/monitoring/distributed_tracing.rb +398 -0
  50. data/lib/a2a/monitoring/health_endpoints.rb +204 -0
  51. data/lib/a2a/monitoring/metrics_collector.rb +438 -0
  52. data/lib/a2a/monitoring.rb +463 -0
  53. data/lib/a2a/plugin.rb +358 -0
  54. data/lib/a2a/plugin_manager.rb +159 -0
  55. data/lib/a2a/plugins/example_auth.rb +81 -0
  56. data/lib/a2a/plugins/example_middleware.rb +118 -0
  57. data/lib/a2a/plugins/example_transport.rb +76 -0
  58. data/lib/a2a/protocol/agent_card.rb +8 -0
  59. data/lib/a2a/protocol/agent_card_server.rb +584 -0
  60. data/lib/a2a/protocol/capability.rb +496 -0
  61. data/lib/a2a/protocol/json_rpc.rb +254 -0
  62. data/lib/a2a/protocol/message.rb +8 -0
  63. data/lib/a2a/protocol/task.rb +8 -0
  64. data/lib/a2a/rails/a2a_controller.rb +258 -0
  65. data/lib/a2a/rails/controller_helpers.rb +499 -0
  66. data/lib/a2a/rails/engine.rb +167 -0
  67. data/lib/a2a/rails/generators/agent_generator.rb +311 -0
  68. data/lib/a2a/rails/generators/install_generator.rb +209 -0
  69. data/lib/a2a/rails/generators/migration_generator.rb +232 -0
  70. data/lib/a2a/rails/generators/templates/add_a2a_indexes.rb +57 -0
  71. data/lib/a2a/rails/generators/templates/agent_controller.rb +122 -0
  72. data/lib/a2a/rails/generators/templates/agent_controller_spec.rb +160 -0
  73. data/lib/a2a/rails/generators/templates/agent_readme.md +200 -0
  74. data/lib/a2a/rails/generators/templates/create_a2a_push_notification_configs.rb +68 -0
  75. data/lib/a2a/rails/generators/templates/create_a2a_tasks.rb +83 -0
  76. data/lib/a2a/rails/generators/templates/example_agent_controller.rb +228 -0
  77. data/lib/a2a/rails/generators/templates/initializer.rb +108 -0
  78. data/lib/a2a/rails/generators/templates/push_notification_config_model.rb +228 -0
  79. data/lib/a2a/rails/generators/templates/task_model.rb +200 -0
  80. data/lib/a2a/rails/tasks/a2a.rake +228 -0
  81. data/lib/a2a/server/a2a_methods.rb +520 -0
  82. data/lib/a2a/server/agent.rb +537 -0
  83. data/lib/a2a/server/agent_execution/agent_executor.rb +279 -0
  84. data/lib/a2a/server/agent_execution/request_context.rb +219 -0
  85. data/lib/a2a/server/apps/rack_app.rb +311 -0
  86. data/lib/a2a/server/apps/sinatra_app.rb +261 -0
  87. data/lib/a2a/server/default_request_handler.rb +350 -0
  88. data/lib/a2a/server/events/event_consumer.rb +116 -0
  89. data/lib/a2a/server/events/event_queue.rb +226 -0
  90. data/lib/a2a/server/example_agent.rb +248 -0
  91. data/lib/a2a/server/handler.rb +281 -0
  92. data/lib/a2a/server/middleware/authentication_middleware.rb +212 -0
  93. data/lib/a2a/server/middleware/cors_middleware.rb +171 -0
  94. data/lib/a2a/server/middleware/logging_middleware.rb +362 -0
  95. data/lib/a2a/server/middleware/rate_limit_middleware.rb +382 -0
  96. data/lib/a2a/server/middleware.rb +213 -0
  97. data/lib/a2a/server/push_notification_manager.rb +327 -0
  98. data/lib/a2a/server/request_handler.rb +136 -0
  99. data/lib/a2a/server/storage/base.rb +141 -0
  100. data/lib/a2a/server/storage/database.rb +266 -0
  101. data/lib/a2a/server/storage/memory.rb +274 -0
  102. data/lib/a2a/server/storage/redis.rb +320 -0
  103. data/lib/a2a/server/storage.rb +38 -0
  104. data/lib/a2a/server/task_manager.rb +534 -0
  105. data/lib/a2a/transport/grpc.rb +481 -0
  106. data/lib/a2a/transport/http.rb +415 -0
  107. data/lib/a2a/transport/sse.rb +499 -0
  108. data/lib/a2a/types/agent_card.rb +540 -0
  109. data/lib/a2a/types/artifact.rb +99 -0
  110. data/lib/a2a/types/base_model.rb +223 -0
  111. data/lib/a2a/types/events.rb +117 -0
  112. data/lib/a2a/types/message.rb +106 -0
  113. data/lib/a2a/types/part.rb +288 -0
  114. data/lib/a2a/types/push_notification.rb +139 -0
  115. data/lib/a2a/types/security.rb +167 -0
  116. data/lib/a2a/types/task.rb +154 -0
  117. data/lib/a2a/types.rb +88 -0
  118. data/lib/a2a/utils/helpers.rb +245 -0
  119. data/lib/a2a/utils/message_buffer.rb +278 -0
  120. data/lib/a2a/utils/performance.rb +247 -0
  121. data/lib/a2a/utils/rails_detection.rb +97 -0
  122. data/lib/a2a/utils/structured_logger.rb +306 -0
  123. data/lib/a2a/utils/time_helpers.rb +167 -0
  124. data/lib/a2a/utils/validation.rb +8 -0
  125. data/lib/a2a/version.rb +6 -0
  126. data/lib/a2a-rails.rb +58 -0
  127. data/lib/a2a.rb +198 -0
  128. metadata +437 -0
@@ -0,0 +1,415 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+ require "concurrent"
6
+
7
+ # Try to load multipart support - graceful degradation if not available
8
+ begin
9
+ require "faraday/multipart"
10
+ MULTIPART_AVAILABLE = true
11
+ rescue LoadError
12
+ MULTIPART_AVAILABLE = false
13
+ end
14
+
15
+ module A2A
16
+ module Transport
17
+ end
18
+ end
19
+
20
+ ##
21
+ # HTTP transport implementation using Faraday adapter pattern
22
+ # Provides connection pooling, timeout management, logging, and HTTPS support
23
+ #
24
+ module A2A
25
+ module Transport
26
+ class Http
27
+ # Default configuration values
28
+ DEFAULT_TIMEOUT = 30
29
+ DEFAULT_OPEN_TIMEOUT = 10
30
+ DEFAULT_READ_TIMEOUT = 30
31
+ DEFAULT_WRITE_TIMEOUT = 30
32
+ DEFAULT_POOL_SIZE = 5
33
+ DEFAULT_POOL_TIMEOUT = 5
34
+ DEFAULT_RETRY_COUNT = 3
35
+ DEFAULT_RETRY_DELAY = 1.0
36
+
37
+ attr_reader :base_url, :config, :connection
38
+
39
+ ##
40
+ # Initialize HTTP transport
41
+ #
42
+ # @param base_url [String] Base URL for the HTTP endpoint
43
+ # @param config [Hash] Configuration options
44
+ # @option config [Integer] :timeout (30) Request timeout in seconds
45
+ # @option config [Integer] :open_timeout (10) Connection open timeout
46
+ # @option config [Integer] :read_timeout (30) Read timeout
47
+ # @option config [Integer] :write_timeout (30) Write timeout
48
+ # @option config [Integer] :pool_size (5) Connection pool size
49
+ # @option config [Integer] :pool_timeout (5) Pool checkout timeout
50
+ # @option config [Integer] :retry_count (3) Number of retries
51
+ # @option config [Float] :retry_delay (1.0) Delay between retries
52
+ # @option config [Boolean] :ssl_verify (true) Verify SSL certificates
53
+ # @option config [String] :ssl_ca_file Path to CA certificate file
54
+ # @option config [String] :ssl_ca_path Path to CA certificate directory
55
+ # @option config [Hash] :headers ({}) Default headers
56
+ # @option config [Boolean] :logging (false) Enable request/response logging
57
+ # @option config [Logger] :logger Logger instance
58
+ # @option config [Hash] :proxy Proxy configuration
59
+ #
60
+ def initialize(base_url, config = {})
61
+ @base_url = base_url
62
+ @config = default_config.merge(config)
63
+ @connection = build_connection
64
+ @metrics = Concurrent::Hash.new(0)
65
+ end
66
+
67
+ ##
68
+ # Send HTTP request
69
+ #
70
+ # @param method [Symbol] HTTP method (:get, :post, :put, :delete, etc.)
71
+ # @param path [String] Request path
72
+ # @param params [Hash] Request parameters
73
+ # @param headers [Hash] Request headers
74
+ # @param body [String, Hash] Request body
75
+ # @return [Faraday::Response] HTTP response
76
+ # @raise [A2A::Errors::HTTPError] On HTTP errors
77
+ # @raise [A2A::Errors::TimeoutError] On timeout
78
+ # @raise [A2A::Errors::TransportError] On transport errors
79
+ #
80
+ def request(method, path = "", params: {}, headers: {}, body: nil)
81
+ start_time = Time.now
82
+
83
+ begin
84
+ response = @connection.public_send(method, path) do |req|
85
+ req.params.update(params) if params.any?
86
+ req.headers.update(headers) if headers.any?
87
+ req.body = prepare_body(body) if body
88
+ end
89
+
90
+ record_metrics(method, response.status, Time.now - start_time)
91
+ handle_response(response)
92
+ response
93
+ rescue A2A::Errors::HTTPError => e
94
+ # Re-raise A2A HTTP errors (from handle_response)
95
+ record_metrics(method, e.status_code || :http_error, Time.now - start_time)
96
+ raise e
97
+ rescue Faraday::TimeoutError => e
98
+ record_metrics(method, :timeout, Time.now - start_time)
99
+ raise A2A::Errors::TimeoutError, "Request timeout: #{e.message}"
100
+ rescue Faraday::ConnectionFailed => e
101
+ record_metrics(method, :connection_failed, Time.now - start_time)
102
+ # Check if it's a timeout-like error
103
+ if e.message.include?("timeout") || e.message.include?("execution expired")
104
+ raise A2A::Errors::TimeoutError, "Request timeout: #{e.message}"
105
+ end
106
+
107
+ raise A2A::Errors::TransportError, "Connection failed: #{e.message}"
108
+ rescue Faraday::SSLError => e
109
+ record_metrics(method, :ssl_error, Time.now - start_time)
110
+ raise A2A::Errors::TransportError, "SSL error: #{e.message}"
111
+ rescue Faraday::ClientError => e
112
+ record_metrics(method, :client_error, Time.now - start_time)
113
+ # Handle HTTP status errors from Faraday
114
+ raise A2A::Errors::TransportError, "Client error: #{e.message}" unless e.response && e.response[:status]
115
+
116
+ status = e.response[:status]
117
+ case status
118
+ when 400..499
119
+ raise A2A::Errors::HTTPError.new(
120
+ "Client error: #{status}",
121
+ status_code: status,
122
+ response_body: e.response[:body]
123
+ )
124
+ when 500..599
125
+ raise A2A::Errors::HTTPError.new(
126
+ "Server error: #{status}",
127
+ status_code: status,
128
+ response_body: e.response[:body]
129
+ )
130
+ else
131
+ raise A2A::Errors::HTTPError.new(
132
+ "HTTP error: #{status}",
133
+ status_code: status,
134
+ response_body: e.response[:body]
135
+ )
136
+ end
137
+ rescue StandardError => e
138
+ record_metrics(method, :error, Time.now - start_time)
139
+ # Check if it's a timeout-like error
140
+ if e.message.include?("timeout") || e.message.include?("execution expired")
141
+ raise A2A::Errors::TimeoutError, "Request timeout: #{e.message}"
142
+ end
143
+
144
+ raise A2A::Errors::TransportError, "Transport error: #{e.message}"
145
+ end
146
+ end
147
+
148
+ ##
149
+ # Send GET request
150
+ #
151
+ # @param path [String] Request path
152
+ # @param params [Hash] Query parameters
153
+ # @param headers [Hash] Request headers
154
+ # @return [Faraday::Response] HTTP response
155
+ #
156
+ def get(path = "", params: {}, headers: {})
157
+ request(:get, path, params: params, headers: headers)
158
+ end
159
+
160
+ ##
161
+ # Send POST request
162
+ #
163
+ # @param path [String] Request path
164
+ # @param body [String, Hash] Request body
165
+ # @param params [Hash] Query parameters
166
+ # @param headers [Hash] Request headers
167
+ # @return [Faraday::Response] HTTP response
168
+ #
169
+ def post(path = "", body: nil, params: {}, headers: {})
170
+ request(:post, path, params: params, headers: headers, body: body)
171
+ end
172
+
173
+ ##
174
+ # Send PUT request
175
+ #
176
+ # @param path [String] Request path
177
+ # @param body [String, Hash] Request body
178
+ # @param params [Hash] Query parameters
179
+ # @param headers [Hash] Request headers
180
+ # @return [Faraday::Response] HTTP response
181
+ #
182
+ def put(path = "", body: nil, params: {}, headers: {})
183
+ request(:put, path, params: params, headers: headers, body: body)
184
+ end
185
+
186
+ ##
187
+ # Send DELETE request
188
+ #
189
+ # @param path [String] Request path
190
+ # @param params [Hash] Query parameters
191
+ # @param headers [Hash] Request headers
192
+ # @return [Faraday::Response] HTTP response
193
+ #
194
+ def delete(path = "", params: {}, headers: {})
195
+ request(:delete, path, params: params, headers: headers)
196
+ end
197
+
198
+ ##
199
+ # Send JSON-RPC request
200
+ #
201
+ # @param rpc_request [Hash] JSON-RPC request object
202
+ # @param headers [Hash] Additional headers
203
+ # @return [Hash] JSON-RPC response
204
+ # @raise [A2A::Errors::JSONError] On JSON parsing errors
205
+ #
206
+ def json_rpc_request(rpc_request, headers: {})
207
+ default_headers = {
208
+ "Content-Type" => "application/json",
209
+ "Accept" => "application/json"
210
+ }
211
+
212
+ response = post(
213
+ body: rpc_request.to_json,
214
+ headers: default_headers.merge(headers)
215
+ )
216
+
217
+ parse_json_response(response)
218
+ end
219
+
220
+ ##
221
+ # Get connection metrics
222
+ #
223
+ # @return [Hash] Metrics data
224
+ #
225
+ def metrics
226
+ @metrics.to_h
227
+ end
228
+
229
+ ##
230
+ # Reset connection metrics
231
+ #
232
+ def reset_metrics!
233
+ @metrics.clear
234
+ end
235
+
236
+ ##
237
+ # Close connection and cleanup resources
238
+ #
239
+ def close
240
+ @connection&.close
241
+ end
242
+
243
+ private
244
+
245
+ ##
246
+ # Build default configuration
247
+ #
248
+ # @return [Hash] Default configuration
249
+ #
250
+ def default_config
251
+ {
252
+ timeout: DEFAULT_TIMEOUT,
253
+ open_timeout: DEFAULT_OPEN_TIMEOUT,
254
+ read_timeout: DEFAULT_READ_TIMEOUT,
255
+ write_timeout: DEFAULT_WRITE_TIMEOUT,
256
+ pool_size: DEFAULT_POOL_SIZE,
257
+ pool_timeout: DEFAULT_POOL_TIMEOUT,
258
+ retry_count: DEFAULT_RETRY_COUNT,
259
+ retry_delay: DEFAULT_RETRY_DELAY,
260
+ ssl_verify: true,
261
+ headers: {},
262
+ logging: false,
263
+ logger: nil,
264
+ proxy: nil
265
+ }
266
+ end
267
+
268
+ ##
269
+ # Build Faraday connection with configuration
270
+ #
271
+ # @return [Faraday::Connection] Configured connection
272
+ #
273
+ def build_connection
274
+ Faraday.new(@base_url) do |conn|
275
+ # Request/response middleware
276
+ conn.request :json
277
+ conn.request :multipart if MULTIPART_AVAILABLE
278
+ conn.request :url_encoded
279
+
280
+ # NOTE: Retry middleware requires faraday-retry gem
281
+ # Uncomment when faraday-retry is available:
282
+ # conn.request :retry,
283
+ # max: @config[:retry_count],
284
+ # interval: @config[:retry_delay],
285
+ # backoff_factor: 2,
286
+ # retry_statuses: [429, 500, 502, 503, 504],
287
+ # methods: [:get, :post, :put, :delete]
288
+
289
+ # Logging middleware
290
+ conn.response :logger, @config[:logger] || default_logger if @config[:logging]
291
+
292
+ # Response middleware
293
+ conn.response :json, content_type: /\bjson$/
294
+ # NOTE: Not using :raise_error to handle errors manually
295
+
296
+ # Adapter (use default net_http adapter)
297
+ # Note: net_http_persistent requires separate gem for connection pooling
298
+ conn.adapter Faraday.default_adapter
299
+
300
+ # Configure timeouts
301
+ conn.options.timeout = @config[:timeout]
302
+ conn.options.open_timeout = @config[:open_timeout]
303
+ conn.options.read_timeout = @config[:read_timeout]
304
+ conn.options.write_timeout = @config[:write_timeout]
305
+
306
+ # Configure SSL
307
+ conn.ssl.verify = @config[:ssl_verify]
308
+ conn.ssl.ca_file = @config[:ssl_ca_file] if @config[:ssl_ca_file]
309
+ conn.ssl.ca_path = @config[:ssl_ca_path] if @config[:ssl_ca_path]
310
+
311
+ # Configure proxy
312
+ conn.proxy = @config[:proxy] if @config[:proxy]
313
+
314
+ # Set default headers
315
+ conn.headers.update(@config[:headers]) if @config[:headers].any?
316
+ end
317
+ end
318
+
319
+ ##
320
+ # Prepare request body
321
+ #
322
+ # @param body [String, Hash, Object] Request body
323
+ # @return [String] Prepared body
324
+ #
325
+ def prepare_body(body)
326
+ case body
327
+ when String
328
+ body
329
+ when Hash, Array
330
+ body.to_json
331
+ else
332
+ body.respond_to?(:to_json) ? body.to_json : body.to_s
333
+ end
334
+ end
335
+
336
+ ##
337
+ # Handle HTTP response
338
+ #
339
+ # @param response [Faraday::Response] HTTP response
340
+ # @return [Faraday::Response] Validated response
341
+ # @raise [A2A::Errors::HTTPError] On HTTP errors
342
+ #
343
+ def handle_response(response)
344
+ case response.status
345
+ when 200..299
346
+ response
347
+ when 400..499
348
+ raise A2A::Errors::HTTPError.new(
349
+ "Client error: #{response.status}",
350
+ status_code: response.status,
351
+ response_body: response.body
352
+ )
353
+ when 500..599
354
+ raise A2A::Errors::HTTPError.new(
355
+ "Server error: #{response.status}",
356
+ status_code: response.status,
357
+ response_body: response.body
358
+ )
359
+ else
360
+ raise A2A::Errors::HTTPError.new(
361
+ "Unexpected status: #{response.status}",
362
+ status_code: response.status,
363
+ response_body: response.body
364
+ )
365
+ end
366
+ end
367
+
368
+ ##
369
+ # Parse JSON response
370
+ #
371
+ # @param response [Faraday::Response] HTTP response
372
+ # @return [Hash] Parsed JSON data
373
+ # @raise [A2A::Errors::JSONError] On JSON parsing errors
374
+ #
375
+ def parse_json_response(response)
376
+ return response.body if response.body.is_a?(Hash)
377
+
378
+ JSON.parse(response.body)
379
+ rescue JSON::ParserError => e
380
+ raise A2A::Errors::JSONError, "Invalid JSON response: #{e.message}"
381
+ end
382
+
383
+ ##
384
+ # Record request metrics
385
+ #
386
+ # @param method [Symbol] HTTP method
387
+ # @param status [Integer, Symbol] Response status or error type
388
+ # @param duration [Float] Request duration in seconds
389
+ #
390
+ def record_metrics(method, status, duration)
391
+ @metrics["#{method}_requests"] += 1
392
+ @metrics["#{method}_#{status}"] += 1
393
+ @metrics["total_requests"] += 1
394
+ @metrics["total_duration"] += duration
395
+
396
+ # Track average duration
397
+ @metrics["average_duration"] = @metrics["total_duration"] / @metrics["total_requests"]
398
+ end
399
+
400
+ ##
401
+ # Get default logger
402
+ #
403
+ # @return [Logger] Default logger instance
404
+ #
405
+ def default_logger
406
+ logger = Logger.new($stdout)
407
+ logger.level = Logger::INFO
408
+ logger.formatter = proc do |severity, datetime, progname, msg|
409
+ "[#{datetime}] #{severity} -- #{progname}: #{msg}\n"
410
+ end
411
+ logger
412
+ end
413
+ end
414
+ end
415
+ end