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,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
|
data/lib/traces/provider/a2a.rb
CHANGED
|
@@ -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:
|
|
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/
|
|
211
|
-
- lib/a2a/
|
|
212
|
-
- lib/a2a/
|
|
213
|
-
- lib/a2a/
|
|
214
|
-
- lib/a2a/
|
|
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/
|
|
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
|