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
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "a2a"
5
+
6
+ module A2A
7
+ class Server
8
+ module Middleware
9
+ # Extracts text from the request message's parts and sets
10
+ # `env["a2a.message"]` to the joined text string.
11
+ #
12
+ # Part of the A2A::Server middleware stack. Requests whose
13
+ # operation carries no message pass through untouched, so
14
+ # env["a2a.message"] is only present when a message is.
15
+ #
16
+ # Replaces the common `extract_text` lambda pattern found in examples:
17
+ #
18
+ # extract_text = ->(message) {
19
+ # parts = message.parts || []
20
+ # parts.filter_map { |p| p.text }.join("\n")
21
+ # }
22
+ #
23
+ class ExtractMessage
24
+ def initialize(app)
25
+ @app = app
26
+ end
27
+
28
+ def call(env)
29
+ request = env["a2a.request"]
30
+
31
+ if request.respond_to?(:message) && (message = request.message)
32
+ parts = message.parts || []
33
+
34
+ env["a2a.message"] = parts.filter_map { |p|
35
+ p.respond_to?(:text) ? p.text : p["text"]
36
+ }.join("\n")
37
+ end
38
+
39
+ @app.call(env)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ __END__
47
+ describe "A2A::Server::Middleware::ExtractMessage" do
48
+ it "extracts text from message parts" do
49
+ part1 = Object.new
50
+ part1.define_singleton_method(:text) { "hello" }
51
+ part2 = Object.new
52
+ part2.define_singleton_method(:text) { "world" }
53
+
54
+ message = Object.new
55
+ message.define_singleton_method(:parts) { [part1, part2] }
56
+
57
+ request = Object.new
58
+ request.define_singleton_method(:message) { message }
59
+
60
+ downstream = -> (env) { env["a2a.message"] }
61
+ mw = A2A::Server::Middleware::ExtractMessage.new(downstream)
62
+ env = { "a2a.request" => request }
63
+
64
+ result = mw.call(env)
65
+ result.should == "hello\nworld"
66
+ end
67
+
68
+ it "handles nil parts" do
69
+ message = Object.new
70
+ message.define_singleton_method(:parts) { nil }
71
+
72
+ request = Object.new
73
+ request.define_singleton_method(:message) { message }
74
+
75
+ downstream = -> (env) { env["a2a.message"] }
76
+ mw = A2A::Server::Middleware::ExtractMessage.new(downstream)
77
+ env = { "a2a.request" => request }
78
+
79
+ result = mw.call(env)
80
+ result.should == ""
81
+ end
82
+
83
+ it "skips parts without text" do
84
+ part1 = Object.new
85
+ part1.define_singleton_method(:text) { "hello" }
86
+ part2 = Object.new
87
+ part2.define_singleton_method(:text) { nil }
88
+
89
+ message = Object.new
90
+ message.define_singleton_method(:parts) { [part1, part2] }
91
+
92
+ request = Object.new
93
+ request.define_singleton_method(:message) { message }
94
+
95
+ downstream = -> (env) { env["a2a.message"] }
96
+ mw = A2A::Server::Middleware::ExtractMessage.new(downstream)
97
+ env = { "a2a.request" => request }
98
+
99
+ result = mw.call(env)
100
+ result.should == "hello"
101
+ end
102
+
103
+ it "handles hash-style parts" do
104
+ message = Object.new
105
+ message.define_singleton_method(:parts) { [{ "text" => "from hash" }] }
106
+
107
+ request = Object.new
108
+ request.define_singleton_method(:message) { message }
109
+
110
+ downstream = -> (env) { env["a2a.message"] }
111
+ mw = A2A::Server::Middleware::ExtractMessage.new(downstream)
112
+ env = { "a2a.request" => request }
113
+
114
+ result = mw.call(env)
115
+ result.should == "from hash"
116
+ end
117
+
118
+ it "passes through when the request has no message reader" do
119
+ request = Object.new
120
+
121
+ downstream = -> (env) { env.key?("a2a.message") }
122
+ mw = A2A::Server::Middleware::ExtractMessage.new(downstream)
123
+ env = { "a2a.request" => request }
124
+
125
+ mw.call(env).should == false
126
+ end
127
+
128
+ it "passes through when the message is nil" do
129
+ request = Object.new
130
+ request.define_singleton_method(:message) { nil }
131
+
132
+ downstream = -> (env) { env.key?("a2a.message") }
133
+ mw = A2A::Server::Middleware::ExtractMessage.new(downstream)
134
+ env = { "a2a.request" => request }
135
+
136
+ mw.call(env).should == false
137
+ end
138
+
139
+ it "passes through when there is no request" do
140
+ downstream = -> (env) { env.key?("a2a.message") }
141
+ mw = A2A::Server::Middleware::ExtractMessage.new(downstream)
142
+
143
+ mw.call({}).should == false
144
+ end
145
+ end
@@ -4,90 +4,84 @@ require "bundler/setup"
4
4
  require "a2a"
5
5
 
6
6
  module A2A
7
- module Middleware
8
- # Loads a task from the store and places it on `env["a2a.task"]`.
9
- # Sets `env["a2a.task"]` to nil when the ID is absent or the task
10
- # does not exist. Does not raise.
11
- #
12
- # The task ID is read from the request — by default from `request.id`.
13
- # Use `id_field:` to read from a different field, and `from:` to
14
- # read from a nested object on the request.
15
- #
16
- # Usage:
17
- #
18
- # on "SendMessage" do
19
- # use A2A::Middleware::FetchTask, store: sqlite_store, id_field: :task_id, from: :message
20
- # respond_with -> (env) {
21
- # existing = env["a2a.task"] # nil if new task
22
- # }
23
- # end
24
- #
25
- class FetchTask
26
- def initialize(app, store:, id_field: :id, from: nil)
27
- @app = app
28
- @store = store
29
- @id_field = id_field
30
- @from = from
7
+ class Server
8
+ module Middleware
9
+ # Loads a task from the store and places it on `env["a2a.task"]`.
10
+ # Sets `env["a2a.task"]` to nil when the ID is absent or the task
11
+ # does not exist. Does not raise.
12
+ #
13
+ # The task ID is read from the request by default from `request.id`.
14
+ # Use `id_field:` to read from a different field, and `from:` to
15
+ # read from a nested object on the request.
16
+ #
17
+ # Not part of the default A2A::Server stack (it needs a store, and
18
+ # the ID field varies per operation) — wrap your agent with it:
19
+ #
20
+ # agent = A2A::Server::Middleware::FetchTask.new(
21
+ # A2A.agent { |env| existing = env["a2a.task"] }, # nil if new task
22
+ # store: sqlite_store, id_field: :task_id, from: :message
23
+ # )
24
+ #
25
+ class FetchTask
26
+ def initialize(app, store:, id_field: :id, from: nil)
27
+ @app = app
28
+ @store = store
29
+ @id_field = id_field
30
+ @from = from
31
+ end
32
+
33
+ def call(env)
34
+ request = env["a2a.request"]
35
+ source = @from ? request.public_send(@from) : request
36
+ id = source.public_send(@id_field)
37
+
38
+ env["a2a.task"] = id.to_s.empty? ? nil : @store.get(id)
39
+
40
+ @app.call(env)
41
+ end
31
42
  end
32
43
 
33
- def call(env)
34
- request = env["a2a.request"]
35
- source = @from ? request.public_send(@from) : request
36
- id = source.public_send(@id_field)
37
-
38
- env["a2a.task"] = id.to_s.empty? ? nil : @store.get(id)
39
-
40
- @app.call(env)
44
+ # Loads a task from the store and places it on `env["a2a.task"]`.
45
+ # Raises `A2A::TaskNotFoundError` if the task does not exist.
46
+ #
47
+ # The task ID is read from the request — by default from `request.id`.
48
+ # Use `id_field:` to read from a different field, and `from:` to
49
+ # read from a nested object on the request.
50
+ #
51
+ # Not part of the default A2A::Server stack — wrap your agent with it:
52
+ #
53
+ # agent = A2A::Server::Middleware::FetchTaskOrRaise.new(
54
+ # A2A.agent { |env| task = env["a2a.task"] },
55
+ # store: sqlite_store
56
+ # )
57
+ #
58
+ class FetchTaskOrRaise
59
+ def initialize(app, store:, id_field: :id, from: nil)
60
+ @app = app
61
+ @store = store
62
+ @id_field = id_field
63
+ @from = from
64
+ end
65
+
66
+ def call(env)
67
+ request = env["a2a.request"]
68
+ source = @from ? request.public_send(@from) : request
69
+ id = source.public_send(@id_field)
70
+
71
+ task = @store.get(id)
72
+ raise A2A::TaskNotFoundError.new(id) unless task
73
+
74
+ env["a2a.task"] = task
75
+
76
+ @app.call(env)
77
+ end
41
78
  end
42
79
  end
43
-
44
- # Loads a task from the store and places it on `env["a2a.task"]`.
45
- # Raises `A2A::TaskNotFoundError` if the task does not exist.
46
- #
47
- # The task ID is read from the request — by default from `request.id`.
48
- # Use `id_field:` to read from a different field, and `from:` to
49
- # read from a nested object on the request.
50
- #
51
- # Usage:
52
- #
53
- # on "GetTask" do
54
- # use A2A::Middleware::FetchTaskOrRaise, store: sqlite_store
55
- # respond_with -> (env) {
56
- # task = env["a2a.task"]
57
- # }
58
- # end
59
- #
60
- # on "GetTaskPushNotificationConfig" do
61
- # use A2A::Middleware::FetchTaskOrRaise, store: sqlite_store, id_field: :task_id
62
- # respond_with -> (env) { ... }
63
- # end
64
- #
65
- class FetchTaskOrRaise
66
- def initialize(app, store:, id_field: :id, from: nil)
67
- @app = app
68
- @store = store
69
- @id_field = id_field
70
- @from = from
71
- end
72
-
73
- def call(env)
74
- request = env["a2a.request"]
75
- source = @from ? request.public_send(@from) : request
76
- id = source.public_send(@id_field)
77
-
78
- task = @store.get(id)
79
- raise A2A::TaskNotFoundError.new(id) unless task
80
-
81
- env["a2a.task"] = task
82
-
83
- @app.call(env)
84
- end
85
- end
86
- end
80
+ end
87
81
  end
88
82
 
89
- test do
90
- describe "A2A::Middleware::FetchTask" do
83
+ __END__
84
+ describe "A2A::Server::Middleware::FetchTask" do
91
85
  before do
92
86
  @store = Object.new
93
87
  end
@@ -100,7 +94,7 @@ test do
100
94
  request.define_singleton_method(:id) { "t-1" }
101
95
 
102
96
  downstream = -> (env) { env["a2a.task"] }
103
- mw = A2A::Middleware::FetchTask.new(downstream, store: @store)
97
+ mw = A2A::Server::Middleware::FetchTask.new(downstream, store: @store)
104
98
  env = { "a2a.request" => request }
105
99
 
106
100
  mw.call(env).should == task_data
@@ -113,7 +107,7 @@ test do
113
107
  request.define_singleton_method(:id) { "missing" }
114
108
 
115
109
  downstream = -> (env) { env["a2a.task"] }
116
- mw = A2A::Middleware::FetchTask.new(downstream, store: @store)
110
+ mw = A2A::Server::Middleware::FetchTask.new(downstream, store: @store)
117
111
  env = { "a2a.request" => request }
118
112
 
119
113
  mw.call(env).should.be.nil
@@ -124,7 +118,7 @@ test do
124
118
  request.define_singleton_method(:id) { "" }
125
119
 
126
120
  downstream = -> (env) { env["a2a.task"] }
127
- mw = A2A::Middleware::FetchTask.new(downstream, store: @store)
121
+ mw = A2A::Server::Middleware::FetchTask.new(downstream, store: @store)
128
122
  env = { "a2a.request" => request }
129
123
 
130
124
  mw.call(env).should.be.nil
@@ -138,7 +132,7 @@ test do
138
132
  request.define_singleton_method(:message) { message }
139
133
 
140
134
  downstream = -> (env) { env["a2a.task"] }
141
- mw = A2A::Middleware::FetchTask.new(downstream, store: @store, id_field: :task_id, from: :message)
135
+ mw = A2A::Server::Middleware::FetchTask.new(downstream, store: @store, id_field: :task_id, from: :message)
142
136
  env = { "a2a.request" => request }
143
137
 
144
138
  mw.call(env).should.be.nil
@@ -155,14 +149,14 @@ test do
155
149
  request.define_singleton_method(:message) { message }
156
150
 
157
151
  downstream = -> (env) { env["a2a.task"] }
158
- mw = A2A::Middleware::FetchTask.new(downstream, store: @store, id_field: :task_id, from: :message)
152
+ mw = A2A::Server::Middleware::FetchTask.new(downstream, store: @store, id_field: :task_id, from: :message)
159
153
  env = { "a2a.request" => request }
160
154
 
161
155
  mw.call(env).should == task_data
162
156
  end
163
157
  end
164
158
 
165
- describe "A2A::Middleware::FetchTaskOrRaise" do
159
+ describe "A2A::Server::Middleware::FetchTaskOrRaise" do
166
160
  before do
167
161
  @store = Object.new
168
162
  end
@@ -175,7 +169,7 @@ test do
175
169
  request.define_singleton_method(:id) { "t-1" }
176
170
 
177
171
  downstream = -> (env) { env["a2a.task"] }
178
- mw = A2A::Middleware::FetchTaskOrRaise.new(downstream, store: @store)
172
+ mw = A2A::Server::Middleware::FetchTaskOrRaise.new(downstream, store: @store)
179
173
  env = { "a2a.request" => request }
180
174
 
181
175
  mw.call(env).should == task_data
@@ -188,7 +182,7 @@ test do
188
182
  request.define_singleton_method(:id) { "missing" }
189
183
 
190
184
  downstream = -> (env) { :should_not_reach }
191
- mw = A2A::Middleware::FetchTaskOrRaise.new(downstream, store: @store)
185
+ mw = A2A::Server::Middleware::FetchTaskOrRaise.new(downstream, store: @store)
192
186
  env = { "a2a.request" => request }
193
187
 
194
188
  lambda { mw.call(env) }.should.raise(A2A::TaskNotFoundError)
@@ -202,7 +196,7 @@ test do
202
196
  request.define_singleton_method(:task_id) { "t-2" }
203
197
 
204
198
  downstream = -> (env) { env["a2a.task"] }
205
- mw = A2A::Middleware::FetchTaskOrRaise.new(downstream, store: @store, id_field: :task_id)
199
+ mw = A2A::Server::Middleware::FetchTaskOrRaise.new(downstream, store: @store, id_field: :task_id)
206
200
  env = { "a2a.request" => request }
207
201
 
208
202
  mw.call(env).should == task_data
@@ -219,10 +213,9 @@ test do
219
213
  request.define_singleton_method(:message) { message }
220
214
 
221
215
  downstream = -> (env) { env["a2a.task"] }
222
- mw = A2A::Middleware::FetchTaskOrRaise.new(downstream, store: @store, id_field: :task_id, from: :message)
216
+ mw = A2A::Server::Middleware::FetchTaskOrRaise.new(downstream, store: @store, id_field: :task_id, from: :message)
223
217
  env = { "a2a.request" => request }
224
218
 
225
219
  mw.call(env).should == task_data
226
220
  end
227
221
  end
228
- end
@@ -4,62 +4,54 @@ require "bundler/setup"
4
4
  require "a2a"
5
5
 
6
6
  module A2A
7
- module Middleware
8
- # Resolves the effective history length limit and sets
9
- # `env["a2a.history_length"]` to an integer.
10
- #
11
- # The server max is required. The effective limit is the *minimum*
12
- # of the client-requested value and the server cap. When the client
13
- # doesn't specify a history_length, the server cap is used.
14
- #
15
- # `env["a2a.history_length"]` is always an Integer (0..max).
16
- # The handler applies it unconditionally:
17
- #
18
- # result["history"] = task[:history]&.last(limit)
19
- #
20
- # Usage:
21
- #
22
- # on "GetTask" do
23
- # use A2A::Middleware::FetchTaskOrRaise, store: sqlite_store
24
- # use A2A::Middleware::LimitHistoryLength, 20
25
- # respond_with -> (env) {
26
- # task = env["a2a.task"]
27
- # limit = env["a2a.history_length"]
28
- # # ...
29
- # result["history"] = task[:history]&.last(limit)
30
- # }
31
- # end
32
- #
33
- class LimitHistoryLength
34
- def initialize(app, max)
35
- @app = app
36
- @max = max
37
- end
7
+ class Server
8
+ module Middleware
9
+ # Resolves the effective history length limit and sets
10
+ # `env["a2a.history_length"]` to an integer.
11
+ #
12
+ # The server max is required. The effective limit is the *minimum*
13
+ # of the client-requested value and the server cap. When the client
14
+ # doesn't specify a history_length, the server cap is used.
15
+ #
16
+ # `env["a2a.history_length"]` is always an Integer (0..max).
17
+ # The agent applies it unconditionally:
18
+ #
19
+ # result["history"] = task[:history]&.last(limit)
20
+ #
21
+ # Part of the A2A::Server middleware stack — the max comes from
22
+ # `A2A::Server.new(history_length: 20)`.
23
+ #
24
+ class LimitHistoryLength
25
+ def initialize(app, max)
26
+ @app = app
27
+ @max = max
28
+ end
38
29
 
39
- def call(env)
40
- request = env["a2a.request"]
41
- limit = @max
30
+ def call(env)
31
+ request = env["a2a.request"]
32
+ limit = @max
42
33
 
43
- if request.respond_to?(:history_length) && request.history_length
44
- limit = [request.history_length.to_i, @max].min
45
- end
34
+ if request.respond_to?(:history_length) && request.history_length
35
+ limit = [request.history_length.to_i, @max].min
36
+ end
46
37
 
47
- env["a2a.history_length"] = limit
38
+ env["a2a.history_length"] = limit
48
39
 
49
- @app.call(env)
40
+ @app.call(env)
41
+ end
50
42
  end
51
43
  end
52
- end
44
+ end
53
45
  end
54
46
 
55
- test do
56
- describe "A2A::Middleware::LimitHistoryLength" do
47
+ __END__
48
+ describe "A2A::Server::Middleware::LimitHistoryLength" do
57
49
  it "defaults to server max when client does not specify" do
58
50
  request = Object.new
59
51
  request.define_singleton_method(:history_length) { nil }
60
52
 
61
53
  downstream = -> (env) { env["a2a.history_length"] }
62
- mw = A2A::Middleware::LimitHistoryLength.new(downstream, 20)
54
+ mw = A2A::Server::Middleware::LimitHistoryLength.new(downstream, 20)
63
55
  env = { "a2a.request" => request }
64
56
 
65
57
  mw.call(env).should == 20
@@ -70,7 +62,7 @@ test do
70
62
  request.define_singleton_method(:history_length) { 5 }
71
63
 
72
64
  downstream = -> (env) { env["a2a.history_length"] }
73
- mw = A2A::Middleware::LimitHistoryLength.new(downstream, 20)
65
+ mw = A2A::Server::Middleware::LimitHistoryLength.new(downstream, 20)
74
66
  env = { "a2a.request" => request }
75
67
 
76
68
  mw.call(env).should == 5
@@ -81,7 +73,7 @@ test do
81
73
  request.define_singleton_method(:history_length) { 100 }
82
74
 
83
75
  downstream = -> (env) { env["a2a.history_length"] }
84
- mw = A2A::Middleware::LimitHistoryLength.new(downstream, 20)
76
+ mw = A2A::Server::Middleware::LimitHistoryLength.new(downstream, 20)
85
77
  env = { "a2a.request" => request }
86
78
 
87
79
  mw.call(env).should == 20
@@ -92,7 +84,7 @@ test do
92
84
  request.define_singleton_method(:history_length) { 0 }
93
85
 
94
86
  downstream = -> (env) { env["a2a.history_length"] }
95
- mw = A2A::Middleware::LimitHistoryLength.new(downstream, 20)
87
+ mw = A2A::Server::Middleware::LimitHistoryLength.new(downstream, 20)
96
88
  env = { "a2a.request" => request }
97
89
 
98
90
  mw.call(env).should == 0
@@ -103,7 +95,7 @@ test do
103
95
  request.define_singleton_method(:history_length) { "3" }
104
96
 
105
97
  downstream = -> (env) { env["a2a.history_length"] }
106
- mw = A2A::Middleware::LimitHistoryLength.new(downstream, 20)
98
+ mw = A2A::Server::Middleware::LimitHistoryLength.new(downstream, 20)
107
99
  env = { "a2a.request" => request }
108
100
 
109
101
  mw.call(env).should == 3
@@ -114,10 +106,9 @@ test do
114
106
  request.define_singleton_method(:history_length) { nil }
115
107
 
116
108
  downstream = -> (env) { env["a2a.history_length"] }
117
- mw = A2A::Middleware::LimitHistoryLength.new(downstream, 10)
109
+ mw = A2A::Server::Middleware::LimitHistoryLength.new(downstream, 10)
118
110
  env = { "a2a.request" => request }
119
111
 
120
112
  mw.call(env).should.be.kind_of(Integer)
121
113
  end
122
114
  end
123
- end
@@ -4,54 +4,49 @@ require "bundler/setup"
4
4
  require "a2a"
5
5
 
6
6
  module A2A
7
- module Middleware
8
- # Clamps `request.page_size` to a valid range and sets
9
- # `env["a2a.page_size"]` for downstream handlers.
10
- #
11
- # Accepts a single integer — the maximum page size (also used as the
12
- # default when the client doesn't specify one). Clamps to [1, max].
13
- #
14
- # Usage:
15
- #
16
- # on "ListTasks" do
17
- # use A2A::Middleware::LimitPaginationSize, 50
18
- # respond_with -> (env) {
19
- # page_size = env["a2a.page_size"]
20
- # # ...
21
- # }
22
- # end
23
- #
24
- class LimitPaginationSize
25
- def initialize(app, max = 100)
26
- @app = app
27
- @max = max
28
- end
7
+ class Server
8
+ module Middleware
9
+ # Clamps `request.page_size` to a valid range and sets
10
+ # `env["a2a.page_size"]` for the agent.
11
+ #
12
+ # Accepts a single integer — the maximum page size (also used as the
13
+ # default when the client doesn't specify one). Clamps to [1, max].
14
+ #
15
+ # Part of the A2A::Server middleware stack — the max comes from
16
+ # `A2A::Server.new(page_size: 50)`.
17
+ #
18
+ class LimitPaginationSize
19
+ def initialize(app, max = 100)
20
+ @app = app
21
+ @max = max
22
+ end
29
23
 
30
- def call(env)
31
- request = env["a2a.request"]
24
+ def call(env)
25
+ request = env["a2a.request"]
32
26
 
33
- page_size = @max
34
- if request.respond_to?(:page_size) && request.page_size
35
- ps = request.page_size.to_i
36
- page_size = [[ps, 1].max, @max].min
37
- end
27
+ page_size = @max
28
+ if request.respond_to?(:page_size) && request.page_size
29
+ ps = request.page_size.to_i
30
+ page_size = [[ps, 1].max, @max].min
31
+ end
38
32
 
39
- env["a2a.page_size"] = page_size
33
+ env["a2a.page_size"] = page_size
40
34
 
41
- @app.call(env)
35
+ @app.call(env)
36
+ end
42
37
  end
43
38
  end
44
- end
39
+ end
45
40
  end
46
41
 
47
- test do
48
- describe "A2A::Middleware::LimitPaginationSize" do
42
+ __END__
43
+ describe "A2A::Server::Middleware::LimitPaginationSize" do
49
44
  it "uses max as default page size when not specified by client" do
50
45
  request = Object.new
51
46
  request.define_singleton_method(:page_size) { nil }
52
47
 
53
48
  downstream = -> (env) { env["a2a.page_size"] }
54
- mw = A2A::Middleware::LimitPaginationSize.new(downstream)
49
+ mw = A2A::Server::Middleware::LimitPaginationSize.new(downstream)
55
50
  env = { "a2a.request" => request }
56
51
 
57
52
  result = mw.call(env)
@@ -63,7 +58,7 @@ test do
63
58
  request.define_singleton_method(:page_size) { nil }
64
59
 
65
60
  downstream = -> (env) { env["a2a.page_size"] }
66
- mw = A2A::Middleware::LimitPaginationSize.new(downstream, 25)
61
+ mw = A2A::Server::Middleware::LimitPaginationSize.new(downstream, 25)
67
62
  env = { "a2a.request" => request }
68
63
 
69
64
  result = mw.call(env)
@@ -75,7 +70,7 @@ test do
75
70
  request.define_singleton_method(:page_size) { 0 }
76
71
 
77
72
  downstream = -> (env) { env["a2a.page_size"] }
78
- mw = A2A::Middleware::LimitPaginationSize.new(downstream)
73
+ mw = A2A::Server::Middleware::LimitPaginationSize.new(downstream)
79
74
  env = { "a2a.request" => request }
80
75
 
81
76
  result = mw.call(env)
@@ -87,7 +82,7 @@ test do
87
82
  request.define_singleton_method(:page_size) { 999 }
88
83
 
89
84
  downstream = -> (env) { env["a2a.page_size"] }
90
- mw = A2A::Middleware::LimitPaginationSize.new(downstream)
85
+ mw = A2A::Server::Middleware::LimitPaginationSize.new(downstream)
91
86
  env = { "a2a.request" => request }
92
87
 
93
88
  result = mw.call(env)
@@ -99,7 +94,7 @@ test do
99
94
  request.define_singleton_method(:page_size) { 999 }
100
95
 
101
96
  downstream = -> (env) { env["a2a.page_size"] }
102
- mw = A2A::Middleware::LimitPaginationSize.new(downstream, 50)
97
+ mw = A2A::Server::Middleware::LimitPaginationSize.new(downstream, 50)
103
98
  env = { "a2a.request" => request }
104
99
 
105
100
  result = mw.call(env)
@@ -111,7 +106,7 @@ test do
111
106
  request.define_singleton_method(:page_size) { 30 }
112
107
 
113
108
  downstream = -> (env) { env["a2a.page_size"] }
114
- mw = A2A::Middleware::LimitPaginationSize.new(downstream)
109
+ mw = A2A::Server::Middleware::LimitPaginationSize.new(downstream)
115
110
  env = { "a2a.request" => request }
116
111
 
117
112
  result = mw.call(env)
@@ -123,11 +118,10 @@ test do
123
118
  request.define_singleton_method(:page_size) { "20" }
124
119
 
125
120
  downstream = -> (env) { env["a2a.page_size"] }
126
- mw = A2A::Middleware::LimitPaginationSize.new(downstream)
121
+ mw = A2A::Server::Middleware::LimitPaginationSize.new(downstream)
127
122
  env = { "a2a.request" => request }
128
123
 
129
124
  result = mw.call(env)
130
125
  result.should == 20
131
126
  end
132
127
  end
133
- end