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,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "auth/oauth2"
4
+ require_relative "auth/jwt"
5
+ require_relative "auth/api_key"
6
+ require_relative "auth/interceptor"
7
+
8
+ ##
9
+ # Authentication strategies and utilities for A2A clients
10
+ #
11
+ # This module provides various authentication mechanisms for communicating
12
+ # with A2A agents, including OAuth 2.0, JWT, and API key authentication.
13
+ #
14
+ # @example OAuth 2.0 authentication
15
+ # oauth = A2A::Client::Auth::OAuth2.new(
16
+ # client_id: 'your-client-id',
17
+ # client_secret: 'your-client-secret',
18
+ # token_url: 'https://auth.example.com/oauth/token'
19
+ # )
20
+ #
21
+ # client = A2A::Client::HttpClient.new(
22
+ # 'https://agent.example.com',
23
+ # middleware: [A2A::Client::Auth::Interceptor.new(oauth)]
24
+ # )
25
+ #
26
+ # @example JWT authentication
27
+ # jwt = A2A::Client::Auth::JWT.new(
28
+ # token: 'your-jwt-token'
29
+ # )
30
+ #
31
+ # interceptor = A2A::Client::Auth::Interceptor.new(jwt)
32
+ #
33
+ # @example API key authentication
34
+ # api_key = A2A::Client::Auth::ApiKey.new(
35
+ # key: 'your-api-key',
36
+ # name: 'X-API-Key',
37
+ # location: 'header'
38
+ # )
39
+ #
40
+ # interceptor = A2A::Client::Auth::Interceptor.new(api_key)
41
+ #
42
+ module A2A
43
+ module Client
44
+ module Auth
45
+ ##
46
+ # Create authentication strategy from configuration
47
+ #
48
+ # @param config [Hash] Authentication configuration
49
+ # @return [Object] Authentication strategy instance
50
+ def self.from_config(config)
51
+ case config["type"] || config[:type]
52
+ when "oauth2"
53
+ OAuth2.new(
54
+ client_id: config["client_id"] || config[:client_id],
55
+ client_secret: config["client_secret"] || config[:client_secret],
56
+ token_url: config["token_url"] || config[:token_url],
57
+ scope: config["scope"] || config[:scope]
58
+ )
59
+ when "jwt"
60
+ JWT.new(
61
+ token: config["token"] || config[:token],
62
+ secret: config["secret"] || config[:secret],
63
+ algorithm: config["algorithm"] || config[:algorithm] || "HS256",
64
+ payload: config["payload"] || config[:payload],
65
+ headers: config["headers"] || config[:headers],
66
+ expires_in: config["expires_in"] || config[:expires_in]
67
+ )
68
+ when "api_key"
69
+ ApiKey.new(
70
+ key: config["key"] || config[:key],
71
+ name: config["name"] || config[:name] || "X-API-Key",
72
+ location: config["location"] || config[:location] || "header"
73
+ )
74
+ else
75
+ raise ArgumentError, "Unknown authentication type: #{config['type'] || config[:type]}"
76
+ end
77
+ end
78
+
79
+ ##
80
+ # Create authentication strategy from security scheme
81
+ #
82
+ # @param scheme [Hash] Security scheme definition from agent card
83
+ # @param credentials [Hash] Authentication credentials
84
+ # @return [Object] Authentication strategy instance
85
+ def self.from_security_scheme(scheme, credentials)
86
+ case scheme["type"]
87
+ when "oauth2"
88
+ OAuth2.new(
89
+ client_id: credentials["client_id"],
90
+ client_secret: credentials["client_secret"],
91
+ token_url: scheme["tokenUrl"],
92
+ scope: credentials["scope"]
93
+ )
94
+ when "http"
95
+ case scheme["scheme"]
96
+ when "bearer"
97
+ JWT.new(token: credentials["token"])
98
+ when "basic"
99
+ # Basic auth configuration
100
+ {
101
+ type: "basic",
102
+ username: credentials["username"],
103
+ password: credentials["password"]
104
+ }
105
+ else
106
+ raise ArgumentError, "Unsupported HTTP scheme: #{scheme['scheme']}"
107
+ end
108
+ when "apiKey"
109
+ ApiKey.from_security_scheme(scheme, credentials["key"])
110
+ else
111
+ raise ArgumentError, "Unsupported security scheme type: #{scheme['type']}"
112
+ end
113
+ end
114
+
115
+ ##
116
+ # Create interceptor from configuration
117
+ #
118
+ # @param config [Hash] Authentication configuration
119
+ # @return [Interceptor] Configured authentication interceptor
120
+ def self.interceptor_from_config(config)
121
+ strategy = from_config(config)
122
+ Interceptor.new(strategy, auto_retry: config["auto_retry"] || config[:auto_retry] || true)
123
+ end
124
+
125
+ ##
126
+ # Create interceptor from security scheme
127
+ #
128
+ # @param scheme [Hash] Security scheme definition
129
+ # @param credentials [Hash] Authentication credentials
130
+ # @return [Interceptor] Configured authentication interceptor
131
+ def self.interceptor_from_security_scheme(scheme, credentials)
132
+ strategy = from_security_scheme(scheme, credentials)
133
+ Interceptor.new(strategy)
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,316 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config"
4
+
5
+ ##
6
+ # Abstract base class for A2A clients
7
+ #
8
+ # Provides the common interface and functionality for all A2A client implementations.
9
+ # Concrete clients should inherit from this class and implement the abstract methods.
10
+ #
11
+ module A2A
12
+ module Client
13
+ class Base
14
+ attr_reader :config, :middleware, :consumers
15
+
16
+ ##
17
+ # Initialize a new client
18
+ #
19
+ # @param config [Config, nil] Client configuration
20
+ # @param middleware [Array] List of middleware interceptors
21
+ # @param consumers [Array] List of event consumers
22
+ def initialize(config: nil, middleware: [], consumers: [])
23
+ @config = config || Config.new
24
+ @middleware = middleware.dup
25
+ @consumers = consumers.dup
26
+ @task_callbacks = {}
27
+ end
28
+
29
+ ##
30
+ # Send a message to the agent
31
+ #
32
+ # @param message [Message, Hash] The message to send
33
+ # @param context [Hash, nil] Optional context information
34
+ # @return [Enumerator, Message] Stream of responses or single response
35
+ # @raise [NotImplementedError] Must be implemented by subclasses
36
+ def send_message(message, context: nil)
37
+ raise NotImplementedError, "#{self.class}#send_message must be implemented"
38
+ end
39
+
40
+ ##
41
+ # Get a task by ID
42
+ #
43
+ # @param task_id [String] The task ID
44
+ # @param context [Hash, nil] Optional context information
45
+ # @param history_length [Integer, nil] Maximum number of history messages to include
46
+ # @return [Task] The task
47
+ # @raise [NotImplementedError] Must be implemented by subclasses
48
+ def get_task(task_id, context: nil, history_length: nil)
49
+ raise NotImplementedError, "#{self.class}#get_task must be implemented"
50
+ end
51
+
52
+ ##
53
+ # Cancel a task
54
+ #
55
+ # @param task_id [String] The task ID to cancel
56
+ # @param context [Hash, nil] Optional context information
57
+ # @return [Task] The updated task
58
+ # @raise [NotImplementedError] Must be implemented by subclasses
59
+ def cancel_task(task_id, context: nil)
60
+ raise NotImplementedError, "#{self.class}#cancel_task must be implemented"
61
+ end
62
+
63
+ ##
64
+ # Get the agent card
65
+ #
66
+ # @param context [Hash, nil] Optional context information
67
+ # @param authenticated [Boolean] Whether to get authenticated extended card
68
+ # @return [AgentCard] The agent card
69
+ # @raise [NotImplementedError] Must be implemented by subclasses
70
+ def get_card(context: nil, authenticated: false)
71
+ raise NotImplementedError, "#{self.class}#get_card must be implemented"
72
+ end
73
+
74
+ ##
75
+ # Resubscribe to a task for streaming updates
76
+ #
77
+ # @param task_id [String] The task ID to resubscribe to
78
+ # @param context [Hash, nil] Optional context information
79
+ # @return [Enumerator] Stream of task updates
80
+ # @raise [NotImplementedError] Must be implemented by subclasses
81
+ def resubscribe(task_id, context: nil)
82
+ raise NotImplementedError, "#{self.class}#resubscribe must be implemented"
83
+ end
84
+
85
+ ##
86
+ # Set a callback for task updates
87
+ #
88
+ # @param task_id [String] The task ID
89
+ # @param push_notification_config [PushNotificationConfig, Hash] The push notification configuration
90
+ # @param context [Hash, nil] Optional context information
91
+ # @return [void]
92
+ # @raise [NotImplementedError] Must be implemented by subclasses
93
+ def set_task_callback(task_id, push_notification_config, context: nil)
94
+ raise NotImplementedError, "#{self.class}#set_task_callback must be implemented"
95
+ end
96
+
97
+ ##
98
+ # Get the callback configuration for a task
99
+ #
100
+ # @param task_id [String] The task ID
101
+ # @param push_notification_config_id [String] The push notification config ID
102
+ # @param context [Hash, nil] Optional context information
103
+ # @return [TaskPushNotificationConfig] The callback configuration
104
+ # @raise [NotImplementedError] Must be implemented by subclasses
105
+ def get_task_callback(task_id, push_notification_config_id, context: nil)
106
+ raise NotImplementedError, "#{self.class}#get_task_callback must be implemented"
107
+ end
108
+
109
+ ##
110
+ # List all callback configurations for a task
111
+ #
112
+ # @param task_id [String] The task ID
113
+ # @param context [Hash, nil] Optional context information
114
+ # @return [Array<TaskPushNotificationConfig>] List of callback configurations
115
+ # @raise [NotImplementedError] Must be implemented by subclasses
116
+ def list_task_callbacks(task_id, context: nil)
117
+ raise NotImplementedError, "#{self.class}#list_task_callbacks must be implemented"
118
+ end
119
+
120
+ ##
121
+ # Delete a callback configuration for a task
122
+ #
123
+ # @param task_id [String] The task ID
124
+ # @param push_notification_config_id [String] The push notification config ID
125
+ # @param context [Hash, nil] Optional context information
126
+ # @return [void]
127
+ # @raise [NotImplementedError] Must be implemented by subclasses
128
+ def delete_task_callback(task_id, push_notification_config_id, context: nil)
129
+ raise NotImplementedError, "#{self.class}#delete_task_callback must be implemented"
130
+ end
131
+
132
+ ##
133
+ # Add middleware to the client
134
+ #
135
+ # @param interceptor [Object] The middleware interceptor
136
+ # @return [void]
137
+ def add_middleware(interceptor)
138
+ @middleware << interceptor
139
+ end
140
+
141
+ ##
142
+ # Remove middleware from the client
143
+ #
144
+ # @param interceptor [Object] The middleware interceptor to remove
145
+ # @return [void]
146
+ def remove_middleware(interceptor)
147
+ @middleware.delete(interceptor)
148
+ end
149
+
150
+ ##
151
+ # Add an event consumer
152
+ #
153
+ # @param consumer [Object] The event consumer
154
+ # @return [void]
155
+ def add_consumer(consumer)
156
+ @consumers << consumer
157
+ end
158
+
159
+ ##
160
+ # Remove an event consumer
161
+ #
162
+ # @param consumer [Object] The event consumer to remove
163
+ # @return [void]
164
+ def remove_consumer(consumer)
165
+ @consumers.delete(consumer)
166
+ end
167
+
168
+ ##
169
+ # Check if the client supports streaming
170
+ #
171
+ # @return [Boolean] True if streaming is supported and enabled
172
+ def streaming?
173
+ @config.streaming?
174
+ end
175
+
176
+ ##
177
+ # Check if the client supports polling
178
+ #
179
+ # @return [Boolean] True if polling is supported and enabled
180
+ def polling?
181
+ @config.polling?
182
+ end
183
+
184
+ ##
185
+ # Get the supported transports
186
+ #
187
+ # @return [Array<String>] List of supported transport protocols
188
+ def supported_transports
189
+ @config.supported_transports
190
+ end
191
+
192
+ ##
193
+ # Negotiate transport with agent card
194
+ #
195
+ # @param agent_card [AgentCard] The agent card
196
+ # @return [String] The negotiated transport protocol
197
+ def negotiate_transport(agent_card)
198
+ # Use client preference if enabled
199
+ if @config.use_client_preference?
200
+ preferred = @config.preferred_transport
201
+ return preferred if agent_supports_transport?(agent_card, preferred)
202
+ end
203
+
204
+ # Find first mutually supported transport
205
+ @config.supported_transports.each do |transport|
206
+ return transport if agent_supports_transport?(agent_card, transport)
207
+ end
208
+
209
+ # Fallback to agent's preferred transport if we support it
210
+ agent_preferred = agent_card.preferred_transport
211
+ return agent_preferred if @config.supports_transport?(agent_preferred)
212
+
213
+ # No compatible transport found
214
+ raise A2A::Errors::ClientError, "No compatible transport protocol found"
215
+ end
216
+
217
+ ##
218
+ # Get the endpoint URL for a specific transport
219
+ #
220
+ # @param agent_card [AgentCard] The agent card
221
+ # @param transport [String] The transport protocol
222
+ # @return [String] The endpoint URL
223
+ def get_endpoint_url(agent_card, transport)
224
+ # Check if the transport matches the preferred transport
225
+ return agent_card.url if agent_card.preferred_transport == transport
226
+
227
+ # Look for the transport in additional interfaces
228
+ interface = agent_card.additional_interfaces&.find { |iface| iface.transport == transport }
229
+ return interface.url if interface
230
+
231
+ # Fallback to main URL if no specific interface found
232
+ agent_card.url
233
+ end
234
+
235
+ protected
236
+
237
+ ##
238
+ # Execute middleware chain for a request
239
+ #
240
+ # @param request [Object] The request object
241
+ # @param context [Hash] The request context
242
+ # @yield [request, context] The block to execute after middleware
243
+ # @return [Object] The result of the block execution
244
+ def execute_with_middleware(request, context = {}, &block)
245
+ # Create a chain of middleware calls
246
+ chain = @middleware.reverse.reduce(proc(&block)) do |next_call, middleware|
247
+ proc { |req, ctx| middleware.call(req, ctx, next_call) }
248
+ end
249
+
250
+ # Execute the chain
251
+ chain.call(request, context)
252
+ end
253
+
254
+ ##
255
+ # Process events with registered consumers
256
+ #
257
+ # @param event [Object] The event to process
258
+ # @return [void]
259
+ def process_event(event)
260
+ @consumers.each do |consumer|
261
+ consumer.call(event)
262
+ rescue StandardError => e
263
+ # Log error but don't fail the entire processing
264
+ warn "Error in event consumer: #{e.message}"
265
+ end
266
+ end
267
+
268
+ ##
269
+ # Convert a message hash or object to a Message instance
270
+ #
271
+ # @param message [Message, Hash] The message to convert
272
+ # @return [Message] The message instance
273
+ def ensure_message(message)
274
+ return message if message.is_a?(A2A::Types::Message)
275
+
276
+ A2A::Types::Message.from_h(message)
277
+ end
278
+
279
+ ##
280
+ # Convert a task hash or object to a Task instance
281
+ #
282
+ # @param task [Task, Hash] The task to convert
283
+ # @return [Task] The task instance
284
+ def ensure_task(task)
285
+ return task if task.is_a?(A2A::Types::Task)
286
+
287
+ A2A::Types::Task.from_h(task)
288
+ end
289
+
290
+ ##
291
+ # Convert an agent card hash or object to an AgentCard instance
292
+ #
293
+ # @param agent_card [AgentCard, Hash] The agent card to convert
294
+ # @return [AgentCard] The agent card instance
295
+ def ensure_agent_card(agent_card)
296
+ return agent_card if agent_card.is_a?(A2A::Types::AgentCard)
297
+
298
+ A2A::Types::AgentCard.from_h(agent_card)
299
+ end
300
+
301
+ private
302
+
303
+ ##
304
+ # Check if an agent supports a specific transport
305
+ #
306
+ # @param agent_card [AgentCard] The agent card
307
+ # @param transport [String] The transport to check
308
+ # @return [Boolean] True if the agent supports the transport
309
+ def agent_supports_transport?(agent_card, transport)
310
+ return true if agent_card.preferred_transport == transport
311
+
312
+ agent_card.additional_interfaces&.any? { |iface| iface.transport == transport }
313
+ end
314
+ end
315
+ end
316
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../version"
4
+
5
+ ##
6
+ # Configuration class for A2A clients
7
+ #
8
+ # Manages client behavior including transport preferences, streaming options,
9
+ # authentication settings, and operational parameters.
10
+ #
11
+ module A2A
12
+ module Client
13
+ class Config
14
+ attr_accessor :streaming, :polling, :supported_transports, :use_client_preference,
15
+ :accepted_output_modes, :push_notification_configs, :timeout,
16
+ :retry_attempts, :retry_delay, :max_retry_delay, :backoff_multiplier,
17
+ :endpoint_url, :authentication, :headers, :user_agent
18
+
19
+ ##
20
+ # Initialize a new client configuration
21
+ #
22
+ # @param streaming [Boolean] Enable streaming responses (default: true)
23
+ # @param polling [Boolean] Enable polling for task updates (default: false)
24
+ # @param supported_transports [Array<String>] Supported transport protocols
25
+ # @param use_client_preference [Boolean] Use client transport preference (default: true)
26
+ # @param accepted_output_modes [Array<String>] Accepted output modes
27
+ # @param push_notification_configs [Array<Hash>] Push notification configurations
28
+ # @param timeout [Integer] Request timeout in seconds (default: 30)
29
+ # @param retry_attempts [Integer] Number of retry attempts (default: 3)
30
+ # @param retry_delay [Float] Initial retry delay in seconds (default: 1.0)
31
+ # @param max_retry_delay [Float] Maximum retry delay in seconds (default: 60.0)
32
+ # @param backoff_multiplier [Float] Backoff multiplier for retries (default: 2.0)
33
+ # @param endpoint_url [String] Base endpoint URL
34
+ # @param authentication [Hash] Authentication configuration
35
+ # @param headers [Hash] Additional HTTP headers
36
+ # @param user_agent [String] User agent string
37
+ def initialize(streaming: true, polling: false, supported_transports: nil,
38
+ use_client_preference: true, accepted_output_modes: nil,
39
+ push_notification_configs: nil, timeout: 30, retry_attempts: 3,
40
+ retry_delay: 1.0, max_retry_delay: 60.0, backoff_multiplier: 2.0,
41
+ endpoint_url: nil, authentication: nil, headers: nil, user_agent: nil)
42
+ @streaming = streaming
43
+ @polling = polling
44
+ @supported_transports = supported_transports || [A2A::Types::TRANSPORT_JSONRPC]
45
+ @use_client_preference = use_client_preference
46
+ @accepted_output_modes = accepted_output_modes || %w[text file data]
47
+ @push_notification_configs = push_notification_configs || []
48
+ @timeout = timeout
49
+ @retry_attempts = retry_attempts
50
+ @retry_delay = retry_delay
51
+ @max_retry_delay = max_retry_delay
52
+ @backoff_multiplier = backoff_multiplier
53
+ @endpoint_url = endpoint_url
54
+ @authentication = authentication || {}
55
+ @headers = headers || {}
56
+ @user_agent = user_agent || "a2a-ruby/#{A2A::VERSION}"
57
+
58
+ validate!
59
+ end
60
+
61
+ ##
62
+ # Check if streaming is enabled
63
+ #
64
+ # @return [Boolean] True if streaming is enabled
65
+ def streaming?
66
+ @streaming
67
+ end
68
+
69
+ ##
70
+ # Check if polling is enabled
71
+ #
72
+ # @return [Boolean] True if polling is enabled
73
+ def polling?
74
+ @polling
75
+ end
76
+
77
+ ##
78
+ # Check if client preference should be used for transport negotiation
79
+ #
80
+ # @return [Boolean] True if client preference should be used
81
+ def use_client_preference?
82
+ @use_client_preference
83
+ end
84
+
85
+ ##
86
+ # Get the preferred transport protocol
87
+ #
88
+ # @return [String] The preferred transport protocol
89
+ def preferred_transport
90
+ @supported_transports.first
91
+ end
92
+
93
+ ##
94
+ # Check if a transport is supported
95
+ #
96
+ # @param transport [String] The transport to check
97
+ # @return [Boolean] True if the transport is supported
98
+ def supports_transport?(transport)
99
+ @supported_transports.include?(transport)
100
+ end
101
+
102
+ ##
103
+ # Add a supported transport
104
+ #
105
+ # @param transport [String] The transport to add
106
+ def add_transport(transport)
107
+ @supported_transports << transport unless @supported_transports.include?(transport)
108
+ end
109
+
110
+ ##
111
+ # Remove a supported transport
112
+ #
113
+ # @param transport [String] The transport to remove
114
+ def remove_transport(transport)
115
+ @supported_transports.delete(transport)
116
+ end
117
+
118
+ ##
119
+ # Get authentication configuration for a specific type
120
+ #
121
+ # @param type [String] The authentication type
122
+ # @return [Hash, nil] The authentication configuration
123
+ def auth_config(type)
124
+ @authentication[type]
125
+ end
126
+
127
+ ##
128
+ # Set authentication configuration
129
+ #
130
+ # @param type [String] The authentication type
131
+ # @param config [Hash] The authentication configuration
132
+ def set_auth_config(type, config)
133
+ @authentication[type] = config
134
+ end
135
+
136
+ ##
137
+ # Get all HTTP headers including authentication headers
138
+ #
139
+ # @return [Hash] All HTTP headers
140
+ def all_headers
141
+ auth_headers = build_auth_headers
142
+ @headers.merge(auth_headers)
143
+ end
144
+
145
+ ##
146
+ # Create a copy of the configuration
147
+ #
148
+ # @return [Config] A new configuration instance
149
+ def dup
150
+ self.class.new(
151
+ streaming: @streaming,
152
+ polling: @polling,
153
+ supported_transports: @supported_transports.dup,
154
+ use_client_preference: @use_client_preference,
155
+ accepted_output_modes: @accepted_output_modes.dup,
156
+ push_notification_configs: @push_notification_configs.dup,
157
+ timeout: @timeout,
158
+ retry_attempts: @retry_attempts,
159
+ retry_delay: @retry_delay,
160
+ max_retry_delay: @max_retry_delay,
161
+ backoff_multiplier: @backoff_multiplier,
162
+ endpoint_url: @endpoint_url,
163
+ authentication: @authentication.dup,
164
+ headers: @headers.dup,
165
+ user_agent: @user_agent
166
+ )
167
+ end
168
+
169
+ private
170
+
171
+ def validate!
172
+ raise ArgumentError, "timeout must be positive" if @timeout <= 0
173
+ raise ArgumentError, "retry_attempts must be non-negative" if @retry_attempts.negative?
174
+ raise ArgumentError, "retry_delay must be positive" if @retry_delay <= 0
175
+ raise ArgumentError, "max_retry_delay must be positive" if @max_retry_delay <= 0
176
+ raise ArgumentError, "backoff_multiplier must be positive" if @backoff_multiplier <= 0
177
+
178
+ @supported_transports.each do |transport|
179
+ raise ArgumentError, "unsupported transport: #{transport}" unless A2A::Types::VALID_TRANSPORTS.include?(transport)
180
+ end
181
+ end
182
+
183
+ def build_auth_headers
184
+ headers = {}
185
+
186
+ # Add API key authentication
187
+ if (api_key_config = @authentication["api_key"])
188
+ case api_key_config["in"]
189
+ when "header"
190
+ headers[api_key_config["name"]] = api_key_config["value"]
191
+ end
192
+ end
193
+
194
+ # Add bearer token authentication
195
+ if (bearer_config = @authentication["bearer"])
196
+ headers["Authorization"] = "Bearer #{bearer_config['token']}"
197
+ end
198
+
199
+ # Add basic authentication
200
+ if (basic_config = @authentication["basic"])
201
+ require "base64"
202
+ credentials = Base64.strict_encode64("#{basic_config['username']}:#{basic_config['password']}")
203
+ headers["Authorization"] = "Basic #{credentials}"
204
+ end
205
+
206
+ headers
207
+ end
208
+ end
209
+ end
210
+ end