acp-sdk 0.10.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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/README.md +15 -5
  4. data/lib/acp/agent_connection/client.rb +7 -19
  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 +17 -40
  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 +227 -384
  11. data/lib/acp/protocol.rb +33 -0
  12. data/lib/acp/request_error.rb +7 -17
  13. data/lib/acp/transport/cancellation.rb +0 -7
  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 +16 -22
  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/nes_edit_suggestion.rb +3 -61
  57. data/lib/acp/types/unstable/nes_jump_suggestion.rb +3 -45
  58. data/lib/acp/types/unstable/nes_rename_suggestion.rb +3 -51
  59. data/lib/acp/types/unstable/nes_search_and_replace_suggestion.rb +3 -60
  60. data/lib/acp/types/unstable/notice.rb +3 -41
  61. data/lib/acp/types/unstable/plan_file.rb +3 -35
  62. data/lib/acp/types/unstable/plan_items.rb +3 -38
  63. data/lib/acp/types/unstable/plan_markdown.rb +3 -35
  64. data/lib/acp/types/unstable/plan_removed.rb +3 -26
  65. data/lib/acp/types/unstable/plan_update.rb +3 -33
  66. data/lib/acp/types/unstable/requires_action_state_update.rb +3 -21
  67. data/lib/acp/types/unstable/running_state_update.rb +3 -21
  68. data/lib/acp/types/unstable/session_message.rb +3 -56
  69. data/lib/acp/types/unstable/session_message_chunk.rb +3 -53
  70. data/lib/acp/types/unstable/subagent_update.rb +3 -65
  71. data/lib/acp/types/unstable/unknown_state_update.rb +3 -21
  72. data/lib/acp/types/unstable.rb +20 -20
  73. data/lib/acp/types/usage_update.rb +3 -41
  74. data/lib/acp/types.rb +7 -246
  75. data/lib/acp/version.rb +1 -1
  76. data/sig/generated/acp/agent_connection/client.rbs +10 -19
  77. data/sig/generated/acp/agent_connection/optional_method/spec.rbs +32 -0
  78. data/sig/generated/acp/agent_connection/optional_method.rbs +3 -0
  79. data/sig/generated/acp/agent_connection.rbs +16 -24
  80. data/sig/generated/acp/agent_process.rbs +0 -13
  81. data/sig/generated/acp/client_connection/streams.rbs +94 -0
  82. data/sig/generated/acp/client_connection.rbs +100 -159
  83. data/sig/generated/acp/protocol.rbs +9 -0
  84. data/sig/generated/acp/request_error.rbs +7 -17
  85. data/sig/generated/acp/transport/cancellation.rbs +0 -7
  86. data/sig/generated/acp/transport/reply.rbs +0 -2
  87. data/sig/generated/acp/transport/stderr_logger.rbs +1 -2
  88. data/sig/generated/acp/transport/stdio.rbs +9 -21
  89. data/sig/generated/acp/types/audio_content.rbs +1 -27
  90. data/sig/generated/acp/types/auth_method_terminal.rbs +1 -35
  91. data/sig/generated/acp/types/available_commands_update.rbs +1 -19
  92. data/sig/generated/acp/types/boolean_property_schema.rbs +1 -27
  93. data/sig/generated/acp/types/check.rbs +115 -0
  94. data/sig/generated/acp/types/config_option_update.rbs +1 -19
  95. data/sig/generated/acp/types/content.rbs +1 -19
  96. data/sig/generated/acp/types/{elicitation_url_mode → create_elicitation_request/mode/form}/scope.rbs +2 -2
  97. data/sig/generated/acp/types/create_elicitation_request/mode/form.rbs +3 -3
  98. data/sig/generated/acp/types/{elicitation_form_mode → create_elicitation_request/mode/url}/scope.rbs +2 -2
  99. data/sig/generated/acp/types/create_elicitation_request/mode/url.rbs +3 -3
  100. data/sig/generated/acp/types/current_mode_update.rbs +1 -19
  101. data/sig/generated/acp/types/diff.rbs +1 -27
  102. data/sig/generated/acp/types/elicitation_accept_action.rbs +1 -15
  103. data/sig/generated/acp/types/elicitation_form_mode.rbs +1 -19
  104. data/sig/generated/acp/types/elicitation_url_mode.rbs +1 -23
  105. data/sig/generated/acp/types/embedded_resource.rbs +1 -23
  106. data/sig/generated/acp/types/image_content.rbs +1 -31
  107. data/sig/generated/acp/types/integer_property_schema.rbs +1 -35
  108. data/sig/generated/acp/types/mcp_server_http.rbs +1 -27
  109. data/sig/generated/acp/types/mcp_server_sse.rbs +1 -27
  110. data/sig/generated/acp/types/multi_select_property_schema.rbs +1 -39
  111. data/sig/generated/acp/types/number_property_schema.rbs +1 -35
  112. data/sig/generated/acp/types/parse_error.rbs +22 -0
  113. data/sig/generated/acp/types/plan.rbs +1 -19
  114. data/sig/generated/acp/types/resource_link.rbs +1 -43
  115. data/sig/generated/acp/types/selected_permission_outcome.rbs +1 -19
  116. data/sig/generated/acp/types/session_config_boolean.rbs +1 -15
  117. data/sig/generated/acp/types/session_config_select.rbs +1 -19
  118. data/sig/generated/acp/types/session_info_update.rbs +1 -23
  119. data/sig/generated/acp/types/string_multi_select_items.rbs +1 -19
  120. data/sig/generated/acp/types/string_property_schema.rbs +1 -51
  121. data/sig/generated/acp/types/terminal.rbs +1 -19
  122. data/sig/generated/acp/types/text_content.rbs +1 -23
  123. data/sig/generated/acp/types/tool_call.rbs +1 -51
  124. data/sig/generated/acp/types/unstable/compaction_summary_chunk.rbs +1 -23
  125. data/sig/generated/acp/types/unstable/compaction_update.rbs +1 -31
  126. data/sig/generated/acp/types/unstable/idle_state_update.rbs +1 -23
  127. data/sig/generated/acp/types/unstable/mcp_server_acp.rbs +1 -23
  128. data/sig/generated/acp/types/unstable/nes_edit_suggestion.rbs +1 -31
  129. data/sig/generated/acp/types/unstable/nes_jump_suggestion.rbs +1 -27
  130. data/sig/generated/acp/types/unstable/nes_rename_suggestion.rbs +1 -31
  131. data/sig/generated/acp/types/unstable/nes_search_and_replace_suggestion.rbs +1 -35
  132. data/sig/generated/acp/types/unstable/notice.rbs +1 -27
  133. data/sig/generated/acp/types/unstable/plan_file.rbs +1 -23
  134. data/sig/generated/acp/types/unstable/plan_items.rbs +1 -23
  135. data/sig/generated/acp/types/unstable/plan_markdown.rbs +1 -23
  136. data/sig/generated/acp/types/unstable/plan_removed.rbs +1 -19
  137. data/sig/generated/acp/types/unstable/plan_update.rbs +1 -19
  138. data/sig/generated/acp/types/unstable/requires_action_state_update.rbs +1 -15
  139. data/sig/generated/acp/types/unstable/running_state_update.rbs +1 -15
  140. data/sig/generated/acp/types/unstable/session_message.rbs +1 -31
  141. data/sig/generated/acp/types/unstable/session_message_chunk.rbs +1 -31
  142. data/sig/generated/acp/types/unstable/subagent_update.rbs +1 -35
  143. data/sig/generated/acp/types/unstable/unknown_state_update.rbs +1 -15
  144. data/sig/generated/acp/types/usage_update.rbs +1 -27
  145. data/sig/generated/acp/types.rbs +6 -143
  146. data/sig/manual/acp/agent_connection.rbs +0 -30
  147. data/sig/manual/acp/client_connection.rbs +7 -2
  148. data/sig/manual/acp/{agent_connection/optional_method/logout.rbs → protocol.rbs} +3 -3
  149. data/sig/manual/acp/transport/stdio.rbs +0 -2
  150. data/sig/manual/acp/transport.rbs +25 -2
  151. metadata +16 -40
  152. data/lib/acp/agent_connection/optional_method/authenticate.rb +0 -21
  153. data/lib/acp/agent_connection/optional_method/close_session.rb +0 -21
  154. data/lib/acp/agent_connection/optional_method/delete_session.rb +0 -21
  155. data/lib/acp/agent_connection/optional_method/disable_provider.rb +0 -23
  156. data/lib/acp/agent_connection/optional_method/fork_session.rb +0 -23
  157. data/lib/acp/agent_connection/optional_method/list_providers.rb +0 -23
  158. data/lib/acp/agent_connection/optional_method/list_sessions.rb +0 -21
  159. data/lib/acp/agent_connection/optional_method/load_session.rb +0 -21
  160. data/lib/acp/agent_connection/optional_method/logout.rb +0 -21
  161. data/lib/acp/agent_connection/optional_method/resume_session.rb +0 -21
  162. data/lib/acp/agent_connection/optional_method/set_provider.rb +0 -23
  163. data/lib/acp/types/content_chunk.rb +0 -40
  164. data/sig/generated/acp/agent_connection/optional_method/authenticate.rbs +0 -13
  165. data/sig/generated/acp/agent_connection/optional_method/close_session.rbs +0 -13
  166. data/sig/generated/acp/agent_connection/optional_method/delete_session.rbs +0 -13
  167. data/sig/generated/acp/agent_connection/optional_method/disable_provider.rbs +0 -13
  168. data/sig/generated/acp/agent_connection/optional_method/fork_session.rbs +0 -13
  169. data/sig/generated/acp/agent_connection/optional_method/list_providers.rbs +0 -13
  170. data/sig/generated/acp/agent_connection/optional_method/list_sessions.rbs +0 -13
  171. data/sig/generated/acp/agent_connection/optional_method/load_session.rbs +0 -13
  172. data/sig/generated/acp/agent_connection/optional_method/logout.rbs +0 -13
  173. data/sig/generated/acp/agent_connection/optional_method/resume_session.rbs +0 -13
  174. data/sig/generated/acp/agent_connection/optional_method/set_provider.rbs +0 -13
  175. data/sig/generated/acp/types/content_chunk.rbs +0 -25
  176. data/sig/manual/acp/agent_connection/optional_method/authenticate.rbs +0 -6
  177. data/sig/manual/acp/agent_connection/optional_method/close_session.rbs +0 -6
  178. data/sig/manual/acp/agent_connection/optional_method/delete_session.rbs +0 -6
  179. data/sig/manual/acp/agent_connection/optional_method/disable_provider.rbs +0 -6
  180. data/sig/manual/acp/agent_connection/optional_method/fork_session.rbs +0 -6
  181. data/sig/manual/acp/agent_connection/optional_method/list_providers.rbs +0 -6
  182. data/sig/manual/acp/agent_connection/optional_method/list_sessions.rbs +0 -6
  183. data/sig/manual/acp/agent_connection/optional_method/load_session.rbs +0 -6
  184. data/sig/manual/acp/agent_connection/optional_method/resume_session.rbs +0 -6
  185. data/sig/manual/acp/agent_connection/optional_method/set_provider.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,8 +13,6 @@ 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
18
16
  # @rbs @mcp_message: (^(ACP::Types::Unstable::MessageMcpRequest) ->
19
17
  # (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError))?
20
18
  # @rbs @mcp_advertised: bool?
@@ -23,21 +21,131 @@ class ACP::ClientConnection
23
21
  # @rbs @elicitation_capabilities: ACP::Types::ElicitationCapabilities?
24
22
  # @rbs @extension_requests: Hash[String, ^(untyped) -> untyped]
25
23
  # @rbs @extension_notifications: Hash[String, ^(untyped) -> void]
26
- # @rbs @lock: Thread::Mutex
27
- # @rbs @streams: Hash[String, Thread::Queue]
28
- # @rbs @pending_permissions: Hash[String, Array[Thread::Queue]]
29
- # @rbs @outstanding_elicitations: Hash[String, bool]
24
+ # @rbs @streams: ACP::ClientConnection::Streams
30
25
  # @rbs @logger: ACP::Transport::_Logger
31
26
 
32
- PROTOCOL_VERSION = ACP::AgentConnection::PROTOCOL_VERSION #: Integer
33
-
34
27
  IGNORE = ->(_notification) {} #: ^(ACP::Types::SessionNotification) -> void
35
28
 
36
- CANCELLED = ACP::Types::RequestPermissionResponse.new(
37
- outcome: ACP::Types::RequestPermissionOutcome::Cancelled.new
38
- ) #: 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
39
81
 
40
- # @rbs transport: ACP::AgentConnection::_Transport
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]
147
+
148
+ # @rbs transport: ACP::Transport::_Transport
41
149
  # @rbs permission: ACP::ClientConnection::_PermissionHandler
42
150
  # @rbs read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
43
151
  # @rbs write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
@@ -55,6 +163,7 @@ class ACP::ClientConnection
55
163
  # (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError))?
56
164
  # @rbs extension_requests: Hash[String, ^(untyped) -> untyped]
57
165
  # @rbs extension_notifications: Hash[String, ^(untyped) -> void]
166
+ # @rbs streams: ACP::ClientConnection::Streams?
58
167
  # @rbs logger: ACP::Transport::_Logger
59
168
  # @rbs return: void
60
169
  def initialize(
@@ -73,10 +182,10 @@ class ACP::ClientConnection
73
182
  mcp_message: nil,
74
183
  extension_requests: {},
75
184
  extension_notifications: {},
185
+ streams: nil,
76
186
  logger: ACP::Transport::StderrLogger.new
77
187
  )
78
188
  @transport = transport
79
- @permission = permission
80
189
  @read_text_file = read_text_file
81
190
  @write_text_file = write_text_file
82
191
  @create_terminal = create_terminal
@@ -85,8 +194,6 @@ class ACP::ClientConnection
85
194
  @kill_terminal = kill_terminal
86
195
  @release_terminal = release_terminal
87
196
  @elicitation = elicitation
88
- @complete_elicitation = complete_elicitation
89
- @updates = updates
90
197
  @mcp_message = mcp_message
91
198
  @mcp_advertised = nil
92
199
  @fs_capabilities = nil
@@ -96,10 +203,7 @@ class ACP::ClientConnection
96
203
  @extension_notifications = extension_notifications
97
204
  extension_requests.each_key { |name| ACP::Extensions.validate_name(name) }
98
205
  extension_notifications.each_key { |name| ACP::Extensions.validate_name(name) }
99
- @lock = Mutex.new
100
- @streams = {}
101
- @pending_permissions = {}
102
- @outstanding_elicitations = {}
206
+ @streams = streams || Streams.new(sender: transport, permission:, updates:, logger:, complete_elicitation:)
103
207
  @logger = logger
104
208
  end
105
209
 
@@ -107,18 +211,13 @@ class ACP::ClientConnection
107
211
  def start
108
212
  @transport.start(
109
213
  requests: {
110
- 'session/request_permission' => method(:request_permission),
111
- 'fs/read_text_file' => method(:read_text_file),
112
- 'fs/write_text_file' => method(:write_text_file),
113
- 'terminal/create' => method(:create_terminal),
114
- 'terminal/output' => method(:terminal_output),
115
- 'terminal/wait_for_exit' => method(:wait_for_terminal_exit),
116
- 'terminal/kill' => method(:kill_terminal),
117
- 'terminal/release' => method(:release_terminal),
214
+ 'session/request_permission' => @streams.method(:request_permission),
118
215
  'elicitation/create' => method(:create_elicitation)
119
- }.merge(mcp_requests).merge(@extension_requests),
120
- notifications: { 'session/update' => method(:dispatch),
121
- '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)
122
221
  )
123
222
  end
124
223
 
@@ -128,33 +227,30 @@ class ACP::ClientConnection
128
227
  # @rbs request: ACP::Types::InitializeRequest
129
228
  # @rbs return: ACP::Types::InitializeResponse | ACP::RequestError
130
229
  def connect(request)
131
- capabilities = request.client_capabilities&.fs
132
- unserved = unserved_fs_methods(capabilities)
133
- unless unserved.empty?
134
- raise ArgumentError, "initialize advertises fs methods no handler serves: #{unserved.join(', ')}"
135
- 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)
136
235
 
137
- terminal = request.client_capabilities&.terminal
138
- unserved = unserved_terminal_methods(terminal)
139
- unless unserved.empty?
140
- raise ArgumentError, "initialize advertises terminal methods no handler serves: #{unserved.join(', ')}"
141
- end
142
-
143
- elicitation = request.client_capabilities&.elicitation
236
+ elicitation = capabilities&.elicitation
144
237
  unserved = unserved_elicitation_modes(elicitation)
145
238
  unless unserved.empty?
146
239
  raise ArgumentError, "initialize advertises elicitation modes no handler serves: #{unserved.join(', ')}"
147
240
  end
148
241
 
149
- @fs_capabilities = capabilities
242
+ @fs_capabilities = fs
150
243
  @terminal = terminal
151
244
  @elicitation_capabilities = elicitation
152
245
  result = @transport.request('initialize', request.to_h)
153
246
  response = parse(ACP::Types::InitializeResponse, result)
154
247
  return response if response.is_a?(ACP::RequestError)
155
248
 
156
- @mcp_advertised = agent_mcp_advertised?(result)
157
- return response if response.protocol_version == PROTOCOL_VERSION
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
158
254
 
159
255
  ACP::RequestError.unsupported_protocol_version(
160
256
  requested: request.protocol_version,
@@ -164,91 +260,91 @@ class ACP::ClientConnection
164
260
 
165
261
  # @rbs request: ACP::Types::NewSessionRequest
166
262
  # @rbs return: ACP::Types::NewSessionResponse | ACP::RequestError
167
- def session_new(request)
263
+ def new_session(request)
168
264
  parse(ACP::Types::NewSessionResponse, @transport.request('session/new', request.to_h))
169
265
  end
170
266
 
171
267
  # @rbs request: ACP::Types::PromptRequest
172
268
  # @rbs &block: (ACP::Types::SessionUpdate::t) -> void
173
269
  # @rbs return: ACP::Types::PromptResponse | ACP::RequestError
174
- def session_prompt(request, &)
175
- 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, &))
176
272
  end
177
273
 
178
274
  # @rbs request: ACP::Types::LoadSessionRequest
179
275
  # @rbs &block: (ACP::Types::SessionUpdate::t) -> void
180
276
  # @rbs return: ACP::Types::LoadSessionResponse | ACP::RequestError
181
- def session_load(request, &)
182
- 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, &))
183
279
  end
184
280
 
185
281
  # @rbs request: ACP::Types::ListSessionsRequest
186
282
  # @rbs return: ACP::Types::ListSessionsResponse | ACP::RequestError
187
- def session_list(request)
283
+ def list_sessions(request)
188
284
  parse(ACP::Types::ListSessionsResponse, @transport.request('session/list', request.to_h))
189
285
  end
190
286
 
191
- # 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
192
288
  # is nothing to stream and no block is taken.
193
289
  #
194
290
  # @rbs request: ACP::Types::ResumeSessionRequest
195
291
  # @rbs return: ACP::Types::ResumeSessionResponse | ACP::RequestError
196
- def session_resume(request)
292
+ def resume_session(request)
197
293
  parse(ACP::Types::ResumeSessionResponse, @transport.request('session/resume', request.to_h))
198
294
  end
199
295
 
200
296
  # @rbs request: ACP::Types::CloseSessionRequest
201
297
  # @rbs return: ACP::Types::CloseSessionResponse | ACP::RequestError
202
- def session_close(request)
298
+ def close_session(request)
203
299
  parse(ACP::Types::CloseSessionResponse, @transport.request('session/close', request.to_h))
204
300
  end
205
301
 
206
302
  # @rbs request: ACP::Types::DeleteSessionRequest
207
303
  # @rbs return: ACP::Types::DeleteSessionResponse | ACP::RequestError
208
- def session_delete(request)
304
+ def delete_session(request)
209
305
  parse(ACP::Types::DeleteSessionResponse, @transport.request('session/delete', request.to_h))
210
306
  end
211
307
 
212
308
  # @rbs request: ACP::Types::Unstable::ForkSessionRequest
213
309
  # @rbs return: (ACP::Types::Unstable::ForkSessionResponse | ACP::RequestError)
214
- def session_fork(request)
310
+ def fork_session(request)
215
311
  parse(ACP::Types::Unstable::ForkSessionResponse, @transport.request('session/fork', request.to_h))
216
312
  end
217
313
 
218
314
  # @rbs request: ACP::Types::Unstable::ListProvidersRequest
219
315
  # @rbs return: (ACP::Types::Unstable::ListProvidersResponse | ACP::RequestError)
220
- def providers_list(request)
316
+ def list_providers(request)
221
317
  parse(ACP::Types::Unstable::ListProvidersResponse, @transport.request('providers/list', request.to_h))
222
318
  end
223
319
 
224
320
  # @rbs request: ACP::Types::Unstable::SetProviderRequest
225
321
  # @rbs return: (ACP::Types::Unstable::SetProviderResponse | ACP::RequestError)
226
- def providers_set(request)
322
+ # The name is the wire-mandated providers/set, not an attribute writer.
323
+ def set_provider(request) # rubocop:disable Naming/AccessorMethodName
227
324
  parse(ACP::Types::Unstable::SetProviderResponse, @transport.request('providers/set', request.to_h))
228
325
  end
229
326
 
230
327
  # @rbs request: ACP::Types::Unstable::DisableProviderRequest
231
328
  # @rbs return: (ACP::Types::Unstable::DisableProviderResponse | ACP::RequestError)
232
- def providers_disable(request)
329
+ def disable_provider(request)
233
330
  parse(ACP::Types::Unstable::DisableProviderResponse, @transport.request('providers/disable', request.to_h))
234
331
  end
235
332
 
236
333
  # The spec requires a pending session/request_permission to be answered with
237
- # the cancelled outcome once the turn is cancelled, so every queue registered
238
- # for the session gets one before this returns. The notification goes first
239
- # 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.
240
336
  #
241
337
  # @rbs notification: ACP::Types::CancelNotification
242
338
  # @rbs return: void
243
- def session_cancel(notification)
339
+ def cancel_session(notification)
244
340
  @transport.notify('session/cancel', notification.to_h)
245
- pending = @lock.synchronize { @pending_permissions.delete(notification.session_id) }
246
- pending&.each { |replies| replies << [:value, CANCELLED] }
341
+ @streams.cancel(notification.session_id)
247
342
  end
248
343
 
249
344
  # @rbs request: ACP::Types::SetSessionModeRequest
250
345
  # @rbs return: ACP::Types::SetSessionModeResponse | ACP::RequestError
251
- 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
252
348
  parse(ACP::Types::SetSessionModeResponse, @transport.request('session/set_mode', request.to_h))
253
349
  end
254
350
 
@@ -257,7 +353,8 @@ class ACP::ClientConnection
257
353
  #
258
354
  # @rbs request: ACP::Types::SetSessionConfigOptionRequest
259
355
  # @rbs return: ACP::Types::SetSessionConfigOptionResponse | ACP::RequestError
260
- 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
261
358
  parse(ACP::Types::SetSessionConfigOptionResponse, @transport.request('session/set_config_option', request.to_h))
262
359
  end
263
360
 
@@ -273,9 +370,7 @@ class ACP::ClientConnection
273
370
  parse(ACP::Types::LogoutResponse, @transport.request('logout', request.to_h))
274
371
  end
275
372
 
276
- # Sends an extension request, keyed by its raw `_`-prefixed wire name, and
277
- # returns the agent's reply as-is: extension methods have no schema to
278
- # parse the result into.
373
+ # Extension methods have no schema to parse the result into.
279
374
  #
280
375
  # @rbs method: String
281
376
  # @rbs params: untyped
@@ -312,290 +407,89 @@ class ACP::ClientConnection
312
407
 
313
408
  private
314
409
 
315
- # The route is only registered when a handler serves it, but the agent's
316
- # advertisement is only known after connect, so an unadvertised request
317
- # still reaches the route and is refused there.
318
- #
319
- # @rbs return: Hash[String, ACP::Transport::Stdio::_Handler]
320
- def mcp_requests
321
- return {} unless @mcp_message
322
-
323
- { 'mcp/message' => method(:mcp_message) }
324
- end
325
-
326
- # The stable McpCapabilities has no acp field, so the gate re-reads the raw
327
- # initialize reply with the unstable types when they are opted into.
328
- #
329
- # @rbs result: (Hash[String, untyped] | ACP::RequestError)
330
- # @rbs return: bool
331
- def agent_mcp_advertised?(result)
332
- return false unless defined?(ACP::Types::Unstable)
333
- return false unless result.is_a?(Hash)
334
-
335
- mcp = ACP::Types::Unstable::McpCapabilities.from_h(result['agentCapabilities']['mcpCapabilities'] || {})
336
- mcp.acp ? true : false
337
- rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError
338
- false
339
- end
340
-
341
- # @rbs type: ACP::AgentConnection::_Parser
342
- # @rbs result: (Hash[String, untyped] | ACP::RequestError)
343
- # @rbs return: untyped
344
- def parse(type, result)
345
- return result if result.is_a?(ACP::RequestError)
346
-
347
- type.from_h(result)
348
- rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError
349
- ACP::RequestError.invalid_response(result)
350
- end
351
-
352
- # The request waits on its own thread so the caller's thread can yield each
353
- # update as it arrives. The reader thread queues an update before it settles
354
- # a reply sent after it, so the queue closes only after the last one.
355
- #
356
- # @rbs session_id: String
357
- # @rbs method: String
358
- # @rbs params: Hash[String, untyped]
359
- # @rbs &block: (ACP::Types::SessionUpdate::t) -> void
360
- # @rbs return: (Hash[String, untyped] | ACP::RequestError)
361
- def stream(session_id, method, params)
362
- queue = Thread::Queue.new
363
- # The check and the insert share one lock acquisition so a concurrent
364
- # stream cannot register between them.
365
- claimed = @lock.synchronize do
366
- if @streams.key?(session_id)
367
- false
368
- else
369
- @streams[session_id] = queue
370
- true
371
- end
372
- end
373
- unless claimed
374
- return ACP::RequestError.new(
375
- code: ACP::RequestError::INVALID_REQUEST, message: "A stream is already open for session #{session_id}"
376
- )
377
- end
378
- requester = Thread.new do
379
- @transport.request(method, params)
380
- ensure
381
- @lock.synchronize { @streams.delete(session_id)&.close }
382
- end
383
- while (update = queue.pop)
384
- yield update
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)
385
425
  end
386
- requester.value
387
- end
388
-
389
- # Pushes under the lock so a stream cannot close its queue between the
390
- # lookup and the push. A malformed notification has no reply to carry the
391
- # failure.
392
- #
393
- # @rbs params: untyped
394
- # @rbs return: void
395
- def dispatch(params)
396
- notification = ACP::Types::SessionNotification.from_h(params)
397
- rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError => e
398
- @logger.warn("dropped malformed session/update: #{e.class}: #{e.message}")
399
- else
400
- queue = @lock.synchronize { @streams[notification.session_id]&.push(notification.update) }
401
- @updates.call(notification) unless queue
402
426
  end
403
427
 
404
- # The handler runs on its own thread so a cancelled request is answered
405
- # without waiting for it, and whatever it returns after that is pushed to a
406
- # queue nobody reads.
407
- #
408
- # @rbs params: untyped
409
- # @rbs return: (ACP::Types::RequestPermissionResponse | ACP::RequestError)
410
- def request_permission(params)
411
- request = ACP::Types::RequestPermissionRequest.from_h(params)
412
- rescue ACP::Types::ParseError => e
413
- ACP::RequestError.invalid_params([e.message])
414
- rescue KeyError, TypeError, NoMethodError
415
- ACP::RequestError.invalid_params
416
- else
417
- replies = Thread::Queue.new
418
- register_permission(request.session_id, replies)
419
- # The handler runs on this connection's own thread, so the serving
420
- # identity must be carried over for cancelled? to see the cancel there.
421
- serving = @transport.cancellation
422
- Thread.new do
423
- ACP::Transport::Cancellation.current = serving
424
- # A raised StandardError is not a returned ACP::RequestError, so the two
425
- # need distinct shapes on the queue.
426
- replies << begin
427
- [:value, @permission.call(request)]
428
- rescue StandardError => e
429
- [:raised, e]
430
- end
431
- ensure
432
- unregister_permission(request.session_id, replies)
433
- end
434
- settled = replies.pop
435
- # Re-raised so the transport's invoke turns it into an error reply, as it
436
- # would if the handler ran on the serve thread itself.
437
- raise settled[1] if settled[0] == :raise
438
-
439
- settled[1]
428
+ # @rbs route: ACP::ClientConnection::Route
429
+ # @rbs return: ACP::RequestError
430
+ def refuse(route)
431
+ ACP::RequestError.public_send(route.refuse, route.key)
440
432
  end
441
433
 
442
- # @rbs session_id: String
443
- # @rbs replies: Thread::Queue
434
+ # @rbs name: String
435
+ # @rbs family: Symbol
436
+ # @rbs capabilities: untyped
444
437
  # @rbs return: void
445
- def register_permission(session_id, replies)
446
- @lock.synchronize { (@pending_permissions[session_id] ||= []) << replies }
447
- end
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)
448
443
 
449
- # @rbs session_id: String
450
- # @rbs replies: Thread::Queue
451
- # @rbs return: void
452
- def unregister_permission(session_id, replies)
453
- @lock.synchronize do
454
- pending = @pending_permissions[session_id]
455
- if pending
456
- pending.delete(replies)
457
- @pending_permissions.delete(session_id) if pending.empty?
458
- end
444
+ route.label
459
445
  end
460
- end
461
-
462
- # The routes are registered at start, before connect records the advertised
463
- # capabilities, so each one answers -32601 until then.
464
- #
465
- # @rbs params: untyped
466
- # @rbs return: (ACP::Types::ReadTextFileResponse | ACP::RequestError)
467
- def read_text_file(params)
468
- return ACP::RequestError.unadvertised('fs.readTextFile') unless @fs_capabilities&.read_text_file
469
-
470
- request = ACP::Types::ReadTextFileRequest.from_h(params)
471
- rescue ACP::Types::ParseError => e
472
- ACP::RequestError.invalid_params([e.message])
473
- rescue KeyError, TypeError, NoMethodError
474
- ACP::RequestError.invalid_params
475
- else
476
- # Safe: connect refuses an advertised capability no handler serves.
477
- handler = @read_text_file #: ^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError)
478
- handler.call(request)
479
- end
480
-
481
- # @rbs params: untyped
482
- # @rbs return: (ACP::Types::WriteTextFileResponse | ACP::RequestError)
483
- def write_text_file(params)
484
- return ACP::RequestError.unadvertised('fs.writeTextFile') unless @fs_capabilities&.write_text_file
485
-
486
- request = ACP::Types::WriteTextFileRequest.from_h(params)
487
- rescue ACP::Types::ParseError => e
488
- ACP::RequestError.invalid_params([e.message])
489
- rescue KeyError, TypeError, NoMethodError
490
- ACP::RequestError.invalid_params
491
- else
492
- # Safe: connect refuses an advertised capability no handler serves.
493
- handler = @write_text_file #: ^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError)
494
- handler.call(request)
495
- end
496
-
497
- # The terminal routes are registered at start, before connect records the
498
- # advertised capability, so each one answers -32601 until then.
499
- #
500
- # @rbs params: untyped
501
- # @rbs return: (ACP::Types::CreateTerminalResponse | ACP::RequestError)
502
- def create_terminal(params)
503
- return ACP::RequestError.unadvertised('terminal') unless @terminal
446
+ return if unserved.empty?
504
447
 
505
- request = ACP::Types::CreateTerminalRequest.from_h(params)
506
- rescue KeyError, TypeError, NoMethodError
507
- ACP::RequestError.invalid_params
508
- else
509
- # Safe: connect refuses an advertised capability no handler serves.
510
- handler = @create_terminal #: ^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError)
511
- handler.call(request)
448
+ raise ArgumentError, "initialize advertises #{name} methods no handler serves: #{unserved.join(', ')}"
512
449
  end
513
450
 
514
- # @rbs params: untyped
515
- # @rbs return: (ACP::Types::TerminalOutputResponse | ACP::RequestError)
516
- def terminal_output(params)
517
- return ACP::RequestError.unadvertised('terminal') unless @terminal
518
-
519
- request = ACP::Types::TerminalOutputRequest.from_h(params)
520
- rescue KeyError, TypeError, NoMethodError
521
- ACP::RequestError.invalid_params
522
- else
523
- # Safe: connect refuses an advertised capability no handler serves.
524
- handler = @terminal_output #: ^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError)
525
- handler.call(request)
526
- end
527
-
528
- # The handler may block for as long as the command runs, so the transport
529
- # 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.
530
456
  #
531
- # @rbs params: untyped
532
- # @rbs return: (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError)
533
- def wait_for_terminal_exit(params)
534
- return ACP::RequestError.unadvertised('terminal') unless @terminal
535
-
536
- request = ACP::Types::WaitForTerminalExitRequest.from_h(params)
537
- rescue KeyError, TypeError, NoMethodError
538
- ACP::RequestError.invalid_params
539
- else
540
- # Safe: connect refuses an advertised capability no handler serves.
541
- handler = @wait_for_terminal_exit #: ^(ACP::Types::WaitForTerminalExitRequest) -> (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError)
542
- handler.call(request)
543
- end
544
-
545
- # @rbs params: untyped
546
- # @rbs return: (ACP::Types::KillTerminalResponse | ACP::RequestError)
547
- def kill_terminal(params)
548
- return ACP::RequestError.unadvertised('terminal') unless @terminal
549
-
550
- request = ACP::Types::KillTerminalRequest.from_h(params)
551
- rescue KeyError, TypeError, NoMethodError
552
- ACP::RequestError.invalid_params
553
- else
554
- # Safe: connect refuses an advertised capability no handler serves.
555
- handler = @kill_terminal #: ^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError)
556
- handler.call(request)
557
- end
558
-
559
- # @rbs params: untyped
560
- # @rbs return: (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
561
- def release_terminal(params)
562
- return ACP::RequestError.unadvertised('terminal') unless @terminal
563
-
564
- request = ACP::Types::ReleaseTerminalRequest.from_h(params)
565
- rescue KeyError, TypeError, NoMethodError
566
- ACP::RequestError.invalid_params
567
- else
568
- # Safe: connect refuses an advertised capability no handler serves.
569
- handler = @release_terminal #: ^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
570
- 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) }
571
471
  end
572
472
 
573
- # The route is registered at start, before connect records the agent's
574
- # advertisement, so an unadvertised request answers -32601 until then.
575
- #
576
- # @rbs params: untyped
577
- # @rbs return: (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError)
578
- def mcp_message(params)
579
- return ACP::RequestError.unadvertised_agent('mcpCapabilities.acp') unless @mcp_advertised
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)
580
478
 
581
- request = ACP::Types::Unstable::MessageMcpRequest.from_h(params)
582
- rescue ACP::Types::ParseError => e
583
- ACP::RequestError.invalid_params([e.message])
584
- rescue KeyError, TypeError, NoMethodError
585
- ACP::RequestError.invalid_params
586
- else
587
- # Safe: the route is only registered when a handler serves it.
588
- handler = @mcp_message #: ^(ACP::Types::Unstable::MessageMcpRequest) -> (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError)
589
- handler.call(request)
479
+ type.from_h(result)
480
+ rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError
481
+ ACP::RequestError.invalid_response(result)
590
482
  end
591
483
 
592
- # The routes are registered at start, before connect records the advertised
484
+ # The route is registered at start, before connect records the advertised
593
485
  # capabilities, so a request for an unadvertised mode answers -32602.
594
486
  #
595
487
  # @rbs params: untyped
596
488
  # @rbs return: (ACP::Types::CreateElicitationResponse | ACP::RequestError)
597
489
  def create_elicitation(params)
598
490
  request = ACP::Types::CreateElicitationRequest.from_h(params)
491
+ rescue ACP::Types::ParseError => e
492
+ ACP::RequestError.invalid_params([e.message])
599
493
  rescue KeyError, TypeError, NoMethodError
600
494
  ACP::RequestError.invalid_params
601
495
  else
@@ -605,7 +499,7 @@ class ACP::ClientConnection
605
499
  when ACP::Types::CreateElicitationRequest::Mode::Url
606
500
  return ACP::RequestError.unadvertised_mode('elicitation.url') unless @elicitation_capabilities&.url
607
501
 
608
- register_elicitation(request.mode.elicitation_id)
502
+ @streams.register_elicitation(request.mode.elicitation_id)
609
503
  else
610
504
  return ACP::RequestError.unadvertised_mode("elicitation.#{request.mode['mode']}")
611
505
  end
@@ -615,57 +509,6 @@ class ACP::ClientConnection
615
509
  handler.call(request)
616
510
  end
617
511
 
618
- # The spec requires a completion for an unknown or already-completed
619
- # elicitation id to be ignored, so only an id registered by a served
620
- # url-mode request reaches the handler. It runs on the transport's reader
621
- # thread, so it must return quickly.
622
- #
623
- # @rbs params: untyped
624
- # @rbs return: void
625
- def complete_elicitation(params)
626
- notification = ACP::Types::CompleteElicitationNotification.from_h(params)
627
- rescue KeyError, TypeError, NoMethodError => e
628
- @logger.warn("dropped malformed elicitation/complete: #{e.class}: #{e.message}")
629
- else
630
- known = @lock.synchronize { @outstanding_elicitations.delete(notification.elicitation_id) }
631
- @complete_elicitation&.call(notification) if known
632
- end
633
-
634
- # @rbs elicitation_id: String
635
- # @rbs return: void
636
- def register_elicitation(elicitation_id)
637
- @lock.synchronize { @outstanding_elicitations[elicitation_id] = true }
638
- end
639
-
640
- # The fs capabilities initialize advertises that no injected handler serves.
641
- #
642
- # @rbs capabilities: ACP::Types::FileSystemCapabilities?
643
- # @rbs return: Array[String]
644
- def unserved_fs_methods(capabilities)
645
- {
646
- 'fs.readTextFile' => capabilities&.read_text_file && !@read_text_file,
647
- 'fs.writeTextFile' => capabilities&.write_text_file && !@write_text_file
648
- }.select { |_, unserved| unserved }.keys
649
- end
650
-
651
- # The terminal capabilities initialize advertises that no injected handler
652
- # serves.
653
- #
654
- # @rbs capabilities: bool?
655
- # @rbs return: Array[String]
656
- def unserved_terminal_methods(capabilities)
657
- {
658
- 'terminal.create' => capabilities && !@create_terminal,
659
- 'terminal.output' => capabilities && !@terminal_output,
660
- 'terminal.wait_for_exit' => capabilities && !@wait_for_terminal_exit,
661
- 'terminal.kill' => capabilities && !@kill_terminal,
662
- 'terminal.release' => capabilities && !@release_terminal
663
- }.select { |_, unserved| unserved }.keys
664
- end
665
-
666
- # The elicitation capabilities initialize advertises that no injected
667
- # handler serves.
668
- #
669
512
  # @rbs capabilities: ACP::Types::ElicitationCapabilities?
670
513
  # @rbs return: Array[String]
671
514
  def unserved_elicitation_modes(capabilities)