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.
- checksums.yaml +4 -4
- data/lib/a2a/agent.rb +165 -117
- data/lib/a2a/client.rb +470 -51
- data/lib/a2a/errors/json_rpc_error.rb +71 -0
- data/lib/a2a/errors/rest_error.rb +68 -0
- data/lib/a2a/errors.rb +535 -0
- data/lib/a2a/faraday/middleware/json_rpc/request.rb +96 -0
- data/lib/a2a/faraday/middleware/json_rpc/response.rb +131 -0
- data/lib/a2a/faraday/middleware/rest/request.rb +166 -0
- data/lib/a2a/faraday/middleware/rest/response.rb +144 -0
- data/lib/a2a/faraday/middleware/schema_request.rb +69 -0
- data/lib/a2a/middleware/extract_message.rb +120 -0
- data/lib/a2a/middleware/fetch_task.rb +228 -0
- data/lib/a2a/middleware/limit_history_length.rb +123 -0
- data/lib/a2a/middleware/limit_pagination_size.rb +133 -0
- data/lib/a2a/middleware/sse_stream.rb +235 -0
- data/lib/a2a/middleware.rb +7 -0
- data/lib/a2a/schema/definition.rb +35 -1
- data/lib/a2a/schema.rb +126 -0
- data/lib/a2a/{bindings → server/bindings}/json_rpc.rb +12 -8
- data/lib/a2a/{bindings → server/bindings}/rest.rb +12 -8
- data/lib/a2a/server/dispatcher.rb +52 -54
- data/lib/a2a/server/env.rb +4 -6
- data/lib/a2a/server/triage.rb +1 -1
- data/lib/a2a/server.rb +10 -10
- data/lib/a2a/sse/event_parser.rb +202 -0
- data/lib/a2a/sse/json_rpc_stream.rb +27 -5
- data/lib/a2a/sse/rest_stream.rb +17 -5
- data/lib/a2a/sse/stream.rb +135 -7
- data/lib/a2a/sse.rb +1 -0
- data/lib/a2a/test_helpers.rb +89 -0
- data/lib/a2a/version.rb +1 -1
- data/lib/a2a.rb +6 -2
- data/lib/traces/provider/a2a/{bindings → server/bindings}/json_rpc.rb +2 -2
- data/lib/traces/provider/a2a/{bindings → server/bindings}/rest.rb +2 -2
- data/lib/traces/provider/a2a.rb +2 -2
- metadata +49 -22
- data/lib/a2a/server/cancel_task.rb +0 -14
- data/lib/a2a/server/create_task_push_notification_config.rb +0 -14
- data/lib/a2a/server/delete_task_push_notification_config.rb +0 -14
- data/lib/a2a/server/get_extended_agent_card.rb +0 -15
- data/lib/a2a/server/get_task.rb +0 -14
- data/lib/a2a/server/get_task_push_notification_config.rb +0 -14
- data/lib/a2a/server/list_task_push_notification_configs.rb +0 -14
- data/lib/a2a/server/list_tasks.rb +0 -14
- data/lib/a2a/server/send_message.rb +0 -14
- data/lib/a2a/server/send_streaming_message.rb +0 -14
- data/lib/a2a/server/subscribe_to_task.rb +0 -14
- data/lib/a2a/store/processor.rb +0 -136
- data/lib/a2a/store/pub_sub.rb +0 -149
- data/lib/a2a/store/sqlite.rb +0 -533
- data/lib/a2a/store/webhooks.rb +0 -94
- data/lib/a2a/store.rb +0 -6
- data/lib/a2a/task_store.rb +0 -315
data/lib/a2a/task_store.rb
DELETED
|
@@ -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
|