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.
- checksums.yaml +4 -4
- data/lib/a2a/agent.rb +98 -241
- data/lib/a2a/{faraday → client}/middleware/json_rpc/request.rb +10 -8
- data/lib/a2a/{faraday → client}/middleware/json_rpc/response.rb +10 -9
- data/lib/a2a/{faraday → client}/middleware/rest/request.rb +24 -18
- data/lib/a2a/{faraday → client}/middleware/rest/response.rb +12 -8
- data/lib/a2a/{faraday → client}/middleware/schema_request.rb +9 -8
- data/lib/a2a/client.rb +40 -39
- data/lib/a2a/errors/json_rpc_error.rb +1 -2
- data/lib/a2a/errors/rest_error.rb +1 -2
- data/lib/a2a/errors.rb +1 -2
- data/lib/a2a/protocol/json_schema/definition.rb +276 -0
- data/lib/a2a/protocol/json_schema/validation_error.rb +132 -0
- data/lib/a2a/{schema.rb → protocol/json_schema.rb} +211 -208
- data/lib/a2a/protocol/protobuf.rb +447 -0
- data/lib/a2a/protocol.rb +31 -0
- data/lib/a2a/server/bindings/grpc.rb +37 -0
- data/lib/a2a/server/bindings/json_rpc.rb +29 -9
- data/lib/a2a/server/bindings/rest.rb +26 -13
- data/lib/a2a/server/middleware/extract_message.rb +145 -0
- data/lib/a2a/{middleware → server/middleware}/fetch_task.rb +82 -89
- data/lib/a2a/{middleware → server/middleware}/limit_history_length.rb +40 -49
- data/lib/a2a/{middleware → server/middleware}/limit_pagination_size.rb +36 -42
- data/lib/a2a/server/middleware/sse_stream.rb +236 -0
- data/lib/a2a/{sse → server/sse}/event_parser.rb +70 -69
- data/lib/a2a/server/sse/json_rpc_stream.rb +89 -0
- data/lib/a2a/server/sse/rest_stream.rb +63 -0
- data/lib/a2a/server/sse/stream.rb +261 -0
- data/lib/a2a/server/triage.rb +35 -4
- data/lib/a2a/server/well_known.rb +27 -0
- data/lib/a2a/server.rb +44 -58
- data/lib/a2a/test_helpers.rb +34 -75
- data/lib/a2a/version.rb +1 -1
- data/lib/a2a.rb +7 -11
- data/lib/traces/provider/a2a/agent.rb +25 -0
- data/lib/traces/provider/a2a.rb +1 -1
- metadata +39 -37
- data/lib/a2a/middleware/extract_message.rb +0 -120
- data/lib/a2a/middleware/sse_stream.rb +0 -235
- data/lib/a2a/proto.rb +0 -444
- data/lib/a2a/schema/definition.rb +0 -148
- data/lib/a2a/schema/validation_error.rb +0 -129
- data/lib/a2a/server/dispatcher.rb +0 -127
- data/lib/a2a/sse/json_rpc_stream.rb +0 -88
- data/lib/a2a/sse/rest_stream.rb +0 -62
- data/lib/a2a/sse/stream.rb +0 -257
- data/lib/traces/provider/a2a/server/dispatcher.rb +0 -26
- /data/lib/a2a/{middleware.rb → server/middleware.rb} +0 -0
- /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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
def call(env)
|
|
31
|
+
request = env["a2a.request"]
|
|
32
|
+
limit = @max
|
|
42
33
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
if request.respond_to?(:history_length) && request.history_length
|
|
35
|
+
limit = [request.history_length.to_i, @max].min
|
|
36
|
+
end
|
|
46
37
|
|
|
47
|
-
|
|
38
|
+
env["a2a.history_length"] = limit
|
|
48
39
|
|
|
49
|
-
|
|
40
|
+
@app.call(env)
|
|
41
|
+
end
|
|
50
42
|
end
|
|
51
43
|
end
|
|
52
|
-
|
|
44
|
+
end
|
|
53
45
|
end
|
|
54
46
|
|
|
55
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
31
|
-
|
|
24
|
+
def call(env)
|
|
25
|
+
request = env["a2a.request"]
|
|
32
26
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
33
|
+
env["a2a.page_size"] = page_size
|
|
40
34
|
|
|
41
|
-
|
|
35
|
+
@app.call(env)
|
|
36
|
+
end
|
|
42
37
|
end
|
|
43
38
|
end
|
|
44
|
-
|
|
39
|
+
end
|
|
45
40
|
end
|
|
46
41
|
|
|
47
|
-
|
|
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
|