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,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "agent_card/signature"
|
|
5
|
+
require_relative "agent_card/verifier"
|
|
6
|
+
require_relative "agent_card/builder"
|
|
7
|
+
|
|
8
|
+
module A2A
|
|
9
|
+
class AgentCard
|
|
10
|
+
attr_reader :name, :description, :supported_interfaces, :provider, :version,
|
|
11
|
+
:documentation_url, :capabilities, :security_schemes, :security_requirements,
|
|
12
|
+
:default_input_modes, :default_output_modes, :skills, :signatures, :icon_url
|
|
13
|
+
|
|
14
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
15
|
+
def initialize(name:, description:, **kwargs)
|
|
16
|
+
supported_interfaces = kwargs.fetch(:supported_interfaces)
|
|
17
|
+
default_input_modes = kwargs.fetch(:default_input_modes)
|
|
18
|
+
default_output_modes = kwargs.fetch(:default_output_modes)
|
|
19
|
+
skills = kwargs.fetch(:skills)
|
|
20
|
+
|
|
21
|
+
validate_required_collections!(supported_interfaces, default_input_modes, default_output_modes, skills)
|
|
22
|
+
|
|
23
|
+
@name = name
|
|
24
|
+
@description = description
|
|
25
|
+
@supported_interfaces = supported_interfaces
|
|
26
|
+
@version = kwargs.fetch(:version)
|
|
27
|
+
@capabilities = kwargs.fetch(:capabilities)
|
|
28
|
+
@default_input_modes = default_input_modes
|
|
29
|
+
@default_output_modes = default_output_modes
|
|
30
|
+
@skills = skills
|
|
31
|
+
@provider = kwargs.fetch(:provider, nil)
|
|
32
|
+
@documentation_url = kwargs.fetch(:documentation_url, nil)
|
|
33
|
+
@security_schemes = kwargs.fetch(:security_schemes, nil)
|
|
34
|
+
@security_requirements = kwargs.fetch(:security_requirements, nil)
|
|
35
|
+
@signatures = kwargs.fetch(:signatures, nil)
|
|
36
|
+
@icon_url = kwargs.fetch(:icon_url, nil)
|
|
37
|
+
end
|
|
38
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
39
|
+
|
|
40
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
41
|
+
def self.from_h(hash)
|
|
42
|
+
new(
|
|
43
|
+
name: hash.fetch("name"),
|
|
44
|
+
description: hash.fetch("description"),
|
|
45
|
+
version: hash.fetch("version"),
|
|
46
|
+
supported_interfaces: hash.fetch("supportedInterfaces").map { AgentInterface.from_h(_1) },
|
|
47
|
+
capabilities: AgentCapabilities.from_h(hash.fetch("capabilities")),
|
|
48
|
+
skills: hash.fetch("skills").map { AgentSkill.from_h(_1) },
|
|
49
|
+
security_schemes: (hash["securitySchemes"] || {}).transform_values { SecurityScheme.from_h(_1) },
|
|
50
|
+
security_requirements: hash["security"],
|
|
51
|
+
default_input_modes: hash.fetch("defaultInputModes"),
|
|
52
|
+
default_output_modes: hash.fetch("defaultOutputModes"),
|
|
53
|
+
provider: hash["provider"] && AgentProvider.from_h(hash["provider"]),
|
|
54
|
+
documentation_url: hash["documentationUrl"],
|
|
55
|
+
icon_url: hash["iconUrl"],
|
|
56
|
+
signatures: hash["signatures"]&.map { Signature.from_h(_1) }
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
60
|
+
|
|
61
|
+
def to_h
|
|
62
|
+
required_fields.merge(optional_fields).compact
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# §8.4.1 — RFC 8785 canonical JSON: to_h with "signatures" excluded and
|
|
66
|
+
# keys sorted recursively. Used as the payload for JWS signing/verification.
|
|
67
|
+
def canonical_json
|
|
68
|
+
JSON.generate(sort_keys_recursive(to_h.except("signatures")))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# §5.2 — returns the first interface whose protocolBinding the caller supports,
|
|
72
|
+
# preserving the agent's declared preference order (index 0 = most preferred).
|
|
73
|
+
def preferred_interface(preference: [AgentInterface::JSONRPC, AgentInterface::HTTP_JSON])
|
|
74
|
+
supported_interfaces.find { |i| preference.include?(i.protocol_binding) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def validate_required_collections!(interfaces, input_modes, output_modes, skills)
|
|
80
|
+
raise ArgumentError, "supported_interfaces must contain at least one element" if Array(interfaces).empty?
|
|
81
|
+
raise ArgumentError, "skills must contain at least one element" if Array(skills).empty?
|
|
82
|
+
raise ArgumentError, "default_input_modes must contain at least one element" if Array(input_modes).empty?
|
|
83
|
+
raise ArgumentError, "default_output_modes must contain at least one element" if Array(output_modes).empty?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def required_fields
|
|
87
|
+
{
|
|
88
|
+
"name" => name,
|
|
89
|
+
"description" => description,
|
|
90
|
+
"version" => version,
|
|
91
|
+
"supportedInterfaces" => supported_interfaces.map(&:to_h),
|
|
92
|
+
"capabilities" => capabilities.to_h,
|
|
93
|
+
"skills" => skills.map(&:to_h),
|
|
94
|
+
"defaultInputModes" => default_input_modes,
|
|
95
|
+
"defaultOutputModes" => default_output_modes
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def optional_fields
|
|
100
|
+
{
|
|
101
|
+
"provider" => provider&.to_h,
|
|
102
|
+
"documentationUrl" => documentation_url,
|
|
103
|
+
"iconUrl" => icon_url,
|
|
104
|
+
"securitySchemes" => security_schemes&.transform_values(&:to_h),
|
|
105
|
+
"security" => security_requirements,
|
|
106
|
+
"signatures" => signatures&.map(&:to_h)
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def sort_keys_recursive(obj)
|
|
111
|
+
case obj
|
|
112
|
+
when Hash then obj.sort.to_h.transform_values { sort_keys_recursive(_1) }
|
|
113
|
+
when Array then obj.map { sort_keys_recursive(_1) }
|
|
114
|
+
else obj
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
class AgentExtension
|
|
5
|
+
attr_reader :uri, :description, :required, :params
|
|
6
|
+
|
|
7
|
+
def initialize(uri: nil, description: nil, required: nil, params: nil)
|
|
8
|
+
@uri = uri
|
|
9
|
+
@description = description
|
|
10
|
+
@required = required
|
|
11
|
+
@params = params
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.from_h(hash)
|
|
15
|
+
new(
|
|
16
|
+
uri: hash["uri"],
|
|
17
|
+
description: hash["description"],
|
|
18
|
+
required: hash["required"],
|
|
19
|
+
params: hash["params"]
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_h
|
|
24
|
+
{
|
|
25
|
+
"uri" => uri,
|
|
26
|
+
"description" => description,
|
|
27
|
+
"required" => required,
|
|
28
|
+
"params" => params
|
|
29
|
+
}.compact
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
class AgentInterface
|
|
5
|
+
attr_reader :url, :protocol_binding, :protocol_version, :tenant
|
|
6
|
+
|
|
7
|
+
JSONRPC = "JSONRPC"
|
|
8
|
+
GRPC = "GRPC"
|
|
9
|
+
HTTP_JSON = "HTTP+JSON"
|
|
10
|
+
|
|
11
|
+
VALID_BINDINGS = [JSONRPC, GRPC, HTTP_JSON].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(url:, protocol_binding:, protocol_version:, tenant: nil)
|
|
14
|
+
unless VALID_BINDINGS.include?(protocol_binding)
|
|
15
|
+
raise ArgumentError, "protocol_binding must be one of #{VALID_BINDINGS.join(', ')}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
@url = url
|
|
19
|
+
@protocol_binding = protocol_binding
|
|
20
|
+
@protocol_version = protocol_version
|
|
21
|
+
@tenant = tenant
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.from_h(hash)
|
|
25
|
+
new(
|
|
26
|
+
url: hash.fetch("url"),
|
|
27
|
+
protocol_binding: hash.fetch("protocolBinding"),
|
|
28
|
+
protocol_version: hash.fetch("protocolVersion"),
|
|
29
|
+
tenant: hash["tenant"]
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_h
|
|
34
|
+
{
|
|
35
|
+
"url" => url,
|
|
36
|
+
"protocolBinding" => protocol_binding,
|
|
37
|
+
"protocolVersion" => protocol_version,
|
|
38
|
+
"tenant" => tenant
|
|
39
|
+
}.compact
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def json_rpc?
|
|
43
|
+
protocol_binding == JSONRPC
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def grpc?
|
|
47
|
+
protocol_binding == GRPC
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def http_json?
|
|
51
|
+
protocol_binding == HTTP_JSON
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
class AgentProvider
|
|
5
|
+
attr_reader :organization, :url
|
|
6
|
+
|
|
7
|
+
def initialize(organization:, url:)
|
|
8
|
+
@organization = organization
|
|
9
|
+
@url = url
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.from_h(hash)
|
|
13
|
+
new(organization: hash.fetch("organization"), url: hash.fetch("url"))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_h
|
|
17
|
+
{ "organization" => organization, "url" => url }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
class AgentSkill
|
|
5
|
+
attr_reader :id, :name, :description, :tags, :examples, :input_modes, :output_modes, :security_requirements
|
|
6
|
+
|
|
7
|
+
def initialize(id:, name:, description:, tags:, **kwargs)
|
|
8
|
+
raise ArgumentError, "tags must contain at least one element" if Array(tags).empty?
|
|
9
|
+
|
|
10
|
+
@id = id
|
|
11
|
+
@name = name
|
|
12
|
+
@description = description
|
|
13
|
+
@tags = tags
|
|
14
|
+
@examples = kwargs[:examples]
|
|
15
|
+
@input_modes = kwargs[:input_modes]
|
|
16
|
+
@output_modes = kwargs[:output_modes]
|
|
17
|
+
@security_requirements = kwargs[:security_requirements]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.from_h(hash)
|
|
21
|
+
new(
|
|
22
|
+
id: hash.fetch("id"),
|
|
23
|
+
name: hash.fetch("name"),
|
|
24
|
+
description: hash.fetch("description"),
|
|
25
|
+
tags: hash.fetch("tags"),
|
|
26
|
+
examples: hash["examples"],
|
|
27
|
+
input_modes: hash["inputModes"],
|
|
28
|
+
output_modes: hash["outputModes"],
|
|
29
|
+
security_requirements: hash["securityRequirements"]&.map { SecurityRequirement.from_h(_1) }
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_h
|
|
34
|
+
{
|
|
35
|
+
"id" => id,
|
|
36
|
+
"name" => name,
|
|
37
|
+
"description" => description,
|
|
38
|
+
"tags" => tags,
|
|
39
|
+
"examples" => examples,
|
|
40
|
+
"inputModes" => input_modes,
|
|
41
|
+
"outputModes" => output_modes,
|
|
42
|
+
"securityRequirements" => security_requirements&.map(&:to_h)
|
|
43
|
+
}.compact
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/a2a/artifact.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
class Artifact
|
|
5
|
+
attr_reader :id, :name, :description, :parts, :extensions, :metadata
|
|
6
|
+
|
|
7
|
+
def initialize(id:, parts:, **kwargs)
|
|
8
|
+
raise ArgumentError, "parts must contain at least one element" if Array(parts).empty?
|
|
9
|
+
|
|
10
|
+
@id = id
|
|
11
|
+
@parts = parts
|
|
12
|
+
@name = kwargs[:name]
|
|
13
|
+
@description = kwargs[:description]
|
|
14
|
+
@extensions = kwargs[:extensions]
|
|
15
|
+
@metadata = kwargs[:metadata]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.from_h(hash)
|
|
19
|
+
new(
|
|
20
|
+
id: hash.fetch("artifactId"),
|
|
21
|
+
name: hash["name"],
|
|
22
|
+
description: hash["description"],
|
|
23
|
+
parts: Array(hash["parts"]).map { Part.from_h(_1) },
|
|
24
|
+
extensions: hash["extensions"],
|
|
25
|
+
metadata: hash["metadata"]
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_h
|
|
30
|
+
{
|
|
31
|
+
"parts" => parts.map(&:to_h),
|
|
32
|
+
"artifactId" => id,
|
|
33
|
+
"name" => name,
|
|
34
|
+
"description" => description,
|
|
35
|
+
"extensions" => extensions,
|
|
36
|
+
"metadata" => metadata
|
|
37
|
+
}.compact
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/a2a/client.rb
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
class Client
|
|
5
|
+
SUPPORTED_BINDINGS = [AgentInterface::JSONRPC, AgentInterface::HTTP_JSON].freeze
|
|
6
|
+
|
|
7
|
+
# §5.2, §8.3.2 — constructs a Client from an AgentCard by negotiating the
|
|
8
|
+
# protocol binding. The agent's supportedInterfaces order is respected:
|
|
9
|
+
# the first interface whose protocolBinding appears in `preference` wins.
|
|
10
|
+
def self.from_agent_card(card, headers: {}, extensions: [], preference: SUPPORTED_BINDINGS)
|
|
11
|
+
iface = card.preferred_interface(preference: preference)
|
|
12
|
+
|
|
13
|
+
unless iface
|
|
14
|
+
found = card.supported_interfaces.map(&:protocol_binding).join(", ")
|
|
15
|
+
raise UnsupportedOperationError, "no supported interface in agent card (available: #{found})"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
Versioning.validate!(iface.protocol_version)
|
|
19
|
+
|
|
20
|
+
protocol = build_protocol(iface, headers: headers, extensions: extensions)
|
|
21
|
+
new(protocol: protocol, capabilities: card.capabilities)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.build_protocol(iface, headers:, extensions:)
|
|
25
|
+
klass = iface.protocol_binding == AgentInterface::JSONRPC ? Protocol::JsonRpc : Protocol::HttpJson
|
|
26
|
+
klass.new(url: iface.url, version: iface.protocol_version, headers: headers, extensions: extensions)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private_class_method :build_protocol
|
|
30
|
+
|
|
31
|
+
def initialize(protocol:, capabilities: nil)
|
|
32
|
+
@protocol = protocol
|
|
33
|
+
@capabilities = capabilities
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def send_message(message, configuration: {}, metadata: nil, tenant: nil)
|
|
37
|
+
run Operation::SendMessage.new(message, configuration: configuration, metadata: metadata, tenant: tenant)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def send_streaming_message(message, configuration: {}, metadata: nil, tenant: nil, &block)
|
|
41
|
+
op = Operation::SendStreamingMessage.new(message, configuration: configuration, metadata: metadata, tenant: tenant)
|
|
42
|
+
run op, &block
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def get_task(id, history_length: nil, tenant: nil)
|
|
46
|
+
run Operation::GetTask.new(id:, history_length:, tenant:)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def list_tasks(**kwargs)
|
|
50
|
+
run Operation::ListTasks.new(**kwargs)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def cancel_task(id, metadata: nil, tenant: nil)
|
|
54
|
+
if id.is_a?(Task) && id.terminal?
|
|
55
|
+
raise TaskNotCancelableError, "task #{id.id} is already in a terminal state (#{id.status.state})"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
task_id = id.is_a?(Task) ? id.id : id
|
|
59
|
+
run Operation::CancelTask.new(id: task_id, metadata:, tenant:)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def subscribe_to_task(id, tenant: nil, &block)
|
|
63
|
+
run Operation::SubscribeToTask.new(id:, tenant:), &block
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def create_task_push_notification_config(config, tenant: nil)
|
|
67
|
+
require_push_notifications!
|
|
68
|
+
run Operation::CreateTaskPushNotificationConfig.new(config, tenant:)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def get_task_push_notification_config(task_id:, id:, tenant: nil)
|
|
72
|
+
require_push_notifications!
|
|
73
|
+
run Operation::GetTaskPushNotificationConfig.new(task_id:, id:, tenant:)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def list_task_push_notification_configs(task_id:, page_size: nil, page_token: nil, tenant: nil)
|
|
77
|
+
require_push_notifications!
|
|
78
|
+
run Operation::ListTaskPushNotificationConfigs.new(task_id:, page_size:, page_token:, tenant:)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def delete_task_push_notification_config(task_id:, id:, tenant: nil)
|
|
82
|
+
require_push_notifications!
|
|
83
|
+
run Operation::DeleteTaskPushNotificationConfig.new(task_id:, id:, tenant:)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def get_extended_agent_card(tenant: nil)
|
|
87
|
+
require_extended_agent_card!
|
|
88
|
+
run Operation::GetExtendedAgentCard.new(tenant:)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def require_push_notifications!
|
|
94
|
+
return unless @capabilities&.push_notifications == false
|
|
95
|
+
|
|
96
|
+
raise PushNotificationNotSupportedError, "agent does not support push notifications"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def require_extended_agent_card!
|
|
100
|
+
return unless @capabilities&.extended_agent_card == false
|
|
101
|
+
|
|
102
|
+
raise ExtendedAgentCardNotConfiguredError, "agent does not support extended agent card"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def run(operation, &)
|
|
106
|
+
operation.execute(@protocol, &)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module A2A
|
|
8
|
+
module Discovery
|
|
9
|
+
WELL_KNOWN_PATH = "/.well-known/agent-card.json"
|
|
10
|
+
|
|
11
|
+
# §8.2, §14.3 — fetches the public AgentCard from /.well-known/a2a.
|
|
12
|
+
# No authentication required.
|
|
13
|
+
def self.fetch(base_url, transport: Transport.new)
|
|
14
|
+
url = "#{base_url.chomp('/')}#{WELL_KNOWN_PATH}"
|
|
15
|
+
AgentCard.from_h(transport.get(url))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# §8.6.2 — fetches the public card, then calls GetExtendedAgentCard.
|
|
19
|
+
# The public card must declare capabilities.extendedAgentCard: true.
|
|
20
|
+
def self.fetch_extended(base_url, headers: {}, extensions: [], transport: Transport.new)
|
|
21
|
+
card = fetch(base_url, transport: transport)
|
|
22
|
+
Client.from_agent_card(card, headers: headers, extensions: extensions)
|
|
23
|
+
.get_extended_agent_card
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class Transport
|
|
27
|
+
def get(url)
|
|
28
|
+
uri = URI.parse(url)
|
|
29
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
30
|
+
http.use_ssl = uri.scheme == "https"
|
|
31
|
+
response = http.request(Net::HTTP::Get.new(uri.request_uri,
|
|
32
|
+
"Accept" => "application/json"))
|
|
33
|
+
handle(response)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def handle(response)
|
|
39
|
+
case response.code.to_i
|
|
40
|
+
when 200..299 then JSON.parse(response.body)
|
|
41
|
+
when 401 then raise AuthenticationError, "HTTP 401"
|
|
42
|
+
when 403 then raise AuthorizationError, "HTTP 403"
|
|
43
|
+
when 404 then raise TransportError, "agent card not found (HTTP 404)"
|
|
44
|
+
else raise TransportError, "HTTP #{response.code}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
module JSONRPCEnvelope
|
|
5
|
+
# Builds a success response envelope.
|
|
6
|
+
def self.success(id:, result:)
|
|
7
|
+
{ "jsonrpc" => "2.0", "id" => id, "result" => result }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Builds an error response envelope from an A2A::Error or any StandardError.
|
|
11
|
+
# If the error carries a numeric code it is used directly; otherwise falls
|
|
12
|
+
# back to -32603 (InternalError).
|
|
13
|
+
def self.error(id:, error:)
|
|
14
|
+
code = error.respond_to?(:code) && error.code ? error.code : -32603
|
|
15
|
+
{
|
|
16
|
+
"jsonrpc" => "2.0",
|
|
17
|
+
"id" => id,
|
|
18
|
+
"error" => { "code" => code, "message" => error.message }
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Parses the JSON-RPC id and method from a raw request hash.
|
|
23
|
+
def self.parse_request(hash)
|
|
24
|
+
id = hash["id"]
|
|
25
|
+
method = hash["method"]
|
|
26
|
+
params = hash["params"] || {}
|
|
27
|
+
raise InvalidRequestError, "missing method" unless method
|
|
28
|
+
|
|
29
|
+
[id, method, params]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/a2a/message.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
class Message
|
|
5
|
+
attr_reader :id, :role, :parts,
|
|
6
|
+
:context_id, :task_id,
|
|
7
|
+
:reference_task_ids, :extensions, :metadata
|
|
8
|
+
|
|
9
|
+
def initialize(id:, role:, parts:, **kwargs)
|
|
10
|
+
raise ArgumentError, "invalid role: #{role.inspect}" unless Role.valid?(role)
|
|
11
|
+
raise ArgumentError, "parts must contain at least one element" if Array(parts).empty?
|
|
12
|
+
|
|
13
|
+
@id = id
|
|
14
|
+
@role = role
|
|
15
|
+
@parts = parts
|
|
16
|
+
@context_id = kwargs[:context_id]
|
|
17
|
+
@task_id = kwargs[:task_id]
|
|
18
|
+
@reference_task_ids = kwargs[:reference_task_ids]
|
|
19
|
+
@extensions = kwargs[:extensions]
|
|
20
|
+
@metadata = kwargs[:metadata]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.from_h(hash)
|
|
24
|
+
new(
|
|
25
|
+
id: hash.fetch("messageId"),
|
|
26
|
+
role: hash.fetch("role"),
|
|
27
|
+
parts: Array(hash["parts"]).map { Part.from_h(_1) },
|
|
28
|
+
context_id: hash["contextId"],
|
|
29
|
+
task_id: hash["taskId"],
|
|
30
|
+
reference_task_ids: hash["referenceTaskIds"],
|
|
31
|
+
extensions: hash["extensions"],
|
|
32
|
+
metadata: hash["metadata"]
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_h
|
|
37
|
+
{
|
|
38
|
+
"messageId" => id,
|
|
39
|
+
"role" => role,
|
|
40
|
+
"parts" => parts.map(&:to_h),
|
|
41
|
+
"contextId" => context_id,
|
|
42
|
+
"taskId" => task_id,
|
|
43
|
+
"metadata" => metadata,
|
|
44
|
+
"referenceTaskIds" => reference_task_ids,
|
|
45
|
+
"extensions" => extensions
|
|
46
|
+
}.compact
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns the plain-text content of the first Parts::Text, or nil.
|
|
50
|
+
def text
|
|
51
|
+
parts.find { |p| p.is_a?(Part::Text) }&.text
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
module OAuthFlow
|
|
5
|
+
class AuthorizationCode
|
|
6
|
+
attr_reader :authorization_url, :token_url, :refresh_url, :scopes, :pkce_required
|
|
7
|
+
|
|
8
|
+
def initialize(authorization_url:, token_url:, scopes:, refresh_url: nil, pkce_required: nil)
|
|
9
|
+
@authorization_url = authorization_url
|
|
10
|
+
@token_url = token_url
|
|
11
|
+
@scopes = scopes
|
|
12
|
+
@refresh_url = refresh_url
|
|
13
|
+
@pkce_required = pkce_required
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.from_h(hash)
|
|
17
|
+
new(
|
|
18
|
+
authorization_url: hash.fetch("authorizationUrl"),
|
|
19
|
+
scopes: hash.fetch("scopes"),
|
|
20
|
+
token_url: hash.fetch("tokenUrl"),
|
|
21
|
+
refresh_url: hash["refreshUrl"],
|
|
22
|
+
pkce_required: hash["pkceRequired"]
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_h
|
|
27
|
+
{
|
|
28
|
+
"authorizationUrl" => authorization_url,
|
|
29
|
+
"tokenUrl" => token_url,
|
|
30
|
+
"scopes" => scopes,
|
|
31
|
+
"refreshUrl" => refresh_url,
|
|
32
|
+
"pkceRequired" => pkce_required
|
|
33
|
+
}.compact
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
module OAuthFlow
|
|
5
|
+
class ClientCredentials
|
|
6
|
+
attr_reader :token_url, :refresh_url, :scopes
|
|
7
|
+
|
|
8
|
+
def initialize(token_url:, scopes:, refresh_url: nil)
|
|
9
|
+
@token_url = token_url
|
|
10
|
+
@scopes = scopes
|
|
11
|
+
@refresh_url = refresh_url
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.from_h(hash)
|
|
15
|
+
new(
|
|
16
|
+
scopes: hash.fetch("scopes"),
|
|
17
|
+
token_url: hash.fetch("tokenUrl"),
|
|
18
|
+
refresh_url: hash["refreshUrl"]
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_h
|
|
23
|
+
{
|
|
24
|
+
"tokenUrl" => token_url,
|
|
25
|
+
"scopes" => scopes,
|
|
26
|
+
"refreshUrl" => refresh_url
|
|
27
|
+
}.compact
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
module OAuthFlow
|
|
5
|
+
class DeviceCode
|
|
6
|
+
attr_reader :device_authorization_url, :token_url, :refresh_url, :scopes
|
|
7
|
+
|
|
8
|
+
def initialize(device_authorization_url:, token_url:, scopes:, refresh_url: nil)
|
|
9
|
+
@device_authorization_url = device_authorization_url
|
|
10
|
+
@token_url = token_url
|
|
11
|
+
@scopes = scopes
|
|
12
|
+
@refresh_url = refresh_url
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.from_h(hash)
|
|
16
|
+
new(
|
|
17
|
+
device_authorization_url: hash.fetch("deviceAuthorizationUrl"),
|
|
18
|
+
scopes: hash.fetch("scopes"),
|
|
19
|
+
token_url: hash.fetch("tokenUrl"),
|
|
20
|
+
refresh_url: hash["refreshUrl"]
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
{
|
|
26
|
+
"deviceAuthorizationUrl" => device_authorization_url,
|
|
27
|
+
"tokenUrl" => token_url,
|
|
28
|
+
"scopes" => scopes,
|
|
29
|
+
"refreshUrl" => refresh_url
|
|
30
|
+
}.compact
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|