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,534 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
require_relative "storage"
|
5
|
+
require_relative "../utils/performance"
|
6
|
+
|
7
|
+
##
|
8
|
+
# Manages task lifecycle, state transitions, and event processing
|
9
|
+
#
|
10
|
+
# The TaskManager is responsible for creating, updating, and managing tasks
|
11
|
+
# throughout their lifecycle. It handles state transitions, artifact management,
|
12
|
+
# message history, and event notifications.
|
13
|
+
#
|
14
|
+
module A2A
|
15
|
+
module Server
|
16
|
+
class TaskManager
|
17
|
+
attr_reader :storage, :event_handlers, :config
|
18
|
+
|
19
|
+
##
|
20
|
+
# Initialize a new TaskManager with performance optimizations
|
21
|
+
#
|
22
|
+
# @param storage [Object] Storage backend for task persistence
|
23
|
+
# @param push_notification_manager [PushNotificationManager, nil] Push notification manager
|
24
|
+
# @param config [Hash] Configuration options
|
25
|
+
def initialize(storage: nil, push_notification_manager: nil, config: {})
|
26
|
+
@storage = storage || A2A::Server::Storage::Memory.new
|
27
|
+
@push_notification_manager = push_notification_manager
|
28
|
+
@event_handlers = []
|
29
|
+
@config = default_config.merge(config)
|
30
|
+
@task_cache = {} # LRU cache for frequently accessed tasks
|
31
|
+
@cache_mutex = Mutex.new
|
32
|
+
@performance_metrics = {
|
33
|
+
tasks_created: 0,
|
34
|
+
tasks_updated: 0,
|
35
|
+
cache_hits: 0,
|
36
|
+
cache_misses: 0,
|
37
|
+
avg_processing_time: 0.0
|
38
|
+
}
|
39
|
+
@metrics_mutex = Mutex.new
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Create a new task with performance tracking
|
44
|
+
#
|
45
|
+
# @param type [String] Task type identifier
|
46
|
+
# @param params [Hash] Task parameters
|
47
|
+
# @param context_id [String, nil] Optional context ID (generated if not provided)
|
48
|
+
# @param metadata [Hash, nil] Optional task metadata
|
49
|
+
# @return [A2A::Types::Task] The created task
|
50
|
+
def create_task(type:, params: {}, context_id: nil, metadata: nil)
|
51
|
+
A2A::Utils::Performance.profile("task_creation") do
|
52
|
+
task_id = generate_task_id
|
53
|
+
context_id ||= generate_context_id
|
54
|
+
|
55
|
+
# Record task creation metric
|
56
|
+
@metrics_mutex.synchronize { @performance_metrics[:tasks_created] += 1 }
|
57
|
+
|
58
|
+
A2A::Monitoring.increment_counter("a2a_tasks_created", task_type: type) if defined?(A2A::Monitoring)
|
59
|
+
A2A::Monitoring.log(:info, "Creating task", task_id: task_id, task_type: type) if defined?(A2A::Monitoring)
|
60
|
+
|
61
|
+
task = A2A::Types::Task.new(
|
62
|
+
id: task_id,
|
63
|
+
context_id: context_id,
|
64
|
+
status: A2A::Types::TaskStatus.new(
|
65
|
+
state: A2A::Types::TASK_STATE_SUBMITTED,
|
66
|
+
message: "Task created",
|
67
|
+
updated_at: Time.now.utc.iso8601
|
68
|
+
),
|
69
|
+
metadata: (metadata || {}).merge(
|
70
|
+
type: type,
|
71
|
+
params: params,
|
72
|
+
created_at: Time.now.utc.iso8601
|
73
|
+
)
|
74
|
+
)
|
75
|
+
|
76
|
+
@storage.save_task(task)
|
77
|
+
|
78
|
+
# Cache the newly created task
|
79
|
+
add_to_cache(task_id, task)
|
80
|
+
|
81
|
+
# Emit task creation event
|
82
|
+
emit_status_update_event(task)
|
83
|
+
|
84
|
+
task
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
##
|
89
|
+
# Get a task by ID with caching for performance
|
90
|
+
#
|
91
|
+
# @param task_id [String] The task ID
|
92
|
+
# @param history_length [Integer, nil] Maximum number of history messages to include
|
93
|
+
# @return [A2A::Types::Task] The task
|
94
|
+
# @raise [A2A::Errors::TaskNotFound] If task doesn't exist
|
95
|
+
def get_task(task_id, history_length: nil)
|
96
|
+
start_time = Time.now
|
97
|
+
|
98
|
+
# Check cache first for frequently accessed tasks
|
99
|
+
cached_task = get_from_cache(task_id)
|
100
|
+
if cached_task && (!history_length || !cached_task.history || cached_task.history.length <= history_length)
|
101
|
+
record_cache_hit
|
102
|
+
return cached_task
|
103
|
+
end
|
104
|
+
|
105
|
+
record_cache_miss
|
106
|
+
task = @storage.get_task(task_id)
|
107
|
+
raise A2A::Errors::TaskNotFound, "Task #{task_id} not found" unless task
|
108
|
+
|
109
|
+
# Limit history if requested
|
110
|
+
if history_length && task.history && task.history.length > history_length
|
111
|
+
limited_history = task.history.last(history_length)
|
112
|
+
# Create a new task instance with limited history
|
113
|
+
task = A2A::Types::Task.new(
|
114
|
+
id: task.id,
|
115
|
+
context_id: task.context_id,
|
116
|
+
status: task.status,
|
117
|
+
artifacts: task.artifacts,
|
118
|
+
history: limited_history,
|
119
|
+
metadata: task.metadata
|
120
|
+
)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Cache the task for future access
|
124
|
+
add_to_cache(task_id, task)
|
125
|
+
|
126
|
+
record_processing_time(Time.now - start_time)
|
127
|
+
task
|
128
|
+
end
|
129
|
+
|
130
|
+
##
|
131
|
+
# Update task status with performance tracking
|
132
|
+
#
|
133
|
+
# @param task_id [String] The task ID
|
134
|
+
# @param status [A2A::Types::TaskStatus, Hash] New status
|
135
|
+
# @param message [String, nil] Optional status message
|
136
|
+
# @return [A2A::Types::Task] The updated task
|
137
|
+
# @raise [A2A::Errors::TaskNotFound] If task doesn't exist
|
138
|
+
def update_task_status(task_id, status, message: nil)
|
139
|
+
A2A::Utils::Performance.profile("task_status_update") do
|
140
|
+
task = get_task(task_id)
|
141
|
+
|
142
|
+
# Validate state transition
|
143
|
+
new_state = status.is_a?(A2A::Types::TaskStatus) ? status.state : status[:state] || status["state"]
|
144
|
+
validate_state_transition(task.status.state, new_state)
|
145
|
+
|
146
|
+
# Record task update metric
|
147
|
+
@metrics_mutex.synchronize { @performance_metrics[:tasks_updated] += 1 }
|
148
|
+
|
149
|
+
# Create new status
|
150
|
+
new_status = if status.is_a?(A2A::Types::TaskStatus)
|
151
|
+
status
|
152
|
+
else
|
153
|
+
status_hash = status.dup
|
154
|
+
status_hash[:message] = message if message
|
155
|
+
status_hash[:updated_at] = Time.now.utc.iso8601
|
156
|
+
A2A::Types::TaskStatus.new(**status_hash)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Update task
|
160
|
+
task.update_status(new_status)
|
161
|
+
@storage.save_task(task)
|
162
|
+
|
163
|
+
# Update cache
|
164
|
+
add_to_cache(task_id, task)
|
165
|
+
|
166
|
+
# Emit status update event
|
167
|
+
emit_status_update_event(task)
|
168
|
+
|
169
|
+
task
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
##
|
174
|
+
# Cancel a task
|
175
|
+
#
|
176
|
+
# @param task_id [String] The task ID
|
177
|
+
# @param reason [String, nil] Optional cancellation reason
|
178
|
+
# @return [A2A::Types::Task] The canceled task
|
179
|
+
# @raise [A2A::Errors::TaskNotFound] If task doesn't exist
|
180
|
+
# @raise [A2A::Errors::TaskNotCancelable] If task cannot be canceled
|
181
|
+
def cancel_task(task_id, reason: nil)
|
182
|
+
task = get_task(task_id)
|
183
|
+
|
184
|
+
unless task.cancelable?
|
185
|
+
raise A2A::Errors::TaskNotCancelable,
|
186
|
+
"Task #{task_id} in state '#{task.status.state}' cannot be canceled"
|
187
|
+
end
|
188
|
+
|
189
|
+
update_task_status(
|
190
|
+
task_id,
|
191
|
+
A2A::Types::TaskStatus.new(
|
192
|
+
state: A2A::Types::TASK_STATE_CANCELED,
|
193
|
+
message: reason || "Task canceled",
|
194
|
+
updated_at: Time.now.utc.iso8601
|
195
|
+
)
|
196
|
+
)
|
197
|
+
end
|
198
|
+
|
199
|
+
##
|
200
|
+
# Add an artifact to a task
|
201
|
+
#
|
202
|
+
# @param task_id [String] The task ID
|
203
|
+
# @param artifact [A2A::Types::Artifact] The artifact to add
|
204
|
+
# @param append [Boolean] Whether to append to existing artifact with same ID
|
205
|
+
# @return [A2A::Types::Task] The updated task
|
206
|
+
# @raise [A2A::Errors::TaskNotFound] If task doesn't exist
|
207
|
+
def add_artifact(task_id, artifact, append: false)
|
208
|
+
task = get_task(task_id)
|
209
|
+
|
210
|
+
if append && task.artifacts
|
211
|
+
# Find existing artifact with same ID
|
212
|
+
existing_artifact = task.artifacts.find { |a| a.artifact_id == artifact.artifact_id }
|
213
|
+
if existing_artifact
|
214
|
+
# Append parts to existing artifact
|
215
|
+
artifact.parts.each { |part| existing_artifact.add_part(part) }
|
216
|
+
else
|
217
|
+
task.add_artifact(artifact)
|
218
|
+
end
|
219
|
+
else
|
220
|
+
task.add_artifact(artifact)
|
221
|
+
end
|
222
|
+
|
223
|
+
@storage.save_task(task)
|
224
|
+
|
225
|
+
# Emit artifact update event
|
226
|
+
emit_artifact_update_event(task, artifact, append)
|
227
|
+
|
228
|
+
task
|
229
|
+
end
|
230
|
+
|
231
|
+
##
|
232
|
+
# Add an artifact to a task (alias for add_artifact for compatibility)
|
233
|
+
#
|
234
|
+
# @param task_id [String] The task ID
|
235
|
+
# @param artifact [A2A::Types::Artifact] The artifact to add
|
236
|
+
# @param append [Boolean] Whether to append to existing artifact with same ID
|
237
|
+
# @return [A2A::Types::Task] The updated task
|
238
|
+
# @raise [A2A::Errors::TaskNotFound] If task doesn't exist
|
239
|
+
def add_task_artifact(task_id, artifact, append: false)
|
240
|
+
add_artifact(task_id, artifact, append: append)
|
241
|
+
end
|
242
|
+
|
243
|
+
##
|
244
|
+
# Add a message to task history
|
245
|
+
#
|
246
|
+
# @param task_id [String] The task ID
|
247
|
+
# @param message [A2A::Types::Message] The message to add
|
248
|
+
# @return [A2A::Types::Task] The updated task
|
249
|
+
# @raise [A2A::Errors::TaskNotFound] If task doesn't exist
|
250
|
+
def add_message(task_id, message)
|
251
|
+
task = get_task(task_id)
|
252
|
+
task.add_message(message)
|
253
|
+
|
254
|
+
# Limit history length if configured
|
255
|
+
if @config[:max_history_length] && task.history && task.history.length > @config[:max_history_length]
|
256
|
+
# Keep only the most recent messages
|
257
|
+
task.instance_variable_set(:@history, task.history.last(@config[:max_history_length]))
|
258
|
+
end
|
259
|
+
|
260
|
+
@storage.save_task(task)
|
261
|
+
task
|
262
|
+
end
|
263
|
+
|
264
|
+
##
|
265
|
+
# List tasks by context ID
|
266
|
+
#
|
267
|
+
# @param context_id [String] The context ID
|
268
|
+
# @return [Array<A2A::Types::Task>] Tasks in the context
|
269
|
+
def list_tasks_by_context(*args)
|
270
|
+
@storage.list_tasks_by_context(*args)
|
271
|
+
end
|
272
|
+
|
273
|
+
##
|
274
|
+
# Add an event handler
|
275
|
+
#
|
276
|
+
# @param handler [Proc] Event handler that receives (event_type, event_data)
|
277
|
+
def add_event_handler(&handler)
|
278
|
+
@event_handlers << handler
|
279
|
+
end
|
280
|
+
|
281
|
+
##
|
282
|
+
# Remove an event handler
|
283
|
+
#
|
284
|
+
# @param handler [Proc] The handler to remove
|
285
|
+
def remove_event_handler(handler)
|
286
|
+
@event_handlers.delete(handler)
|
287
|
+
end
|
288
|
+
|
289
|
+
private
|
290
|
+
|
291
|
+
##
|
292
|
+
# Generate a unique task ID
|
293
|
+
#
|
294
|
+
# @return [String] A unique task ID
|
295
|
+
def generate_task_id
|
296
|
+
SecureRandom.uuid
|
297
|
+
end
|
298
|
+
|
299
|
+
##
|
300
|
+
# Generate a unique context ID
|
301
|
+
#
|
302
|
+
# @return [String] A unique context ID
|
303
|
+
def generate_context_id
|
304
|
+
SecureRandom.uuid
|
305
|
+
end
|
306
|
+
|
307
|
+
##
|
308
|
+
# Validate state transition
|
309
|
+
#
|
310
|
+
# @param current_state [String] Current task state
|
311
|
+
# @param new_state [String] Proposed new state
|
312
|
+
# @raise [ArgumentError] If transition is invalid
|
313
|
+
def validate_state_transition(current_state, new_state)
|
314
|
+
# Define valid state transitions
|
315
|
+
valid_transitions = {
|
316
|
+
A2A::Types::TASK_STATE_SUBMITTED => [
|
317
|
+
A2A::Types::TASK_STATE_WORKING,
|
318
|
+
A2A::Types::TASK_STATE_CANCELED,
|
319
|
+
A2A::Types::TASK_STATE_REJECTED,
|
320
|
+
A2A::Types::TASK_STATE_AUTH_REQUIRED
|
321
|
+
],
|
322
|
+
A2A::Types::TASK_STATE_WORKING => [
|
323
|
+
A2A::Types::TASK_STATE_INPUT_REQUIRED,
|
324
|
+
A2A::Types::TASK_STATE_COMPLETED,
|
325
|
+
A2A::Types::TASK_STATE_CANCELED,
|
326
|
+
A2A::Types::TASK_STATE_FAILED,
|
327
|
+
A2A::Types::TASK_STATE_AUTH_REQUIRED
|
328
|
+
],
|
329
|
+
A2A::Types::TASK_STATE_INPUT_REQUIRED => [
|
330
|
+
A2A::Types::TASK_STATE_WORKING,
|
331
|
+
A2A::Types::TASK_STATE_COMPLETED,
|
332
|
+
A2A::Types::TASK_STATE_CANCELED,
|
333
|
+
A2A::Types::TASK_STATE_FAILED
|
334
|
+
],
|
335
|
+
A2A::Types::TASK_STATE_AUTH_REQUIRED => [
|
336
|
+
A2A::Types::TASK_STATE_WORKING,
|
337
|
+
A2A::Types::TASK_STATE_CANCELED,
|
338
|
+
A2A::Types::TASK_STATE_REJECTED
|
339
|
+
]
|
340
|
+
}
|
341
|
+
|
342
|
+
# Terminal states cannot transition
|
343
|
+
terminal_states = [
|
344
|
+
A2A::Types::TASK_STATE_COMPLETED,
|
345
|
+
A2A::Types::TASK_STATE_CANCELED,
|
346
|
+
A2A::Types::TASK_STATE_FAILED,
|
347
|
+
A2A::Types::TASK_STATE_REJECTED,
|
348
|
+
A2A::Types::TASK_STATE_UNKNOWN
|
349
|
+
]
|
350
|
+
|
351
|
+
raise ArgumentError, "Cannot transition from terminal state '#{current_state}'" if terminal_states.include?(current_state)
|
352
|
+
|
353
|
+
allowed_states = valid_transitions[current_state] || []
|
354
|
+
return if allowed_states.include?(new_state)
|
355
|
+
|
356
|
+
raise ArgumentError, "Invalid state transition from '#{current_state}' to '#{new_state}'"
|
357
|
+
end
|
358
|
+
|
359
|
+
##
|
360
|
+
# Emit a task status update event
|
361
|
+
#
|
362
|
+
# @param task [A2A::Types::Task] The task
|
363
|
+
def emit_status_update_event(task)
|
364
|
+
event = A2A::Types::TaskStatusUpdateEvent.new(
|
365
|
+
task_id: task.id,
|
366
|
+
context_id: task.context_id,
|
367
|
+
status: task.status,
|
368
|
+
metadata: {
|
369
|
+
timestamp: Time.now.utc.iso8601,
|
370
|
+
event_id: SecureRandom.uuid
|
371
|
+
}
|
372
|
+
)
|
373
|
+
|
374
|
+
emit_event("task_status_update", event)
|
375
|
+
|
376
|
+
# Send push notifications if manager is available
|
377
|
+
@push_notification_manager&.notify_task_status_update(event)
|
378
|
+
end
|
379
|
+
|
380
|
+
##
|
381
|
+
# Emit a task artifact update event
|
382
|
+
#
|
383
|
+
# @param task [A2A::Types::Task] The task
|
384
|
+
# @param artifact [A2A::Types::Artifact] The artifact
|
385
|
+
# @param append [Boolean] Whether this was an append operation
|
386
|
+
def emit_artifact_update_event(task, artifact, append)
|
387
|
+
event = A2A::Types::TaskArtifactUpdateEvent.new(
|
388
|
+
task_id: task.id,
|
389
|
+
context_id: task.context_id,
|
390
|
+
artifact: artifact,
|
391
|
+
append: append,
|
392
|
+
metadata: {
|
393
|
+
timestamp: Time.now.utc.iso8601,
|
394
|
+
event_id: SecureRandom.uuid
|
395
|
+
}
|
396
|
+
)
|
397
|
+
|
398
|
+
emit_event("task_artifact_update", event)
|
399
|
+
|
400
|
+
# Send push notifications if manager is available
|
401
|
+
@push_notification_manager&.notify_task_artifact_update(event)
|
402
|
+
end
|
403
|
+
|
404
|
+
##
|
405
|
+
# Emit an event to all registered handlers
|
406
|
+
#
|
407
|
+
# @param event_type [String] The event type
|
408
|
+
# @param event_data [Object] The event data
|
409
|
+
def emit_event(event_type, event_data)
|
410
|
+
@event_handlers.each do |handler|
|
411
|
+
handler.call(event_type, event_data)
|
412
|
+
rescue StandardError => e
|
413
|
+
# Log error but don't fail the operation
|
414
|
+
warn "Error in event handler: #{e.message}"
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
##
|
419
|
+
# Default configuration
|
420
|
+
#
|
421
|
+
# @return [Hash] Default configuration
|
422
|
+
def default_config
|
423
|
+
{
|
424
|
+
max_history_length: 100,
|
425
|
+
cache_size: 1000,
|
426
|
+
cache_ttl: 300 # 5 minutes
|
427
|
+
}
|
428
|
+
end
|
429
|
+
|
430
|
+
##
|
431
|
+
# Get task from cache
|
432
|
+
#
|
433
|
+
# @param task_id [String] The task ID
|
434
|
+
# @return [A2A::Types::Task, nil] Cached task or nil
|
435
|
+
def get_from_cache(task_id)
|
436
|
+
@cache_mutex.synchronize do
|
437
|
+
entry = @task_cache[task_id]
|
438
|
+
return nil unless entry
|
439
|
+
|
440
|
+
# Check TTL
|
441
|
+
cache_ttl = @config[:cache_ttl] || 300
|
442
|
+
if Time.now - entry[:timestamp] > cache_ttl
|
443
|
+
@task_cache.delete(task_id)
|
444
|
+
return nil
|
445
|
+
end
|
446
|
+
|
447
|
+
entry[:task]
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
##
|
452
|
+
# Add task to cache with LRU eviction
|
453
|
+
#
|
454
|
+
# @param task_id [String] The task ID
|
455
|
+
# @param task [A2A::Types::Task] The task to cache
|
456
|
+
def add_to_cache(task_id, task)
|
457
|
+
@cache_mutex.synchronize do
|
458
|
+
# Evict oldest entries if cache is full
|
459
|
+
cache_size = @config[:cache_size] || 1000
|
460
|
+
if @task_cache.size >= cache_size
|
461
|
+
oldest_key = @task_cache.min_by { |_, entry| entry[:timestamp] }.first
|
462
|
+
@task_cache.delete(oldest_key)
|
463
|
+
end
|
464
|
+
|
465
|
+
@task_cache[task_id] = {
|
466
|
+
task: task,
|
467
|
+
timestamp: Time.now
|
468
|
+
}
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
##
|
473
|
+
# Clear task cache
|
474
|
+
#
|
475
|
+
def clear_cache!
|
476
|
+
@cache_mutex.synchronize { @task_cache.clear }
|
477
|
+
end
|
478
|
+
|
479
|
+
##
|
480
|
+
# Record cache hit
|
481
|
+
#
|
482
|
+
def record_cache_hit
|
483
|
+
@metrics_mutex.synchronize { @performance_metrics[:cache_hits] += 1 }
|
484
|
+
end
|
485
|
+
|
486
|
+
##
|
487
|
+
# Record cache miss
|
488
|
+
#
|
489
|
+
def record_cache_miss
|
490
|
+
@metrics_mutex.synchronize { @performance_metrics[:cache_misses] += 1 }
|
491
|
+
end
|
492
|
+
|
493
|
+
##
|
494
|
+
# Record processing time
|
495
|
+
#
|
496
|
+
# @param duration [Float] Processing time in seconds
|
497
|
+
def record_processing_time(duration)
|
498
|
+
@metrics_mutex.synchronize do
|
499
|
+
current_avg = @performance_metrics[:avg_processing_time]
|
500
|
+
total_ops = @performance_metrics[:tasks_created] + @performance_metrics[:tasks_updated]
|
501
|
+
|
502
|
+
@performance_metrics[:avg_processing_time] = if total_ops.positive?
|
503
|
+
((current_avg * (total_ops - 1)) + duration) / total_ops
|
504
|
+
else
|
505
|
+
duration
|
506
|
+
end
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
##
|
511
|
+
# Get performance metrics
|
512
|
+
#
|
513
|
+
# @return [Hash] Performance metrics
|
514
|
+
def performance_metrics
|
515
|
+
@metrics_mutex.synchronize { @performance_metrics.dup }
|
516
|
+
end
|
517
|
+
|
518
|
+
##
|
519
|
+
# Reset performance metrics
|
520
|
+
#
|
521
|
+
def reset_performance_metrics!
|
522
|
+
@metrics_mutex.synchronize do
|
523
|
+
@performance_metrics = {
|
524
|
+
tasks_created: 0,
|
525
|
+
tasks_updated: 0,
|
526
|
+
cache_hits: 0,
|
527
|
+
cache_misses: 0,
|
528
|
+
avg_processing_time: 0.0
|
529
|
+
}
|
530
|
+
end
|
531
|
+
end
|
532
|
+
end
|
533
|
+
end
|
534
|
+
end
|