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,496 @@
1
+ # frozen_string_literal: true
2
+
3
+ module A2A
4
+ module Protocol
5
+ ##
6
+ # Represents a capability definition for A2A methods
7
+ #
8
+ # Capabilities define the methods that an agent can perform,
9
+ # including input/output schemas, validation rules, and metadata.
10
+ #
11
+ # @example Defining a capability
12
+ # capability = A2A::Protocol::Capability.new(
13
+ # name: "analyze_text",
14
+ # description: "Analyze text content for sentiment and topics",
15
+ # method: "text/analyze",
16
+ # input_schema: {
17
+ # type: "object",
18
+ # properties: {
19
+ # text: { type: "string" },
20
+ # options: { type: "object" }
21
+ # },
22
+ # required: ["text"]
23
+ # },
24
+ # output_schema: {
25
+ # type: "object",
26
+ # properties: {
27
+ # sentiment: { type: "string" },
28
+ # topics: { type: "array", items: { type: "string" } }
29
+ # }
30
+ # }
31
+ # )
32
+ #
33
+ class Capability
34
+ attr_reader :name, :description, :method, :input_schema, :output_schema,
35
+ :examples, :tags, :security_requirements, :metadata,
36
+ :streaming_supported, :async_supported
37
+
38
+ ##
39
+ # Initialize a new capability
40
+ #
41
+ # @param name [String] Capability name (required)
42
+ # @param description [String] Capability description (required)
43
+ # @param method [String] A2A method name (required)
44
+ # @param input_schema [Hash, nil] JSON Schema for input validation
45
+ # @param output_schema [Hash, nil] JSON Schema for output validation
46
+ # @param examples [Array<Hash>, nil] Usage examples
47
+ # @param tags [Array<String>, nil] Capability tags
48
+ # @param security_requirements [Array<String>, nil] Required security schemes
49
+ # @param metadata [Hash, nil] Additional metadata
50
+ # @param streaming_supported [Boolean] Whether streaming is supported
51
+ # @param async_supported [Boolean] Whether async execution is supported
52
+ def initialize(name:, description:, method:, input_schema: nil, output_schema: nil,
53
+ examples: nil, tags: nil, security_requirements: nil, metadata: nil,
54
+ streaming_supported: false, async_supported: false)
55
+ @name = name
56
+ @description = description
57
+ @method = method
58
+ @input_schema = input_schema
59
+ @output_schema = output_schema
60
+ @examples = examples
61
+ @tags = tags
62
+ @security_requirements = security_requirements
63
+ @metadata = metadata
64
+ @streaming_supported = streaming_supported
65
+ @async_supported = async_supported
66
+
67
+ validate!
68
+ end
69
+
70
+ ##
71
+ # Validate input data against the input schema
72
+ #
73
+ # @param input [Hash] The input data to validate
74
+ # @return [Boolean] True if valid
75
+ # @raise [ArgumentError] If validation fails
76
+ def validate_input(input)
77
+ return true if @input_schema.nil?
78
+
79
+ validate_against_schema(input, @input_schema, "input")
80
+ end
81
+
82
+ ##
83
+ # Validate output data against the output schema
84
+ #
85
+ # @param output [Hash] The output data to validate
86
+ # @return [Boolean] True if valid
87
+ # @raise [ArgumentError] If validation fails
88
+ def validate_output(output)
89
+ return true if @output_schema.nil?
90
+
91
+ validate_against_schema(output, @output_schema, "output")
92
+ end
93
+
94
+ ##
95
+ # Check if the capability supports streaming
96
+ #
97
+ # @return [Boolean] True if streaming is supported
98
+ def streaming?
99
+ @streaming_supported == true
100
+ end
101
+
102
+ ##
103
+ # Check if the capability supports async execution
104
+ #
105
+ # @return [Boolean] True if async is supported
106
+ def async?
107
+ @async_supported == true
108
+ end
109
+
110
+ ##
111
+ # Check if the capability has a specific tag
112
+ #
113
+ # @param tag [String] The tag to check
114
+ # @return [Boolean] True if the tag is present
115
+ def has_tag?(tag)
116
+ return false if @tags.nil?
117
+
118
+ @tags.include?(tag)
119
+ end
120
+
121
+ ##
122
+ # Check if the capability requires a specific security scheme
123
+ #
124
+ # @param scheme [String] The security scheme to check
125
+ # @return [Boolean] True if the scheme is required
126
+ def requires_security?(scheme)
127
+ return false if @security_requirements.nil?
128
+
129
+ @security_requirements.include?(scheme)
130
+ end
131
+
132
+ ##
133
+ # Convert to hash representation
134
+ #
135
+ # @return [Hash] The capability as a hash
136
+ def to_h
137
+ {
138
+ name: @name,
139
+ description: @description,
140
+ method: @method,
141
+ input_schema: @input_schema,
142
+ output_schema: @output_schema,
143
+ examples: @examples,
144
+ tags: @tags,
145
+ security_requirements: @security_requirements,
146
+ metadata: @metadata,
147
+ streaming_supported: @streaming_supported,
148
+ async_supported: @async_supported
149
+ }.compact
150
+ end
151
+
152
+ ##
153
+ # Create from hash representation
154
+ #
155
+ # @param hash [Hash] The hash to create from
156
+ # @return [Capability] The new capability instance
157
+ def self.from_h(hash)
158
+ return nil if hash.nil?
159
+
160
+ # Convert string keys to symbols
161
+ normalized_hash = {}
162
+ hash.each do |key, value|
163
+ snake_key = key.to_s.gsub(/([A-Z])/, '_\1').downcase.to_sym
164
+ normalized_hash[snake_key] = value
165
+ end
166
+
167
+ new(**normalized_hash)
168
+ end
169
+
170
+ private
171
+
172
+ def validate!
173
+ if @name.nil? || (respond_to?(:empty?) && empty?) || (is_a?(String) && strip.empty?)
174
+ raise ArgumentError,
175
+ "name is required"
176
+ end
177
+ if @description.nil? || (respond_to?(:empty?) && empty?) || (is_a?(String) && strip.empty?)
178
+ raise ArgumentError,
179
+ "description is required"
180
+ end
181
+ if @method.nil? || (respond_to?(:empty?) && empty?) || (is_a?(String) && strip.empty?)
182
+ raise ArgumentError,
183
+ "method is required"
184
+ end
185
+
186
+ raise ArgumentError, "name must be a String" unless @name.is_a?(String)
187
+ raise ArgumentError, "description must be a String" unless @description.is_a?(String)
188
+ raise ArgumentError, "method must be a String" unless @method.is_a?(String)
189
+
190
+ validate_schema(@input_schema, "input_schema") if @input_schema
191
+ validate_schema(@output_schema, "output_schema") if @output_schema
192
+ validate_examples if @examples
193
+ validate_array_of_strings(@tags, "tags") if @tags
194
+ validate_array_of_strings(@security_requirements, "security_requirements") if @security_requirements
195
+ validate_hash(@metadata, "metadata") if @metadata
196
+ validate_boolean(@streaming_supported, "streaming_supported")
197
+ validate_boolean(@async_supported, "async_supported")
198
+ end
199
+
200
+ ##
201
+ # Validate that a value is a boolean
202
+ def validate_boolean(value, field_name)
203
+ return if value.is_a?(TrueClass) || value.is_a?(FalseClass)
204
+
205
+ raise ArgumentError, "#{field_name} must be a Boolean"
206
+ end
207
+
208
+ ##
209
+ # Validate that a value is a hash
210
+ def validate_hash(value, field_name)
211
+ return if value.is_a?(Hash)
212
+
213
+ raise ArgumentError, "#{field_name} must be a Hash"
214
+ end
215
+
216
+ ##
217
+ # Validate that a value is an array of strings
218
+ def validate_array_of_strings(value, field_name)
219
+ raise ArgumentError, "#{field_name} must be an Array" unless value.is_a?(Array)
220
+
221
+ value.each_with_index do |item, index|
222
+ raise ArgumentError, "#{field_name}[#{index}] must be a String" unless item.is_a?(String)
223
+ end
224
+ end
225
+
226
+ ##
227
+ # Validate that a schema is a valid JSON Schema
228
+ def validate_schema(schema, field_name)
229
+ raise ArgumentError, "#{field_name} must be a Hash" unless schema.is_a?(Hash)
230
+
231
+ # Basic JSON Schema validation - check for type field
232
+ return if schema.key?(:type) || schema.key?("type")
233
+
234
+ raise ArgumentError, "#{field_name} must have a 'type' field"
235
+ end
236
+
237
+ ##
238
+ # Validate examples structure
239
+ def validate_examples
240
+ raise ArgumentError, "examples must be an Array" unless @examples.is_a?(Array)
241
+
242
+ @examples.each_with_index do |example, index|
243
+ raise ArgumentError, "examples[#{index}] must be a Hash" unless example.is_a?(Hash)
244
+
245
+ # Examples should have at least input or description
246
+ unless example.key?(:input) || example.key?("input") ||
247
+ example.key?(:description) || example.key?("description")
248
+ raise ArgumentError, "examples[#{index}] must have 'input' or 'description'"
249
+ end
250
+ end
251
+ end
252
+
253
+ ##
254
+ # Validate data against a JSON Schema (basic implementation)
255
+ #
256
+ # @param data [Object] The data to validate
257
+ # @param schema [Hash] The JSON Schema
258
+ # @param context [String] Context for error messages
259
+ def validate_against_schema(data, schema, context)
260
+ type = schema[:type] || schema["type"]
261
+
262
+ case type
263
+ when "object"
264
+ validate_object(data, schema, context)
265
+ when "array"
266
+ validate_array(data, schema, context)
267
+ when "string"
268
+ validate_string(data, context)
269
+ when "number", "integer"
270
+ validate_number(data, context)
271
+ when "boolean"
272
+ validate_boolean_value(data, context)
273
+ else
274
+ # Allow unknown types for extensibility
275
+ true
276
+ end
277
+ end
278
+
279
+ ##
280
+ # Validate object type
281
+ def validate_object(data, schema, context)
282
+ raise ArgumentError, "#{context} must be an object" unless data.is_a?(Hash)
283
+
284
+ # Check required properties
285
+ required = schema[:required] || schema["required"] || []
286
+ required.each do |prop|
287
+ raise ArgumentError, "#{context} missing required property: #{prop}" unless data.key?(prop) || data.key?(prop.to_sym)
288
+ end
289
+
290
+ # Validate properties if schema defines them
291
+ properties = schema[:properties] || schema["properties"]
292
+ properties&.each do |prop_name, prop_schema|
293
+ prop_value = data[prop_name] || data[prop_name.to_sym]
294
+ validate_against_schema(prop_value, prop_schema, "#{context}.#{prop_name}") if prop_value
295
+ end
296
+
297
+ true
298
+ end
299
+
300
+ ##
301
+ # Validate array type
302
+ def validate_array(data, schema, context)
303
+ raise ArgumentError, "#{context} must be an array" unless data.is_a?(Array)
304
+
305
+ # Validate items if schema defines them
306
+ items_schema = schema[:items] || schema["items"]
307
+ if items_schema
308
+ data.each_with_index do |item, index|
309
+ validate_against_schema(item, items_schema, "#{context}[#{index}]")
310
+ end
311
+ end
312
+
313
+ true
314
+ end
315
+
316
+ ##
317
+ # Validate string type
318
+ def validate_string(data, context)
319
+ raise ArgumentError, "#{context} must be a string" unless data.is_a?(String)
320
+
321
+ true
322
+ end
323
+
324
+ ##
325
+ # Validate number type
326
+ def validate_number(data, context)
327
+ raise ArgumentError, "#{context} must be a number" unless data.is_a?(Numeric)
328
+
329
+ true
330
+ end
331
+
332
+ ##
333
+ # Validate boolean type
334
+ def validate_boolean_value(data, context)
335
+ raise ArgumentError, "#{context} must be a boolean" unless data.is_a?(TrueClass) || data.is_a?(FalseClass)
336
+
337
+ true
338
+ end
339
+ end
340
+
341
+ ##
342
+ # Manages a registry of capabilities
343
+ #
344
+ # The capability registry allows for registration, discovery, and
345
+ # dynamic updates of agent capabilities.
346
+ #
347
+ class CapabilityRegistry
348
+ def initialize
349
+ @capabilities = {}
350
+ @listeners = []
351
+ end
352
+
353
+ ##
354
+ # Register a capability
355
+ #
356
+ # @param capability [Capability] The capability to register
357
+ # @return [Capability] The registered capability
358
+ def register(capability)
359
+ raise ArgumentError, "capability must be a Capability instance" unless capability.is_a?(Capability)
360
+
361
+ @capabilities[capability.name] = capability
362
+ notify_listeners(:registered, capability)
363
+ capability
364
+ end
365
+
366
+ ##
367
+ # Unregister a capability
368
+ #
369
+ # @param name [String] The capability name to unregister
370
+ # @return [Capability, nil] The unregistered capability or nil if not found
371
+ def unregister(name)
372
+ capability = @capabilities.delete(name)
373
+ notify_listeners(:unregistered, capability) if capability
374
+ capability
375
+ end
376
+
377
+ ##
378
+ # Get a capability by name
379
+ #
380
+ # @param name [String] The capability name
381
+ # @return [Capability, nil] The capability or nil if not found
382
+ def get(name)
383
+ @capabilities[name]
384
+ end
385
+
386
+ ##
387
+ # Get all registered capabilities
388
+ #
389
+ # @return [Array<Capability>] All capabilities
390
+ def all
391
+ @capabilities.values
392
+ end
393
+
394
+ ##
395
+ # Find capabilities by tag
396
+ #
397
+ # @param tag [String] The tag to search for
398
+ # @return [Array<Capability>] Capabilities with the tag
399
+ def find_by_tag(tag)
400
+ @capabilities.values.select { |cap| cap.has_tag?(tag) }
401
+ end
402
+
403
+ ##
404
+ # Find capabilities by method pattern
405
+ #
406
+ # @param pattern [String, Regexp] The method pattern to match
407
+ # @return [Array<Capability>] Matching capabilities
408
+ def find_by_method(pattern)
409
+ if pattern.is_a?(String)
410
+ @capabilities.values.select { |cap| cap.method == pattern }
411
+ elsif pattern.is_a?(Regexp)
412
+ @capabilities.values.select { |cap| cap.method.match?(pattern) }
413
+ else
414
+ raise ArgumentError, "pattern must be a String or Regexp"
415
+ end
416
+ end
417
+
418
+ ##
419
+ # Find capabilities requiring specific security
420
+ #
421
+ # @param scheme [String] The security scheme
422
+ # @return [Array<Capability>] Capabilities requiring the scheme
423
+ def find_by_security(scheme)
424
+ @capabilities.values.select { |cap| cap.requires_security?(scheme) }
425
+ end
426
+
427
+ ##
428
+ # Check if a capability is registered
429
+ #
430
+ # @param name [String] The capability name
431
+ # @return [Boolean] True if registered
432
+ def registered?(name)
433
+ @capabilities.key?(name)
434
+ end
435
+
436
+ ##
437
+ # Get the number of registered capabilities
438
+ #
439
+ # @return [Integer] The count
440
+ def count
441
+ @capabilities.size
442
+ end
443
+
444
+ ##
445
+ # Clear all capabilities
446
+ def clear
447
+ old_capabilities = @capabilities.values
448
+ @capabilities.clear
449
+ old_capabilities.each { |cap| notify_listeners(:unregistered, cap) }
450
+ end
451
+
452
+ ##
453
+ # Add a listener for capability changes
454
+ #
455
+ # @param listener [Proc] The listener proc that receives (event, capability)
456
+ def add_listener(&listener)
457
+ @listeners << listener
458
+ end
459
+
460
+ ##
461
+ # Remove a listener
462
+ #
463
+ # @param listener [Proc] The listener to remove
464
+ def remove_listener(listener)
465
+ @listeners.delete(listener)
466
+ end
467
+
468
+ ##
469
+ # Convert registry to hash representation
470
+ #
471
+ # @return [Hash] The registry as a hash
472
+ def to_h
473
+ {
474
+ capabilities: @capabilities.transform_values(&:to_h),
475
+ count: count
476
+ }
477
+ end
478
+
479
+ private
480
+
481
+ ##
482
+ # Notify all listeners of a capability event
483
+ #
484
+ # @param event [Symbol] The event type (:registered, :unregistered)
485
+ # @param capability [Capability] The capability involved
486
+ def notify_listeners(event, capability)
487
+ @listeners.each do |listener|
488
+ listener.call(event, capability)
489
+ rescue StandardError => e
490
+ # Log error but don't let listener errors break the registry
491
+ warn "Capability registry listener error: #{e.message}"
492
+ end
493
+ end
494
+ end
495
+ end
496
+ end