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,235 @@
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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "middleware/fetch_task"
4
+ require_relative "middleware/limit_history_length"
5
+ require_relative "middleware/limit_pagination_size"
6
+ require_relative "middleware/extract_message"
7
+ require_relative "middleware/sse_stream"
@@ -27,6 +27,7 @@ module A2A
27
27
  def initialize(hash = {})
28
28
  props = self.class.schema_properties
29
29
  snake = self.class.snake_to_camel_map
30
+ refs = self.class.property_refs
30
31
  @data = {}
31
32
 
32
33
  hash.each do |key, value|
@@ -36,7 +37,13 @@ module A2A
36
37
  camel = snake[k] || k
37
38
 
38
39
  if props.include?(camel)
39
- @data[camel] = value.is_a?(Definition) ? value.to_h : value
40
+ @data[camel] = if value.is_a?(Definition)
41
+ value.to_h
42
+ elsif (ref_info = refs[camel])
43
+ wrap_ref(value, ref_info)
44
+ else
45
+ value
46
+ end
40
47
  end
41
48
  end
42
49
  end
@@ -59,6 +66,10 @@ module A2A
59
66
  raise "A2A::Schema::Definition should NOT be instantiated directly"
60
67
  end
61
68
 
69
+ def self.property_refs
70
+ raise "A2A::Schema::Definition should NOT be instantiated directly"
71
+ end
72
+
62
73
  # --- validation ----------------------------------------------------
63
74
 
64
75
  def valid?
@@ -94,6 +105,29 @@ module A2A
94
105
 
95
106
  private
96
107
 
108
+ def wrap_ref(value, ref_info)
109
+ kind, title = ref_info
110
+
111
+ case kind
112
+ when :object
113
+ value.is_a?(Hash) ? A2A::Schema[title].new(value) : value
114
+ when :array
115
+ if value.is_a?(Array)
116
+ value.map { |el| el.is_a?(Hash) ? A2A::Schema[title].new(el) : el }
117
+ else
118
+ value
119
+ end
120
+ when :map
121
+ if value.is_a?(Hash)
122
+ value.transform_values { |v| v.is_a?(Hash) ? A2A::Schema[title].new(v) : v }
123
+ else
124
+ value
125
+ end
126
+ else
127
+ value
128
+ end
129
+ end
130
+
97
131
  def deep_compact(obj)
98
132
  case obj
99
133
  when Hash
data/lib/a2a/schema.rb CHANGED
@@ -157,6 +157,7 @@ module A2A
157
157
  properties = raw_definition.fetch("properties", {})
158
158
  camel_keys = properties.keys
159
159
  snake_to_camel = build_snake_to_camel(camel_keys)
160
+ prop_refs = build_property_refs(properties)
160
161
 
161
162
  reader_pairs = camel_keys.map { |ck| [camel_to_snake(ck).to_sym, ck] }
162
163
 
@@ -165,12 +166,14 @@ module A2A
165
166
  @definition_name = definition_name
166
167
  @schema_properties = camel_keys
167
168
  @snake_to_camel = snake_to_camel
169
+ @property_refs = prop_refs
168
170
 
169
171
  class << self
170
172
  def schema = @schema
171
173
  def definition_name = @definition_name
172
174
  def schema_properties = @schema_properties
173
175
  def snake_to_camel_map = @snake_to_camel
176
+ def property_refs = @property_refs
174
177
  end
175
178
 
176
179
  reader_pairs.each do |snake_sym, camel_key|
@@ -179,6 +182,51 @@ module A2A
179
182
  end
180
183
  end
181
184
 
185
+ # Inspect each property's raw schema for $ref pointers and build
186
+ # a map of { camelKey => [:kind, "Definition Title"] } so that
187
+ # Definition#initialize can auto-wrap nested Hashes.
188
+ #
189
+ # Three patterns:
190
+ # :object — direct $ref (e.g. task → Task)
191
+ # :array — items.$ref (e.g. artifacts → [Artifact, ...])
192
+ # :map — additionalProperties.$ref (e.g. securitySchemes → {k => SecurityScheme})
193
+ def build_property_refs(properties)
194
+ definitions = raw_schema.fetch("definitions", {})
195
+ refs = {}
196
+
197
+ properties.each do |camel_key, prop_schema|
198
+ if (ref = prop_schema["$ref"])
199
+ # Direct $ref — singular nested object
200
+ title = ref_title_for(ref)
201
+ if title && definitions.dig(title, "properties")
202
+ refs[camel_key] = [:object, title]
203
+ end
204
+ elsif prop_schema["type"] == "array" && (ref = prop_schema.dig("items", "$ref"))
205
+ # Array with $ref items
206
+ title = ref_title_for(ref)
207
+ if title && definitions.dig(title, "properties")
208
+ refs[camel_key] = [:array, title]
209
+ end
210
+ elsif prop_schema["type"] == "object" && (ref = prop_schema.dig("additionalProperties", "$ref"))
211
+ # Map with $ref additionalProperties
212
+ title = ref_title_for(ref)
213
+ if title && definitions.dig(title, "properties")
214
+ refs[camel_key] = [:map, title]
215
+ end
216
+ end
217
+ end
218
+
219
+ refs
220
+ end
221
+
222
+ # Extract the definition title from an internal $ref pointer.
223
+ # e.g. "#/definitions/Task%20Status" => "Task Status"
224
+ def ref_title_for(ref)
225
+ return nil unless ref.start_with?("#/definitions/")
226
+
227
+ URI::DEFAULT_PARSER.unescape(ref.sub("#/definitions/", ""))
228
+ end
229
+
182
230
  def build_snake_to_camel(camel_keys)
183
231
  map = {}
184
232
  camel_keys.each do |camel|
@@ -426,4 +474,82 @@ test do
426
474
  instance.is_a?(A2A::Schema::Definition).should == true
427
475
  end
428
476
  end
477
+
478
+ it "auto-wraps nested $ref Hash into Definition (object pattern)" do
479
+ response = schema["Send Message Response"].new(
480
+ task: {
481
+ "id" => "task-123",
482
+ "contextId" => "ctx-456",
483
+ "status" => {
484
+ "state" => "TASK_STATE_SUBMITTED",
485
+ "timestamp" => "2025-01-01T00:00:00Z"
486
+ }
487
+ }
488
+ )
489
+ response.task.should.be.kind_of(A2A::Schema::Definition)
490
+ response.task.id.should == "task-123"
491
+ response.task.context_id.should == "ctx-456"
492
+ # Deeply nested: Task.status is also auto-wrapped
493
+ response.task.status.should.be.kind_of(A2A::Schema::Definition)
494
+ response.task.status.state.should == "TASK_STATE_SUBMITTED"
495
+ end
496
+
497
+ it "auto-wraps nested $ref arrays into Definitions (array pattern)" do
498
+ task = schema["Task"].new(
499
+ id: "task-1",
500
+ context_id: "ctx-1",
501
+ history: [
502
+ { "role" => "ROLE_USER", "messageId" => "msg-1", "parts" => [{ "text" => "Hi" }] },
503
+ { "role" => "ROLE_AGENT", "messageId" => "msg-2", "parts" => [{ "text" => "Hello" }] }
504
+ ]
505
+ )
506
+ task.history.should.be.kind_of(Array)
507
+ task.history.length.should == 2
508
+ task.history[0].should.be.kind_of(A2A::Schema::Definition)
509
+ task.history[0].role.should == "ROLE_USER"
510
+ task.history[1].role.should == "ROLE_AGENT"
511
+ end
512
+
513
+ it "preserves to_h serialization after auto-wrapping" do
514
+ response = schema["Send Message Response"].new(
515
+ task: {
516
+ "id" => "task-123",
517
+ "contextId" => "ctx-456",
518
+ "status" => {
519
+ "state" => "TASK_STATE_SUBMITTED",
520
+ "timestamp" => "2025-01-01T00:00:00Z"
521
+ }
522
+ }
523
+ )
524
+ h = response.to_h
525
+ h["task"].should.be.kind_of(Hash)
526
+ h["task"]["id"].should == "task-123"
527
+ h["task"]["status"].should.be.kind_of(Hash)
528
+ h["task"]["status"]["state"].should == "TASK_STATE_SUBMITTED"
529
+ end
530
+
531
+ it "skips wrapping for opaque types without properties (Struct, Timestamp)" do
532
+ task = schema["Task"].new(
533
+ id: "task-1",
534
+ context_id: "ctx-1",
535
+ metadata: { "foo" => "bar" }
536
+ )
537
+ # metadata refs Struct which has no properties — should stay a plain Hash
538
+ task.metadata.should.be.kind_of(Hash)
539
+ task.metadata["foo"].should == "bar"
540
+ end
541
+
542
+ it "does not re-wrap values that are already Definition instances" do
543
+ status = schema["Task Status"].new(
544
+ state: "TASK_STATE_SUBMITTED",
545
+ timestamp: "2025-01-01T00:00:00Z"
546
+ )
547
+ task = schema["Task"].new(
548
+ id: "task-1",
549
+ context_id: "ctx-1",
550
+ status: status
551
+ )
552
+ # Passing a Definition instance should serialize it (existing behavior)
553
+ task.to_h["status"]["state"].should == "TASK_STATE_SUBMITTED"
554
+ end
429
555
  end
@@ -5,8 +5,9 @@ require "a2a"
5
5
  require "a2a/sse"
6
6
 
7
7
  module A2A
8
- module Bindings
9
- # Rack middleware implementing the A2A JSON-RPC 2.0 protocol binding.
8
+ class Server
9
+ module Bindings
10
+ # Rack middleware implementing the A2A JSON-RPC 2.0 protocol binding.
10
11
  #
11
12
  # Strips the JSON-RPC envelope from the inbound request, setting
12
13
  # env keys for the method name, request id, and parsed params.
@@ -47,11 +48,11 @@ module A2A
47
48
  env["a2a.json_rpc_method"] = method
48
49
  env["a2a.body"] = params
49
50
 
50
- @app.call(env)
51
+ result = @app.call(env)
51
52
 
52
- # Check if handler signalled a JSON-RPC error
53
- if (err = env["a2a.error"])
54
- return error_response(id, err[:code], err[:message], err[:data])
53
+ # Check if the result is an error object
54
+ if result.is_a?(A2A::Error)
55
+ return error_response(id, result.code, result.message, result.error_data)
55
56
  end
56
57
 
57
58
  # Check if handler set up a streaming response.
@@ -60,7 +61,6 @@ module A2A
60
61
  return [200, A2A::SSE::Stream.headers, stream]
61
62
  end
62
63
 
63
- result = env["a2a.result"]
64
64
  success_response(id, result)
65
65
  end
66
66
 
@@ -79,12 +79,16 @@ module A2A
79
79
  [JSON.generate(jsonrpc: "2.0", id: id, error: err)]]
80
80
  end
81
81
  end
82
+ end
82
83
  end
83
84
  end
84
85
 
85
86
  test do
87
+ require "a2a/test_helpers"
88
+
86
89
  server = A2A::Server.new(agent_card: { "name" => "Test" })
87
- rack = Rack::MockRequest.new(server)
90
+ server.register(A2A::TestHelpers.stub_agent)
91
+ rack = Rack::MockRequest.new(server)
88
92
 
89
93
  A2A::Proto.operations.each do |op|
90
94
  it "#{op.json_rpc_method} returns valid #{op.response_type}" do
@@ -5,8 +5,9 @@ require "a2a"
5
5
  require "a2a/sse"
6
6
 
7
7
  module A2A
8
- module Bindings
9
- # Rack middleware implementing the A2A HTTP+JSON/REST protocol binding.
8
+ class Server
9
+ module Bindings
10
+ # Rack middleware implementing the A2A HTTP+JSON/REST protocol binding.
10
11
  #
11
12
  # Extracts the HTTP verb, path, and request body/params into env keys.
12
13
  # Calls downstream. On return, wraps env["a2a.result"] into a REST
@@ -40,11 +41,11 @@ module A2A
40
41
 
41
42
  env["a2a.body"] = params
42
43
 
43
- @app.call(env)
44
+ result = @app.call(env)
44
45
 
45
- # Check if handler signalled a REST error
46
- if (err = env["a2a.error"])
47
- return error_response(err[:http_status] || 400, err[:message], err[:data])
46
+ # Check if the result is an error object
47
+ if result.is_a?(A2A::Error)
48
+ return error_response(result.http_status, result.message, result.error_data)
48
49
  end
49
50
 
50
51
  # Check if handler set up a streaming response.
@@ -53,7 +54,6 @@ module A2A
53
54
  return [200, A2A::SSE::Stream.headers, stream]
54
55
  end
55
56
 
56
- result = env["a2a.result"]
57
57
  success_response(result)
58
58
  end
59
59
 
@@ -72,12 +72,16 @@ module A2A
72
72
  [JSON.generate(body)]]
73
73
  end
74
74
  end
75
+ end
75
76
  end
76
77
  end
77
78
 
78
79
  test do
80
+ require "a2a/test_helpers"
81
+
79
82
  server = A2A::Server.new(agent_card: { "name" => "Test" })
80
- rack = Rack::MockRequest.new(server)
83
+ server.register(A2A::TestHelpers.stub_agent)
84
+ rack = Rack::MockRequest.new(server)
81
85
 
82
86
  A2A::Proto.operations.each do |op|
83
87
  it "#{op.rest_verb.upcase} #{op.rest_path} returns valid #{op.response_type}" do