acp-sdk 0.9.0 → 0.11.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 +32 -0
- data/README.md +20 -8
- data/lib/acp/agent_connection/client.rb +31 -13
- 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 +67 -20
- data/lib/acp/agent_process.rb +0 -13
- data/lib/acp/client_connection/streams.rb +183 -0
- data/lib/acp/client_connection.rb +280 -338
- data/lib/acp/protocol.rb +33 -0
- data/lib/acp/request_error.rb +13 -13
- data/lib/acp/transport/cancellation.rb +13 -3
- data/lib/acp/transport/reply.rb +0 -2
- data/lib/acp/transport/stderr_logger.rb +1 -2
- data/lib/acp/transport/stdio.rb +25 -26
- 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/message_mcp_response/result.rb +1 -1
- 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 +25 -15
- 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 +34 -13
- 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 +128 -131
- data/sig/generated/acp/protocol.rbs +9 -0
- data/sig/generated/acp/request_error.rbs +11 -13
- data/sig/generated/acp/transport/cancellation.rbs +9 -3
- 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.rbs +14 -22
- 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 +28 -28
- 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 +17 -29
- 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/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/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/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/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/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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class ACP::ClientConnection
|
|
4
|
-
# @rbs @transport: ACP::
|
|
4
|
+
# @rbs @transport: ACP::Transport::_Transport
|
|
5
5
|
# @rbs @permission: ACP::ClientConnection::_PermissionHandler
|
|
6
6
|
# @rbs @read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
7
7
|
# @rbs @write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
|
@@ -13,28 +13,139 @@ class ACP::ClientConnection
|
|
|
13
13
|
# @rbs @release_terminal: (^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError))?
|
|
14
14
|
# @rbs @elicitation: (^(ACP::Types::CreateElicitationRequest) ->
|
|
15
15
|
# (ACP::Types::CreateElicitationResponse | ACP::RequestError))?
|
|
16
|
-
# @rbs @
|
|
17
|
-
#
|
|
16
|
+
# @rbs @mcp_message: (^(ACP::Types::Unstable::MessageMcpRequest) ->
|
|
17
|
+
# (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError))?
|
|
18
|
+
# @rbs @mcp_advertised: bool?
|
|
18
19
|
# @rbs @fs_capabilities: ACP::Types::FileSystemCapabilities?
|
|
19
20
|
# @rbs @terminal: bool?
|
|
20
21
|
# @rbs @elicitation_capabilities: ACP::Types::ElicitationCapabilities?
|
|
21
22
|
# @rbs @extension_requests: Hash[String, ^(untyped) -> untyped]
|
|
22
23
|
# @rbs @extension_notifications: Hash[String, ^(untyped) -> void]
|
|
23
|
-
# @rbs @
|
|
24
|
-
# @rbs @streams: Hash[String, Thread::Queue]
|
|
25
|
-
# @rbs @pending_permissions: Hash[String, Array[Thread::Queue]]
|
|
26
|
-
# @rbs @outstanding_elicitations: Hash[String, bool]
|
|
24
|
+
# @rbs @streams: ACP::ClientConnection::Streams
|
|
27
25
|
# @rbs @logger: ACP::Transport::_Logger
|
|
28
26
|
|
|
29
|
-
PROTOCOL_VERSION = ACP::AgentConnection::PROTOCOL_VERSION #: Integer
|
|
30
|
-
|
|
31
27
|
IGNORE = ->(_notification) {} #: ^(ACP::Types::SessionNotification) -> void
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
class Route
|
|
30
|
+
# @rbs @wire: String
|
|
31
|
+
# @rbs @type: ACP::Transport::_Parser
|
|
32
|
+
# @rbs @handler: Symbol
|
|
33
|
+
# @rbs @family: Symbol
|
|
34
|
+
# @rbs @advertised: ^(untyped) -> (bool | nil)
|
|
35
|
+
# @rbs @refuse: Symbol
|
|
36
|
+
# @rbs @key: String
|
|
37
|
+
# @rbs @label: String
|
|
38
|
+
|
|
39
|
+
# @rbs wire: String
|
|
40
|
+
# @rbs type: ACP::Transport::_Parser
|
|
41
|
+
# @rbs handler: Symbol
|
|
42
|
+
# @rbs family: Symbol
|
|
43
|
+
# @rbs key: String
|
|
44
|
+
# @rbs advertised: ^(untyped) -> (bool | nil)
|
|
45
|
+
# @rbs refuse: Symbol
|
|
46
|
+
# @rbs label: String?
|
|
47
|
+
# @rbs return: void
|
|
48
|
+
def initialize(
|
|
49
|
+
wire:,
|
|
50
|
+
type:,
|
|
51
|
+
handler:,
|
|
52
|
+
family:,
|
|
53
|
+
key:,
|
|
54
|
+
advertised: ->(caps) { caps },
|
|
55
|
+
refuse: :unadvertised,
|
|
56
|
+
label: nil
|
|
57
|
+
)
|
|
58
|
+
@wire = wire
|
|
59
|
+
@type = type
|
|
60
|
+
@handler = handler
|
|
61
|
+
@family = family
|
|
62
|
+
@advertised = advertised
|
|
63
|
+
@refuse = refuse
|
|
64
|
+
@key = key
|
|
65
|
+
# The capability key connect's pre-flight names; it differs from the
|
|
66
|
+
# refusal key where a family gates several routes at once.
|
|
67
|
+
@label = label || key
|
|
68
|
+
freeze
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @dynamic wire, type, handler, family, advertised, refuse, key, label
|
|
72
|
+
attr_reader :wire #: String
|
|
73
|
+
attr_reader :type #: ACP::Transport::_Parser
|
|
74
|
+
attr_reader :handler #: Symbol
|
|
75
|
+
attr_reader :family #: Symbol
|
|
76
|
+
attr_reader :advertised #: ^(untyped) -> (bool | nil)
|
|
77
|
+
attr_reader :refuse #: Symbol
|
|
78
|
+
attr_reader :key #: String
|
|
79
|
+
attr_reader :label #: String
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The routes are registered at start, before connect records the
|
|
83
|
+
# advertisement, so each one answers its refusal until then. The terminal
|
|
84
|
+
# capability is a single bool, so its five routes share one gate.
|
|
85
|
+
#
|
|
86
|
+
# The terminal handlers may block for as long as the command runs, so the
|
|
87
|
+
# transport must serve each request on its own thread for this not to hold
|
|
88
|
+
# up others.
|
|
89
|
+
SERVE = [
|
|
90
|
+
Route.new(
|
|
91
|
+
wire: 'fs/read_text_file',
|
|
92
|
+
type: ACP::Types::ReadTextFileRequest,
|
|
93
|
+
handler: :@read_text_file,
|
|
94
|
+
family: :@fs_capabilities,
|
|
95
|
+
advertised: ->(caps) { caps&.read_text_file },
|
|
96
|
+
key: 'fs.readTextFile'
|
|
97
|
+
),
|
|
98
|
+
Route.new(
|
|
99
|
+
wire: 'fs/write_text_file',
|
|
100
|
+
type: ACP::Types::WriteTextFileRequest,
|
|
101
|
+
handler: :@write_text_file,
|
|
102
|
+
family: :@fs_capabilities,
|
|
103
|
+
advertised: ->(caps) { caps&.write_text_file },
|
|
104
|
+
key: 'fs.writeTextFile'
|
|
105
|
+
),
|
|
106
|
+
Route.new(
|
|
107
|
+
wire: 'terminal/create',
|
|
108
|
+
type: ACP::Types::CreateTerminalRequest,
|
|
109
|
+
handler: :@create_terminal,
|
|
110
|
+
family: :@terminal,
|
|
111
|
+
key: 'terminal',
|
|
112
|
+
label: 'terminal.create'
|
|
113
|
+
),
|
|
114
|
+
Route.new(
|
|
115
|
+
wire: 'terminal/output',
|
|
116
|
+
type: ACP::Types::TerminalOutputRequest,
|
|
117
|
+
handler: :@terminal_output,
|
|
118
|
+
family: :@terminal,
|
|
119
|
+
key: 'terminal',
|
|
120
|
+
label: 'terminal.output'
|
|
121
|
+
),
|
|
122
|
+
Route.new(
|
|
123
|
+
wire: 'terminal/wait_for_exit',
|
|
124
|
+
type: ACP::Types::WaitForTerminalExitRequest,
|
|
125
|
+
handler: :@wait_for_terminal_exit,
|
|
126
|
+
family: :@terminal,
|
|
127
|
+
key: 'terminal',
|
|
128
|
+
label: 'terminal.wait_for_exit'
|
|
129
|
+
),
|
|
130
|
+
Route.new(
|
|
131
|
+
wire: 'terminal/kill',
|
|
132
|
+
type: ACP::Types::KillTerminalRequest,
|
|
133
|
+
handler: :@kill_terminal,
|
|
134
|
+
family: :@terminal,
|
|
135
|
+
key: 'terminal',
|
|
136
|
+
label: 'terminal.kill'
|
|
137
|
+
),
|
|
138
|
+
Route.new(
|
|
139
|
+
wire: 'terminal/release',
|
|
140
|
+
type: ACP::Types::ReleaseTerminalRequest,
|
|
141
|
+
handler: :@release_terminal,
|
|
142
|
+
family: :@terminal,
|
|
143
|
+
key: 'terminal',
|
|
144
|
+
label: 'terminal.release'
|
|
145
|
+
)
|
|
146
|
+
].freeze #: Array[ACP::ClientConnection::Route]
|
|
36
147
|
|
|
37
|
-
# @rbs transport: ACP::
|
|
148
|
+
# @rbs transport: ACP::Transport::_Transport
|
|
38
149
|
# @rbs permission: ACP::ClientConnection::_PermissionHandler
|
|
39
150
|
# @rbs read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
40
151
|
# @rbs write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
|
@@ -48,8 +159,11 @@ class ACP::ClientConnection
|
|
|
48
159
|
# (ACP::Types::CreateElicitationResponse | ACP::RequestError))?
|
|
49
160
|
# @rbs complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?
|
|
50
161
|
# @rbs updates: ACP::ClientConnection::_UpdateHandler
|
|
162
|
+
# @rbs mcp_message: (^(ACP::Types::Unstable::MessageMcpRequest) ->
|
|
163
|
+
# (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError))?
|
|
51
164
|
# @rbs extension_requests: Hash[String, ^(untyped) -> untyped]
|
|
52
165
|
# @rbs extension_notifications: Hash[String, ^(untyped) -> void]
|
|
166
|
+
# @rbs streams: ACP::ClientConnection::Streams?
|
|
53
167
|
# @rbs logger: ACP::Transport::_Logger
|
|
54
168
|
# @rbs return: void
|
|
55
169
|
def initialize(
|
|
@@ -65,12 +179,13 @@ class ACP::ClientConnection
|
|
|
65
179
|
elicitation: nil,
|
|
66
180
|
complete_elicitation: nil,
|
|
67
181
|
updates: IGNORE,
|
|
182
|
+
mcp_message: nil,
|
|
68
183
|
extension_requests: {},
|
|
69
184
|
extension_notifications: {},
|
|
185
|
+
streams: nil,
|
|
70
186
|
logger: ACP::Transport::StderrLogger.new
|
|
71
187
|
)
|
|
72
188
|
@transport = transport
|
|
73
|
-
@permission = permission
|
|
74
189
|
@read_text_file = read_text_file
|
|
75
190
|
@write_text_file = write_text_file
|
|
76
191
|
@create_terminal = create_terminal
|
|
@@ -79,8 +194,8 @@ class ACP::ClientConnection
|
|
|
79
194
|
@kill_terminal = kill_terminal
|
|
80
195
|
@release_terminal = release_terminal
|
|
81
196
|
@elicitation = elicitation
|
|
82
|
-
@
|
|
83
|
-
@
|
|
197
|
+
@mcp_message = mcp_message
|
|
198
|
+
@mcp_advertised = nil
|
|
84
199
|
@fs_capabilities = nil
|
|
85
200
|
@terminal = nil
|
|
86
201
|
@elicitation_capabilities = nil
|
|
@@ -88,10 +203,7 @@ class ACP::ClientConnection
|
|
|
88
203
|
@extension_notifications = extension_notifications
|
|
89
204
|
extension_requests.each_key { |name| ACP::Extensions.validate_name(name) }
|
|
90
205
|
extension_notifications.each_key { |name| ACP::Extensions.validate_name(name) }
|
|
91
|
-
@
|
|
92
|
-
@streams = {}
|
|
93
|
-
@pending_permissions = {}
|
|
94
|
-
@outstanding_elicitations = {}
|
|
206
|
+
@streams = streams || Streams.new(sender: transport, permission:, updates:, logger:, complete_elicitation:)
|
|
95
207
|
@logger = logger
|
|
96
208
|
end
|
|
97
209
|
|
|
@@ -99,18 +211,13 @@ class ACP::ClientConnection
|
|
|
99
211
|
def start
|
|
100
212
|
@transport.start(
|
|
101
213
|
requests: {
|
|
102
|
-
'session/request_permission' => method(:request_permission),
|
|
103
|
-
'fs/read_text_file' => method(:read_text_file),
|
|
104
|
-
'fs/write_text_file' => method(:write_text_file),
|
|
105
|
-
'terminal/create' => method(:create_terminal),
|
|
106
|
-
'terminal/output' => method(:terminal_output),
|
|
107
|
-
'terminal/wait_for_exit' => method(:wait_for_terminal_exit),
|
|
108
|
-
'terminal/kill' => method(:kill_terminal),
|
|
109
|
-
'terminal/release' => method(:release_terminal),
|
|
214
|
+
'session/request_permission' => @streams.method(:request_permission),
|
|
110
215
|
'elicitation/create' => method(:create_elicitation)
|
|
111
|
-
}.merge(@extension_requests),
|
|
112
|
-
notifications: {
|
|
113
|
-
|
|
216
|
+
}.merge(SERVE.to_h { |route| [route.wire, serve(route)] }).merge(mcp_requests).merge(@extension_requests),
|
|
217
|
+
notifications: {
|
|
218
|
+
'session/update' => @streams.method(:dispatch),
|
|
219
|
+
'elicitation/complete' => @streams.method(:complete_elicitation)
|
|
220
|
+
}.merge(@extension_notifications)
|
|
114
221
|
)
|
|
115
222
|
end
|
|
116
223
|
|
|
@@ -120,102 +227,124 @@ class ACP::ClientConnection
|
|
|
120
227
|
# @rbs request: ACP::Types::InitializeRequest
|
|
121
228
|
# @rbs return: ACP::Types::InitializeResponse | ACP::RequestError
|
|
122
229
|
def connect(request)
|
|
123
|
-
capabilities = request.client_capabilities
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
230
|
+
capabilities = request.client_capabilities
|
|
231
|
+
fs = capabilities&.fs
|
|
232
|
+
terminal = capabilities&.terminal
|
|
233
|
+
check_served('fs', :@fs_capabilities, fs)
|
|
234
|
+
check_served('terminal', :@terminal, terminal)
|
|
128
235
|
|
|
129
|
-
|
|
130
|
-
unserved = unserved_terminal_methods(terminal)
|
|
131
|
-
unless unserved.empty?
|
|
132
|
-
raise ArgumentError, "initialize advertises terminal methods no handler serves: #{unserved.join(', ')}"
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
elicitation = request.client_capabilities&.elicitation
|
|
236
|
+
elicitation = capabilities&.elicitation
|
|
136
237
|
unserved = unserved_elicitation_modes(elicitation)
|
|
137
238
|
unless unserved.empty?
|
|
138
239
|
raise ArgumentError, "initialize advertises elicitation modes no handler serves: #{unserved.join(', ')}"
|
|
139
240
|
end
|
|
140
241
|
|
|
141
|
-
@fs_capabilities =
|
|
242
|
+
@fs_capabilities = fs
|
|
142
243
|
@terminal = terminal
|
|
143
244
|
@elicitation_capabilities = elicitation
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
245
|
+
result = @transport.request('initialize', request.to_h)
|
|
246
|
+
response = parse(ACP::Types::InitializeResponse, result)
|
|
247
|
+
return response if response.is_a?(ACP::RequestError)
|
|
248
|
+
|
|
249
|
+
# Safe: parse only calls from_h on a Hash, and its RequestError answer
|
|
250
|
+
# short-circuited above.
|
|
251
|
+
reply = result #: Hash[String, untyped]
|
|
252
|
+
@mcp_advertised = ACP::Protocol.mcp_acp_advertised?(reply['agentCapabilities'] || {})
|
|
253
|
+
return response if response.protocol_version == ACP::Protocol::PROTOCOL_VERSION
|
|
254
|
+
|
|
255
|
+
ACP::RequestError.unsupported_protocol_version(
|
|
256
|
+
requested: request.protocol_version,
|
|
257
|
+
returned: response.protocol_version
|
|
258
|
+
)
|
|
154
259
|
end
|
|
155
260
|
|
|
156
261
|
# @rbs request: ACP::Types::NewSessionRequest
|
|
157
262
|
# @rbs return: ACP::Types::NewSessionResponse | ACP::RequestError
|
|
158
|
-
def
|
|
263
|
+
def new_session(request)
|
|
159
264
|
parse(ACP::Types::NewSessionResponse, @transport.request('session/new', request.to_h))
|
|
160
265
|
end
|
|
161
266
|
|
|
162
267
|
# @rbs request: ACP::Types::PromptRequest
|
|
163
268
|
# @rbs &block: (ACP::Types::SessionUpdate::t) -> void
|
|
164
269
|
# @rbs return: ACP::Types::PromptResponse | ACP::RequestError
|
|
165
|
-
def
|
|
166
|
-
parse(ACP::Types::PromptResponse, stream(request.session_id, 'session/prompt', request.to_h, &))
|
|
270
|
+
def prompt_session(request, &)
|
|
271
|
+
parse(ACP::Types::PromptResponse, @streams.stream(request.session_id, 'session/prompt', request.to_h, &))
|
|
167
272
|
end
|
|
168
273
|
|
|
169
274
|
# @rbs request: ACP::Types::LoadSessionRequest
|
|
170
275
|
# @rbs &block: (ACP::Types::SessionUpdate::t) -> void
|
|
171
276
|
# @rbs return: ACP::Types::LoadSessionResponse | ACP::RequestError
|
|
172
|
-
def
|
|
173
|
-
parse(ACP::Types::LoadSessionResponse, stream(request.session_id, 'session/load', request.to_h, &))
|
|
277
|
+
def load_session(request, &)
|
|
278
|
+
parse(ACP::Types::LoadSessionResponse, @streams.stream(request.session_id, 'session/load', request.to_h, &))
|
|
174
279
|
end
|
|
175
280
|
|
|
176
281
|
# @rbs request: ACP::Types::ListSessionsRequest
|
|
177
282
|
# @rbs return: ACP::Types::ListSessionsResponse | ACP::RequestError
|
|
178
|
-
def
|
|
283
|
+
def list_sessions(request)
|
|
179
284
|
parse(ACP::Types::ListSessionsResponse, @transport.request('session/list', request.to_h))
|
|
180
285
|
end
|
|
181
286
|
|
|
182
|
-
# The agent does not replay history on resume, so unlike
|
|
287
|
+
# The agent does not replay history on resume, so unlike load_session there
|
|
183
288
|
# is nothing to stream and no block is taken.
|
|
184
289
|
#
|
|
185
290
|
# @rbs request: ACP::Types::ResumeSessionRequest
|
|
186
291
|
# @rbs return: ACP::Types::ResumeSessionResponse | ACP::RequestError
|
|
187
|
-
def
|
|
292
|
+
def resume_session(request)
|
|
188
293
|
parse(ACP::Types::ResumeSessionResponse, @transport.request('session/resume', request.to_h))
|
|
189
294
|
end
|
|
190
295
|
|
|
191
296
|
# @rbs request: ACP::Types::CloseSessionRequest
|
|
192
297
|
# @rbs return: ACP::Types::CloseSessionResponse | ACP::RequestError
|
|
193
|
-
def
|
|
298
|
+
def close_session(request)
|
|
194
299
|
parse(ACP::Types::CloseSessionResponse, @transport.request('session/close', request.to_h))
|
|
195
300
|
end
|
|
196
301
|
|
|
197
302
|
# @rbs request: ACP::Types::DeleteSessionRequest
|
|
198
303
|
# @rbs return: ACP::Types::DeleteSessionResponse | ACP::RequestError
|
|
199
|
-
def
|
|
304
|
+
def delete_session(request)
|
|
200
305
|
parse(ACP::Types::DeleteSessionResponse, @transport.request('session/delete', request.to_h))
|
|
201
306
|
end
|
|
202
307
|
|
|
308
|
+
# @rbs request: ACP::Types::Unstable::ForkSessionRequest
|
|
309
|
+
# @rbs return: (ACP::Types::Unstable::ForkSessionResponse | ACP::RequestError)
|
|
310
|
+
def fork_session(request)
|
|
311
|
+
parse(ACP::Types::Unstable::ForkSessionResponse, @transport.request('session/fork', request.to_h))
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# @rbs request: ACP::Types::Unstable::ListProvidersRequest
|
|
315
|
+
# @rbs return: (ACP::Types::Unstable::ListProvidersResponse | ACP::RequestError)
|
|
316
|
+
def list_providers(request)
|
|
317
|
+
parse(ACP::Types::Unstable::ListProvidersResponse, @transport.request('providers/list', request.to_h))
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# @rbs request: ACP::Types::Unstable::SetProviderRequest
|
|
321
|
+
# @rbs return: (ACP::Types::Unstable::SetProviderResponse | ACP::RequestError)
|
|
322
|
+
# The name is the wire-mandated providers/set, not an attribute writer.
|
|
323
|
+
def set_provider(request) # rubocop:disable Naming/AccessorMethodName
|
|
324
|
+
parse(ACP::Types::Unstable::SetProviderResponse, @transport.request('providers/set', request.to_h))
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# @rbs request: ACP::Types::Unstable::DisableProviderRequest
|
|
328
|
+
# @rbs return: (ACP::Types::Unstable::DisableProviderResponse | ACP::RequestError)
|
|
329
|
+
def disable_provider(request)
|
|
330
|
+
parse(ACP::Types::Unstable::DisableProviderResponse, @transport.request('providers/disable', request.to_h))
|
|
331
|
+
end
|
|
332
|
+
|
|
203
333
|
# The spec requires a pending session/request_permission to be answered with
|
|
204
|
-
# the cancelled outcome once the turn is cancelled
|
|
205
|
-
#
|
|
206
|
-
# so the wire order is cancel, then the cancelled replies.
|
|
334
|
+
# the cancelled outcome once the turn is cancelled. The notification goes
|
|
335
|
+
# first so the wire order is cancel, then the cancelled replies.
|
|
207
336
|
#
|
|
208
337
|
# @rbs notification: ACP::Types::CancelNotification
|
|
209
338
|
# @rbs return: void
|
|
210
|
-
def
|
|
339
|
+
def cancel_session(notification)
|
|
211
340
|
@transport.notify('session/cancel', notification.to_h)
|
|
212
|
-
|
|
213
|
-
pending&.each { |replies| replies << [:value, CANCELLED] }
|
|
341
|
+
@streams.cancel(notification.session_id)
|
|
214
342
|
end
|
|
215
343
|
|
|
216
344
|
# @rbs request: ACP::Types::SetSessionModeRequest
|
|
217
345
|
# @rbs return: ACP::Types::SetSessionModeResponse | ACP::RequestError
|
|
218
|
-
|
|
346
|
+
# The name is the wire-mandated session/set_mode, not an attribute writer.
|
|
347
|
+
def set_session_mode(request) # rubocop:disable Naming/AccessorMethodName
|
|
219
348
|
parse(ACP::Types::SetSessionModeResponse, @transport.request('session/set_mode', request.to_h))
|
|
220
349
|
end
|
|
221
350
|
|
|
@@ -224,7 +353,8 @@ class ACP::ClientConnection
|
|
|
224
353
|
#
|
|
225
354
|
# @rbs request: ACP::Types::SetSessionConfigOptionRequest
|
|
226
355
|
# @rbs return: ACP::Types::SetSessionConfigOptionResponse | ACP::RequestError
|
|
227
|
-
|
|
356
|
+
# The name is the wire-mandated session/set_config_option, not an attribute writer.
|
|
357
|
+
def set_session_config_option(request) # rubocop:disable Naming/AccessorMethodName
|
|
228
358
|
parse(ACP::Types::SetSessionConfigOptionResponse, @transport.request('session/set_config_option', request.to_h))
|
|
229
359
|
end
|
|
230
360
|
|
|
@@ -240,9 +370,7 @@ class ACP::ClientConnection
|
|
|
240
370
|
parse(ACP::Types::LogoutResponse, @transport.request('logout', request.to_h))
|
|
241
371
|
end
|
|
242
372
|
|
|
243
|
-
#
|
|
244
|
-
# returns the agent's reply as-is: extension methods have no schema to
|
|
245
|
-
# parse the result into.
|
|
373
|
+
# Extension methods have no schema to parse the result into.
|
|
246
374
|
#
|
|
247
375
|
# @rbs method: String
|
|
248
376
|
# @rbs params: untyped
|
|
@@ -252,6 +380,13 @@ class ACP::ClientConnection
|
|
|
252
380
|
@transport.request(method, params)
|
|
253
381
|
end
|
|
254
382
|
|
|
383
|
+
# Always false off a handler thread.
|
|
384
|
+
#
|
|
385
|
+
# @rbs return: bool
|
|
386
|
+
def cancelled?
|
|
387
|
+
!!@transport.cancellation&.cancelled?
|
|
388
|
+
end
|
|
389
|
+
|
|
255
390
|
# @rbs method: String
|
|
256
391
|
# @rbs params: untyped
|
|
257
392
|
# @rbs return: void
|
|
@@ -260,243 +395,101 @@ class ACP::ClientConnection
|
|
|
260
395
|
@transport.notify(method, params)
|
|
261
396
|
end
|
|
262
397
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
#
|
|
266
|
-
# @rbs result: (Hash[String, untyped] | ACP::RequestError)
|
|
267
|
-
# @rbs return: untyped
|
|
268
|
-
def parse(type, result)
|
|
269
|
-
return result if result.is_a?(ACP::RequestError)
|
|
270
|
-
|
|
271
|
-
type.from_h(result)
|
|
272
|
-
rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError
|
|
273
|
-
ACP::RequestError.invalid_response(result)
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
# The request waits on its own thread so the caller's thread can yield each
|
|
277
|
-
# update as it arrives. The reader thread queues an update before it settles
|
|
278
|
-
# a reply sent after it, so the queue closes only after the last one.
|
|
398
|
+
# Unlike the mcp/message serve route, this send is not gated: a notification
|
|
399
|
+
# has no reply to carry a refusal, and the agent drops what it did not
|
|
400
|
+
# advertise.
|
|
279
401
|
#
|
|
280
|
-
# @rbs
|
|
281
|
-
# @rbs method: String
|
|
282
|
-
# @rbs params: Hash[String, untyped]
|
|
283
|
-
# @rbs &block: (ACP::Types::SessionUpdate::t) -> void
|
|
284
|
-
# @rbs return: (Hash[String, untyped] | ACP::RequestError)
|
|
285
|
-
def stream(session_id, method, params)
|
|
286
|
-
queue = Thread::Queue.new
|
|
287
|
-
# The check and the insert share one lock acquisition so a concurrent
|
|
288
|
-
# stream cannot register between them.
|
|
289
|
-
claimed = @lock.synchronize do
|
|
290
|
-
if @streams.key?(session_id)
|
|
291
|
-
false
|
|
292
|
-
else
|
|
293
|
-
@streams[session_id] = queue
|
|
294
|
-
true
|
|
295
|
-
end
|
|
296
|
-
end
|
|
297
|
-
unless claimed
|
|
298
|
-
return ACP::RequestError.new(
|
|
299
|
-
code: ACP::RequestError::INVALID_REQUEST, message: "A stream is already open for session #{session_id}"
|
|
300
|
-
)
|
|
301
|
-
end
|
|
302
|
-
requester = Thread.new do
|
|
303
|
-
@transport.request(method, params)
|
|
304
|
-
ensure
|
|
305
|
-
@lock.synchronize { @streams.delete(session_id)&.close }
|
|
306
|
-
end
|
|
307
|
-
while (update = queue.pop)
|
|
308
|
-
yield update
|
|
309
|
-
end
|
|
310
|
-
requester.value
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
# Pushes under the lock so a stream cannot close its queue between the
|
|
314
|
-
# lookup and the push. A malformed notification has no reply to carry the
|
|
315
|
-
# failure.
|
|
316
|
-
#
|
|
317
|
-
# @rbs params: untyped
|
|
402
|
+
# @rbs notification: ACP::Types::Unstable::MessageMcpNotification
|
|
318
403
|
# @rbs return: void
|
|
319
|
-
def
|
|
320
|
-
notification
|
|
321
|
-
rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError => e
|
|
322
|
-
@logger.warn("dropped malformed session/update: #{e.class}: #{e.message}")
|
|
323
|
-
else
|
|
324
|
-
queue = @lock.synchronize { @streams[notification.session_id]&.push(notification.update) }
|
|
325
|
-
@updates.call(notification) unless queue
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
# The handler runs on its own thread so a cancelled request is answered
|
|
329
|
-
# without waiting for it, and whatever it returns after that is pushed to a
|
|
330
|
-
# queue nobody reads.
|
|
331
|
-
#
|
|
332
|
-
# @rbs params: untyped
|
|
333
|
-
# @rbs return: (ACP::Types::RequestPermissionResponse | ACP::RequestError)
|
|
334
|
-
def request_permission(params)
|
|
335
|
-
request = ACP::Types::RequestPermissionRequest.from_h(params)
|
|
336
|
-
rescue ACP::Types::ParseError => e
|
|
337
|
-
ACP::RequestError.invalid_params([e.message])
|
|
338
|
-
rescue KeyError, TypeError, NoMethodError
|
|
339
|
-
ACP::RequestError.invalid_params
|
|
340
|
-
else
|
|
341
|
-
replies = Thread::Queue.new
|
|
342
|
-
register_permission(request.session_id, replies)
|
|
343
|
-
Thread.new do
|
|
344
|
-
# A raised StandardError is not a returned ACP::RequestError, so the two
|
|
345
|
-
# need distinct shapes on the queue.
|
|
346
|
-
replies << begin
|
|
347
|
-
[:value, @permission.call(request)]
|
|
348
|
-
rescue StandardError => e
|
|
349
|
-
[:raised, e]
|
|
350
|
-
end
|
|
351
|
-
ensure
|
|
352
|
-
unregister_permission(request.session_id, replies)
|
|
353
|
-
end
|
|
354
|
-
settled = replies.pop
|
|
355
|
-
# Re-raised so the transport's invoke turns it into an error reply, as it
|
|
356
|
-
# would if the handler ran on the serve thread itself.
|
|
357
|
-
raise settled[1] if settled[0] == :raise
|
|
358
|
-
|
|
359
|
-
settled[1]
|
|
404
|
+
def mcp_message_notification(notification)
|
|
405
|
+
@transport.notify('mcp/message', notification.to_h)
|
|
360
406
|
end
|
|
361
407
|
|
|
362
|
-
|
|
363
|
-
# @rbs replies: Thread::Queue
|
|
364
|
-
# @rbs return: void
|
|
365
|
-
def register_permission(session_id, replies)
|
|
366
|
-
@lock.synchronize { (@pending_permissions[session_id] ||= []) << replies }
|
|
367
|
-
end
|
|
408
|
+
private
|
|
368
409
|
|
|
369
|
-
# @rbs
|
|
370
|
-
# @rbs
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
410
|
+
# @rbs route: ACP::ClientConnection::Route
|
|
411
|
+
# @rbs return: ^(untyped) -> (untyped | ACP::RequestError)
|
|
412
|
+
def serve(route)
|
|
413
|
+
lambda do |params|
|
|
414
|
+
next refuse(route) unless route.advertised.call(instance_variable_get(route.family))
|
|
415
|
+
|
|
416
|
+
request = route.type.from_h(params)
|
|
417
|
+
rescue ACP::Types::ParseError => e
|
|
418
|
+
ACP::RequestError.invalid_params([e.message])
|
|
419
|
+
rescue KeyError, TypeError, NoMethodError
|
|
420
|
+
ACP::RequestError.invalid_params
|
|
421
|
+
else
|
|
422
|
+
# Safe: connect refuses an advertised capability no handler serves, and
|
|
423
|
+
# the mcp route is only registered when one is injected.
|
|
424
|
+
instance_variable_get(route.handler).call(request)
|
|
379
425
|
end
|
|
380
426
|
end
|
|
381
427
|
|
|
382
|
-
#
|
|
383
|
-
#
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
# @rbs return: (ACP::Types::ReadTextFileResponse | ACP::RequestError)
|
|
387
|
-
def read_text_file(params)
|
|
388
|
-
return ACP::RequestError.unadvertised('fs.readTextFile') unless @fs_capabilities&.read_text_file
|
|
389
|
-
|
|
390
|
-
request = ACP::Types::ReadTextFileRequest.from_h(params)
|
|
391
|
-
rescue ACP::Types::ParseError => e
|
|
392
|
-
ACP::RequestError.invalid_params([e.message])
|
|
393
|
-
rescue KeyError, TypeError, NoMethodError
|
|
394
|
-
ACP::RequestError.invalid_params
|
|
395
|
-
else
|
|
396
|
-
# Safe: connect refuses an advertised capability no handler serves.
|
|
397
|
-
handler = @read_text_file #: ^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError)
|
|
398
|
-
handler.call(request)
|
|
428
|
+
# @rbs route: ACP::ClientConnection::Route
|
|
429
|
+
# @rbs return: ACP::RequestError
|
|
430
|
+
def refuse(route)
|
|
431
|
+
ACP::RequestError.public_send(route.refuse, route.key)
|
|
399
432
|
end
|
|
400
433
|
|
|
401
|
-
# @rbs
|
|
402
|
-
# @rbs
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
ACP::RequestError.invalid_params
|
|
411
|
-
else
|
|
412
|
-
# Safe: connect refuses an advertised capability no handler serves.
|
|
413
|
-
handler = @write_text_file #: ^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError)
|
|
414
|
-
handler.call(request)
|
|
415
|
-
end
|
|
416
|
-
|
|
417
|
-
# The terminal routes are registered at start, before connect records the
|
|
418
|
-
# advertised capability, so each one answers -32601 until then.
|
|
419
|
-
#
|
|
420
|
-
# @rbs params: untyped
|
|
421
|
-
# @rbs return: (ACP::Types::CreateTerminalResponse | ACP::RequestError)
|
|
422
|
-
def create_terminal(params)
|
|
423
|
-
return ACP::RequestError.unadvertised('terminal') unless @terminal
|
|
424
|
-
|
|
425
|
-
request = ACP::Types::CreateTerminalRequest.from_h(params)
|
|
426
|
-
rescue KeyError, TypeError, NoMethodError
|
|
427
|
-
ACP::RequestError.invalid_params
|
|
428
|
-
else
|
|
429
|
-
# Safe: connect refuses an advertised capability no handler serves.
|
|
430
|
-
handler = @create_terminal #: ^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError)
|
|
431
|
-
handler.call(request)
|
|
432
|
-
end
|
|
434
|
+
# @rbs name: String
|
|
435
|
+
# @rbs family: Symbol
|
|
436
|
+
# @rbs capabilities: untyped
|
|
437
|
+
# @rbs return: void
|
|
438
|
+
def check_served(name, family, capabilities)
|
|
439
|
+
unserved = SERVE.filter_map do |route|
|
|
440
|
+
next unless route.family == family
|
|
441
|
+
next if instance_variable_get(route.handler)
|
|
442
|
+
next unless route.advertised.call(capabilities)
|
|
433
443
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
return ACP::RequestError.unadvertised('terminal') unless @terminal
|
|
444
|
+
route.label
|
|
445
|
+
end
|
|
446
|
+
return if unserved.empty?
|
|
438
447
|
|
|
439
|
-
|
|
440
|
-
rescue KeyError, TypeError, NoMethodError
|
|
441
|
-
ACP::RequestError.invalid_params
|
|
442
|
-
else
|
|
443
|
-
# Safe: connect refuses an advertised capability no handler serves.
|
|
444
|
-
handler = @terminal_output #: ^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError)
|
|
445
|
-
handler.call(request)
|
|
448
|
+
raise ArgumentError, "initialize advertises #{name} methods no handler serves: #{unserved.join(', ')}"
|
|
446
449
|
end
|
|
447
450
|
|
|
448
|
-
# The
|
|
449
|
-
#
|
|
451
|
+
# The unstable types are referenced while the row is built, so the route can
|
|
452
|
+
# only be served when the caller opted in with `require 'acp/types/unstable'`.
|
|
453
|
+
# It is registered only when a handler serves it: unlike the fs and terminal
|
|
454
|
+
# gates, the mcp gate reads the agent's advertisement, so nothing forces an
|
|
455
|
+
# advertised request to have a handler.
|
|
450
456
|
#
|
|
451
|
-
# @rbs
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
return ACP::
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
# @rbs params: untyped
|
|
466
|
-
# @rbs return: (ACP::Types::KillTerminalResponse | ACP::RequestError)
|
|
467
|
-
def kill_terminal(params)
|
|
468
|
-
return ACP::RequestError.unadvertised('terminal') unless @terminal
|
|
469
|
-
|
|
470
|
-
request = ACP::Types::KillTerminalRequest.from_h(params)
|
|
471
|
-
rescue KeyError, TypeError, NoMethodError
|
|
472
|
-
ACP::RequestError.invalid_params
|
|
473
|
-
else
|
|
474
|
-
# Safe: connect refuses an advertised capability no handler serves.
|
|
475
|
-
handler = @kill_terminal #: ^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError)
|
|
476
|
-
handler.call(request)
|
|
457
|
+
# @rbs return: Hash[String, ^(untyped) -> (untyped | ACP::RequestError)]
|
|
458
|
+
def mcp_requests
|
|
459
|
+
return {} unless @mcp_message
|
|
460
|
+
return {} unless ACP::Types.unstable_enabled?
|
|
461
|
+
|
|
462
|
+
route = Route.new(
|
|
463
|
+
wire: 'mcp/message',
|
|
464
|
+
type: ACP::Types::Unstable::MessageMcpRequest,
|
|
465
|
+
handler: :@mcp_message,
|
|
466
|
+
family: :@mcp_advertised,
|
|
467
|
+
refuse: :unadvertised_agent,
|
|
468
|
+
key: 'mcpCapabilities.acp'
|
|
469
|
+
)
|
|
470
|
+
{ 'mcp/message' => serve(route) }
|
|
477
471
|
end
|
|
478
472
|
|
|
479
|
-
# @rbs
|
|
480
|
-
# @rbs
|
|
481
|
-
|
|
482
|
-
|
|
473
|
+
# @rbs type: ACP::Transport::_Parser
|
|
474
|
+
# @rbs result: (Hash[String, untyped] | ACP::RequestError)
|
|
475
|
+
# @rbs return: untyped
|
|
476
|
+
def parse(type, result)
|
|
477
|
+
return result if result.is_a?(ACP::RequestError)
|
|
483
478
|
|
|
484
|
-
|
|
485
|
-
rescue KeyError, TypeError, NoMethodError
|
|
486
|
-
ACP::RequestError.
|
|
487
|
-
else
|
|
488
|
-
# Safe: connect refuses an advertised capability no handler serves.
|
|
489
|
-
handler = @release_terminal #: ^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
|
|
490
|
-
handler.call(request)
|
|
479
|
+
type.from_h(result)
|
|
480
|
+
rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError
|
|
481
|
+
ACP::RequestError.invalid_response(result)
|
|
491
482
|
end
|
|
492
483
|
|
|
493
|
-
# The
|
|
484
|
+
# The route is registered at start, before connect records the advertised
|
|
494
485
|
# capabilities, so a request for an unadvertised mode answers -32602.
|
|
495
486
|
#
|
|
496
487
|
# @rbs params: untyped
|
|
497
488
|
# @rbs return: (ACP::Types::CreateElicitationResponse | ACP::RequestError)
|
|
498
489
|
def create_elicitation(params)
|
|
499
490
|
request = ACP::Types::CreateElicitationRequest.from_h(params)
|
|
491
|
+
rescue ACP::Types::ParseError => e
|
|
492
|
+
ACP::RequestError.invalid_params([e.message])
|
|
500
493
|
rescue KeyError, TypeError, NoMethodError
|
|
501
494
|
ACP::RequestError.invalid_params
|
|
502
495
|
else
|
|
@@ -506,7 +499,7 @@ class ACP::ClientConnection
|
|
|
506
499
|
when ACP::Types::CreateElicitationRequest::Mode::Url
|
|
507
500
|
return ACP::RequestError.unadvertised_mode('elicitation.url') unless @elicitation_capabilities&.url
|
|
508
501
|
|
|
509
|
-
register_elicitation(request.mode.elicitation_id)
|
|
502
|
+
@streams.register_elicitation(request.mode.elicitation_id)
|
|
510
503
|
else
|
|
511
504
|
return ACP::RequestError.unadvertised_mode("elicitation.#{request.mode['mode']}")
|
|
512
505
|
end
|
|
@@ -516,57 +509,6 @@ class ACP::ClientConnection
|
|
|
516
509
|
handler.call(request)
|
|
517
510
|
end
|
|
518
511
|
|
|
519
|
-
# The spec requires a completion for an unknown or already-completed
|
|
520
|
-
# elicitation id to be ignored, so only an id registered by a served
|
|
521
|
-
# url-mode request reaches the handler. It runs on the transport's reader
|
|
522
|
-
# thread, so it must return quickly.
|
|
523
|
-
#
|
|
524
|
-
# @rbs params: untyped
|
|
525
|
-
# @rbs return: void
|
|
526
|
-
def complete_elicitation(params)
|
|
527
|
-
notification = ACP::Types::CompleteElicitationNotification.from_h(params)
|
|
528
|
-
rescue KeyError, TypeError, NoMethodError => e
|
|
529
|
-
@logger.warn("dropped malformed elicitation/complete: #{e.class}: #{e.message}")
|
|
530
|
-
else
|
|
531
|
-
known = @lock.synchronize { @outstanding_elicitations.delete(notification.elicitation_id) }
|
|
532
|
-
@complete_elicitation&.call(notification) if known
|
|
533
|
-
end
|
|
534
|
-
|
|
535
|
-
# @rbs elicitation_id: String
|
|
536
|
-
# @rbs return: void
|
|
537
|
-
def register_elicitation(elicitation_id)
|
|
538
|
-
@lock.synchronize { @outstanding_elicitations[elicitation_id] = true }
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
# The fs capabilities initialize advertises that no injected handler serves.
|
|
542
|
-
#
|
|
543
|
-
# @rbs capabilities: ACP::Types::FileSystemCapabilities?
|
|
544
|
-
# @rbs return: Array[String]
|
|
545
|
-
def unserved_fs_methods(capabilities)
|
|
546
|
-
{
|
|
547
|
-
'fs.readTextFile' => capabilities&.read_text_file && !@read_text_file,
|
|
548
|
-
'fs.writeTextFile' => capabilities&.write_text_file && !@write_text_file
|
|
549
|
-
}.select { |_, unserved| unserved }.keys
|
|
550
|
-
end
|
|
551
|
-
|
|
552
|
-
# The terminal capabilities initialize advertises that no injected handler
|
|
553
|
-
# serves.
|
|
554
|
-
#
|
|
555
|
-
# @rbs capabilities: bool?
|
|
556
|
-
# @rbs return: Array[String]
|
|
557
|
-
def unserved_terminal_methods(capabilities)
|
|
558
|
-
{
|
|
559
|
-
'terminal.create' => capabilities && !@create_terminal,
|
|
560
|
-
'terminal.output' => capabilities && !@terminal_output,
|
|
561
|
-
'terminal.wait_for_exit' => capabilities && !@wait_for_terminal_exit,
|
|
562
|
-
'terminal.kill' => capabilities && !@kill_terminal,
|
|
563
|
-
'terminal.release' => capabilities && !@release_terminal
|
|
564
|
-
}.select { |_, unserved| unserved }.keys
|
|
565
|
-
end
|
|
566
|
-
|
|
567
|
-
# The elicitation capabilities initialize advertises that no injected
|
|
568
|
-
# handler serves.
|
|
569
|
-
#
|
|
570
512
|
# @rbs capabilities: ACP::Types::ElicitationCapabilities?
|
|
571
513
|
# @rbs return: Array[String]
|
|
572
514
|
def unserved_elicitation_modes(capabilities)
|