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,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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module A2A
4
+ VERSION = "0.1.1"
5
+ SPEC_VERSION = "1.0"
6
+ end
@@ -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