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,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # <%= model_class_name("push_notification_config") %>
5
+ #
6
+ # ActiveRecord model for A2A push notification configurations.
7
+ # This model stores webhook URLs and authentication details for task notifications.
8
+ #
9
+ class <%= model_class_name("push_notification_config") %> < ApplicationRecord
10
+ self.table_name = "<%= push_notification_configs_table_name %>"
11
+ self.primary_key = "id"
12
+
13
+ # Associations
14
+ belongs_to :<%= model_file_name("task") %>,
15
+ foreign_key: :task_id,
16
+ class_name: "<%= model_class_name("task") %>"
17
+
18
+ # Validations
19
+ validates :id, presence: true, uniqueness: true
20
+ validates :task_id, presence: true
21
+ validates :url, presence: true, format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]) }
22
+ validates :retry_count, presence: true, numericality: { greater_than_or_equal_to: 0 }
23
+
24
+ # Scopes
25
+ scope :active, -> { where(active: true, deleted_at: nil) }
26
+ scope :inactive, -> { where(active: false) }
27
+ scope :by_task, ->(task_id) { where(task_id: task_id) }
28
+ scope :failed_recently, -> { where("last_failure_at > ?", 1.hour.ago) }
29
+ scope :needs_retry, -> { where("retry_count < ? AND (last_failure_at IS NULL OR last_failure_at < ?)", max_retries, retry_delay.ago) }
30
+
31
+ # Callbacks
32
+ before_create :ensure_id
33
+ after_create :log_creation
34
+ after_update :log_status_change, if: :saved_change_to_active?
35
+
36
+ # Configuration
37
+ MAX_RETRIES = 5
38
+ RETRY_DELAY = 5.minutes
39
+
40
+ def self.max_retries
41
+ MAX_RETRIES
42
+ end
43
+
44
+ def self.retry_delay
45
+ RETRY_DELAY
46
+ end
47
+
48
+ # Soft delete
49
+ def soft_delete!
50
+ update!(deleted_at: Time.now, active: false)
51
+ end
52
+
53
+ def deleted?
54
+ deleted_at.present?
55
+ end
56
+
57
+ # Status management
58
+ def mark_success!
59
+ update!(
60
+ last_success_at: Time.now,
61
+ last_failure_at: nil,
62
+ last_error: nil,
63
+ retry_count: 0
64
+ )
65
+ end
66
+
67
+ def mark_failure!(error_message)
68
+ update!(
69
+ last_failure_at: Time.now,
70
+ last_error: error_message,
71
+ retry_count: retry_count + 1,
72
+ active: retry_count < self.class.max_retries
73
+ )
74
+ end
75
+
76
+ def can_retry?
77
+ active && retry_count < self.class.max_retries &&
78
+ (last_failure_at.nil? || last_failure_at < self.class.retry_delay.ago)
79
+ end
80
+
81
+ def should_disable?
82
+ retry_count >= self.class.max_retries
83
+ end
84
+
85
+ # Convert to A2A types
86
+ def to_a2a_push_notification_config
87
+ A2A::Types::PushNotificationConfig.new(
88
+ id: id,
89
+ url: url,
90
+ token: token,
91
+ authentication: authentication || {}
92
+ )
93
+ end
94
+
95
+ def to_a2a_task_push_notification_config
96
+ A2A::Types::TaskPushNotificationConfig.new(
97
+ task_id: task_id,
98
+ push_notification_config: to_a2a_push_notification_config
99
+ )
100
+ end
101
+
102
+ # Create from A2A types
103
+ def self.from_a2a_config(task_id, config)
104
+ if config.is_a?(A2A::Types::TaskPushNotificationConfig)
105
+ pn_config = config.push_notification_config
106
+ task_id = config.task_id
107
+ elsif config.is_a?(A2A::Types::PushNotificationConfig)
108
+ pn_config = config
109
+ else
110
+ raise ArgumentError, "Invalid config type"
111
+ end
112
+
113
+ new(
114
+ id: pn_config.id || SecureRandom.uuid,
115
+ task_id: task_id,
116
+ url: pn_config.url,
117
+ token: pn_config.token,
118
+ authentication: pn_config.authentication || {}
119
+ )
120
+ end
121
+
122
+ # Webhook delivery
123
+ def deliver_notification(event_data)
124
+ return false unless active? && !deleted?
125
+
126
+ begin
127
+ response = send_webhook_request(event_data)
128
+
129
+ if response.success?
130
+ mark_success!
131
+ true
132
+ else
133
+ mark_failure!("HTTP #{response.code}: #{response.body}")
134
+ false
135
+ end
136
+ rescue => e
137
+ mark_failure!(e.message)
138
+ false
139
+ end
140
+ end
141
+
142
+ # Authentication helpers
143
+ def has_authentication?
144
+ authentication.present? && authentication.any?
145
+ end
146
+
147
+ def authentication_type
148
+ return nil unless has_authentication?
149
+
150
+ if authentication["type"].present?
151
+ authentication["type"]
152
+ elsif token.present?
153
+ "bearer"
154
+ else
155
+ "custom"
156
+ end
157
+ end
158
+
159
+ # Statistics
160
+ def success_rate
161
+ total_attempts = retry_count + (last_success_at.present? ? 1 : 0)
162
+ return 0.0 if total_attempts == 0
163
+
164
+ successful_attempts = last_success_at.present? ? 1 : 0
165
+ (successful_attempts.to_f / total_attempts * 100).round(2)
166
+ end
167
+
168
+ def last_activity
169
+ [last_success_at, last_failure_at].compact.max
170
+ end
171
+
172
+ private
173
+
174
+ def ensure_id
175
+ self.id ||= SecureRandom.uuid
176
+ end
177
+
178
+ def log_creation
179
+ Rails.logger.info "Created push notification config #{id} for task #{task_id}"
180
+ end
181
+
182
+ def log_status_change
183
+ status = active? ? "activated" : "deactivated"
184
+ Rails.logger.info "Push notification config #{id} #{status}"
185
+ end
186
+
187
+ def send_webhook_request(event_data)
188
+ require 'faraday'
189
+
190
+ conn = Faraday.new do |f|
191
+ f.request :json
192
+ f.response :json
193
+ f.adapter Faraday.default_adapter
194
+ end
195
+
196
+ headers = build_request_headers
197
+
198
+ conn.post(url) do |req|
199
+ req.headers.merge!(headers)
200
+ req.body = event_data
201
+ end
202
+ end
203
+
204
+ def build_request_headers
205
+ headers = {
206
+ 'Content-Type' => 'application/json',
207
+ 'User-Agent' => "A2A-Ruby/#{A2A::VERSION}",
208
+ 'X-A2A-Task-ID' => task_id,
209
+ 'X-A2A-Config-ID' => id
210
+ }
211
+
212
+ # Add authentication headers
213
+ case authentication_type
214
+ when "bearer"
215
+ headers['Authorization'] = "Bearer #{token}"
216
+ when "api_key"
217
+ headers['X-API-Key'] = token
218
+ when "custom"
219
+ # Add custom authentication headers from authentication hash
220
+ authentication.each do |key, value|
221
+ next if %w[type].include?(key)
222
+ headers[key] = value
223
+ end
224
+ end
225
+
226
+ headers
227
+ end
228
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # <%= model_class_name("task") %>
5
+ #
6
+ # ActiveRecord model for A2A tasks with JSON serialization and validation.
7
+ # This model provides persistence for A2A task data including status, artifacts,
8
+ # and message history.
9
+ #
10
+ class <%= model_class_name("task") %> < ApplicationRecord
11
+ self.table_name = "<%= tasks_table_name %>"
12
+ self.primary_key = "id"
13
+
14
+ # Associations
15
+ has_many :<%= model_file_name("push_notification_config").pluralize %>,
16
+ foreign_key: :task_id,
17
+ dependent: :destroy,
18
+ class_name: "<%= model_class_name("push_notification_config") %>"
19
+
20
+ # Validations
21
+ validates :id, presence: true, uniqueness: true
22
+ validates :context_id, presence: true
23
+ validates :kind, presence: true, inclusion: { in: %w[task] }
24
+ validates :status_state, presence: true, inclusion: {
25
+ in: %w[submitted working input-required completed canceled failed rejected auth-required unknown]
26
+ }
27
+
28
+ # Scopes
29
+ scope :active, -> { where(deleted_at: nil) }
30
+ scope :by_status, ->(status) { where(status_state: status) }
31
+ scope :by_context, ->(context_id) { where(context_id: context_id) }
32
+ scope :by_type, ->(type) { where(type: type) }
33
+ scope :recent, -> { order(created_at: :desc) }
34
+ scope :processing, -> { where(status_state: %w[submitted working]) }
35
+ scope :completed, -> { where(status_state: %w[completed canceled failed rejected]) }
36
+
37
+ # Callbacks
38
+ before_create :ensure_id
39
+ before_save :update_status_timestamp
40
+ after_update :notify_status_change, if: :saved_change_to_status_state?
41
+
42
+ # Soft delete
43
+ def soft_delete!
44
+ update!(deleted_at: Time.now)
45
+ end
46
+
47
+ def deleted?
48
+ deleted_at.present?
49
+ end
50
+
51
+ # Status management
52
+ def status
53
+ A2A::Types::TaskStatus.new(
54
+ state: status_state,
55
+ message: status_message,
56
+ progress: status_progress,
57
+ result: status_result,
58
+ error: status_error,
59
+ updated_at: status_updated_at&.iso8601
60
+ )
61
+ end
62
+
63
+ def status=(new_status)
64
+ if new_status.is_a?(A2A::Types::TaskStatus)
65
+ self.status_state = new_status.state
66
+ self.status_message = new_status.message
67
+ self.status_progress = new_status.progress
68
+ self.status_result = new_status.result
69
+ self.status_error = new_status.error
70
+ self.status_updated_at = Time.now
71
+ elsif new_status.is_a?(Hash)
72
+ self.status = A2A::Types::TaskStatus.from_h(new_status)
73
+ else
74
+ raise ArgumentError, "Status must be TaskStatus or Hash"
75
+ end
76
+ end
77
+
78
+ # Artifact management
79
+ def artifacts_objects
80
+ return [] unless artifacts.present?
81
+
82
+ artifacts.map { |artifact_data| A2A::Types::Artifact.from_h(artifact_data) }
83
+ end
84
+
85
+ def add_artifact(artifact)
86
+ artifact_data = artifact.is_a?(A2A::Types::Artifact) ? artifact.to_h : artifact
87
+
88
+ self.artifacts = (artifacts || []) + [artifact_data]
89
+ save!
90
+ end
91
+
92
+ def update_artifact(artifact_id, new_artifact, append: false)
93
+ return unless artifacts.present?
94
+
95
+ artifact_index = artifacts.find_index { |a| a["artifact_id"] == artifact_id }
96
+ return unless artifact_index
97
+
98
+ new_artifact_data = new_artifact.is_a?(A2A::Types::Artifact) ? new_artifact.to_h : new_artifact
99
+
100
+ if append && artifacts[artifact_index]["parts"].present?
101
+ # Append parts to existing artifact
102
+ existing_parts = artifacts[artifact_index]["parts"] || []
103
+ new_parts = new_artifact_data["parts"] || []
104
+ artifacts[artifact_index]["parts"] = existing_parts + new_parts
105
+ else
106
+ # Replace entire artifact
107
+ artifacts[artifact_index] = new_artifact_data
108
+ end
109
+
110
+ save!
111
+ end
112
+
113
+ # Message history management
114
+ def history_objects
115
+ return [] unless history.present?
116
+
117
+ history.map { |message_data| A2A::Types::Message.from_h(message_data) }
118
+ end
119
+
120
+ def add_message(message)
121
+ message_data = message.is_a?(A2A::Types::Message) ? message.to_h : message
122
+
123
+ self.history = (history || []) + [message_data]
124
+
125
+ # Limit history length if configured
126
+ max_history = A2A.config.max_history_length || 100
127
+ if history.length > max_history
128
+ self.history = history.last(max_history)
129
+ end
130
+
131
+ save!
132
+ end
133
+
134
+ # Convert to A2A::Types::Task
135
+ def to_a2a_task
136
+ A2A::Types::Task.new(
137
+ id: id,
138
+ context_id: context_id,
139
+ kind: kind,
140
+ status: status,
141
+ artifacts: artifacts_objects,
142
+ history: history_objects,
143
+ metadata: metadata || {}
144
+ )
145
+ end
146
+
147
+ # Create from A2A::Types::Task
148
+ def self.from_a2a_task(task)
149
+ new(
150
+ id: task.id,
151
+ context_id: task.context_id,
152
+ kind: task.kind,
153
+ status_state: task.status.state,
154
+ status_message: task.status.message,
155
+ status_progress: task.status.progress,
156
+ status_result: task.status.result,
157
+ status_error: task.status.error,
158
+ status_updated_at: task.status.updated_at ? Time.parse(task.status.updated_at) : Time.now,
159
+ artifacts: task.artifacts&.map(&:to_h),
160
+ history: task.history&.map(&:to_h),
161
+ metadata: task.metadata
162
+ )
163
+ end
164
+
165
+ # Search and filtering
166
+ def self.search(query)
167
+ return all if query.nil? || (respond_to?(:empty?) && empty?) || (is_a?(String) && strip.empty?)
168
+
169
+ where(
170
+ "status_message ILIKE ? OR metadata::text ILIKE ?",
171
+ "%#{query}%", "%#{query}%"
172
+ )
173
+ end
174
+
175
+ def self.by_metadata(key, value)
176
+ <% if postgresql? %>
177
+ where("metadata->? = ?", key, value.to_json)
178
+ <% else %>
179
+ where("JSON_EXTRACT(metadata, ?) = ?", "$.#{key}", value.to_s)
180
+ <% end %>
181
+ end
182
+
183
+ private
184
+
185
+ def ensure_id
186
+ self.id ||= SecureRandom.uuid
187
+ self.context_id ||= SecureRandom.uuid
188
+ end
189
+
190
+ def update_status_timestamp
191
+ if status_state_changed?
192
+ self.status_updated_at = Time.now
193
+ end
194
+ end
195
+
196
+ def notify_status_change
197
+ # Trigger push notifications and events
198
+ A2A::Server::TaskManager.instance.notify_task_status_change(self) if A2A.config.push_notifications_enabled
199
+ end
200
+ end
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :a2a do
4
+ desc "Show A2A configuration"
5
+ task config: :environment do
6
+ puts "A2A Configuration:"
7
+ puts "=================="
8
+ puts "Rails Integration: #{A2A.config.rails_integration}"
9
+ puts "Mount Path: #{A2A.config.mount_path}"
10
+ puts "Authentication Required: #{A2A.config.authentication_required}"
11
+ puts "CORS Enabled: #{A2A.config.cors_enabled}"
12
+ puts "Rate Limiting Enabled: #{A2A.config.rate_limiting_enabled}"
13
+ puts "Logging Enabled: #{A2A.config.logging_enabled}"
14
+ puts "Version: #{A2A::VERSION}"
15
+ end
16
+
17
+ desc "List all registered A2A agents and capabilities"
18
+ task agents: :environment do
19
+ puts "Registered A2A Agents:"
20
+ puts "====================="
21
+
22
+ agent_count = 0
23
+ capability_count = 0
24
+
25
+ ObjectSpace.each_object(Class) do |klass|
26
+ next unless klass < ActionController::Base && klass.included_modules.include?(A2A::Server::Agent)
27
+
28
+ agent_count += 1
29
+ puts "\n#{klass.name}:"
30
+
31
+ capabilities = klass._a2a_capabilities || []
32
+ capability_count += capabilities.length
33
+
34
+ if capabilities.any?
35
+ capabilities.each do |capability|
36
+ puts " - #{capability.name}: #{capability.description}"
37
+ end
38
+ else
39
+ puts " (no capabilities defined)"
40
+ end
41
+
42
+ methods = klass._a2a_methods || {}
43
+ next unless methods.any?
44
+
45
+ puts " Methods:"
46
+ methods.each_key do |method_name|
47
+ puts " - #{method_name}"
48
+ end
49
+ end
50
+
51
+ puts "\nSummary:"
52
+ puts "--------"
53
+ puts "Total Agents: #{agent_count}"
54
+ puts "Total Capabilities: #{capability_count}"
55
+ end
56
+
57
+ desc "Generate agent card for all registered agents"
58
+ task agent_cards: :environment do
59
+ puts "Agent Cards:"
60
+ puts "============"
61
+
62
+ ObjectSpace.each_object(Class) do |klass|
63
+ next unless klass < ActionController::Base && klass.included_modules.include?(A2A::Server::Agent)
64
+
65
+ puts "\n#{klass.name}:"
66
+ puts "-" * (klass.name.length + 1)
67
+
68
+ begin
69
+ # Create a mock controller instance to generate the card
70
+ controller = klass.new
71
+ controller.request = ActionDispatch::Request.new({})
72
+
73
+ card = controller.send(:generate_agent_card)
74
+ puts JSON.pretty_generate(card.to_h)
75
+ rescue StandardError => e
76
+ puts "Error generating card: #{e.message}"
77
+ end
78
+ end
79
+ end
80
+
81
+ desc "Validate A2A protocol compliance"
82
+ task validate: :environment do
83
+ puts "A2A Protocol Validation:"
84
+ puts "======================="
85
+
86
+ errors = []
87
+ warnings = []
88
+
89
+ # Check Rails version compatibility
90
+ errors << "Rails version #{Rails.version} is not supported. Minimum version is 6.0" if Rails.version < "6.0"
91
+
92
+ # Check required dependencies
93
+ required_gems = %w[faraday json jwt redis concurrent-ruby]
94
+ required_gems.each do |gem_name|
95
+ require gem_name
96
+ rescue LoadError
97
+ errors << "Required gem '#{gem_name}' is not available"
98
+ end
99
+
100
+ # Validate configuration
101
+ config = A2A.config
102
+ errors << "Mount path must start with '/'" if config.mount_path && !config.mount_path.start_with?("/")
103
+
104
+ # Check for registered agents
105
+ agent_classes = []
106
+ ObjectSpace.each_object(Class) do |klass|
107
+ agent_classes << klass if klass < ActionController::Base && klass.included_modules.include?(A2A::Server::Agent)
108
+ end
109
+
110
+ warnings << "No A2A agents found. Consider creating some with 'rails generate a2a:agent'" if agent_classes.empty?
111
+
112
+ # Validate agent implementations
113
+ agent_classes.each do |klass|
114
+ capabilities = klass._a2a_capabilities || []
115
+ methods = klass._a2a_methods || {}
116
+
117
+ warnings << "#{klass.name} has no capabilities defined" if capabilities.empty?
118
+
119
+ warnings << "#{klass.name} has no A2A methods defined" if methods.empty?
120
+ end
121
+
122
+ # Report results
123
+ if errors.any?
124
+ puts "❌ Validation failed with #{errors.length} error(s):"
125
+ errors.each { |error| puts " - #{error}" }
126
+ else
127
+ puts "✅ Validation passed!"
128
+ end
129
+
130
+ if warnings.any?
131
+ puts "\n⚠️ #{warnings.length} warning(s):"
132
+ warnings.each { |warning| puts " - #{warning}" }
133
+ end
134
+
135
+ puts "\nSummary:"
136
+ puts "--------"
137
+ puts "Agents found: #{agent_classes.length}"
138
+ puts "Total capabilities: #{agent_classes.sum { |k| (k._a2a_capabilities || []).length }}"
139
+ puts "Total methods: #{agent_classes.sum { |k| (k._a2a_methods || {}).length }}"
140
+ end
141
+
142
+ desc "Start A2A development server"
143
+ task server: :environment do
144
+ puts "Starting A2A development server..."
145
+ puts "A2A endpoints will be available at: #{A2A.config.mount_path}"
146
+ puts "Press Ctrl+C to stop"
147
+
148
+ # This would typically start a development server
149
+ # For now, just show the configuration
150
+ Rake::Task["a2a:config"].invoke
151
+ puts "\nUse 'rails server' to start the full Rails application"
152
+ end
153
+
154
+ namespace :db do
155
+ desc "Create A2A database tables"
156
+ task migrate: :environment do
157
+ puts "Creating A2A database tables..."
158
+
159
+ # Check if migrations exist
160
+ migration_path = Rails.root.join("db", "migrate")
161
+ a2a_migrations = Dir.glob(migration_path.join("*_create_a2a_*.rb"))
162
+
163
+ if a2a_migrations.empty?
164
+ puts "No A2A migrations found. Run 'rails generate a2a:migration' first."
165
+ else
166
+ puts "Found #{a2a_migrations.length} A2A migration(s)"
167
+ Rake::Task["db:migrate"].invoke
168
+ end
169
+ end
170
+
171
+ desc "Seed A2A database with sample data"
172
+ task seed: :environment do
173
+ puts "Seeding A2A database..."
174
+
175
+ # Create sample tasks for development
176
+ if Rails.env.development?
177
+ task_manager = A2A::Server::TaskManager.instance
178
+
179
+ 3.times do |i|
180
+ task = task_manager.create_task(
181
+ type: "sample_task_#{i + 1}",
182
+ params: { message: "Sample task #{i + 1}" }
183
+ )
184
+ puts "Created sample task: #{task.id}"
185
+ end
186
+ else
187
+ puts "Seeding is only available in development environment"
188
+ end
189
+ end
190
+ end
191
+
192
+ namespace :test do
193
+ desc "Run A2A protocol compliance tests"
194
+ task compliance: :environment do
195
+ puts "Running A2A protocol compliance tests..."
196
+
197
+ # This would run specific compliance tests
198
+ # For now, just validate the setup
199
+ Rake::Task["a2a:validate"].invoke
200
+ end
201
+
202
+ desc "Test A2A endpoints"
203
+ task endpoints: :environment do
204
+ puts "Testing A2A endpoints..."
205
+
206
+ require "net/http"
207
+ require "uri"
208
+
209
+ base_url = "http://localhost:3000#{A2A.config.mount_path}"
210
+
211
+ endpoints = [
212
+ { path: "/health", method: "GET" },
213
+ { path: "/agent-card", method: "GET" },
214
+ { path: "/capabilities", method: "GET" }
215
+ ]
216
+
217
+ endpoints.each do |endpoint|
218
+ uri = URI("#{base_url}#{endpoint[:path]}")
219
+ response = Net::HTTP.get_response(uri)
220
+
221
+ status = response.code.to_i < 400 ? "✅" : "❌"
222
+ puts "#{status} #{endpoint[:method]} #{endpoint[:path]} - #{response.code}"
223
+ rescue StandardError => e
224
+ puts "❌ #{endpoint[:method]} #{endpoint[:path]} - Error: #{e.message}"
225
+ end
226
+ end
227
+ end
228
+ end