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,499 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "concurrent"
5
+
6
+ module A2A
7
+ module Transport
8
+ ##
9
+ # Server-Sent Events (SSE) transport implementation
10
+ # Provides streaming responses with event parsing, connection management, and heartbeat support
11
+ #
12
+ class SSE
13
+ # SSE event types
14
+ EVENT_TYPES = %w[
15
+ message
16
+ error
17
+ heartbeat
18
+ task_status_update
19
+ task_artifact_update
20
+ connection_established
21
+ connection_closed
22
+ ].freeze
23
+
24
+ # Default configuration values
25
+ DEFAULT_HEARTBEAT_INTERVAL = 30
26
+ DEFAULT_RECONNECT_DELAY = 3000
27
+ DEFAULT_MAX_RECONNECT_ATTEMPTS = 10
28
+ DEFAULT_BUFFER_SIZE = 1024 * 8 # 8KB
29
+ DEFAULT_TIMEOUT = 60
30
+
31
+ attr_reader :url, :config, :connection_state, :event_buffer, :last_event_id
32
+
33
+ ##
34
+ # Initialize SSE transport
35
+ #
36
+ # @param url [String] SSE endpoint URL
37
+ # @param config [Hash] Configuration options
38
+ # @option config [Integer] :heartbeat_interval (30) Heartbeat interval in seconds
39
+ # @option config [Integer] :reconnect_delay (3000) Reconnection delay in milliseconds
40
+ # @option config [Integer] :max_reconnect_attempts (10) Maximum reconnection attempts
41
+ # @option config [Integer] :buffer_size (8192) Event buffer size in bytes
42
+ # @option config [Integer] :timeout (60) Connection timeout in seconds
43
+ # @option config [Hash] :headers ({}) Default headers
44
+ # @option config [Boolean] :auto_reconnect (true) Enable automatic reconnection
45
+ # @option config [String] :last_event_id Last received event ID for replay
46
+ #
47
+ def initialize(url, config = {})
48
+ @url = url
49
+ @config = default_config.merge(config)
50
+ @connection_state = :disconnected
51
+ @event_buffer = Concurrent::Array.new
52
+ @last_event_id = @config[:last_event_id]
53
+ @reconnect_attempts = 0
54
+ @heartbeat_timer = nil
55
+ @event_listeners = Concurrent::Hash.new { |h, k| h[k] = [] }
56
+ @mutex = Mutex.new
57
+ end
58
+
59
+ ##
60
+ # Connect to SSE endpoint and start streaming
61
+ #
62
+ # @param headers [Hash] Additional headers
63
+ # @yield [event] Block to handle incoming events
64
+ # @yieldparam event [SSEEvent] Received SSE event
65
+ # @return [Enumerator] Event stream enumerator
66
+ #
67
+ def connect(headers: {}, &block)
68
+ @mutex.synchronize do
69
+ return if @connection_state == :connected
70
+
71
+ @connection_state = :connecting
72
+ @reconnect_attempts = 0
73
+ end
74
+
75
+ if block_given?
76
+ connect_with_callback(headers, &block)
77
+ else
78
+ connect_with_enumerator(headers)
79
+ end
80
+ end
81
+
82
+ ##
83
+ # Disconnect from SSE endpoint
84
+ #
85
+ def disconnect
86
+ @mutex.synchronize do
87
+ @connection_state = :disconnected
88
+ stop_heartbeat
89
+ @event_buffer.clear
90
+ end
91
+
92
+ emit_event(SSEEvent.new(type: "connection_closed", data: { reason: "manual_disconnect" }))
93
+ end
94
+
95
+ ##
96
+ # Add event listener for specific event type
97
+ #
98
+ # @param event_type [String] Event type to listen for
99
+ # @param &block [Proc] Event handler block
100
+ #
101
+ def on(event_type, &block)
102
+ @event_listeners[event_type.to_s] << block if block_given?
103
+ end
104
+
105
+ ##
106
+ # Remove event listener
107
+ #
108
+ # @param event_type [String] Event type
109
+ # @param handler [Proc] Handler to remove (optional, removes all if nil)
110
+ #
111
+ def off(event_type, handler = nil)
112
+ if handler
113
+ @event_listeners[event_type.to_s].delete(handler)
114
+ else
115
+ @event_listeners[event_type.to_s].clear
116
+ end
117
+ end
118
+
119
+ ##
120
+ # Send data to SSE endpoint (for bidirectional communication)
121
+ #
122
+ # @param data [Hash] Data to send
123
+ # @param event_type [String] Event type
124
+ # @return [Boolean] Success status
125
+ #
126
+ def send_data(data, event_type: "message")
127
+ return false unless @connection_state == :connected
128
+
129
+ # This would typically use a separate HTTP connection for sending
130
+ # as SSE is primarily unidirectional from server to client
131
+ begin
132
+ http_transport = A2A::Transport::Http.new(@url.gsub("/events", ""))
133
+ response = http_transport.post(
134
+ "/events/send",
135
+ body: {
136
+ type: event_type,
137
+ data: data,
138
+ last_event_id: @last_event_id
139
+ }
140
+ )
141
+ response.status == 200
142
+ rescue StandardError => e
143
+ emit_event(SSEEvent.new(type: "error", data: { error: e.message }))
144
+ false
145
+ end
146
+ end
147
+
148
+ ##
149
+ # Get connection status
150
+ #
151
+ # @return [Symbol] Connection state (:disconnected, :connecting, :connected, :reconnecting)
152
+ #
153
+ def connected?
154
+ @connection_state == :connected
155
+ end
156
+
157
+ ##
158
+ # Get buffered events
159
+ #
160
+ # @return [Array<SSEEvent>] Buffered events
161
+ #
162
+ def buffered_events
163
+ @event_buffer.to_a
164
+ end
165
+
166
+ ##
167
+ # Clear event buffer
168
+ #
169
+ def clear_buffer!
170
+ @event_buffer.clear
171
+ end
172
+
173
+ private
174
+
175
+ ##
176
+ # Connect with callback-based handling
177
+ #
178
+ # @param headers [Hash] Request headers
179
+ # @yield [event] Event handler block
180
+ #
181
+ def connect_with_callback(headers)
182
+ Thread.new do
183
+ establish_connection(headers) do |event|
184
+ yield(event) if block_given?
185
+ end
186
+ rescue StandardError => e
187
+ handle_connection_error(e)
188
+ end
189
+ end
190
+
191
+ ##
192
+ # Connect with enumerator-based handling
193
+ #
194
+ # @param headers [Hash] Request headers
195
+ # @return [Enumerator] Event stream enumerator
196
+ #
197
+ def connect_with_enumerator(headers)
198
+ Enumerator.new do |yielder|
199
+ establish_connection(headers) do |event|
200
+ yielder << event
201
+ end
202
+ end
203
+ end
204
+
205
+ ##
206
+ # Establish SSE connection
207
+ #
208
+ # @param headers [Hash] Request headers
209
+ # @yield [event] Event handler block
210
+ #
211
+ def establish_connection(headers, &block)
212
+ request_headers = build_headers(headers)
213
+
214
+ # Use HTTP transport for the underlying connection
215
+ http = A2A::Transport::Http.new(@url, timeout: @config[:timeout])
216
+
217
+ http.get(headers: request_headers) do |req|
218
+ req.options.on_data = proc do |chunk, _size|
219
+ process_chunk(chunk, &block)
220
+ end
221
+ end
222
+
223
+ @connection_state = :connected
224
+ start_heartbeat
225
+ emit_event(SSEEvent.new(type: "connection_established", data: { url: @url }))
226
+ rescue StandardError => e
227
+ handle_connection_error(e)
228
+ end
229
+
230
+ ##
231
+ # Process incoming data chunk
232
+ #
233
+ # @param chunk [String] Data chunk
234
+ # @yield [event] Event handler block
235
+ #
236
+ def process_chunk(chunk, &block)
237
+ lines = chunk.split("\n")
238
+
239
+ lines.each do |line|
240
+ event = parse_sse_line(line.strip)
241
+ next unless event
242
+
243
+ @last_event_id = event.id if event.id
244
+ buffer_event(event)
245
+ emit_event(event, &block)
246
+ end
247
+ end
248
+
249
+ ##
250
+ # Parse SSE line into event
251
+ #
252
+ # @param line [String] SSE line
253
+ # @return [SSEEvent, nil] Parsed event or nil
254
+ #
255
+ def parse_sse_line(line)
256
+ return nil if line.empty? || line.start_with?(":")
257
+
258
+ if line.start_with?("data: ")
259
+ data_content = line[6..]
260
+ begin
261
+ data = JSON.parse(data_content)
262
+ SSEEvent.new(
263
+ type: data["type"] || "message",
264
+ data: data["data"] || data,
265
+ id: data["id"],
266
+ retry_interval: data["retry"]
267
+ )
268
+ rescue JSON::ParserError
269
+ SSEEvent.new(type: "message", data: data_content)
270
+ end
271
+ elsif line.start_with?("event: ")
272
+ # Store event type for next data line (simplified parsing)
273
+ nil
274
+ elsif line.start_with?("id: ")
275
+ # Store event ID for next data line (simplified parsing)
276
+ nil
277
+ elsif line.start_with?("retry: ")
278
+ # Update reconnection delay
279
+ @config[:reconnect_delay] = line[7..].to_i
280
+ nil
281
+ else
282
+ nil
283
+ end
284
+ end
285
+
286
+ ##
287
+ # Buffer event for replay
288
+ #
289
+ # @param event [SSEEvent] Event to buffer
290
+ #
291
+ def buffer_event(event)
292
+ @event_buffer << event
293
+
294
+ # Limit buffer size
295
+ @event_buffer.shift while @event_buffer.size > @config[:buffer_size]
296
+ end
297
+
298
+ ##
299
+ # Emit event to listeners
300
+ #
301
+ # @param event [SSEEvent] Event to emit
302
+ # @yield [event] Event handler block
303
+ #
304
+ def emit_event(event)
305
+ # Call specific event listeners
306
+ @event_listeners[event.type].each do |listener|
307
+ listener.call(event)
308
+ rescue StandardError => e
309
+ # Log error but don't stop processing
310
+ logger = if defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
311
+ ::Rails.logger
312
+ else
313
+ require "logger"
314
+ Logger.new($stdout)
315
+ end
316
+ logger.debug { "Error in event listener: #{e.message}" }
317
+ end
318
+
319
+ # Call generic block handler
320
+ yield(event) if block_given?
321
+ end
322
+
323
+ ##
324
+ # Handle connection errors
325
+ #
326
+ # @param error [Exception] Connection error
327
+ #
328
+ def handle_connection_error(error)
329
+ @connection_state = :disconnected
330
+ stop_heartbeat
331
+
332
+ error_event = SSEEvent.new(
333
+ type: "error",
334
+ data: {
335
+ error: error.message,
336
+ reconnect_attempts: @reconnect_attempts
337
+ }
338
+ )
339
+ emit_event(error_event)
340
+
341
+ # Attempt reconnection if enabled
342
+ return unless @config[:auto_reconnect] && @reconnect_attempts < @config[:max_reconnect_attempts]
343
+
344
+ schedule_reconnection
345
+ end
346
+
347
+ ##
348
+ # Schedule reconnection attempt
349
+ #
350
+ def schedule_reconnection
351
+ @reconnect_attempts += 1
352
+ @connection_state = :reconnecting
353
+
354
+ Thread.new do
355
+ sleep(@config[:reconnect_delay] / 1000.0)
356
+ connect if @connection_state == :reconnecting
357
+ end
358
+ end
359
+
360
+ ##
361
+ # Start heartbeat timer
362
+ #
363
+ def start_heartbeat
364
+ return unless @config[:heartbeat_interval].positive?
365
+
366
+ @heartbeat_timer = Thread.new do
367
+ loop do
368
+ sleep(@config[:heartbeat_interval])
369
+ break unless @connection_state == :connected
370
+
371
+ emit_event(SSEEvent.new(
372
+ type: "heartbeat",
373
+ data: { timestamp: Time.now.iso8601 }
374
+ ))
375
+ end
376
+ end
377
+ end
378
+
379
+ ##
380
+ # Stop heartbeat timer
381
+ #
382
+ def stop_heartbeat
383
+ @heartbeat_timer&.kill
384
+ @heartbeat_timer = nil
385
+ end
386
+
387
+ ##
388
+ # Build request headers
389
+ #
390
+ # @param additional_headers [Hash] Additional headers
391
+ # @return [Hash] Complete headers
392
+ #
393
+ def build_headers(additional_headers = {})
394
+ headers = {
395
+ "Accept" => "text/event-stream",
396
+ "Cache-Control" => "no-cache"
397
+ }
398
+
399
+ headers["Last-Event-ID"] = @last_event_id if @last_event_id
400
+ headers.merge(@config[:headers]).merge(additional_headers)
401
+ end
402
+
403
+ ##
404
+ # Build default configuration
405
+ #
406
+ # @return [Hash] Default configuration
407
+ #
408
+ def default_config
409
+ {
410
+ heartbeat_interval: DEFAULT_HEARTBEAT_INTERVAL,
411
+ reconnect_delay: DEFAULT_RECONNECT_DELAY,
412
+ max_reconnect_attempts: DEFAULT_MAX_RECONNECT_ATTEMPTS,
413
+ buffer_size: DEFAULT_BUFFER_SIZE,
414
+ timeout: DEFAULT_TIMEOUT,
415
+ headers: {},
416
+ auto_reconnect: true,
417
+ last_event_id: nil
418
+ }
419
+ end
420
+ end
421
+
422
+ ##
423
+ # SSE Event representation
424
+ #
425
+ class SSEEvent
426
+ attr_reader :type, :data, :id, :retry, :timestamp
427
+
428
+ ##
429
+ # Initialize SSE event
430
+ #
431
+ # @param type [String] Event type
432
+ # @param data [Object] Event data
433
+ # @param id [String, nil] Event ID
434
+ # @param retry_interval [Integer, nil] Retry interval
435
+ #
436
+ def initialize(type:, data: nil, id: nil, retry_interval: nil)
437
+ @type = type.to_s
438
+ @data = data
439
+ @id = id
440
+ @retry = retry_interval
441
+ @timestamp = Time.now
442
+ end
443
+
444
+ ##
445
+ # Convert event to SSE format
446
+ #
447
+ # @return [String] SSE formatted string
448
+ #
449
+ def to_sse_format
450
+ lines = []
451
+ lines << "event: #{@type}" if @type != "message"
452
+ lines << "id: #{@id}" if @id
453
+ lines << "retry: #{@retry}" if @retry
454
+
455
+ data_json = @data.is_a?(String) ? @data : @data.to_json
456
+ data_json.split("\n").each do |line|
457
+ lines << "data: #{line}"
458
+ end
459
+
460
+ lines << ""
461
+ lines.join("\n")
462
+ end
463
+
464
+ ##
465
+ # Convert to hash representation
466
+ #
467
+ # @return [Hash] Event as hash
468
+ #
469
+ def to_h
470
+ {
471
+ type: @type,
472
+ data: @data,
473
+ id: @id,
474
+ retry: @retry,
475
+ timestamp: @timestamp.iso8601
476
+ }.compact
477
+ end
478
+
479
+ ##
480
+ # Check if event is of specific type
481
+ #
482
+ # @param event_type [String] Event type to check
483
+ # @return [Boolean] True if event matches type
484
+ #
485
+ def type?(event_type)
486
+ @type == event_type.to_s
487
+ end
488
+
489
+ ##
490
+ # Check if event has data
491
+ #
492
+ # @return [Boolean] True if event has data
493
+ #
494
+ def has_data?
495
+ !@data.nil?
496
+ end
497
+ end
498
+ end
499
+ end