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,524 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+ require "securerandom"
6
+ require_relative "base"
7
+ require_relative "performance_tracker"
8
+ require_relative "json_rpc_handler"
9
+ require_relative "api_methods"
10
+
11
+ # Try to use connection pooling adapter if available
12
+ begin
13
+ require "net/http/persistent"
14
+ HTTP_ADAPTER = :net_http_persistent
15
+ rescue LoadError
16
+ HTTP_ADAPTER = Faraday.default_adapter
17
+ end
18
+
19
+ ##
20
+ # HTTP client implementation for A2A protocol
21
+ #
22
+ # Provides JSON-RPC 2.0 over HTTP(S) communication with A2A agents,
23
+ # including support for streaming responses via Server-Sent Events.
24
+ #
25
+ module A2A
26
+ module Client
27
+ class HttpClient < A2A::Client::Base
28
+ include PerformanceTracker
29
+ include JsonRpcHandler
30
+ include ApiMethods
31
+
32
+ attr_reader :endpoint_url, :connection
33
+
34
+ ##
35
+ # Initialize a new HTTP client
36
+ #
37
+ # @param endpoint_url [String] The base URL for the A2A agent
38
+ # @param config [Config, nil] Client configuration
39
+ # @param middleware [Array] List of middleware interceptors
40
+ # @param consumers [Array] List of event consumers
41
+ def initialize(endpoint_url, config: nil, middleware: [], consumers: [])
42
+ super(config: config, middleware: middleware, consumers: consumers)
43
+ @endpoint_url = endpoint_url.chomp("/")
44
+ @connection = build_connection
45
+ @connection_pool = nil
46
+ initialize_performance_tracking
47
+ initialize_json_rpc_handling
48
+ end
49
+
50
+ ##
51
+ # Get the agent card
52
+ #
53
+ # @param context [Hash, nil] Optional context information
54
+ # @param authenticated [Boolean] Whether to get authenticated extended card
55
+ # @return [AgentCard] The agent card
56
+ def get_card(context: nil, authenticated: false)
57
+ if authenticated
58
+ request = build_json_rpc_request("agent/getAuthenticatedExtendedCard", {})
59
+ response = execute_with_middleware(request, context || {}) do |req, _ctx|
60
+ send_json_rpc_request(req)
61
+ end
62
+ ensure_agent_card(response["result"])
63
+ else
64
+ # Use HTTP GET for basic agent card
65
+ response = execute_with_middleware({}, context || {}) do |_req, _ctx|
66
+ @connection.get("/agent-card") do |request|
67
+ request.headers.merge!(@config.all_headers)
68
+ end
69
+ end
70
+
71
+ if response.success?
72
+ ensure_agent_card(JSON.parse(response.body))
73
+ else
74
+ raise A2A::Errors::HTTPError.new(
75
+ "Failed to get agent card: #{response.status}",
76
+ status_code: response.status,
77
+ response_body: response.body
78
+ )
79
+ end
80
+ end
81
+ end
82
+
83
+ ##
84
+ # Resubscribe to a task for streaming updates
85
+ #
86
+ # @param task_id [String] The task ID to resubscribe to
87
+ # @param context [Hash, nil] Optional context information
88
+ # @return [Enumerator] Stream of task updates
89
+ def resubscribe(task_id, context: nil)
90
+ request = build_json_rpc_request("tasks/resubscribe", { id: task_id })
91
+
92
+ execute_with_middleware(request, context || {}) do |req, _ctx|
93
+ send_streaming_request(req)
94
+ end
95
+ end
96
+
97
+ ##
98
+ # Set a callback for task updates
99
+ #
100
+ # @param task_id [String] The task ID
101
+ # @param push_notification_config [PushNotificationConfig, Hash] The push notification configuration
102
+ # @param context [Hash, nil] Optional context information
103
+ # @return [void]
104
+ def set_task_callback(task_id, push_notification_config, context: nil)
105
+ config = if push_notification_config.is_a?(A2A::Types::PushNotificationConfig)
106
+ push_notification_config
107
+ else
108
+ A2A::Types::PushNotificationConfig.from_h(push_notification_config)
109
+ end
110
+
111
+ params = {
112
+ taskId: task_id,
113
+ pushNotificationConfig: config.to_h
114
+ }
115
+
116
+ request = build_json_rpc_request("tasks/pushNotificationConfig/set", params)
117
+ execute_with_middleware(request, context || {}) do |req, _ctx|
118
+ send_json_rpc_request(req)
119
+ end
120
+ end
121
+
122
+ ##
123
+ # Get the callback configuration for a task
124
+ #
125
+ # @param task_id [String] The task ID
126
+ # @param push_notification_config_id [String] The push notification config ID
127
+ # @param context [Hash, nil] Optional context information
128
+ # @return [TaskPushNotificationConfig] The callback configuration
129
+ def get_task_callback(task_id, push_notification_config_id, context: nil)
130
+ params = {
131
+ taskId: task_id,
132
+ pushNotificationConfigId: push_notification_config_id
133
+ }
134
+
135
+ request = build_json_rpc_request("tasks/pushNotificationConfig/get", params)
136
+ response = execute_with_middleware(request, context || {}) do |req, _ctx|
137
+ send_json_rpc_request(req)
138
+ end
139
+
140
+ A2A::Types::TaskPushNotificationConfig.from_h(response["result"])
141
+ end
142
+
143
+ ##
144
+ # List all callback configurations for a task
145
+ #
146
+ # @param task_id [String] The task ID
147
+ # @param context [Hash, nil] Optional context information
148
+ # @return [Array<TaskPushNotificationConfig>] List of callback configurations
149
+ def list_task_callbacks(task_id, context: nil)
150
+ request = build_json_rpc_request("tasks/pushNotificationConfig/list", { taskId: task_id })
151
+ response = execute_with_middleware(request, context || {}) do |req, _ctx|
152
+ send_json_rpc_request(req)
153
+ end
154
+
155
+ response["result"].map { |config| A2A::Types::TaskPushNotificationConfig.from_h(config) }
156
+ end
157
+
158
+ ##
159
+ # Delete a callback configuration for a task
160
+ #
161
+ # @param task_id [String] The task ID
162
+ # @param push_notification_config_id [String] The push notification config ID
163
+ # @param context [Hash, nil] Optional context information
164
+ # @return [void]
165
+ def delete_task_callback(task_id, push_notification_config_id, context: nil)
166
+ params = {
167
+ taskId: task_id,
168
+ pushNotificationConfigId: push_notification_config_id
169
+ }
170
+
171
+ request = build_json_rpc_request("tasks/pushNotificationConfig/delete", params)
172
+ execute_with_middleware(request, context || {}) do |req, _ctx|
173
+ send_json_rpc_request(req)
174
+ end
175
+ end
176
+
177
+ ##
178
+ # Set push notification configuration for a task
179
+ #
180
+ # @param task_id [String] The task ID
181
+ # @param config [PushNotificationConfig, Hash] The push notification configuration
182
+ # @param context [Hash, nil] Optional context information
183
+ # @return [Hash] The created configuration with ID
184
+ def set_task_push_notification_config(task_id, config, context: nil)
185
+ # Validate and normalize config
186
+ normalized_config = if config.is_a?(A2A::Types::PushNotificationConfig)
187
+ config.to_h
188
+ elsif config.is_a?(Hash)
189
+ # Validate required fields
190
+ raise ArgumentError, "config must include 'url'" unless config[:url] || config["url"]
191
+
192
+ config
193
+ else
194
+ raise ArgumentError, "config must be a Hash or PushNotificationConfig"
195
+ end
196
+
197
+ params = {
198
+ taskId: task_id,
199
+ pushNotificationConfig: normalized_config
200
+ }
201
+
202
+ request = build_json_rpc_request("tasks/pushNotificationConfig/set", params)
203
+ response = execute_with_middleware(request, context || {}) do |req, _ctx|
204
+ send_json_rpc_request(req)
205
+ end
206
+
207
+ response["result"]
208
+ end
209
+
210
+ ##
211
+ # Get push notification configuration for a task
212
+ #
213
+ # @param task_id [String] The task ID
214
+ # @param config_id [String] The push notification config ID
215
+ # @param context [Hash, nil] Optional context information
216
+ # @return [Hash] The push notification configuration
217
+ def get_task_push_notification_config(task_id, config_id, context: nil)
218
+ params = {
219
+ taskId: task_id,
220
+ pushNotificationConfigId: config_id
221
+ }
222
+
223
+ request = build_json_rpc_request("tasks/pushNotificationConfig/get", params)
224
+ response = execute_with_middleware(request, context || {}) do |req, _ctx|
225
+ send_json_rpc_request(req)
226
+ end
227
+
228
+ response["result"]
229
+ end
230
+
231
+ ##
232
+ # List all push notification configurations for a task
233
+ #
234
+ # @param task_id [String] The task ID
235
+ # @param context [Hash, nil] Optional context information
236
+ # @return [Array<Hash>] List of push notification configurations
237
+ def list_task_push_notification_configs(task_id, context: nil)
238
+ request = build_json_rpc_request("tasks/pushNotificationConfig/list", { taskId: task_id })
239
+ response = execute_with_middleware(request, context || {}) do |req, _ctx|
240
+ send_json_rpc_request(req)
241
+ end
242
+
243
+ response["result"] || []
244
+ end
245
+
246
+ ##
247
+ # Delete push notification configuration for a task
248
+ #
249
+ # @param task_id [String] The task ID
250
+ # @param config_id [String] The push notification config ID
251
+ # @param context [Hash, nil] Optional context information
252
+ # @return [Boolean] True if deletion was successful
253
+ def delete_task_push_notification_config(task_id, config_id, context: nil)
254
+ params = {
255
+ taskId: task_id,
256
+ pushNotificationConfigId: config_id
257
+ }
258
+
259
+ request = build_json_rpc_request("tasks/pushNotificationConfig/delete", params)
260
+ execute_with_middleware(request, context || {}) do |req, _ctx|
261
+ send_json_rpc_request(req)
262
+ end
263
+
264
+ true
265
+ end
266
+
267
+ private
268
+
269
+ ##
270
+ # Build the Faraday connection with performance optimizations
271
+ #
272
+ # @return [Faraday::Connection] The configured connection
273
+ def build_connection
274
+ Faraday.new(@endpoint_url) do |conn|
275
+ # Request middleware
276
+ conn.request :json
277
+
278
+ # Response middleware
279
+ conn.response :json, content_type: /\bjson$/
280
+
281
+ # Use connection pooling adapter if available
282
+ conn.adapter HTTP_ADAPTER
283
+
284
+ # Set timeouts
285
+ conn.options.timeout = @config.timeout
286
+ conn.options.read_timeout = @config.timeout
287
+ conn.options.write_timeout = @config.timeout
288
+
289
+ # Performance optimizations (only set if supported)
290
+ conn.options.pool_size = @config.pool_size || 5 if conn.options.respond_to?(:pool_size=)
291
+
292
+ # Enable compression if supported
293
+ conn.headers["Accept-Encoding"] = "gzip, deflate"
294
+
295
+ # Set keep-alive headers
296
+ conn.headers["Connection"] = "keep-alive"
297
+ conn.headers["Keep-Alive"] = "timeout=30, max=100"
298
+ end
299
+ end
300
+
301
+ ##
302
+ # Send a synchronous message
303
+ #
304
+ # @param message [Message] The message to send
305
+ # @param context [Hash] The request context
306
+ # @return [Message] The response message
307
+ def send_sync_message(message, context)
308
+ request = build_json_rpc_request("message/send", message.to_h)
309
+ response = execute_with_middleware(request, context) do |req, _ctx|
310
+ send_json_rpc_request(req)
311
+ end
312
+
313
+ ensure_message(response["result"])
314
+ end
315
+
316
+ ##
317
+ # Send a streaming message
318
+ #
319
+ # @param message [Message] The message to send
320
+ # @param context [Hash] The request context
321
+ # @return [Enumerator] Stream of response messages
322
+ def send_streaming_message(message, context)
323
+ request = build_json_rpc_request("message/stream", message.to_h)
324
+
325
+ execute_with_middleware(request, context) do |req, _ctx|
326
+ send_streaming_request(req)
327
+ end
328
+ end
329
+
330
+ ##
331
+ # Send a JSON-RPC request and get response
332
+ #
333
+ # @param request [Hash] The JSON-RPC request
334
+ # @return [Hash] The JSON-RPC response
335
+ def send_json_rpc_request(request)
336
+ response = @connection.post do |req|
337
+ req.headers.merge!(@config.all_headers)
338
+ req.headers["Content-Type"] = "application/json"
339
+ req.body = request.to_json
340
+ end
341
+
342
+ handle_http_response(response)
343
+ end
344
+
345
+ ##
346
+ # Send a streaming request using Server-Sent Events
347
+ #
348
+ # @param request [Hash] The JSON-RPC request
349
+ # @return [Enumerator] Stream of events
350
+ def send_streaming_request(request)
351
+ Enumerator.new do |yielder|
352
+ response = @connection.post do |req|
353
+ req.headers.merge!(@config.all_headers)
354
+ req.headers["Content-Type"] = "application/json"
355
+ req.headers["Accept"] = "text/event-stream"
356
+ req.body = request.to_json
357
+
358
+ # Handle streaming response
359
+ req.options.on_data = proc do |chunk, _size|
360
+ events = parse_sse_chunk(chunk)
361
+ events.each do |event|
362
+ case event[:event]
363
+ when "message"
364
+ yielder << ensure_message(JSON.parse(event[:data]))
365
+ when "task_status_update"
366
+ event_data = A2A::Types::TaskStatusUpdateEvent.from_h(JSON.parse(event[:data]))
367
+ process_event(event_data)
368
+ yielder << event_data
369
+ when "task_artifact_update"
370
+ event_data = A2A::Types::TaskArtifactUpdateEvent.from_h(JSON.parse(event[:data]))
371
+ process_event(event_data)
372
+ yielder << event_data
373
+ when "error"
374
+ error_data = JSON.parse(event[:data])
375
+ error = A2A::Errors::ErrorUtils.from_json_rpc_code(
376
+ error_data["code"],
377
+ error_data["message"],
378
+ data: error_data["data"]
379
+ )
380
+ raise error
381
+ end
382
+ end
383
+ end
384
+ end
385
+
386
+ unless response.success?
387
+ raise A2A::Errors::HTTPError.new(
388
+ "Streaming request failed: #{response.status}",
389
+ status_code: response.status,
390
+ response_body: response.body
391
+ )
392
+ end
393
+ end
394
+ end
395
+
396
+ ##
397
+ # Handle HTTP response and extract JSON-RPC result
398
+ #
399
+ # @param response [Faraday::Response] The HTTP response
400
+ # @return [Hash] The JSON-RPC response
401
+ def handle_http_response(response)
402
+ unless response.success?
403
+ raise A2A::Errors::HTTPError.new(
404
+ "HTTP request failed: #{response.status}",
405
+ status_code: response.status,
406
+ response_body: response.body
407
+ )
408
+ end
409
+
410
+ begin
411
+ json_response = response.body.is_a?(Hash) ? response.body : JSON.parse(response.body)
412
+ rescue JSON::ParserError => e
413
+ raise A2A::Errors::JSONError, "Invalid JSON response: #{e.message}"
414
+ end
415
+
416
+ # Check for JSON-RPC error
417
+ if json_response["error"]
418
+ error = json_response["error"]
419
+ raise A2A::Errors::ErrorUtils.from_json_rpc_code(
420
+ error["code"],
421
+ error["message"],
422
+ data: error["data"]
423
+ )
424
+ end
425
+
426
+ json_response
427
+ end
428
+
429
+ ##
430
+ # Parse Server-Sent Events chunk
431
+ #
432
+ # @param chunk [String] The SSE chunk
433
+ # @return [Array<Hash>] Parsed events
434
+ def parse_sse_chunk(chunk)
435
+ events = []
436
+ current_event = {}
437
+
438
+ chunk.split("\n").each do |line|
439
+ line = line.strip
440
+ next if line.empty?
441
+
442
+ if line.start_with?("data: ")
443
+ current_event[:data] = line[6..]
444
+ elsif line.start_with?("event: ")
445
+ current_event[:event] = line[7..]
446
+ elsif line.start_with?("id: ")
447
+ current_event[:id] = line[4..]
448
+ elsif line.start_with?("retry: ")
449
+ current_event[:retry] = line[7..].to_i
450
+ elsif line == ""
451
+ # Empty line indicates end of event
452
+ events << current_event.dup if current_event[:data]
453
+ current_event.clear
454
+ end
455
+ end
456
+
457
+ # Handle case where chunk doesn't end with empty line
458
+ events << current_event if current_event[:data]
459
+ events
460
+ end
461
+
462
+ ##
463
+ # Build a JSON-RPC request
464
+ #
465
+ # @param method [String] The method name
466
+ # @param params [Hash] The method parameters
467
+ # @return [A2A::Protocol::Request] The JSON-RPC request
468
+ def build_json_rpc_request(method, params = {})
469
+ A2A::Protocol::Request.new(
470
+ jsonrpc: A2A::Protocol::JsonRpc::JSONRPC_VERSION,
471
+ method: method,
472
+ params: params,
473
+ id: next_request_id
474
+ )
475
+ end
476
+
477
+ ##
478
+ # Generate next request ID
479
+ #
480
+ # @return [Integer] The next request ID
481
+ def next_request_id
482
+ @request_id_mutex.synchronize do
483
+ @request_id_counter += 1
484
+ end
485
+ end
486
+
487
+ ##
488
+ # Get performance statistics
489
+ #
490
+ # @return [Hash] Performance statistics
491
+ def performance_stats
492
+ @stats_mutex.synchronize { @performance_stats.dup }
493
+ end
494
+
495
+ ##
496
+ # Reset performance statistics
497
+ #
498
+ def reset_performance_stats!
499
+ @stats_mutex.synchronize do
500
+ @performance_stats = {
501
+ requests_count: 0,
502
+ total_time: 0.0,
503
+ avg_response_time: 0.0,
504
+ cache_hits: 0,
505
+ cache_misses: 0
506
+ }
507
+ end
508
+ end
509
+
510
+ ##
511
+ # Record request performance metrics
512
+ #
513
+ # @param duration [Float] Request duration in seconds
514
+ def record_request_performance(duration)
515
+ @stats_mutex.synchronize do
516
+ @performance_stats[:requests_count] += 1
517
+ @performance_stats[:total_time] += duration
518
+ @performance_stats[:avg_response_time] =
519
+ @performance_stats[:total_time] / @performance_stats[:requests_count]
520
+ end
521
+ end
522
+ end
523
+ end
524
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "securerandom"
5
+
6
+ module A2A
7
+ module Client
8
+ ##
9
+ # JSON-RPC request handling functionality
10
+ #
11
+ module JsonRpcHandler
12
+ ##
13
+ # Initialize JSON-RPC handling
14
+ #
15
+ def initialize_json_rpc_handling
16
+ @request_id_counter = 0
17
+ @request_id_mutex = Mutex.new
18
+ end
19
+
20
+ ##
21
+ # Send a JSON-RPC request
22
+ #
23
+ # @param request [Hash] The JSON-RPC request
24
+ # @return [Hash] The parsed response
25
+ def send_json_rpc_request(request)
26
+ start_time = Time.now
27
+ response = @connection.post("/", request.to_json, "Content-Type" => "application/json")
28
+ duration = Time.now - start_time
29
+
30
+ record_request_performance(duration)
31
+ handle_http_response(response)
32
+ end
33
+
34
+ ##
35
+ # Send a streaming JSON-RPC request
36
+ #
37
+ # @param request [Hash] The JSON-RPC request
38
+ # @return [Enumerator] Stream of parsed responses
39
+ def send_streaming_request(request)
40
+ Enumerator.new do |yielder|
41
+ response = @connection.post("/stream", request.to_json) do |req|
42
+ req.headers["Content-Type"] = "application/json"
43
+ req.headers["Accept"] = "text/event-stream"
44
+ end
45
+
46
+ raise A2A::Errors::HTTPError, "HTTP #{response.status}: #{response.body}" unless response.success?
47
+
48
+ response.body.each_line do |line|
49
+ event = parse_sse_chunk(line.strip)
50
+ yielder << event if event
51
+ end
52
+ end
53
+ end
54
+
55
+ ##
56
+ # Handle HTTP response
57
+ #
58
+ # @param response [Faraday::Response] The HTTP response
59
+ # @return [Hash] The parsed JSON-RPC response
60
+ def handle_http_response(response)
61
+ unless response.success?
62
+ case response.status
63
+ when 408
64
+ raise A2A::Errors::TimeoutError, "Request timeout"
65
+ when 400..499
66
+ raise A2A::Errors::HTTPError, "HTTP #{response.status}: #{response.body}"
67
+ when 500..599
68
+ raise A2A::Errors::HTTPError, "HTTP #{response.status}: #{response.body}"
69
+ else
70
+ raise A2A::Errors::HTTPError, "HTTP #{response.status}: #{response.body}"
71
+ end
72
+ end
73
+
74
+ begin
75
+ parsed_response = JSON.parse(response.body)
76
+ rescue JSON::ParserError => e
77
+ raise A2A::Errors::ParseError, "Invalid JSON response: #{e.message}"
78
+ end
79
+
80
+ if parsed_response["error"]
81
+ error_code = parsed_response["error"]["code"]
82
+ error_message = parsed_response["error"]["message"]
83
+ raise A2A::Errors::A2AError.new(error_message, error_code)
84
+ end
85
+
86
+ parsed_response
87
+ end
88
+
89
+ ##
90
+ # Parse Server-Sent Events chunk
91
+ #
92
+ # @param chunk [String] The SSE chunk
93
+ # @return [Hash, nil] The parsed event or nil
94
+ def parse_sse_chunk(chunk)
95
+ return nil if chunk.empty? || chunk.start_with?(":")
96
+
97
+ return unless chunk.start_with?("data: ")
98
+
99
+ data = chunk[6..] # Remove "data: " prefix
100
+ return nil if data == "[DONE]"
101
+
102
+ begin
103
+ JSON.parse(data)
104
+ rescue JSON::ParserError
105
+ nil
106
+ end
107
+ end
108
+
109
+ ##
110
+ # Build a JSON-RPC request
111
+ #
112
+ # @param method [String] The method name
113
+ # @param params [Hash] The parameters
114
+ # @return [Hash] The JSON-RPC request
115
+ def build_json_rpc_request(method, params = {})
116
+ {
117
+ jsonrpc: "2.0",
118
+ method: method,
119
+ params: params,
120
+ id: next_request_id
121
+ }
122
+ end
123
+
124
+ ##
125
+ # Generate the next request ID
126
+ #
127
+ # @return [String] The request ID
128
+ def next_request_id
129
+ @request_id_mutex.synchronize do
130
+ @request_id_counter += 1
131
+ "#{SecureRandom.hex(8)}-#{@request_id_counter}"
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end