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,534 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require_relative "storage"
5
+ require_relative "../utils/performance"
6
+
7
+ ##
8
+ # Manages task lifecycle, state transitions, and event processing
9
+ #
10
+ # The TaskManager is responsible for creating, updating, and managing tasks
11
+ # throughout their lifecycle. It handles state transitions, artifact management,
12
+ # message history, and event notifications.
13
+ #
14
+ module A2A
15
+ module Server
16
+ class TaskManager
17
+ attr_reader :storage, :event_handlers, :config
18
+
19
+ ##
20
+ # Initialize a new TaskManager with performance optimizations
21
+ #
22
+ # @param storage [Object] Storage backend for task persistence
23
+ # @param push_notification_manager [PushNotificationManager, nil] Push notification manager
24
+ # @param config [Hash] Configuration options
25
+ def initialize(storage: nil, push_notification_manager: nil, config: {})
26
+ @storage = storage || A2A::Server::Storage::Memory.new
27
+ @push_notification_manager = push_notification_manager
28
+ @event_handlers = []
29
+ @config = default_config.merge(config)
30
+ @task_cache = {} # LRU cache for frequently accessed tasks
31
+ @cache_mutex = Mutex.new
32
+ @performance_metrics = {
33
+ tasks_created: 0,
34
+ tasks_updated: 0,
35
+ cache_hits: 0,
36
+ cache_misses: 0,
37
+ avg_processing_time: 0.0
38
+ }
39
+ @metrics_mutex = Mutex.new
40
+ end
41
+
42
+ ##
43
+ # Create a new task with performance tracking
44
+ #
45
+ # @param type [String] Task type identifier
46
+ # @param params [Hash] Task parameters
47
+ # @param context_id [String, nil] Optional context ID (generated if not provided)
48
+ # @param metadata [Hash, nil] Optional task metadata
49
+ # @return [A2A::Types::Task] The created task
50
+ def create_task(type:, params: {}, context_id: nil, metadata: nil)
51
+ A2A::Utils::Performance.profile("task_creation") do
52
+ task_id = generate_task_id
53
+ context_id ||= generate_context_id
54
+
55
+ # Record task creation metric
56
+ @metrics_mutex.synchronize { @performance_metrics[:tasks_created] += 1 }
57
+
58
+ A2A::Monitoring.increment_counter("a2a_tasks_created", task_type: type) if defined?(A2A::Monitoring)
59
+ A2A::Monitoring.log(:info, "Creating task", task_id: task_id, task_type: type) if defined?(A2A::Monitoring)
60
+
61
+ task = A2A::Types::Task.new(
62
+ id: task_id,
63
+ context_id: context_id,
64
+ status: A2A::Types::TaskStatus.new(
65
+ state: A2A::Types::TASK_STATE_SUBMITTED,
66
+ message: "Task created",
67
+ updated_at: Time.now.utc.iso8601
68
+ ),
69
+ metadata: (metadata || {}).merge(
70
+ type: type,
71
+ params: params,
72
+ created_at: Time.now.utc.iso8601
73
+ )
74
+ )
75
+
76
+ @storage.save_task(task)
77
+
78
+ # Cache the newly created task
79
+ add_to_cache(task_id, task)
80
+
81
+ # Emit task creation event
82
+ emit_status_update_event(task)
83
+
84
+ task
85
+ end
86
+ end
87
+
88
+ ##
89
+ # Get a task by ID with caching for performance
90
+ #
91
+ # @param task_id [String] The task ID
92
+ # @param history_length [Integer, nil] Maximum number of history messages to include
93
+ # @return [A2A::Types::Task] The task
94
+ # @raise [A2A::Errors::TaskNotFound] If task doesn't exist
95
+ def get_task(task_id, history_length: nil)
96
+ start_time = Time.now
97
+
98
+ # Check cache first for frequently accessed tasks
99
+ cached_task = get_from_cache(task_id)
100
+ if cached_task && (!history_length || !cached_task.history || cached_task.history.length <= history_length)
101
+ record_cache_hit
102
+ return cached_task
103
+ end
104
+
105
+ record_cache_miss
106
+ task = @storage.get_task(task_id)
107
+ raise A2A::Errors::TaskNotFound, "Task #{task_id} not found" unless task
108
+
109
+ # Limit history if requested
110
+ if history_length && task.history && task.history.length > history_length
111
+ limited_history = task.history.last(history_length)
112
+ # Create a new task instance with limited history
113
+ task = A2A::Types::Task.new(
114
+ id: task.id,
115
+ context_id: task.context_id,
116
+ status: task.status,
117
+ artifacts: task.artifacts,
118
+ history: limited_history,
119
+ metadata: task.metadata
120
+ )
121
+ end
122
+
123
+ # Cache the task for future access
124
+ add_to_cache(task_id, task)
125
+
126
+ record_processing_time(Time.now - start_time)
127
+ task
128
+ end
129
+
130
+ ##
131
+ # Update task status with performance tracking
132
+ #
133
+ # @param task_id [String] The task ID
134
+ # @param status [A2A::Types::TaskStatus, Hash] New status
135
+ # @param message [String, nil] Optional status message
136
+ # @return [A2A::Types::Task] The updated task
137
+ # @raise [A2A::Errors::TaskNotFound] If task doesn't exist
138
+ def update_task_status(task_id, status, message: nil)
139
+ A2A::Utils::Performance.profile("task_status_update") do
140
+ task = get_task(task_id)
141
+
142
+ # Validate state transition
143
+ new_state = status.is_a?(A2A::Types::TaskStatus) ? status.state : status[:state] || status["state"]
144
+ validate_state_transition(task.status.state, new_state)
145
+
146
+ # Record task update metric
147
+ @metrics_mutex.synchronize { @performance_metrics[:tasks_updated] += 1 }
148
+
149
+ # Create new status
150
+ new_status = if status.is_a?(A2A::Types::TaskStatus)
151
+ status
152
+ else
153
+ status_hash = status.dup
154
+ status_hash[:message] = message if message
155
+ status_hash[:updated_at] = Time.now.utc.iso8601
156
+ A2A::Types::TaskStatus.new(**status_hash)
157
+ end
158
+
159
+ # Update task
160
+ task.update_status(new_status)
161
+ @storage.save_task(task)
162
+
163
+ # Update cache
164
+ add_to_cache(task_id, task)
165
+
166
+ # Emit status update event
167
+ emit_status_update_event(task)
168
+
169
+ task
170
+ end
171
+ end
172
+
173
+ ##
174
+ # Cancel a task
175
+ #
176
+ # @param task_id [String] The task ID
177
+ # @param reason [String, nil] Optional cancellation reason
178
+ # @return [A2A::Types::Task] The canceled task
179
+ # @raise [A2A::Errors::TaskNotFound] If task doesn't exist
180
+ # @raise [A2A::Errors::TaskNotCancelable] If task cannot be canceled
181
+ def cancel_task(task_id, reason: nil)
182
+ task = get_task(task_id)
183
+
184
+ unless task.cancelable?
185
+ raise A2A::Errors::TaskNotCancelable,
186
+ "Task #{task_id} in state '#{task.status.state}' cannot be canceled"
187
+ end
188
+
189
+ update_task_status(
190
+ task_id,
191
+ A2A::Types::TaskStatus.new(
192
+ state: A2A::Types::TASK_STATE_CANCELED,
193
+ message: reason || "Task canceled",
194
+ updated_at: Time.now.utc.iso8601
195
+ )
196
+ )
197
+ end
198
+
199
+ ##
200
+ # Add an artifact to a task
201
+ #
202
+ # @param task_id [String] The task ID
203
+ # @param artifact [A2A::Types::Artifact] The artifact to add
204
+ # @param append [Boolean] Whether to append to existing artifact with same ID
205
+ # @return [A2A::Types::Task] The updated task
206
+ # @raise [A2A::Errors::TaskNotFound] If task doesn't exist
207
+ def add_artifact(task_id, artifact, append: false)
208
+ task = get_task(task_id)
209
+
210
+ if append && task.artifacts
211
+ # Find existing artifact with same ID
212
+ existing_artifact = task.artifacts.find { |a| a.artifact_id == artifact.artifact_id }
213
+ if existing_artifact
214
+ # Append parts to existing artifact
215
+ artifact.parts.each { |part| existing_artifact.add_part(part) }
216
+ else
217
+ task.add_artifact(artifact)
218
+ end
219
+ else
220
+ task.add_artifact(artifact)
221
+ end
222
+
223
+ @storage.save_task(task)
224
+
225
+ # Emit artifact update event
226
+ emit_artifact_update_event(task, artifact, append)
227
+
228
+ task
229
+ end
230
+
231
+ ##
232
+ # Add an artifact to a task (alias for add_artifact for compatibility)
233
+ #
234
+ # @param task_id [String] The task ID
235
+ # @param artifact [A2A::Types::Artifact] The artifact to add
236
+ # @param append [Boolean] Whether to append to existing artifact with same ID
237
+ # @return [A2A::Types::Task] The updated task
238
+ # @raise [A2A::Errors::TaskNotFound] If task doesn't exist
239
+ def add_task_artifact(task_id, artifact, append: false)
240
+ add_artifact(task_id, artifact, append: append)
241
+ end
242
+
243
+ ##
244
+ # Add a message to task history
245
+ #
246
+ # @param task_id [String] The task ID
247
+ # @param message [A2A::Types::Message] The message to add
248
+ # @return [A2A::Types::Task] The updated task
249
+ # @raise [A2A::Errors::TaskNotFound] If task doesn't exist
250
+ def add_message(task_id, message)
251
+ task = get_task(task_id)
252
+ task.add_message(message)
253
+
254
+ # Limit history length if configured
255
+ if @config[:max_history_length] && task.history && task.history.length > @config[:max_history_length]
256
+ # Keep only the most recent messages
257
+ task.instance_variable_set(:@history, task.history.last(@config[:max_history_length]))
258
+ end
259
+
260
+ @storage.save_task(task)
261
+ task
262
+ end
263
+
264
+ ##
265
+ # List tasks by context ID
266
+ #
267
+ # @param context_id [String] The context ID
268
+ # @return [Array<A2A::Types::Task>] Tasks in the context
269
+ def list_tasks_by_context(*args)
270
+ @storage.list_tasks_by_context(*args)
271
+ end
272
+
273
+ ##
274
+ # Add an event handler
275
+ #
276
+ # @param handler [Proc] Event handler that receives (event_type, event_data)
277
+ def add_event_handler(&handler)
278
+ @event_handlers << handler
279
+ end
280
+
281
+ ##
282
+ # Remove an event handler
283
+ #
284
+ # @param handler [Proc] The handler to remove
285
+ def remove_event_handler(handler)
286
+ @event_handlers.delete(handler)
287
+ end
288
+
289
+ private
290
+
291
+ ##
292
+ # Generate a unique task ID
293
+ #
294
+ # @return [String] A unique task ID
295
+ def generate_task_id
296
+ SecureRandom.uuid
297
+ end
298
+
299
+ ##
300
+ # Generate a unique context ID
301
+ #
302
+ # @return [String] A unique context ID
303
+ def generate_context_id
304
+ SecureRandom.uuid
305
+ end
306
+
307
+ ##
308
+ # Validate state transition
309
+ #
310
+ # @param current_state [String] Current task state
311
+ # @param new_state [String] Proposed new state
312
+ # @raise [ArgumentError] If transition is invalid
313
+ def validate_state_transition(current_state, new_state)
314
+ # Define valid state transitions
315
+ valid_transitions = {
316
+ A2A::Types::TASK_STATE_SUBMITTED => [
317
+ A2A::Types::TASK_STATE_WORKING,
318
+ A2A::Types::TASK_STATE_CANCELED,
319
+ A2A::Types::TASK_STATE_REJECTED,
320
+ A2A::Types::TASK_STATE_AUTH_REQUIRED
321
+ ],
322
+ A2A::Types::TASK_STATE_WORKING => [
323
+ A2A::Types::TASK_STATE_INPUT_REQUIRED,
324
+ A2A::Types::TASK_STATE_COMPLETED,
325
+ A2A::Types::TASK_STATE_CANCELED,
326
+ A2A::Types::TASK_STATE_FAILED,
327
+ A2A::Types::TASK_STATE_AUTH_REQUIRED
328
+ ],
329
+ A2A::Types::TASK_STATE_INPUT_REQUIRED => [
330
+ A2A::Types::TASK_STATE_WORKING,
331
+ A2A::Types::TASK_STATE_COMPLETED,
332
+ A2A::Types::TASK_STATE_CANCELED,
333
+ A2A::Types::TASK_STATE_FAILED
334
+ ],
335
+ A2A::Types::TASK_STATE_AUTH_REQUIRED => [
336
+ A2A::Types::TASK_STATE_WORKING,
337
+ A2A::Types::TASK_STATE_CANCELED,
338
+ A2A::Types::TASK_STATE_REJECTED
339
+ ]
340
+ }
341
+
342
+ # Terminal states cannot transition
343
+ terminal_states = [
344
+ A2A::Types::TASK_STATE_COMPLETED,
345
+ A2A::Types::TASK_STATE_CANCELED,
346
+ A2A::Types::TASK_STATE_FAILED,
347
+ A2A::Types::TASK_STATE_REJECTED,
348
+ A2A::Types::TASK_STATE_UNKNOWN
349
+ ]
350
+
351
+ raise ArgumentError, "Cannot transition from terminal state '#{current_state}'" if terminal_states.include?(current_state)
352
+
353
+ allowed_states = valid_transitions[current_state] || []
354
+ return if allowed_states.include?(new_state)
355
+
356
+ raise ArgumentError, "Invalid state transition from '#{current_state}' to '#{new_state}'"
357
+ end
358
+
359
+ ##
360
+ # Emit a task status update event
361
+ #
362
+ # @param task [A2A::Types::Task] The task
363
+ def emit_status_update_event(task)
364
+ event = A2A::Types::TaskStatusUpdateEvent.new(
365
+ task_id: task.id,
366
+ context_id: task.context_id,
367
+ status: task.status,
368
+ metadata: {
369
+ timestamp: Time.now.utc.iso8601,
370
+ event_id: SecureRandom.uuid
371
+ }
372
+ )
373
+
374
+ emit_event("task_status_update", event)
375
+
376
+ # Send push notifications if manager is available
377
+ @push_notification_manager&.notify_task_status_update(event)
378
+ end
379
+
380
+ ##
381
+ # Emit a task artifact update event
382
+ #
383
+ # @param task [A2A::Types::Task] The task
384
+ # @param artifact [A2A::Types::Artifact] The artifact
385
+ # @param append [Boolean] Whether this was an append operation
386
+ def emit_artifact_update_event(task, artifact, append)
387
+ event = A2A::Types::TaskArtifactUpdateEvent.new(
388
+ task_id: task.id,
389
+ context_id: task.context_id,
390
+ artifact: artifact,
391
+ append: append,
392
+ metadata: {
393
+ timestamp: Time.now.utc.iso8601,
394
+ event_id: SecureRandom.uuid
395
+ }
396
+ )
397
+
398
+ emit_event("task_artifact_update", event)
399
+
400
+ # Send push notifications if manager is available
401
+ @push_notification_manager&.notify_task_artifact_update(event)
402
+ end
403
+
404
+ ##
405
+ # Emit an event to all registered handlers
406
+ #
407
+ # @param event_type [String] The event type
408
+ # @param event_data [Object] The event data
409
+ def emit_event(event_type, event_data)
410
+ @event_handlers.each do |handler|
411
+ handler.call(event_type, event_data)
412
+ rescue StandardError => e
413
+ # Log error but don't fail the operation
414
+ warn "Error in event handler: #{e.message}"
415
+ end
416
+ end
417
+
418
+ ##
419
+ # Default configuration
420
+ #
421
+ # @return [Hash] Default configuration
422
+ def default_config
423
+ {
424
+ max_history_length: 100,
425
+ cache_size: 1000,
426
+ cache_ttl: 300 # 5 minutes
427
+ }
428
+ end
429
+
430
+ ##
431
+ # Get task from cache
432
+ #
433
+ # @param task_id [String] The task ID
434
+ # @return [A2A::Types::Task, nil] Cached task or nil
435
+ def get_from_cache(task_id)
436
+ @cache_mutex.synchronize do
437
+ entry = @task_cache[task_id]
438
+ return nil unless entry
439
+
440
+ # Check TTL
441
+ cache_ttl = @config[:cache_ttl] || 300
442
+ if Time.now - entry[:timestamp] > cache_ttl
443
+ @task_cache.delete(task_id)
444
+ return nil
445
+ end
446
+
447
+ entry[:task]
448
+ end
449
+ end
450
+
451
+ ##
452
+ # Add task to cache with LRU eviction
453
+ #
454
+ # @param task_id [String] The task ID
455
+ # @param task [A2A::Types::Task] The task to cache
456
+ def add_to_cache(task_id, task)
457
+ @cache_mutex.synchronize do
458
+ # Evict oldest entries if cache is full
459
+ cache_size = @config[:cache_size] || 1000
460
+ if @task_cache.size >= cache_size
461
+ oldest_key = @task_cache.min_by { |_, entry| entry[:timestamp] }.first
462
+ @task_cache.delete(oldest_key)
463
+ end
464
+
465
+ @task_cache[task_id] = {
466
+ task: task,
467
+ timestamp: Time.now
468
+ }
469
+ end
470
+ end
471
+
472
+ ##
473
+ # Clear task cache
474
+ #
475
+ def clear_cache!
476
+ @cache_mutex.synchronize { @task_cache.clear }
477
+ end
478
+
479
+ ##
480
+ # Record cache hit
481
+ #
482
+ def record_cache_hit
483
+ @metrics_mutex.synchronize { @performance_metrics[:cache_hits] += 1 }
484
+ end
485
+
486
+ ##
487
+ # Record cache miss
488
+ #
489
+ def record_cache_miss
490
+ @metrics_mutex.synchronize { @performance_metrics[:cache_misses] += 1 }
491
+ end
492
+
493
+ ##
494
+ # Record processing time
495
+ #
496
+ # @param duration [Float] Processing time in seconds
497
+ def record_processing_time(duration)
498
+ @metrics_mutex.synchronize do
499
+ current_avg = @performance_metrics[:avg_processing_time]
500
+ total_ops = @performance_metrics[:tasks_created] + @performance_metrics[:tasks_updated]
501
+
502
+ @performance_metrics[:avg_processing_time] = if total_ops.positive?
503
+ ((current_avg * (total_ops - 1)) + duration) / total_ops
504
+ else
505
+ duration
506
+ end
507
+ end
508
+ end
509
+
510
+ ##
511
+ # Get performance metrics
512
+ #
513
+ # @return [Hash] Performance metrics
514
+ def performance_metrics
515
+ @metrics_mutex.synchronize { @performance_metrics.dup }
516
+ end
517
+
518
+ ##
519
+ # Reset performance metrics
520
+ #
521
+ def reset_performance_metrics!
522
+ @metrics_mutex.synchronize do
523
+ @performance_metrics = {
524
+ tasks_created: 0,
525
+ tasks_updated: 0,
526
+ cache_hits: 0,
527
+ cache_misses: 0,
528
+ avg_processing_time: 0.0
529
+ }
530
+ end
531
+ end
532
+ end
533
+ end
534
+ end