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,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../a2a/agent"
4
+ require "traces/provider"
5
+
6
+ Traces::Provider(A2A::Agent) do
7
+ def call(env)
8
+ attributes = {
9
+ "a2a.operation" => env["a2a.operation"],
10
+ }
11
+
12
+ Traces.trace("a2a.agent.call", attributes: attributes) do |span|
13
+ super.tap do |result|
14
+ if result.is_a?(A2A::Error)
15
+ span["a2a.response_type"] = "error"
16
+ span["a2a.error.code"] = result.code
17
+ elsif env["a2a.stream"].is_a?(A2A::Server::SSE::Stream)
18
+ span["a2a.response_type"] = "stream"
19
+ else
20
+ span["a2a.response_type"] = "result"
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "a2a/agent"
3
4
  require_relative "a2a/server"
4
- require_relative "a2a/server/dispatcher"
5
5
  require_relative "a2a/server/triage"
6
6
  require_relative "a2a/server/bindings/json_rpc"
7
7
  require_relative "a2a/server/bindings/rest"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: agent2agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - A2A Contributors
@@ -51,20 +51,6 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0.62'
54
- - !ruby/object:Gem::Dependency
55
- name: scampi
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: '0'
61
- type: :runtime
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: '0'
68
54
  - !ruby/object:Gem::Dependency
69
55
  name: rack
70
56
  requirement: !ruby/object:Gem::Requirement
@@ -191,6 +177,20 @@ dependencies:
191
177
  - - "~>"
192
178
  - !ruby/object:Gem::Version
193
179
  version: '0.55'
180
+ - !ruby/object:Gem::Dependency
181
+ name: scampi
182
+ requirement: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - "~>"
185
+ - !ruby/object:Gem::Version
186
+ version: '1.0'
187
+ type: :development
188
+ prerelease: false
189
+ version_requirements: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - "~>"
192
+ - !ruby/object:Gem::Version
193
+ version: '1.0'
194
194
  description: Abstraction to help work with A2A protocol
195
195
  executables: []
196
196
  extensions: []
@@ -204,42 +204,44 @@ files:
204
204
  - lib/a2a.rb
205
205
  - lib/a2a/agent.rb
206
206
  - lib/a2a/client.rb
207
+ - lib/a2a/client/middleware/json_rpc/request.rb
208
+ - lib/a2a/client/middleware/json_rpc/response.rb
209
+ - lib/a2a/client/middleware/rest/request.rb
210
+ - lib/a2a/client/middleware/rest/response.rb
211
+ - lib/a2a/client/middleware/schema_request.rb
207
212
  - lib/a2a/errors.rb
208
213
  - lib/a2a/errors/json_rpc_error.rb
209
214
  - lib/a2a/errors/rest_error.rb
210
- - lib/a2a/faraday/middleware/json_rpc/request.rb
211
- - lib/a2a/faraday/middleware/json_rpc/response.rb
212
- - lib/a2a/faraday/middleware/rest/request.rb
213
- - lib/a2a/faraday/middleware/rest/response.rb
214
- - lib/a2a/faraday/middleware/schema_request.rb
215
- - lib/a2a/middleware.rb
216
- - lib/a2a/middleware/extract_message.rb
217
- - lib/a2a/middleware/fetch_task.rb
218
- - lib/a2a/middleware/limit_history_length.rb
219
- - lib/a2a/middleware/limit_pagination_size.rb
220
- - lib/a2a/middleware/sse_stream.rb
221
- - lib/a2a/proto.rb
222
- - lib/a2a/schema.rb
223
- - lib/a2a/schema/definition.rb
224
- - lib/a2a/schema/validation_error.rb
215
+ - lib/a2a/protocol.rb
216
+ - lib/a2a/protocol/json_schema.rb
217
+ - lib/a2a/protocol/json_schema/definition.rb
218
+ - lib/a2a/protocol/json_schema/validation_error.rb
219
+ - lib/a2a/protocol/protobuf.rb
225
220
  - lib/a2a/server.rb
221
+ - lib/a2a/server/bindings/grpc.rb
226
222
  - lib/a2a/server/bindings/json_rpc.rb
227
223
  - lib/a2a/server/bindings/rest.rb
228
- - lib/a2a/server/dispatcher.rb
229
224
  - lib/a2a/server/env.rb
225
+ - lib/a2a/server/middleware.rb
226
+ - lib/a2a/server/middleware/extract_message.rb
227
+ - lib/a2a/server/middleware/fetch_task.rb
228
+ - lib/a2a/server/middleware/limit_history_length.rb
229
+ - lib/a2a/server/middleware/limit_pagination_size.rb
230
+ - lib/a2a/server/middleware/sse_stream.rb
231
+ - lib/a2a/server/sse.rb
232
+ - lib/a2a/server/sse/event_parser.rb
233
+ - lib/a2a/server/sse/json_rpc_stream.rb
234
+ - lib/a2a/server/sse/rest_stream.rb
235
+ - lib/a2a/server/sse/stream.rb
230
236
  - lib/a2a/server/triage.rb
231
- - lib/a2a/sse.rb
232
- - lib/a2a/sse/event_parser.rb
233
- - lib/a2a/sse/json_rpc_stream.rb
234
- - lib/a2a/sse/rest_stream.rb
235
- - lib/a2a/sse/stream.rb
237
+ - lib/a2a/server/well_known.rb
236
238
  - lib/a2a/test_helpers.rb
237
239
  - lib/a2a/version.rb
238
240
  - lib/traces/provider/a2a.rb
241
+ - lib/traces/provider/a2a/agent.rb
239
242
  - lib/traces/provider/a2a/server.rb
240
243
  - lib/traces/provider/a2a/server/bindings/json_rpc.rb
241
244
  - lib/traces/provider/a2a/server/bindings/rest.rb
242
- - lib/traces/provider/a2a/server/dispatcher.rb
243
245
  - lib/traces/provider/a2a/server/triage.rb
244
246
  licenses:
245
247
  - MIT
@@ -1,120 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/setup"
4
- require "a2a"
5
-
6
- module A2A
7
- module Middleware
8
- # Extracts text from the request message's parts and sets
9
- # `env["a2a.message"]` to the joined text string.
10
- #
11
- # Replaces the common `extract_text` lambda pattern found in examples:
12
- #
13
- # extract_text = ->(message) {
14
- # parts = message.parts || []
15
- # parts.filter_map { |p| p.text }.join("\n")
16
- # }
17
- #
18
- # Usage:
19
- #
20
- # on "SendMessage" do
21
- # use A2A::Middleware::ExtractMessage
22
- # respond_with -> (env) {
23
- # text = env["a2a.message"]
24
- # # ...
25
- # }
26
- # end
27
- #
28
- class ExtractMessage
29
- def initialize(app)
30
- @app = app
31
- end
32
-
33
- def call(env)
34
- request = env["a2a.request"]
35
- message = request.message
36
- parts = message.parts || []
37
-
38
- env["a2a.message"] = parts.filter_map { |p|
39
- p.respond_to?(:text) ? p.text : p["text"]
40
- }.join("\n")
41
-
42
- @app.call(env)
43
- end
44
- end
45
- end
46
- end
47
-
48
- test do
49
- describe "A2A::Middleware::ExtractMessage" do
50
- it "extracts text from message parts" do
51
- part1 = Object.new
52
- part1.define_singleton_method(:text) { "hello" }
53
- part2 = Object.new
54
- part2.define_singleton_method(:text) { "world" }
55
-
56
- message = Object.new
57
- message.define_singleton_method(:parts) { [part1, part2] }
58
-
59
- request = Object.new
60
- request.define_singleton_method(:message) { message }
61
-
62
- downstream = -> (env) { env["a2a.message"] }
63
- mw = A2A::Middleware::ExtractMessage.new(downstream)
64
- env = { "a2a.request" => request }
65
-
66
- result = mw.call(env)
67
- result.should == "hello\nworld"
68
- end
69
-
70
- it "handles nil parts" do
71
- message = Object.new
72
- message.define_singleton_method(:parts) { nil }
73
-
74
- request = Object.new
75
- request.define_singleton_method(:message) { message }
76
-
77
- downstream = -> (env) { env["a2a.message"] }
78
- mw = A2A::Middleware::ExtractMessage.new(downstream)
79
- env = { "a2a.request" => request }
80
-
81
- result = mw.call(env)
82
- result.should == ""
83
- end
84
-
85
- it "skips parts without text" do
86
- part1 = Object.new
87
- part1.define_singleton_method(:text) { "hello" }
88
- part2 = Object.new
89
- part2.define_singleton_method(:text) { nil }
90
-
91
- message = Object.new
92
- message.define_singleton_method(:parts) { [part1, part2] }
93
-
94
- request = Object.new
95
- request.define_singleton_method(:message) { message }
96
-
97
- downstream = -> (env) { env["a2a.message"] }
98
- mw = A2A::Middleware::ExtractMessage.new(downstream)
99
- env = { "a2a.request" => request }
100
-
101
- result = mw.call(env)
102
- result.should == "hello"
103
- end
104
-
105
- it "handles hash-style parts" do
106
- message = Object.new
107
- message.define_singleton_method(:parts) { [{ "text" => "from hash" }] }
108
-
109
- request = Object.new
110
- request.define_singleton_method(:message) { message }
111
-
112
- downstream = -> (env) { env["a2a.message"] }
113
- mw = A2A::Middleware::ExtractMessage.new(downstream)
114
- env = { "a2a.request" => request }
115
-
116
- result = mw.call(env)
117
- result.should == "from hash"
118
- end
119
- end
120
- end
@@ -1,235 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/setup"
4
- require "a2a"
5
- require "a2a/sse"
6
- require "async"
7
-
8
- module A2A
9
- module Middleware
10
- # Sets up an SSE stream builder on `env["a2a.stream"]`.
11
- #
12
- # The builder detects the protocol binding (REST vs JSON-RPC) from
13
- # env and creates the correct stream subclass when `open` is called.
14
- # The `open` block runs inside an Async fiber and the stream is
15
- # automatically finished when the block exits (even on exception).
16
- #
17
- # If the handler never calls `open`, the builder is removed from env
18
- # so the binding layer doesn't mistake it for a real stream.
19
- #
20
- # Usage:
21
- #
22
- # on "SendStreamingMessage" do
23
- # use A2A::Middleware::SSEStream
24
- # use A2A::Middleware::ExtractMessage
25
- # respond_with -> (env) {
26
- # env["a2a.stream"].open(task_id: "t1", context_id: "c1") do |s|
27
- # s.task(status: { state: "TASK_STATE_WORKING" })
28
- # s.status_update(status: { state: "TASK_STATE_COMPLETED" })
29
- # end
30
- # }
31
- # end
32
- #
33
- class SSEStream
34
- def initialize(app)
35
- @app = app
36
- end
37
-
38
- def call(env)
39
- builder = StreamBuilder.new(env)
40
- env["a2a.stream"] = builder
41
-
42
- result = @app.call(env)
43
-
44
- # If open was never called, clear the builder so the binding
45
- # layer doesn't mistake it for a real stream.
46
- env.delete("a2a.stream") if env["a2a.stream"].equal?(builder)
47
-
48
- result
49
- end
50
- end
51
-
52
- # Factory that creates the correct SSE stream subclass based on the
53
- # protocol binding, then runs the caller's block inside Async with
54
- # automatic finish on exit.
55
- #
56
- # Created by SSEStream middleware — not intended for direct use.
57
- #
58
- class StreamBuilder
59
- def initialize(env)
60
- @env = env
61
- end
62
-
63
- # Create and open an SSE stream for the current request.
64
- #
65
- # Detects REST vs JSON-RPC from the env, constructs the correct
66
- # stream subclass, and yields it to the block. The block runs
67
- # inside an Async fiber. The stream is automatically finished
68
- # when the block exits, even if an exception is raised.
69
- #
70
- # @param task_id [String] the task identifier for this stream
71
- # @param context_id [String] the context identifier for this stream
72
- # @yieldparam stream [A2A::SSE::Stream] the opened stream
73
- # @return [nil]
74
- #
75
- def open(task_id:, context_id:, &block)
76
- stream = if @env["a2a.json_rpc_id"]
77
- A2A::SSE::JsonRpcStream.new(
78
- task_id: task_id, context_id: context_id,
79
- json_rpc_id: @env["a2a.json_rpc_id"]
80
- )
81
- else
82
- A2A::SSE::RestStream.new(
83
- task_id: task_id, context_id: context_id
84
- )
85
- end
86
-
87
- @env["a2a.stream"] = stream
88
-
89
- Async do
90
- block.call(stream)
91
- ensure
92
- stream.finish
93
- end
94
-
95
- nil
96
- end
97
- end
98
- end
99
- end
100
-
101
- test do
102
- describe "A2A::Middleware::SSEStream" do
103
- it "sets a StreamBuilder on env[\"a2a.stream\"]" do
104
- seen_stream = nil
105
- downstream = ->(env) { seen_stream = env["a2a.stream"]; :ok }
106
-
107
- mw = A2A::Middleware::SSEStream.new(downstream)
108
- env = {}
109
- mw.call(env)
110
-
111
- seen_stream.should.be.kind_of(A2A::Middleware::StreamBuilder)
112
- end
113
-
114
- it "auto-clears builder if open was never called" do
115
- downstream = ->(env) { :ok }
116
-
117
- mw = A2A::Middleware::SSEStream.new(downstream)
118
- env = {}
119
- mw.call(env)
120
-
121
- env.key?("a2a.stream").should == false
122
- end
123
-
124
- it "preserves env[\"a2a.stream\"] when open was called" do
125
- downstream = ->(env) {
126
- env["a2a.stream"].open(task_id: "t1", context_id: "c1") do |s|
127
- # no-op
128
- end
129
- }
130
-
131
- mw = A2A::Middleware::SSEStream.new(downstream)
132
- env = {}
133
- mw.call(env)
134
-
135
- env["a2a.stream"].should.be.kind_of(A2A::SSE::RestStream)
136
- end
137
-
138
- it "returns the downstream result" do
139
- downstream = ->(env) { :result_value }
140
-
141
- mw = A2A::Middleware::SSEStream.new(downstream)
142
- result = mw.call({})
143
-
144
- result.should == :result_value
145
- end
146
- end
147
-
148
- describe "A2A::Middleware::StreamBuilder" do
149
- it "creates a RestStream when no JSON-RPC ID" do
150
- env = {}
151
- builder = A2A::Middleware::StreamBuilder.new(env)
152
-
153
- builder.open(task_id: "t1", context_id: "c1") do |s|
154
- s.should.be.kind_of(A2A::SSE::RestStream)
155
- end
156
-
157
- env["a2a.stream"].should.be.kind_of(A2A::SSE::RestStream)
158
- end
159
-
160
- it "creates a JsonRpcStream when JSON-RPC ID is present" do
161
- env = { "a2a.json_rpc_id" => 42 }
162
- builder = A2A::Middleware::StreamBuilder.new(env)
163
-
164
- builder.open(task_id: "t1", context_id: "c1") do |s|
165
- s.should.be.kind_of(A2A::SSE::JsonRpcStream)
166
- end
167
-
168
- env["a2a.stream"].should.be.kind_of(A2A::SSE::JsonRpcStream)
169
- end
170
-
171
- it "passes task_id and context_id to the stream" do
172
- env = {}
173
- builder = A2A::Middleware::StreamBuilder.new(env)
174
-
175
- builder.open(task_id: "t1", context_id: "c1") do |s|
176
- s.task_id.should == "t1"
177
- s.context_id.should == "c1"
178
- end
179
- end
180
-
181
- it "auto-finishes the stream when block completes" do
182
- env = {}
183
- builder = A2A::Middleware::StreamBuilder.new(env)
184
-
185
- builder.open(task_id: "t1", context_id: "c1") do |s|
186
- s.task(status: { state: "TASK_STATE_WORKING" })
187
- end
188
-
189
- stream = env["a2a.stream"]
190
- # After finish, read drains remaining data then returns nil
191
- stream.read # the task event
192
- stream.read.should.be.nil
193
- end
194
-
195
- it "auto-finishes even when block raises" do
196
- env = {}
197
- builder = A2A::Middleware::StreamBuilder.new(env)
198
-
199
- builder.open(task_id: "t1", context_id: "c1") do |s|
200
- s.task(status: { state: "TASK_STATE_WORKING" })
201
- raise "boom"
202
- end
203
-
204
- stream = env["a2a.stream"]
205
- stream.read # the task event
206
- stream.read.should.be.nil
207
- end
208
-
209
- it "returns nil" do
210
- env = {}
211
- builder = A2A::Middleware::StreamBuilder.new(env)
212
-
213
- result = builder.open(task_id: "t1", context_id: "c1") do |s|
214
- # no-op
215
- end
216
-
217
- result.should.be.nil
218
- end
219
-
220
- it "typed methods inject task_id and context_id" do
221
- env = {}
222
- builder = A2A::Middleware::StreamBuilder.new(env)
223
-
224
- builder.open(task_id: "t1", context_id: "c1") do |s|
225
- s.status_update(status: { state: "TASK_STATE_COMPLETED", timestamp: "2025-01-01T00:00:00Z" })
226
- end
227
-
228
- stream = env["a2a.stream"]
229
- chunk = stream.read
230
- parsed = JSON.parse(chunk.sub(/\Adata: /, "").strip)
231
- parsed["statusUpdate"]["taskId"].should == "t1"
232
- parsed["statusUpdate"]["contextId"].should == "c1"
233
- end
234
- end
235
- end