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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +137 -0
- data/.simplecov +46 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +33 -0
- data/CODE_OF_CONDUCT.md +128 -0
- data/CONTRIBUTING.md +165 -0
- data/Gemfile +43 -0
- data/Guardfile +34 -0
- data/LICENSE.txt +21 -0
- data/PUBLISHING_CHECKLIST.md +214 -0
- data/README.md +171 -0
- data/Rakefile +165 -0
- data/docs/agent_execution.md +309 -0
- data/docs/api_reference.md +792 -0
- data/docs/configuration.md +780 -0
- data/docs/events.md +475 -0
- data/docs/getting_started.md +668 -0
- data/docs/integration.md +262 -0
- data/docs/server_apps.md +621 -0
- data/docs/troubleshooting.md +765 -0
- data/lib/a2a/client/api_methods.rb +263 -0
- data/lib/a2a/client/auth/api_key.rb +161 -0
- data/lib/a2a/client/auth/interceptor.rb +288 -0
- data/lib/a2a/client/auth/jwt.rb +189 -0
- data/lib/a2a/client/auth/oauth2.rb +146 -0
- data/lib/a2a/client/auth.rb +137 -0
- data/lib/a2a/client/base.rb +316 -0
- data/lib/a2a/client/config.rb +210 -0
- data/lib/a2a/client/connection_pool.rb +233 -0
- data/lib/a2a/client/http_client.rb +524 -0
- data/lib/a2a/client/json_rpc_handler.rb +136 -0
- data/lib/a2a/client/middleware/circuit_breaker_interceptor.rb +245 -0
- data/lib/a2a/client/middleware/logging_interceptor.rb +371 -0
- data/lib/a2a/client/middleware/rate_limit_interceptor.rb +142 -0
- data/lib/a2a/client/middleware/retry_interceptor.rb +161 -0
- data/lib/a2a/client/middleware.rb +116 -0
- data/lib/a2a/client/performance_tracker.rb +60 -0
- data/lib/a2a/configuration/defaults.rb +34 -0
- data/lib/a2a/configuration/environment_loader.rb +76 -0
- data/lib/a2a/configuration/file_loader.rb +115 -0
- data/lib/a2a/configuration/inheritance.rb +101 -0
- data/lib/a2a/configuration/validator.rb +180 -0
- data/lib/a2a/configuration.rb +201 -0
- data/lib/a2a/errors.rb +291 -0
- data/lib/a2a/modules.rb +50 -0
- data/lib/a2a/monitoring/alerting.rb +490 -0
- data/lib/a2a/monitoring/distributed_tracing.rb +398 -0
- data/lib/a2a/monitoring/health_endpoints.rb +204 -0
- data/lib/a2a/monitoring/metrics_collector.rb +438 -0
- data/lib/a2a/monitoring.rb +463 -0
- data/lib/a2a/plugin.rb +358 -0
- data/lib/a2a/plugin_manager.rb +159 -0
- data/lib/a2a/plugins/example_auth.rb +81 -0
- data/lib/a2a/plugins/example_middleware.rb +118 -0
- data/lib/a2a/plugins/example_transport.rb +76 -0
- data/lib/a2a/protocol/agent_card.rb +8 -0
- data/lib/a2a/protocol/agent_card_server.rb +584 -0
- data/lib/a2a/protocol/capability.rb +496 -0
- data/lib/a2a/protocol/json_rpc.rb +254 -0
- data/lib/a2a/protocol/message.rb +8 -0
- data/lib/a2a/protocol/task.rb +8 -0
- data/lib/a2a/rails/a2a_controller.rb +258 -0
- data/lib/a2a/rails/controller_helpers.rb +499 -0
- data/lib/a2a/rails/engine.rb +167 -0
- data/lib/a2a/rails/generators/agent_generator.rb +311 -0
- data/lib/a2a/rails/generators/install_generator.rb +209 -0
- data/lib/a2a/rails/generators/migration_generator.rb +232 -0
- data/lib/a2a/rails/generators/templates/add_a2a_indexes.rb +57 -0
- data/lib/a2a/rails/generators/templates/agent_controller.rb +122 -0
- data/lib/a2a/rails/generators/templates/agent_controller_spec.rb +160 -0
- data/lib/a2a/rails/generators/templates/agent_readme.md +200 -0
- data/lib/a2a/rails/generators/templates/create_a2a_push_notification_configs.rb +68 -0
- data/lib/a2a/rails/generators/templates/create_a2a_tasks.rb +83 -0
- data/lib/a2a/rails/generators/templates/example_agent_controller.rb +228 -0
- data/lib/a2a/rails/generators/templates/initializer.rb +108 -0
- data/lib/a2a/rails/generators/templates/push_notification_config_model.rb +228 -0
- data/lib/a2a/rails/generators/templates/task_model.rb +200 -0
- data/lib/a2a/rails/tasks/a2a.rake +228 -0
- data/lib/a2a/server/a2a_methods.rb +520 -0
- data/lib/a2a/server/agent.rb +537 -0
- data/lib/a2a/server/agent_execution/agent_executor.rb +279 -0
- data/lib/a2a/server/agent_execution/request_context.rb +219 -0
- data/lib/a2a/server/apps/rack_app.rb +311 -0
- data/lib/a2a/server/apps/sinatra_app.rb +261 -0
- data/lib/a2a/server/default_request_handler.rb +350 -0
- data/lib/a2a/server/events/event_consumer.rb +116 -0
- data/lib/a2a/server/events/event_queue.rb +226 -0
- data/lib/a2a/server/example_agent.rb +248 -0
- data/lib/a2a/server/handler.rb +281 -0
- data/lib/a2a/server/middleware/authentication_middleware.rb +212 -0
- data/lib/a2a/server/middleware/cors_middleware.rb +171 -0
- data/lib/a2a/server/middleware/logging_middleware.rb +362 -0
- data/lib/a2a/server/middleware/rate_limit_middleware.rb +382 -0
- data/lib/a2a/server/middleware.rb +213 -0
- data/lib/a2a/server/push_notification_manager.rb +327 -0
- data/lib/a2a/server/request_handler.rb +136 -0
- data/lib/a2a/server/storage/base.rb +141 -0
- data/lib/a2a/server/storage/database.rb +266 -0
- data/lib/a2a/server/storage/memory.rb +274 -0
- data/lib/a2a/server/storage/redis.rb +320 -0
- data/lib/a2a/server/storage.rb +38 -0
- data/lib/a2a/server/task_manager.rb +534 -0
- data/lib/a2a/transport/grpc.rb +481 -0
- data/lib/a2a/transport/http.rb +415 -0
- data/lib/a2a/transport/sse.rb +499 -0
- data/lib/a2a/types/agent_card.rb +540 -0
- data/lib/a2a/types/artifact.rb +99 -0
- data/lib/a2a/types/base_model.rb +223 -0
- data/lib/a2a/types/events.rb +117 -0
- data/lib/a2a/types/message.rb +106 -0
- data/lib/a2a/types/part.rb +288 -0
- data/lib/a2a/types/push_notification.rb +139 -0
- data/lib/a2a/types/security.rb +167 -0
- data/lib/a2a/types/task.rb +154 -0
- data/lib/a2a/types.rb +88 -0
- data/lib/a2a/utils/helpers.rb +245 -0
- data/lib/a2a/utils/message_buffer.rb +278 -0
- data/lib/a2a/utils/performance.rb +247 -0
- data/lib/a2a/utils/rails_detection.rb +97 -0
- data/lib/a2a/utils/structured_logger.rb +306 -0
- data/lib/a2a/utils/time_helpers.rb +167 -0
- data/lib/a2a/utils/validation.rb +8 -0
- data/lib/a2a/version.rb +6 -0
- data/lib/a2a-rails.rb +58 -0
- data/lib/a2a.rb +198 -0
- 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
|