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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a4242ad33f35042f8b326a6ae26d73a86024cb81b3b79dd894e00b061b68dffd
4
- data.tar.gz: 145eafed320e355014100c6544f8b71c6ddd670d58553100055f0566b2609616
3
+ metadata.gz: fadb35d489fe59b7b010bafd2ee6e94adeb0d3cb0d8c6baa2d05fefcdd449fef
4
+ data.tar.gz: 1fa489faf0e9f155d56d995bb49f208dbc4e28f9ae3bf84796b0bd4af02264b9
5
5
  SHA512:
6
- metadata.gz: 996b233809f2451ffb736944c86e16d285962828ee8d5ae0fb533566e8a1afe04eeb64fbeb6f269b61f76263725e5e8a701eae7bf579faa11b711735fab28b70
7
- data.tar.gz: 64213c6aa6899a4ccec29e319258379653082a43bee58b77a9e8807b01ef11c3d7fa3cda3cb892264e85a5392a5980ce5adb044bf90a755164c5a995f4720a55
6
+ metadata.gz: 7ce0595b5f4fbb554306d49fe40588ff16b9036e048012b946930d95143dd3f9f98db62cf7e53c9977b8b0d723e469dbe478aa524ee019da57fd1f29e5b90059
7
+ data.tar.gz: 8caf8f2af8d3f8becdc7b1404ccaebd0333d05c0452cd68a49b59a0ab419bebed4886b1e8f35c1b7eb131a044cd11f97f5ba1ddc70bbf484f923b8bb55826e1e
data/lib/a2a/agent.rb CHANGED
@@ -7,17 +7,22 @@ module A2A
7
7
  # DSL wrapper that collects operation handlers for an A2A agent.
8
8
  #
9
9
  # An Agent produces handler objects that conform to the Dispatcher's
10
- # duck-type contract (#operations, #call). Register an agent on a
11
- # Server the same way you would register a plain handler.
10
+ # contract (#operations, #call). Register an agent on a Server the
11
+ # same way you would register a plain handler.
12
12
  #
13
13
  # agent = A2A::Agent.new do
14
- # on "SendMessage" do |request|
15
- # respond A2A::Schema["Send Message Response"].new({})
14
+ # on "SendMessage" do
15
+ # respond_with -> (env) {
16
+ # A2A::Schema["Send Message Response"].new({})
17
+ # }
16
18
  # end
17
19
  #
18
- # on "GetTask" do |request|
19
- # task = store.get(request.id)
20
- # respond A2A::Schema["Task"].new(task.to_h)
20
+ # on "GetTask" do
21
+ # respond_with -> (env) {
22
+ # task = Task.find_by(id: env["a2a.request"].id)
23
+ # raise A2A::TaskNotFoundError.new(env["a2a.request"].id) unless task
24
+ # task.to_a2a
25
+ # }
21
26
  # end
22
27
  # end
23
28
  #
@@ -28,105 +33,89 @@ module A2A
28
33
 
29
34
  def initialize(&block)
30
35
  @handlers = []
31
-
32
36
  instance_eval(&block) if block
33
37
  end
34
38
 
35
- # Register a handler block for one or more A2A operations.
39
+ # Define a handler stack for one or more A2A operations.
40
+ #
41
+ # The block is evaluated at definition time to build the middleware
42
+ # stack. Use `use` to add middleware, `respond_with` to set the
43
+ # terminal handler.
36
44
  #
37
- # Operations are identified by their proto name (e.g. "SendMessage",
38
- # "GetTask", "CancelTask"). See A2A::Proto.operations for the full list.
45
+ # on "SendMessage" do
46
+ # use SomeMiddleware
47
+ # respond_with -> (env) { ... }
48
+ # end
39
49
  #
40
50
  def on(*operations, &block)
41
51
  raise ArgumentError, "on requires at least one operation" if operations.empty?
42
52
  raise ArgumentError, "on requires a block" unless block
43
53
 
54
+ builder = StackBuilder.new
55
+ builder.instance_eval(&block)
56
+
44
57
  handler = Handler.new(
45
- agent: self,
46
58
  operations: operations.flatten,
47
- block: block
59
+ app: builder.to_app
48
60
  )
49
61
 
50
62
  @handlers << handler
51
63
  handler
52
64
  end
53
65
 
54
- # Internal handler object produced by the #on DSL method.
55
- # Conforms to the Dispatcher duck-type: #operations, #call.
56
- class Handler
57
- attr_reader :operations
58
-
59
- def initialize(agent:, operations:, block:)
60
- @agent = agent
61
- @operations = operations
62
- @block = block
66
+ # Builds a per-operation middleware stack.
67
+ # Collects `use` and `respond_with` calls, compiles into a callable app.
68
+ class StackBuilder
69
+ def initialize
70
+ @middleware = []
71
+ @terminal = nil
63
72
  end
64
73
 
65
- def call(env)
66
- Context.new(env).execute(&@block)
74
+ def use(middleware, *args, &block)
75
+ @middleware << [middleware, args, block]
67
76
  end
68
- end
69
77
 
70
- # Execution context for handler blocks.
71
- # Provides helper methods so blocks can call store, respond, stream, etc.
72
- # directly without holding a reference to the env hash.
73
- class Context
74
- def initialize(env)
75
- @env = env
78
+ def respond_with(callable)
79
+ @terminal = callable
76
80
  end
77
81
 
78
- def execute(&block)
79
- instance_exec(@env["a2a.request"], &block)
80
- end
82
+ def to_app
83
+ raise ArgumentError, "respond_with is required" unless @terminal
81
84
 
82
- def store
83
- @env["a2a.store"]
84
- end
85
+ app = Terminal.new(@terminal)
85
86
 
86
- def request
87
- @env["a2a.request"]
87
+ @middleware.reverse_each do |klass, args, block|
88
+ app = klass.new(app, *args, &block)
89
+ end
90
+
91
+ app
88
92
  end
93
+ end
89
94
 
90
- def agent_card
91
- @env["a2a.agent_card"]
95
+ # Wraps the respond_with lambda as a callable.
96
+ # Returns whatever the lambda returns.
97
+ class Terminal
98
+ def initialize(callable)
99
+ @callable = callable
92
100
  end
93
101
 
94
- def respond(result)
95
- @env["a2a.result"] = result
102
+ def call(env)
103
+ @callable.call(env)
96
104
  end
105
+ end
97
106
 
98
- # Create an SSE stream for streaming responses.
99
- #
100
- # Automatically selects the right stream type based on the binding:
101
- # - JSON-RPC binding -> JsonRpcStream (wraps events in envelopes)
102
- # - REST binding -> RestStream (bare JSON events)
103
- #
104
- # The stream is registered on env["a2a.stream"] so the binding
105
- # middleware returns it as the Rack body. Falcon streams it natively
106
- # via Protocol::HTTP::Body::Writable — no threads, no polling.
107
- #
108
- # Usage in a handler block:
109
- #
110
- # on "SendStreamingMessage" do |request|
111
- # s = stream
112
- # Async do
113
- # s.event({ "task" => { ... } })
114
- # s.event({ "statusUpdate" => { ... } })
115
- # s.finish
116
- # end
117
- # end
118
- #
119
- def stream
120
- require "a2a/sse"
107
+ # Handler object produced by the #on DSL method.
108
+ # Conforms to the Dispatcher contract: #operations, #call.
109
+ class Handler
110
+ attr_reader :operations
121
111
 
122
- s = if @env["a2a.json_rpc_id"]
123
- A2A::SSE::JsonRpcStream.new(json_rpc_id: @env["a2a.json_rpc_id"])
124
- else
125
- A2A::SSE::RestStream.new
126
- end
112
+ def initialize(operations:, app:)
113
+ @operations = operations
114
+ @app = app
115
+ end
127
116
 
128
- @env["a2a.stream"] = s
129
- s
117
+ def call(env)
118
+ @app.call(env)
130
119
  end
131
120
  end
132
121
  end
@@ -136,8 +125,8 @@ test do
136
125
  describe "A2A::Agent" do
137
126
  it "registers handlers via the on DSL" do
138
127
  agent = A2A::Agent.new do
139
- on "SendMessage" do |request|
140
- # no-op
128
+ on "SendMessage" do
129
+ respond_with -> (env) { :ok }
141
130
  end
142
131
  end
143
132
 
@@ -147,8 +136,8 @@ test do
147
136
 
148
137
  it "registers multiple operations on a single handler" do
149
138
  agent = A2A::Agent.new do
150
- on "SendMessage", "GetTask" do |request|
151
- # no-op
139
+ on "SendMessage", "GetTask" do
140
+ respond_with -> (env) { :ok }
152
141
  end
153
142
  end
154
143
 
@@ -158,7 +147,9 @@ test do
158
147
  it "raises if on is called without operations" do
159
148
  lambda {
160
149
  A2A::Agent.new do
161
- on do |request|; end
150
+ on do
151
+ respond_with -> (env) { :ok }
152
+ end
162
153
  end
163
154
  }.should.raise(ArgumentError)
164
155
  end
@@ -170,74 +161,131 @@ test do
170
161
  }.should.raise(ArgumentError)
171
162
  end
172
163
 
173
- it "executes handler block in Context with access to env" do
164
+ it "raises if respond_with is not provided" do
165
+ lambda {
166
+ A2A::Agent.new do
167
+ on "SendMessage" do
168
+ use Class.new
169
+ end
170
+ end
171
+ }.should.raise(ArgumentError)
172
+ end
173
+
174
+ it "returns the respond_with lambda result" do
174
175
  agent = A2A::Agent.new do
175
- on "SendMessage" do |request|
176
- respond({ "echo" => true })
176
+ on "SendMessage" do
177
+ respond_with -> (env) { { "echo" => true } }
177
178
  end
178
179
  end
179
180
 
180
- env = {
181
- "a2a.store" => A2A::TaskStore.new,
182
- "a2a.request" => { "message" => "hello" },
183
- "a2a.agent_card" => { "name" => "Test" },
184
- }
181
+ env = { "a2a.request" => {} }
182
+ result = agent.handlers.first.call(env)
183
+ result.should == { "echo" => true }
184
+ end
185
185
 
186
- agent.handlers.first.call(env)
186
+ it "returns a Schema::Definition from respond_with" do
187
+ schema_obj = A2A::Schema["Task"].new(
188
+ "id" => "t-1",
189
+ "contextId" => "c-1",
190
+ "status" => { "state" => "TASK_STATE_COMPLETED", "timestamp" => "2025-01-01T00:00:00.000Z" },
191
+ )
192
+
193
+ agent = A2A::Agent.new do
194
+ on "GetTask" do
195
+ respond_with -> (env) { schema_obj }
196
+ end
197
+ end
187
198
 
188
- env["a2a.result"].should == { "echo" => true }
199
+ env = { "a2a.request" => {} }
200
+ result = agent.handlers.first.call(env)
201
+ result.should == schema_obj
189
202
  end
190
203
 
191
- it "provides store access in handler context" do
192
- store = A2A::TaskStore.new
193
- seen_store = nil
204
+ it "passes env to the respond_with lambda" do
205
+ seen_env = nil
194
206
 
195
207
  agent = A2A::Agent.new do
196
- on "SendMessage" do |request|
197
- seen_store = store
208
+ on "SendMessage" do
209
+ respond_with -> (env) { seen_env = env; :ok }
198
210
  end
199
211
  end
200
212
 
201
- env = { "a2a.store" => store, "a2a.request" => {} }
213
+ env = { "a2a.request" => { "msg" => "hi" } }
202
214
  agent.handlers.first.call(env)
203
-
204
- seen_store.should == store
215
+ seen_env.should == env
205
216
  end
206
217
 
207
- it "creates a JsonRpcStream when JSON-RPC binding is active" do
218
+ it "executes middleware in order" do
219
+ order = []
220
+
221
+ mw1 = Class.new do
222
+ define_method(:initialize) { |app| @app = app }
223
+ define_method(:call) { |env| order << :mw1; @app.call(env) }
224
+ end
225
+
226
+ mw2 = Class.new do
227
+ define_method(:initialize) { |app| @app = app }
228
+ define_method(:call) { |env| order << :mw2; @app.call(env) }
229
+ end
230
+
208
231
  agent = A2A::Agent.new do
209
- on "SendStreamingMessage" do |request|
210
- s = stream
211
- s.is_a?(A2A::SSE::JsonRpcStream).should == true
232
+ on "SendMessage" do
233
+ use mw1
234
+ use mw2
235
+ respond_with -> (env) { order << :terminal; :done }
212
236
  end
213
237
  end
214
238
 
215
- env = {
216
- "a2a.store" => A2A::TaskStore.new,
217
- "a2a.request" => {},
218
- "a2a.json_rpc_id" => 42,
219
- }
239
+ env = { "a2a.request" => {} }
220
240
  agent.handlers.first.call(env)
241
+ order.should == [:mw1, :mw2, :terminal]
242
+ end
221
243
 
222
- env["a2a.stream"].should.not.be.nil
223
- env["a2a.stream"].is_a?(Protocol::HTTP::Body::Readable).should == true
244
+ it "middleware can intercept and return early" do
245
+ blocker = Class.new do
246
+ define_method(:initialize) { |app| @app = app }
247
+ define_method(:call) { |env| :blocked }
248
+ end
249
+
250
+ agent = A2A::Agent.new do
251
+ on "SendMessage" do
252
+ use blocker
253
+ respond_with -> (env) { :should_not_reach }
254
+ end
255
+ end
256
+
257
+ env = { "a2a.request" => {} }
258
+ result = agent.handlers.first.call(env)
259
+ result.should == :blocked
224
260
  end
225
261
 
226
- it "creates a RestStream when REST binding is active" do
262
+ # ── Error behavior ─────────────────────────────────────────────────
263
+ # Errors propagate to the Dispatcher which catches them.
264
+
265
+ it "lets A2A::Error propagate (Dispatcher catches it)" do
227
266
  agent = A2A::Agent.new do
228
- on "SendStreamingMessage" do |request|
229
- s = stream
230
- s.is_a?(A2A::SSE::RestStream).should == true
267
+ on "GetTask" do
268
+ respond_with -> (env) {
269
+ raise A2A::TaskNotFoundError.new("task-abc")
270
+ }
231
271
  end
232
272
  end
233
273
 
234
- env = {
235
- "a2a.store" => A2A::TaskStore.new,
236
- "a2a.request" => {},
237
- }
238
- agent.handlers.first.call(env)
274
+ env = { "a2a.request" => {} }
275
+ lambda { agent.handlers.first.call(env) }.should.raise(A2A::TaskNotFoundError)
276
+ end
277
+
278
+ it "lets unexpected errors propagate" do
279
+ agent = A2A::Agent.new do
280
+ on "SendMessage" do
281
+ respond_with -> (env) {
282
+ raise RuntimeError, "unexpected bug"
283
+ }
284
+ end
285
+ end
239
286
 
240
- env["a2a.stream"].should.not.be.nil
287
+ env = { "a2a.request" => {} }
288
+ lambda { agent.handlers.first.call(env) }.should.raise(RuntimeError)
241
289
  end
242
290
  end
243
291
  end