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,350 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "request_handler"
4
+ require_relative "agent_execution/agent_executor"
5
+ require_relative "agent_execution/request_context"
6
+ require_relative "events/event_queue"
7
+ require_relative "events/event_consumer"
8
+ require_relative "task_manager"
9
+ require_relative "push_notification_manager"
10
+
11
+ module A2A
12
+ module Server
13
+ ##
14
+ # Default implementation of the RequestHandler interface
15
+ #
16
+ # This class provides a complete implementation of the A2A request handler
17
+ # that uses an AgentExecutor for processing requests and manages tasks
18
+ # through a TaskManager. It mirrors the Python DefaultRequestHandler.
19
+ #
20
+ class DefaultRequestHandler < RequestHandler
21
+ attr_reader :agent_executor, :task_manager, :push_notification_manager, :task_store
22
+
23
+ ##
24
+ # Initialize the default request handler
25
+ #
26
+ # @param agent_executor [AgentExecution::AgentExecutor] The agent executor for processing requests
27
+ # @param task_store [Object, nil] Optional task store for persistence
28
+ # @param push_notification_manager [PushNotificationManager, nil] Optional push notification manager
29
+ def initialize(agent_executor, task_store: nil, push_notification_manager: nil)
30
+ @agent_executor = agent_executor
31
+ @task_store = task_store
32
+ @task_manager = TaskManager.new(
33
+ storage: task_store,
34
+ push_notification_manager: push_notification_manager
35
+ )
36
+ @push_notification_manager = push_notification_manager || PushNotificationManager.new
37
+ end
38
+
39
+ ##
40
+ # Handle the 'tasks/get' method
41
+ #
42
+ # @param params [Hash] Parameters with task ID and optional history length
43
+ # @param context [A2A::Server::Context, nil] Server context
44
+ # @return [A2A::Types::Task, nil] The task if found
45
+ def on_get_task(params, _context = nil)
46
+ task_id = params["id"] || params[:id]
47
+ history_length = params["historyLength"] || params[:history_length]
48
+
49
+ raise A2A::Errors::InvalidParams, "Task ID is required" unless task_id
50
+
51
+ @task_manager.get_task(task_id, history_length: history_length)
52
+ rescue A2A::Errors::TaskNotFound
53
+ nil
54
+ end
55
+
56
+ ##
57
+ # Handle the 'tasks/cancel' method
58
+ #
59
+ # @param params [Hash] Parameters with task ID
60
+ # @param context [A2A::Server::Context, nil] Server context
61
+ # @return [A2A::Types::Task, nil] The canceled task if found
62
+ def on_cancel_task(params, context = nil)
63
+ task_id = params["id"] || params[:id]
64
+ params["reason"] || params[:reason]
65
+
66
+ raise A2A::Errors::InvalidParams, "Task ID is required" unless task_id
67
+
68
+ # Create request context for cancellation
69
+ request_context = AgentExecution::RequestContextBuilder.from_task_operation(
70
+ params, context, operation: "cancel"
71
+ )
72
+
73
+ # Create event queue for the cancellation
74
+ event_queue = Events::InMemoryEventQueue.new
75
+
76
+ # Set up event processing
77
+ setup_event_processing(event_queue, task_id, request_context.context_id)
78
+
79
+ begin
80
+ # Execute cancellation through agent executor
81
+ @agent_executor.cancel(request_context, event_queue)
82
+
83
+ # Wait briefly for the cancellation to be processed
84
+ sleep 0.1
85
+
86
+ # Return the updated task
87
+ @task_manager.get_task(task_id)
88
+ rescue A2A::Errors::TaskNotFound
89
+ nil
90
+ ensure
91
+ event_queue.close
92
+ end
93
+ end
94
+
95
+ ##
96
+ # Handle the 'message/send' method (non-streaming)
97
+ #
98
+ # @param params [Hash] Parameters with message and configuration
99
+ # @param context [A2A::Server::Context, nil] Server context
100
+ # @return [A2A::Types::Task, A2A::Types::Message] The result task or message
101
+ def on_message_send(params, context = nil)
102
+ # Build request context
103
+ request_context = AgentExecution::RequestContextBuilder.from_message_send(params, context)
104
+
105
+ # Create event queue for execution
106
+ event_queue = Events::InMemoryEventQueue.new
107
+ result = nil
108
+
109
+ # Set up event processing to capture the final result
110
+ setup_event_processing(event_queue, request_context.task_id, request_context.context_id) do |event|
111
+ case event.type
112
+ when "task"
113
+ result = event.data if event.data.status.state == A2A::Types::TASK_STATE_COMPLETED
114
+ when "message"
115
+ result = event.data
116
+ end
117
+ end
118
+
119
+ begin
120
+ # Execute through agent executor
121
+ @agent_executor.execute(request_context, event_queue)
122
+
123
+ # Wait for completion (with timeout)
124
+ timeout = 30 # 30 seconds timeout
125
+ start_time = Time.now
126
+
127
+ sleep 0.1 while result.nil? && (Time.now - start_time) < timeout
128
+
129
+ raise A2A::Errors::Timeout, "Request timed out" unless result
130
+
131
+ result
132
+ ensure
133
+ event_queue.close
134
+ end
135
+ end
136
+
137
+ ##
138
+ # Handle the 'message/stream' method (streaming)
139
+ #
140
+ # @param params [Hash] Parameters with message and configuration
141
+ # @param context [A2A::Server::Context, nil] Server context
142
+ # @return [Enumerator] Enumerator yielding events
143
+ def on_message_send_stream(params, context = nil)
144
+ # Build request context
145
+ request_context = AgentExecution::RequestContextBuilder.from_streaming_message(params, context)
146
+
147
+ # Create event queue for execution
148
+ event_queue = Events::InMemoryEventQueue.new
149
+
150
+ Enumerator.new do |yielder|
151
+ # Set up event processing to yield events
152
+ setup_event_processing(event_queue, request_context.task_id, request_context.context_id) do |event|
153
+ yielder << event.data
154
+ end
155
+
156
+ begin
157
+ # Execute through agent executor
158
+ @agent_executor.execute(request_context, event_queue)
159
+
160
+ # Keep the stream alive until the task completes
161
+ loop do
162
+ sleep 0.1
163
+
164
+ # Check if task is complete
165
+ next unless request_context.task_id
166
+
167
+ begin
168
+ task = @task_manager.get_task(request_context.task_id)
169
+ break if task&.completed?
170
+ rescue A2A::Errors::TaskNotFound
171
+ break
172
+ end
173
+ end
174
+ ensure
175
+ event_queue.close
176
+ end
177
+ end
178
+ end
179
+
180
+ ##
181
+ # Handle the 'tasks/resubscribe' method
182
+ #
183
+ # @param params [Hash] Parameters with task ID
184
+ # @param context [A2A::Server::Context, nil] Server context
185
+ # @return [Enumerator] Enumerator yielding events
186
+ def on_resubscribe_to_task(params, _context = nil)
187
+ task_id = params["id"] || params[:id]
188
+ raise A2A::Errors::InvalidParams, "Task ID is required" unless task_id
189
+
190
+ # Verify task exists
191
+ task = @task_manager.get_task(task_id)
192
+ raise A2A::Errors::TaskNotFound, "Task #{task_id} not found" unless task
193
+
194
+ # Create event queue for resubscription
195
+ event_queue = Events::InMemoryEventQueue.new
196
+
197
+ Enumerator.new do |yielder|
198
+ # Send current task state immediately
199
+ yielder << task
200
+
201
+ # Set up event processing for future updates
202
+ setup_event_processing(event_queue, task_id, task.context_id) do |event|
203
+ yielder << event.data
204
+ end
205
+
206
+ begin
207
+ # Keep the stream alive
208
+ loop do
209
+ sleep 1
210
+
211
+ # Check if task is still active
212
+ begin
213
+ current_task = @task_manager.get_task(task_id)
214
+ break if current_task&.completed?
215
+ rescue A2A::Errors::TaskNotFound
216
+ break
217
+ end
218
+ end
219
+ ensure
220
+ event_queue.close
221
+ end
222
+ end
223
+ end
224
+
225
+ ##
226
+ # Handle push notification config operations
227
+ #
228
+ # @param params [Hash] Parameters with task ID and config
229
+ # @param context [A2A::Server::Context, nil] Server context
230
+ # @return [A2A::Types::TaskPushNotificationConfig] The config
231
+ def on_set_task_push_notification_config(params, _context = nil)
232
+ task_id = params["taskId"] || params[:task_id]
233
+ config_data = params["config"] || params[:config]
234
+
235
+ raise A2A::Errors::InvalidParams, "Task ID is required" unless task_id
236
+ raise A2A::Errors::InvalidParams, "Config is required" unless config_data
237
+
238
+ # Verify task exists
239
+ @task_manager.get_task(task_id)
240
+
241
+ @push_notification_manager.set_push_notification_config(task_id, config_data)
242
+ end
243
+
244
+ ##
245
+ # Get push notification config
246
+ #
247
+ # @param params [Hash] Parameters with task ID and optional config ID
248
+ # @param context [A2A::Server::Context, nil] Server context
249
+ # @return [A2A::Types::TaskPushNotificationConfig] The config
250
+ def on_get_task_push_notification_config(params, _context = nil)
251
+ task_id = params["taskId"] || params[:task_id]
252
+ config_id = params["configId"] || params[:config_id]
253
+
254
+ raise A2A::Errors::InvalidParams, "Task ID is required" unless task_id
255
+
256
+ config = @push_notification_manager.get_push_notification_config(task_id, config_id: config_id)
257
+ raise A2A::Errors::NotFound, "Push notification config not found" unless config
258
+
259
+ config
260
+ end
261
+
262
+ ##
263
+ # List push notification configs
264
+ #
265
+ # @param params [Hash] Parameters with task ID
266
+ # @param context [A2A::Server::Context, nil] Server context
267
+ # @return [Array<A2A::Types::TaskPushNotificationConfig>] The configs
268
+ def on_list_task_push_notification_config(params, _context = nil)
269
+ task_id = params["taskId"] || params[:task_id]
270
+ raise A2A::Errors::InvalidParams, "Task ID is required" unless task_id
271
+
272
+ @push_notification_manager.list_push_notification_configs(task_id)
273
+ end
274
+
275
+ ##
276
+ # Delete push notification config
277
+ #
278
+ # @param params [Hash] Parameters with task ID and config ID
279
+ # @param context [A2A::Server::Context, nil] Server context
280
+ def on_delete_task_push_notification_config(params, _context = nil)
281
+ task_id = params["taskId"] || params[:task_id]
282
+ config_id = params["configId"] || params[:config_id]
283
+
284
+ raise A2A::Errors::InvalidParams, "Task ID and config ID are required" unless task_id && config_id
285
+
286
+ deleted = @push_notification_manager.delete_push_notification_config(task_id, config_id)
287
+ raise A2A::Errors::NotFound, "Push notification config not found" unless deleted
288
+ end
289
+
290
+ private
291
+
292
+ ##
293
+ # Set up event processing for a task
294
+ #
295
+ # @param event_queue [Events::EventQueue] The event queue
296
+ # @param task_id [String, nil] The task ID to filter events for
297
+ # @param context_id [String, nil] The context ID to filter events for
298
+ # @yield [event] Block to process each event
299
+ def setup_event_processing(event_queue, task_id, context_id, &block)
300
+ # Create event consumer
301
+ consumer = Events::EventConsumer.new(event_queue)
302
+
303
+ # Register handlers for different event types
304
+ consumer.register_handler("task") do |event|
305
+ # Update task manager with task events
306
+ @task_manager.storage.save_task(event.data) if @task_manager.storage.respond_to?(:save_task)
307
+ block&.call(event)
308
+ end
309
+
310
+ consumer.register_handler("task_status_update") do |event|
311
+ # Update task status
312
+ @task_manager.update_task_status(
313
+ event.data.task_id,
314
+ event.data.status
315
+ )
316
+ block&.call(event)
317
+ end
318
+
319
+ consumer.register_handler("task_artifact_update") do |event|
320
+ # Add artifact to task
321
+ @task_manager.add_artifact(
322
+ event.data.task_id,
323
+ event.data.artifact,
324
+ append: event.data.append
325
+ )
326
+ block&.call(event)
327
+ end
328
+
329
+ consumer.register_handler("message") do |event|
330
+ # Add message to task history if we have a task
331
+ @task_manager.add_message(task_id, event.data) if task_id
332
+ block&.call(event)
333
+ end
334
+
335
+ # Start consuming events
336
+ filter = if task_id || context_id
337
+ lambda { |event|
338
+ (task_id.nil? || event.task_id == task_id) &&
339
+ (context_id.nil? || event.context_id == context_id)
340
+ }
341
+ else
342
+ nil
343
+ end
344
+
345
+ consumer.start(filter)
346
+ consumer
347
+ end
348
+ end
349
+ end
350
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "event_queue"
4
+
5
+ module A2A
6
+ module Server
7
+ module Events
8
+ ##
9
+ # Event consumer for processing events from an event queue
10
+ #
11
+ # Provides functionality to consume events from an EventQueue and process them
12
+ # with registered handlers. Supports filtering and error handling.
13
+ #
14
+ class EventConsumer
15
+ attr_reader :queue, :handlers, :running
16
+
17
+ ##
18
+ # Initialize a new event consumer
19
+ #
20
+ # @param queue [EventQueue] The event queue to consume from
21
+ def initialize(queue)
22
+ @queue = queue
23
+ @handlers = {}
24
+ @running = false
25
+ @thread = nil
26
+ @mutex = Mutex.new
27
+ end
28
+
29
+ ##
30
+ # Register a handler for a specific event type
31
+ #
32
+ # @param event_type [String] The event type to handle
33
+ # @param handler [Proc] The handler proc that receives the event
34
+ def register_handler(event_type, &handler)
35
+ @mutex.synchronize do
36
+ @handlers[event_type] ||= []
37
+ @handlers[event_type] << handler
38
+ end
39
+ end
40
+
41
+ ##
42
+ # Remove a handler for a specific event type
43
+ #
44
+ # @param event_type [String] The event type
45
+ # @param handler [Proc] The handler to remove
46
+ def remove_handler(event_type, handler)
47
+ @mutex.synchronize do
48
+ @handlers[event_type]&.delete(handler)
49
+ @handlers.delete(event_type) if @handlers[event_type] && @handlers[event_type].empty?
50
+ end
51
+ end
52
+
53
+ ##
54
+ # Start consuming events in a background thread
55
+ #
56
+ # @param filter [Proc, nil] Optional filter for events
57
+ def start(filter = nil)
58
+ return if @running
59
+
60
+ @running = true
61
+ @thread = Thread.new do
62
+ consume_events(filter)
63
+ end
64
+ end
65
+
66
+ ##
67
+ # Stop consuming events
68
+ def stop
69
+ @running = false
70
+ @thread&.join
71
+ @thread = nil
72
+ end
73
+
74
+ ##
75
+ # Process a single event synchronously
76
+ #
77
+ # @param event [Event] The event to process
78
+ def process_event(event)
79
+ handlers = @mutex.synchronize { @handlers[event.type]&.dup || [] }
80
+
81
+ handlers.each do |handler|
82
+ handler.call(event)
83
+ rescue StandardError => e
84
+ handle_error(event, e)
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ ##
91
+ # Consume events from the queue
92
+ #
93
+ # @param filter [Proc, nil] Optional event filter
94
+ def consume_events(filter)
95
+ @queue.subscribe(filter) do |event|
96
+ break unless @running
97
+
98
+ process_event(event)
99
+ end
100
+ rescue StandardError => e
101
+ handle_error(nil, e)
102
+ end
103
+
104
+ ##
105
+ # Handle errors during event processing
106
+ #
107
+ # @param event [Event, nil] The event being processed (if any)
108
+ # @param error [StandardError] The error that occurred
109
+ def handle_error(event, error)
110
+ warn "Error processing event #{event&.id}: #{error.message}"
111
+ warn error.backtrace.join("\n") if error.backtrace
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ module A2A
4
+ module Server
5
+ module Events
6
+ ##
7
+ # Event object for the event queue system
8
+ #
9
+ # Represents an event that can be published to and consumed from an event queue.
10
+ # Events can be Task objects, Message objects, or status/artifact update events.
11
+ #
12
+ class Event
13
+ attr_reader :type, :data, :timestamp, :id
14
+
15
+ ##
16
+ # Initialize a new event
17
+ #
18
+ # @param type [String] The event type (e.g., 'task', 'message', 'task_status_update')
19
+ # @param data [Object] The event data (Task, Message, or update event object)
20
+ # @param id [String, nil] Optional event ID (generated if not provided)
21
+ def initialize(type:, data:, id: nil)
22
+ @type = type
23
+ @data = data
24
+ @timestamp = Time.now.utc
25
+ @id = id || SecureRandom.uuid
26
+ end
27
+
28
+ ##
29
+ # Convert event to hash representation
30
+ #
31
+ # @return [Hash] Hash representation of the event
32
+ def to_h
33
+ {
34
+ id: @id,
35
+ type: @type,
36
+ data: @data.respond_to?(:to_h) ? @data.to_h : @data,
37
+ timestamp: @timestamp.iso8601
38
+ }
39
+ end
40
+
41
+ ##
42
+ # Check if this is a task-related event
43
+ #
44
+ # @return [Boolean] True if the event is task-related
45
+ def task_event?
46
+ %w[task task_status_update task_artifact_update].include?(@type)
47
+ end
48
+
49
+ ##
50
+ # Check if this is a message event
51
+ #
52
+ # @return [Boolean] True if the event is a message
53
+ def message_event?
54
+ @type == "message"
55
+ end
56
+
57
+ ##
58
+ # Get the task ID from the event data if available
59
+ #
60
+ # @return [String, nil] The task ID or nil if not available
61
+ def task_id
62
+ case @data
63
+ when A2A::Types::Task
64
+ @data.id
65
+ when A2A::Types::TaskStatusUpdateEvent, A2A::Types::TaskArtifactUpdateEvent
66
+ @data.task_id
67
+ else
68
+ nil
69
+ end
70
+ end
71
+
72
+ ##
73
+ # Get the context ID from the event data if available
74
+ #
75
+ # @return [String, nil] The context ID or nil if not available
76
+ def context_id
77
+ case @data
78
+ when A2A::Types::Task
79
+ @data.context_id
80
+ when A2A::Types::TaskStatusUpdateEvent, A2A::Types::TaskArtifactUpdateEvent
81
+ @data.context_id
82
+ when A2A::Types::Message
83
+ @data.context_id
84
+ else
85
+ nil
86
+ end
87
+ end
88
+ end
89
+
90
+ ##
91
+ # Abstract base class for event queues
92
+ #
93
+ # Defines the interface for event queue implementations that can be used
94
+ # to publish and consume events during agent execution.
95
+ #
96
+ class EventQueue
97
+ ##
98
+ # Publish an event to the queue
99
+ #
100
+ # @param event [Event] The event to publish
101
+ # @abstract Subclasses must implement this method
102
+ def publish(event)
103
+ raise NotImplementedError, "Subclasses must implement publish"
104
+ end
105
+
106
+ ##
107
+ # Subscribe to events from the queue
108
+ #
109
+ # @param filter [Proc, nil] Optional filter to apply to events
110
+ # @return [Enumerator] Enumerator yielding events
111
+ # @abstract Subclasses must implement this method
112
+ def subscribe(filter = nil)
113
+ raise NotImplementedError, "Subclasses must implement subscribe"
114
+ end
115
+
116
+ ##
117
+ # Close the event queue and clean up resources
118
+ #
119
+ # @abstract Subclasses must implement this method
120
+ def close
121
+ raise NotImplementedError, "Subclasses must implement close"
122
+ end
123
+
124
+ ##
125
+ # Check if the queue is closed
126
+ #
127
+ # @return [Boolean] True if the queue is closed
128
+ # @abstract Subclasses must implement this method
129
+ def closed?
130
+ raise NotImplementedError, "Subclasses must implement closed?"
131
+ end
132
+ end
133
+
134
+ ##
135
+ # In-memory event queue implementation
136
+ #
137
+ # A simple in-memory event queue that uses Ruby's Queue class for
138
+ # thread-safe event publishing and consumption.
139
+ #
140
+ class InMemoryEventQueue < EventQueue
141
+ def initialize
142
+ @queue = Queue.new
143
+ @subscribers = []
144
+ @closed = false
145
+ @mutex = Mutex.new
146
+ end
147
+
148
+ ##
149
+ # Publish an event to all subscribers
150
+ #
151
+ # @param event [Event] The event to publish
152
+ def publish(event)
153
+ return if @closed
154
+
155
+ @mutex.synchronize do
156
+ @subscribers.each do |subscriber|
157
+ subscriber[:queue].push(event) if subscriber[:filter].nil? || subscriber[:filter].call(event)
158
+ rescue StandardError => e
159
+ # Log error but don't fail the publish operation
160
+ warn "Error publishing to subscriber: #{e.message}"
161
+ end
162
+ end
163
+ end
164
+
165
+ ##
166
+ # Subscribe to events with optional filtering
167
+ #
168
+ # @param filter [Proc, nil] Optional filter proc that receives an event and returns boolean
169
+ # @return [Enumerator] Enumerator that yields events
170
+ def subscribe(filter = nil)
171
+ return enum_for(:subscribe, filter) unless block_given?
172
+
173
+ subscriber_queue = Queue.new
174
+ subscriber = { queue: subscriber_queue, filter: filter }
175
+
176
+ @mutex.synchronize do
177
+ @subscribers << subscriber
178
+ end
179
+
180
+ begin
181
+ loop do
182
+ break if @closed
183
+
184
+ begin
185
+ event = subscriber_queue.pop(true) # Non-blocking pop
186
+ yield event
187
+ rescue ThreadError
188
+ # Queue is empty, sleep briefly and try again
189
+ sleep 0.001 # Reduced sleep time for better responsiveness
190
+ end
191
+ end
192
+ ensure
193
+ @mutex.synchronize do
194
+ @subscribers.delete(subscriber)
195
+ end
196
+ end
197
+ end
198
+
199
+ ##
200
+ # Close the event queue
201
+ def close
202
+ @closed = true
203
+ @mutex.synchronize do
204
+ @subscribers.clear
205
+ end
206
+ end
207
+
208
+ ##
209
+ # Check if the queue is closed
210
+ #
211
+ # @return [Boolean] True if closed
212
+ def closed?
213
+ @closed
214
+ end
215
+
216
+ ##
217
+ # Get the number of active subscribers
218
+ #
219
+ # @return [Integer] Number of subscribers
220
+ def subscriber_count
221
+ @mutex.synchronize { @subscribers.length }
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end