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,540 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module A2A
6
+ module Types
7
+ ##
8
+ # Represents an agent card in the A2A protocol
9
+ #
10
+ # Agent cards describe an agent's capabilities, interfaces, and metadata.
11
+ # They are used for agent discovery and capability negotiation.
12
+ #
13
+ # @example Creating a basic agent card
14
+ # card = A2A::Types::AgentCard.new(
15
+ # name: "My Agent",
16
+ # description: "A helpful agent",
17
+ # version: "1.0.0",
18
+ # url: "https://example.com/agent",
19
+ # preferred_transport: "JSONRPC",
20
+ # skills: [skill],
21
+ # capabilities: capabilities,
22
+ # default_input_modes: ["text"],
23
+ # default_output_modes: ["text"]
24
+ # )
25
+ #
26
+ class AgentCard < A2A::Types::BaseModel
27
+ attr_reader :name, :description, :version, :url, :preferred_transport,
28
+ :skills, :capabilities, :default_input_modes, :default_output_modes,
29
+ :additional_interfaces, :security, :security_schemes, :provider,
30
+ :protocol_version, :supports_authenticated_extended_card,
31
+ :signatures, :documentation_url, :icon_url
32
+
33
+ ##
34
+ # Initialize a new agent card
35
+ #
36
+ # @param name [String] Agent name (required)
37
+ # @param description [String] Agent description (required)
38
+ # @param version [String] Agent version (required)
39
+ # @param url [String] Primary agent URL (required)
40
+ # @param preferred_transport [String] Preferred transport protocol (required)
41
+ # @param skills [Array<AgentSkill>] Agent skills (required)
42
+ # @param capabilities [AgentCapabilities] Agent capabilities (required)
43
+ # @param default_input_modes [Array<String>] Default input modes (required)
44
+ # @param default_output_modes [Array<String>] Default output modes (required)
45
+ # @param additional_interfaces [Array<AgentInterface>, nil] Additional interfaces
46
+ # @param security [Array<String>, nil] Security requirements
47
+ # @param security_schemes [Hash<String, SecurityScheme>, nil] Security scheme definitions
48
+ # @param provider [String, nil] Provider information
49
+ # @param protocol_version [String, nil] A2A protocol version
50
+ # @param supports_authenticated_extended_card [Boolean, nil] Extended card support
51
+ # @param signatures [Array<AgentCardSignature>, nil] JWS signatures
52
+ # @param documentation_url [String, nil] Documentation URL
53
+ # @param icon_url [String, nil] Icon URL
54
+ def initialize(name:, description:, version:, url:, preferred_transport:, skills:,
55
+ capabilities:, default_input_modes:, default_output_modes:,
56
+ additional_interfaces: nil, security: nil, security_schemes: nil,
57
+ provider: nil, protocol_version: nil, supports_authenticated_extended_card: nil,
58
+ signatures: nil, documentation_url: nil, icon_url: nil)
59
+ @name = name
60
+ @description = description
61
+ @version = version
62
+ @url = url
63
+ @preferred_transport = preferred_transport
64
+ @skills = skills.map { |s| s.is_a?(AgentSkill) ? s : AgentSkill.from_h(s) }
65
+ @capabilities = capabilities.is_a?(AgentCapabilities) ? capabilities : AgentCapabilities.from_h(capabilities)
66
+ @default_input_modes = default_input_modes
67
+ @default_output_modes = default_output_modes
68
+ @additional_interfaces = additional_interfaces&.map do |i|
69
+ i.is_a?(AgentInterface) ? i : AgentInterface.from_h(i)
70
+ end
71
+ @security = security
72
+ @security_schemes = process_security_schemes(security_schemes)
73
+ @provider = provider
74
+ @protocol_version = protocol_version || "1.0"
75
+ @supports_authenticated_extended_card = supports_authenticated_extended_card
76
+ @signatures = signatures&.map { |s| s.is_a?(AgentCardSignature) ? s : AgentCardSignature.from_h(s) }
77
+ @documentation_url = documentation_url
78
+ @icon_url = icon_url
79
+
80
+ validate!
81
+ end
82
+
83
+ ##
84
+ # Get all available interfaces (primary + additional)
85
+ #
86
+ # @return [Array<AgentInterface>] All interfaces
87
+ def all_interfaces
88
+ interfaces = [AgentInterface.new(transport: @preferred_transport, url: @url)]
89
+ interfaces.concat(@additional_interfaces) if @additional_interfaces
90
+ interfaces
91
+ end
92
+
93
+ ##
94
+ # Check if the agent supports a specific transport
95
+ #
96
+ # @param transport [String] The transport to check
97
+ # @return [Boolean] True if supported
98
+ def supports_transport?(transport)
99
+ all_interfaces.any? { |i| i.transport == transport }
100
+ end
101
+
102
+ ##
103
+ # Get the URL for a specific transport
104
+ #
105
+ # @param transport [String] The transport to get URL for
106
+ # @return [String, nil] The URL or nil if not supported
107
+ def url_for_transport(transport)
108
+ interface = all_interfaces.find { |i| i.transport == transport }
109
+ interface&.url
110
+ end
111
+
112
+ private
113
+
114
+ ##
115
+ # Process security schemes hash into SecurityScheme objects
116
+ #
117
+ # @param schemes [Hash, nil] Security schemes hash
118
+ # @return [Hash<String, SecurityScheme>, nil] Processed security schemes
119
+ def process_security_schemes(schemes)
120
+ return nil if schemes.nil?
121
+ return schemes if schemes.is_a?(Hash) && schemes.values.all?(SecurityScheme)
122
+
123
+ processed = {}
124
+ schemes.each do |name, scheme_data|
125
+ processed[name.to_s] = if scheme_data.is_a?(SecurityScheme)
126
+ scheme_data
127
+ else
128
+ SecurityScheme.from_h(scheme_data)
129
+ end
130
+ end
131
+ processed
132
+ end
133
+
134
+ def validate!
135
+ validate_required(:name, :description, :version, :url, :preferred_transport,
136
+ :skills, :capabilities, :default_input_modes, :default_output_modes)
137
+ validate_inclusion(:preferred_transport, VALID_TRANSPORTS)
138
+ validate_array_type(:skills, AgentSkill)
139
+ validate_type(:capabilities, AgentCapabilities)
140
+ validate_array_type(:additional_interfaces, AgentInterface) if @additional_interfaces
141
+ validate_array_type(:signatures, AgentCardSignature) if @signatures
142
+ validate_array_type(:default_input_modes, String)
143
+ validate_array_type(:default_output_modes, String)
144
+ validate_array_type(:security, String) if @security
145
+ validate_security_schemes if @security_schemes
146
+ if @supports_authenticated_extended_card
147
+ validate_type(:supports_authenticated_extended_card,
148
+ [TrueClass, FalseClass])
149
+ end
150
+ validate_url_format(:url)
151
+ validate_url_format(:documentation_url) if @documentation_url
152
+ validate_url_format(:icon_url) if @icon_url
153
+ end
154
+
155
+ ##
156
+ # Validate security schemes
157
+ def validate_security_schemes
158
+ validate_type(:security_schemes, Hash)
159
+ @security_schemes.each do |name, scheme|
160
+ raise ArgumentError, "security_schemes[#{name}] must be a SecurityScheme" unless scheme.is_a?(SecurityScheme)
161
+ end
162
+ end
163
+
164
+ ##
165
+ # Validate URL format
166
+ #
167
+ # @param field [Symbol] The field name containing the URL
168
+ def validate_url_format(field)
169
+ value = instance_variable_get("@#{field}")
170
+ return if value.nil?
171
+
172
+ validate_type(field, String)
173
+
174
+ begin
175
+ uri = URI.parse(value)
176
+ raise ArgumentError, "#{field} must be a valid HTTP or HTTPS URL" unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
177
+ rescue URI::InvalidURIError
178
+ raise ArgumentError, "#{field} must be a valid URL"
179
+ end
180
+ end
181
+ end
182
+
183
+ ##
184
+ # Represents an agent skill
185
+ #
186
+ # Skills define specific capabilities that an agent can perform,
187
+ # including supported input/output modes and security requirements.
188
+ #
189
+ # @example Creating a skill
190
+ # skill = A2A::Types::AgentSkill.new(
191
+ # id: "text_analysis",
192
+ # name: "Text Analysis",
193
+ # description: "Analyze and process text content",
194
+ # tags: ["nlp", "analysis"],
195
+ # examples: [
196
+ # {
197
+ # input: "Analyze this text",
198
+ # output: "Analysis results..."
199
+ # }
200
+ # ],
201
+ # input_modes: ["text"],
202
+ # output_modes: ["text", "data"],
203
+ # security: ["api_key"]
204
+ # )
205
+ #
206
+ class AgentSkill < A2A::Types::BaseModel
207
+ attr_reader :id, :name, :description, :tags, :examples, :input_modes, :output_modes, :security
208
+
209
+ ##
210
+ # Initialize a new agent skill
211
+ #
212
+ # @param id [String] Skill identifier (required)
213
+ # @param name [String] Skill name (required)
214
+ # @param description [String] Skill description (required)
215
+ # @param tags [Array<String>, nil] Skill tags for categorization
216
+ # @param examples [Array<Hash>, nil] Usage examples with input/output
217
+ # @param input_modes [Array<String>, nil] Supported input modes (text, file, data)
218
+ # @param output_modes [Array<String>, nil] Supported output modes (text, file, data)
219
+ # @param security [Array<String>, nil] Security requirements for this skill
220
+ def initialize(id:, name:, description:, tags: nil, examples: nil,
221
+ input_modes: nil, output_modes: nil, security: nil)
222
+ @id = id
223
+ @name = name
224
+ @description = description
225
+ @tags = tags
226
+ @examples = examples
227
+ @input_modes = input_modes
228
+ @output_modes = output_modes
229
+ @security = security
230
+
231
+ validate!
232
+ end
233
+
234
+ ##
235
+ # Check if the skill supports a specific input mode
236
+ #
237
+ # @param mode [String] The input mode to check
238
+ # @return [Boolean] True if supported
239
+ def supports_input_mode?(mode)
240
+ return true if @input_modes.nil? # nil means all modes supported
241
+
242
+ @input_modes.include?(mode)
243
+ end
244
+
245
+ ##
246
+ # Check if the skill supports a specific output mode
247
+ #
248
+ # @param mode [String] The output mode to check
249
+ # @return [Boolean] True if supported
250
+ def supports_output_mode?(mode)
251
+ return true if @output_modes.nil? # nil means all modes supported
252
+
253
+ @output_modes.include?(mode)
254
+ end
255
+
256
+ ##
257
+ # Check if the skill has a specific security requirement
258
+ #
259
+ # @param requirement [String] The security requirement to check
260
+ # @return [Boolean] True if required
261
+ def requires_security?(requirement)
262
+ return false if @security.nil?
263
+
264
+ @security.include?(requirement)
265
+ end
266
+
267
+ private
268
+
269
+ def validate!
270
+ validate_required(:id, :name, :description)
271
+ validate_type(:id, String)
272
+ validate_type(:name, String)
273
+ validate_type(:description, String)
274
+ validate_array_type(:tags, String) if @tags
275
+ validate_array_type(:input_modes, String) if @input_modes
276
+ validate_array_type(:output_modes, String) if @output_modes
277
+ validate_array_type(:security, String) if @security
278
+ validate_examples if @examples
279
+ end
280
+
281
+ ##
282
+ # Validate examples structure
283
+ def validate_examples
284
+ validate_type(:examples, Array)
285
+ @examples.each_with_index do |example, index|
286
+ raise ArgumentError, "examples[#{index}] must be a Hash" unless example.is_a?(Hash)
287
+
288
+ # Examples should have at least input or description
289
+ unless example.key?(:input) || example.key?("input") ||
290
+ example.key?(:description) || example.key?("description")
291
+ raise ArgumentError, "examples[#{index}] must have input or description"
292
+ end
293
+ end
294
+ end
295
+ end
296
+
297
+ ##
298
+ # Represents agent capabilities
299
+ #
300
+ # Capabilities define what features and protocols the agent supports,
301
+ # such as streaming, push notifications, and extensions.
302
+ #
303
+ # @example Creating capabilities
304
+ # capabilities = A2A::Types::AgentCapabilities.new(
305
+ # streaming: true,
306
+ # push_notifications: true,
307
+ # state_transition_history: false,
308
+ # extensions: ["custom-extension-1"]
309
+ # )
310
+ #
311
+ class AgentCapabilities < A2A::Types::BaseModel
312
+ attr_reader :streaming, :push_notifications, :state_transition_history, :extensions
313
+
314
+ ##
315
+ # Initialize new agent capabilities
316
+ #
317
+ # @param streaming [Boolean, nil] Whether the agent supports streaming responses
318
+ # @param push_notifications [Boolean, nil] Whether the agent supports push notifications
319
+ # @param state_transition_history [Boolean, nil] Whether the agent maintains state history
320
+ # @param extensions [Array<String>, nil] List of supported extension URIs
321
+ def initialize(streaming: nil, push_notifications: nil, state_transition_history: nil, extensions: nil)
322
+ @streaming = streaming
323
+ @push_notifications = push_notifications
324
+ @state_transition_history = state_transition_history
325
+ @extensions = extensions
326
+
327
+ validate!
328
+ end
329
+
330
+ ##
331
+ # Check if streaming is supported
332
+ #
333
+ # @return [Boolean] True if streaming is supported
334
+ def streaming?
335
+ @streaming == true
336
+ end
337
+
338
+ ##
339
+ # Check if push notifications are supported
340
+ #
341
+ # @return [Boolean] True if push notifications are supported
342
+ def push_notifications?
343
+ @push_notifications == true
344
+ end
345
+
346
+ ##
347
+ # Check if state transition history is supported
348
+ #
349
+ # @return [Boolean] True if state history is supported
350
+ def state_transition_history?
351
+ @state_transition_history == true
352
+ end
353
+
354
+ ##
355
+ # Check if a specific extension is supported
356
+ #
357
+ # @param extension_uri [String] The extension URI to check
358
+ # @return [Boolean] True if the extension is supported
359
+ def supports_extension?(extension_uri)
360
+ return false if @extensions.nil?
361
+
362
+ @extensions.include?(extension_uri)
363
+ end
364
+
365
+ private
366
+
367
+ def validate!
368
+ validate_type(:streaming, [TrueClass, FalseClass]) if @streaming
369
+ validate_type(:push_notifications, [TrueClass, FalseClass]) if @push_notifications
370
+ validate_type(:state_transition_history, [TrueClass, FalseClass]) if @state_transition_history
371
+ validate_array_type(:extensions, String) if @extensions
372
+ end
373
+ end
374
+
375
+ ##
376
+ # Represents an agent interface
377
+ #
378
+ # Interfaces define the transport protocols and URLs that can be used
379
+ # to communicate with the agent.
380
+ #
381
+ # @example Creating an interface
382
+ # interface = A2A::Types::AgentInterface.new(
383
+ # transport: "JSONRPC",
384
+ # url: "https://example.com/agent/rpc"
385
+ # )
386
+ #
387
+ class AgentInterface < A2A::Types::BaseModel
388
+ attr_reader :transport, :url
389
+
390
+ ##
391
+ # Initialize a new agent interface
392
+ #
393
+ # @param transport [String] Transport protocol (JSONRPC, GRPC, HTTP+JSON)
394
+ # @param url [String] Interface URL
395
+ def initialize(transport:, url:)
396
+ @transport = transport
397
+ @url = url
398
+ validate!
399
+ end
400
+
401
+ ##
402
+ # Check if this interface uses the specified transport
403
+ #
404
+ # @param transport_type [String] The transport type to check
405
+ # @return [Boolean] True if this interface uses the transport
406
+ def uses_transport?(transport_type)
407
+ @transport == transport_type
408
+ end
409
+
410
+ ##
411
+ # Check if this is a secure interface (HTTPS)
412
+ #
413
+ # @return [Boolean] True if the URL uses HTTPS
414
+ def secure?
415
+ @url.start_with?("https://")
416
+ end
417
+
418
+ private
419
+
420
+ def validate!
421
+ validate_required(:transport, :url)
422
+ validate_inclusion(:transport, VALID_TRANSPORTS)
423
+ validate_type(:url, String)
424
+ validate_url_format
425
+ end
426
+
427
+ ##
428
+ # Validate URL format
429
+ def validate_url_format
430
+ uri = URI.parse(@url)
431
+ raise ArgumentError, "url must be a valid HTTP or HTTPS URL" unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
432
+ rescue URI::InvalidURIError
433
+ raise ArgumentError, "url must be a valid URL"
434
+ end
435
+ end
436
+
437
+ ##
438
+ # Represents an agent card signature
439
+ #
440
+ # Signatures provide cryptographic verification of agent cards using
441
+ # JSON Web Signature (JWS) format.
442
+ #
443
+ # @example Creating a signature
444
+ # signature = A2A::Types::AgentCardSignature.new(
445
+ # signature: "eyJhbGciOiJSUzI1NiJ9...",
446
+ # protected_header: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
447
+ # )
448
+ #
449
+ class AgentCardSignature < A2A::Types::BaseModel
450
+ attr_reader :signature, :protected_header
451
+
452
+ ##
453
+ # Initialize a new agent card signature
454
+ #
455
+ # @param signature [String] JWS signature value (base64url encoded)
456
+ # @param protected_header [String] JWS protected header (base64url encoded JSON)
457
+ def initialize(signature:, protected_header:)
458
+ @signature = signature
459
+ @protected_header = protected_header
460
+ validate!
461
+ end
462
+
463
+ ##
464
+ # Decode the protected header
465
+ #
466
+ # @return [Hash] The decoded header as a hash
467
+ def decoded_header
468
+ require "base64"
469
+ require "json"
470
+
471
+ # Add padding if needed for base64url decoding
472
+ padded = @protected_header
473
+ case padded.length % 4
474
+ when 2
475
+ padded += "=="
476
+ when 3
477
+ padded += "="
478
+ end
479
+
480
+ decoded = Base64.urlsafe_decode64(padded)
481
+ JSON.parse(decoded)
482
+ rescue StandardError => e
483
+ raise ArgumentError, "Invalid protected header: #{e.message}"
484
+ end
485
+
486
+ ##
487
+ # Get the algorithm from the protected header
488
+ #
489
+ # @return [String, nil] The algorithm or nil if not present
490
+ def algorithm
491
+ decoded_header["alg"]
492
+ rescue StandardError
493
+ nil
494
+ end
495
+
496
+ ##
497
+ # Check if the signature uses a specific algorithm
498
+ #
499
+ # @param alg [String] The algorithm to check
500
+ # @return [Boolean] True if the signature uses the algorithm
501
+ def uses_algorithm?(alg)
502
+ algorithm == alg
503
+ end
504
+
505
+ private
506
+
507
+ def validate!
508
+ validate_required(:signature, :protected_header)
509
+ validate_type(:signature, String)
510
+ validate_type(:protected_header, String)
511
+ validate_base64url_format(:signature)
512
+ validate_base64url_format(:protected_header)
513
+ validate_protected_header_content
514
+ end
515
+
516
+ ##
517
+ # Validate base64url format
518
+ #
519
+ # @param field [Symbol] The field name
520
+ def validate_base64url_format(field)
521
+ value = instance_variable_get("@#{field}")
522
+ return if value.match?(/\A[A-Za-z0-9_-]+\z/)
523
+
524
+ raise ArgumentError, "#{field} must be valid base64url encoded"
525
+ end
526
+
527
+ ##
528
+ # Validate protected header content
529
+ def validate_protected_header_content
530
+ header = decoded_header
531
+ unless header.is_a?(Hash) && header["alg"]
532
+ raise ArgumentError,
533
+ "protected_header must contain a valid algorithm"
534
+ end
535
+ rescue StandardError => e
536
+ raise ArgumentError, "protected_header must be valid base64url encoded JSON: #{e.message}"
537
+ end
538
+ end
539
+ end
540
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Represents an artifact in the A2A protocol
5
+ #
6
+ # Artifacts are outputs or intermediate results produced by agents during task execution.
7
+ # They can contain multiple parts (text, files, or data) and have associated metadata.
8
+ #
9
+ module A2A
10
+ module Types
11
+ class Artifact < A2A::Types::BaseModel
12
+ attr_reader :artifact_id, :name, :description, :parts, :metadata, :extensions
13
+
14
+ ##
15
+ # Initialize a new artifact
16
+ #
17
+ # @param artifact_id [String] Unique artifact identifier
18
+ # @param parts [Array<Part>] Artifact parts
19
+ # @param name [String, nil] Optional artifact name
20
+ # @param description [String, nil] Optional artifact description
21
+ # @param metadata [Hash, nil] Additional metadata
22
+ # @param extensions [Array<Hash>, nil] Protocol extensions
23
+ def initialize(artifact_id:, parts:, name: nil, description: nil, metadata: nil, extensions: nil)
24
+ @artifact_id = artifact_id
25
+ @parts = parts.map { |p| p.is_a?(Part) ? p : Part.from_h(p) }
26
+ @name = name
27
+ @description = description
28
+ @metadata = metadata
29
+ @extensions = extensions
30
+
31
+ validate!
32
+ end
33
+
34
+ ##
35
+ # Get all text content from the artifact
36
+ #
37
+ # @return [String] Combined text from all text parts
38
+ def text_content
39
+ @parts.select { |p| p.is_a?(TextPart) }
40
+ .map(&:text)
41
+ .join("\n")
42
+ end
43
+
44
+ ##
45
+ # Get all file parts from the artifact
46
+ #
47
+ # @return [Array<FilePart>] All file parts
48
+ def file_parts
49
+ @parts.select { |p| p.is_a?(FilePart) }
50
+ end
51
+
52
+ ##
53
+ # Get all data parts from the artifact
54
+ #
55
+ # @return [Array<DataPart>] All data parts
56
+ def data_parts
57
+ @parts.select { |p| p.is_a?(DataPart) }
58
+ end
59
+
60
+ ##
61
+ # Add a part to the artifact
62
+ #
63
+ # @param part [Part] The part to add
64
+ def add_part(part)
65
+ @parts << part
66
+ end
67
+
68
+ ##
69
+ # Check if the artifact has any content
70
+ #
71
+ # @return [Boolean] True if the artifact has parts
72
+ def has_content?
73
+ !@parts.empty?
74
+ end
75
+
76
+ ##
77
+ # Get the total size of all file parts
78
+ #
79
+ # @return [Integer] Total size in bytes
80
+ def total_file_size
81
+ file_parts.sum do |file_part|
82
+ file_part.file.respond_to?(:size) ? file_part.file.size : 0
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def validate!
89
+ validate_required(:artifact_id, :parts)
90
+ validate_type(:artifact_id, String)
91
+ validate_array_type(:parts, Part)
92
+
93
+ return unless @parts.empty?
94
+
95
+ raise ArgumentError, "Artifact must have at least one part"
96
+ end
97
+ end
98
+ end
99
+ end