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,320 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "redis"
5
+ require "json"
6
+ rescue LoadError
7
+ # Redis is optional - only load if available
8
+ end
9
+
10
+ ##
11
+ # Redis storage backend for tasks
12
+ #
13
+ # This storage backend persists tasks to Redis using JSON serialization.
14
+ # It's suitable for distributed deployments and provides good performance
15
+ # for task storage and retrieval.
16
+ #
17
+ module A2A
18
+ module Server
19
+ module Storage
20
+ class Redis < A2A::Server::Storage::Base
21
+ # Redis key prefixes
22
+ TASK_KEY_PREFIX = "a2a:task:"
23
+ CONTEXT_KEY_PREFIX = "a2a:context:"
24
+ TASK_LIST_KEY = "a2a:tasks:all"
25
+
26
+ ##
27
+ # Initialize the Redis storage
28
+ #
29
+ # @param redis [Redis, nil] Redis client instance
30
+ # @param url [String, nil] Redis URL (if redis client not provided)
31
+ # @param namespace [String] Key namespace prefix
32
+ # @param ttl [Integer, nil] Optional TTL for task keys (in seconds)
33
+ # @raise [LoadError] If Redis gem is not available
34
+ def initialize(redis: nil, url: nil, namespace: "a2a", ttl: nil)
35
+ unless defined?(::Redis)
36
+ raise LoadError,
37
+ "Redis gem is required for Redis storage. Add 'redis' to your Gemfile."
38
+ end
39
+
40
+ @redis = redis || ::Redis.new(url: url || ENV["REDIS_URL"] || "redis://localhost:6379")
41
+ @namespace = namespace
42
+ @ttl = ttl
43
+ end
44
+
45
+ ##
46
+ # Save a task to Redis
47
+ #
48
+ # @param task [A2A::Types::Task] The task to save
49
+ # @return [void]
50
+ def save_task(task)
51
+ task_key = build_task_key(task.id)
52
+ context_key = build_context_key(task.context_id)
53
+ task_data = serialize_task(task)
54
+
55
+ @redis.multi do |multi|
56
+ # Store the task data
57
+ multi.set(task_key, task_data)
58
+
59
+ # Add to context set
60
+ multi.sadd(context_key, task.id)
61
+
62
+ # Add to global task list
63
+ multi.sadd(TASK_LIST_KEY, task.id)
64
+
65
+ # Set TTL if configured
66
+ if @ttl
67
+ multi.expire(task_key, @ttl)
68
+ multi.expire(context_key, @ttl)
69
+ end
70
+ end
71
+ end
72
+
73
+ ##
74
+ # Get a task by ID
75
+ #
76
+ # @param task_id [String] The task ID
77
+ # @return [A2A::Types::Task, nil] The task or nil if not found
78
+ def get_task(task_id)
79
+ task_key = build_task_key(task_id)
80
+ task_data = @redis.get(task_key)
81
+
82
+ return nil unless task_data
83
+
84
+ deserialize_task(task_data)
85
+ end
86
+
87
+ ##
88
+ # Delete a task by ID
89
+ #
90
+ # @param task_id [String] The task ID
91
+ # @return [Boolean] True if task was deleted, false if not found
92
+ def delete_task(task_id)
93
+ task = get_task(task_id)
94
+ return false unless task
95
+
96
+ task_key = build_task_key(task_id)
97
+ context_key = build_context_key(task.context_id)
98
+
99
+ @redis.multi do |multi|
100
+ # Remove task data
101
+ multi.del(task_key)
102
+
103
+ # Remove from context set
104
+ multi.srem(context_key, task_id)
105
+
106
+ # Remove from global task list
107
+ multi.srem(TASK_LIST_KEY, task_id)
108
+ end
109
+
110
+ true
111
+ end
112
+
113
+ ##
114
+ # List all tasks for a given context ID
115
+ #
116
+ # @param context_id [String] The context ID
117
+ # @return [Array<A2A::Types::Task>] Tasks in the context
118
+ def list_tasks_by_context(context_id)
119
+ context_key = build_context_key(context_id)
120
+ task_ids = @redis.smembers(context_key)
121
+
122
+ return [] if task_ids.empty?
123
+
124
+ # Get all tasks in a single pipeline
125
+ task_keys = task_ids.map { |id| build_task_key(id) }
126
+ task_data_list = @redis.mget(*task_keys)
127
+
128
+ tasks = task_data_list.compact.map { |data| deserialize_task(data) }
129
+
130
+ # Sort by creation time (from metadata)
131
+ tasks.sort_by { |task| task.metadata&.dig("created_at") || "" }
132
+ end
133
+
134
+ ##
135
+ # List all tasks
136
+ #
137
+ # @return [Array<A2A::Types::Task>] All tasks
138
+ def list_all_tasks
139
+ task_ids = @redis.smembers(TASK_LIST_KEY)
140
+
141
+ return [] if task_ids.empty?
142
+
143
+ # Get all tasks in a single pipeline
144
+ task_keys = task_ids.map { |id| build_task_key(id) }
145
+ task_data_list = @redis.mget(*task_keys)
146
+
147
+ tasks = task_data_list.compact.map { |data| deserialize_task(data) }
148
+
149
+ # Sort by creation time (from metadata)
150
+ tasks.sort_by { |task| task.metadata&.dig("created_at") || "" }
151
+ end
152
+
153
+ ##
154
+ # List tasks with optional filtering
155
+ #
156
+ # @param filters [Hash] Optional filters (state, context_id, etc.)
157
+ # @return [Array<A2A::Types::Task>] Filtered tasks
158
+ def list_tasks(**filters)
159
+ tasks = get_base_tasks(filters)
160
+ tasks = apply_state_filter(tasks, filters)
161
+ apply_metadata_filters(tasks, filters)
162
+ end
163
+
164
+ ##
165
+ # Clear all tasks
166
+ #
167
+ # @return [void]
168
+ def clear_all_tasks
169
+ # Get all task IDs
170
+ task_ids = @redis.smembers(TASK_LIST_KEY)
171
+
172
+ return if task_ids.empty?
173
+
174
+ # Build all keys to delete
175
+ task_keys = task_ids.map { |id| build_task_key(id) }
176
+
177
+ # Get all context IDs to clean up context sets
178
+ tasks = task_keys.filter_map { |key| @redis.get(key) }.map { |data| deserialize_task(data) }
179
+ context_keys = tasks.map { |task| build_context_key(task.context_id) }.uniq
180
+
181
+ # Delete everything in a transaction
182
+ @redis.multi do |multi|
183
+ # Delete all task data
184
+ multi.del(*task_keys) unless task_keys.empty?
185
+
186
+ # Delete all context sets
187
+ multi.del(*context_keys) unless context_keys.empty?
188
+
189
+ # Clear the global task list
190
+ multi.del(TASK_LIST_KEY)
191
+ end
192
+ end
193
+
194
+ ##
195
+ # Get the number of stored tasks
196
+ #
197
+ # @return [Integer] Number of tasks
198
+ def task_count
199
+ @redis.scard(TASK_LIST_KEY)
200
+ end
201
+
202
+ ##
203
+ # Check Redis connection
204
+ #
205
+ # @return [Boolean] True if connected
206
+ def connected?
207
+ @redis.ping == "PONG"
208
+ rescue ::Redis::BaseError
209
+ false
210
+ end
211
+
212
+ ##
213
+ # Get Redis info
214
+ #
215
+ # @return [Hash] Redis server info
216
+ def info
217
+ @redis.info
218
+ end
219
+
220
+ ##
221
+ # Flush all A2A data from Redis (dangerous!)
222
+ #
223
+ # This removes all A2A-related keys from Redis.
224
+ # Use with caution in production.
225
+ #
226
+ # @return [void]
227
+ def flush_all_a2a_data!
228
+ pattern = "#{@namespace}:*"
229
+ keys = @redis.keys(pattern)
230
+
231
+ return if keys.empty?
232
+
233
+ @redis.del(*keys)
234
+ end
235
+
236
+ ##
237
+ # Build a Redis key for a task
238
+ #
239
+ # @param task_id [String] The task ID
240
+ # @return [String] Redis key
241
+ def build_task_key(task_id)
242
+ "#{@namespace}:#{TASK_KEY_PREFIX}#{task_id}"
243
+ end
244
+
245
+ ##
246
+ # Build a Redis key for a context set
247
+ #
248
+ # @param context_id [String] The context ID
249
+ # @return [String] Redis key
250
+ def build_context_key(context_id)
251
+ "#{@namespace}:#{CONTEXT_KEY_PREFIX}#{context_id}"
252
+ end
253
+
254
+ ##
255
+ # Serialize a task to JSON for Redis storage
256
+ #
257
+ # @param task [A2A::Types::Task] The task to serialize
258
+ # @return [String] JSON string
259
+ def serialize_task(task)
260
+ JSON.generate(task.to_h)
261
+ end
262
+
263
+ ##
264
+ # Deserialize a task from JSON
265
+ #
266
+ # @param task_data [String] JSON string
267
+ # @return [A2A::Types::Task] The deserialized task
268
+ def deserialize_task(task_data)
269
+ data = JSON.parse(task_data)
270
+ A2A::Types::Task.from_h(data)
271
+ end
272
+
273
+ private
274
+
275
+ def get_base_tasks(filters)
276
+ if filters[:context_id]
277
+ list_tasks_by_context(filters[:context_id])
278
+ else
279
+ list_all_tasks
280
+ end
281
+ end
282
+
283
+ def apply_state_filter(tasks, filters)
284
+ return tasks unless filters[:state]
285
+
286
+ tasks.select { |task| task.status&.state == filters[:state] }
287
+ end
288
+
289
+ def apply_metadata_filters(tasks, filters)
290
+ filters.each do |key, value|
291
+ next if %i[state context_id].include?(key)
292
+
293
+ tasks = tasks.select { |task| apply_single_filter(task, key, value) }
294
+ end
295
+ tasks
296
+ end
297
+
298
+ def apply_single_filter(task, key, value)
299
+ case key
300
+ when :task_type
301
+ task.metadata&.dig(:type) == value || task.metadata&.dig("type") == value
302
+ when :created_after
303
+ created_at = get_created_at(task)
304
+ created_at && Time.parse(created_at) > value
305
+ when :created_before
306
+ created_at = get_created_at(task)
307
+ created_at && Time.parse(created_at) < value
308
+ else
309
+ # Generic metadata filter
310
+ task.metadata&.dig(key) == value || task.metadata&.dig(key.to_s) == value
311
+ end
312
+ end
313
+
314
+ def get_created_at(task)
315
+ task.metadata&.dig(:created_at) || task.metadata&.dig("created_at")
316
+ end
317
+ end
318
+ end
319
+ end
320
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "storage/base"
4
+ require_relative "storage/memory"
5
+
6
+ # Optional storage backends (only loaded if dependencies are available)
7
+ begin
8
+ require_relative "storage/database"
9
+ rescue LoadError
10
+ # ActiveRecord not available
11
+ end
12
+
13
+ begin
14
+ require_relative "storage/redis"
15
+ rescue LoadError
16
+ # Redis not available
17
+ end
18
+
19
+ ##
20
+ # Storage backends for task persistence
21
+ #
22
+ # This module provides different storage implementations for persisting
23
+ # tasks and related data. The storage layer is abstracted to allow
24
+ # for different backends (memory, database, Redis, etc.).
25
+ #
26
+ module A2A
27
+ module Server
28
+ module Storage
29
+ # Storage backend types
30
+ TYPE_MEMORY = "memory"
31
+ TYPE_DATABASE = "database"
32
+ TYPE_REDIS = "redis"
33
+
34
+ # Valid storage types
35
+ VALID_TYPES = [TYPE_MEMORY, TYPE_DATABASE, TYPE_REDIS].freeze
36
+ end
37
+ end
38
+ end