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,266 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "active_record"
|
5
|
+
rescue LoadError
|
6
|
+
# ActiveRecord is optional - only load if available
|
7
|
+
end
|
8
|
+
|
9
|
+
module A2A
|
10
|
+
module Server
|
11
|
+
module Storage
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Only define the class if ActiveRecord is available
|
17
|
+
if defined?(ActiveRecord)
|
18
|
+
##
|
19
|
+
# Database storage backend for tasks using ActiveRecord
|
20
|
+
#
|
21
|
+
# This storage backend persists tasks to a database using ActiveRecord.
|
22
|
+
# It requires ActiveRecord to be available and properly configured.
|
23
|
+
#
|
24
|
+
module A2A
|
25
|
+
module Server
|
26
|
+
module Storage
|
27
|
+
class Database < A2A::Server::Storage::Base
|
28
|
+
##
|
29
|
+
# ActiveRecord model for task persistence
|
30
|
+
#
|
31
|
+
class TaskRecord < (defined?(ApplicationRecord) ? ApplicationRecord : ActiveRecord::Base)
|
32
|
+
self.table_name = "a2a_tasks"
|
33
|
+
|
34
|
+
validates :task_id, presence: true, uniqueness: true
|
35
|
+
validates :context_id, presence: true
|
36
|
+
validates :task_data, presence: true
|
37
|
+
|
38
|
+
# Serialize task data as JSON
|
39
|
+
serialize :task_data, coder: JSON
|
40
|
+
|
41
|
+
# Indexes for efficient querying
|
42
|
+
# These should be created in a migration:
|
43
|
+
# add_index :a2a_tasks, :task_id, unique: true
|
44
|
+
# add_index :a2a_tasks, :context_id
|
45
|
+
# add_index :a2a_tasks, :created_at
|
46
|
+
# add_index :a2a_tasks, :updated_at
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Initialize the database storage
|
51
|
+
#
|
52
|
+
# @param connection [ActiveRecord::Base, nil] Optional AR connection
|
53
|
+
# @raise [LoadError] If ActiveRecord is not available
|
54
|
+
def initialize(connection: nil)
|
55
|
+
unless defined?(ActiveRecord)
|
56
|
+
raise LoadError, "ActiveRecord is required for database storage. Add 'activerecord' to your Gemfile."
|
57
|
+
end
|
58
|
+
|
59
|
+
@connection = connection || ActiveRecord::Base
|
60
|
+
ensure_table_exists!
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Save a task to the database
|
65
|
+
#
|
66
|
+
# @param task [A2A::Types::Task] The task to save
|
67
|
+
# @return [void]
|
68
|
+
def save_task(task)
|
69
|
+
task_data = serialize_task(task)
|
70
|
+
|
71
|
+
record = TaskRecord.find_or_initialize_by(task_id: task.id)
|
72
|
+
record.assign_attributes(
|
73
|
+
context_id: task.context_id,
|
74
|
+
task_data: task_data,
|
75
|
+
updated_at: Time.now
|
76
|
+
)
|
77
|
+
|
78
|
+
record.created_at = Time.now if record.new_record?
|
79
|
+
|
80
|
+
record.save!
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Get a task by ID
|
85
|
+
#
|
86
|
+
# @param task_id [String] The task ID
|
87
|
+
# @return [A2A::Types::Task, nil] The task or nil if not found
|
88
|
+
def get_task(task_id)
|
89
|
+
record = TaskRecord.find_by(task_id: task_id)
|
90
|
+
return nil unless record
|
91
|
+
|
92
|
+
deserialize_task(record.task_data)
|
93
|
+
end
|
94
|
+
|
95
|
+
##
|
96
|
+
# Delete a task by ID
|
97
|
+
#
|
98
|
+
# @param task_id [String] The task ID
|
99
|
+
# @return [Boolean] True if task was deleted, false if not found
|
100
|
+
def delete_task(task_id)
|
101
|
+
deleted_count = TaskRecord.where(task_id: task_id).delete_all
|
102
|
+
deleted_count.positive?
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# List all tasks for a given context ID
|
107
|
+
#
|
108
|
+
# @param context_id [String] The context ID
|
109
|
+
# @return [Array<A2A::Types::Task>] Tasks in the context
|
110
|
+
def list_tasks_by_context(context_id)
|
111
|
+
records = TaskRecord.where(context_id: context_id).order(:created_at)
|
112
|
+
records.map { |record| deserialize_task(record.task_data) }
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# List all tasks
|
117
|
+
#
|
118
|
+
# @return [Array<A2A::Types::Task>] All tasks
|
119
|
+
def list_all_tasks
|
120
|
+
records = TaskRecord.order(:created_at)
|
121
|
+
records.map { |record| deserialize_task(record.task_data) }
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# List tasks with optional filtering
|
126
|
+
#
|
127
|
+
# @param filters [Hash] Optional filters (state, context_id, etc.)
|
128
|
+
# @return [Array<A2A::Types::Task>] Filtered tasks
|
129
|
+
def list_tasks(**filters)
|
130
|
+
query = build_base_query(filters)
|
131
|
+
tasks = query.map { |record| deserialize_task(record.task_data) }
|
132
|
+
|
133
|
+
apply_post_query_filters(tasks, filters)
|
134
|
+
end
|
135
|
+
|
136
|
+
##
|
137
|
+
# Clear all tasks
|
138
|
+
#
|
139
|
+
# @return [void]
|
140
|
+
def clear_all_tasks
|
141
|
+
TaskRecord.delete_all
|
142
|
+
end
|
143
|
+
|
144
|
+
##
|
145
|
+
# Get the number of stored tasks
|
146
|
+
#
|
147
|
+
# @return [Integer] Number of tasks
|
148
|
+
def task_count
|
149
|
+
TaskRecord.count
|
150
|
+
end
|
151
|
+
|
152
|
+
##
|
153
|
+
# Create the tasks table if it doesn't exist
|
154
|
+
#
|
155
|
+
# This is a convenience method for development/testing.
|
156
|
+
# In production, use proper migrations.
|
157
|
+
#
|
158
|
+
# @return [void]
|
159
|
+
def create_table!
|
160
|
+
return if table_exists?
|
161
|
+
|
162
|
+
@connection.connection.create_table :a2a_tasks, force: true do |t|
|
163
|
+
t.string :task_id, null: false, limit: 255
|
164
|
+
t.string :context_id, null: false, limit: 255
|
165
|
+
t.text :task_data, null: false
|
166
|
+
t.timestamps null: false
|
167
|
+
|
168
|
+
t.index :task_id, unique: true
|
169
|
+
t.index :context_id
|
170
|
+
t.index :created_at
|
171
|
+
t.index :updated_at
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
##
|
176
|
+
# Drop the tasks table
|
177
|
+
#
|
178
|
+
# @return [void]
|
179
|
+
def drop_table!
|
180
|
+
@connection.connection.drop_table :a2a_tasks if table_exists?
|
181
|
+
end
|
182
|
+
|
183
|
+
##
|
184
|
+
# Check if the tasks table exists
|
185
|
+
#
|
186
|
+
# @return [Boolean] True if table exists
|
187
|
+
def table_exists?
|
188
|
+
@connection.connection.table_exists?(:a2a_tasks)
|
189
|
+
end
|
190
|
+
|
191
|
+
##
|
192
|
+
# Ensure the tasks table exists
|
193
|
+
#
|
194
|
+
# @return [void]
|
195
|
+
def ensure_table_exists!
|
196
|
+
return if table_exists?
|
197
|
+
|
198
|
+
warn "A2A tasks table does not exist. Creating it automatically."
|
199
|
+
warn "In production, you should create this table using a proper migration."
|
200
|
+
create_table!
|
201
|
+
end
|
202
|
+
|
203
|
+
##
|
204
|
+
# Serialize a task to a hash for database storage
|
205
|
+
#
|
206
|
+
# @param task [A2A::Types::Task] The task to serialize
|
207
|
+
# @return [Hash] Serialized task data
|
208
|
+
def serialize_task(task)
|
209
|
+
task.to_h
|
210
|
+
end
|
211
|
+
|
212
|
+
##
|
213
|
+
# Deserialize a task from database storage
|
214
|
+
#
|
215
|
+
# @param task_data [Hash] Serialized task data
|
216
|
+
# @return [A2A::Types::Task] The deserialized task
|
217
|
+
def deserialize_task(task_data)
|
218
|
+
A2A::Types::Task.from_h(task_data)
|
219
|
+
end
|
220
|
+
|
221
|
+
private
|
222
|
+
|
223
|
+
def build_base_query(filters)
|
224
|
+
query = TaskRecord.order(:created_at)
|
225
|
+
query = query.where(context_id: filters[:context_id]) if filters[:context_id]
|
226
|
+
query
|
227
|
+
end
|
228
|
+
|
229
|
+
def apply_post_query_filters(tasks, filters)
|
230
|
+
tasks = tasks.select { |task| task.status&.state == filters[:state] } if filters[:state]
|
231
|
+
apply_metadata_filters(tasks, filters)
|
232
|
+
end
|
233
|
+
|
234
|
+
def apply_metadata_filters(tasks, filters)
|
235
|
+
filters.each do |key, value|
|
236
|
+
next if %i[state context_id].include?(key)
|
237
|
+
|
238
|
+
tasks = tasks.select { |task| apply_single_filter(task, key, value) }
|
239
|
+
end
|
240
|
+
tasks
|
241
|
+
end
|
242
|
+
|
243
|
+
def apply_single_filter(task, key, value)
|
244
|
+
case key
|
245
|
+
when :task_type
|
246
|
+
task.metadata&.dig(:type) == value || task.metadata&.dig("type") == value
|
247
|
+
when :created_after
|
248
|
+
created_at = get_created_at(task)
|
249
|
+
created_at && Time.parse(created_at) > value
|
250
|
+
when :created_before
|
251
|
+
created_at = get_created_at(task)
|
252
|
+
created_at && Time.parse(created_at) < value
|
253
|
+
else
|
254
|
+
# Generic metadata filter
|
255
|
+
task.metadata&.dig(key) == value || task.metadata&.dig(key.to_s) == value
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def get_created_at(task)
|
260
|
+
task.metadata&.dig(:created_at) || task.metadata&.dig("created_at")
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
@@ -0,0 +1,274 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# In-memory storage backend for tasks
|
5
|
+
#
|
6
|
+
# This storage backend keeps all tasks in memory using a simple hash.
|
7
|
+
# It's suitable for development, testing, and single-process deployments
|
8
|
+
# where persistence across restarts is not required.
|
9
|
+
#
|
10
|
+
module A2A
|
11
|
+
module Server
|
12
|
+
module Storage
|
13
|
+
class Memory < A2A::Server::Storage::Base
|
14
|
+
##
|
15
|
+
# Initialize the memory storage with performance optimizations
|
16
|
+
def initialize
|
17
|
+
@tasks = {}
|
18
|
+
@push_configs = {}
|
19
|
+
@context_index = {} # Index for faster context-based lookups
|
20
|
+
@mutex = Mutex.new
|
21
|
+
@stats = {
|
22
|
+
reads: 0,
|
23
|
+
writes: 0,
|
24
|
+
cache_hits: 0,
|
25
|
+
cache_misses: 0
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# Save a task to memory with context indexing
|
31
|
+
#
|
32
|
+
# @param task [A2A::Types::Task] The task to save
|
33
|
+
# @return [void]
|
34
|
+
def save_task(task)
|
35
|
+
@mutex.synchronize do
|
36
|
+
@tasks[task.id] = task
|
37
|
+
|
38
|
+
# Update context index for faster lookups
|
39
|
+
@context_index[task.context_id] ||= []
|
40
|
+
@context_index[task.context_id] << task.id unless @context_index[task.context_id].include?(task.id)
|
41
|
+
|
42
|
+
@stats[:writes] += 1
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Get a task by ID with performance tracking
|
48
|
+
#
|
49
|
+
# @param task_id [String] The task ID
|
50
|
+
# @return [A2A::Types::Task, nil] The task or nil if not found
|
51
|
+
def get_task(task_id)
|
52
|
+
@mutex.synchronize do
|
53
|
+
@stats[:reads] += 1
|
54
|
+
task = @tasks[task_id]
|
55
|
+
if task
|
56
|
+
@stats[:cache_hits] += 1
|
57
|
+
else
|
58
|
+
@stats[:cache_misses] += 1
|
59
|
+
end
|
60
|
+
task
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Delete a task by ID
|
66
|
+
#
|
67
|
+
# @param task_id [String] The task ID
|
68
|
+
# @return [Boolean] True if task was deleted, false if not found
|
69
|
+
def delete_task(task_id)
|
70
|
+
@mutex.synchronize do
|
71
|
+
!@tasks.delete(task_id).nil?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# List all tasks for a given context ID using optimized index
|
77
|
+
#
|
78
|
+
# @param context_id [String] The context ID
|
79
|
+
# @return [Array<A2A::Types::Task>] Tasks in the context
|
80
|
+
def list_tasks_by_context(context_id)
|
81
|
+
@mutex.synchronize do
|
82
|
+
@stats[:reads] += 1
|
83
|
+
|
84
|
+
# Use context index for faster lookups
|
85
|
+
task_ids = @context_index[context_id] || []
|
86
|
+
tasks = task_ids.filter_map { |id| @tasks[id] }
|
87
|
+
|
88
|
+
if tasks.any?
|
89
|
+
@stats[:cache_hits] += 1
|
90
|
+
else
|
91
|
+
@stats[:cache_misses] += 1
|
92
|
+
end
|
93
|
+
|
94
|
+
tasks
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
##
|
99
|
+
# List all tasks
|
100
|
+
#
|
101
|
+
# @return [Array<A2A::Types::Task>] All tasks
|
102
|
+
def list_all_tasks
|
103
|
+
@mutex.synchronize do
|
104
|
+
@tasks.values.dup
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# List tasks with optional filtering
|
110
|
+
#
|
111
|
+
# @param filters [Hash] Optional filters (state, context_id, etc.)
|
112
|
+
# @return [Array<A2A::Types::Task>] Filtered tasks
|
113
|
+
def list_tasks(**filters)
|
114
|
+
@mutex.synchronize do
|
115
|
+
@stats[:reads] += 1
|
116
|
+
tasks = @tasks.values
|
117
|
+
|
118
|
+
tasks = apply_basic_filters(tasks, filters)
|
119
|
+
tasks = apply_metadata_filters(tasks, filters)
|
120
|
+
|
121
|
+
tasks.dup
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
##
|
126
|
+
# Clear all tasks
|
127
|
+
#
|
128
|
+
# @return [void]
|
129
|
+
def clear_all_tasks
|
130
|
+
@mutex.synchronize do
|
131
|
+
@tasks.clear
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# Get the number of stored tasks
|
137
|
+
#
|
138
|
+
# @return [Integer] Number of tasks
|
139
|
+
def task_count
|
140
|
+
@mutex.synchronize do
|
141
|
+
@tasks.size
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
##
|
146
|
+
# Save a push notification config
|
147
|
+
#
|
148
|
+
# @param config [A2A::Types::TaskPushNotificationConfig] The config to save
|
149
|
+
# @return [void]
|
150
|
+
def save_push_notification_config(config)
|
151
|
+
@mutex.synchronize do
|
152
|
+
@push_configs[config.task_id] ||= {}
|
153
|
+
@push_configs[config.task_id][config.push_notification_config.id] = config
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
##
|
158
|
+
# Get a push notification config by task and config ID
|
159
|
+
#
|
160
|
+
# @param task_id [String] The task ID
|
161
|
+
# @param config_id [String] The config ID
|
162
|
+
# @return [A2A::Types::TaskPushNotificationConfig, nil] The config or nil if not found
|
163
|
+
def get_push_notification_config_by_id(task_id, config_id)
|
164
|
+
@mutex.synchronize do
|
165
|
+
@push_configs.dig(task_id, config_id)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
##
|
170
|
+
# List all push notification configs for a task
|
171
|
+
#
|
172
|
+
# @param task_id [String] The task ID
|
173
|
+
# @return [Array<A2A::Types::TaskPushNotificationConfig>] List of configs
|
174
|
+
def list_push_notification_configs(task_id)
|
175
|
+
@mutex.synchronize do
|
176
|
+
(@push_configs[task_id] || {}).values
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
##
|
181
|
+
# Delete a push notification config
|
182
|
+
#
|
183
|
+
# @param task_id [String] The task ID
|
184
|
+
# @param config_id [String] The config ID
|
185
|
+
# @return [Boolean] True if deleted, false if not found
|
186
|
+
def delete_push_notification_config(task_id, config_id)
|
187
|
+
@mutex.synchronize do
|
188
|
+
task_configs = @push_configs[task_id]
|
189
|
+
return false unless task_configs
|
190
|
+
|
191
|
+
deleted = !task_configs.delete(config_id).nil?
|
192
|
+
|
193
|
+
# Clean up empty task entries
|
194
|
+
@push_configs.delete(task_id) if task_configs.empty?
|
195
|
+
|
196
|
+
deleted
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
##
|
201
|
+
# Get storage performance statistics
|
202
|
+
#
|
203
|
+
# @return [Hash] Performance statistics
|
204
|
+
def performance_stats
|
205
|
+
@mutex.synchronize { @stats.dup }
|
206
|
+
end
|
207
|
+
|
208
|
+
##
|
209
|
+
# Reset performance statistics
|
210
|
+
#
|
211
|
+
def reset_performance_stats!
|
212
|
+
@mutex.synchronize do
|
213
|
+
@stats = {
|
214
|
+
reads: 0,
|
215
|
+
writes: 0,
|
216
|
+
cache_hits: 0,
|
217
|
+
cache_misses: 0
|
218
|
+
}
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
##
|
223
|
+
# Get cache hit ratio
|
224
|
+
#
|
225
|
+
# @return [Float] Cache hit ratio (0.0 to 1.0)
|
226
|
+
def cache_hit_ratio
|
227
|
+
@mutex.synchronize do
|
228
|
+
total_reads = @stats[:cache_hits] + @stats[:cache_misses]
|
229
|
+
return 0.0 if total_reads.zero?
|
230
|
+
|
231
|
+
@stats[:cache_hits].to_f / total_reads
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
private
|
236
|
+
|
237
|
+
def apply_basic_filters(tasks, filters)
|
238
|
+
tasks = tasks.select { |task| task.status&.state == filters[:state] } if filters[:state]
|
239
|
+
tasks = tasks.select { |task| task.context_id == filters[:context_id] } if filters[:context_id]
|
240
|
+
tasks
|
241
|
+
end
|
242
|
+
|
243
|
+
def apply_metadata_filters(tasks, filters)
|
244
|
+
filters.each do |key, value|
|
245
|
+
next if %i[state context_id].include?(key)
|
246
|
+
|
247
|
+
tasks = tasks.select { |task| apply_single_filter(task, key, value) }
|
248
|
+
end
|
249
|
+
tasks
|
250
|
+
end
|
251
|
+
|
252
|
+
def apply_single_filter(task, key, value)
|
253
|
+
case key
|
254
|
+
when :task_type
|
255
|
+
task.metadata&.dig(:type) == value || task.metadata&.dig("type") == value
|
256
|
+
when :created_after
|
257
|
+
created_at = get_created_at(task)
|
258
|
+
created_at && Time.parse(created_at) > value
|
259
|
+
when :created_before
|
260
|
+
created_at = get_created_at(task)
|
261
|
+
created_at && Time.parse(created_at) < value
|
262
|
+
else
|
263
|
+
# Generic metadata filter
|
264
|
+
task.metadata&.dig(key) == value || task.metadata&.dig(key.to_s) == value
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def get_created_at(task)
|
269
|
+
task.metadata&.dig(:created_at) || task.metadata&.dig("created_at")
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|