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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +137 -0
- data/.simplecov +46 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +33 -0
- data/CODE_OF_CONDUCT.md +128 -0
- data/CONTRIBUTING.md +165 -0
- data/Gemfile +43 -0
- data/Guardfile +34 -0
- data/LICENSE.txt +21 -0
- data/PUBLISHING_CHECKLIST.md +214 -0
- data/README.md +171 -0
- data/Rakefile +165 -0
- data/docs/agent_execution.md +309 -0
- data/docs/api_reference.md +792 -0
- data/docs/configuration.md +780 -0
- data/docs/events.md +475 -0
- data/docs/getting_started.md +668 -0
- data/docs/integration.md +262 -0
- data/docs/server_apps.md +621 -0
- data/docs/troubleshooting.md +765 -0
- data/lib/a2a/client/api_methods.rb +263 -0
- data/lib/a2a/client/auth/api_key.rb +161 -0
- data/lib/a2a/client/auth/interceptor.rb +288 -0
- data/lib/a2a/client/auth/jwt.rb +189 -0
- data/lib/a2a/client/auth/oauth2.rb +146 -0
- data/lib/a2a/client/auth.rb +137 -0
- data/lib/a2a/client/base.rb +316 -0
- data/lib/a2a/client/config.rb +210 -0
- data/lib/a2a/client/connection_pool.rb +233 -0
- data/lib/a2a/client/http_client.rb +524 -0
- data/lib/a2a/client/json_rpc_handler.rb +136 -0
- data/lib/a2a/client/middleware/circuit_breaker_interceptor.rb +245 -0
- data/lib/a2a/client/middleware/logging_interceptor.rb +371 -0
- data/lib/a2a/client/middleware/rate_limit_interceptor.rb +142 -0
- data/lib/a2a/client/middleware/retry_interceptor.rb +161 -0
- data/lib/a2a/client/middleware.rb +116 -0
- data/lib/a2a/client/performance_tracker.rb +60 -0
- data/lib/a2a/configuration/defaults.rb +34 -0
- data/lib/a2a/configuration/environment_loader.rb +76 -0
- data/lib/a2a/configuration/file_loader.rb +115 -0
- data/lib/a2a/configuration/inheritance.rb +101 -0
- data/lib/a2a/configuration/validator.rb +180 -0
- data/lib/a2a/configuration.rb +201 -0
- data/lib/a2a/errors.rb +291 -0
- data/lib/a2a/modules.rb +50 -0
- data/lib/a2a/monitoring/alerting.rb +490 -0
- data/lib/a2a/monitoring/distributed_tracing.rb +398 -0
- data/lib/a2a/monitoring/health_endpoints.rb +204 -0
- data/lib/a2a/monitoring/metrics_collector.rb +438 -0
- data/lib/a2a/monitoring.rb +463 -0
- data/lib/a2a/plugin.rb +358 -0
- data/lib/a2a/plugin_manager.rb +159 -0
- data/lib/a2a/plugins/example_auth.rb +81 -0
- data/lib/a2a/plugins/example_middleware.rb +118 -0
- data/lib/a2a/plugins/example_transport.rb +76 -0
- data/lib/a2a/protocol/agent_card.rb +8 -0
- data/lib/a2a/protocol/agent_card_server.rb +584 -0
- data/lib/a2a/protocol/capability.rb +496 -0
- data/lib/a2a/protocol/json_rpc.rb +254 -0
- data/lib/a2a/protocol/message.rb +8 -0
- data/lib/a2a/protocol/task.rb +8 -0
- data/lib/a2a/rails/a2a_controller.rb +258 -0
- data/lib/a2a/rails/controller_helpers.rb +499 -0
- data/lib/a2a/rails/engine.rb +167 -0
- data/lib/a2a/rails/generators/agent_generator.rb +311 -0
- data/lib/a2a/rails/generators/install_generator.rb +209 -0
- data/lib/a2a/rails/generators/migration_generator.rb +232 -0
- data/lib/a2a/rails/generators/templates/add_a2a_indexes.rb +57 -0
- data/lib/a2a/rails/generators/templates/agent_controller.rb +122 -0
- data/lib/a2a/rails/generators/templates/agent_controller_spec.rb +160 -0
- data/lib/a2a/rails/generators/templates/agent_readme.md +200 -0
- data/lib/a2a/rails/generators/templates/create_a2a_push_notification_configs.rb +68 -0
- data/lib/a2a/rails/generators/templates/create_a2a_tasks.rb +83 -0
- data/lib/a2a/rails/generators/templates/example_agent_controller.rb +228 -0
- data/lib/a2a/rails/generators/templates/initializer.rb +108 -0
- data/lib/a2a/rails/generators/templates/push_notification_config_model.rb +228 -0
- data/lib/a2a/rails/generators/templates/task_model.rb +200 -0
- data/lib/a2a/rails/tasks/a2a.rake +228 -0
- data/lib/a2a/server/a2a_methods.rb +520 -0
- data/lib/a2a/server/agent.rb +537 -0
- data/lib/a2a/server/agent_execution/agent_executor.rb +279 -0
- data/lib/a2a/server/agent_execution/request_context.rb +219 -0
- data/lib/a2a/server/apps/rack_app.rb +311 -0
- data/lib/a2a/server/apps/sinatra_app.rb +261 -0
- data/lib/a2a/server/default_request_handler.rb +350 -0
- data/lib/a2a/server/events/event_consumer.rb +116 -0
- data/lib/a2a/server/events/event_queue.rb +226 -0
- data/lib/a2a/server/example_agent.rb +248 -0
- data/lib/a2a/server/handler.rb +281 -0
- data/lib/a2a/server/middleware/authentication_middleware.rb +212 -0
- data/lib/a2a/server/middleware/cors_middleware.rb +171 -0
- data/lib/a2a/server/middleware/logging_middleware.rb +362 -0
- data/lib/a2a/server/middleware/rate_limit_middleware.rb +382 -0
- data/lib/a2a/server/middleware.rb +213 -0
- data/lib/a2a/server/push_notification_manager.rb +327 -0
- data/lib/a2a/server/request_handler.rb +136 -0
- data/lib/a2a/server/storage/base.rb +141 -0
- data/lib/a2a/server/storage/database.rb +266 -0
- data/lib/a2a/server/storage/memory.rb +274 -0
- data/lib/a2a/server/storage/redis.rb +320 -0
- data/lib/a2a/server/storage.rb +38 -0
- data/lib/a2a/server/task_manager.rb +534 -0
- data/lib/a2a/transport/grpc.rb +481 -0
- data/lib/a2a/transport/http.rb +415 -0
- data/lib/a2a/transport/sse.rb +499 -0
- data/lib/a2a/types/agent_card.rb +540 -0
- data/lib/a2a/types/artifact.rb +99 -0
- data/lib/a2a/types/base_model.rb +223 -0
- data/lib/a2a/types/events.rb +117 -0
- data/lib/a2a/types/message.rb +106 -0
- data/lib/a2a/types/part.rb +288 -0
- data/lib/a2a/types/push_notification.rb +139 -0
- data/lib/a2a/types/security.rb +167 -0
- data/lib/a2a/types/task.rb +154 -0
- data/lib/a2a/types.rb +88 -0
- data/lib/a2a/utils/helpers.rb +245 -0
- data/lib/a2a/utils/message_buffer.rb +278 -0
- data/lib/a2a/utils/performance.rb +247 -0
- data/lib/a2a/utils/rails_detection.rb +97 -0
- data/lib/a2a/utils/structured_logger.rb +306 -0
- data/lib/a2a/utils/time_helpers.rb +167 -0
- data/lib/a2a/utils/validation.rb +8 -0
- data/lib/a2a/version.rb +6 -0
- data/lib/a2a-rails.rb +58 -0
- data/lib/a2a.rb +198 -0
- 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
|