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.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +38 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +311 -0
  5. data/lib/a2a/agent_capabilities.rb +32 -0
  6. data/lib/a2a/agent_card/builder.rb +135 -0
  7. data/lib/a2a/agent_card/signature.rb +35 -0
  8. data/lib/a2a/agent_card/verifier.rb +19 -0
  9. data/lib/a2a/agent_card.rb +118 -0
  10. data/lib/a2a/agent_extension.rb +32 -0
  11. data/lib/a2a/agent_interface.rb +54 -0
  12. data/lib/a2a/agent_provider.rb +20 -0
  13. data/lib/a2a/agent_skill.rb +46 -0
  14. data/lib/a2a/artifact.rb +40 -0
  15. data/lib/a2a/client.rb +109 -0
  16. data/lib/a2a/discovery.rb +49 -0
  17. data/lib/a2a/json_rpc_envelope.rb +32 -0
  18. data/lib/a2a/message.rb +54 -0
  19. data/lib/a2a/oauth_flow/authorization_code.rb +37 -0
  20. data/lib/a2a/oauth_flow/client_credentials.rb +31 -0
  21. data/lib/a2a/oauth_flow/device_code.rb +34 -0
  22. data/lib/a2a/oauth_flow.rb +37 -0
  23. data/lib/a2a/operation/cancel_task.rb +39 -0
  24. data/lib/a2a/operation/create_task_push_notification_config.rb +38 -0
  25. data/lib/a2a/operation/delete_task_push_notification_config.rb +38 -0
  26. data/lib/a2a/operation/executable.rb +27 -0
  27. data/lib/a2a/operation/get_extended_agent_card.rb +32 -0
  28. data/lib/a2a/operation/get_task.rb +39 -0
  29. data/lib/a2a/operation/get_task_push_notification_config.rb +38 -0
  30. data/lib/a2a/operation/list_task_push_notification_configs.rb +64 -0
  31. data/lib/a2a/operation/list_tasks.rb +78 -0
  32. data/lib/a2a/operation/send_message/configuration.rb +39 -0
  33. data/lib/a2a/operation/send_message.rb +58 -0
  34. data/lib/a2a/operation/send_message_request.rb +37 -0
  35. data/lib/a2a/operation/send_streaming_message.rb +53 -0
  36. data/lib/a2a/operation/subscribe_to_task.rb +40 -0
  37. data/lib/a2a/operation.rb +19 -0
  38. data/lib/a2a/part/data.rb +34 -0
  39. data/lib/a2a/part/file.rb +45 -0
  40. data/lib/a2a/part/text.rb +34 -0
  41. data/lib/a2a/part.rb +21 -0
  42. data/lib/a2a/protocol/http_json/transport.rb +82 -0
  43. data/lib/a2a/protocol/http_json.rb +53 -0
  44. data/lib/a2a/protocol/json_rpc/transport.rb +54 -0
  45. data/lib/a2a/protocol/json_rpc.rb +55 -0
  46. data/lib/a2a/push_notification/authentication_info.rb +29 -0
  47. data/lib/a2a/push_notification/config.rb +40 -0
  48. data/lib/a2a/push_notification/dispatcher.rb +52 -0
  49. data/lib/a2a/push_notification/receiver.rb +54 -0
  50. data/lib/a2a/push_notification.rb +11 -0
  51. data/lib/a2a/role.rb +13 -0
  52. data/lib/a2a/security_requirement.rb +19 -0
  53. data/lib/a2a/security_scheme/api_key.rb +33 -0
  54. data/lib/a2a/security_scheme/http_auth.rb +33 -0
  55. data/lib/a2a/security_scheme/mutual_tls.rb +25 -0
  56. data/lib/a2a/security_scheme/oauth2.rb +52 -0
  57. data/lib/a2a/security_scheme/open_id_connect.rb +30 -0
  58. data/lib/a2a/security_scheme.rb +26 -0
  59. data/lib/a2a/streaming/artifact_update_event.rb +40 -0
  60. data/lib/a2a/streaming/response.rb +65 -0
  61. data/lib/a2a/streaming/sse_parser.rb +43 -0
  62. data/lib/a2a/streaming/sse_writer.rb +25 -0
  63. data/lib/a2a/streaming/status_update_event.rb +43 -0
  64. data/lib/a2a/streaming/subscription.rb +56 -0
  65. data/lib/a2a/streaming.rb +12 -0
  66. data/lib/a2a/task/state.rb +31 -0
  67. data/lib/a2a/task/status.rb +35 -0
  68. data/lib/a2a/task.rb +66 -0
  69. data/lib/a2a/version.rb +6 -0
  70. data/lib/a2a/versioning.rb +28 -0
  71. data/lib/a2a.rb +90 -0
  72. 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
@@ -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
@@ -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