agent-client-protocol 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentClientProtocol
4
+ module ErrorCode
5
+ PARSE_ERROR = -32_700
6
+ INVALID_REQUEST = -32_600
7
+ METHOD_NOT_FOUND = -32_601
8
+ INVALID_PARAMS = -32_602
9
+ INTERNAL_ERROR = -32_603
10
+ REQUEST_CANCELLED = -32_800
11
+ AUTH_REQUIRED = -32_000
12
+ RESOURCE_NOT_FOUND = -32_002
13
+
14
+ DEFAULT_MESSAGES = {
15
+ PARSE_ERROR => "Parse error",
16
+ INVALID_REQUEST => "Invalid request",
17
+ METHOD_NOT_FOUND => "Method not found",
18
+ INVALID_PARAMS => "Invalid params",
19
+ INTERNAL_ERROR => "Internal error",
20
+ REQUEST_CANCELLED => "Request cancelled",
21
+ AUTH_REQUIRED => "Authentication required",
22
+ RESOURCE_NOT_FOUND => "Resource not found"
23
+ }.freeze
24
+
25
+ module_function
26
+
27
+ def default_message(code)
28
+ DEFAULT_MESSAGES.fetch(code, "Unknown error")
29
+ end
30
+ end
31
+
32
+ class Error < StandardError
33
+ attr_reader :code, :data
34
+
35
+ def initialize(code:, message: nil, data: nil)
36
+ @code = Integer(code)
37
+ @data = data
38
+ super(message || ErrorCode.default_message(@code))
39
+ end
40
+
41
+ def to_h
42
+ error = {
43
+ "code" => code,
44
+ "message" => message
45
+ }
46
+ error["data"] = data unless data.nil?
47
+ error
48
+ end
49
+
50
+ class << self
51
+ def from_h(hash)
52
+ normalized = stringify_keys(hash)
53
+ new(
54
+ code: normalized.fetch("code"),
55
+ message: normalized["message"],
56
+ data: normalized["data"]
57
+ )
58
+ end
59
+
60
+ def parse_error(data = nil)
61
+ new(code: ErrorCode::PARSE_ERROR, data: data)
62
+ end
63
+
64
+ def invalid_request(data = nil)
65
+ new(code: ErrorCode::INVALID_REQUEST, data: data)
66
+ end
67
+
68
+ def method_not_found(data = nil)
69
+ new(code: ErrorCode::METHOD_NOT_FOUND, data: data)
70
+ end
71
+
72
+ def invalid_params(data = nil)
73
+ new(code: ErrorCode::INVALID_PARAMS, data: data)
74
+ end
75
+
76
+ def internal_error(data = nil)
77
+ new(code: ErrorCode::INTERNAL_ERROR, data: data)
78
+ end
79
+
80
+ def request_cancelled(data = nil)
81
+ new(code: ErrorCode::REQUEST_CANCELLED, data: data)
82
+ end
83
+
84
+ def auth_required(data = nil)
85
+ new(code: ErrorCode::AUTH_REQUIRED, data: data)
86
+ end
87
+
88
+ def resource_not_found(uri = nil)
89
+ payload = uri.nil? ? nil : { "uri" => uri }
90
+ new(code: ErrorCode::RESOURCE_NOT_FOUND, data: payload)
91
+ end
92
+
93
+ private
94
+
95
+ def stringify_keys(value)
96
+ return value unless value.is_a?(Hash)
97
+
98
+ value.each_with_object({}) do |(k, v), acc|
99
+ acc[k.to_s] = v
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentClientProtocol
4
+ module Methods
5
+ module_function
6
+
7
+ def agent(unstable: false)
8
+ SchemaRegistry.agent_methods(unstable: unstable)
9
+ end
10
+
11
+ def client(unstable: false)
12
+ SchemaRegistry.client_methods(unstable: unstable)
13
+ end
14
+
15
+ def protocol(unstable: false)
16
+ SchemaRegistry.protocol_methods(unstable: unstable)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentClientProtocol
4
+ class ProtocolVersion
5
+ include Comparable
6
+
7
+ V0 = 0
8
+ V1 = 1
9
+ LATEST = V1
10
+ MAX_VALUE = 65_535
11
+
12
+ attr_reader :value
13
+
14
+ def initialize(value)
15
+ unless value.is_a?(Integer) && value >= 0 && value <= MAX_VALUE
16
+ raise ArgumentError, "protocol version must be an Integer between 0 and #{MAX_VALUE}"
17
+ end
18
+
19
+ @value = value
20
+ end
21
+
22
+ def self.parse(raw)
23
+ case raw
24
+ when Integer
25
+ new(raw)
26
+ when String
27
+ # ACP legacy behavior: old string versions map to protocol version 0.
28
+ new(V0)
29
+ else
30
+ raise ArgumentError, "protocol version must be an Integer or String"
31
+ end
32
+ end
33
+
34
+ def <=>(other)
35
+ other_value = other.is_a?(ProtocolVersion) ? other.value : other
36
+ value <=> other_value
37
+ end
38
+
39
+ def ==(other)
40
+ other_value = other.is_a?(ProtocolVersion) ? other.value : other
41
+ value == other_value
42
+ end
43
+
44
+ def to_i
45
+ value
46
+ end
47
+
48
+ def to_h
49
+ value
50
+ end
51
+
52
+ def to_s
53
+ value.to_s
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module AgentClientProtocol
6
+ module RPC
7
+ JSONRPC_VERSION = "2.0"
8
+
9
+ module_function
10
+
11
+ def parse(message)
12
+ normalized = normalize_input(message)
13
+
14
+ if normalized.key?("jsonrpc") && normalized["jsonrpc"] != JSONRPC_VERSION
15
+ raise Error.invalid_request("unsupported jsonrpc version: #{normalized['jsonrpc']}")
16
+ end
17
+
18
+ if normalized.key?("method")
19
+ return Request.new(
20
+ id: normalized["id"],
21
+ method: normalized["method"],
22
+ params: normalized["params"]
23
+ ) if normalized.key?("id")
24
+
25
+ return Notification.new(method: normalized["method"], params: normalized["params"])
26
+ end
27
+
28
+ if normalized.key?("result") || normalized.key?("error")
29
+ response_kwargs = { id: normalized["id"] }
30
+ response_kwargs[:result] = normalized["result"] if normalized.key?("result")
31
+ response_kwargs[:error] = normalized["error"] if normalized.key?("error")
32
+ return Response.new(**response_kwargs)
33
+ end
34
+
35
+ raise Error.invalid_request("message is neither request, response, nor notification")
36
+ end
37
+
38
+ def parse_json(json)
39
+ parse(JSON.parse(json))
40
+ rescue JSON::ParserError => e
41
+ raise Error.parse_error(e.message)
42
+ end
43
+
44
+ def normalize_input(message)
45
+ case message
46
+ when String
47
+ JSON.parse(message)
48
+ when Hash
49
+ deep_stringify_keys(message)
50
+ else
51
+ raise ArgumentError, "message must be a Hash or JSON String"
52
+ end
53
+ end
54
+
55
+ def deep_stringify_keys(value)
56
+ case value
57
+ when Hash
58
+ value.each_with_object({}) do |(k, v), result|
59
+ result[k.to_s] = deep_stringify_keys(v)
60
+ end
61
+ when Array
62
+ value.map { |item| deep_stringify_keys(item) }
63
+ else
64
+ value
65
+ end
66
+ end
67
+
68
+ class Request
69
+ attr_reader :id, :method, :params
70
+
71
+ def initialize(id:, method:, params: nil)
72
+ @id = RequestId.coerce(id)
73
+ @method = String(method)
74
+ @params = params
75
+ end
76
+
77
+ def to_h(include_jsonrpc: true)
78
+ payload = {
79
+ "id" => id,
80
+ "method" => method
81
+ }
82
+ payload["params"] = params unless params.nil?
83
+ include_jsonrpc ? { "jsonrpc" => JSONRPC_VERSION }.merge(payload) : payload
84
+ end
85
+ end
86
+
87
+ class Notification
88
+ attr_reader :method, :params
89
+
90
+ def initialize(method:, params: nil)
91
+ @method = String(method)
92
+ @params = params
93
+ end
94
+
95
+ def to_h(include_jsonrpc: true)
96
+ payload = { "method" => method }
97
+ payload["params"] = params unless params.nil?
98
+ include_jsonrpc ? { "jsonrpc" => JSONRPC_VERSION }.merge(payload) : payload
99
+ end
100
+ end
101
+
102
+ class Response
103
+ attr_reader :id, :result, :error
104
+
105
+ def initialize(id:, result: :__undefined__, error: :__undefined__)
106
+ @id = RequestId.coerce(id)
107
+ has_result = result != :__undefined__
108
+ has_error = error != :__undefined__
109
+
110
+ if has_result == has_error
111
+ raise ArgumentError, "response must have exactly one of result or error"
112
+ end
113
+
114
+ @result = has_result ? result : nil
115
+ @error = normalize_error(error) if has_error
116
+ end
117
+
118
+ def success?
119
+ error.nil?
120
+ end
121
+
122
+ def failure?
123
+ !success?
124
+ end
125
+
126
+ def to_h(include_jsonrpc: true)
127
+ payload = { "id" => id }
128
+ if success?
129
+ payload["result"] = result
130
+ else
131
+ payload["error"] = error.is_a?(Error) ? error.to_h : error
132
+ end
133
+
134
+ include_jsonrpc ? { "jsonrpc" => JSONRPC_VERSION }.merge(payload) : payload
135
+ end
136
+
137
+ private
138
+
139
+ def normalize_error(error)
140
+ return error if error.is_a?(Error)
141
+ return Error.from_h(error) if error.is_a?(Hash)
142
+
143
+ raise ArgumentError, "error must be an AgentClientProtocol::Error or a Hash"
144
+ end
145
+ end
146
+
147
+ module RequestId
148
+ module_function
149
+
150
+ def coerce(value)
151
+ case value
152
+ when nil, String, Integer
153
+ value
154
+ else
155
+ raise ArgumentError, "request id must be nil, String, or Integer"
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module AgentClientProtocol
6
+ class SchemaRegistry
7
+ ROOT = File.expand_path("../..", __dir__)
8
+ STABLE_SCHEMA_PATH = File.join(ROOT, "schema", "schema.json")
9
+ UNSTABLE_SCHEMA_PATH = File.join(ROOT, "schema", "schema.unstable.json")
10
+ STABLE_META_PATH = File.join(ROOT, "schema", "meta.json")
11
+ UNSTABLE_META_PATH = File.join(ROOT, "schema", "meta.unstable.json")
12
+
13
+ class << self
14
+ def stable_schema
15
+ @stable_schema ||= load_json(STABLE_SCHEMA_PATH)
16
+ end
17
+
18
+ def unstable_schema
19
+ @unstable_schema ||= load_json(UNSTABLE_SCHEMA_PATH)
20
+ end
21
+
22
+ def stable_meta
23
+ @stable_meta ||= symbolize_keys(load_json(STABLE_META_PATH))
24
+ end
25
+
26
+ def unstable_meta
27
+ @unstable_meta ||= symbolize_keys(load_json(UNSTABLE_META_PATH))
28
+ end
29
+
30
+ def defs(unstable: false)
31
+ schema = unstable ? unstable_schema : stable_schema
32
+ schema.fetch("$defs")
33
+ end
34
+
35
+ def method_catalog(unstable: false)
36
+ key = unstable ? :unstable : :stable
37
+ @method_catalog ||= {}
38
+ @method_catalog[key] ||= build_method_catalog(defs(unstable: unstable))
39
+ end
40
+
41
+ def agent_methods(unstable: false)
42
+ meta = unstable ? unstable_meta : stable_meta
43
+ (meta[:agentMethods] || {}).dup
44
+ end
45
+
46
+ def client_methods(unstable: false)
47
+ meta = unstable ? unstable_meta : stable_meta
48
+ (meta[:clientMethods] || {}).dup
49
+ end
50
+
51
+ def protocol_methods(unstable: false)
52
+ meta = unstable ? unstable_meta : stable_meta
53
+ (meta[:protocolMethods] || {}).dup
54
+ end
55
+
56
+ def schema_for(definition_name, unstable: false)
57
+ defs(unstable: unstable).fetch(definition_name)
58
+ end
59
+
60
+ private
61
+
62
+ def load_json(path)
63
+ JSON.parse(File.read(path))
64
+ end
65
+
66
+ def build_method_catalog(definitions)
67
+ catalog = Hash.new { |h, side| h[side] = {} }
68
+
69
+ definitions.each do |definition_name, schema|
70
+ side = schema["x-side"]
71
+ method = schema["x-method"]
72
+ next if side.nil? || method.nil?
73
+
74
+ kind = infer_kind(definition_name)
75
+ next if kind.nil?
76
+
77
+ side_key = side.to_sym
78
+ method_entry = catalog[side_key][method] ||= {}
79
+ method_entry[kind] = definition_name
80
+ end
81
+
82
+ catalog.each_value do |methods|
83
+ methods.each_value(&:freeze)
84
+ methods.freeze
85
+ end
86
+
87
+ catalog.freeze
88
+ end
89
+
90
+ def infer_kind(definition_name)
91
+ return :request if definition_name.end_with?("Request")
92
+ return :response if definition_name.end_with?("Response")
93
+ return :notification if definition_name.end_with?("Notification")
94
+
95
+ nil
96
+ end
97
+
98
+ def symbolize_keys(value)
99
+ case value
100
+ when Hash
101
+ value.each_with_object({}) do |(k, v), result|
102
+ result[k.to_sym] = symbolize_keys(v)
103
+ end
104
+ when Array
105
+ value.map { |item| symbolize_keys(item) }
106
+ else
107
+ value
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentClientProtocol
4
+ module Types
5
+ module Unstable
6
+ end
7
+ end
8
+
9
+ class TypeRegistry
10
+ @mutex = Mutex.new
11
+ @loaded = {}
12
+
13
+ class << self
14
+ def fetch(definition_name, unstable: false)
15
+ ensure_loaded!(unstable: unstable)
16
+ namespace = unstable ? Types::Unstable : Types
17
+ return nil unless namespace.const_defined?(definition_name, false)
18
+
19
+ namespace.const_get(definition_name, false)
20
+ end
21
+
22
+ def all(unstable: false)
23
+ ensure_loaded!(unstable: unstable)
24
+ namespace = unstable ? Types::Unstable : Types
25
+
26
+ namespace.constants(false).sort.each_with_object({}) do |const_name, acc|
27
+ klass = namespace.const_get(const_name, false)
28
+ next unless klass.is_a?(Class)
29
+ next unless klass.respond_to?(:definition_name)
30
+ next if klass.definition_name.nil?
31
+
32
+ acc[const_name.to_s] = klass
33
+ end
34
+ end
35
+
36
+ def build(definition_name, payload, unstable: false)
37
+ klass = fetch(definition_name, unstable: unstable)
38
+ return payload if klass.nil?
39
+
40
+ klass.coerce(payload)
41
+ end
42
+
43
+ private
44
+
45
+ def ensure_loaded!(unstable: false)
46
+ key = unstable ? :unstable : :stable
47
+ return if @loaded[key]
48
+
49
+ @mutex.synchronize do
50
+ return if @loaded[key]
51
+
52
+ namespace = unstable ? Types::Unstable : Types
53
+ definitions = SchemaRegistry.defs(unstable: unstable)
54
+ build_types!(namespace, definitions, unstable: unstable)
55
+
56
+ @loaded[key] = true
57
+ end
58
+ end
59
+
60
+ def build_types!(namespace, definitions, unstable:)
61
+ definitions.each do |definition_name, schema|
62
+ next if namespace.const_defined?(definition_name, false)
63
+
64
+ parent = object_schema?(schema) ? Types::Base : Types::Scalar
65
+ klass = Class.new(parent)
66
+ if parent == Types::Scalar
67
+ klass.configure(
68
+ definition_name: definition_name,
69
+ schema: schema,
70
+ unstable: unstable,
71
+ coercer: scalar_coercer_for(definition_name)
72
+ )
73
+ else
74
+ klass.configure(
75
+ definition_name: definition_name,
76
+ schema: schema,
77
+ unstable: unstable
78
+ )
79
+ end
80
+
81
+ namespace.const_set(definition_name, klass)
82
+ end
83
+ end
84
+
85
+ def object_schema?(schema)
86
+ schema["type"] == "object" || schema.key?("properties")
87
+ end
88
+
89
+ def scalar_coercer_for(definition_name)
90
+ case definition_name
91
+ when "ProtocolVersion"
92
+ lambda { |value| ::AgentClientProtocol::ProtocolVersion.parse(value).to_i }
93
+ else
94
+ nil
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end