agent2agent 1.1.0 → 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 +30 -20
  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 -40
  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: fadb35d489fe59b7b010bafd2ee6e94adeb0d3cb0d8c6baa2d05fefcdd449fef
4
- data.tar.gz: 1fa489faf0e9f155d56d995bb49f208dbc4e28f9ae3bf84796b0bd4af02264b9
3
+ metadata.gz: dcbe8b4376828c18b601b4ed92a16afc7adc63db6ebb3354b144d1510c0a91bb
4
+ data.tar.gz: '096dd66beddb0b01c21fb8fa631318cb202a8abde4cc475de1c05fd3327cdbb3'
5
5
  SHA512:
6
- metadata.gz: 7ce0595b5f4fbb554306d49fe40588ff16b9036e048012b946930d95143dd3f9f98db62cf7e53c9977b8b0d723e469dbe478aa524ee019da57fd1f29e5b90059
7
- data.tar.gz: 8caf8f2af8d3f8becdc7b1404ccaebd0333d05c0452cd68a49b59a0ab419bebed4886b1e8f35c1b7eb131a044cd11f97f5ba1ddc70bbf484f923b8bb55826e1e
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,14 +28,22 @@ 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
- 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?
35
41
  env.body = nil
36
42
  else
37
- env.body = remaining
43
+ # Serialize to JSON here rather than relying on Faraday's
44
+ # built-in :json middleware, which only recognizes
45
+ # application/json and application/vnd.*+json content types.
46
+ env.body = remaining.is_a?(String) ? remaining : JSON.generate(remaining)
38
47
  env.request_headers["content-type"] = "application/a2a+json"
39
48
  end
40
49
  end
@@ -76,13 +85,14 @@ module A2A
76
85
  end
77
86
  end
78
87
 
79
- ::Faraday::Request.register_middleware(a2a_rest: A2A::Faraday::Middleware::REST::Request)
88
+ ::Faraday::Request.register_middleware(a2a_rest: A2A::Client::Middleware::REST::Request)
80
89
 
81
- test do
82
- middleware = A2A::Faraday::Middleware::REST::Request
90
+ __END__
91
+ describe "A2A::Client::Middleware::REST::Request" do
92
+ middleware = A2A::Client::Middleware::REST::Request
83
93
 
84
94
  it "rewrites POST operation with correct path, verb, and content-type" do
85
- operation = A2A::Proto.operation("SendMessage")
95
+ operation = A2A::Protocol::Protobuf.operation("SendMessage")
86
96
  env = ::Faraday::Env.new
87
97
  env.url = URI.parse("http://localhost:9292/")
88
98
  env.body = { "message" => { "role" => "ROLE_USER" } }
@@ -93,14 +103,14 @@ test do
93
103
 
94
104
  middleware.new(nil).on_request(env)
95
105
 
96
- env.url.path.should == "/message:send"
106
+ env.url.path.should == "/rest/message:send"
97
107
  env.method.should == :post
98
- env.body.should == { "message" => { "role" => "ROLE_USER" } }
108
+ JSON.parse(env.body).should == { "message" => { "role" => "ROLE_USER" } }
99
109
  env.request_headers["content-type"].should == "application/a2a+json"
100
110
  end
101
111
 
102
112
  it "rewrites GET operation with path interpolation and query params" do
103
- operation = A2A::Proto.operation("GetTask")
113
+ operation = A2A::Protocol::Protobuf.operation("GetTask")
104
114
  env = ::Faraday::Env.new
105
115
  env.url = URI.parse("http://localhost:9292/")
106
116
  env.body = { "id" => "task-123", "historyLength" => "5" }
@@ -111,14 +121,14 @@ test do
111
121
 
112
122
  middleware.new(nil).on_request(env)
113
123
 
114
- env.url.path.should == "/tasks/task-123"
124
+ env.url.path.should == "/rest/tasks/task-123"
115
125
  env.method.should == :get
116
126
  env.body.should.be.nil
117
- env.params["historyLength"].should == "5"
127
+ env.url.query.should == "historyLength=5"
118
128
  end
119
129
 
120
130
  it "rewrites DELETE operation with multi-param path interpolation" do
121
- operation = A2A::Proto.operation("DeleteTaskPushNotificationConfig")
131
+ operation = A2A::Protocol::Protobuf.operation("DeleteTaskPushNotificationConfig")
122
132
  env = ::Faraday::Env.new
123
133
  env.url = URI.parse("http://localhost:9292/")
124
134
  env.body = { "taskId" => "task-123", "id" => "config-1" }
@@ -129,13 +139,13 @@ test do
129
139
 
130
140
  middleware.new(nil).on_request(env)
131
141
 
132
- env.url.path.should == "/tasks/task-123/pushNotificationConfigs/config-1"
142
+ env.url.path.should == "/rest/tasks/task-123/pushNotificationConfigs/config-1"
133
143
  env.method.should == :delete
134
144
  env.body.should.be.nil
135
145
  end
136
146
 
137
147
  it "rewrites POST cancel with path interpolation" do
138
- operation = A2A::Proto.operation("CancelTask")
148
+ operation = A2A::Protocol::Protobuf.operation("CancelTask")
139
149
  env = ::Faraday::Env.new
140
150
  env.url = URI.parse("http://localhost:9292/")
141
151
  env.body = { "id" => "task-123", "metadata" => { "key" => "val" } }
@@ -146,9 +156,9 @@ test do
146
156
 
147
157
  middleware.new(nil).on_request(env)
148
158
 
149
- env.url.path.should == "/tasks/task-123:cancel"
159
+ env.url.path.should == "/rest/tasks/task-123:cancel"
150
160
  env.method.should == :post
151
- env.body.should == { "metadata" => { "key" => "val" } }
161
+ JSON.parse(env.body).should == { "metadata" => { "key" => "val" } }
152
162
  end
153
163
 
154
164
  it "passes through when no operation is set" do