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,438 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module A2A
6
+ module Monitoring
7
+ end
8
+ end
9
+
10
+ ##
11
+ # Metrics collector for performance monitoring and alerting
12
+ #
13
+ # Collects and aggregates performance metrics, provides alerting capabilities,
14
+ # and integrates with monitoring systems like Prometheus, StatsD, etc.
15
+ #
16
+ module A2A
17
+ module Monitoring
18
+ class MetricsCollector
19
+ include MonitorMixin
20
+
21
+ # Metric types
22
+ COUNTER = :counter
23
+ GAUGE = :gauge
24
+ HISTOGRAM = :histogram
25
+ TIMER = :timer
26
+
27
+ attr_reader :metrics, :start_time
28
+
29
+ ##
30
+ # Initialize metrics collector
31
+ #
32
+ # @param flush_interval [Integer] Interval to flush metrics (seconds)
33
+ # @param retention_period [Integer] How long to keep metrics (seconds)
34
+ def initialize(flush_interval: 60, retention_period: 3600)
35
+ super()
36
+
37
+ @metrics = {}
38
+ @flush_interval = flush_interval
39
+ @retention_period = retention_period
40
+ @start_time = Time.now
41
+ @last_flush = Time.now
42
+ @exporters = []
43
+ @alert_rules = []
44
+
45
+ start_background_flush if flush_interval.positive?
46
+ end
47
+
48
+ ##
49
+ # Increment a counter metric
50
+ #
51
+ # @param name [String] Metric name
52
+ # @param value [Numeric] Value to add (default: 1)
53
+ # @param tags [Hash] Metric tags
54
+ def increment(name, value: 1, **tags)
55
+ synchronize do
56
+ metric = get_or_create_metric(name, COUNTER, tags)
57
+ metric[:value] += value
58
+ metric[:last_updated] = Time.now
59
+
60
+ check_alerts(name, metric[:value], tags)
61
+ end
62
+ end
63
+
64
+ ##
65
+ # Set a gauge metric value
66
+ #
67
+ # @param name [String] Metric name
68
+ # @param value [Numeric] Metric value
69
+ # @param tags [Hash] Metric tags
70
+ def gauge(name, value, **tags)
71
+ synchronize do
72
+ metric = get_or_create_metric(name, GAUGE, tags)
73
+ metric[:value] = value
74
+ metric[:last_updated] = Time.now
75
+
76
+ check_alerts(name, value, tags)
77
+ end
78
+ end
79
+
80
+ ##
81
+ # Record a histogram value
82
+ #
83
+ # @param name [String] Metric name
84
+ # @param value [Numeric] Value to record
85
+ # @param tags [Hash] Metric tags
86
+ def histogram(name, value, **tags)
87
+ synchronize do
88
+ metric = get_or_create_metric(name, HISTOGRAM, tags)
89
+ metric[:values] << value
90
+ metric[:count] += 1
91
+ metric[:sum] += value
92
+ metric[:last_updated] = Time.now
93
+
94
+ # Calculate percentiles
95
+ update_histogram_stats(metric)
96
+
97
+ check_alerts(name, value, tags)
98
+ end
99
+ end
100
+
101
+ ##
102
+ # Time a block of code
103
+ #
104
+ # @param name [String] Metric name
105
+ # @param tags [Hash] Metric tags
106
+ # @yield Block to time
107
+ # @return [Object] Result of the block
108
+ def time(name, **tags)
109
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
110
+
111
+ begin
112
+ result = yield
113
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
114
+ histogram("#{name}.duration", duration * 1000, **tags) # Convert to milliseconds
115
+ increment("#{name}.success", **tags)
116
+ result
117
+ rescue StandardError
118
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
119
+ histogram("#{name}.duration", duration * 1000, **tags)
120
+ increment("#{name}.error", **tags)
121
+ raise
122
+ end
123
+ end
124
+
125
+ ##
126
+ # Record HTTP request metrics
127
+ #
128
+ # @param method [String] HTTP method
129
+ # @param path [String] Request path
130
+ # @param status [Integer] Response status
131
+ # @param duration [Float] Request duration in seconds
132
+ # @param tags [Hash] Additional tags
133
+ def http_request(method:, path:, status:, duration:, **tags)
134
+ base_tags = {
135
+ method: method.to_s.upcase,
136
+ path: normalize_path(path),
137
+ status: status,
138
+ **tags
139
+ }
140
+
141
+ increment("http_requests_total", **base_tags)
142
+ histogram("http_request_duration_ms", duration * 1000, **base_tags)
143
+
144
+ # Track error rates
145
+ return unless status >= 400
146
+
147
+ increment("http_requests_errors_total", **base_tags)
148
+ end
149
+
150
+ ##
151
+ # Record task metrics
152
+ #
153
+ # @param operation [String] Task operation
154
+ # @param task_type [String] Type of task
155
+ # @param status [String] Task status
156
+ # @param duration [Float, nil] Operation duration
157
+ # @param tags [Hash] Additional tags
158
+ def task_operation(operation:, task_type: nil, status: nil, duration: nil, **tags)
159
+ base_tags = {
160
+ operation: operation,
161
+ task_type: task_type,
162
+ status: status,
163
+ **tags
164
+ }.compact
165
+
166
+ increment("task_operations_total", **base_tags)
167
+
168
+ return unless duration
169
+
170
+ histogram("task_operation_duration_ms", duration * 1000, **base_tags)
171
+ end
172
+
173
+ ##
174
+ # Add a metrics exporter
175
+ #
176
+ # @param exporter [Object] Exporter object with export(metrics) method
177
+ def add_exporter(exporter)
178
+ synchronize { @exporters << exporter }
179
+ end
180
+
181
+ ##
182
+ # Remove a metrics exporter
183
+ #
184
+ # @param exporter [Object] Exporter to remove
185
+ def remove_exporter(exporter)
186
+ synchronize { @exporters.delete(exporter) }
187
+ end
188
+
189
+ ##
190
+ # Add an alert rule
191
+ #
192
+ # @param name [String] Alert name
193
+ # @param metric [String] Metric name to monitor
194
+ # @param condition [Symbol] Condition (:gt, :lt, :eq, :gte, :lte)
195
+ # @param threshold [Numeric] Threshold value
196
+ # @param callback [Proc] Callback to execute when alert fires
197
+ def add_alert(name, metric:, condition:, threshold:, &callback)
198
+ synchronize do
199
+ @alert_rules << {
200
+ name: name,
201
+ metric: metric,
202
+ condition: condition,
203
+ threshold: threshold,
204
+ callback: callback,
205
+ last_fired: nil
206
+ }
207
+ end
208
+ end
209
+
210
+ ##
211
+ # Get current metrics snapshot
212
+ #
213
+ # @return [Hash] Current metrics
214
+ def snapshot
215
+ synchronize { deep_copy(@metrics) }
216
+ end
217
+
218
+ ##
219
+ # Get metrics summary
220
+ #
221
+ # @return [Hash] Metrics summary
222
+ def summary
223
+ synchronize do
224
+ {
225
+ total_metrics: @metrics.size,
226
+ uptime: Time.now - @start_time,
227
+ last_flush: @last_flush,
228
+ exporters: @exporters.size,
229
+ alert_rules: @alert_rules.size,
230
+ memory_usage: get_memory_usage
231
+ }
232
+ end
233
+ end
234
+
235
+ ##
236
+ # Flush metrics to exporters
237
+ #
238
+ def flush!
239
+ metrics_snapshot = snapshot
240
+
241
+ @exporters.each do |exporter|
242
+ exporter.export(metrics_snapshot)
243
+ rescue StandardError => e
244
+ warn "Failed to export metrics: #{e.message}"
245
+ end
246
+
247
+ synchronize { @last_flush = Time.now }
248
+ cleanup_old_metrics
249
+ end
250
+
251
+ ##
252
+ # Reset all metrics
253
+ #
254
+ def reset!
255
+ synchronize do
256
+ @metrics.clear
257
+ @start_time = Time.now
258
+ @last_flush = Time.now
259
+ end
260
+ end
261
+
262
+ ##
263
+ # Stop the metrics collector
264
+ #
265
+ def stop
266
+ @flush_thread&.kill
267
+ @flush_thread = nil
268
+ end
269
+
270
+ private
271
+
272
+ ##
273
+ # Get or create a metric
274
+ #
275
+ # @param name [String] Metric name
276
+ # @param type [Symbol] Metric type
277
+ # @param tags [Hash] Metric tags
278
+ # @return [Hash] Metric data
279
+ def get_or_create_metric(name, type, tags)
280
+ key = build_metric_key(name, tags)
281
+
282
+ @metrics[key] ||= {
283
+ name: name,
284
+ type: type,
285
+ tags: tags,
286
+ value: type == COUNTER ? 0 : nil,
287
+ values: type == HISTOGRAM ? [] : nil,
288
+ count: type == HISTOGRAM ? 0 : nil,
289
+ sum: type == HISTOGRAM ? 0 : nil,
290
+ created_at: Time.now,
291
+ last_updated: Time.now
292
+ }
293
+ end
294
+
295
+ ##
296
+ # Build metric key from name and tags
297
+ #
298
+ # @param name [String] Metric name
299
+ # @param tags [Hash] Metric tags
300
+ # @return [String] Metric key
301
+ def build_metric_key(name, tags)
302
+ if tags.empty?
303
+ name
304
+ else
305
+ tag_string = tags.sort.map { |k, v| "#{k}=#{v}" }.join(",")
306
+ "#{name}{#{tag_string}}"
307
+ end
308
+ end
309
+
310
+ ##
311
+ # Update histogram statistics
312
+ #
313
+ # @param metric [Hash] Histogram metric
314
+ def update_histogram_stats(metric)
315
+ values = metric[:values].sort
316
+ count = values.size
317
+
318
+ return if count.zero?
319
+
320
+ metric[:min] = values.first
321
+ metric[:max] = values.last
322
+ metric[:avg] = metric[:sum].to_f / count
323
+
324
+ # Calculate percentiles
325
+ metric[:p50] = percentile(values, 0.5)
326
+ metric[:p95] = percentile(values, 0.95)
327
+ metric[:p99] = percentile(values, 0.99)
328
+
329
+ # Keep only recent values to prevent memory growth
330
+ return unless values.size > 1000
331
+
332
+ metric[:values] = values.last(1000)
333
+ end
334
+
335
+ ##
336
+ # Calculate percentile from sorted values
337
+ #
338
+ # @param values [Array] Sorted array of values
339
+ # @param percentile [Float] Percentile (0.0 to 1.0)
340
+ # @return [Numeric] Percentile value
341
+ def percentile(values, percentile)
342
+ return 0 if values.empty?
343
+
344
+ index = (percentile * (values.size - 1)).round
345
+ values[index]
346
+ end
347
+
348
+ ##
349
+ # Check alert rules
350
+ #
351
+ # @param metric_name [String] Metric name
352
+ # @param value [Numeric] Metric value
353
+ # @param tags [Hash] Metric tags
354
+ def check_alerts(metric_name, value, tags)
355
+ @alert_rules.each do |rule|
356
+ next unless rule[:metric] == metric_name
357
+
358
+ should_fire = case rule[:condition]
359
+ when :gt then value > rule[:threshold]
360
+ when :gte then value >= rule[:threshold]
361
+ when :lt then value < rule[:threshold]
362
+ when :lte then value <= rule[:threshold]
363
+ when :eq then value == rule[:threshold]
364
+ else false
365
+ end
366
+
367
+ if should_fire && (rule[:last_fired].nil? || Time.now - rule[:last_fired] > 60)
368
+ rule[:callback]&.call(rule[:name], metric_name, value, tags)
369
+ rule[:last_fired] = Time.now
370
+ end
371
+ end
372
+ end
373
+
374
+ ##
375
+ # Normalize URL path for metrics
376
+ #
377
+ # @param path [String] URL path
378
+ # @return [String] Normalized path
379
+ def normalize_path(path)
380
+ # Replace IDs and UUIDs with placeholders
381
+ path.gsub(%r{/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}, "/:uuid")
382
+ .gsub(%r{/\d+}, "/:id")
383
+ end
384
+
385
+ ##
386
+ # Start background flush thread
387
+ #
388
+ def start_background_flush
389
+ @flush_thread = Thread.new do
390
+ loop do
391
+ sleep @flush_interval
392
+ flush!
393
+ rescue StandardError => e
394
+ warn "Error in metrics flush thread: #{e.message}"
395
+ end
396
+ end
397
+ end
398
+
399
+ ##
400
+ # Clean up old metrics
401
+ #
402
+ def cleanup_old_metrics
403
+ cutoff_time = Time.now - @retention_period
404
+
405
+ synchronize do
406
+ @metrics.reject! do |_, metric|
407
+ metric[:last_updated] < cutoff_time
408
+ end
409
+ end
410
+ end
411
+
412
+ ##
413
+ # Get current memory usage
414
+ #
415
+ # @return [Integer] Memory usage in bytes
416
+ def get_memory_usage
417
+ if defined?(GC.stat)
418
+ GC.stat(:heap_allocated_pages) * GC.stat(:heap_page_size)
419
+ else
420
+ 0
421
+ end
422
+ rescue StandardError
423
+ 0
424
+ end
425
+
426
+ ##
427
+ # Deep copy a hash
428
+ #
429
+ # @param obj [Object] Object to copy
430
+ # @return [Object] Deep copy
431
+ def deep_copy(obj)
432
+ Marshal.load(Marshal.dump(obj))
433
+ rescue StandardError
434
+ obj.dup
435
+ end
436
+ end
437
+ end
438
+ end