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,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "protocol/http/body/writable"
4
+ require "json"
5
+
6
+ module A2A
7
+ class Server
8
+ module SSE
9
+ # Async-native SSE body built on ::Protocol::HTTP::Body::Writable.
10
+ #
11
+ # Falcon's protocol-rack passes ::Protocol::HTTP::Body::Readable subclasses
12
+ # through untouched — no Enumerable wrapping, no intermediate buffering.
13
+ # This gives us true async streaming with backpressure for free.
14
+ #
15
+ # The gospel (protocol-http) teaches:
16
+ # - Writable is a producer/consumer queue body
17
+ # - write() pushes chunks; read() pops them (blocks when empty)
18
+ # - close_write signals EOF (reader gets nil)
19
+ # - Client disconnect raises on next write()
20
+ #
21
+ # Usage:
22
+ #
23
+ # stream = A2A::Server::SSE::RestStream.new(task_id: "t1", context_id: "c1")
24
+ #
25
+ # Async do
26
+ # stream.task(status: { state: "TASK_STATE_WORKING", timestamp: "..." })
27
+ # stream.status_update(status: { state: "TASK_STATE_COMPLETED", timestamp: "..." })
28
+ # stream.finish
29
+ # end
30
+ #
31
+ # # Return as Rack body — Falcon streams it natively
32
+ # [200, { "content-type" => "text/event-stream" }, stream]
33
+ #
34
+ class Stream < ::Protocol::HTTP::Body::Writable
35
+ SSE_HEADERS = {
36
+ "content-type" => "text/event-stream",
37
+ "cache-control" => "no-cache, no-transform",
38
+ "x-accel-buffering" => "no",
39
+ "connection" => "keep-alive",
40
+ }.freeze
41
+
42
+ attr_reader :task_id, :context_id
43
+
44
+ def initialize(task_id:, context_id:, **options)
45
+ @task_id = task_id
46
+ @context_id = context_id
47
+ super(**options)
48
+ end
49
+
50
+ # Emit an SSE event.
51
+ #
52
+ # @param data [Hash, String] the event payload (Hashes are JSON-encoded)
53
+ # @param type [String, nil] optional SSE event type field
54
+ # @param id [String, nil] optional SSE event id field
55
+ #
56
+ def event(data, type: nil, id: nil)
57
+ payload = data.is_a?(String) ? data : JSON.generate(data)
58
+
59
+ buf = String.new
60
+ buf << "event: #{type}\n" if type
61
+ buf << "id: #{id}\n" if id
62
+
63
+ # SSE spec: each line of data gets its own `data:` prefix.
64
+ # For single-line JSON this is one line; multi-line is handled correctly.
65
+ payload.each_line do |line|
66
+ buf << "data: #{line.chomp}\n"
67
+ end
68
+ buf << "\n" # blank line terminates the event
69
+
70
+ write(buf)
71
+ end
72
+
73
+ # Signal end-of-stream.
74
+ # The reader will receive nil on next read(), closing the SSE connection.
75
+ def finish
76
+ close_write
77
+ end
78
+
79
+ # Convenience: the SSE headers to return in the Rack response.
80
+ # Returns a fresh copy — Rack response headers must be mutable,
81
+ # since middleware further up the stack (e.g. Rails' RequestId)
82
+ # adds headers in place.
83
+ def self.headers
84
+ SSE_HEADERS.dup
85
+ end
86
+
87
+ # --- Typed event emitters -------------------------------------------
88
+ #
89
+ # Dynamically generated from the StreamResponse schema.
90
+ # Each property in StreamResponse (task, message, statusUpdate,
91
+ # artifactUpdate) gets a snake_case method that:
92
+ #
93
+ # 1. Injects @task_id and @context_id (using the correct key name
94
+ # for each type — Task uses `id`, others use `task_id`)
95
+ # 2. Builds the schema Definition from kwargs
96
+ # 3. Wraps it in the StreamResponse envelope
97
+ # 4. Calls #event to emit the SSE wire format
98
+ #
99
+ # This means you never pass task_id/context_id manually:
100
+ #
101
+ # stream.task(status: { state: "TASK_STATE_WORKING" })
102
+ # stream.artifact_update(artifact: { ... }, append: false, last_chunk: true)
103
+ # stream.status_update(status: { state: "TASK_STATE_COMPLETED" })
104
+ # stream.message(role: "ROLE_AGENT", parts: [{ text: "Hello" }])
105
+ #
106
+
107
+ stream_response = A2A::Protocol::JsonSchema["Stream Response"]
108
+
109
+ stream_response.property_refs.each do |camel_key, (_kind, title)|
110
+ target_props = A2A::Protocol::JsonSchema[title].schema_properties
111
+
112
+ # Task uses `id` for the task identifier; the other three use `taskId`
113
+ task_id_key = target_props.include?("id") ? :id : :task_id
114
+
115
+ # camelCase -> snake_case method name
116
+ method_name = camel_key
117
+ .gsub(/([A-Z])/) { "_#{$1.downcase}" }
118
+ .delete_prefix("_")
119
+
120
+ define_method(method_name) do |**kwargs|
121
+ merged = { task_id_key => @task_id, context_id: @context_id }.merge(kwargs)
122
+ event({ camel_key => A2A::Protocol::JsonSchema[title].new(merged).to_h })
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ __END__
131
+ require "async"
132
+
133
+ describe "A2A::Server::SSE::Stream" do
134
+ it "formats SSE events correctly" do
135
+ stream = A2A::Server::SSE::Stream.new(task_id: "t1", context_id: "c1")
136
+
137
+ stream.event({ "task" => { "id" => "t1" } })
138
+ stream.finish
139
+
140
+ chunk = stream.read
141
+ chunk.should.include("data: ")
142
+ chunk.should.include('"task"')
143
+ chunk.should.end_with("\n\n")
144
+
145
+ # After finish, read returns nil
146
+ stream.read.should.be.nil
147
+ end
148
+
149
+ it "includes event type when provided" do
150
+ stream = A2A::Server::SSE::Stream.new(task_id: "t1", context_id: "c1")
151
+
152
+ stream.event("hello", type: "ping")
153
+ stream.finish
154
+
155
+ chunk = stream.read
156
+ chunk.should.include("event: ping\n")
157
+ chunk.should.include("data: hello\n")
158
+ end
159
+
160
+ it "includes event id when provided" do
161
+ stream = A2A::Server::SSE::Stream.new(task_id: "t1", context_id: "c1")
162
+
163
+ stream.event("test", id: "42")
164
+ stream.finish
165
+
166
+ chunk = stream.read
167
+ chunk.should.include("id: 42\n")
168
+ end
169
+
170
+ it "is a ::Protocol::HTTP::Body::Readable" do
171
+ stream = A2A::Server::SSE::Stream.new(task_id: "t1", context_id: "c1")
172
+ stream.is_a?(::Protocol::HTTP::Body::Readable).should == true
173
+ end
174
+
175
+ it "provides standard SSE headers" do
176
+ headers = A2A::Server::SSE::Stream.headers
177
+ headers["content-type"].should == "text/event-stream"
178
+ headers["cache-control"].should.include("no-cache")
179
+ end
180
+
181
+ it "stores task_id and context_id" do
182
+ stream = A2A::Server::SSE::Stream.new(task_id: "t1", context_id: "c1")
183
+ stream.task_id.should == "t1"
184
+ stream.context_id.should == "c1"
185
+ end
186
+
187
+ # --- Typed emit methods ---
188
+
189
+ it "#task emits a task event with injected id and context_id" do
190
+ stream = A2A::Server::SSE::Stream.new(task_id: "t1", context_id: "c1")
191
+
192
+ stream.task(status: { state: "TASK_STATE_WORKING", timestamp: "2025-01-01T00:00:00Z" })
193
+ stream.finish
194
+
195
+ chunk = stream.read
196
+ parsed = JSON.parse(chunk.sub(/\Adata: /, "").strip)
197
+ parsed["task"]["id"].should == "t1"
198
+ parsed["task"]["contextId"].should == "c1"
199
+ parsed["task"]["status"]["state"].should == "TASK_STATE_WORKING"
200
+ end
201
+
202
+ it "#status_update emits with injected task_id and context_id" do
203
+ stream = A2A::Server::SSE::Stream.new(task_id: "t1", context_id: "c1")
204
+
205
+ stream.status_update(status: { state: "TASK_STATE_COMPLETED", timestamp: "2025-01-01T00:00:00Z" })
206
+ stream.finish
207
+
208
+ chunk = stream.read
209
+ parsed = JSON.parse(chunk.sub(/\Adata: /, "").strip)
210
+ parsed["statusUpdate"]["taskId"].should == "t1"
211
+ parsed["statusUpdate"]["contextId"].should == "c1"
212
+ parsed["statusUpdate"]["status"]["state"].should == "TASK_STATE_COMPLETED"
213
+ end
214
+
215
+ it "#artifact_update emits with injected task_id and context_id" do
216
+ stream = A2A::Server::SSE::Stream.new(task_id: "t1", context_id: "c1")
217
+
218
+ stream.artifact_update(
219
+ artifact: { artifact_id: "a1", parts: [{ text: "hello" }] },
220
+ append: false,
221
+ last_chunk: true
222
+ )
223
+ stream.finish
224
+
225
+ chunk = stream.read
226
+ parsed = JSON.parse(chunk.sub(/\Adata: /, "").strip)
227
+ parsed["artifactUpdate"]["taskId"].should == "t1"
228
+ parsed["artifactUpdate"]["contextId"].should == "c1"
229
+ parsed["artifactUpdate"]["artifact"]["artifactId"].should == "a1"
230
+ parsed["artifactUpdate"]["append"].should == false
231
+ parsed["artifactUpdate"]["lastChunk"].should == true
232
+ end
233
+
234
+ it "#message emits with injected task_id and context_id" do
235
+ stream = A2A::Server::SSE::Stream.new(task_id: "t1", context_id: "c1")
236
+
237
+ stream.message(
238
+ message_id: "m1",
239
+ role: "ROLE_AGENT",
240
+ parts: [{ text: "Hello" }]
241
+ )
242
+ stream.finish
243
+
244
+ chunk = stream.read
245
+ parsed = JSON.parse(chunk.sub(/\Adata: /, "").strip)
246
+ parsed["message"]["taskId"].should == "t1"
247
+ parsed["message"]["contextId"].should == "c1"
248
+ parsed["message"]["role"].should == "ROLE_AGENT"
249
+ end
250
+
251
+ it "typed methods allow overriding injected values" do
252
+ stream = A2A::Server::SSE::Stream.new(task_id: "t1", context_id: "c1")
253
+
254
+ stream.task(id: "override", status: { state: "TASK_STATE_WORKING" })
255
+ stream.finish
256
+
257
+ chunk = stream.read
258
+ parsed = JSON.parse(chunk.sub(/\Adata: /, "").strip)
259
+ parsed["task"]["id"].should == "override"
260
+ end
261
+ end
@@ -10,7 +10,7 @@ module A2A
10
10
  # Reads the raw protocol data left in env by the binding middleware
11
11
  # (JSON-RPC method name or HTTP verb + path), looks up the matching
12
12
  # Proto::Operation, wraps the request body into a Schema::Definition,
13
- # and sets env keys for downstream consumption by the Dispatcher.
13
+ # and sets env keys for downstream consumption by the agent.
14
14
  #
15
15
  # Env keys read:
16
16
  # "a2a.json_rpc_method" — set by Bindings::JsonRpc
@@ -58,14 +58,14 @@ module A2A
58
58
 
59
59
  def resolve_operation(env)
60
60
  if env["a2a.json_rpc_method"]
61
- Proto.operation(env["a2a.json_rpc_method"])
61
+ Protocol::Protobuf.operation(env["a2a.json_rpc_method"])
62
62
  elsif env["a2a.verb"] && env["a2a.path"]
63
63
  match_rest_operation(env["a2a.verb"], env["a2a.path"])
64
64
  end
65
65
  end
66
66
 
67
67
  def match_rest_operation(verb, path)
68
- Proto.operations.find do |op|
68
+ Protocol::Protobuf.operations.find do |op|
69
69
  op.http_bindings.any? do |b|
70
70
  b.verb == verb && path_matches?(b.path, path)
71
71
  end
@@ -80,7 +80,10 @@ module A2A
80
80
  end
81
81
 
82
82
  def pattern_to_regex(pattern)
83
- re = pattern.gsub(/\{[^}]+\}/, '([^/]+)')
83
+ # Path params must not swallow ":" — custom-method suffixes like
84
+ # "/tasks/{id=*}:subscribe" would otherwise be captured into the
85
+ # id by plainer patterns like "/tasks/{id=*}".
86
+ re = pattern.gsub(/\{[^}]+\}/, '([^/:]+)')
84
87
  /\A#{re}\z/
85
88
  end
86
89
 
@@ -95,3 +98,31 @@ module A2A
95
98
  end
96
99
  end
97
100
  end
101
+
102
+ __END__
103
+ describe "A2A::Server::Triage" do
104
+ triage = -> (env) { A2A::Server::Triage.new(->(e) { e }).call(env) }
105
+
106
+ it "resolves GET /tasks/{id} to GetTask" do
107
+ env = triage.({ "a2a.verb" => "get", "a2a.path" => "/tasks/abc-123", "a2a.body" => {} })
108
+ env["a2a.operation"].should == "GetTask"
109
+ env["a2a.request"].id.should == "abc-123"
110
+ end
111
+
112
+ it "resolves GET /tasks/{id}:subscribe to SubscribeToTask, not GetTask" do
113
+ env = triage.({ "a2a.verb" => "get", "a2a.path" => "/tasks/abc-123:subscribe", "a2a.body" => {} })
114
+ env["a2a.operation"].should == "SubscribeToTask"
115
+ env["a2a.request"].id.should == "abc-123"
116
+ end
117
+
118
+ it "resolves POST /tasks/{id}:cancel to CancelTask" do
119
+ env = triage.({ "a2a.verb" => "post", "a2a.path" => "/tasks/abc-123:cancel", "a2a.body" => {} })
120
+ env["a2a.operation"].should == "CancelTask"
121
+ env["a2a.request"].id.should == "abc-123"
122
+ end
123
+
124
+ it "returns UnsupportedOperationError for an unknown path" do
125
+ result = triage.({ "a2a.verb" => "get", "a2a.path" => "/nope", "a2a.body" => {} })
126
+ result.should.be.is_a(A2A::UnsupportedOperationError)
127
+ end
128
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "a2a"
5
+
6
+ module A2A
7
+ class Server
8
+ # Rack middleware that serves the agent card at the well-known
9
+ # discovery path. All other requests pass through untouched.
10
+ #
11
+ class WellKnown
12
+ PATH = "/.well-known/agent-card.json"
13
+
14
+ def initialize(app)
15
+ @app = app
16
+ end
17
+
18
+ def call(env)
19
+ return @app.call(env) unless env["PATH_INFO"] == PATH
20
+
21
+ card = env["a2a.agent_card"] || {}
22
+ body = card.is_a?(Hash) ? card : card.to_h
23
+ [200, { "content-type" => "application/json" }, [JSON.generate(body)]]
24
+ end
25
+ end
26
+ end
27
+ end
data/lib/a2a/server.rb CHANGED
@@ -4,57 +4,49 @@ require "bundler/setup"
4
4
  require "a2a"
5
5
 
6
6
  require "a2a/server/env"
7
+ require "a2a/server/well_known"
7
8
  require "a2a/server/triage"
8
- require "a2a/server/dispatcher"
9
9
 
10
10
  module A2A
11
11
  # Rack application that exposes an A2A-compliant agent server.
12
12
  #
13
- # Composes two separate middleware stacks, one for each protocol binding:
13
+ # Composes a single linear middleware stack. Each middleware checks the
14
+ # request path and either handles the request or passes it downstream:
14
15
  #
15
- # /.well-known/agent-card.json
16
- # Env → serve agent card
16
+ # Env → injects shared context into the env
17
+ # WellKnown serves /.well-known/agent-card.json
18
+ # Bindings::Grpc → /grpc (reserved) → 501 Not Implemented
19
+ # Bindings::Rest → /rest (HTTP+JSON/REST)
20
+ # Bindings::JsonRpc → everything else (JSON-RPC 2.0)
21
+ # SSEStream → offers env["a2a.stream"] to the agent
22
+ # Triage → resolves the target operation
23
+ # ExtractMessage → sets env["a2a.message"] when a message is present
24
+ # LimitHistoryLength → sets env["a2a.history_length"]
25
+ # LimitPaginationSize → sets env["a2a.page_size"]
26
+ # Agent → the terminal app (your block)
17
27
  #
18
- # /a2a (JSON-RPC 2.0)
19
- # Env Bindings::JsonRpc Triage → Dispatcher
28
+ # The block is the terminal rack app — it is executed at the end of
29
+ # the stack, wrapped in the A2A::Agent error boundary. Usage
30
+ # (A2A.agent is a shorthand for this):
20
31
  #
21
- # / (HTTP+JSON/REST)
22
- # Env → Bindings::Rest → Triage → Dispatcher
23
- #
24
- # Usage:
25
- #
26
- # agent = A2A::Agent.new do
27
- # on "SendMessage" do
28
- # respond_with -> (env) {
29
- # A2A::Schema["Send Message Response"].new({})
30
- # }
32
+ # app = A2A::Server.new(agent_card: { "name" => "My Agent", ... }) do |env|
33
+ # case env["a2a.operation"]
34
+ # in "SendMessage"
35
+ # A2A::Protocol::JsonSchema["Send Message Response"].new({})
31
36
  # end
32
37
  # end
33
38
  #
34
- # app = A2A::Server.new(agent_card: { "name" => "My Agent", ... })
35
- # app.register(agent)
36
- #
37
39
  # run app
38
40
  #
39
41
  class Server
40
- def initialize(agent_card: {})
41
- @agent_card = agent_card
42
- @dispatcher = Dispatcher.new
43
- @app = build_app
44
- end
42
+ def initialize(agent_card: {}, history_length: 100, page_size: 100, &block)
43
+ raise ArgumentError, "Server requires a block" unless block
45
44
 
46
- # Register an Agent or a plain handler on the internal dispatcher.
47
- #
48
- # Accepts either:
49
- # - An Agent (responds to #handlers) — registers all its handlers
50
- # - A plain handler (responds to #operations and #call)
51
- #
52
- def register(handler)
53
- if handler.respond_to?(:handlers)
54
- handler.handlers.each { |h| @dispatcher.register(h) }
55
- else
56
- @dispatcher.register(handler)
57
- end
45
+ @agent_card = agent_card
46
+ @agent = Agent.new(&block)
47
+ @history_length = history_length
48
+ @page_size = page_size
49
+ @app = build_app
58
50
  end
59
51
 
60
52
  def call(env)
@@ -64,34 +56,28 @@ module A2A
64
56
  private
65
57
 
66
58
  def build_app
59
+ require "a2a/server/bindings/grpc"
67
60
  require "a2a/server/bindings/json_rpc"
68
61
  require "a2a/server/bindings/rest"
62
+ require "a2a/server/middleware"
69
63
 
70
- agent_card = @agent_card
71
- dispatcher = @dispatcher
64
+ agent_card = @agent_card
65
+ agent = @agent
66
+ history_length = @history_length
67
+ page_size = @page_size
72
68
 
73
69
  Rack::Builder.app do
74
70
  use A2A::Server::Env, agent_card: agent_card
75
-
76
- map "/.well-known/agent-card.json" do
77
- run ->(env) {
78
- card = env["a2a.agent_card"] || {}
79
- body = card.is_a?(Hash) ? card : card.to_h
80
- [200, { "content-type" => "application/json" }, [JSON.generate(body)]]
81
- }
82
- end
83
-
84
- map "/a2a" do
85
- use A2A::Server::Bindings::JsonRpc
86
- use A2A::Server::Triage
87
- run dispatcher
88
- end
89
-
90
- map "/" do
91
- use A2A::Server::Bindings::Rest
92
- use A2A::Server::Triage
93
- run dispatcher
94
- end
71
+ use A2A::Server::WellKnown
72
+ use A2A::Server::Bindings::Grpc, path_prefix: "/grpc"
73
+ use A2A::Server::Bindings::Rest, path_prefix: "/rest"
74
+ use A2A::Server::Bindings::JsonRpc, path_prefix: "/"
75
+ use A2A::Server::Middleware::SSEStream
76
+ use A2A::Server::Triage
77
+ use A2A::Server::Middleware::ExtractMessage
78
+ use A2A::Server::Middleware::LimitHistoryLength, history_length
79
+ use A2A::Server::Middleware::LimitPaginationSize, page_size
80
+ run agent
95
81
  end
96
82
  end
97
83
  end
@@ -2,86 +2,45 @@
2
2
 
3
3
  require "a2a"
4
4
  require "a2a/agent"
5
- require "a2a/proto"
6
- require "a2a/schema"
5
+ require "a2a/protocol/protobuf"
6
+ require "a2a/protocol/json_schema"
7
7
 
8
8
  module A2A
9
9
  module TestHelpers
10
- # Returns a stub agent that handles all A2A operations with
10
+ # Returns a stub agent server that handles all A2A operations with
11
11
  # minimal valid responses. Useful for integration tests that
12
12
  # need a working server without real business logic.
13
- def self.stub_agent
14
- Agent.new do
15
- on "SendMessage" do
16
- respond_with -> (env) {
17
- A2A::Schema["Send Message Response"].new({})
18
- }
19
- end
20
-
21
- on "SendStreamingMessage" do
22
- respond_with -> (env) {
23
- A2A::Schema["Stream Response"].new({})
24
- }
25
- end
26
-
27
- on "GetTask" do
28
- respond_with -> (env) {
29
- A2A::Schema["Task"].new(
30
- "id" => "test-id",
31
- "contextId" => "ctx-1",
32
- "status" => { "state" => "TASK_STATE_COMPLETED", "timestamp" => "2025-01-01T00:00:00Z" }
33
- )
34
- }
35
- end
36
-
37
- on "ListTasks" do
38
- respond_with -> (env) {
39
- A2A::Schema["List Tasks Response"].new({})
40
- }
41
- end
42
-
43
- on "CancelTask" do
44
- respond_with -> (env) {
45
- A2A::Schema["Task"].new(
46
- "id" => "test-id",
47
- "contextId" => "ctx-1",
48
- "status" => { "state" => "TASK_STATE_CANCELED", "timestamp" => "2025-01-01T00:00:00Z" }
49
- )
50
- }
51
- end
52
-
53
- on "SubscribeToTask" do
54
- respond_with -> (env) {
55
- A2A::Schema["Stream Response"].new({})
56
- }
57
- end
58
-
59
- on "CreateTaskPushNotificationConfig" do
60
- respond_with -> (env) {
61
- A2A::Schema["Task Push Notification Config"].new("url" => "http://example.com")
62
- }
63
- end
64
-
65
- on "GetTaskPushNotificationConfig" do
66
- respond_with -> (env) {
67
- A2A::Schema["Task Push Notification Config"].new("url" => "http://example.com")
68
- }
69
- end
70
-
71
- on "ListTaskPushNotificationConfigs" do
72
- respond_with -> (env) {
73
- A2A::Schema["List Task Push Notification Configs Response"].new({})
74
- }
75
- end
76
-
77
- on "GetExtendedAgentCard" do
78
- respond_with -> (env) {
79
- A2A::Schema["Agent Card"].new("name" => "Test", "version" => "1.0")
80
- }
81
- end
82
-
83
- on "DeleteTaskPushNotificationConfig" do
84
- respond_with -> (env) { nil }
13
+ def self.stub_agent(agent_card: {})
14
+ schema = A2A::Protocol::JsonSchema
15
+
16
+ A2A.agent(agent_card: agent_card) do |env|
17
+ case env["a2a.operation"]
18
+ in "SendMessage"
19
+ schema["Send Message Response"].new({})
20
+ in "SendStreamingMessage" | "SubscribeToTask"
21
+ schema["Stream Response"].new({})
22
+ in "GetTask"
23
+ schema["Task"].new(
24
+ "id" => "test-id",
25
+ "contextId" => "ctx-1",
26
+ "status" => { "state" => "TASK_STATE_COMPLETED", "timestamp" => "2025-01-01T00:00:00Z" }
27
+ )
28
+ in "CancelTask"
29
+ schema["Task"].new(
30
+ "id" => "test-id",
31
+ "contextId" => "ctx-1",
32
+ "status" => { "state" => "TASK_STATE_CANCELED", "timestamp" => "2025-01-01T00:00:00Z" }
33
+ )
34
+ in "ListTasks"
35
+ schema["List Tasks Response"].new({})
36
+ in "CreateTaskPushNotificationConfig" | "GetTaskPushNotificationConfig"
37
+ schema["Task Push Notification Config"].new("url" => "http://example.com")
38
+ in "ListTaskPushNotificationConfigs"
39
+ schema["List Task Push Notification Configs Response"].new({})
40
+ in "GetExtendedAgentCard"
41
+ schema["Agent Card"].new("name" => "Test", "version" => "1.0")
42
+ in "DeleteTaskPushNotificationConfig"
43
+ nil
85
44
  end
86
45
  end
87
46
  end
data/lib/a2a/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module A2A
4
- VERSION = "1.1.0"
4
+ VERSION = "2.0.0"
5
5
  end
data/lib/a2a.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/setup"
4
- require "scampi"
5
4
  require "json"
6
5
  require "json_schemer"
7
6
  require "async"
@@ -13,18 +12,15 @@ module A2A
13
12
  end
14
13
 
15
14
  require "a2a/errors"
16
- require "a2a/proto"
17
- require "a2a/schema"
18
- require "a2a/schema/definition"
19
- require "a2a/schema/validation_error"
20
- require "a2a/sse"
15
+ require "a2a/protocol"
16
+ require "a2a/server/sse"
21
17
  require "a2a/agent"
22
18
 
23
19
  require "a2a/server"
24
20
 
25
- require "a2a/faraday/middleware/schema_request"
26
- require "a2a/faraday/middleware/json_rpc/request"
27
- require "a2a/faraday/middleware/json_rpc/response"
28
- require "a2a/faraday/middleware/rest/request"
29
- require "a2a/faraday/middleware/rest/response"
21
+ require "a2a/client/middleware/schema_request"
22
+ require "a2a/client/middleware/json_rpc/request"
23
+ require "a2a/client/middleware/json_rpc/response"
24
+ require "a2a/client/middleware/rest/request"
25
+ require "a2a/client/middleware/rest/response"
30
26
  require "a2a/client"