agent2agent 1.0.8 → 1.1.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/a2a/agent.rb +165 -117
  3. data/lib/a2a/client.rb +470 -51
  4. data/lib/a2a/errors/json_rpc_error.rb +71 -0
  5. data/lib/a2a/errors/rest_error.rb +68 -0
  6. data/lib/a2a/errors.rb +535 -0
  7. data/lib/a2a/faraday/middleware/json_rpc/request.rb +96 -0
  8. data/lib/a2a/faraday/middleware/json_rpc/response.rb +131 -0
  9. data/lib/a2a/faraday/middleware/rest/request.rb +166 -0
  10. data/lib/a2a/faraday/middleware/rest/response.rb +144 -0
  11. data/lib/a2a/faraday/middleware/schema_request.rb +69 -0
  12. data/lib/a2a/middleware/extract_message.rb +120 -0
  13. data/lib/a2a/middleware/fetch_task.rb +228 -0
  14. data/lib/a2a/middleware/limit_history_length.rb +123 -0
  15. data/lib/a2a/middleware/limit_pagination_size.rb +133 -0
  16. data/lib/a2a/middleware/sse_stream.rb +235 -0
  17. data/lib/a2a/middleware.rb +7 -0
  18. data/lib/a2a/schema/definition.rb +35 -1
  19. data/lib/a2a/schema.rb +126 -0
  20. data/lib/a2a/{bindings → server/bindings}/json_rpc.rb +12 -8
  21. data/lib/a2a/{bindings → server/bindings}/rest.rb +12 -8
  22. data/lib/a2a/server/dispatcher.rb +52 -54
  23. data/lib/a2a/server/env.rb +4 -6
  24. data/lib/a2a/server/triage.rb +1 -1
  25. data/lib/a2a/server.rb +10 -10
  26. data/lib/a2a/sse/event_parser.rb +202 -0
  27. data/lib/a2a/sse/json_rpc_stream.rb +27 -5
  28. data/lib/a2a/sse/rest_stream.rb +17 -5
  29. data/lib/a2a/sse/stream.rb +135 -7
  30. data/lib/a2a/sse.rb +1 -0
  31. data/lib/a2a/test_helpers.rb +89 -0
  32. data/lib/a2a/version.rb +1 -1
  33. data/lib/a2a.rb +6 -2
  34. data/lib/traces/provider/a2a/{bindings → server/bindings}/json_rpc.rb +2 -2
  35. data/lib/traces/provider/a2a/{bindings → server/bindings}/rest.rb +2 -2
  36. data/lib/traces/provider/a2a.rb +2 -2
  37. metadata +49 -22
  38. data/lib/a2a/server/cancel_task.rb +0 -14
  39. data/lib/a2a/server/create_task_push_notification_config.rb +0 -14
  40. data/lib/a2a/server/delete_task_push_notification_config.rb +0 -14
  41. data/lib/a2a/server/get_extended_agent_card.rb +0 -15
  42. data/lib/a2a/server/get_task.rb +0 -14
  43. data/lib/a2a/server/get_task_push_notification_config.rb +0 -14
  44. data/lib/a2a/server/list_task_push_notification_configs.rb +0 -14
  45. data/lib/a2a/server/list_tasks.rb +0 -14
  46. data/lib/a2a/server/send_message.rb +0 -14
  47. data/lib/a2a/server/send_streaming_message.rb +0 -14
  48. data/lib/a2a/server/subscribe_to_task.rb +0 -14
  49. data/lib/a2a/store/processor.rb +0 -136
  50. data/lib/a2a/store/pub_sub.rb +0 -149
  51. data/lib/a2a/store/sqlite.rb +0 -533
  52. data/lib/a2a/store/webhooks.rb +0 -94
  53. data/lib/a2a/store.rb +0 -6
  54. data/lib/a2a/task_store.rb +0 -315
@@ -1,315 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/setup"
4
- require "a2a"
5
- require "securerandom"
6
- require "net/http"
7
- require "uri"
8
- require "json"
9
-
10
- module A2A
11
- Task = Struct.new(
12
- :id, :context_id, :state, :result, :artifacts, :history,
13
- :push_configs, :subscribers, :created_at, :updated_at,
14
- keyword_init: true
15
- )
16
-
17
- # In-memory task registry with pub/sub for streaming and webhook delivery.
18
- # Swap for a DB-backed implementation in production --
19
- # if the server crashes, in-memory tasks vanish and the client never gets notified.
20
- class TaskStore
21
- TERMINAL_STATES = %w[
22
- TASK_STATE_COMPLETED TASK_STATE_FAILED
23
- TASK_STATE_CANCELED TASK_STATE_REJECTED
24
- ].freeze
25
-
26
- def initialize
27
- @tasks = {}
28
- @mutex = Mutex.new
29
- end
30
-
31
- # ── Task CRUD ──────────────────────────────────────────────────────
32
-
33
- def create(id, context_id, push_config = nil)
34
- @mutex.synchronize do
35
- now = Time.now.utc
36
- configs = {}
37
- if push_config
38
- cfg_id = push_config["id"] || SecureRandom.uuid
39
- push_config["id"] = cfg_id
40
- configs[cfg_id] = push_config
41
- end
42
-
43
- @tasks[id] = Task.new(
44
- id: id,
45
- context_id: context_id,
46
- state: "TASK_STATE_SUBMITTED",
47
- result: nil,
48
- artifacts: [],
49
- history: [],
50
- push_configs: configs,
51
- subscribers: [],
52
- created_at: now,
53
- updated_at: now,
54
- )
55
- end
56
- end
57
-
58
- def get(id)
59
- @mutex.synchronize { @tasks[id] }
60
- end
61
-
62
- def update_state(id, state, message: nil)
63
- task = nil
64
- @mutex.synchronize do
65
- task = @tasks[id]
66
- return nil unless task
67
- task.state = state
68
- task.updated_at = Time.now.utc
69
- end
70
- if task
71
- event = build_status_event(task, message)
72
- notify_subscribers(task, event)
73
- deliver_webhooks(task, { "statusUpdate" => event })
74
- end
75
- task
76
- end
77
-
78
- def complete(id, result)
79
- task = nil
80
- @mutex.synchronize do
81
- task = @tasks[id]
82
- return nil unless task
83
- task.state = "TASK_STATE_COMPLETED"
84
- task.result = result
85
- task.updated_at = Time.now.utc
86
- end
87
- if task
88
- event = build_status_event(task)
89
- notify_subscribers(task, event)
90
- deliver_webhooks(task, { "statusUpdate" => event })
91
- close_subscribers(task)
92
- end
93
- task
94
- end
95
-
96
- def fail(id, msg)
97
- task = nil
98
- @mutex.synchronize do
99
- task = @tasks[id]
100
- return nil unless task
101
- task.state = "TASK_STATE_FAILED"
102
- task.result = msg
103
- task.updated_at = Time.now.utc
104
- end
105
- if task
106
- event = build_status_event(task)
107
- notify_subscribers(task, event)
108
- deliver_webhooks(task, { "statusUpdate" => event })
109
- close_subscribers(task)
110
- end
111
- task
112
- end
113
-
114
- def cancel(id)
115
- update_state(id, "TASK_STATE_CANCELED")
116
- task = get(id)
117
- close_subscribers(task) if task
118
- task
119
- end
120
-
121
- def terminal?(id)
122
- task = get(id)
123
- task && TERMINAL_STATES.include?(task.state)
124
- end
125
-
126
- # ── Artifacts ──────────────────────────────────────────────────────
127
-
128
- def add_artifact(id, artifact)
129
- task = nil
130
- @mutex.synchronize do
131
- task = @tasks[id]
132
- return nil unless task
133
- task.artifacts << artifact
134
- task.updated_at = Time.now.utc
135
- end
136
- if task
137
- event = {
138
- "taskId" => task.id,
139
- "contextId" => task.context_id,
140
- "artifact" => artifact,
141
- "append" => false,
142
- "lastChunk" => true,
143
- }
144
- notify_subscribers(task, event, type: :artifact)
145
- deliver_webhooks(task, { "artifactUpdate" => event })
146
- end
147
- task
148
- end
149
-
150
- # ── History ────────────────────────────────────────────────────────
151
-
152
- def add_message(id, msg)
153
- @mutex.synchronize do
154
- task = @tasks[id]
155
- return nil unless task
156
- task.history << msg
157
- task.updated_at = Time.now.utc
158
- task
159
- end
160
- end
161
-
162
- # ── Listing ────────────────────────────────────────────────────────
163
-
164
- def list(context_id: nil, state: nil)
165
- @mutex.synchronize do
166
- @tasks.values
167
- .select { |t| context_id.nil? || t.context_id == context_id }
168
- .select { |t| state.nil? || t.state == state }
169
- .sort_by { |t| t.updated_at }
170
- .reverse
171
- end
172
- end
173
-
174
- # ── Push Notification Config CRUD ──────────────────────────────────
175
-
176
- def create_push_config(task_id, config)
177
- @mutex.synchronize do
178
- task = @tasks[task_id]
179
- return nil unless task
180
- cfg_id = config["id"] || SecureRandom.uuid
181
- config["id"] = cfg_id
182
- config["taskId"] = task_id
183
- task.push_configs[cfg_id] = config
184
- task.updated_at = Time.now.utc
185
- config
186
- end
187
- end
188
-
189
- def get_push_config(task_id, config_id)
190
- @mutex.synchronize do
191
- task = @tasks[task_id]
192
- return nil unless task
193
- task.push_configs[config_id]
194
- end
195
- end
196
-
197
- def list_push_configs(task_id)
198
- @mutex.synchronize do
199
- task = @tasks[task_id]
200
- return [] unless task
201
- task.push_configs.values
202
- end
203
- end
204
-
205
- def delete_push_config(task_id, config_id)
206
- @mutex.synchronize do
207
- task = @tasks[task_id]
208
- return nil unless task
209
- task.push_configs.delete(config_id)
210
- task.updated_at = Time.now.utc
211
- end
212
- end
213
-
214
- # ── Streaming / Pub-Sub ────────────────────────────────────────────
215
-
216
- # Subscribe to task updates. Returns a Queue that will receive events.
217
- # Each event is a Hash: { type: :status|:artifact, data: Hash }
218
- # A nil sentinel signals stream end.
219
- def subscribe(task_id)
220
- queue = Thread::Queue.new
221
- @mutex.synchronize do
222
- task = @tasks[task_id]
223
- return nil unless task
224
- task.subscribers << queue
225
- end
226
- queue
227
- end
228
-
229
- def unsubscribe(task_id, queue)
230
- @mutex.synchronize do
231
- task = @tasks[task_id]
232
- return unless task
233
- task.subscribers.delete(queue)
234
- end
235
- end
236
-
237
- private
238
-
239
- def build_status_event(task, message = nil)
240
- event = {
241
- "taskId" => task.id,
242
- "contextId" => task.context_id,
243
- "status" => {
244
- "state" => task.state,
245
- "timestamp" => task.updated_at.strftime("%Y-%m-%dT%H:%M:%S.%3NZ"),
246
- },
247
- }
248
- event["status"]["message"] = message if message
249
- event
250
- end
251
-
252
- def notify_subscribers(task, event, type: :status)
253
- subs = @mutex.synchronize { task.subscribers.dup }
254
- subs.each do |queue|
255
- queue << { type: type, data: event }
256
- rescue ClosedQueueError
257
- # subscriber disconnected
258
- end
259
- end
260
-
261
- def close_subscribers(task)
262
- subs = @mutex.synchronize { task.subscribers.dup }
263
- subs.each do |queue|
264
- queue << nil # sentinel
265
- queue.close
266
- rescue ClosedQueueError
267
- # already closed
268
- end
269
- @mutex.synchronize { task.subscribers.clear }
270
- end
271
-
272
- def deliver_webhooks(task, payload)
273
- configs = @mutex.synchronize { task.push_configs.values.dup }
274
- return if configs.empty?
275
-
276
- configs.each do |config|
277
- Thread.new do
278
- deliver_single_webhook(config, payload)
279
- rescue => e
280
- # Log but don't fail
281
- $stderr.puts "[A2A::TaskStore] Webhook delivery failed for #{config["url"]}: #{e.message}"
282
- end
283
- end
284
- end
285
-
286
- def deliver_single_webhook(config, payload)
287
- url = config["url"]
288
- return unless url && !url.empty?
289
-
290
- uri = URI.parse(url)
291
- http = Net::HTTP.new(uri.host, uri.port)
292
- http.use_ssl = (uri.scheme == "https")
293
- http.open_timeout = 10
294
- http.read_timeout = 30
295
-
296
- request = Net::HTTP::Post.new(uri.request_uri)
297
- request["Content-Type"] = "application/a2a+json"
298
-
299
- # Authentication
300
- if (auth = config["authentication"])
301
- scheme = auth["scheme"] || "Bearer"
302
- creds = auth["credentials"] || ""
303
- request["Authorization"] = "#{scheme} #{creds}"
304
- end
305
-
306
- # Token header
307
- if (token = config["token"])
308
- request["X-A2A-Notification-Token"] = token
309
- end
310
-
311
- request.body = JSON.generate(payload)
312
- http.request(request)
313
- end
314
- end
315
- end