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,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
|