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,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "a2a"
5
+
6
+ module A2A
7
+ # Raised by the REST response middleware when the server returns an
8
+ # HTTP 4xx/5xx with an application/problem+json body. Preserves
9
+ # the HTTP status, title/message, and optional detail array.
10
+ #
11
+ class RestError < Error
12
+ def initialize(message, http_status:, data: nil)
13
+ @wire_data = data
14
+ super(message, code: http_status, http_status: http_status)
15
+ end
16
+
17
+ def error_data
18
+ @wire_data
19
+ end
20
+ end
21
+ end
22
+
23
+ test do
24
+ describe "A2A::RestError" do
25
+ it "has correct http_status and message" do
26
+ err = A2A::RestError.new("Not Found", http_status: 404)
27
+ err.http_status.should == 404
28
+ err.message.should == "Not Found"
29
+ end
30
+
31
+ it "uses http_status as the code" do
32
+ err = A2A::RestError.new("Bad Request", http_status: 400)
33
+ err.code.should == 400
34
+ end
35
+
36
+ it "preserves wire data" do
37
+ data = [{
38
+ "@type" => "type.googleapis.com/google.rpc.ErrorInfo",
39
+ "reason" => "TASK_NOT_FOUND",
40
+ "domain" => "a2a-protocol.org",
41
+ "metadata" => { "taskId" => "t-1" },
42
+ }]
43
+ err = A2A::RestError.new("Not Found", http_status: 404, data: data)
44
+ err.error_data.should == data
45
+ err.error_data.first["reason"].should == "TASK_NOT_FOUND"
46
+ end
47
+
48
+ it "returns nil error_data when no data provided" do
49
+ err = A2A::RestError.new("Internal Server Error", http_status: 500)
50
+ err.error_data.should.be.nil
51
+ end
52
+
53
+ it "is a subclass of A2A::Error" do
54
+ err = A2A::RestError.new("fail", http_status: 400)
55
+ err.is_a?(A2A::Error).should == true
56
+ end
57
+
58
+ it "serializes to_h with wire data" do
59
+ data = [{ "reason" => "UNSUPPORTED_OPERATION" }]
60
+ err = A2A::RestError.new("Unsupported", http_status: 400, data: data)
61
+ h = err.to_h
62
+ h[:code].should == 400
63
+ h[:http_status].should == 400
64
+ h[:message].should == "Unsupported"
65
+ h[:data].should == data
66
+ end
67
+ end
68
+ end
data/lib/a2a/errors.rb ADDED
@@ -0,0 +1,535 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A2A Protocol Error Types
4
+ #
5
+ # Reference: A2A Specification v1.0
6
+ # - Section 3.3.2 Error Handling (error categories & A2A-specific errors)
7
+ # - Section 5.4 Error Code Mappings (JSON-RPC / gRPC / HTTP mappings)
8
+ #
9
+ # Source: refs/A2A/docs/specification.md
10
+ #
11
+ # Error Code Mappings (Section 5.4):
12
+ #
13
+ # A2A Error Type | JSON-RPC | gRPC | HTTP
14
+ # ------------------------------------|----------|---------------------|-----
15
+ # TaskNotFoundError | -32001 | NOT_FOUND | 404
16
+ # TaskNotCancelableError | -32002 | FAILED_PRECONDITION | 400
17
+ # PushNotificationNotSupportedError | -32003 | FAILED_PRECONDITION | 400
18
+ # UnsupportedOperationError | -32004 | FAILED_PRECONDITION | 400
19
+ # ContentTypeNotSupportedError | -32005 | INVALID_ARGUMENT | 400
20
+ # InvalidAgentResponseError | -32006 | INTERNAL | 500
21
+ # ExtendedAgentCardNotConfiguredError | -32007 | FAILED_PRECONDITION | 400
22
+ # ExtensionSupportRequiredError | -32008 | FAILED_PRECONDITION | 400
23
+ # VersionNotSupportedError | -32009 | FAILED_PRECONDITION | 400
24
+ #
25
+ # Additionally, JSON-RPC standard -32602 is used for validation errors (InvalidParamsError).
26
+
27
+ require "bundler/setup"
28
+ require "a2a"
29
+
30
+ module A2A
31
+ # Base error class for A2A protocol errors.
32
+ #
33
+ # Each subclass carries its own JSON-RPC error code, HTTP status,
34
+ # and structured error data. The handler layer rescues these and
35
+ # calls #to_h to populate env["a2a.error"] for the binding layer.
36
+ #
37
+ class Error < StandardError
38
+ attr_reader :code, :http_status
39
+
40
+ def initialize(message, code:, http_status: 400)
41
+ @code = code
42
+ @http_status = http_status
43
+ super(message)
44
+ end
45
+
46
+ def error_data = nil
47
+
48
+ def to_h
49
+ h = { code: code, http_status: http_status, message: message }
50
+ h[:data] = error_data if error_data
51
+ h
52
+ end
53
+ end
54
+
55
+ # The specified task ID does not correspond to an existing or accessible task.
56
+ # It might be invalid, expired, or already completed and purged.
57
+ class TaskNotFoundError < Error
58
+ def initialize(task_id, message: "Task not found")
59
+ @task_id = task_id
60
+ super(message, code: -32001, http_status: 404)
61
+ end
62
+
63
+ def error_data
64
+ [{
65
+ "@type" => "type.googleapis.com/google.rpc.ErrorInfo",
66
+ "reason" => "TASK_NOT_FOUND",
67
+ "domain" => "a2a-protocol.org",
68
+ "metadata" => { "taskId" => @task_id.to_s },
69
+ }]
70
+ end
71
+ end
72
+
73
+ # An attempt was made to cancel a task that is not in a cancelable state
74
+ # (e.g., it has already reached a terminal state like completed, failed, or canceled).
75
+ class TaskNotCancelableError < Error
76
+ def initialize(task_id, state:, message: "Task is not cancelable")
77
+ @task_id = task_id
78
+ @state = state
79
+ super(message, code: -32002, http_status: 400)
80
+ end
81
+
82
+ def error_data
83
+ [{
84
+ "@type" => "type.googleapis.com/google.rpc.ErrorInfo",
85
+ "reason" => "TASK_NOT_CANCELABLE",
86
+ "domain" => "a2a-protocol.org",
87
+ "metadata" => { "taskId" => @task_id.to_s, "state" => @state.to_s },
88
+ }]
89
+ end
90
+ end
91
+
92
+ # Client attempted to use push notification features but the server agent
93
+ # does not support them (i.e., AgentCard.capabilities.pushNotifications is false).
94
+ class PushNotificationNotSupportedError < Error
95
+ def initialize(message: "Push notifications are not supported")
96
+ super(message, code: -32003, http_status: 400)
97
+ end
98
+
99
+ def error_data
100
+ [{
101
+ "@type" => "type.googleapis.com/google.rpc.ErrorInfo",
102
+ "reason" => "PUSH_NOTIFICATION_NOT_SUPPORTED",
103
+ "domain" => "a2a-protocol.org",
104
+ }]
105
+ end
106
+ end
107
+
108
+ # The requested operation or a specific aspect of it is not supported
109
+ # by this server agent implementation.
110
+ class UnsupportedOperationError < Error
111
+ def initialize(message: "Unsupported operation")
112
+ super(message, code: -32004, http_status: 400)
113
+ end
114
+
115
+ def error_data
116
+ [{
117
+ "@type" => "type.googleapis.com/google.rpc.ErrorInfo",
118
+ "reason" => "UNSUPPORTED_OPERATION",
119
+ "domain" => "a2a-protocol.org",
120
+ }]
121
+ end
122
+ end
123
+
124
+ # A Media Type provided in the request's message parts or implied for an artifact
125
+ # is not supported by the agent or the specific skill being invoked.
126
+ class ContentTypeNotSupportedError < Error
127
+ def initialize(content_type = nil, message: "Content type not supported")
128
+ @content_type = content_type
129
+ super(message, code: -32005, http_status: 400)
130
+ end
131
+
132
+ def error_data
133
+ meta = {}
134
+ meta["contentType"] = @content_type.to_s if @content_type
135
+ [{
136
+ "@type" => "type.googleapis.com/google.rpc.ErrorInfo",
137
+ "reason" => "CONTENT_TYPE_NOT_SUPPORTED",
138
+ "domain" => "a2a-protocol.org",
139
+ "metadata" => meta,
140
+ }]
141
+ end
142
+ end
143
+
144
+ # An agent returned a response that does not conform to the specification
145
+ # for the current method.
146
+ class InvalidAgentResponseError < Error
147
+ def initialize(message: "Invalid agent response")
148
+ super(message, code: -32006, http_status: 500)
149
+ end
150
+
151
+ def error_data
152
+ [{
153
+ "@type" => "type.googleapis.com/google.rpc.ErrorInfo",
154
+ "reason" => "INVALID_AGENT_RESPONSE",
155
+ "domain" => "a2a-protocol.org",
156
+ }]
157
+ end
158
+ end
159
+
160
+ # The agent does not have an extended agent card configured when one is
161
+ # required for the requested operation.
162
+ class ExtendedAgentCardNotConfiguredError < Error
163
+ def initialize(message: "Extended agent card is not configured")
164
+ super(message, code: -32007, http_status: 400)
165
+ end
166
+
167
+ def error_data
168
+ [{
169
+ "@type" => "type.googleapis.com/google.rpc.ErrorInfo",
170
+ "reason" => "EXTENDED_AGENT_CARD_NOT_CONFIGURED",
171
+ "domain" => "a2a-protocol.org",
172
+ }]
173
+ end
174
+ end
175
+
176
+ # Server requested use of an extension marked as required: true in the
177
+ # Agent Card but the client did not declare support for it in the request.
178
+ class ExtensionSupportRequiredError < Error
179
+ def initialize(extension = nil, message: "Extension support required")
180
+ @extension = extension
181
+ super(message, code: -32008, http_status: 400)
182
+ end
183
+
184
+ def error_data
185
+ meta = {}
186
+ meta["extension"] = @extension.to_s if @extension
187
+ [{
188
+ "@type" => "type.googleapis.com/google.rpc.ErrorInfo",
189
+ "reason" => "EXTENSION_SUPPORT_REQUIRED",
190
+ "domain" => "a2a-protocol.org",
191
+ "metadata" => meta,
192
+ }]
193
+ end
194
+ end
195
+
196
+ # The A2A protocol version specified in the request (via A2A-Version service
197
+ # parameter) is not supported by the agent.
198
+ class VersionNotSupportedError < Error
199
+ def initialize(version = nil, message: "Version not supported")
200
+ @version = version
201
+ super(message, code: -32009, http_status: 400)
202
+ end
203
+
204
+ def error_data
205
+ meta = {}
206
+ meta["version"] = @version.to_s if @version
207
+ [{
208
+ "@type" => "type.googleapis.com/google.rpc.ErrorInfo",
209
+ "reason" => "VERSION_NOT_SUPPORTED",
210
+ "domain" => "a2a-protocol.org",
211
+ "metadata" => meta,
212
+ }]
213
+ end
214
+ end
215
+
216
+ # Validation error for invalid input parameters or message format.
217
+ # Maps to JSON-RPC standard code -32602 (Invalid params).
218
+ class InvalidParamsError < Error
219
+ def initialize(message, fields: nil)
220
+ @fields = fields
221
+ super(message, code: -32602, http_status: 400)
222
+ end
223
+
224
+ def error_data
225
+ return nil unless @fields
226
+ [{
227
+ "@type" => "type.googleapis.com/google.rpc.ErrorInfo",
228
+ "reason" => "INVALID_PARAMS",
229
+ "domain" => "a2a-protocol.org",
230
+ "metadata" => { "fields" => Array(@fields).join(", ") },
231
+ }]
232
+ end
233
+ end
234
+
235
+ # Internal (non-spec) errors used by this library's implementation.
236
+ module Internal
237
+ module Errors
238
+ # Raised when a specific push notification config cannot be found by ID.
239
+ # This is an implementation detail — the spec does not define this error.
240
+ class PushNotificationConfigNotFoundError < A2A::Error
241
+ def initialize(task_id, config_id, message: "Push notification config not found")
242
+ @task_id = task_id
243
+ @config_id = config_id
244
+ super(message, code: -32001, http_status: 404)
245
+ end
246
+
247
+ def error_data
248
+ [{
249
+ "@type" => "type.googleapis.com/google.rpc.ErrorInfo",
250
+ "reason" => "TASK_NOT_FOUND",
251
+ "domain" => "a2a-protocol.org",
252
+ "metadata" => { "taskId" => @task_id.to_s, "configId" => @config_id.to_s },
253
+ }]
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
259
+
260
+ require "a2a/errors/json_rpc_error"
261
+ require "a2a/errors/rest_error"
262
+
263
+ # Specification excerpt: refs/A2A/docs/specification.md, Section 3.3.2 Error Handling
264
+ #
265
+ # All operations may return errors in the following categories. Servers MUST return appropriate
266
+ # errors and SHOULD provide actionable information to help clients resolve issues.
267
+ #
268
+ # Error Categories and Server Requirements:
269
+ #
270
+ # - Authentication Errors: Invalid or missing credentials
271
+ # - Servers MUST reject requests with invalid or missing authentication credentials
272
+ # - Servers SHOULD include authentication challenge information in the error response
273
+ # - Servers SHOULD specify which authentication scheme is required
274
+ # - Example error codes: HTTP 401 Unauthorized, gRPC UNAUTHENTICATED, JSON-RPC custom error
275
+ # - Example scenarios: Missing bearer token, expired API key, invalid OAuth token
276
+ #
277
+ # - Authorization Errors: Insufficient permissions for requested operation
278
+ # - Servers MUST return an authorization error when the authenticated client lacks required permissions
279
+ # - Servers SHOULD indicate what permission or scope is missing (without leaking sensitive information
280
+ # about resources the client cannot access)
281
+ # - Servers MUST NOT reveal the existence of resources the client is not authorized to access
282
+ # - Example error codes: HTTP 403 Forbidden, gRPC PERMISSION_DENIED, JSON-RPC custom error
283
+ # - Example scenarios: Attempting to access a task created by another user, insufficient OAuth scopes
284
+ #
285
+ # - Validation Errors: Invalid input parameters or message format
286
+ # - Servers MUST validate all input parameters before processing
287
+ # - Servers SHOULD specify which parameter(s) failed validation and why
288
+ # - Servers SHOULD provide guidance on valid parameter values or formats
289
+ # - Example error codes: HTTP 400 Bad Request, gRPC INVALID_ARGUMENT, JSON-RPC -32602 Invalid params
290
+ # - Example scenarios: Invalid task ID format, missing required message parts, unsupported content type
291
+ #
292
+ # - Resource Errors: Requested task not found or not accessible
293
+ # - Servers MUST return a not found error when a requested resource does not exist or is not accessible
294
+ # to the authenticated client
295
+ # - Servers SHOULD NOT distinguish between "does not exist" and "not authorized" to prevent
296
+ # information leakage
297
+ # - Example error codes: HTTP 404 Not Found, gRPC NOT_FOUND, JSON-RPC custom error
298
+ # (see A2A-specific errors)
299
+ # - Example scenarios: Task ID does not exist, task has been deleted, configuration not found
300
+ #
301
+ # - System Errors: Internal agent failures or temporary unavailability
302
+ # - Servers SHOULD return appropriate error codes for temporary failures vs. permanent errors
303
+ # - Servers MAY include retry guidance (e.g., Retry-After header in HTTP)
304
+ # - Servers SHOULD log system errors for diagnostic purposes
305
+ # - Example error codes: HTTP 500 Internal Server Error or 503 Service Unavailable, gRPC INTERNAL or
306
+ # UNAVAILABLE, JSON-RPC -32603 Internal error
307
+ # - Example scenarios: Database connection failure, downstream service timeout, rate limit exceeded
308
+ #
309
+ # Error Payload Structure:
310
+ #
311
+ # All error responses in the A2A protocol, regardless of binding, MUST convey the following information:
312
+ #
313
+ # 1. Error Code: A machine-readable identifier for the error type (e.g., string code, numeric code, or
314
+ # protocol-specific status)
315
+ # 2. Error Message: A human-readable description of the error
316
+ # 3. Error Details (optional): An array of objects providing additional structured information about the
317
+ # error. Each object in the array MUST include a @type key that identifies the object's type (using
318
+ # ProtoJSON Any representation). Well-known types from the google.rpc error model (e.g., ErrorInfo,
319
+ # BadRequest) SHOULD be used where applicable. Error details may be used for:
320
+ # - Affected fields or parameters
321
+ # - Contextual information (e.g., task ID, timestamp)
322
+ # - Suggestions for resolution
323
+ #
324
+ # Protocol bindings MUST map these elements to their native error representations while preserving
325
+ # semantic meaning.
326
+ #
327
+ # A2A-Specific Errors:
328
+ #
329
+ # | Error Name | Description
330
+ # | ------------------------------------- | ---------------------------------------------------------------------------
331
+ # | TaskNotFoundError | The specified task ID does not correspond to an existing or accessible task.
332
+ # | TaskNotCancelableError | An attempt was made to cancel a task that is not in a cancelable state.
333
+ # | PushNotificationNotSupportedError | Client attempted to use unsupported push notification features.
334
+ # | UnsupportedOperationError | The requested operation or aspect is unsupported by this server agent.
335
+ # | ContentTypeNotSupportedError | A Media Type in request parts or artifacts is unsupported.
336
+ # | InvalidAgentResponseError | An agent returned a response that does not conform to the specification.
337
+ # | ExtendedAgentCardNotConfiguredError | The agent lacks a configured extended agent card when required.
338
+ # | ExtensionSupportRequiredError | A required extension was not declared as supported by the client.
339
+ # | VersionNotSupportedError | The requested A2A-Version is not supported by the agent.
340
+ test do
341
+ describe "A2A::Error" do
342
+ it "has code, http_status, and message" do
343
+ err = A2A::Error.new("boom", code: -32000, http_status: 500)
344
+ err.code.should == -32000
345
+ err.http_status.should == 500
346
+ err.message.should == "boom"
347
+ end
348
+
349
+ it "serializes to_h without data when error_data is nil" do
350
+ err = A2A::Error.new("boom", code: -32000, http_status: 500)
351
+ h = err.to_h
352
+ h[:code].should == -32000
353
+ h[:http_status].should == 500
354
+ h[:message].should == "boom"
355
+ h.key?(:data).should == false
356
+ end
357
+ end
358
+
359
+ describe "A2A::TaskNotFoundError" do
360
+ it "has correct code and http_status" do
361
+ err = A2A::TaskNotFoundError.new("task-123")
362
+ err.code.should == -32001
363
+ err.http_status.should == 404
364
+ err.message.should == "Task not found"
365
+ end
366
+
367
+ it "serializes to_h with structured error_data" do
368
+ err = A2A::TaskNotFoundError.new("task-123")
369
+ h = err.to_h
370
+ h[:data].first["reason"].should == "TASK_NOT_FOUND"
371
+ h[:data].first["metadata"]["taskId"].should == "task-123"
372
+ end
373
+
374
+ it "accepts a custom message" do
375
+ err = A2A::TaskNotFoundError.new("task-123", message: "No such task")
376
+ err.message.should == "No such task"
377
+ end
378
+ end
379
+
380
+ describe "A2A::TaskNotCancelableError" do
381
+ it "has correct code and http_status" do
382
+ err = A2A::TaskNotCancelableError.new("task-123", state: "TASK_STATE_COMPLETED")
383
+ err.code.should == -32002
384
+ err.http_status.should == 400
385
+ end
386
+
387
+ it "includes task and state in error_data" do
388
+ err = A2A::TaskNotCancelableError.new("task-123", state: "TASK_STATE_COMPLETED")
389
+ h = err.to_h
390
+ h[:data].first["reason"].should == "TASK_NOT_CANCELABLE"
391
+ h[:data].first["metadata"]["taskId"].should == "task-123"
392
+ h[:data].first["metadata"]["state"].should == "TASK_STATE_COMPLETED"
393
+ end
394
+ end
395
+
396
+ describe "A2A::PushNotificationNotSupportedError" do
397
+ it "has correct code and http_status" do
398
+ err = A2A::PushNotificationNotSupportedError.new
399
+ err.code.should == -32003
400
+ err.http_status.should == 400
401
+ end
402
+
403
+ it "has PUSH_NOTIFICATION_NOT_SUPPORTED in error_data" do
404
+ err = A2A::PushNotificationNotSupportedError.new
405
+ err.to_h[:data].first["reason"].should == "PUSH_NOTIFICATION_NOT_SUPPORTED"
406
+ err.to_h[:data].first["domain"].should == "a2a-protocol.org"
407
+ end
408
+ end
409
+
410
+ describe "A2A::UnsupportedOperationError" do
411
+ it "has correct code and http_status" do
412
+ err = A2A::UnsupportedOperationError.new
413
+ err.code.should == -32004
414
+ err.http_status.should == 400
415
+ end
416
+
417
+ it "accepts a custom message" do
418
+ err = A2A::UnsupportedOperationError.new(message: "Streaming not supported")
419
+ err.message.should == "Streaming not supported"
420
+ end
421
+
422
+ it "has UNSUPPORTED_OPERATION in error_data" do
423
+ err = A2A::UnsupportedOperationError.new
424
+ err.to_h[:data].first["reason"].should == "UNSUPPORTED_OPERATION"
425
+ end
426
+ end
427
+
428
+ describe "A2A::ContentTypeNotSupportedError" do
429
+ it "has correct code and http_status" do
430
+ err = A2A::ContentTypeNotSupportedError.new("image/bmp")
431
+ err.code.should == -32005
432
+ err.http_status.should == 400
433
+ end
434
+
435
+ it "includes content type in metadata" do
436
+ err = A2A::ContentTypeNotSupportedError.new("image/bmp")
437
+ err.to_h[:data].first["reason"].should == "CONTENT_TYPE_NOT_SUPPORTED"
438
+ err.to_h[:data].first["metadata"]["contentType"].should == "image/bmp"
439
+ end
440
+ end
441
+
442
+ describe "A2A::InvalidAgentResponseError" do
443
+ it "has correct code and http_status" do
444
+ err = A2A::InvalidAgentResponseError.new
445
+ err.code.should == -32006
446
+ err.http_status.should == 500
447
+ end
448
+
449
+ it "has INVALID_AGENT_RESPONSE in error_data" do
450
+ err = A2A::InvalidAgentResponseError.new
451
+ err.to_h[:data].first["reason"].should == "INVALID_AGENT_RESPONSE"
452
+ end
453
+ end
454
+
455
+ describe "A2A::ExtendedAgentCardNotConfiguredError" do
456
+ it "has correct code and http_status" do
457
+ err = A2A::ExtendedAgentCardNotConfiguredError.new
458
+ err.code.should == -32007
459
+ err.http_status.should == 400
460
+ end
461
+
462
+ it "has EXTENDED_AGENT_CARD_NOT_CONFIGURED in error_data" do
463
+ err = A2A::ExtendedAgentCardNotConfiguredError.new
464
+ err.to_h[:data].first["reason"].should == "EXTENDED_AGENT_CARD_NOT_CONFIGURED"
465
+ end
466
+ end
467
+
468
+ describe "A2A::ExtensionSupportRequiredError" do
469
+ it "has correct code and http_status" do
470
+ err = A2A::ExtensionSupportRequiredError.new("my-extension")
471
+ err.code.should == -32008
472
+ err.http_status.should == 400
473
+ end
474
+
475
+ it "includes extension in metadata" do
476
+ err = A2A::ExtensionSupportRequiredError.new("my-extension")
477
+ err.to_h[:data].first["reason"].should == "EXTENSION_SUPPORT_REQUIRED"
478
+ err.to_h[:data].first["metadata"]["extension"].should == "my-extension"
479
+ end
480
+ end
481
+
482
+ describe "A2A::VersionNotSupportedError" do
483
+ it "has correct code and http_status" do
484
+ err = A2A::VersionNotSupportedError.new("0.5")
485
+ err.code.should == -32009
486
+ err.http_status.should == 400
487
+ end
488
+
489
+ it "includes version in metadata" do
490
+ err = A2A::VersionNotSupportedError.new("0.5")
491
+ err.to_h[:data].first["reason"].should == "VERSION_NOT_SUPPORTED"
492
+ err.to_h[:data].first["metadata"]["version"].should == "0.5"
493
+ end
494
+ end
495
+
496
+ describe "A2A::InvalidParamsError" do
497
+ it "has correct code and http_status" do
498
+ err = A2A::InvalidParamsError.new("topic is required")
499
+ err.code.should == -32602
500
+ err.http_status.should == 400
501
+ err.message.should == "topic is required"
502
+ end
503
+
504
+ it "has no error_data when fields not provided" do
505
+ err = A2A::InvalidParamsError.new("bad params")
506
+ err.to_h.key?(:data).should == false
507
+ end
508
+
509
+ it "includes fields in error_data when provided" do
510
+ err = A2A::InvalidParamsError.new("invalid fields", fields: ["topic", "message"])
511
+ err.to_h[:data].first["reason"].should == "INVALID_PARAMS"
512
+ err.to_h[:data].first["metadata"]["fields"].should == "topic, message"
513
+ end
514
+ end
515
+
516
+ describe "A2A::Internal::Errors::PushNotificationConfigNotFoundError" do
517
+ it "has correct code and http_status" do
518
+ err = A2A::Internal::Errors::PushNotificationConfigNotFoundError.new("task-123", "config-456")
519
+ err.code.should == -32001
520
+ err.http_status.should == 404
521
+ end
522
+
523
+ it "includes task and config in error_data" do
524
+ err = A2A::Internal::Errors::PushNotificationConfigNotFoundError.new("task-123", "config-456")
525
+ h = err.to_h
526
+ h[:data].first["metadata"]["taskId"].should == "task-123"
527
+ h[:data].first["metadata"]["configId"].should == "config-456"
528
+ end
529
+
530
+ it "is a subclass of A2A::Error" do
531
+ err = A2A::Internal::Errors::PushNotificationConfigNotFoundError.new("t", "c")
532
+ err.is_a?(A2A::Error).should == true
533
+ end
534
+ end
535
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "a2a"
5
+ require "faraday"
6
+
7
+ module A2A
8
+ module Faraday
9
+ module Middleware
10
+ module JsonRpc
11
+ # Faraday request middleware that wraps the request body in a
12
+ # JSON-RPC 2.0 envelope.
13
+ #
14
+ # Reads env.request.context[:a2a_operation] to determine the
15
+ # JSON-RPC method name. If no operation is set, passes through.
16
+ #
17
+ class Request < ::Faraday::Middleware
18
+ def on_request(env)
19
+ operation = env.request.context&.dig(:a2a_operation)
20
+ return unless operation
21
+
22
+ env.url.path = "/a2a"
23
+ env.method = :post
24
+
25
+ env.body = {
26
+ jsonrpc: "2.0",
27
+ id: next_id,
28
+ method: operation.json_rpc_method,
29
+ params: env.body || {},
30
+ }
31
+ end
32
+
33
+ private
34
+
35
+ def next_id
36
+ @id_counter = (@id_counter || 0) + 1
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ ::Faraday::Request.register_middleware(a2a_json_rpc: A2A::Faraday::Middleware::JsonRpc::Request)
45
+
46
+ test do
47
+ middleware = A2A::Faraday::Middleware::JsonRpc::Request
48
+ operation = A2A::Proto.operation("SendMessage")
49
+
50
+ it "wraps body in JSON-RPC 2.0 envelope and sets path to /a2a" do
51
+ env = ::Faraday::Env.new
52
+ env.url = URI.parse("http://localhost:9292/")
53
+ env.body = { "message" => { "role" => "ROLE_USER" } }
54
+ env.request = ::Faraday::RequestOptions.new
55
+ env.request.context = { a2a_operation: operation }
56
+
57
+ middleware.new(nil).on_request(env)
58
+
59
+ env.url.path.should == "/a2a"
60
+ env.method.should == :post
61
+ env.body[:jsonrpc].should == "2.0"
62
+ env.body[:id].should == 1
63
+ env.body[:method].should == "SendMessage"
64
+ env.body[:params].should == { "message" => { "role" => "ROLE_USER" } }
65
+ end
66
+
67
+ it "increments the id" do
68
+ mw = middleware.new(nil)
69
+
70
+ env1 = ::Faraday::Env.new
71
+ env1.url = URI.parse("http://localhost:9292/")
72
+ env1.body = {}
73
+ env1.request = ::Faraday::RequestOptions.new
74
+ env1.request.context = { a2a_operation: operation }
75
+ mw.on_request(env1)
76
+
77
+ env2 = ::Faraday::Env.new
78
+ env2.url = URI.parse("http://localhost:9292/")
79
+ env2.body = {}
80
+ env2.request = ::Faraday::RequestOptions.new
81
+ env2.request.context = { a2a_operation: operation }
82
+ mw.on_request(env2)
83
+
84
+ env2.body[:id].should == env1.body[:id] + 1
85
+ end
86
+
87
+ it "passes through when no operation is set" do
88
+ env = ::Faraday::Env.new
89
+ env.body = { "foo" => "bar" }
90
+ env.request = ::Faraday::RequestOptions.new
91
+
92
+ middleware.new(nil).on_request(env)
93
+
94
+ env.body.should == { "foo" => "bar" }
95
+ end
96
+ end