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,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module A2A
6
+ module Types
7
+ end
8
+ end
9
+
10
+ ##
11
+ # Base class for all A2A protocol types
12
+ #
13
+ # Provides common functionality for validation, serialization, and
14
+ # camelCase/snake_case conversion for A2A protocol compatibility.
15
+ #
16
+ module A2A
17
+ module Types
18
+ class BaseModel
19
+ ##
20
+ # Initialize a new model instance
21
+ #
22
+ # @param attributes [Hash] The attributes to set
23
+ def initialize(**attributes)
24
+ # Set instance variables for all provided attributes
25
+ attributes.each do |key, value|
26
+ instance_variable_set("@#{key}", value)
27
+ end
28
+
29
+ # Validate the instance after initialization
30
+ validate! if respond_to?(:validate!, true)
31
+ end
32
+
33
+ ##
34
+ # Convert the model to a hash representation
35
+ #
36
+ # @param camel_case [Boolean] Whether to convert keys to camelCase
37
+ # @return [Hash] The model as a hash
38
+ def to_h(camel_case: true)
39
+ hash = {}
40
+
41
+ instance_variables.each do |var|
42
+ key = var.to_s.delete("@")
43
+ value = instance_variable_get(var)
44
+
45
+ # Convert nested models
46
+ case value
47
+ when BaseModel
48
+ value = value.to_h(camel_case: camel_case)
49
+ when Array
50
+ value = value.map do |item|
51
+ item.is_a?(BaseModel) ? item.to_h(camel_case: camel_case) : item
52
+ end
53
+ when Hash
54
+ value = value.transform_values do |v|
55
+ v.is_a?(BaseModel) ? v.to_h(camel_case: camel_case) : v
56
+ end
57
+ end
58
+
59
+ # Convert key to camelCase if requested
60
+ key = camelize(key) if camel_case
61
+ hash[key] = value unless value.nil?
62
+ end
63
+
64
+ hash
65
+ end
66
+
67
+ ##
68
+ # Create an instance from a hash
69
+ #
70
+ # @param hash [Hash] The hash to create from
71
+ # @return [BaseModel] The new instance
72
+ def self.from_h(hash)
73
+ return hash if hash.is_a?(self) # Already an instance of this class
74
+ return nil if hash.nil?
75
+
76
+ # Convert string keys to symbols and snake_case camelCase keys
77
+ normalized_hash = {}
78
+ hash.each do |key, value|
79
+ snake_key = underscore(key.to_s).to_sym
80
+ normalized_hash[snake_key] = value
81
+ end
82
+
83
+ new(**normalized_hash)
84
+ end
85
+
86
+ ##
87
+ # Convert to JSON string
88
+ #
89
+ # @param options [Hash] JSON generation options
90
+ # @return [String] The JSON representation
91
+ def to_json(**options)
92
+ require "json"
93
+ to_h.to_json(**options)
94
+ end
95
+
96
+ ##
97
+ # Create an instance from JSON string
98
+ #
99
+ # @param json_string [String] The JSON string
100
+ # @return [BaseModel] The new instance
101
+ def self.from_json(json_string)
102
+ require "json"
103
+ hash = JSON.parse(json_string)
104
+ from_h(hash)
105
+ end
106
+
107
+ ##
108
+ # Check equality with another model
109
+ #
110
+ # @param other [Object] The other object to compare
111
+ # @return [Boolean] True if equal
112
+ def ==(other)
113
+ return false unless other.is_a?(self.class)
114
+
115
+ to_h == other.to_h
116
+ end
117
+
118
+ ##
119
+ # Generate hash code for the model
120
+ #
121
+ # @return [Integer] The hash code
122
+ def hash
123
+ to_h.hash
124
+ end
125
+
126
+ ##
127
+ # Check if the model is valid
128
+ #
129
+ # @return [Boolean] True if valid
130
+ def valid?
131
+ validate!
132
+ true
133
+ rescue StandardError
134
+ false
135
+ end
136
+
137
+ private
138
+
139
+ ##
140
+ # Convert snake_case to camelCase
141
+ #
142
+ # @param string [String] The string to convert
143
+ # @return [String] The camelCase string
144
+ def camelize(string)
145
+ string.to_s.gsub(/_([a-z])/) { ::Regexp.last_match(1).upcase }
146
+ end
147
+
148
+ ##
149
+ # Convert camelCase to snake_case
150
+ #
151
+ # @param string [String] The string to convert
152
+ # @return [String] The snake_case string
153
+ def self.underscore(string)
154
+ string.to_s
155
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
156
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
157
+ .downcase
158
+ end
159
+
160
+ ##
161
+ # Validate required fields
162
+ #
163
+ # @param fields [Array<Symbol>] The required field names
164
+ # @raise [ArgumentError] If any required field is missing
165
+ def validate_required(*fields)
166
+ fields.each do |field|
167
+ value = instance_variable_get("@#{field}")
168
+ raise ArgumentError, "#{field} is required" if value.nil? || (value.respond_to?(:empty?) && value.empty?)
169
+ end
170
+ end
171
+
172
+ ##
173
+ # Validate that a field is one of the allowed values
174
+ #
175
+ # @param field [Symbol] The field name
176
+ # @param allowed_values [Array] The allowed values
177
+ # @raise [ArgumentError] If the field value is not allowed
178
+ def validate_inclusion(field, allowed_values)
179
+ value = instance_variable_get("@#{field}")
180
+ return if value.nil?
181
+
182
+ return if allowed_values.include?(value)
183
+
184
+ raise ArgumentError, "#{field} must be one of: #{allowed_values.join(', ')}"
185
+ end
186
+
187
+ ##
188
+ # Validate that a field is of the expected type
189
+ #
190
+ # @param field [Symbol] The field name
191
+ # @param expected_type [Class, Array<Class>] The expected type(s)
192
+ # @raise [ArgumentError] If the field is not of the expected type
193
+ def validate_type(field, expected_type)
194
+ value = instance_variable_get("@#{field}")
195
+ return if value.nil?
196
+
197
+ types = expected_type.is_a?(Array) ? expected_type : [expected_type]
198
+
199
+ return if types.any? { |type| value.is_a?(type) }
200
+
201
+ type_names = types.map(&:to_s).join(" or ")
202
+ raise ArgumentError, "#{field} must be a #{type_names}"
203
+ end
204
+
205
+ ##
206
+ # Validate that an array field contains only items of the expected type
207
+ #
208
+ # @param field [Symbol] The field name
209
+ # @param expected_type [Class] The expected item type
210
+ # @raise [ArgumentError] If any item is not of the expected type
211
+ def validate_array_type(field, expected_type)
212
+ value = instance_variable_get("@#{field}")
213
+ return if value.nil?
214
+
215
+ validate_type(field, Array)
216
+
217
+ value.each_with_index do |item, index|
218
+ raise ArgumentError, "#{field}[#{index}] must be a #{expected_type}" unless item.is_a?(expected_type)
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module A2A
4
+ module Types
5
+ ##
6
+ # Represents a task status update event
7
+ #
8
+ # These events are sent when a task's status changes, allowing clients
9
+ # to track task progress in real-time.
10
+ #
11
+ class TaskStatusUpdateEvent < A2A::Types::BaseModel
12
+ attr_reader :task_id, :context_id, :status, :metadata
13
+
14
+ ##
15
+ # Initialize a new task status update event
16
+ #
17
+ # @param task_id [String] The task identifier
18
+ # @param context_id [String] The context identifier
19
+ # @param status [TaskStatus, Hash] The new task status
20
+ # @param metadata [Hash, nil] Additional event metadata
21
+ def initialize(task_id:, context_id:, status:, metadata: nil)
22
+ @task_id = task_id
23
+ @context_id = context_id
24
+ @status = status.is_a?(TaskStatus) ? status : TaskStatus.from_h(status)
25
+ @metadata = metadata
26
+
27
+ validate!
28
+ end
29
+
30
+ ##
31
+ # Get the event type
32
+ #
33
+ # @return [String] The event type
34
+ def event_type
35
+ "task_status_update"
36
+ end
37
+
38
+ ##
39
+ # Check if this is a terminal status update
40
+ #
41
+ # @return [Boolean] True if the status is terminal
42
+ def terminal?
43
+ @status.state.in?(%w[completed canceled failed rejected])
44
+ end
45
+
46
+ private
47
+
48
+ def validate!
49
+ validate_required(:task_id, :context_id, :status)
50
+ validate_type(:task_id, String)
51
+ validate_type(:context_id, String)
52
+ validate_type(:status, TaskStatus)
53
+ end
54
+ end
55
+
56
+ ##
57
+ # Represents a task artifact update event
58
+ #
59
+ # These events are sent when artifacts are added or updated for a task,
60
+ # supporting streaming artifact delivery.
61
+ #
62
+ class TaskArtifactUpdateEvent < A2A::Types::BaseModel
63
+ attr_reader :task_id, :context_id, :artifact, :append, :metadata
64
+
65
+ ##
66
+ # Initialize a new task artifact update event
67
+ #
68
+ # @param task_id [String] The task identifier
69
+ # @param context_id [String] The context identifier
70
+ # @param artifact [Artifact, Hash] The artifact being updated
71
+ # @param append [Boolean] Whether to append to existing artifact
72
+ # @param metadata [Hash, nil] Additional event metadata
73
+ def initialize(task_id:, context_id:, artifact:, append: false, metadata: nil)
74
+ @task_id = task_id
75
+ @context_id = context_id
76
+ @artifact = artifact.is_a?(Artifact) ? artifact : Artifact.from_h(artifact)
77
+ @append = append
78
+ @metadata = metadata
79
+
80
+ validate!
81
+ end
82
+
83
+ ##
84
+ # Get the event type
85
+ #
86
+ # @return [String] The event type
87
+ def event_type
88
+ "task_artifact_update"
89
+ end
90
+
91
+ ##
92
+ # Check if this is an append operation
93
+ #
94
+ # @return [Boolean] True if appending to existing artifact
95
+ def append?
96
+ @append
97
+ end
98
+
99
+ ##
100
+ # Check if this is a replace operation
101
+ #
102
+ # @return [Boolean] True if replacing existing artifact
103
+ def replace?
104
+ !@append
105
+ end
106
+
107
+ private
108
+
109
+ def validate!
110
+ validate_required(:task_id, :context_id, :artifact)
111
+ validate_type(:task_id, String)
112
+ validate_type(:context_id, String)
113
+ validate_type(:artifact, Artifact)
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Represents a message in the A2A protocol
5
+ #
6
+ # Messages are the primary means of communication between agents and users.
7
+ # They contain one or more parts (text, files, or data) and metadata.
8
+ #
9
+ module A2A
10
+ module Types
11
+ class Message < A2A::Types::BaseModel
12
+ attr_reader :message_id, :role, :parts, :context_id, :task_id, :kind,
13
+ :metadata, :extensions, :reference_task_ids
14
+
15
+ ##
16
+ # Initialize a new message
17
+ #
18
+ # @param message_id [String] Unique message identifier
19
+ # @param role [String] Message role ("user" or "agent")
20
+ # @param parts [Array<Part>] Message parts
21
+ # @param kind [String] Message kind (always "message")
22
+ # @param context_id [String, nil] Context identifier
23
+ # @param task_id [String, nil] Associated task identifier
24
+ # @param metadata [Hash, nil] Additional metadata
25
+ # @param extensions [Array<Hash>, nil] Protocol extensions
26
+ # @param reference_task_ids [Array<String>, nil] Referenced task IDs
27
+ def initialize(message_id:, role:, parts:, kind: KIND_MESSAGE, context_id: nil,
28
+ task_id: nil, metadata: nil, extensions: nil, reference_task_ids: nil)
29
+ @message_id = message_id
30
+ @role = role
31
+ @parts = parts.map { |p| p.is_a?(Part) ? p : Part.from_h(p) }
32
+ @kind = kind
33
+ @context_id = context_id
34
+ @task_id = task_id
35
+ @metadata = metadata
36
+ @extensions = extensions
37
+ @reference_task_ids = reference_task_ids
38
+
39
+ validate!
40
+ end
41
+
42
+ ##
43
+ # Get all text content from the message
44
+ #
45
+ # @return [String] Combined text from all text parts
46
+ def text_content
47
+ @parts.select { |p| p.is_a?(TextPart) }
48
+ .map(&:text)
49
+ .join("\n")
50
+ end
51
+
52
+ ##
53
+ # Get all file parts from the message
54
+ #
55
+ # @return [Array<FilePart>] All file parts
56
+ def file_parts
57
+ @parts.select { |p| p.is_a?(FilePart) }
58
+ end
59
+
60
+ ##
61
+ # Get all data parts from the message
62
+ #
63
+ # @return [Array<DataPart>] All data parts
64
+ def data_parts
65
+ @parts.select { |p| p.is_a?(DataPart) }
66
+ end
67
+
68
+ ##
69
+ # Add a part to the message
70
+ #
71
+ # @param part [Part] The part to add
72
+ def add_part(part)
73
+ @parts << part
74
+ end
75
+
76
+ ##
77
+ # Check if the message is from a user
78
+ #
79
+ # @return [Boolean] True if the message is from a user
80
+ def from_user?
81
+ @role == ROLE_USER
82
+ end
83
+
84
+ ##
85
+ # Check if the message is from an agent
86
+ #
87
+ # @return [Boolean] True if the message is from an agent
88
+ def from_agent?
89
+ @role == ROLE_AGENT
90
+ end
91
+
92
+ private
93
+
94
+ def validate!
95
+ validate_required(:message_id, :role, :parts, :kind)
96
+ validate_inclusion(:role, VALID_ROLES)
97
+ validate_inclusion(:kind, [KIND_MESSAGE])
98
+ validate_array_type(:parts, Part)
99
+
100
+ return unless @parts.empty?
101
+
102
+ raise ArgumentError, "Message must have at least one part"
103
+ end
104
+ end
105
+ end
106
+ end