a2a-rb 0.1.1
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 +7 -0
- data/CHANGELOG.md +38 -0
- data/LICENSE.txt +21 -0
- data/README.md +311 -0
- data/lib/a2a/agent_capabilities.rb +32 -0
- data/lib/a2a/agent_card/builder.rb +135 -0
- data/lib/a2a/agent_card/signature.rb +35 -0
- data/lib/a2a/agent_card/verifier.rb +19 -0
- data/lib/a2a/agent_card.rb +118 -0
- data/lib/a2a/agent_extension.rb +32 -0
- data/lib/a2a/agent_interface.rb +54 -0
- data/lib/a2a/agent_provider.rb +20 -0
- data/lib/a2a/agent_skill.rb +46 -0
- data/lib/a2a/artifact.rb +40 -0
- data/lib/a2a/client.rb +109 -0
- data/lib/a2a/discovery.rb +49 -0
- data/lib/a2a/json_rpc_envelope.rb +32 -0
- data/lib/a2a/message.rb +54 -0
- data/lib/a2a/oauth_flow/authorization_code.rb +37 -0
- data/lib/a2a/oauth_flow/client_credentials.rb +31 -0
- data/lib/a2a/oauth_flow/device_code.rb +34 -0
- data/lib/a2a/oauth_flow.rb +37 -0
- data/lib/a2a/operation/cancel_task.rb +39 -0
- data/lib/a2a/operation/create_task_push_notification_config.rb +38 -0
- data/lib/a2a/operation/delete_task_push_notification_config.rb +38 -0
- data/lib/a2a/operation/executable.rb +27 -0
- data/lib/a2a/operation/get_extended_agent_card.rb +32 -0
- data/lib/a2a/operation/get_task.rb +39 -0
- data/lib/a2a/operation/get_task_push_notification_config.rb +38 -0
- data/lib/a2a/operation/list_task_push_notification_configs.rb +64 -0
- data/lib/a2a/operation/list_tasks.rb +78 -0
- data/lib/a2a/operation/send_message/configuration.rb +39 -0
- data/lib/a2a/operation/send_message.rb +58 -0
- data/lib/a2a/operation/send_message_request.rb +37 -0
- data/lib/a2a/operation/send_streaming_message.rb +53 -0
- data/lib/a2a/operation/subscribe_to_task.rb +40 -0
- data/lib/a2a/operation.rb +19 -0
- data/lib/a2a/part/data.rb +34 -0
- data/lib/a2a/part/file.rb +45 -0
- data/lib/a2a/part/text.rb +34 -0
- data/lib/a2a/part.rb +21 -0
- data/lib/a2a/protocol/http_json/transport.rb +82 -0
- data/lib/a2a/protocol/http_json.rb +53 -0
- data/lib/a2a/protocol/json_rpc/transport.rb +54 -0
- data/lib/a2a/protocol/json_rpc.rb +55 -0
- data/lib/a2a/push_notification/authentication_info.rb +29 -0
- data/lib/a2a/push_notification/config.rb +40 -0
- data/lib/a2a/push_notification/dispatcher.rb +52 -0
- data/lib/a2a/push_notification/receiver.rb +54 -0
- data/lib/a2a/push_notification.rb +11 -0
- data/lib/a2a/role.rb +13 -0
- data/lib/a2a/security_requirement.rb +19 -0
- data/lib/a2a/security_scheme/api_key.rb +33 -0
- data/lib/a2a/security_scheme/http_auth.rb +33 -0
- data/lib/a2a/security_scheme/mutual_tls.rb +25 -0
- data/lib/a2a/security_scheme/oauth2.rb +52 -0
- data/lib/a2a/security_scheme/open_id_connect.rb +30 -0
- data/lib/a2a/security_scheme.rb +26 -0
- data/lib/a2a/streaming/artifact_update_event.rb +40 -0
- data/lib/a2a/streaming/response.rb +65 -0
- data/lib/a2a/streaming/sse_parser.rb +43 -0
- data/lib/a2a/streaming/sse_writer.rb +25 -0
- data/lib/a2a/streaming/status_update_event.rb +43 -0
- data/lib/a2a/streaming/subscription.rb +56 -0
- data/lib/a2a/streaming.rb +12 -0
- data/lib/a2a/task/state.rb +31 -0
- data/lib/a2a/task/status.rb +35 -0
- data/lib/a2a/task.rb +66 -0
- data/lib/a2a/version.rb +6 -0
- data/lib/a2a/versioning.rb +28 -0
- data/lib/a2a.rb +90 -0
- metadata +116 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
module SecurityScheme
|
|
5
|
+
class MutualTLS
|
|
6
|
+
attr_reader :description
|
|
7
|
+
|
|
8
|
+
def initialize(description: nil)
|
|
9
|
+
@description = description
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.from_h(hash)
|
|
13
|
+
new(description: hash["description"])
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_h
|
|
17
|
+
{
|
|
18
|
+
"mtlsSecurityScheme" => {
|
|
19
|
+
"description" => description
|
|
20
|
+
}.compact
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
module SecurityScheme
|
|
5
|
+
class OAuth2
|
|
6
|
+
attr_reader :flows, :oauth2_metadata_url, :description
|
|
7
|
+
|
|
8
|
+
FLOW_TYPES = [
|
|
9
|
+
OAuthFlow::AuthorizationCode,
|
|
10
|
+
OAuthFlow::ClientCredentials,
|
|
11
|
+
OAuthFlow::DeviceCode
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(flows:, oauth2_metadata_url: nil, description: nil)
|
|
15
|
+
unless flows.is_a?(Hash) && flows.size == 1 && FLOW_TYPES.any? { |t| flows.values.first.is_a?(t) }
|
|
16
|
+
raise ArgumentError, "flows must be a Hash with exactly one OAuthFlow entry"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@flows = flows
|
|
20
|
+
@oauth2_metadata_url = oauth2_metadata_url
|
|
21
|
+
@description = description
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.from_h(hash)
|
|
25
|
+
new(
|
|
26
|
+
flows: OAuthFlow.from_h(hash.fetch("flows")),
|
|
27
|
+
oauth2_metadata_url: hash["oauth2MetadataUrl"],
|
|
28
|
+
description: hash["description"]
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_h
|
|
33
|
+
{
|
|
34
|
+
"oauth2SecurityScheme" => {
|
|
35
|
+
"flows" => serialize_flows,
|
|
36
|
+
"oauth2MetadataUrl" => oauth2_metadata_url,
|
|
37
|
+
"description" => description
|
|
38
|
+
}.compact
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def serialize_flows
|
|
45
|
+
flows.each_with_object({}) do |(key, flow), hash|
|
|
46
|
+
protocol_key = key.to_s.gsub(/_([a-z])/) { ::Regexp.last_match(1).upcase }
|
|
47
|
+
hash[protocol_key] = flow.to_h
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
module SecurityScheme
|
|
5
|
+
class OpenIDConnect
|
|
6
|
+
attr_reader :open_id_connect_url, :description
|
|
7
|
+
|
|
8
|
+
def initialize(open_id_connect_url:, description: nil)
|
|
9
|
+
@open_id_connect_url = open_id_connect_url
|
|
10
|
+
@description = description
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.from_h(hash)
|
|
14
|
+
new(
|
|
15
|
+
open_id_connect_url: hash.fetch("openIdConnectUrl"),
|
|
16
|
+
description: hash["description"],
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_h
|
|
21
|
+
{
|
|
22
|
+
"openIdConnectSecurityScheme" => {
|
|
23
|
+
"openIdConnectUrl" => open_id_connect_url,
|
|
24
|
+
"description" => description
|
|
25
|
+
}.compact
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "security_scheme/api_key"
|
|
4
|
+
require_relative "security_scheme/http_auth"
|
|
5
|
+
require_relative "security_scheme/oauth2"
|
|
6
|
+
require_relative "security_scheme/open_id_connect"
|
|
7
|
+
require_relative "security_scheme/mutual_tls"
|
|
8
|
+
|
|
9
|
+
module A2A
|
|
10
|
+
module SecurityScheme
|
|
11
|
+
BUILDERS = {
|
|
12
|
+
"apiKeySecurityScheme" => ->(v) { APIKey.from_h(v) },
|
|
13
|
+
"httpAuthSecurityScheme" => ->(v) { HTTPAuth.from_h(v) },
|
|
14
|
+
"oauth2SecurityScheme" => ->(v) { OAuth2.from_h(v) },
|
|
15
|
+
"openIdConnectSecurityScheme" => ->(v) { OpenIDConnect.from_h(v) },
|
|
16
|
+
"mtlsSecurityScheme" => ->(v) { MutualTLS.from_h(v) }
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def self.from_h(hash)
|
|
20
|
+
key, builder = BUILDERS.find { |k, _| hash.key?(k) }
|
|
21
|
+
raise ArgumentError, "unknown SecurityScheme: #{hash.keys.inspect}" unless key
|
|
22
|
+
|
|
23
|
+
builder.call(hash[key])
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
module Streaming
|
|
5
|
+
class ArtifactUpdateEvent
|
|
6
|
+
attr_reader :task_id, :context_id, :artifact, :append, :last_chunk, :metadata
|
|
7
|
+
|
|
8
|
+
def initialize(task_id:, context_id:, artifact:, **kwargs)
|
|
9
|
+
@task_id = task_id
|
|
10
|
+
@context_id = context_id
|
|
11
|
+
@artifact = artifact
|
|
12
|
+
@append = kwargs[:append] || false
|
|
13
|
+
@last_chunk = kwargs[:last_chunk] || false
|
|
14
|
+
@metadata = kwargs[:metadata]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
{
|
|
19
|
+
"taskId" => task_id,
|
|
20
|
+
"contextId" => context_id,
|
|
21
|
+
"artifact" => artifact.to_h,
|
|
22
|
+
"append" => append,
|
|
23
|
+
"lastChunk" => last_chunk,
|
|
24
|
+
"metadata" => metadata
|
|
25
|
+
}.compact
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.from_h(hash)
|
|
29
|
+
new(
|
|
30
|
+
task_id: hash.fetch("taskId"),
|
|
31
|
+
context_id: hash.fetch("contextId"),
|
|
32
|
+
artifact: Artifact.from_h(hash.fetch("artifact")),
|
|
33
|
+
append: hash["append"],
|
|
34
|
+
last_chunk: hash["lastChunk"],
|
|
35
|
+
metadata: hash["metadata"]
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
module Streaming
|
|
5
|
+
class Response
|
|
6
|
+
PAYLOAD_TYPES = {
|
|
7
|
+
task: Task,
|
|
8
|
+
message: Message,
|
|
9
|
+
status_update: StatusUpdateEvent,
|
|
10
|
+
artifact_update: ArtifactUpdateEvent
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
BUILDERS = {
|
|
14
|
+
"task" => ->(v) { new(:task, Task.from_h(v)) },
|
|
15
|
+
"message" => ->(v) { new(:message, Message.from_h(v)) },
|
|
16
|
+
"statusUpdate" => ->(v) { new(:status_update, StatusUpdateEvent.from_h(v)) },
|
|
17
|
+
"artifactUpdate" => ->(v) { new(:artifact_update, ArtifactUpdateEvent.from_h(v)) }
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :type, :payload
|
|
21
|
+
|
|
22
|
+
def initialize(type, payload)
|
|
23
|
+
expected = PAYLOAD_TYPES.fetch(type) { raise ArgumentError, "unknown type: #{type.inspect}" }
|
|
24
|
+
raise TypeError, "payload must be a #{expected}" unless payload.is_a?(expected)
|
|
25
|
+
|
|
26
|
+
@type = type
|
|
27
|
+
@payload = payload
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.from_h(hash)
|
|
31
|
+
key, builder = BUILDERS.find { |k, _| hash.key?(k) }
|
|
32
|
+
raise ArgumentError, "unrecognised StreamResponse keys: #{hash.keys.inspect}" unless key
|
|
33
|
+
|
|
34
|
+
builder.call(hash[key])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
WIRE_KEYS = {
|
|
38
|
+
task: "task",
|
|
39
|
+
message: "message",
|
|
40
|
+
status_update: "statusUpdate",
|
|
41
|
+
artifact_update: "artifactUpdate"
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
44
|
+
def to_h
|
|
45
|
+
{ WIRE_KEYS.fetch(type) => payload.to_h }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def task?
|
|
49
|
+
type == :task
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def message?
|
|
53
|
+
type == :message
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def status_update?
|
|
57
|
+
type == :status_update
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def artifact_update?
|
|
61
|
+
type == :artifact_update
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module A2A
|
|
6
|
+
module Streaming
|
|
7
|
+
module SseParser
|
|
8
|
+
# Parses a stream of SSE lines, yielding one Streaming::Response per
|
|
9
|
+
# logical event (blank-line delimited, multi-line data: concatenated).
|
|
10
|
+
def self.each(io, &block)
|
|
11
|
+
buffer = +""
|
|
12
|
+
|
|
13
|
+
io.each_line do |line|
|
|
14
|
+
line = line.chomp
|
|
15
|
+
|
|
16
|
+
if line.empty?
|
|
17
|
+
emit(buffer, &block) unless buffer.empty?
|
|
18
|
+
buffer = +""
|
|
19
|
+
elsif line.start_with?("data:")
|
|
20
|
+
# SSE spec: multiple data: lines are concatenated with U+000A
|
|
21
|
+
buffer << "\n" unless buffer.empty?
|
|
22
|
+
buffer << line.delete_prefix("data:").lstrip
|
|
23
|
+
end
|
|
24
|
+
# skip comment lines (":"), "event:", "id:", "retry:" fields
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
emit(buffer, &block) unless buffer.empty?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.emit(buffer)
|
|
31
|
+
data = buffer.strip
|
|
32
|
+
return if data == "[DONE]"
|
|
33
|
+
|
|
34
|
+
envelope = JSON.parse(data)
|
|
35
|
+
raise A2A.from_json_rpc_error(envelope["error"]) if envelope["error"]
|
|
36
|
+
|
|
37
|
+
event = Response.from_h(Hash(envelope["result"]))
|
|
38
|
+
yield event
|
|
39
|
+
end
|
|
40
|
+
private_class_method :emit
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module A2A
|
|
6
|
+
module Streaming
|
|
7
|
+
# Server-side counterpart to SseParser. Formats Streaming::Response objects
|
|
8
|
+
# (or raw hashes) as SSE frames ready to write to an HTTP response body.
|
|
9
|
+
module SSEWriter
|
|
10
|
+
# Returns the SSE wire representation of a Streaming::Response as a String.
|
|
11
|
+
# The result includes the trailing blank line that delimits SSE events.
|
|
12
|
+
def self.encode(response, id: nil)
|
|
13
|
+
payload = response.is_a?(Response) ? response.to_h : response
|
|
14
|
+
envelope = JSONRPCEnvelope.success(id: id, result: payload)
|
|
15
|
+
"data: #{JSON.generate(envelope)}\n\n"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Encodes a JSON-RPC error as an SSE frame.
|
|
19
|
+
def self.encode_error(error, id: nil)
|
|
20
|
+
envelope = JSONRPCEnvelope.error(id: id, error: error)
|
|
21
|
+
"data: #{JSON.generate(envelope)}\n\n"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
module Streaming
|
|
5
|
+
# The `final` field is a gem-level streaming-termination hint, not in the A2A proto.
|
|
6
|
+
# Servers that set "final": true signal this is the last status event for the task.
|
|
7
|
+
class StatusUpdateEvent
|
|
8
|
+
attr_reader :task_id, :context_id, :status, :final, :metadata
|
|
9
|
+
|
|
10
|
+
def initialize(context_id:, task_id:, status:, **kwargs)
|
|
11
|
+
raise TypeError, "status must be a Task::Status" unless status.is_a?(Task::Status)
|
|
12
|
+
|
|
13
|
+
@context_id = context_id
|
|
14
|
+
@task_id = task_id
|
|
15
|
+
@status = status
|
|
16
|
+
@final = kwargs.fetch(:final, false)
|
|
17
|
+
@metadata = kwargs[:metadata]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def final? = @final
|
|
21
|
+
|
|
22
|
+
def to_h
|
|
23
|
+
{
|
|
24
|
+
"taskId" => task_id,
|
|
25
|
+
"contextId" => context_id,
|
|
26
|
+
"status" => status.to_h,
|
|
27
|
+
"final" => final,
|
|
28
|
+
"metadata" => metadata
|
|
29
|
+
}.compact
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.from_h(hash)
|
|
33
|
+
new(
|
|
34
|
+
task_id: hash.fetch("taskId"),
|
|
35
|
+
context_id: hash.fetch("contextId"),
|
|
36
|
+
status: Task::Status.from_h(hash.fetch("status")),
|
|
37
|
+
final: hash.fetch("final", false),
|
|
38
|
+
metadata: hash["metadata"]
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
module Streaming
|
|
5
|
+
class Subscription
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
def initialize(io)
|
|
9
|
+
@io = io
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def each(&block)
|
|
13
|
+
return enum_for(:each) unless block
|
|
14
|
+
|
|
15
|
+
SseParser.each(reader) do |event|
|
|
16
|
+
yield event
|
|
17
|
+
break if terminal?(event)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# Net::HTTPResponse streams via read_body; plain IO responds to each_line.
|
|
24
|
+
def reader
|
|
25
|
+
return @io unless @io.respond_to?(:read_body)
|
|
26
|
+
|
|
27
|
+
ChunkedReader.new(@io)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def terminal?(event)
|
|
31
|
+
case event.type
|
|
32
|
+
when :status_update
|
|
33
|
+
payload = event.payload
|
|
34
|
+
payload.final? || Task::State.terminal?(payload.status.state)
|
|
35
|
+
when :task
|
|
36
|
+
Task::State.terminal?(event.payload.status.state)
|
|
37
|
+
else
|
|
38
|
+
false
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Wraps Net::HTTPResponse so SseParser can call each_line on it.
|
|
43
|
+
class ChunkedReader
|
|
44
|
+
def initialize(response)
|
|
45
|
+
@response = response
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def each_line(&block)
|
|
49
|
+
@response.read_body do |chunk|
|
|
50
|
+
chunk.each_line(&block)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "streaming/status_update_event"
|
|
4
|
+
require_relative "streaming/artifact_update_event"
|
|
5
|
+
require_relative "streaming/response"
|
|
6
|
+
require_relative "streaming/sse_parser"
|
|
7
|
+
require_relative "streaming/subscription"
|
|
8
|
+
|
|
9
|
+
module A2A
|
|
10
|
+
module Streaming
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
class Task
|
|
5
|
+
module State
|
|
6
|
+
UNSPECIFIED = "TASK_STATE_UNSPECIFIED"
|
|
7
|
+
SUBMITTED = "TASK_STATE_SUBMITTED"
|
|
8
|
+
WORKING = "TASK_STATE_WORKING"
|
|
9
|
+
INPUT_REQUIRED = "TASK_STATE_INPUT_REQUIRED"
|
|
10
|
+
AUTH_REQUIRED = "TASK_STATE_AUTH_REQUIRED"
|
|
11
|
+
COMPLETED = "TASK_STATE_COMPLETED"
|
|
12
|
+
FAILED = "TASK_STATE_FAILED"
|
|
13
|
+
CANCELED = "TASK_STATE_CANCELED"
|
|
14
|
+
REJECTED = "TASK_STATE_REJECTED"
|
|
15
|
+
|
|
16
|
+
ALL = [UNSPECIFIED, SUBMITTED, WORKING, INPUT_REQUIRED, AUTH_REQUIRED,
|
|
17
|
+
COMPLETED, FAILED, CANCELED, REJECTED].freeze
|
|
18
|
+
|
|
19
|
+
TERMINAL = [COMPLETED, FAILED, CANCELED, REJECTED].freeze
|
|
20
|
+
RESUMABLE = [INPUT_REQUIRED, AUTH_REQUIRED].freeze
|
|
21
|
+
|
|
22
|
+
def self.valid?(value)
|
|
23
|
+
ALL.include?(value)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.terminal?(value)
|
|
27
|
+
TERMINAL.include?(value)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
class Task
|
|
5
|
+
class Status
|
|
6
|
+
attr_reader :state, :message, :timestamp
|
|
7
|
+
|
|
8
|
+
def initialize(state:, message: nil, timestamp: nil)
|
|
9
|
+
@state = state
|
|
10
|
+
@message = message
|
|
11
|
+
@timestamp = timestamp
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.from_h(hash)
|
|
15
|
+
new(
|
|
16
|
+
state: hash.fetch("state"),
|
|
17
|
+
message: hash["message"] && Message.from_h(hash["message"]),
|
|
18
|
+
timestamp: hash["timestamp"]
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_h
|
|
23
|
+
{
|
|
24
|
+
"state" => state,
|
|
25
|
+
"message" => message&.to_h,
|
|
26
|
+
"timestamp" => timestamp
|
|
27
|
+
}.compact
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def terminal?
|
|
31
|
+
Task::State.terminal?(state)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/a2a/task.rb
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "task/state"
|
|
4
|
+
require_relative "task/status"
|
|
5
|
+
|
|
6
|
+
module A2A
|
|
7
|
+
class Task
|
|
8
|
+
attr_reader :id, :context_id, :status, :artifacts, :history, :metadata
|
|
9
|
+
|
|
10
|
+
def initialize(id:, status:, **kwargs)
|
|
11
|
+
raise TypeError, "status must be a Task::Status" unless status.is_a?(Task::Status)
|
|
12
|
+
|
|
13
|
+
@id = id
|
|
14
|
+
@status = status
|
|
15
|
+
@context_id = kwargs[:context_id]
|
|
16
|
+
@artifacts = kwargs[:artifacts]
|
|
17
|
+
@history = kwargs[:history]
|
|
18
|
+
@metadata = kwargs[:metadata]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.from_h(hash)
|
|
22
|
+
new(
|
|
23
|
+
id: hash.fetch("id"),
|
|
24
|
+
context_id: hash["contextId"],
|
|
25
|
+
status: Task::Status.from_h(hash.fetch("status")),
|
|
26
|
+
artifacts: Array(hash["artifacts"]).map { Artifact.from_h(_1) },
|
|
27
|
+
history: Array(hash["history"]).map { Message.from_h(_1) },
|
|
28
|
+
metadata: hash["metadata"]
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def terminal?
|
|
33
|
+
status.terminal?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns a new Task with the status transitioned to `state`.
|
|
37
|
+
# Optionally attaches a status message and ISO-8601 timestamp.
|
|
38
|
+
# Raises ArgumentError for unknown states; raises TaskNotCancelableError
|
|
39
|
+
# when trying to transition away from a terminal state.
|
|
40
|
+
def transition_to(state, message: nil, timestamp: nil)
|
|
41
|
+
raise ArgumentError, "unknown state: #{state.inspect}" unless Task::State.valid?(state)
|
|
42
|
+
raise TaskNotCancelableError, "task #{id} is already in terminal state #{status.state}" if terminal?
|
|
43
|
+
|
|
44
|
+
new_status = Task::Status.new(state: state, message: message, timestamp: timestamp)
|
|
45
|
+
Task.new(
|
|
46
|
+
id: id,
|
|
47
|
+
context_id: context_id,
|
|
48
|
+
status: new_status,
|
|
49
|
+
artifacts: artifacts,
|
|
50
|
+
history: history,
|
|
51
|
+
metadata: metadata
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def to_h
|
|
56
|
+
{
|
|
57
|
+
"id" => id,
|
|
58
|
+
"contextId" => context_id,
|
|
59
|
+
"status" => status.to_h,
|
|
60
|
+
"artifacts" => artifacts&.map(&:to_h),
|
|
61
|
+
"history" => history&.map(&:to_h),
|
|
62
|
+
"metadata" => metadata
|
|
63
|
+
}.compact
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/a2a/version.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
module Versioning
|
|
5
|
+
CURRENT = SPEC_VERSION
|
|
6
|
+
SUPPORTED = [CURRENT].freeze
|
|
7
|
+
|
|
8
|
+
# Returns the normalized Major.Minor string from a version value.
|
|
9
|
+
# Strips any patch segment so "1.0.2" is treated as "1.0".
|
|
10
|
+
def self.normalize(version)
|
|
11
|
+
parts = version.to_s.split(".")
|
|
12
|
+
"#{parts[0]}.#{parts[1]}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.supported?(version)
|
|
16
|
+
SUPPORTED.include?(version.to_s)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Raises VersionNotSupportedError when the version is not in SUPPORTED.
|
|
20
|
+
# Returns the normalized version string when valid.
|
|
21
|
+
def self.validate!(version)
|
|
22
|
+
v = normalize(version)
|
|
23
|
+
raise VersionNotSupportedError, "unsupported A2A version: #{v}" unless supported?(v)
|
|
24
|
+
|
|
25
|
+
v
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/a2a.rb
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "a2a/version"
|
|
4
|
+
require_relative "a2a/task"
|
|
5
|
+
require_relative "a2a/role"
|
|
6
|
+
require_relative "a2a/part"
|
|
7
|
+
require_relative "a2a/oauth_flow"
|
|
8
|
+
require_relative "a2a/security_scheme"
|
|
9
|
+
require_relative "a2a/security_requirement"
|
|
10
|
+
require_relative "a2a/push_notification"
|
|
11
|
+
require_relative "a2a/message"
|
|
12
|
+
require_relative "a2a/artifact"
|
|
13
|
+
require_relative "a2a/streaming"
|
|
14
|
+
require_relative "a2a/agent_extension"
|
|
15
|
+
require_relative "a2a/agent_interface"
|
|
16
|
+
require_relative "a2a/agent_capabilities"
|
|
17
|
+
require_relative "a2a/agent_provider"
|
|
18
|
+
require_relative "a2a/agent_skill"
|
|
19
|
+
require_relative "a2a/agent_card"
|
|
20
|
+
require_relative "a2a/versioning"
|
|
21
|
+
require_relative "a2a/protocol/json_rpc"
|
|
22
|
+
require_relative "a2a/protocol/http_json"
|
|
23
|
+
require_relative "a2a/operation"
|
|
24
|
+
require_relative "a2a/client"
|
|
25
|
+
require_relative "a2a/discovery"
|
|
26
|
+
require_relative "a2a/json_rpc_envelope"
|
|
27
|
+
require_relative "a2a/streaming/sse_writer"
|
|
28
|
+
require_relative "a2a/operation/send_message_request"
|
|
29
|
+
|
|
30
|
+
module A2A
|
|
31
|
+
class Error < StandardError
|
|
32
|
+
attr_reader :code, :details
|
|
33
|
+
|
|
34
|
+
def initialize(message, code: nil, details: nil)
|
|
35
|
+
super(message)
|
|
36
|
+
|
|
37
|
+
@code = code
|
|
38
|
+
@details = details
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Transport / HTTP errors
|
|
43
|
+
class TransportError < Error; end
|
|
44
|
+
class AuthenticationError < Error; end
|
|
45
|
+
class AuthorizationError < Error; end
|
|
46
|
+
class ValidationError < Error; end
|
|
47
|
+
|
|
48
|
+
# Standard JSON-RPC errors
|
|
49
|
+
class JSONParseError < Error; end
|
|
50
|
+
class InvalidRequestError < Error; end
|
|
51
|
+
class MethodNotFoundError < Error; end
|
|
52
|
+
class InvalidParamsError < Error; end
|
|
53
|
+
class InternalError < Error; end
|
|
54
|
+
|
|
55
|
+
# A2A protocol errors
|
|
56
|
+
class TaskNotFoundError < Error; end
|
|
57
|
+
class TaskNotCancelableError < Error; end
|
|
58
|
+
class PushNotificationNotSupportedError < Error; end
|
|
59
|
+
class UnsupportedOperationError < Error; end
|
|
60
|
+
class ContentTypeNotSupportedError < Error; end
|
|
61
|
+
class InvalidAgentResponseError < Error; end
|
|
62
|
+
class ExtendedAgentCardNotConfiguredError < Error; end
|
|
63
|
+
class ExtensionSupportRequiredError < Error; end
|
|
64
|
+
class VersionNotSupportedError < Error; end
|
|
65
|
+
|
|
66
|
+
CODE_MAP = {
|
|
67
|
+
-32700 => JSONParseError,
|
|
68
|
+
-32600 => InvalidRequestError,
|
|
69
|
+
-32601 => MethodNotFoundError,
|
|
70
|
+
-32602 => InvalidParamsError,
|
|
71
|
+
-32603 => InternalError,
|
|
72
|
+
-32001 => TaskNotFoundError,
|
|
73
|
+
-32002 => TaskNotCancelableError,
|
|
74
|
+
-32003 => PushNotificationNotSupportedError,
|
|
75
|
+
-32004 => UnsupportedOperationError,
|
|
76
|
+
-32005 => ContentTypeNotSupportedError,
|
|
77
|
+
-32006 => InvalidAgentResponseError,
|
|
78
|
+
-32007 => ExtendedAgentCardNotConfiguredError,
|
|
79
|
+
-32008 => ExtensionSupportRequiredError,
|
|
80
|
+
-32009 => VersionNotSupportedError
|
|
81
|
+
}.freeze
|
|
82
|
+
|
|
83
|
+
def self.from_json_rpc_error(hash)
|
|
84
|
+
(CODE_MAP[hash["code"]] || Error).new(
|
|
85
|
+
hash["message"] || "unknown A2A error",
|
|
86
|
+
code: hash["code"],
|
|
87
|
+
details: hash["data"]
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end
|