acp-sdk 0.10.0 → 0.12.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +19 -5
- data/lib/acp/agent_connection/client.rb +7 -19
- data/lib/acp/agent_connection/optional_method/spec.rb +42 -0
- data/lib/acp/agent_connection/optional_method.rb +36 -0
- data/lib/acp/agent_connection.rb +17 -40
- data/lib/acp/agent_process.rb +0 -13
- data/lib/acp/client_connection/streams.rb +183 -0
- data/lib/acp/client_connection.rb +227 -384
- data/lib/acp/protocol.rb +33 -0
- data/lib/acp/request_error.rb +17 -17
- data/lib/acp/transport/cancellation.rb +0 -7
- data/lib/acp/transport/reply.rb +0 -2
- data/lib/acp/transport/stderr_logger.rb +1 -2
- data/lib/acp/transport/stdio/serving.rb +18 -0
- data/lib/acp/transport/stdio.rb +67 -59
- data/lib/acp/types/audio_content.rb +3 -46
- data/lib/acp/types/auth_method_terminal.rb +3 -59
- data/lib/acp/types/available_commands_update.rb +3 -32
- data/lib/acp/types/boolean_property_schema.rb +3 -41
- data/lib/acp/types/check.rb +210 -0
- data/lib/acp/types/config_option_update.rb +3 -32
- data/lib/acp/types/content.rb +3 -29
- data/lib/acp/types/{elicitation_url_mode → create_elicitation_request/mode/form}/scope.rb +1 -1
- data/lib/acp/types/create_elicitation_request/mode/form.rb +3 -3
- data/lib/acp/types/{elicitation_form_mode → create_elicitation_request/mode/url}/scope.rb +1 -1
- data/lib/acp/types/create_elicitation_request/mode/url.rb +3 -3
- data/lib/acp/types/current_mode_update.rb +3 -29
- data/lib/acp/types/diff.rb +3 -41
- data/lib/acp/types/elicitation_accept_action.rb +3 -26
- data/lib/acp/types/elicitation_form_mode.rb +3 -33
- data/lib/acp/types/elicitation_url_mode.rb +3 -35
- data/lib/acp/types/embedded_resource.rb +3 -44
- data/lib/acp/types/image_content.rb +3 -58
- data/lib/acp/types/integer_property_schema.rb +3 -60
- data/lib/acp/types/mcp_server_http.rb +3 -44
- data/lib/acp/types/mcp_server_sse.rb +3 -44
- data/lib/acp/types/multi_select_property_schema.rb +3 -70
- data/lib/acp/types/number_property_schema.rb +3 -60
- data/lib/acp/types/parse_error.rb +37 -0
- data/lib/acp/types/plan.rb +3 -29
- data/lib/acp/types/resource_link.rb +3 -79
- data/lib/acp/types/selected_permission_outcome.rb +3 -26
- data/lib/acp/types/session_config_boolean.rb +3 -21
- data/lib/acp/types/session_config_select.rb +3 -33
- data/lib/acp/types/session_info_update.rb +3 -42
- data/lib/acp/types/string_multi_select_items.rb +3 -29
- data/lib/acp/types/string_property_schema.rb +3 -105
- data/lib/acp/types/terminal.rb +3 -29
- data/lib/acp/types/text_content.rb +3 -40
- data/lib/acp/types/tool_call.rb +3 -115
- data/lib/acp/types/unstable/compaction_summary_chunk.rb +3 -35
- data/lib/acp/types/unstable/compaction_update.rb +3 -56
- data/lib/acp/types/unstable/idle_state_update.rb +3 -40
- data/lib/acp/types/unstable/mcp_server_acp.rb +3 -35
- data/lib/acp/types/unstable/nes_edit_suggestion.rb +3 -61
- data/lib/acp/types/unstable/nes_jump_suggestion.rb +3 -45
- data/lib/acp/types/unstable/nes_rename_suggestion.rb +3 -51
- data/lib/acp/types/unstable/nes_search_and_replace_suggestion.rb +3 -60
- data/lib/acp/types/unstable/notice.rb +3 -41
- data/lib/acp/types/unstable/plan_file.rb +3 -35
- data/lib/acp/types/unstable/plan_items.rb +3 -38
- data/lib/acp/types/unstable/plan_markdown.rb +3 -35
- data/lib/acp/types/unstable/plan_removed.rb +3 -26
- data/lib/acp/types/unstable/plan_update.rb +3 -33
- data/lib/acp/types/unstable/requires_action_state_update.rb +3 -21
- data/lib/acp/types/unstable/running_state_update.rb +3 -21
- data/lib/acp/types/unstable/session_message.rb +3 -56
- data/lib/acp/types/unstable/session_message_chunk.rb +3 -53
- data/lib/acp/types/unstable/subagent_update.rb +3 -65
- data/lib/acp/types/unstable/unknown_state_update.rb +3 -21
- data/lib/acp/types/unstable.rb +20 -20
- data/lib/acp/types/usage_update.rb +3 -41
- data/lib/acp/types.rb +7 -246
- data/lib/acp/version.rb +1 -1
- data/sig/generated/acp/agent_connection/client.rbs +10 -19
- data/sig/generated/acp/agent_connection/optional_method/spec.rbs +32 -0
- data/sig/generated/acp/agent_connection/optional_method.rbs +3 -0
- data/sig/generated/acp/agent_connection.rbs +16 -24
- data/sig/generated/acp/agent_process.rbs +0 -13
- data/sig/generated/acp/client_connection/streams.rbs +94 -0
- data/sig/generated/acp/client_connection.rbs +100 -159
- data/sig/generated/acp/protocol.rbs +9 -0
- data/sig/generated/acp/request_error.rbs +15 -17
- data/sig/generated/acp/transport/cancellation.rbs +0 -7
- data/sig/generated/acp/transport/reply.rbs +0 -2
- data/sig/generated/acp/transport/stderr_logger.rbs +1 -2
- data/sig/generated/acp/transport/stdio/serving.rbs +14 -0
- data/sig/generated/acp/transport/stdio.rbs +31 -35
- data/sig/generated/acp/types/audio_content.rbs +1 -27
- data/sig/generated/acp/types/auth_method_terminal.rbs +1 -35
- data/sig/generated/acp/types/available_commands_update.rbs +1 -19
- data/sig/generated/acp/types/boolean_property_schema.rbs +1 -27
- data/sig/generated/acp/types/check.rbs +115 -0
- data/sig/generated/acp/types/config_option_update.rbs +1 -19
- data/sig/generated/acp/types/content.rbs +1 -19
- data/sig/generated/acp/types/{elicitation_url_mode → create_elicitation_request/mode/form}/scope.rbs +2 -2
- data/sig/generated/acp/types/create_elicitation_request/mode/form.rbs +3 -3
- data/sig/generated/acp/types/{elicitation_form_mode → create_elicitation_request/mode/url}/scope.rbs +2 -2
- data/sig/generated/acp/types/create_elicitation_request/mode/url.rbs +3 -3
- data/sig/generated/acp/types/current_mode_update.rbs +1 -19
- data/sig/generated/acp/types/diff.rbs +1 -27
- data/sig/generated/acp/types/elicitation_accept_action.rbs +1 -15
- data/sig/generated/acp/types/elicitation_form_mode.rbs +1 -19
- data/sig/generated/acp/types/elicitation_url_mode.rbs +1 -23
- data/sig/generated/acp/types/embedded_resource.rbs +1 -23
- data/sig/generated/acp/types/image_content.rbs +1 -31
- data/sig/generated/acp/types/integer_property_schema.rbs +1 -35
- data/sig/generated/acp/types/mcp_server_http.rbs +1 -27
- data/sig/generated/acp/types/mcp_server_sse.rbs +1 -27
- data/sig/generated/acp/types/multi_select_property_schema.rbs +1 -39
- data/sig/generated/acp/types/number_property_schema.rbs +1 -35
- data/sig/generated/acp/types/parse_error.rbs +22 -0
- data/sig/generated/acp/types/plan.rbs +1 -19
- data/sig/generated/acp/types/resource_link.rbs +1 -43
- data/sig/generated/acp/types/selected_permission_outcome.rbs +1 -19
- data/sig/generated/acp/types/session_config_boolean.rbs +1 -15
- data/sig/generated/acp/types/session_config_select.rbs +1 -19
- data/sig/generated/acp/types/session_info_update.rbs +1 -23
- data/sig/generated/acp/types/string_multi_select_items.rbs +1 -19
- data/sig/generated/acp/types/string_property_schema.rbs +1 -51
- data/sig/generated/acp/types/terminal.rbs +1 -19
- data/sig/generated/acp/types/text_content.rbs +1 -23
- data/sig/generated/acp/types/tool_call.rbs +1 -51
- data/sig/generated/acp/types/unstable/compaction_summary_chunk.rbs +1 -23
- data/sig/generated/acp/types/unstable/compaction_update.rbs +1 -31
- data/sig/generated/acp/types/unstable/idle_state_update.rbs +1 -23
- data/sig/generated/acp/types/unstable/mcp_server_acp.rbs +1 -23
- data/sig/generated/acp/types/unstable/nes_edit_suggestion.rbs +1 -31
- data/sig/generated/acp/types/unstable/nes_jump_suggestion.rbs +1 -27
- data/sig/generated/acp/types/unstable/nes_rename_suggestion.rbs +1 -31
- data/sig/generated/acp/types/unstable/nes_search_and_replace_suggestion.rbs +1 -35
- data/sig/generated/acp/types/unstable/notice.rbs +1 -27
- data/sig/generated/acp/types/unstable/plan_file.rbs +1 -23
- data/sig/generated/acp/types/unstable/plan_items.rbs +1 -23
- data/sig/generated/acp/types/unstable/plan_markdown.rbs +1 -23
- data/sig/generated/acp/types/unstable/plan_removed.rbs +1 -19
- data/sig/generated/acp/types/unstable/plan_update.rbs +1 -19
- data/sig/generated/acp/types/unstable/requires_action_state_update.rbs +1 -15
- data/sig/generated/acp/types/unstable/running_state_update.rbs +1 -15
- data/sig/generated/acp/types/unstable/session_message.rbs +1 -31
- data/sig/generated/acp/types/unstable/session_message_chunk.rbs +1 -31
- data/sig/generated/acp/types/unstable/subagent_update.rbs +1 -35
- data/sig/generated/acp/types/unstable/unknown_state_update.rbs +1 -15
- data/sig/generated/acp/types/usage_update.rbs +1 -27
- data/sig/generated/acp/types.rbs +6 -143
- data/sig/manual/acp/agent_connection.rbs +0 -30
- data/sig/manual/acp/client_connection.rbs +7 -2
- data/sig/manual/acp/{agent_connection/optional_method/logout.rbs → protocol.rbs} +3 -3
- data/sig/manual/acp/transport/stdio.rbs +0 -2
- data/sig/manual/acp/transport.rbs +25 -2
- metadata +18 -40
- data/lib/acp/agent_connection/optional_method/authenticate.rb +0 -21
- data/lib/acp/agent_connection/optional_method/close_session.rb +0 -21
- data/lib/acp/agent_connection/optional_method/delete_session.rb +0 -21
- data/lib/acp/agent_connection/optional_method/disable_provider.rb +0 -23
- data/lib/acp/agent_connection/optional_method/fork_session.rb +0 -23
- data/lib/acp/agent_connection/optional_method/list_providers.rb +0 -23
- data/lib/acp/agent_connection/optional_method/list_sessions.rb +0 -21
- data/lib/acp/agent_connection/optional_method/load_session.rb +0 -21
- data/lib/acp/agent_connection/optional_method/logout.rb +0 -21
- data/lib/acp/agent_connection/optional_method/resume_session.rb +0 -21
- data/lib/acp/agent_connection/optional_method/set_provider.rb +0 -23
- data/lib/acp/types/content_chunk.rb +0 -40
- data/sig/generated/acp/agent_connection/optional_method/authenticate.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/close_session.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/delete_session.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/disable_provider.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/fork_session.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/list_providers.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/list_sessions.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/load_session.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/logout.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/resume_session.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/set_provider.rbs +0 -13
- data/sig/generated/acp/types/content_chunk.rbs +0 -25
- data/sig/manual/acp/agent_connection/optional_method/authenticate.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/close_session.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/delete_session.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/disable_provider.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/fork_session.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/list_providers.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/list_sessions.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/load_session.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/resume_session.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/set_provider.rbs +0 -6
data/lib/acp/protocol.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ACP::Protocol
|
|
4
|
+
PROTOCOL_VERSION = 1 #: Integer
|
|
5
|
+
|
|
6
|
+
extend self
|
|
7
|
+
|
|
8
|
+
# @rbs capabilities: (ACP::Types::AgentCapabilities | ACP::Types::Unstable::AgentCapabilities | Hash[String, untyped])
|
|
9
|
+
# @rbs return: bool
|
|
10
|
+
def mcp_acp_advertised?(capabilities)
|
|
11
|
+
# The stable McpCapabilities has no acp field, so the gate can only read it
|
|
12
|
+
# when the caller opted in with `require 'acp/types/unstable'`.
|
|
13
|
+
return false unless ACP::Types.unstable_enabled?
|
|
14
|
+
|
|
15
|
+
mcp =
|
|
16
|
+
case capabilities
|
|
17
|
+
when ACP::Types::AgentCapabilities, ACP::Types::Unstable::AgentCapabilities
|
|
18
|
+
capabilities.mcp_capabilities #: ACP::Types::McpCapabilities | ACP::Types::Unstable::McpCapabilities?
|
|
19
|
+
when Hash
|
|
20
|
+
# The client's stable parse of the initialize reply drops acp, so
|
|
21
|
+
# mcpCapabilities is re-parsed from the raw hash with the unstable
|
|
22
|
+
# type. Only that sub-object: the other unstable fields arrive
|
|
23
|
+
# unvalidated and must not decide whether MCP is advertised.
|
|
24
|
+
raw = capabilities['mcpCapabilities']
|
|
25
|
+
ACP::Types::Unstable::McpCapabilities.from_h(raw) if raw.is_a?(Hash)
|
|
26
|
+
end
|
|
27
|
+
return false unless mcp.is_a?(ACP::Types::Unstable::McpCapabilities)
|
|
28
|
+
|
|
29
|
+
mcp.acp ? true : false
|
|
30
|
+
rescue ACP::Types::ParseError
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/acp/request_error.rb
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
# RequestError: built by handlers and local refusals, and decoded from a
|
|
5
|
-
# peer's error replies by the transport.
|
|
3
|
+
# Named after the official SDKs' RequestError rather than the schema's Error.
|
|
6
4
|
class ACP::RequestError
|
|
7
5
|
PARSE_ERROR = -32_700 #: Integer
|
|
8
6
|
INVALID_REQUEST = -32_600 #: Integer
|
|
@@ -38,6 +36,16 @@ class ACP::RequestError
|
|
|
38
36
|
{ 'code' => code, 'message' => message, 'data' => data }.compact
|
|
39
37
|
end
|
|
40
38
|
|
|
39
|
+
CONNECTION_CLOSED = new(code: INTERNAL_ERROR, message: 'Connection closed') #: ACP::RequestError
|
|
40
|
+
|
|
41
|
+
# Identity rather than code and message: a live peer can answer with the
|
|
42
|
+
# same pair, and that refusal must not read as a closed connection.
|
|
43
|
+
#
|
|
44
|
+
# @rbs return: bool
|
|
45
|
+
def connection_closed?
|
|
46
|
+
equal?(CONNECTION_CLOSED)
|
|
47
|
+
end
|
|
48
|
+
|
|
41
49
|
# @rbs return: ACP::RequestError
|
|
42
50
|
def self.parse_error
|
|
43
51
|
new(code: PARSE_ERROR, message: 'Parse error')
|
|
@@ -60,9 +68,8 @@ class ACP::RequestError
|
|
|
60
68
|
new(code: INVALID_PARAMS, message: 'Invalid params', data: errors && { errors: errors })
|
|
61
69
|
end
|
|
62
70
|
|
|
63
|
-
# The
|
|
64
|
-
#
|
|
65
|
-
# apart from an unknown method.
|
|
71
|
+
# The message names the capability so the refusal can be told apart from an
|
|
72
|
+
# unknown method, though both use the -32601 code.
|
|
66
73
|
#
|
|
67
74
|
# @rbs capability: String
|
|
68
75
|
# @rbs return: ACP::RequestError
|
|
@@ -70,11 +77,9 @@ class ACP::RequestError
|
|
|
70
77
|
new(code: METHOD_NOT_FOUND, message: "Client does not advertise #{capability}")
|
|
71
78
|
end
|
|
72
79
|
|
|
73
|
-
# The
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
# mode in its params, so an unadvertised one is invalid params rather than
|
|
77
|
-
# method not found.
|
|
80
|
+
# The message names the mode so the refusal can be told apart from malformed
|
|
81
|
+
# params. The spec gives elicitation a mode in its params, so an unadvertised
|
|
82
|
+
# one is invalid params rather than method not found.
|
|
78
83
|
#
|
|
79
84
|
# @rbs mode: String
|
|
80
85
|
# @rbs return: ACP::RequestError
|
|
@@ -82,10 +87,6 @@ class ACP::RequestError
|
|
|
82
87
|
new(code: INVALID_PARAMS, message: "Client does not advertise #{mode}")
|
|
83
88
|
end
|
|
84
89
|
|
|
85
|
-
# unadvertised names a capability of the client's, this one a capability of
|
|
86
|
-
# the agent's: the same method-not-found code, with a message naming which
|
|
87
|
-
# side's advertisement is missing.
|
|
88
|
-
#
|
|
89
90
|
# @rbs capability: String
|
|
90
91
|
# @rbs return: ACP::RequestError
|
|
91
92
|
def self.unadvertised_agent(capability)
|
|
@@ -106,8 +107,7 @@ class ACP::RequestError
|
|
|
106
107
|
new(code: INTERNAL_ERROR, message: 'Invalid response', data: response)
|
|
107
108
|
end
|
|
108
109
|
|
|
109
|
-
#
|
|
110
|
-
# not speak; the message names both versions so logs can tell it from a
|
|
110
|
+
# The message names both versions so logs can tell this local refusal from a
|
|
111
111
|
# peer's error.
|
|
112
112
|
#
|
|
113
113
|
# @rbs requested: Integer
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Whether the peer cancelled one request. The transport creates one per served
|
|
4
|
-
# request, marks it when the peer's $/cancel_request arrives, and hands it to
|
|
5
|
-
# the serving thread, whose handler can observe it and end the request early.
|
|
6
3
|
class ACP::Transport::Cancellation
|
|
7
4
|
KEY = :acp_serving_cancellation #: Symbol
|
|
8
5
|
|
|
9
|
-
# Connections read this to expose the serving state to their handlers and
|
|
10
|
-
# write it to carry serving identity onto a thread they spawn; nil off a
|
|
11
|
-
# serve thread.
|
|
12
|
-
#
|
|
13
6
|
# @rbs return: ACP::Transport::Cancellation?
|
|
14
7
|
def self.current
|
|
15
8
|
Thread.current[KEY] #: ACP::Transport::Cancellation?
|
data/lib/acp/transport/reply.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# A request handler's outcome plus a callback the transport runs once that
|
|
4
|
-
# outcome is on the wire, for messages the peer must only see after the reply.
|
|
5
3
|
class ACP::Transport::Reply
|
|
6
4
|
# @dynamic result
|
|
7
5
|
attr_reader :result #: ACP::Transport::Stdio::_ToH
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# The
|
|
4
|
-
# sanctions agents logging to stderr.
|
|
3
|
+
# The spec sanctions agents logging to stderr.
|
|
5
4
|
class ACP::Transport::StderrLogger
|
|
6
5
|
# @rbs message: String
|
|
7
6
|
# @rbs return: void
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ACP::Transport::Stdio::Serving
|
|
4
|
+
# @dynamic id
|
|
5
|
+
attr_reader :id #: untyped
|
|
6
|
+
|
|
7
|
+
# @dynamic cancellation
|
|
8
|
+
attr_reader :cancellation #: ACP::Transport::Cancellation
|
|
9
|
+
|
|
10
|
+
# @rbs id: untyped
|
|
11
|
+
# @rbs cancellation: ACP::Transport::Cancellation
|
|
12
|
+
# @rbs return: void
|
|
13
|
+
def initialize(id:, cancellation:)
|
|
14
|
+
@id = id
|
|
15
|
+
@cancellation = cancellation
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/acp/transport/stdio.rb
CHANGED
|
@@ -2,25 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# that arrive meanwhile.
|
|
5
|
+
# A reader thread dispatches inbound messages while outbound requests are
|
|
6
|
+
# pending, and each inbound request is served on its own thread, so a handler
|
|
7
|
+
# can make an outbound request mid-turn and still see notifications (e.g.
|
|
8
|
+
# session/cancel) that arrive meanwhile.
|
|
10
9
|
class ACP::Transport::Stdio
|
|
11
10
|
# @rbs @input: _Reader
|
|
12
11
|
# @rbs @output: _Writer
|
|
13
12
|
# @rbs @write_lock: Thread::Mutex
|
|
14
13
|
# @rbs @lock: Thread::Mutex
|
|
15
14
|
# @rbs @pending: Hash[untyped, Thread::Queue]
|
|
16
|
-
# @rbs @serving: Hash[
|
|
15
|
+
# @rbs @serving: Hash[Thread, ACP::Transport::Stdio::Serving]
|
|
17
16
|
# @rbs @next_id: Integer
|
|
18
17
|
# @rbs @closed: bool
|
|
19
18
|
# @rbs @logger: ACP::Transport::_Logger
|
|
20
19
|
|
|
21
|
-
CONNECTION_CLOSED = ACP::RequestError
|
|
22
|
-
code: ACP::RequestError::INTERNAL_ERROR, message: 'Connection closed'
|
|
23
|
-
) #: ACP::RequestError
|
|
20
|
+
CONNECTION_CLOSED = ACP::RequestError::CONNECTION_CLOSED #: ACP::RequestError
|
|
24
21
|
|
|
25
22
|
# @rbs input: _Reader
|
|
26
23
|
# @rbs output: _Writer
|
|
@@ -38,8 +35,12 @@ class ACP::Transport::Stdio
|
|
|
38
35
|
@logger = logger
|
|
39
36
|
end
|
|
40
37
|
|
|
41
|
-
#
|
|
42
|
-
#
|
|
38
|
+
# Joining the returned thread does not wait for the requests still being
|
|
39
|
+
# served. EOF cancels none of them (only the peer's $/cancel_request does):
|
|
40
|
+
# each handler runs to completion and its reply is still written, to be
|
|
41
|
+
# warned and dropped if the peer is gone. A caller blocked in #request, a
|
|
42
|
+
# handler waiting on its own request included, is released with
|
|
43
|
+
# CONNECTION_CLOSED.
|
|
43
44
|
#
|
|
44
45
|
# @rbs requests: Hash[String, _Handler]
|
|
45
46
|
# @rbs notifications: Hash[String, _NotificationHandler]
|
|
@@ -55,14 +56,16 @@ class ACP::Transport::Stdio
|
|
|
55
56
|
|
|
56
57
|
# @rbs method: String
|
|
57
58
|
# @rbs params: untyped
|
|
58
|
-
# @rbs return:
|
|
59
|
+
# @rbs return: untyped
|
|
59
60
|
def request(method, params = nil)
|
|
60
61
|
queue = Thread::Queue.new
|
|
61
62
|
id = register(queue)
|
|
62
63
|
return CONNECTION_CLOSED unless id
|
|
63
64
|
|
|
64
65
|
write({ 'jsonrpc' => '2.0', 'id' => id, 'method' => method, 'params' => params }.compact)
|
|
65
|
-
queue.pop
|
|
66
|
+
result = queue.pop
|
|
67
|
+
# A null result pops nil just as a closed queue does.
|
|
68
|
+
queue.closed? ? CONNECTION_CLOSED : result
|
|
66
69
|
end
|
|
67
70
|
|
|
68
71
|
# @rbs method: String
|
|
@@ -90,16 +93,24 @@ class ACP::Transport::Stdio
|
|
|
90
93
|
@lock.synchronize { @pending.keys }.each { |id| cancel(id) }
|
|
91
94
|
end
|
|
92
95
|
|
|
93
|
-
# The cancellation of the request the calling thread is serving, or nil off
|
|
94
|
-
# a serve thread. A handler observes the peer's $/cancel_request through it
|
|
95
|
-
# and can end early with ACP::RequestError.request_cancelled; whatever it
|
|
96
|
-
# returns, the transport sends exactly one response.
|
|
97
|
-
#
|
|
98
96
|
# @rbs return: ACP::Transport::Cancellation?
|
|
99
97
|
def cancellation
|
|
100
98
|
ACP::Transport::Cancellation.current
|
|
101
99
|
end
|
|
102
100
|
|
|
101
|
+
# @rbs return: Array[untyped]
|
|
102
|
+
def serving_ids
|
|
103
|
+
@lock.synchronize { @serving.each_value.map(&:id) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Joins the requests in flight at the call, not one the reader dispatches
|
|
107
|
+
# meanwhile: join the reader thread first to leave nothing in flight.
|
|
108
|
+
#
|
|
109
|
+
# @rbs return: void
|
|
110
|
+
def drain
|
|
111
|
+
@lock.synchronize { @serving.keys }.each(&:join)
|
|
112
|
+
end
|
|
113
|
+
|
|
103
114
|
private
|
|
104
115
|
|
|
105
116
|
# @rbs line: String
|
|
@@ -137,8 +148,12 @@ class ACP::Transport::Stdio
|
|
|
137
148
|
elsif error.is_a?(Hash) && error['code'].is_a?(Integer) && error['message'].is_a?(String)
|
|
138
149
|
code, text, data = error.values_at('code', 'message', 'data')
|
|
139
150
|
settle(message['id'], ACP::RequestError.new(code:, message: text, data:))
|
|
140
|
-
|
|
151
|
+
elsif message.key?('method')
|
|
141
152
|
reply(detectable_id(message), ACP::RequestError.invalid_request)
|
|
153
|
+
elsif !settle(message['id'], ACP::RequestError.invalid_response(message))
|
|
154
|
+
# JSON-RPC responses only answer requests, so a malformed one is never
|
|
155
|
+
# replied to: the error could settle an unrelated request on the peer.
|
|
156
|
+
@logger.warn('dropped malformed response: no pending request for its id')
|
|
142
157
|
end
|
|
143
158
|
end
|
|
144
159
|
|
|
@@ -170,39 +185,29 @@ class ACP::Transport::Stdio
|
|
|
170
185
|
# @rbs method: String
|
|
171
186
|
# @rbs return: void
|
|
172
187
|
def serve(handler, id, params, method)
|
|
173
|
-
cancellation =
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
188
|
+
cancellation = ACP::Transport::Cancellation.new
|
|
189
|
+
# Registered on the reader thread so a cancel racing the request cannot
|
|
190
|
+
# be missed. The serve thread starts under the lock so it is registered
|
|
191
|
+
# before a drain can miss it or it can unregister itself.
|
|
192
|
+
@lock.synchronize do
|
|
193
|
+
thread = Thread.new do
|
|
194
|
+
ACP::Transport::Cancellation.current = cancellation
|
|
195
|
+
outcome = handler ? invoke(handler, params) : ACP::RequestError.method_not_found(method)
|
|
196
|
+
case outcome
|
|
197
|
+
when ACP::Transport::Reply
|
|
198
|
+
reply(id, outcome.result)
|
|
199
|
+
quietly('reply after callback', &outcome.after)
|
|
200
|
+
else reply(id, outcome)
|
|
201
|
+
end
|
|
202
|
+
ensure
|
|
203
|
+
# Unregistered only after the reply and its after callback, so drain waits
|
|
204
|
+
# for both and later cancels for the id are ignored.
|
|
205
|
+
@lock.synchronize { @serving.delete(Thread.current) }
|
|
182
206
|
end
|
|
183
|
-
|
|
184
|
-
unregister_serving(id)
|
|
207
|
+
@serving[thread] = Serving.new(id:, cancellation:)
|
|
185
208
|
end
|
|
186
209
|
end
|
|
187
210
|
|
|
188
|
-
# Registered before the serve thread starts so a cancel racing the request
|
|
189
|
-
# cannot be missed, and dropped once the response is on the wire so later
|
|
190
|
-
# cancels for the id are ignored.
|
|
191
|
-
#
|
|
192
|
-
# @rbs id: untyped
|
|
193
|
-
# @rbs return: ACP::Transport::Cancellation
|
|
194
|
-
def register_serving(id)
|
|
195
|
-
cancellation = ACP::Transport::Cancellation.new
|
|
196
|
-
@lock.synchronize { @serving[id] = cancellation }
|
|
197
|
-
cancellation
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
# @rbs id: untyped
|
|
201
|
-
# @rbs return: void
|
|
202
|
-
def unregister_serving(id)
|
|
203
|
-
@lock.synchronize { @serving.delete(id) }
|
|
204
|
-
end
|
|
205
|
-
|
|
206
211
|
# A notification has no reply to carry a malformed params failure, so one
|
|
207
212
|
# is warned and dropped rather than answered.
|
|
208
213
|
#
|
|
@@ -214,8 +219,10 @@ class ACP::Transport::Stdio
|
|
|
214
219
|
return @logger.warn('dropped malformed $/cancel_request: missing requestId')
|
|
215
220
|
end
|
|
216
221
|
|
|
217
|
-
|
|
218
|
-
|
|
222
|
+
cancellations = @lock.synchronize do
|
|
223
|
+
@serving.each_value.filter_map { |entry| entry.cancellation if entry.id == id }
|
|
224
|
+
end
|
|
225
|
+
cancellations.each(&:cancel)
|
|
219
226
|
end
|
|
220
227
|
|
|
221
228
|
# For code with no reply to carry an error: a notification handler, or a
|
|
@@ -246,9 +253,6 @@ class ACP::Transport::Stdio
|
|
|
246
253
|
ACP::RequestError.internal_error
|
|
247
254
|
end
|
|
248
255
|
|
|
249
|
-
# The success arm is whatever the handler returned, which the transport only
|
|
250
|
-
# knows serializes with to_h; RequestError picks the error arm of the reply.
|
|
251
|
-
#
|
|
252
256
|
# @rbs id: untyped
|
|
253
257
|
# @rbs outcome: (_ToH | ACP::RequestError)
|
|
254
258
|
# @rbs return: void
|
|
@@ -273,12 +277,15 @@ class ACP::Transport::Stdio
|
|
|
273
277
|
end
|
|
274
278
|
|
|
275
279
|
# @rbs id: untyped
|
|
276
|
-
# @rbs result:
|
|
277
|
-
# @rbs return:
|
|
280
|
+
# @rbs result: untyped
|
|
281
|
+
# @rbs return: Thread::Queue?
|
|
278
282
|
def settle(id, result)
|
|
279
|
-
@lock.synchronize { @pending.delete(id) }&.push(result)
|
|
283
|
+
@lock.synchronize { @pending.delete(id) }&.tap { |queue| queue.push(result) }
|
|
280
284
|
end
|
|
281
285
|
|
|
286
|
+
# Leaves @serving alone on purpose: embedders rely on in-flight handlers
|
|
287
|
+
# running to completion after the input ends.
|
|
288
|
+
#
|
|
282
289
|
# @rbs return: void
|
|
283
290
|
def close
|
|
284
291
|
@lock.synchronize do
|
|
@@ -288,8 +295,9 @@ class ACP::Transport::Stdio
|
|
|
288
295
|
end
|
|
289
296
|
end
|
|
290
297
|
|
|
291
|
-
# A closed peer
|
|
292
|
-
#
|
|
298
|
+
# A closed peer (EPIPE) or an output the embedder already closed (IOError)
|
|
299
|
+
# must not kill the thread writing the reply: the reader has to keep
|
|
300
|
+
# draining input so pending requests still settle at EOF.
|
|
293
301
|
#
|
|
294
302
|
# @rbs message: Hash[String, untyped]
|
|
295
303
|
# @rbs return: void
|
|
@@ -299,7 +307,7 @@ class ACP::Transport::Stdio
|
|
|
299
307
|
@output.write(line)
|
|
300
308
|
@output.flush
|
|
301
309
|
end
|
|
302
|
-
rescue Errno::EPIPE => e
|
|
310
|
+
rescue Errno::EPIPE, IOError => e
|
|
303
311
|
@logger.warn("write to closed peer: #{e.class}: #{e.message}")
|
|
304
312
|
end
|
|
305
313
|
end
|
|
@@ -2,50 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Generated by bin/types from schema/schema.json. Do not edit.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
attr_reader :annotations #: ACP::Types::Annotations?
|
|
5
|
+
# @rbs!
|
|
6
|
+
# class ACP::Types::AudioContent = ACP::Types::ContentBlock::Audio
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
attr_reader :data #: String
|
|
11
|
-
|
|
12
|
-
# @dynamic mime_type
|
|
13
|
-
attr_reader :mime_type #: String
|
|
14
|
-
|
|
15
|
-
# @dynamic meta
|
|
16
|
-
attr_reader :meta #: Hash[String, untyped]?
|
|
17
|
-
|
|
18
|
-
# @rbs annotations: ACP::Types::Annotations?
|
|
19
|
-
# @rbs data: String
|
|
20
|
-
# @rbs mime_type: String
|
|
21
|
-
# @rbs meta: Hash[String, untyped]?
|
|
22
|
-
# @rbs return: void
|
|
23
|
-
def initialize(data:, mime_type:, annotations: nil, meta: nil)
|
|
24
|
-
@annotations = annotations
|
|
25
|
-
@data = data
|
|
26
|
-
@mime_type = mime_type
|
|
27
|
-
@meta = meta
|
|
28
|
-
freeze
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# @rbs hash: Hash[String, untyped]
|
|
32
|
-
# @rbs return: ACP::Types::AudioContent
|
|
33
|
-
def self.from_h(hash)
|
|
34
|
-
new(
|
|
35
|
-
annotations: ACP::Types::Check.object(
|
|
36
|
-
hash['annotations'],
|
|
37
|
-
'annotations',
|
|
38
|
-
ACP::Types::Annotations,
|
|
39
|
-
allow_nil: true
|
|
40
|
-
),
|
|
41
|
-
data: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'data'), 'data'),
|
|
42
|
-
mime_type: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'mimeType'), 'mimeType'),
|
|
43
|
-
meta: hash['_meta']
|
|
44
|
-
)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# @rbs return: Hash[String, untyped]
|
|
48
|
-
def to_h
|
|
49
|
-
{ 'data' => data, 'mimeType' => mime_type, 'annotations' => annotations&.to_h, '_meta' => meta }.compact
|
|
50
|
-
end
|
|
51
|
-
end
|
|
8
|
+
ACP::Types::AudioContent = ACP::Types::ContentBlock::Audio
|
|
@@ -2,63 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Generated by bin/types from schema/schema.json. Do not edit.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
attr_reader :id #: String
|
|
5
|
+
# @rbs!
|
|
6
|
+
# class ACP::Types::AuthMethodTerminal = ACP::Types::AuthMethod::Terminal
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
attr_reader :name #: String
|
|
11
|
-
|
|
12
|
-
# @dynamic description
|
|
13
|
-
attr_reader :description #: String?
|
|
14
|
-
|
|
15
|
-
# @dynamic args
|
|
16
|
-
attr_reader :args #: Array[String]?
|
|
17
|
-
|
|
18
|
-
# @dynamic env
|
|
19
|
-
attr_reader :env #: Hash[String, String]?
|
|
20
|
-
|
|
21
|
-
# @dynamic meta
|
|
22
|
-
attr_reader :meta #: Hash[String, untyped]?
|
|
23
|
-
|
|
24
|
-
# @rbs id: String
|
|
25
|
-
# @rbs name: String
|
|
26
|
-
# @rbs description: String?
|
|
27
|
-
# @rbs args: Array[String]?
|
|
28
|
-
# @rbs env: Hash[String, String]?
|
|
29
|
-
# @rbs meta: Hash[String, untyped]?
|
|
30
|
-
# @rbs return: void
|
|
31
|
-
def initialize(id:, name:, description: nil, args: nil, env: nil, meta: nil)
|
|
32
|
-
@id = id
|
|
33
|
-
@name = name
|
|
34
|
-
@description = description
|
|
35
|
-
@args = args
|
|
36
|
-
@env = env
|
|
37
|
-
@meta = meta
|
|
38
|
-
freeze
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# @rbs hash: Hash[String, untyped]
|
|
42
|
-
# @rbs return: ACP::Types::AuthMethodTerminal
|
|
43
|
-
def self.from_h(hash)
|
|
44
|
-
args = ACP::Types::Check.array(hash['args'], 'args', allow_nil: true) do |item, path|
|
|
45
|
-
ACP::Types::Check.string(item, path)
|
|
46
|
-
end
|
|
47
|
-
env = ACP::Types::Check.hash(hash['env'], 'env', allow_nil: true) do |value, path|
|
|
48
|
-
ACP::Types::Check.string(value, path)
|
|
49
|
-
end
|
|
50
|
-
new(
|
|
51
|
-
id: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'id'), 'id'),
|
|
52
|
-
name: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'name'), 'name'),
|
|
53
|
-
description: ACP::Types::Check.string(hash['description'], 'description', allow_nil: true),
|
|
54
|
-
args:,
|
|
55
|
-
env:,
|
|
56
|
-
meta: hash['_meta']
|
|
57
|
-
)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# @rbs return: Hash[String, untyped]
|
|
61
|
-
def to_h
|
|
62
|
-
{ 'id' => id, 'name' => name, 'description' => description, 'args' => args, 'env' => env, '_meta' => meta }.compact
|
|
63
|
-
end
|
|
64
|
-
end
|
|
8
|
+
ACP::Types::AuthMethodTerminal = ACP::Types::AuthMethod::Terminal
|
|
@@ -2,36 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Generated by bin/types from schema/schema.json. Do not edit.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
attr_reader :available_commands #: Array[ACP::Types::AvailableCommand]
|
|
5
|
+
# @rbs!
|
|
6
|
+
# class ACP::Types::AvailableCommandsUpdate = ACP::Types::SessionUpdate::AvailableCommandsUpdate
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
attr_reader :meta #: Hash[String, untyped]?
|
|
11
|
-
|
|
12
|
-
# @rbs available_commands: Array[ACP::Types::AvailableCommand]
|
|
13
|
-
# @rbs meta: Hash[String, untyped]?
|
|
14
|
-
# @rbs return: void
|
|
15
|
-
def initialize(available_commands:, meta: nil)
|
|
16
|
-
@available_commands = available_commands
|
|
17
|
-
@meta = meta
|
|
18
|
-
freeze
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# @rbs hash: Hash[String, untyped]
|
|
22
|
-
# @rbs return: ACP::Types::AvailableCommandsUpdate
|
|
23
|
-
def self.from_h(hash)
|
|
24
|
-
available_commands = ACP::Types::Check.array(
|
|
25
|
-
ACP::Types::Check.key(hash, 'availableCommands'),
|
|
26
|
-
'availableCommands'
|
|
27
|
-
) do |item, path|
|
|
28
|
-
ACP::Types::Check.object(item, path, ACP::Types::AvailableCommand)
|
|
29
|
-
end
|
|
30
|
-
new(available_commands:, meta: hash['_meta'])
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# @rbs return: Hash[String, untyped]
|
|
34
|
-
def to_h
|
|
35
|
-
{ 'availableCommands' => available_commands.map(&:to_h), '_meta' => meta }.compact
|
|
36
|
-
end
|
|
37
|
-
end
|
|
8
|
+
ACP::Types::AvailableCommandsUpdate = ACP::Types::SessionUpdate::AvailableCommandsUpdate
|
|
@@ -2,45 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Generated by bin/types from schema/schema.json. Do not edit.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
attr_reader :title #: String?
|
|
5
|
+
# @rbs!
|
|
6
|
+
# class ACP::Types::BooleanPropertySchema = ACP::Types::ElicitationPropertySchema::BooleanPropertySchema
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
attr_reader :description #: String?
|
|
11
|
-
|
|
12
|
-
# @dynamic default
|
|
13
|
-
attr_reader :default #: bool?
|
|
14
|
-
|
|
15
|
-
# @dynamic meta
|
|
16
|
-
attr_reader :meta #: Hash[String, untyped]?
|
|
17
|
-
|
|
18
|
-
# @rbs title: String?
|
|
19
|
-
# @rbs description: String?
|
|
20
|
-
# @rbs default: bool?
|
|
21
|
-
# @rbs meta: Hash[String, untyped]?
|
|
22
|
-
# @rbs return: void
|
|
23
|
-
def initialize(title: nil, description: nil, default: nil, meta: nil)
|
|
24
|
-
@title = title
|
|
25
|
-
@description = description
|
|
26
|
-
@default = default
|
|
27
|
-
@meta = meta
|
|
28
|
-
freeze
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# @rbs hash: Hash[String, untyped]
|
|
32
|
-
# @rbs return: ACP::Types::BooleanPropertySchema
|
|
33
|
-
def self.from_h(hash)
|
|
34
|
-
new(
|
|
35
|
-
title: ACP::Types::Check.string(hash['title'], 'title', allow_nil: true),
|
|
36
|
-
description: ACP::Types::Check.string(hash['description'], 'description', allow_nil: true),
|
|
37
|
-
default: ACP::Types::Check.boolean(hash['default'], 'default', allow_nil: true),
|
|
38
|
-
meta: hash['_meta']
|
|
39
|
-
)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# @rbs return: Hash[String, untyped]
|
|
43
|
-
def to_h
|
|
44
|
-
{ 'title' => title, 'description' => description, 'default' => default, '_meta' => meta }.compact
|
|
45
|
-
end
|
|
46
|
-
end
|
|
8
|
+
ACP::Types::BooleanPropertySchema = ACP::Types::ElicitationPropertySchema::BooleanPropertySchema
|