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.
- checksums.yaml +4 -4
- data/lib/a2a/agent.rb +165 -117
- data/lib/a2a/client.rb +470 -51
- data/lib/a2a/errors/json_rpc_error.rb +71 -0
- data/lib/a2a/errors/rest_error.rb +68 -0
- data/lib/a2a/errors.rb +535 -0
- data/lib/a2a/faraday/middleware/json_rpc/request.rb +96 -0
- data/lib/a2a/faraday/middleware/json_rpc/response.rb +131 -0
- data/lib/a2a/faraday/middleware/rest/request.rb +166 -0
- data/lib/a2a/faraday/middleware/rest/response.rb +144 -0
- data/lib/a2a/faraday/middleware/schema_request.rb +69 -0
- data/lib/a2a/middleware/extract_message.rb +120 -0
- data/lib/a2a/middleware/fetch_task.rb +228 -0
- data/lib/a2a/middleware/limit_history_length.rb +123 -0
- data/lib/a2a/middleware/limit_pagination_size.rb +133 -0
- data/lib/a2a/middleware/sse_stream.rb +235 -0
- data/lib/a2a/middleware.rb +7 -0
- data/lib/a2a/schema/definition.rb +35 -1
- data/lib/a2a/schema.rb +126 -0
- data/lib/a2a/{bindings → server/bindings}/json_rpc.rb +12 -8
- data/lib/a2a/{bindings → server/bindings}/rest.rb +12 -8
- data/lib/a2a/server/dispatcher.rb +52 -54
- data/lib/a2a/server/env.rb +4 -6
- data/lib/a2a/server/triage.rb +1 -1
- data/lib/a2a/server.rb +10 -10
- data/lib/a2a/sse/event_parser.rb +202 -0
- data/lib/a2a/sse/json_rpc_stream.rb +27 -5
- data/lib/a2a/sse/rest_stream.rb +17 -5
- data/lib/a2a/sse/stream.rb +135 -7
- data/lib/a2a/sse.rb +1 -0
- data/lib/a2a/test_helpers.rb +89 -0
- data/lib/a2a/version.rb +1 -1
- data/lib/a2a.rb +6 -2
- data/lib/traces/provider/a2a/{bindings → server/bindings}/json_rpc.rb +2 -2
- data/lib/traces/provider/a2a/{bindings → server/bindings}/rest.rb +2 -2
- data/lib/traces/provider/a2a.rb +2 -2
- metadata +49 -22
- data/lib/a2a/server/cancel_task.rb +0 -14
- data/lib/a2a/server/create_task_push_notification_config.rb +0 -14
- data/lib/a2a/server/delete_task_push_notification_config.rb +0 -14
- data/lib/a2a/server/get_extended_agent_card.rb +0 -15
- data/lib/a2a/server/get_task.rb +0 -14
- data/lib/a2a/server/get_task_push_notification_config.rb +0 -14
- data/lib/a2a/server/list_task_push_notification_configs.rb +0 -14
- data/lib/a2a/server/list_tasks.rb +0 -14
- data/lib/a2a/server/send_message.rb +0 -14
- data/lib/a2a/server/send_streaming_message.rb +0 -14
- data/lib/a2a/server/subscribe_to_task.rb +0 -14
- data/lib/a2a/store/processor.rb +0 -136
- data/lib/a2a/store/pub_sub.rb +0 -149
- data/lib/a2a/store/sqlite.rb +0 -533
- data/lib/a2a/store/webhooks.rb +0 -94
- data/lib/a2a/store.rb +0 -6
- 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
|