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.
Files changed (174) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/README.md +20 -8
  4. data/lib/acp/agent_connection/client.rb +31 -13
  5. data/lib/acp/agent_connection/optional_method/spec.rb +42 -0
  6. data/lib/acp/agent_connection/optional_method.rb +36 -0
  7. data/lib/acp/agent_connection.rb +67 -20
  8. data/lib/acp/agent_process.rb +0 -13
  9. data/lib/acp/client_connection/streams.rb +183 -0
  10. data/lib/acp/client_connection.rb +280 -338
  11. data/lib/acp/protocol.rb +33 -0
  12. data/lib/acp/request_error.rb +13 -13
  13. data/lib/acp/transport/cancellation.rb +13 -3
  14. data/lib/acp/transport/reply.rb +0 -2
  15. data/lib/acp/transport/stderr_logger.rb +1 -2
  16. data/lib/acp/transport/stdio.rb +25 -26
  17. data/lib/acp/types/audio_content.rb +3 -46
  18. data/lib/acp/types/auth_method_terminal.rb +3 -59
  19. data/lib/acp/types/available_commands_update.rb +3 -32
  20. data/lib/acp/types/boolean_property_schema.rb +3 -41
  21. data/lib/acp/types/check.rb +210 -0
  22. data/lib/acp/types/config_option_update.rb +3 -32
  23. data/lib/acp/types/content.rb +3 -29
  24. data/lib/acp/types/{elicitation_url_mode → create_elicitation_request/mode/form}/scope.rb +1 -1
  25. data/lib/acp/types/create_elicitation_request/mode/form.rb +3 -3
  26. data/lib/acp/types/{elicitation_form_mode → create_elicitation_request/mode/url}/scope.rb +1 -1
  27. data/lib/acp/types/create_elicitation_request/mode/url.rb +3 -3
  28. data/lib/acp/types/current_mode_update.rb +3 -29
  29. data/lib/acp/types/diff.rb +3 -41
  30. data/lib/acp/types/elicitation_accept_action.rb +3 -26
  31. data/lib/acp/types/elicitation_form_mode.rb +3 -33
  32. data/lib/acp/types/elicitation_url_mode.rb +3 -35
  33. data/lib/acp/types/embedded_resource.rb +3 -44
  34. data/lib/acp/types/image_content.rb +3 -58
  35. data/lib/acp/types/integer_property_schema.rb +3 -60
  36. data/lib/acp/types/mcp_server_http.rb +3 -44
  37. data/lib/acp/types/mcp_server_sse.rb +3 -44
  38. data/lib/acp/types/multi_select_property_schema.rb +3 -70
  39. data/lib/acp/types/number_property_schema.rb +3 -60
  40. data/lib/acp/types/parse_error.rb +37 -0
  41. data/lib/acp/types/plan.rb +3 -29
  42. data/lib/acp/types/resource_link.rb +3 -79
  43. data/lib/acp/types/selected_permission_outcome.rb +3 -26
  44. data/lib/acp/types/session_config_boolean.rb +3 -21
  45. data/lib/acp/types/session_config_select.rb +3 -33
  46. data/lib/acp/types/session_info_update.rb +3 -42
  47. data/lib/acp/types/string_multi_select_items.rb +3 -29
  48. data/lib/acp/types/string_property_schema.rb +3 -105
  49. data/lib/acp/types/terminal.rb +3 -29
  50. data/lib/acp/types/text_content.rb +3 -40
  51. data/lib/acp/types/tool_call.rb +3 -115
  52. data/lib/acp/types/unstable/compaction_summary_chunk.rb +3 -35
  53. data/lib/acp/types/unstable/compaction_update.rb +3 -56
  54. data/lib/acp/types/unstable/idle_state_update.rb +3 -40
  55. data/lib/acp/types/unstable/mcp_server_acp.rb +3 -35
  56. data/lib/acp/types/unstable/message_mcp_response/result.rb +1 -1
  57. data/lib/acp/types/unstable/nes_edit_suggestion.rb +3 -61
  58. data/lib/acp/types/unstable/nes_jump_suggestion.rb +3 -45
  59. data/lib/acp/types/unstable/nes_rename_suggestion.rb +3 -51
  60. data/lib/acp/types/unstable/nes_search_and_replace_suggestion.rb +3 -60
  61. data/lib/acp/types/unstable/notice.rb +3 -41
  62. data/lib/acp/types/unstable/plan_file.rb +3 -35
  63. data/lib/acp/types/unstable/plan_items.rb +3 -38
  64. data/lib/acp/types/unstable/plan_markdown.rb +3 -35
  65. data/lib/acp/types/unstable/plan_removed.rb +3 -26
  66. data/lib/acp/types/unstable/plan_update.rb +3 -33
  67. data/lib/acp/types/unstable/requires_action_state_update.rb +3 -21
  68. data/lib/acp/types/unstable/running_state_update.rb +3 -21
  69. data/lib/acp/types/unstable/session_message.rb +3 -56
  70. data/lib/acp/types/unstable/session_message_chunk.rb +3 -53
  71. data/lib/acp/types/unstable/subagent_update.rb +3 -65
  72. data/lib/acp/types/unstable/unknown_state_update.rb +3 -21
  73. data/lib/acp/types/unstable.rb +20 -20
  74. data/lib/acp/types/usage_update.rb +3 -41
  75. data/lib/acp/types.rb +7 -246
  76. data/lib/acp/version.rb +1 -1
  77. data/sig/generated/acp/agent_connection/client.rbs +25 -15
  78. data/sig/generated/acp/agent_connection/optional_method/spec.rbs +32 -0
  79. data/sig/generated/acp/agent_connection/optional_method.rbs +3 -0
  80. data/sig/generated/acp/agent_connection.rbs +34 -13
  81. data/sig/generated/acp/agent_process.rbs +0 -13
  82. data/sig/generated/acp/client_connection/streams.rbs +94 -0
  83. data/sig/generated/acp/client_connection.rbs +128 -131
  84. data/sig/generated/acp/protocol.rbs +9 -0
  85. data/sig/generated/acp/request_error.rbs +11 -13
  86. data/sig/generated/acp/transport/cancellation.rbs +9 -3
  87. data/sig/generated/acp/transport/reply.rbs +0 -2
  88. data/sig/generated/acp/transport/stderr_logger.rbs +1 -2
  89. data/sig/generated/acp/transport/stdio.rbs +14 -22
  90. data/sig/generated/acp/types/audio_content.rbs +1 -27
  91. data/sig/generated/acp/types/auth_method_terminal.rbs +1 -35
  92. data/sig/generated/acp/types/available_commands_update.rbs +1 -19
  93. data/sig/generated/acp/types/boolean_property_schema.rbs +1 -27
  94. data/sig/generated/acp/types/check.rbs +115 -0
  95. data/sig/generated/acp/types/config_option_update.rbs +1 -19
  96. data/sig/generated/acp/types/content.rbs +1 -19
  97. data/sig/generated/acp/types/{elicitation_url_mode → create_elicitation_request/mode/form}/scope.rbs +2 -2
  98. data/sig/generated/acp/types/create_elicitation_request/mode/form.rbs +3 -3
  99. data/sig/generated/acp/types/{elicitation_form_mode → create_elicitation_request/mode/url}/scope.rbs +2 -2
  100. data/sig/generated/acp/types/create_elicitation_request/mode/url.rbs +3 -3
  101. data/sig/generated/acp/types/current_mode_update.rbs +1 -19
  102. data/sig/generated/acp/types/diff.rbs +1 -27
  103. data/sig/generated/acp/types/elicitation_accept_action.rbs +1 -15
  104. data/sig/generated/acp/types/elicitation_form_mode.rbs +1 -19
  105. data/sig/generated/acp/types/elicitation_url_mode.rbs +1 -23
  106. data/sig/generated/acp/types/embedded_resource.rbs +1 -23
  107. data/sig/generated/acp/types/image_content.rbs +1 -31
  108. data/sig/generated/acp/types/integer_property_schema.rbs +1 -35
  109. data/sig/generated/acp/types/mcp_server_http.rbs +1 -27
  110. data/sig/generated/acp/types/mcp_server_sse.rbs +1 -27
  111. data/sig/generated/acp/types/multi_select_property_schema.rbs +1 -39
  112. data/sig/generated/acp/types/number_property_schema.rbs +1 -35
  113. data/sig/generated/acp/types/parse_error.rbs +22 -0
  114. data/sig/generated/acp/types/plan.rbs +1 -19
  115. data/sig/generated/acp/types/resource_link.rbs +1 -43
  116. data/sig/generated/acp/types/selected_permission_outcome.rbs +1 -19
  117. data/sig/generated/acp/types/session_config_boolean.rbs +1 -15
  118. data/sig/generated/acp/types/session_config_select.rbs +1 -19
  119. data/sig/generated/acp/types/session_info_update.rbs +1 -23
  120. data/sig/generated/acp/types/string_multi_select_items.rbs +1 -19
  121. data/sig/generated/acp/types/string_property_schema.rbs +1 -51
  122. data/sig/generated/acp/types/terminal.rbs +1 -19
  123. data/sig/generated/acp/types/text_content.rbs +1 -23
  124. data/sig/generated/acp/types/tool_call.rbs +1 -51
  125. data/sig/generated/acp/types/unstable/compaction_summary_chunk.rbs +1 -23
  126. data/sig/generated/acp/types/unstable/compaction_update.rbs +1 -31
  127. data/sig/generated/acp/types/unstable/idle_state_update.rbs +1 -23
  128. data/sig/generated/acp/types/unstable/mcp_server_acp.rbs +1 -23
  129. data/sig/generated/acp/types/unstable/nes_edit_suggestion.rbs +1 -31
  130. data/sig/generated/acp/types/unstable/nes_jump_suggestion.rbs +1 -27
  131. data/sig/generated/acp/types/unstable/nes_rename_suggestion.rbs +1 -31
  132. data/sig/generated/acp/types/unstable/nes_search_and_replace_suggestion.rbs +1 -35
  133. data/sig/generated/acp/types/unstable/notice.rbs +1 -27
  134. data/sig/generated/acp/types/unstable/plan_file.rbs +1 -23
  135. data/sig/generated/acp/types/unstable/plan_items.rbs +1 -23
  136. data/sig/generated/acp/types/unstable/plan_markdown.rbs +1 -23
  137. data/sig/generated/acp/types/unstable/plan_removed.rbs +1 -19
  138. data/sig/generated/acp/types/unstable/plan_update.rbs +1 -19
  139. data/sig/generated/acp/types/unstable/requires_action_state_update.rbs +1 -15
  140. data/sig/generated/acp/types/unstable/running_state_update.rbs +1 -15
  141. data/sig/generated/acp/types/unstable/session_message.rbs +1 -31
  142. data/sig/generated/acp/types/unstable/session_message_chunk.rbs +1 -31
  143. data/sig/generated/acp/types/unstable/subagent_update.rbs +1 -35
  144. data/sig/generated/acp/types/unstable/unknown_state_update.rbs +1 -15
  145. data/sig/generated/acp/types/usage_update.rbs +1 -27
  146. data/sig/generated/acp/types.rbs +6 -143
  147. data/sig/manual/acp/agent_connection.rbs +28 -28
  148. data/sig/manual/acp/client_connection.rbs +7 -2
  149. data/sig/manual/acp/{agent_connection/optional_method/logout.rbs → protocol.rbs} +3 -3
  150. data/sig/manual/acp/transport/stdio.rbs +0 -2
  151. data/sig/manual/acp/transport.rbs +25 -2
  152. metadata +17 -29
  153. data/lib/acp/agent_connection/optional_method/authenticate.rb +0 -21
  154. data/lib/acp/agent_connection/optional_method/close_session.rb +0 -21
  155. data/lib/acp/agent_connection/optional_method/delete_session.rb +0 -21
  156. data/lib/acp/agent_connection/optional_method/list_sessions.rb +0 -21
  157. data/lib/acp/agent_connection/optional_method/load_session.rb +0 -21
  158. data/lib/acp/agent_connection/optional_method/logout.rb +0 -21
  159. data/lib/acp/agent_connection/optional_method/resume_session.rb +0 -21
  160. data/lib/acp/types/content_chunk.rb +0 -40
  161. data/sig/generated/acp/agent_connection/optional_method/authenticate.rbs +0 -13
  162. data/sig/generated/acp/agent_connection/optional_method/close_session.rbs +0 -13
  163. data/sig/generated/acp/agent_connection/optional_method/delete_session.rbs +0 -13
  164. data/sig/generated/acp/agent_connection/optional_method/list_sessions.rbs +0 -13
  165. data/sig/generated/acp/agent_connection/optional_method/load_session.rbs +0 -13
  166. data/sig/generated/acp/agent_connection/optional_method/logout.rbs +0 -13
  167. data/sig/generated/acp/agent_connection/optional_method/resume_session.rbs +0 -13
  168. data/sig/generated/acp/types/content_chunk.rbs +0 -25
  169. data/sig/manual/acp/agent_connection/optional_method/authenticate.rbs +0 -6
  170. data/sig/manual/acp/agent_connection/optional_method/close_session.rbs +0 -6
  171. data/sig/manual/acp/agent_connection/optional_method/delete_session.rbs +0 -6
  172. data/sig/manual/acp/agent_connection/optional_method/list_sessions.rbs +0 -6
  173. data/sig/manual/acp/agent_connection/optional_method/load_session.rbs +0 -6
  174. 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::AgentConnection::_Transport
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 @complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?
17
- # @rbs @updates: ACP::ClientConnection::_UpdateHandler
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 @lock: Thread::Mutex
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
- CANCELLED = ACP::Types::RequestPermissionResponse.new(
34
- outcome: ACP::Types::RequestPermissionOutcome::Cancelled.new
35
- ) #: ACP::Types::RequestPermissionResponse
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::AgentConnection::_Transport
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
- @complete_elicitation = complete_elicitation
83
- @updates = updates
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
- @lock = Mutex.new
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: { 'session/update' => method(:dispatch),
113
- 'elicitation/complete' => method(:complete_elicitation) }.merge(@extension_notifications)
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&.fs
124
- unserved = unserved_fs_methods(capabilities)
125
- unless unserved.empty?
126
- raise ArgumentError, "initialize advertises fs methods no handler serves: #{unserved.join(', ')}"
127
- end
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
- terminal = request.client_capabilities&.terminal
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 = capabilities
242
+ @fs_capabilities = fs
142
243
  @terminal = terminal
143
244
  @elicitation_capabilities = elicitation
144
- parse(ACP::Types::InitializeResponse, @transport.request('initialize', request.to_h)).then do |response|
145
- next response if response.is_a?(ACP::RequestError)
146
-
147
- next response if response.protocol_version == PROTOCOL_VERSION
148
-
149
- ACP::RequestError.unsupported_protocol_version(
150
- requested: request.protocol_version,
151
- returned: response.protocol_version
152
- )
153
- end
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 session_new(request)
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 session_prompt(request, &)
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 session_load(request, &)
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 session_list(request)
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 session_load there
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 session_resume(request)
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 session_close(request)
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 session_delete(request)
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, so every queue registered
205
- # for the session gets one before this returns. The notification goes first
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 session_cancel(notification)
339
+ def cancel_session(notification)
211
340
  @transport.notify('session/cancel', notification.to_h)
212
- pending = @lock.synchronize { @pending_permissions.delete(notification.session_id) }
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
- def session_set_mode(request)
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
- def session_set_config_option(request)
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
- # Sends an extension request, keyed by its raw `_`-prefixed wire name, and
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
- private
264
-
265
- # @rbs type: ACP::AgentConnection::_Parser
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 session_id: String
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 dispatch(params)
320
- notification = ACP::Types::SessionNotification.from_h(params)
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
- # @rbs session_id: String
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 session_id: String
370
- # @rbs replies: Thread::Queue
371
- # @rbs return: void
372
- def unregister_permission(session_id, replies)
373
- @lock.synchronize do
374
- pending = @pending_permissions[session_id]
375
- if pending
376
- pending.delete(replies)
377
- @pending_permissions.delete(session_id) if pending.empty?
378
- end
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
- # The routes are registered at start, before connect records the advertised
383
- # capabilities, so each one answers -32601 until then.
384
- #
385
- # @rbs params: untyped
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 params: untyped
402
- # @rbs return: (ACP::Types::WriteTextFileResponse | ACP::RequestError)
403
- def write_text_file(params)
404
- return ACP::RequestError.unadvertised('fs.writeTextFile') unless @fs_capabilities&.write_text_file
405
-
406
- request = ACP::Types::WriteTextFileRequest.from_h(params)
407
- rescue ACP::Types::ParseError => e
408
- ACP::RequestError.invalid_params([e.message])
409
- rescue KeyError, TypeError, NoMethodError
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
- # @rbs params: untyped
435
- # @rbs return: (ACP::Types::TerminalOutputResponse | ACP::RequestError)
436
- def terminal_output(params)
437
- return ACP::RequestError.unadvertised('terminal') unless @terminal
444
+ route.label
445
+ end
446
+ return if unserved.empty?
438
447
 
439
- request = ACP::Types::TerminalOutputRequest.from_h(params)
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 handler may block for as long as the command runs, so the transport
449
- # must serve each request on its own thread for this not to hold up others.
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 params: untyped
452
- # @rbs return: (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError)
453
- def wait_for_terminal_exit(params)
454
- return ACP::RequestError.unadvertised('terminal') unless @terminal
455
-
456
- request = ACP::Types::WaitForTerminalExitRequest.from_h(params)
457
- rescue KeyError, TypeError, NoMethodError
458
- ACP::RequestError.invalid_params
459
- else
460
- # Safe: connect refuses an advertised capability no handler serves.
461
- handler = @wait_for_terminal_exit #: ^(ACP::Types::WaitForTerminalExitRequest) -> (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError)
462
- handler.call(request)
463
- end
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 params: untyped
480
- # @rbs return: (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
481
- def release_terminal(params)
482
- return ACP::RequestError.unadvertised('terminal') unless @terminal
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
- request = ACP::Types::ReleaseTerminalRequest.from_h(params)
485
- rescue KeyError, TypeError, NoMethodError
486
- ACP::RequestError.invalid_params
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 routes are registered at start, before connect records the advertised
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)