agent2agent 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/a2a/agent.rb +165 -117
  3. data/lib/a2a/client.rb +470 -51
  4. data/lib/a2a/errors/json_rpc_error.rb +71 -0
  5. data/lib/a2a/errors/rest_error.rb +68 -0
  6. data/lib/a2a/errors.rb +535 -0
  7. data/lib/a2a/faraday/middleware/json_rpc/request.rb +96 -0
  8. data/lib/a2a/faraday/middleware/json_rpc/response.rb +131 -0
  9. data/lib/a2a/faraday/middleware/rest/request.rb +166 -0
  10. data/lib/a2a/faraday/middleware/rest/response.rb +144 -0
  11. data/lib/a2a/faraday/middleware/schema_request.rb +69 -0
  12. data/lib/a2a/middleware/extract_message.rb +120 -0
  13. data/lib/a2a/middleware/fetch_task.rb +228 -0
  14. data/lib/a2a/middleware/limit_history_length.rb +123 -0
  15. data/lib/a2a/middleware/limit_pagination_size.rb +133 -0
  16. data/lib/a2a/middleware/sse_stream.rb +235 -0
  17. data/lib/a2a/middleware.rb +7 -0
  18. data/lib/a2a/schema/definition.rb +35 -1
  19. data/lib/a2a/schema.rb +126 -0
  20. data/lib/a2a/{bindings → server/bindings}/json_rpc.rb +12 -8
  21. data/lib/a2a/{bindings → server/bindings}/rest.rb +12 -8
  22. data/lib/a2a/server/dispatcher.rb +52 -54
  23. data/lib/a2a/server/env.rb +4 -6
  24. data/lib/a2a/server/triage.rb +1 -1
  25. data/lib/a2a/server.rb +10 -10
  26. data/lib/a2a/sse/event_parser.rb +202 -0
  27. data/lib/a2a/sse/json_rpc_stream.rb +27 -5
  28. data/lib/a2a/sse/rest_stream.rb +17 -5
  29. data/lib/a2a/sse/stream.rb +135 -7
  30. data/lib/a2a/sse.rb +1 -0
  31. data/lib/a2a/test_helpers.rb +89 -0
  32. data/lib/a2a/version.rb +1 -1
  33. data/lib/a2a.rb +6 -2
  34. data/lib/traces/provider/a2a/{bindings → server/bindings}/json_rpc.rb +2 -2
  35. data/lib/traces/provider/a2a/{bindings → server/bindings}/rest.rb +2 -2
  36. data/lib/traces/provider/a2a.rb +2 -2
  37. metadata +49 -22
  38. data/lib/a2a/server/cancel_task.rb +0 -14
  39. data/lib/a2a/server/create_task_push_notification_config.rb +0 -14
  40. data/lib/a2a/server/delete_task_push_notification_config.rb +0 -14
  41. data/lib/a2a/server/get_extended_agent_card.rb +0 -15
  42. data/lib/a2a/server/get_task.rb +0 -14
  43. data/lib/a2a/server/get_task_push_notification_config.rb +0 -14
  44. data/lib/a2a/server/list_task_push_notification_configs.rb +0 -14
  45. data/lib/a2a/server/list_tasks.rb +0 -14
  46. data/lib/a2a/server/send_message.rb +0 -14
  47. data/lib/a2a/server/send_streaming_message.rb +0 -14
  48. data/lib/a2a/server/subscribe_to_task.rb +0 -14
  49. data/lib/a2a/store/processor.rb +0 -136
  50. data/lib/a2a/store/pub_sub.rb +0 -149
  51. data/lib/a2a/store/sqlite.rb +0 -533
  52. data/lib/a2a/store/webhooks.rb +0 -94
  53. data/lib/a2a/store.rb +0 -6
  54. data/lib/a2a/task_store.rb +0 -315
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "a2a"
5
+
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
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
42
+ 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
87
+ end
88
+
89
+ test do
90
+ describe "A2A::Middleware::FetchTask" do
91
+ before do
92
+ @store = Object.new
93
+ end
94
+
95
+ it "sets env[\"a2a.task\"] when task exists" do
96
+ task_data = { id: "t-1", state: "TASK_STATE_COMPLETED" }
97
+ @store.define_singleton_method(:get) { |id| id == "t-1" ? task_data : nil }
98
+
99
+ request = Object.new
100
+ request.define_singleton_method(:id) { "t-1" }
101
+
102
+ downstream = -> (env) { env["a2a.task"] }
103
+ mw = A2A::Middleware::FetchTask.new(downstream, store: @store)
104
+ env = { "a2a.request" => request }
105
+
106
+ mw.call(env).should == task_data
107
+ end
108
+
109
+ it "sets env[\"a2a.task\"] to nil when task does not exist" do
110
+ @store.define_singleton_method(:get) { |id| nil }
111
+
112
+ request = Object.new
113
+ request.define_singleton_method(:id) { "missing" }
114
+
115
+ downstream = -> (env) { env["a2a.task"] }
116
+ mw = A2A::Middleware::FetchTask.new(downstream, store: @store)
117
+ env = { "a2a.request" => request }
118
+
119
+ mw.call(env).should.be.nil
120
+ end
121
+
122
+ it "sets env[\"a2a.task\"] to nil when id is empty" do
123
+ request = Object.new
124
+ request.define_singleton_method(:id) { "" }
125
+
126
+ downstream = -> (env) { env["a2a.task"] }
127
+ mw = A2A::Middleware::FetchTask.new(downstream, store: @store)
128
+ env = { "a2a.request" => request }
129
+
130
+ mw.call(env).should.be.nil
131
+ end
132
+
133
+ it "sets env[\"a2a.task\"] to nil when id is nil" do
134
+ message = Object.new
135
+ message.define_singleton_method(:task_id) { nil }
136
+
137
+ request = Object.new
138
+ request.define_singleton_method(:message) { message }
139
+
140
+ downstream = -> (env) { env["a2a.task"] }
141
+ mw = A2A::Middleware::FetchTask.new(downstream, store: @store, id_field: :task_id, from: :message)
142
+ env = { "a2a.request" => request }
143
+
144
+ mw.call(env).should.be.nil
145
+ end
146
+
147
+ it "reads id from a nested object via from:" do
148
+ task_data = { id: "t-2", state: "TASK_STATE_WORKING" }
149
+ @store.define_singleton_method(:get) { |id| id == "t-2" ? task_data : nil }
150
+
151
+ message = Object.new
152
+ message.define_singleton_method(:task_id) { "t-2" }
153
+
154
+ request = Object.new
155
+ request.define_singleton_method(:message) { message }
156
+
157
+ downstream = -> (env) { env["a2a.task"] }
158
+ mw = A2A::Middleware::FetchTask.new(downstream, store: @store, id_field: :task_id, from: :message)
159
+ env = { "a2a.request" => request }
160
+
161
+ mw.call(env).should == task_data
162
+ end
163
+ end
164
+
165
+ describe "A2A::Middleware::FetchTaskOrRaise" do
166
+ before do
167
+ @store = Object.new
168
+ end
169
+
170
+ it "sets env[\"a2a.task\"] when task exists" do
171
+ task_data = { id: "t-1", state: "TASK_STATE_COMPLETED" }
172
+ @store.define_singleton_method(:get) { |id| id == "t-1" ? task_data : nil }
173
+
174
+ request = Object.new
175
+ request.define_singleton_method(:id) { "t-1" }
176
+
177
+ downstream = -> (env) { env["a2a.task"] }
178
+ mw = A2A::Middleware::FetchTaskOrRaise.new(downstream, store: @store)
179
+ env = { "a2a.request" => request }
180
+
181
+ mw.call(env).should == task_data
182
+ end
183
+
184
+ it "raises TaskNotFoundError when task does not exist" do
185
+ @store.define_singleton_method(:get) { |id| nil }
186
+
187
+ request = Object.new
188
+ request.define_singleton_method(:id) { "missing" }
189
+
190
+ downstream = -> (env) { :should_not_reach }
191
+ mw = A2A::Middleware::FetchTaskOrRaise.new(downstream, store: @store)
192
+ env = { "a2a.request" => request }
193
+
194
+ lambda { mw.call(env) }.should.raise(A2A::TaskNotFoundError)
195
+ end
196
+
197
+ it "reads task_id when id_field: :task_id is specified" do
198
+ task_data = { id: "t-2", state: "TASK_STATE_WORKING" }
199
+ @store.define_singleton_method(:get) { |id| id == "t-2" ? task_data : nil }
200
+
201
+ request = Object.new
202
+ request.define_singleton_method(:task_id) { "t-2" }
203
+
204
+ downstream = -> (env) { env["a2a.task"] }
205
+ mw = A2A::Middleware::FetchTaskOrRaise.new(downstream, store: @store, id_field: :task_id)
206
+ env = { "a2a.request" => request }
207
+
208
+ mw.call(env).should == task_data
209
+ end
210
+
211
+ it "reads id from a nested object via from:" do
212
+ task_data = { id: "t-3", state: "TASK_STATE_WORKING" }
213
+ @store.define_singleton_method(:get) { |id| id == "t-3" ? task_data : nil }
214
+
215
+ message = Object.new
216
+ message.define_singleton_method(:task_id) { "t-3" }
217
+
218
+ request = Object.new
219
+ request.define_singleton_method(:message) { message }
220
+
221
+ downstream = -> (env) { env["a2a.task"] }
222
+ mw = A2A::Middleware::FetchTaskOrRaise.new(downstream, store: @store, id_field: :task_id, from: :message)
223
+ env = { "a2a.request" => request }
224
+
225
+ mw.call(env).should == task_data
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "a2a"
5
+
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
38
+
39
+ def call(env)
40
+ request = env["a2a.request"]
41
+ limit = @max
42
+
43
+ if request.respond_to?(:history_length) && request.history_length
44
+ limit = [request.history_length.to_i, @max].min
45
+ end
46
+
47
+ env["a2a.history_length"] = limit
48
+
49
+ @app.call(env)
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ test do
56
+ describe "A2A::Middleware::LimitHistoryLength" do
57
+ it "defaults to server max when client does not specify" do
58
+ request = Object.new
59
+ request.define_singleton_method(:history_length) { nil }
60
+
61
+ downstream = -> (env) { env["a2a.history_length"] }
62
+ mw = A2A::Middleware::LimitHistoryLength.new(downstream, 20)
63
+ env = { "a2a.request" => request }
64
+
65
+ mw.call(env).should == 20
66
+ end
67
+
68
+ it "uses client value when smaller than max" do
69
+ request = Object.new
70
+ request.define_singleton_method(:history_length) { 5 }
71
+
72
+ downstream = -> (env) { env["a2a.history_length"] }
73
+ mw = A2A::Middleware::LimitHistoryLength.new(downstream, 20)
74
+ env = { "a2a.request" => request }
75
+
76
+ mw.call(env).should == 5
77
+ end
78
+
79
+ it "caps client value to server max" do
80
+ request = Object.new
81
+ request.define_singleton_method(:history_length) { 100 }
82
+
83
+ downstream = -> (env) { env["a2a.history_length"] }
84
+ mw = A2A::Middleware::LimitHistoryLength.new(downstream, 20)
85
+ env = { "a2a.request" => request }
86
+
87
+ mw.call(env).should == 20
88
+ end
89
+
90
+ it "returns 0 when client requests 0" do
91
+ request = Object.new
92
+ request.define_singleton_method(:history_length) { 0 }
93
+
94
+ downstream = -> (env) { env["a2a.history_length"] }
95
+ mw = A2A::Middleware::LimitHistoryLength.new(downstream, 20)
96
+ env = { "a2a.request" => request }
97
+
98
+ mw.call(env).should == 0
99
+ end
100
+
101
+ it "handles string values" do
102
+ request = Object.new
103
+ request.define_singleton_method(:history_length) { "3" }
104
+
105
+ downstream = -> (env) { env["a2a.history_length"] }
106
+ mw = A2A::Middleware::LimitHistoryLength.new(downstream, 20)
107
+ env = { "a2a.request" => request }
108
+
109
+ mw.call(env).should == 3
110
+ end
111
+
112
+ it "always returns an integer" do
113
+ request = Object.new
114
+ request.define_singleton_method(:history_length) { nil }
115
+
116
+ downstream = -> (env) { env["a2a.history_length"] }
117
+ mw = A2A::Middleware::LimitHistoryLength.new(downstream, 10)
118
+ env = { "a2a.request" => request }
119
+
120
+ mw.call(env).should.be.kind_of(Integer)
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "a2a"
5
+
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
29
+
30
+ def call(env)
31
+ request = env["a2a.request"]
32
+
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
38
+
39
+ env["a2a.page_size"] = page_size
40
+
41
+ @app.call(env)
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ test do
48
+ describe "A2A::Middleware::LimitPaginationSize" do
49
+ it "uses max as default page size when not specified by client" do
50
+ request = Object.new
51
+ request.define_singleton_method(:page_size) { nil }
52
+
53
+ downstream = -> (env) { env["a2a.page_size"] }
54
+ mw = A2A::Middleware::LimitPaginationSize.new(downstream)
55
+ env = { "a2a.request" => request }
56
+
57
+ result = mw.call(env)
58
+ result.should == 100
59
+ end
60
+
61
+ it "uses a custom max as the default" do
62
+ request = Object.new
63
+ request.define_singleton_method(:page_size) { nil }
64
+
65
+ downstream = -> (env) { env["a2a.page_size"] }
66
+ mw = A2A::Middleware::LimitPaginationSize.new(downstream, 25)
67
+ env = { "a2a.request" => request }
68
+
69
+ result = mw.call(env)
70
+ result.should == 25
71
+ end
72
+
73
+ it "clamps to minimum of 1" do
74
+ request = Object.new
75
+ request.define_singleton_method(:page_size) { 0 }
76
+
77
+ downstream = -> (env) { env["a2a.page_size"] }
78
+ mw = A2A::Middleware::LimitPaginationSize.new(downstream)
79
+ env = { "a2a.request" => request }
80
+
81
+ result = mw.call(env)
82
+ result.should == 1
83
+ end
84
+
85
+ it "clamps to the max" do
86
+ request = Object.new
87
+ request.define_singleton_method(:page_size) { 999 }
88
+
89
+ downstream = -> (env) { env["a2a.page_size"] }
90
+ mw = A2A::Middleware::LimitPaginationSize.new(downstream)
91
+ env = { "a2a.request" => request }
92
+
93
+ result = mw.call(env)
94
+ result.should == 100
95
+ end
96
+
97
+ it "clamps to a custom max" do
98
+ request = Object.new
99
+ request.define_singleton_method(:page_size) { 999 }
100
+
101
+ downstream = -> (env) { env["a2a.page_size"] }
102
+ mw = A2A::Middleware::LimitPaginationSize.new(downstream, 50)
103
+ env = { "a2a.request" => request }
104
+
105
+ result = mw.call(env)
106
+ result.should == 50
107
+ end
108
+
109
+ it "passes through a valid page size" do
110
+ request = Object.new
111
+ request.define_singleton_method(:page_size) { 30 }
112
+
113
+ downstream = -> (env) { env["a2a.page_size"] }
114
+ mw = A2A::Middleware::LimitPaginationSize.new(downstream)
115
+ env = { "a2a.request" => request }
116
+
117
+ result = mw.call(env)
118
+ result.should == 30
119
+ end
120
+
121
+ it "handles string values" do
122
+ request = Object.new
123
+ request.define_singleton_method(:page_size) { "20" }
124
+
125
+ downstream = -> (env) { env["a2a.page_size"] }
126
+ mw = A2A::Middleware::LimitPaginationSize.new(downstream)
127
+ env = { "a2a.request" => request }
128
+
129
+ result = mw.call(env)
130
+ result.should == 20
131
+ end
132
+ end
133
+ end