agent2agent 1.1.1 → 2.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/a2a/agent.rb +98 -241
  3. data/lib/a2a/{faraday → client}/middleware/json_rpc/request.rb +10 -8
  4. data/lib/a2a/{faraday → client}/middleware/json_rpc/response.rb +10 -9
  5. data/lib/a2a/{faraday → client}/middleware/rest/request.rb +24 -18
  6. data/lib/a2a/{faraday → client}/middleware/rest/response.rb +12 -8
  7. data/lib/a2a/{faraday → client}/middleware/schema_request.rb +9 -8
  8. data/lib/a2a/client.rb +40 -39
  9. data/lib/a2a/errors/json_rpc_error.rb +1 -2
  10. data/lib/a2a/errors/rest_error.rb +1 -2
  11. data/lib/a2a/errors.rb +1 -2
  12. data/lib/a2a/protocol/json_schema/definition.rb +276 -0
  13. data/lib/a2a/protocol/json_schema/validation_error.rb +132 -0
  14. data/lib/a2a/{schema.rb → protocol/json_schema.rb} +211 -208
  15. data/lib/a2a/protocol/protobuf.rb +447 -0
  16. data/lib/a2a/protocol.rb +31 -0
  17. data/lib/a2a/server/bindings/grpc.rb +37 -0
  18. data/lib/a2a/server/bindings/json_rpc.rb +29 -9
  19. data/lib/a2a/server/bindings/rest.rb +26 -13
  20. data/lib/a2a/server/middleware/extract_message.rb +145 -0
  21. data/lib/a2a/{middleware → server/middleware}/fetch_task.rb +82 -89
  22. data/lib/a2a/{middleware → server/middleware}/limit_history_length.rb +40 -49
  23. data/lib/a2a/{middleware → server/middleware}/limit_pagination_size.rb +36 -42
  24. data/lib/a2a/server/middleware/sse_stream.rb +236 -0
  25. data/lib/a2a/{sse → server/sse}/event_parser.rb +70 -69
  26. data/lib/a2a/server/sse/json_rpc_stream.rb +89 -0
  27. data/lib/a2a/server/sse/rest_stream.rb +63 -0
  28. data/lib/a2a/server/sse/stream.rb +261 -0
  29. data/lib/a2a/server/triage.rb +35 -4
  30. data/lib/a2a/server/well_known.rb +27 -0
  31. data/lib/a2a/server.rb +44 -58
  32. data/lib/a2a/test_helpers.rb +34 -75
  33. data/lib/a2a/version.rb +1 -1
  34. data/lib/a2a.rb +7 -11
  35. data/lib/traces/provider/a2a/agent.rb +25 -0
  36. data/lib/traces/provider/a2a.rb +1 -1
  37. metadata +39 -37
  38. data/lib/a2a/middleware/extract_message.rb +0 -120
  39. data/lib/a2a/middleware/sse_stream.rb +0 -235
  40. data/lib/a2a/proto.rb +0 -444
  41. data/lib/a2a/schema/definition.rb +0 -148
  42. data/lib/a2a/schema/validation_error.rb +0 -129
  43. data/lib/a2a/server/dispatcher.rb +0 -127
  44. data/lib/a2a/sse/json_rpc_stream.rb +0 -88
  45. data/lib/a2a/sse/rest_stream.rb +0 -62
  46. data/lib/a2a/sse/stream.rb +0 -257
  47. data/lib/traces/provider/a2a/server/dispatcher.rb +0 -26
  48. /data/lib/a2a/{middleware.rb → server/middleware.rb} +0 -0
  49. /data/lib/a2a/{sse.rb → server/sse.rb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43f9f9cd543c06630586eb7ca1e389d8b2b6429aa378c0b6191c1e98bbf3191f
4
- data.tar.gz: d5f41c6a497994e01da926886708b78fdce6700edfd5ec6ee818da4ccbd2e08a
3
+ metadata.gz: dcbe8b4376828c18b601b4ed92a16afc7adc63db6ebb3354b144d1510c0a91bb
4
+ data.tar.gz: '096dd66beddb0b01c21fb8fa631318cb202a8abde4cc475de1c05fd3327cdbb3'
5
5
  SHA512:
6
- metadata.gz: ae5e8f96ad9ae28ac237ec6b578e0a16e643ee23df9cd727758b8d3cccdd6eb135f94000dd4383d1c2af4b2f0f5a88dcb24f40a9eef3500a8f1eebe35e645a66
7
- data.tar.gz: 7c2d7718f246add65270cb51f0ddca3840b04d360959348f1cc8c03440a339646f2f81c5b1318bb28aa01f9536fb1a1a0023a75679137026b48ee8bb3ccfecb6
6
+ metadata.gz: d5ff820324edc012ed3c6f417187afe73381572cfa078a226bebe770a88d21330eec4df404d4f0080f340900e51044999ec0c685ae278f259d38e6da80e51223
7
+ data.tar.gz: 4c2fb80eb26b98ad58003599e3f8d87fc864876d10f4c007b6c1fb473225883b3d2a288a806ebd7fd5a3f1917fe193dec8f0cf6682cfd0334261c140ed384a7f
data/lib/a2a/agent.rb CHANGED
@@ -1,291 +1,148 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/setup"
4
+ require "console"
4
5
  require "a2a"
5
6
 
6
7
  module A2A
7
- # DSL wrapper that collects operation handlers for an A2A agent.
8
+ # Builds a complete A2A server (a rack app) from a handler block —
9
+ # the one-call entrypoint.
8
10
  #
9
- # An Agent produces handler objects that conform to the Dispatcher's
10
- # contract (#operations, #call). Register an agent on a Server the
11
- # same way you would register a plain handler.
11
+ # The block receives the rack env and returns a domain object
12
+ # (A2A::Protocol::JsonSchema::Definition) the binding layer formats
13
+ # it into HTTP. Route operations with pattern matching:
12
14
  #
13
- # agent = A2A::Agent.new do
14
- # on "SendMessage" do
15
- # respond_with -> (env) {
16
- # A2A::Schema["Send Message Response"].new({})
17
- # }
15
+ # run A2A.agent(agent_card: { "name" => "My Agent" }) do |env|
16
+ # case env["a2a.operation"]
17
+ # in "SendMessage"
18
+ # A2A::Protocol::JsonSchema["Send Message Response"].new({})
19
+ # in "GetTask"
20
+ # task = Task.find_by(id: env["a2a.request"].id)
21
+ # raise A2A::TaskNotFoundError.new(env["a2a.request"].id) unless task
22
+ # task.to_a2a
18
23
  # end
24
+ # end
19
25
  #
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
- # }
26
- # end
26
+ # To route on request contents as well, match a tuple — Definition
27
+ # implements deconstruct_keys, so patterns destructure recursively:
28
+ #
29
+ # case [env["a2a.operation"], env["a2a.request"]]
30
+ # in ["SendMessage", { message: { task_id: String => id } }] if !id.empty?
31
+ # # continuation of task `id`
32
+ # in ["SendMessage", _]
33
+ # # new task
27
34
  # end
28
35
  #
29
- # server.register(agent)
36
+ def self.agent(**options, &block) = Server.new(**options, &block)
37
+
38
+ # Wraps the agent block with the server's error boundary:
39
+ #
40
+ # NoMatchingPatternError -> A2A::UnsupportedOperationError, so an
41
+ # unhandled operation falls out of `case ... in` with no else branch
42
+ # A2A::Error -> returned for the binding layer to format
43
+ # anything else -> logged, returned as a JSON-RPC internal error
30
44
  #
31
45
  class Agent
32
- attr_reader :handlers
33
-
34
46
  def initialize(&block)
35
- @handlers = []
36
- instance_eval(&block) if block
37
- end
38
-
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.
44
- #
45
- # on "SendMessage" do
46
- # use SomeMiddleware
47
- # respond_with -> (env) { ... }
48
- # end
49
- #
50
- def on(*operations, &block)
51
- raise ArgumentError, "on requires at least one operation" if operations.empty?
52
- raise ArgumentError, "on requires a block" unless block
53
-
54
- builder = StackBuilder.new
55
- builder.instance_eval(&block)
56
-
57
- handler = Handler.new(
58
- operations: operations.flatten,
59
- app: builder.to_app
60
- )
61
-
62
- @handlers << handler
63
- handler
64
- end
65
-
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
72
- end
73
-
74
- def use(middleware, *args, &block)
75
- @middleware << [middleware, args, block]
76
- end
77
-
78
- def respond_with(callable)
79
- @terminal = callable
80
- end
47
+ raise ArgumentError, "agent requires a block" unless block
81
48
 
82
- def to_app
83
- raise ArgumentError, "respond_with is required" unless @terminal
84
-
85
- app = Terminal.new(@terminal)
86
-
87
- @middleware.reverse_each do |klass, args, block|
88
- app = klass.new(app, *args, &block)
89
- end
90
-
91
- app
92
- end
93
- end
94
-
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
100
- end
101
-
102
- def call(env)
103
- @callable.call(env)
104
- end
49
+ @block = block
105
50
  end
106
51
 
107
- # Handler object produced by the #on DSL method.
108
- # Conforms to the Dispatcher contract: #operations, #call.
109
- class Handler
110
- attr_reader :operations
111
-
112
- def initialize(operations:, app:)
113
- @operations = operations
114
- @app = app
115
- end
116
-
117
- def call(env)
118
- @app.call(env)
119
- end
52
+ def call(env)
53
+ @block.call(env)
54
+ rescue NoMatchingPatternError
55
+ A2A::UnsupportedOperationError.new(
56
+ message: "Operation not supported: #{env["a2a.operation"]}"
57
+ )
58
+ rescue A2A::Error => e
59
+ e
60
+ rescue => e
61
+ Console.error(self, "Agent raised #{e.class}: #{e.message}", e)
62
+ A2A::Error.new("Internal error", code: -32603, http_status: 500)
120
63
  end
121
64
  end
122
65
  end
123
66
 
124
- test do
125
- describe "A2A::Agent" do
126
- it "registers handlers via the on DSL" do
127
- agent = A2A::Agent.new do
128
- on "SendMessage" do
129
- respond_with -> (env) { :ok }
130
- end
131
- end
132
-
133
- agent.handlers.length.should == 1
134
- agent.handlers.first.operations.should == ["SendMessage"]
135
- end
136
-
137
- it "registers multiple operations on a single handler" do
138
- agent = A2A::Agent.new do
139
- on "SendMessage", "GetTask" do
140
- respond_with -> (env) { :ok }
141
- end
142
- end
143
-
144
- agent.handlers.first.operations.should == ["SendMessage", "GetTask"]
145
- end
146
-
147
- it "raises if on is called without operations" do
148
- lambda {
149
- A2A::Agent.new do
150
- on do
151
- respond_with -> (env) { :ok }
152
- end
153
- end
154
- }.should.raise(ArgumentError)
155
- end
156
-
157
- it "raises if on is called without a block" do
158
- lambda {
159
- agent = A2A::Agent.new
160
- agent.on "SendMessage"
161
- }.should.raise(ArgumentError)
67
+ __END__
68
+ describe "A2A.agent" do
69
+ it "returns an A2A::Server" do
70
+ server = A2A.agent(agent_card: { "name" => "Test" }) { |env| nil }
71
+ server.should.be.is_a(A2A::Server)
162
72
  end
73
+ end
163
74
 
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)
75
+ describe "A2A::Agent" do
76
+ it "requires a block" do
77
+ lambda { A2A::Agent.new }.should.raise(ArgumentError)
172
78
  end
173
79
 
174
- it "returns the respond_with lambda result" do
175
- agent = A2A::Agent.new do
176
- on "SendMessage" do
177
- respond_with -> (env) { { "echo" => true } }
178
- end
179
- end
180
-
181
- env = { "a2a.request" => {} }
182
- result = agent.handlers.first.call(env)
183
- result.should == { "echo" => true }
80
+ it "calls the block with env and returns its result" do
81
+ agent = A2A::Agent.new { |env| env["a2a.operation"] }
82
+ agent.call({ "a2a.operation" => "SendMessage" }).should == "SendMessage"
184
83
  end
185
84
 
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 }
85
+ it "routes operations with pattern matching" do
86
+ agent = A2A::Agent.new do |env|
87
+ case env["a2a.operation"]
88
+ in "SendMessage" then :sent
89
+ in "GetTask" then :got
196
90
  end
197
91
  end
198
92
 
199
- env = { "a2a.request" => {} }
200
- result = agent.handlers.first.call(env)
201
- result.should == schema_obj
93
+ agent.call({ "a2a.operation" => "SendMessage" }).should == :sent
94
+ agent.call({ "a2a.operation" => "GetTask" }).should == :got
202
95
  end
203
96
 
204
- it "passes env to the respond_with lambda" do
205
- seen_env = nil
206
-
207
- agent = A2A::Agent.new do
208
- on "SendMessage" do
209
- respond_with -> (env) { seen_env = env; :ok }
97
+ it "returns UnsupportedOperationError when no pattern matches" do
98
+ agent = A2A::Agent.new do |env|
99
+ case env["a2a.operation"]
100
+ in "SendMessage" then :sent
210
101
  end
211
102
  end
212
103
 
213
- env = { "a2a.request" => { "msg" => "hi" } }
214
- agent.handlers.first.call(env)
215
- seen_env.should == env
104
+ result = agent.call({ "a2a.operation" => "UnknownOp" })
105
+ result.should.be.is_a(A2A::UnsupportedOperationError)
106
+ result.code.should == -32004
216
107
  end
217
108
 
218
- it "executes middleware in order" do
219
- order = []
109
+ it "returns A2A::Error when the block raises one" do
110
+ agent = A2A::Agent.new { |env| raise A2A::TaskNotFoundError.new("t-1") }
220
111
 
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
-
231
- agent = A2A::Agent.new do
232
- on "SendMessage" do
233
- use mw1
234
- use mw2
235
- respond_with -> (env) { order << :terminal; :done }
236
- end
237
- end
238
-
239
- env = { "a2a.request" => {} }
240
- agent.handlers.first.call(env)
241
- order.should == [:mw1, :mw2, :terminal]
112
+ result = agent.call({})
113
+ result.should.be.is_a(A2A::TaskNotFoundError)
114
+ result.code.should == -32001
242
115
  end
243
116
 
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
117
+ it "returns internal error when the block raises unexpectedly" do
118
+ agent = A2A::Agent.new { |env| raise "boom" }
249
119
 
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
120
+ result = agent.call({})
121
+ result.should.be.is_a(A2A::Error)
122
+ result.code.should == -32603
123
+ result.http_status.should == 500
260
124
  end
261
125
 
262
- # ── Error behavior ─────────────────────────────────────────────────
263
- # Errors propagate to the Dispatcher which catches them.
264
-
265
- it "lets A2A::Error propagate (Dispatcher catches it)" do
266
- agent = A2A::Agent.new do
267
- on "GetTask" do
268
- respond_with -> (env) {
269
- raise A2A::TaskNotFoundError.new("task-abc")
270
- }
126
+ it "matches request contents via deconstruct_keys" do
127
+ agent = A2A::Agent.new do |env|
128
+ case [env["a2a.operation"], env["a2a.request"]]
129
+ in ["SendMessage", { message: { task_id: String => id } }] if !id.empty?
130
+ [:continuation, id]
131
+ in ["SendMessage", _]
132
+ :new_task
271
133
  end
272
134
  end
273
135
 
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
136
+ request = A2A::Protocol::JsonSchema["Send Message Request"].new(
137
+ message: { "taskId" => "t-1", "role" => "ROLE_USER" }
138
+ )
139
+ agent.call({ "a2a.operation" => "SendMessage", "a2a.request" => request })
140
+ .should == [:continuation, "t-1"]
286
141
 
287
- env = { "a2a.request" => {} }
288
- lambda { agent.handlers.first.call(env) }.should.raise(RuntimeError)
142
+ request = A2A::Protocol::JsonSchema["Send Message Request"].new(
143
+ message: { "role" => "ROLE_USER" }
144
+ )
145
+ agent.call({ "a2a.operation" => "SendMessage", "a2a.request" => request })
146
+ .should == :new_task
289
147
  end
290
148
  end
291
- end
@@ -5,7 +5,7 @@ require "a2a"
5
5
  require "faraday"
6
6
 
7
7
  module A2A
8
- module Faraday
8
+ class Client
9
9
  module Middleware
10
10
  module JsonRpc
11
11
  # Faraday request middleware that wraps the request body in a
@@ -19,7 +19,8 @@ module A2A
19
19
  operation = env.request.context&.dig(:a2a_operation)
20
20
  return unless operation
21
21
 
22
- env.url.path = "/a2a"
22
+ # POST to the server's mount root — env.url already carries the
23
+ # base path (e.g. "/agent1" when mounted at a subpath).
23
24
  env.method = :post
24
25
 
25
26
  env.body = {
@@ -41,13 +42,14 @@ module A2A
41
42
  end
42
43
  end
43
44
 
44
- ::Faraday::Request.register_middleware(a2a_json_rpc: A2A::Faraday::Middleware::JsonRpc::Request)
45
+ ::Faraday::Request.register_middleware(a2a_json_rpc: A2A::Client::Middleware::JsonRpc::Request)
45
46
 
46
- test do
47
- middleware = A2A::Faraday::Middleware::JsonRpc::Request
48
- operation = A2A::Proto.operation("SendMessage")
47
+ __END__
48
+ describe "A2A::Client::Middleware::JsonRpc::Request" do
49
+ middleware = A2A::Client::Middleware::JsonRpc::Request
50
+ operation = A2A::Protocol::Protobuf.operation("SendMessage")
49
51
 
50
- it "wraps body in JSON-RPC 2.0 envelope and sets path to /a2a" do
52
+ it "wraps body in JSON-RPC 2.0 envelope and sets path to /" do
51
53
  env = ::Faraday::Env.new
52
54
  env.url = URI.parse("http://localhost:9292/")
53
55
  env.body = { "message" => { "role" => "ROLE_USER" } }
@@ -56,7 +58,7 @@ test do
56
58
 
57
59
  middleware.new(nil).on_request(env)
58
60
 
59
- env.url.path.should == "/a2a"
61
+ env.url.path.should == "/"
60
62
  env.method.should == :post
61
63
  env.body[:jsonrpc].should == "2.0"
62
64
  env.body[:id].should == 1
@@ -6,11 +6,11 @@ require "faraday"
6
6
  require "json"
7
7
 
8
8
  module A2A
9
- module Faraday
9
+ class Client
10
10
  module Middleware
11
11
  module JsonRpc
12
12
  # Faraday response middleware that unwraps JSON-RPC 2.0 responses
13
- # and converts the result into A2A::Schema::Definition objects.
13
+ # and converts the result into A2A::Protocol::JsonSchema::Definition objects.
14
14
  #
15
15
  # Reads env.request.context[:a2a_operation] to determine which
16
16
  # Schema class to instantiate. If no operation is set, passes through.
@@ -55,11 +55,12 @@ module A2A
55
55
  end
56
56
  end
57
57
 
58
- ::Faraday::Response.register_middleware(a2a_json_rpc: A2A::Faraday::Middleware::JsonRpc::Response)
58
+ ::Faraday::Response.register_middleware(a2a_json_rpc: A2A::Client::Middleware::JsonRpc::Response)
59
59
 
60
- test do
61
- middleware = A2A::Faraday::Middleware::JsonRpc::Response
62
- operation = A2A::Proto.operation("GetTask")
60
+ __END__
61
+ describe "A2A::Client::Middleware::JsonRpc::Response" do
62
+ middleware = A2A::Client::Middleware::JsonRpc::Response
63
+ operation = A2A::Protocol::Protobuf.operation("GetTask")
63
64
 
64
65
  it "unwraps JSON-RPC result into Schema object" do
65
66
  env = ::Faraday::Env.new
@@ -75,7 +76,7 @@ test do
75
76
 
76
77
  middleware.new(nil).on_complete(env)
77
78
 
78
- env.body.should.be.kind_of(A2A::Schema::Definition)
79
+ env.body.should.be.kind_of(A2A::Protocol::JsonSchema::Definition)
79
80
  env.body.id.should == "task-123"
80
81
  env.body.context_id.should == "ctx-456"
81
82
  end
@@ -93,7 +94,7 @@ test do
93
94
  end
94
95
 
95
96
  it "returns raw hash when response_schema is nil" do
96
- op = A2A::Proto.operation("DeleteTaskPushNotificationConfig")
97
+ op = A2A::Protocol::Protobuf.operation("DeleteTaskPushNotificationConfig")
97
98
  env = ::Faraday::Env.new
98
99
  env.body = { "jsonrpc" => "2.0", "id" => 1, "result" => {} }
99
100
  env.request = ::Faraday::RequestOptions.new
@@ -125,7 +126,7 @@ test do
125
126
 
126
127
  middleware.new(nil).on_complete(env)
127
128
 
128
- env.body.should.be.kind_of(A2A::Schema::Definition)
129
+ env.body.should.be.kind_of(A2A::Protocol::JsonSchema::Definition)
129
130
  env.body.id.should == "task-1"
130
131
  end
131
132
  end
@@ -5,7 +5,7 @@ require "a2a"
5
5
  require "faraday"
6
6
 
7
7
  module A2A
8
- module Faraday
8
+ class Client
9
9
  module Middleware
10
10
  module REST
11
11
  # Faraday request middleware that rewrites the request for the
@@ -13,8 +13,9 @@ module A2A
13
13
  #
14
14
  # Reads env.request.context[:a2a_operation] to determine the
15
15
  # HTTP verb and path. Interpolates path parameters from the
16
- # request body (e.g. {id=*} placeholders). For GET/DELETE
17
- # operations, remaining params become query string parameters.
16
+ # request body (e.g. {id=*} placeholders) and prefixes the
17
+ # /rest mount point. For GET/DELETE operations, remaining
18
+ # params become query string parameters.
18
19
  #
19
20
  # Sets Content-Type to application/a2a+json for POST requests.
20
21
  #
@@ -27,12 +28,16 @@ module A2A
27
28
  path = interpolate_path(operation.rest_path, params)
28
29
  remaining = remove_path_params(operation.rest_path, params)
29
30
 
30
- env.url.path = path
31
+ # Prefix /rest under the base path, so servers mounted at a
32
+ # subpath (e.g. "/agent1") keep working.
33
+ env.url.path = "#{env.url.path.chomp("/")}/rest#{path}"
31
34
  env.method = operation.rest_verb.to_sym
32
35
 
33
36
  if [:get, :delete].include?(env.method)
34
- env.params ||= {}
35
- remaining.each { |k, v| env.params[k.to_s] = v }
37
+ # The URL is already built by the time request middleware runs,
38
+ # so query params must be encoded into env.url directly
39
+ # env.params is not consulted after this point.
40
+ env.url.query = URI.encode_www_form(remaining) unless remaining.empty?
36
41
  env.body = nil
37
42
  else
38
43
  # Serialize to JSON here rather than relying on Faraday's
@@ -80,13 +85,14 @@ module A2A
80
85
  end
81
86
  end
82
87
 
83
- ::Faraday::Request.register_middleware(a2a_rest: A2A::Faraday::Middleware::REST::Request)
88
+ ::Faraday::Request.register_middleware(a2a_rest: A2A::Client::Middleware::REST::Request)
84
89
 
85
- test do
86
- middleware = A2A::Faraday::Middleware::REST::Request
90
+ __END__
91
+ describe "A2A::Client::Middleware::REST::Request" do
92
+ middleware = A2A::Client::Middleware::REST::Request
87
93
 
88
94
  it "rewrites POST operation with correct path, verb, and content-type" do
89
- operation = A2A::Proto.operation("SendMessage")
95
+ operation = A2A::Protocol::Protobuf.operation("SendMessage")
90
96
  env = ::Faraday::Env.new
91
97
  env.url = URI.parse("http://localhost:9292/")
92
98
  env.body = { "message" => { "role" => "ROLE_USER" } }
@@ -97,14 +103,14 @@ test do
97
103
 
98
104
  middleware.new(nil).on_request(env)
99
105
 
100
- env.url.path.should == "/message:send"
106
+ env.url.path.should == "/rest/message:send"
101
107
  env.method.should == :post
102
108
  JSON.parse(env.body).should == { "message" => { "role" => "ROLE_USER" } }
103
109
  env.request_headers["content-type"].should == "application/a2a+json"
104
110
  end
105
111
 
106
112
  it "rewrites GET operation with path interpolation and query params" do
107
- operation = A2A::Proto.operation("GetTask")
113
+ operation = A2A::Protocol::Protobuf.operation("GetTask")
108
114
  env = ::Faraday::Env.new
109
115
  env.url = URI.parse("http://localhost:9292/")
110
116
  env.body = { "id" => "task-123", "historyLength" => "5" }
@@ -115,14 +121,14 @@ test do
115
121
 
116
122
  middleware.new(nil).on_request(env)
117
123
 
118
- env.url.path.should == "/tasks/task-123"
124
+ env.url.path.should == "/rest/tasks/task-123"
119
125
  env.method.should == :get
120
126
  env.body.should.be.nil
121
- env.params["historyLength"].should == "5"
127
+ env.url.query.should == "historyLength=5"
122
128
  end
123
129
 
124
130
  it "rewrites DELETE operation with multi-param path interpolation" do
125
- operation = A2A::Proto.operation("DeleteTaskPushNotificationConfig")
131
+ operation = A2A::Protocol::Protobuf.operation("DeleteTaskPushNotificationConfig")
126
132
  env = ::Faraday::Env.new
127
133
  env.url = URI.parse("http://localhost:9292/")
128
134
  env.body = { "taskId" => "task-123", "id" => "config-1" }
@@ -133,13 +139,13 @@ test do
133
139
 
134
140
  middleware.new(nil).on_request(env)
135
141
 
136
- env.url.path.should == "/tasks/task-123/pushNotificationConfigs/config-1"
142
+ env.url.path.should == "/rest/tasks/task-123/pushNotificationConfigs/config-1"
137
143
  env.method.should == :delete
138
144
  env.body.should.be.nil
139
145
  end
140
146
 
141
147
  it "rewrites POST cancel with path interpolation" do
142
- operation = A2A::Proto.operation("CancelTask")
148
+ operation = A2A::Protocol::Protobuf.operation("CancelTask")
143
149
  env = ::Faraday::Env.new
144
150
  env.url = URI.parse("http://localhost:9292/")
145
151
  env.body = { "id" => "task-123", "metadata" => { "key" => "val" } }
@@ -150,7 +156,7 @@ test do
150
156
 
151
157
  middleware.new(nil).on_request(env)
152
158
 
153
- env.url.path.should == "/tasks/task-123:cancel"
159
+ env.url.path.should == "/rest/tasks/task-123:cancel"
154
160
  env.method.should == :post
155
161
  JSON.parse(env.body).should == { "metadata" => { "key" => "val" } }
156
162
  end