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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e8a4bb93508ff8a9737b9b5b044e4b63e19ba87393490f10f522b9b63aee4396
4
- data.tar.gz: 9e62dd726471fd718c2a234b9ea65bc90ca473c67d979e207b2702edd33b30b5
3
+ metadata.gz: 96418a5250fccc2c0eb4380e15309f7658f347bc91d5df3c1218cc3794389f48
4
+ data.tar.gz: 52c089fd1c4238da195880f4fcbf823452272de94a6c3f6ad3a2d7c06f0580d1
5
5
  SHA512:
6
- metadata.gz: 50ef3954c1104ef512b757421bb0df10d3eb0be7f245c5a26c8641e815fa54d160a1d4d2c8369b9b41eb4b64affce206c584627650a5ce3446be09f88f2239b4
7
- data.tar.gz: 0e505cd00fd0fef743dc1c047892db07b1ea4bf0999b3c0bff402fc10f73609a23fcbd4310b4a8deb7822fd06d8a64d1bab517426302004c205b3eee2972a104
6
+ metadata.gz: 84820ff6950c13e9d2fd30f5e5936f7951700af3622bfdc84af49cfcd6b7f24465e00d4889d2d451eaa01fc5091b6af7ecc103579c175d683f451e7ac27a86ce
7
+ data.tar.gz: 696efcc33f82fd2bc5ecd1a8e9843138c5be690e13ed4a72c955202c95af404c43a7c794868a1a0dad712174bb6ceaf71b0ab9d51bcf86e5954f6f6acdee3d61
data/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.0](https://github.com/bottrall/acp-sdk/compare/v0.10.0...v0.11.0) (2026-10-12)
4
+
5
+
6
+ ### Features
7
+
8
+ * standardize ClientConnection senders on verb-noun naming ([#118](https://github.com/bottrall/acp-sdk/issues/118)) ([1b844ec](https://github.com/bottrall/acp-sdk/commit/1b844ecccef5b8714c5e3f2d0299713f0ee09e0f))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * answer malformed elicitation params as invalid params ([#133](https://github.com/bottrall/acp-sdk/issues/133)) ([b9e1c51](https://github.com/bottrall/acp-sdk/commit/b9e1c510b60f78986998ba8be56c8a039488ee95)), closes [#125](https://github.com/bottrall/acp-sdk/issues/125)
14
+ * answer the agent when a permission handler raises ([#134](https://github.com/bottrall/acp-sdk/issues/134)) ([a0f61a9](https://github.com/bottrall/acp-sdk/commit/a0f61a9db3589848fafadb7b4e19ad70b260f508)), closes [#124](https://github.com/bottrall/acp-sdk/issues/124)
15
+ * cancel the echo agent's in-flight turn when its session is closed or deleted ([#136](https://github.com/bottrall/acp-sdk/issues/136)) ([e04a8c0](https://github.com/bottrall/acp-sdk/commit/e04a8c0bb672b569d68029c63eaee826d00c84fd)), closes [#130](https://github.com/bottrall/acp-sdk/issues/130)
16
+ * **codegen:** keep the unset sentinel out of clearable to_h conversions ([#140](https://github.com/bottrall/acp-sdk/issues/140)) ([67769d7](https://github.com/bottrall/acp-sdk/commit/67769d7ff5d9f93dfe3f3ea2ba4ac71126caad60)), closes [#131](https://github.com/bottrall/acp-sdk/issues/131)
17
+ * **codegen:** resolve primitive unions from each option's resolved shape ([#141](https://github.com/bottrall/acp-sdk/issues/141)) ([0b574ae](https://github.com/bottrall/acp-sdk/commit/0b574ae9d33c6c10eb0a00866b6c07f1601e01e8)), closes [#132](https://github.com/bottrall/acp-sdk/issues/132)
18
+ * **codegen:** subsume union-variant twins into variant aliases ([#103](https://github.com/bottrall/acp-sdk/issues/103)) ([bdc0005](https://github.com/bottrall/acp-sdk/commit/bdc0005460c0fd133a09f10cf61f9f5717012e16)), closes [#93](https://github.com/bottrall/acp-sdk/issues/93)
19
+ * echo slash commands followed only by whitespace ([#135](https://github.com/bottrall/acp-sdk/issues/135)) ([bb4bde6](https://github.com/bottrall/acp-sdk/commit/bb4bde6951ceb54ac87c92ec335dcdd319fd7b81)), closes [#129](https://github.com/bottrall/acp-sdk/issues/129)
20
+ * read mcpCapabilities alone when deciding whether MCP is advertised ([#139](https://github.com/bottrall/acp-sdk/issues/139)) ([a6e882e](https://github.com/bottrall/acp-sdk/commit/a6e882ebf3c69f0aa8692100e81e61a085007a8b)), closes [#128](https://github.com/bottrall/acp-sdk/issues/128)
21
+ * return a null or false result instead of "Connection closed" ([#137](https://github.com/bottrall/acp-sdk/issues/137)) ([19be64d](https://github.com/bottrall/acp-sdk/commit/19be64df6064e370bb23614a3b8df1056649cd3b)), closes [#127](https://github.com/bottrall/acp-sdk/issues/127)
22
+ * settle a pending request when its response is malformed ([#138](https://github.com/bottrall/acp-sdk/issues/138)) ([7d0586e](https://github.com/bottrall/acp-sdk/commit/7d0586e856a54e5779e4d523bd0862ab57f44b10)), closes [#126](https://github.com/bottrall/acp-sdk/issues/126)
23
+
24
+ ## [0.10.0](https://github.com/bottrall/acp-sdk/compare/v0.9.0...v0.10.0) (2026-10-07)
25
+
26
+
27
+ ### Features
28
+
29
+ * MCP-over-ACP mcp/message (unstable) ([#89](https://github.com/bottrall/acp-sdk/issues/89)) ([3d6d30c](https://github.com/bottrall/acp-sdk/commit/3d6d30c6e31e88dd0fe78e08a5bc4308948cacdd))
30
+ * providers/list, providers/set and providers/disable (unstable) ([#88](https://github.com/bottrall/acp-sdk/issues/88)) ([eac16f7](https://github.com/bottrall/acp-sdk/commit/eac16f78809ea5a677835c6fe064063dcb64c162)), closes [#50](https://github.com/bottrall/acp-sdk/issues/50)
31
+ * request cancellation on the agent and client connections ([#85](https://github.com/bottrall/acp-sdk/issues/85)) ([58ae0ab](https://github.com/bottrall/acp-sdk/commit/58ae0abe6387e2887482ec14e361cfac91cd54ea))
32
+ * session/fork (unstable) ([#86](https://github.com/bottrall/acp-sdk/issues/86)) ([9868cb6](https://github.com/bottrall/acp-sdk/commit/9868cb666a8111c414bf3b041167d6aa022e1e95)), closes [#49](https://github.com/bottrall/acp-sdk/issues/49)
33
+ * support all currently supported Ruby versions ([#90](https://github.com/bottrall/acp-sdk/issues/90)) ([343d07b](https://github.com/bottrall/acp-sdk/commit/343d07b9e6d98b1c24201c5eef8b250858c0626b)), closes [#73](https://github.com/bottrall/acp-sdk/issues/73)
34
+
3
35
  ## [0.9.0](https://github.com/bottrall/acp-sdk/compare/v0.8.0...v0.9.0) (2026-10-07)
4
36
 
5
37
 
data/README.md CHANGED
@@ -11,7 +11,7 @@ Every ACP request, response and notification is a generated Ruby class under `AC
11
11
 
12
12
  ## Installation
13
13
 
14
- Requires Ruby 4.0 or later.
14
+ Requires Ruby 3.4 or later (CI tests 3.4 and 4.0).
15
15
 
16
16
  Add the gem to your Gemfile:
17
17
 
@@ -48,11 +48,12 @@ connection.start.join
48
48
 
49
49
  The agent's contract:
50
50
 
51
- - It defines `new_session`, `prompt` and `cancel`. `load_session`, `list_sessions`, `resume_session`, `close_session` and `delete_session` are required only when `capabilities` advertises `load_session` or `session_capabilities.list`, `.resume`, `.close` or `.delete`, and `authenticate` only when `auth_methods` is non-empty, and `logout` only when `capabilities` advertises `auth.logout`; `start` raises `ArgumentError` when one is advertised but missing, and a method that is not advertised is answered with `-32601`. `change_session_mode` and `change_session_config_option` are optional, since modes and config options are offered per session rather than in `initialize`; `session/set_mode` and `session/set_config_option` are answered with `-32601` when the agent does not define them. `session_created`, if defined, runs right after the `session/new` reply is sent.
51
+ - It defines `new_session`, `prompt` and `cancel`. `load_session`, `list_sessions`, `resume_session`, `close_session` and `delete_session` are required only when `capabilities` advertises `load_session` or `session_capabilities.list`, `.resume`, `.close` or `.delete`, and `authenticate` only when `auth_methods` is non-empty, and `logout` only when `capabilities` advertises `auth.logout`; `start` raises `ArgumentError` when one is advertised but missing, and a method that is not advertised is answered with `-32601`. `change_session_mode` and `change_session_config_option` are optional, since modes and config options are offered per session rather than in `initialize`; `session/set_mode` and `session/set_config_option` are answered with `-32601` when the agent does not define them. `session_created`, if defined, runs right after the `session/new` reply is sent. `fork_session` is the same shape but unstable: `session/fork` routes only when the caller has opted into the unstable types with `require 'acp/types/unstable'` and `capabilities` advertises `session_capabilities.fork`, and is answered with `-32601` otherwise. `list_providers`, `set_provider` and `disable_provider` are the same shape but unstable too: `providers/list`, `providers/set` and `providers/disable` route only when the caller has opted into the unstable types with `require 'acp/types/unstable'` and `capabilities` advertises `providers`, and are answered with `-32601` otherwise.
52
52
  - Each request method returns its response type or an `ACP::RequestError`, which is sent as the error reply. Params that do not match the schema are answered with `-32602` before the agent sees them, with `data` naming the failing path, e.g. `{ errors: ['sessionId: expected String, got Integer'] }`.
53
53
  - A handler that raises is answered with `-32603` and the message `Internal error`. Nothing from the exception reaches the peer — its class and message are logged to the transport's logger instead, since messages can interpolate paths, queries or credentials. Return an `ACP::RequestError` from the handler when the peer should see a real error message.
54
- - `cancel` runs on the transport's reader thread, so it must return quickly: set a flag and let the prompt notice it.
54
+ - `cancel` runs on the transport's reader thread, so it must return quickly: set a flag and let the prompt notice it, and call `client.cancel_requests` to cancel the connection's outstanding requests to the client — a blocked `request_permission` or `wait_for_terminal_exit`, say. That sends `$/cancel_request` for each one, and the blocked call returns once the client answers it, with its response or the `-32800` error.
55
55
  - After a cancel, the agent must itself end the turn with `stopReason: cancelled`. `ACP::AgentConnection` does not enforce it.
56
+ - `client.cancelled?` is true in a handler when the client cancelled the request it is serving with `$/cancel_request`. The handler can end early by returning `ACP::RequestError.request_cancelled`, which is answered as `-32800`, or return a normal response, which is sent as-is — the transport sends exactly one response either way.
56
57
  - `client.capabilities` is `nil` until the client sends `initialize`.
57
58
  - `client.read_text_file` and `client.write_text_file` take an `ACP::Types::ReadTextFileRequest` or `ACP::Types::WriteTextFileRequest` and return the response type or an `ACP::RequestError`. Unless `client.capabilities` advertises `fs.read_text_file` or `fs.write_text_file`, they return `-32601` without sending anything to the client.
58
59
  - `client.create_terminal`, `client.terminal_output`, `client.wait_for_terminal_exit`, `client.kill_terminal` and `client.release_terminal` take the matching `ACP::Types` request (`CreateTerminalRequest`, `TerminalOutputRequest`, `WaitForTerminalExitRequest`, `KillTerminalRequest`, `ReleaseTerminalRequest`) and return its response type or an `ACP::RequestError`. Unless `client.capabilities` advertises `terminal`, they return `-32601` without sending anything to the client. The agent must release every terminal it creates.
@@ -64,7 +65,7 @@ Extensions are `_`-prefixed methods outside the spec. `AgentConnection` takes `e
64
65
 
65
66
  ## Driving an agent
66
67
 
67
- `ACP::ClientConnection` is the other side of the same transport. Each method takes the request's generated `ACP::Types` object and returns its response type or the agent's `ACP::RequestError`. `connect` sends `initialize`, since Ruby reserves that name for the constructor, and `authenticate` and `logout` send their namesake methods. `session_set_mode` sends `session/set_mode`, and `session_set_config_option` sends `session/set_config_option` and returns the session's complete option list, which replaces any state held from an earlier reply. A handler that raises is answered with `-32603` and the message `Internal error`, and the exception is only logged to the transport's logger.
68
+ `ACP::ClientConnection` is the other side of the same transport. Each method takes the request's generated `ACP::Types` object and returns its response type or the agent's `ACP::RequestError`. `connect` sends `initialize`, since Ruby reserves that name for the constructor, and `authenticate` and `logout` send their namesake methods. `set_session_mode` sends `session/set_mode`, and `set_session_config_option` sends `session/set_config_option` and returns the session's complete option list, which replaces any state held from an earlier reply. `fork_session` sends `session/fork`; it is unstable, so the request and response types exist only under `ACP::Types::Unstable` and calling it before `require 'acp/types/unstable'` raises `NameError`. `list_providers`, `set_provider` and `disable_provider` send `providers/list`, `providers/set` and `providers/disable` on the same unstable terms. A handler that raises is answered with `-32603` and the message `Internal error`, and the exception is only logged to the transport's logger.
68
69
 
69
70
  `ACP::AgentProcess.spawn` starts a command with args, env and cwd, wires its stdio to a new `ACP::ClientConnection` and starts it. The block receives the connection and the process handle (`pid`, plus `stderr` when captured), and the child is terminated — after its stdin is closed, with TERM and then KILL if it will not exit — when the block exits, so a raised block cannot leak the process. Without a block, `spawn` returns the process and calling `terminate` is yours. `stderr` is `:inherit` by default, so the child writes to your stderr; pass `:capture` to collect it on the process instead.
70
71
 
@@ -80,9 +81,9 @@ response = ACP::AgentProcess.spawn(
80
81
  updates: ->(notification) { show(notification) }
81
82
  ) do |connection|
82
83
  connection.connect(ACP::Types::InitializeRequest.new(protocol_version: 1))
83
- session = connection.session_new(ACP::Types::NewSessionRequest.new(cwd: Dir.pwd, mcp_servers: []))
84
+ session = connection.new_session(ACP::Types::NewSessionRequest.new(cwd: Dir.pwd, mcp_servers: []))
84
85
  prompt = [ACP::Types::ContentBlock::Text.new(text: 'hello')]
85
- connection.session_prompt(ACP::Types::PromptRequest.new(session_id: session.session_id, prompt:)) do |update|
86
+ connection.prompt_session(ACP::Types::PromptRequest.new(session_id: session.session_id, prompt:)) do |update|
86
87
  print update.content.text if update.is_a?(ACP::Types::SessionUpdate::AgentMessageChunk)
87
88
  end
88
89
  end
@@ -103,14 +104,23 @@ connection = ACP::ClientConnection.new(
103
104
  )
104
105
  ```
105
106
 
106
- - `session_prompt` and `session_load` yield the session's updates on the calling thread as they arrive and return once the agent replies. Updates outside those calls, such as the available commands after `session/new`, go to `updates`, which runs on the reader thread and must return quickly. `session_resume` does not take a block: the agent must not replay history on resume, so unlike `session_load` there is nothing to stream. `session_close` and `session_delete` end the session, discarding it on the agent, and return once the agent replies.
107
- - `permission` answers `session/request_permission` with an `ACP::Types::RequestPermissionResponse` or an `ACP::RequestError`. It runs on its own thread, so it may block while the user decides, and `session_cancel` can be sent meanwhile.
107
+ - `prompt_session` and `load_session` yield the session's updates on the calling thread as they arrive and return once the agent replies. Updates outside those calls, such as the available commands after `session/new`, go to `updates`, which runs on the reader thread and must return quickly. `resume_session` does not take a block: the agent must not replay history on resume, so unlike `load_session` there is nothing to stream. `close_session` and `delete_session` end the session, discarding it on the agent, and return once the agent replies.
108
+ - `permission` answers `session/request_permission` with an `ACP::Types::RequestPermissionResponse` or an `ACP::RequestError`. It runs on its own thread, so it may block while the user decides, and `cancel_session` can be sent meanwhile.
109
+ - The agent may cancel any outstanding request with `$/cancel_request`: a handler that can end early checks `connection.cancelled?` — close over the connection when constructing it — and returns `ACP::RequestError.request_cancelled`, which is answered as `-32800`. The permission handler still sees the cancel on its own thread. Whatever the handler returns, the transport sends exactly one response.
108
110
  - `read_text_file` and `write_text_file` answer the agent's `fs/read_text_file` and `fs/write_text_file` with an `ACP::Types::ReadTextFileResponse`, an `ACP::Types::WriteTextFileResponse` or an `ACP::RequestError`. They are routed by the `fs` capabilities `connect` sends: a method the client does not advertise is answered with `-32601` without reaching the handler, and `connect` raises `ArgumentError` — before sending `initialize` — when the request advertises a capability that has no handler.
109
111
  - `create_terminal`, `terminal_output`, `wait_for_terminal_exit`, `kill_terminal` and `release_terminal` answer the agent's `terminal/create`, `terminal/output`, `terminal/wait_for_exit`, `terminal/kill` and `terminal/release` with the matching `ACP::Types` response or an `ACP::RequestError`. They are routed by the `terminal` capability `connect` sends, the same way as `fs`: a method the client does not advertise is answered with `-32601` without reaching the handler, and `connect` raises `ArgumentError` — before sending `initialize` — when the request advertises `terminal` with any of the five missing. The transport serves each request on its own thread, so `wait_for_terminal_exit` may block without holding up the others.
110
112
  - `elicitation` answers the agent's `elicitation/create` with an `ACP::Types::CreateElicitationResponse` or an `ACP::RequestError`, serving both form and url mode, and `complete_elicitation` receives the agent's `elicitation/complete` notification. They are routed by the `elicitation` capabilities `connect` sends: a request whose mode the client does not advertise is answered with `-32602` without reaching the handler, and `connect` raises `ArgumentError` — before sending `initialize` — when the request advertises `elicitation.form` or `elicitation.url` with no `elicitation` handler. The client tracks the elicitation ids of the url-mode requests it serves, so a completion for an unknown or already-completed id is ignored. `complete_elicitation` runs on the reader thread and must return quickly; it may be omitted, in which case completions are only tracked.
111
113
 
112
114
  Extensions work the same way in both directions: `extension_requests` and `extension_notifications` are hashes keyed by the raw `_`-prefixed wire name, `ext_request` sends one to the agent and returns its result as-is, and `ext_notify` sends an extension notification. A handler name without the `_` prefix raises `ArgumentError`, and an extension request nothing answers gets `-32601`.
113
115
 
116
+ ## Types
117
+
118
+ Every ACP request, response and notification is a generated, frozen value class under `ACP::Types` with `from_h` and `to_h`. A few of its names are deliberate enough to deserve a note:
119
+
120
+ - `CancelRequestNotification` and `CancelNotification` are different methods with different payloads: the former is the params of the JSON-RPC-level `$/cancel_request`, which the transport handles itself, the latter is the params of ACP's `session/cancel`. Both spellings are the schema's, not a typo.
121
+ - The schema names `session/new`, `session/load`, `session/list` and `session/prompt`'s params and result `NewSessionRequest`, `LoadSessionRequest`, `ListSessionsRequest` and `PromptRequest`, so the wire-shaped spellings exist as alias constants: `ACP::Types::SessionNewRequest`, `SessionLoadRequest`, `SessionListRequest`, `SessionListResponse` and `SessionPromptRequest` are the same classes as their targets. Both names are public API and here to stay — use whichever reads better.
122
+ - The schema's pure string enums are generated as constant modules, for building requests and matching responses without string literals: `ACP::Types::ToolKind::EDIT`, `ACP::Types::StopReason::CANCELLED`, `ACP::Types::PlanEntryPriority::HIGH`, `ACP::Types::Role::USER` and the rest. They carry no validation of their own — the generated `from_h` checks incoming values against the schema regardless of how you build outgoing ones.
123
+
114
124
  ## Unstable types
115
125
 
116
126
  Some protocol features are not part of the spec yet, so their schema lives in a separate unstable file: draft session updates (`plan_update`, `plan_removed`, `notice`, `compaction_update`, `compaction_summary_chunk`, `subagent_update`, `session_message`, `session_message_chunk`), `PromptResponse#usage`, the NE (next edit) and provider methods, and the matching capabilities. They are generated under `ACP::Types::Unstable` and load only when you opt in:
@@ -123,6 +133,8 @@ Until then, referencing `ACP::Types::Unstable` is a `NameError`, and the stable
123
133
 
124
134
  These types are **unstable**: they are not part of the ACP spec, agents and clients must not rely on the peer supporting them, and any of them may change or disappear between releases without a major version bump. When a stable schema absorbs one of them, the unstable twin goes away and the stable name stays.
125
135
 
136
+ The connections route `session/fork`, the provider methods and `mcp/message`, but not every unstable method has routing yet: the NE methods (`nes/start`, `nes/suggest`, `nes/accept`, `nes/reject`, `nes/close`), the document notifications (`document/didOpen` and friends) and the unstable `ClientCapabilities` are generated as public API for callers wiring those methods at the transport level, and may gain connection routing later.
137
+
126
138
  ## Contributing
127
139
 
128
140
  Bug reports and pull requests are welcome on [GitHub](https://github.com/bottrall/acp-sdk). See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and workflow. Everyone interacting in the project is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
@@ -1,15 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ACP::AgentConnection::Client
4
- # @rbs @peer: ACP::AgentConnection::_Peer
4
+ # @rbs @peer: ACP::Transport::_Peer
5
+ # @rbs @mcp_advertised: bool
5
6
 
6
7
  # @dynamic capabilities, capabilities=
7
8
  attr_accessor :capabilities #: ACP::Types::ClientCapabilities?
8
9
 
9
- # @rbs peer: ACP::AgentConnection::_Peer
10
+ # @rbs peer: ACP::Transport::_Peer
11
+ # @rbs mcp_advertised: bool
10
12
  # @rbs return: void
11
- def initialize(peer:)
13
+ def initialize(peer:, mcp_advertised: false)
12
14
  @peer = peer
15
+ @mcp_advertised = mcp_advertised
13
16
  @capabilities = nil
14
17
  end
15
18
 
@@ -27,15 +30,24 @@ class ACP::AgentConnection::Client
27
30
  update(session_id, ACP::Types::SessionUpdate::AvailableCommandsUpdate.new(available_commands: commands))
28
31
  end
29
32
 
30
- # Blocks the calling thread until the client answers or the connection
31
- # closes.
32
- #
33
33
  # @rbs request: ACP::Types::RequestPermissionRequest
34
34
  # @rbs return: ACP::Types::RequestPermissionResponse | ACP::RequestError
35
35
  def request_permission(request)
36
36
  call('session/request_permission', ACP::Types::RequestPermissionResponse, request)
37
37
  end
38
38
 
39
+ # @rbs return: void
40
+ def cancel_requests
41
+ @peer.cancel_requests
42
+ end
43
+
44
+ # Always false off a handler thread.
45
+ #
46
+ # @rbs return: bool
47
+ def cancelled?
48
+ !!@peer.cancellation&.cancelled?
49
+ end
50
+
39
51
  # @rbs request: ACP::Types::ReadTextFileRequest
40
52
  # @rbs return: ACP::Types::ReadTextFileResponse | ACP::RequestError
41
53
  def read_text_file(request)
@@ -105,9 +117,15 @@ class ACP::AgentConnection::Client
105
117
  @peer.notify('elicitation/complete', notification.to_h)
106
118
  end
107
119
 
108
- # Sends an extension request, keyed by its raw `_`-prefixed wire name, and
109
- # returns the client's reply as-is: extension methods have no schema to
110
- # parse the result into.
120
+ # @rbs request: ACP::Types::Unstable::MessageMcpRequest
121
+ # @rbs return: (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError)
122
+ def mcp_message(request)
123
+ return ACP::RequestError.unadvertised_agent('mcpCapabilities.acp') unless @mcp_advertised
124
+
125
+ call('mcp/message', ACP::Types::Unstable::MessageMcpResponse, request)
126
+ end
127
+
128
+ # Extension methods have no schema to parse the result into.
111
129
  #
112
130
  # @rbs method: String
113
131
  # @rbs params: untyped
@@ -128,8 +146,8 @@ class ACP::AgentConnection::Client
128
146
  private
129
147
 
130
148
  # @rbs rpc_method: String
131
- # @rbs type: ACP::AgentConnection::_Parser
132
- # @rbs request: ACP::AgentConnection::_Response
149
+ # @rbs type: ACP::Transport::_Parser
150
+ # @rbs request: ACP::Transport::_Response
133
151
  # @rbs return: untyped
134
152
  def call(rpc_method, type, request)
135
153
  result = @peer.request(rpc_method, request.to_h)
@@ -141,8 +159,8 @@ class ACP::AgentConnection::Client
141
159
  end
142
160
 
143
161
  # @rbs rpc_method: String
144
- # @rbs type: ACP::AgentConnection::_Parser
145
- # @rbs request: ACP::AgentConnection::_Response
162
+ # @rbs type: ACP::Transport::_Parser
163
+ # @rbs request: ACP::Transport::_Response
146
164
  # @rbs return: untyped
147
165
  def terminal_call(rpc_method, type, request)
148
166
  return ACP::RequestError.unadvertised('terminal') unless capabilities&.terminal
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ACP::AgentConnection::OptionalMethod::Spec
4
+ # @dynamic rpc_method
5
+ attr_reader :rpc_method #: String
6
+ # @dynamic agent_method
7
+ attr_reader :agent_method #: Symbol
8
+ # @dynamic path
9
+ attr_reader :path #: Array[Symbol]
10
+
11
+ # @rbs rpc_method: String
12
+ # @rbs agent_method: Symbol
13
+ # @rbs path: Array[Symbol]
14
+ # @rbs return: void
15
+ def initialize(rpc_method:, agent_method:, path:)
16
+ @rpc_method = rpc_method
17
+ @agent_method = agent_method
18
+ @path = path.freeze
19
+ freeze
20
+ end
21
+
22
+ # Readers missing along the chain count as unadvertised, so the stable
23
+ # capabilities answer false for the unstable-only paths (fork, providers).
24
+ #
25
+ # @rbs initialize_response: ACP::Types::InitializeResponse
26
+ # @rbs return: bool
27
+ def advertised?(initialize_response)
28
+ # reduce's U pins to the InitializeResponse argument, but the chain walks
29
+ # through unrelated capability types.
30
+ value = path.reduce(initialize_response) { |node, name| follow(node, name) } #: untyped
31
+ value ? true : false
32
+ end
33
+
34
+ private
35
+
36
+ # @rbs node: untyped
37
+ # @rbs name: Symbol
38
+ # @rbs return: untyped
39
+ def follow(node, name)
40
+ node.respond_to?(name) ? node.public_send(name) : nil
41
+ end
42
+ end
@@ -1,4 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ACP::AgentConnection::OptionalMethod
4
+ SPECS = [
5
+ Spec.new(rpc_method: 'authenticate', agent_method: :authenticate, path: %i[auth_methods any?]),
6
+ Spec.new(rpc_method: 'session/load', agent_method: :load_session, path: %i[agent_capabilities load_session]),
7
+ Spec.new(
8
+ rpc_method: 'session/list',
9
+ agent_method: :list_sessions,
10
+ path: %i[agent_capabilities session_capabilities list]
11
+ ),
12
+ Spec.new(
13
+ rpc_method: 'session/resume',
14
+ agent_method: :resume_session,
15
+ path: %i[agent_capabilities session_capabilities resume]
16
+ ),
17
+ Spec.new(
18
+ rpc_method: 'session/close',
19
+ agent_method: :close_session,
20
+ path: %i[agent_capabilities session_capabilities close]
21
+ ),
22
+ Spec.new(
23
+ rpc_method: 'session/delete',
24
+ agent_method: :delete_session,
25
+ path: %i[agent_capabilities session_capabilities delete]
26
+ ),
27
+ Spec.new(rpc_method: 'logout', agent_method: :logout, path: %i[agent_capabilities auth logout])
28
+ ].freeze #: Array[Spec]
29
+
30
+ UNSTABLE_SPECS = [
31
+ Spec.new(
32
+ rpc_method: 'session/fork',
33
+ agent_method: :fork_session,
34
+ path: %i[agent_capabilities session_capabilities fork]
35
+ ),
36
+ Spec.new(rpc_method: 'providers/list', agent_method: :list_providers, path: %i[agent_capabilities providers]),
37
+ Spec.new(rpc_method: 'providers/set', agent_method: :set_provider, path: %i[agent_capabilities providers]),
38
+ Spec.new(rpc_method: 'providers/disable', agent_method: :disable_provider, path: %i[agent_capabilities providers])
39
+ ].freeze #: Array[Spec]
4
40
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ACP::AgentConnection
4
- # @rbs @transport: ACP::AgentConnection::_Transport
4
+ # @rbs @transport: ACP::Transport::_Transport
5
5
  # @rbs @capabilities: ACP::Types::AgentCapabilities
6
6
  # @rbs @agent_info: ACP::Types::Implementation?
7
7
  # @rbs @auth_methods: Array[ACP::Types::AuthMethod::t]
@@ -10,16 +10,6 @@ class ACP::AgentConnection
10
10
  # @rbs @extension_notifications: Hash[String, ^(untyped) -> void]
11
11
  # @rbs @logger: ACP::Transport::_Logger
12
12
 
13
- PROTOCOL_VERSION = 1 #: Integer
14
- OPTIONAL = [
15
- ACP::AgentConnection::OptionalMethod::LoadSession,
16
- ACP::AgentConnection::OptionalMethod::ListSessions,
17
- ACP::AgentConnection::OptionalMethod::Authenticate,
18
- ACP::AgentConnection::OptionalMethod::ResumeSession,
19
- ACP::AgentConnection::OptionalMethod::CloseSession,
20
- ACP::AgentConnection::OptionalMethod::DeleteSession,
21
- ACP::AgentConnection::OptionalMethod::Logout
22
- ].freeze #: Array[ACP::AgentConnection::_OptionalMethod]
23
13
  # The schema has no initialize capability for modes or config options: an
24
14
  # agent offers them per session, in its session responses, so they are
25
15
  # routed whenever the agent defines them.
@@ -28,7 +18,7 @@ class ACP::AgentConnection
28
18
  'session/set_config_option' => :change_session_config_option
29
19
  }.freeze #: Hash[String, Symbol]
30
20
 
31
- # @rbs transport: ACP::AgentConnection::_Transport
21
+ # @rbs transport: ACP::Transport::_Transport
32
22
  # @rbs capabilities: ACP::Types::AgentCapabilities
33
23
  # @rbs agent_info: ACP::Types::Implementation?
34
24
  # @rbs auth_methods: Array[ACP::Types::AuthMethod::t]
@@ -61,15 +51,21 @@ class ACP::AgentConnection
61
51
 
62
52
  # @rbs return: Thread
63
53
  def start
64
- client = ACP::AgentConnection::Client.new(peer: @transport)
54
+ client = ACP::AgentConnection::Client.new(peer: @transport, mcp_advertised: mcp_acp_advertised?)
65
55
  agent = @factory.call(client)
66
56
  # Routes are fixed before a client connects, so the check sees the
67
57
  # unfiltered auth methods: a terminal-capable client still needs
68
58
  # authenticate routed.
69
- advertised, unadvertised = OPTIONAL.partition { |method| method.advertised?(initialize_response(@auth_methods)) }
59
+ advertised, unadvertised = optional_methods.partition do |method|
60
+ method.advertised?(initialize_response(@auth_methods))
61
+ end
70
62
  missing = advertised.map(&:agent_method).reject { |name| agent.respond_to?(name) }
71
63
  raise ArgumentError, "initialize advertises methods the agent lacks: #{missing.join(', ')}" unless missing.empty?
72
64
 
65
+ if mcp_acp_advertised? && !agent.respond_to?(:mcp_message_notification)
66
+ raise ArgumentError, 'initialize advertises mcpCapabilities.acp but the agent lacks mcp_message_notification'
67
+ end
68
+
73
69
  unrouted = unadvertised.map(&:rpc_method) + PER_SESSION.reject { |_, name| agent.respond_to?(name) }.keys
74
70
  @transport.start(
75
71
  requests: requests(agent, client).except(*unrouted).merge(@extension_requests),
@@ -81,7 +77,7 @@ class ACP::AgentConnection
81
77
 
82
78
  # @rbs agent: ACP::AgentConnection::_Agent
83
79
  # @rbs client: ACP::AgentConnection::Client
84
- # @rbs return: Hash[String, ^(untyped) -> (ACP::AgentConnection::_Response | ACP::RequestError | ACP::Transport::Reply)]
80
+ # @rbs return: Hash[String, ^(untyped) -> (ACP::Transport::_Response | ACP::RequestError | ACP::Transport::Reply)]
85
81
  def requests(agent, client)
86
82
  # Safe: start drops the optional handlers initialize does not advertise
87
83
  # or the agent does not define, and checks the agent defines the rest.
@@ -101,13 +97,45 @@ class ACP::AgentConnection
101
97
  'session/set_config_option' => route(ACP::Types::SetSessionConfigOptionRequest) do |request|
102
98
  full.change_session_config_option(request)
103
99
  end
100
+ }.merge(unstable_requests(full))
101
+ end
102
+
103
+ # The unstable request types are referenced while the hash is built, so
104
+ # without the guard start would raise NameError even for an agent that never
105
+ # advertises fork or providers.
106
+ #
107
+ # @rbs full: ACP::AgentConnection::_FullAgent
108
+ # @rbs return: Hash[String, ^(untyped) -> (ACP::Transport::_Response | ACP::RequestError | ACP::Transport::Reply)]
109
+ def unstable_requests(full)
110
+ return {} unless ACP::Types.unstable_enabled?
111
+
112
+ {
113
+ 'session/fork' => route(ACP::Types::Unstable::ForkSessionRequest) { |request| full.fork_session(request) },
114
+ 'providers/list' => route(ACP::Types::Unstable::ListProvidersRequest) { |request| full.list_providers(request) },
115
+ 'providers/set' => route(ACP::Types::Unstable::SetProviderRequest) { |request| full.set_provider(request) },
116
+ 'providers/disable' => route(ACP::Types::Unstable::DisableProviderRequest) { |request| full.disable_provider(request) }
104
117
  }
105
118
  end
106
119
 
120
+ # The unstable specs pair with the unstable request routes, which reference
121
+ # the unstable request classes, so both are only probed after the caller
122
+ # opted in with `require 'acp/types/unstable'`.
123
+ #
124
+ # @rbs return: Array[ACP::AgentConnection::OptionalMethod::Spec]
125
+ def optional_methods
126
+ specs = ACP::AgentConnection::OptionalMethod::SPECS
127
+ return specs unless ACP::Types.unstable_enabled?
128
+
129
+ specs + ACP::AgentConnection::OptionalMethod::UNSTABLE_SPECS
130
+ end
131
+
107
132
  # @rbs agent: ACP::AgentConnection::_Agent
108
133
  # @rbs return: Hash[String, ^(untyped) -> void]
109
134
  def notifications(agent)
110
- { 'session/cancel' => ->(params) { cancel(agent, params) } }
135
+ routes = { 'session/cancel' => ->(params) { cancel(agent, params) } }
136
+ return routes unless mcp_acp_advertised?
137
+
138
+ routes.merge('mcp/message' => ->(params) { mcp_message_notification(agent, params) })
111
139
  end
112
140
 
113
141
  # A notification has no reply to carry a parse failure.
@@ -123,11 +151,30 @@ class ACP::AgentConnection
123
151
  agent.cancel(notification)
124
152
  end
125
153
 
154
+ # @rbs return: bool
155
+ def mcp_acp_advertised?
156
+ ACP::Protocol.mcp_acp_advertised?(@capabilities)
157
+ end
158
+
159
+ # A notification has no reply to carry a parse failure.
160
+ #
161
+ # @rbs agent: ACP::AgentConnection::_Agent
162
+ # @rbs params: untyped
163
+ # @rbs return: void
164
+ def mcp_message_notification(agent, params)
165
+ notification = ACP::Types::Unstable::MessageMcpNotification.from_h(params)
166
+ rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError => e
167
+ @logger.warn("dropped malformed mcp/message: #{e.class}: #{e.message}")
168
+ else
169
+ hook = agent #: ACP::AgentConnection::_Agent & ACP::AgentConnection::_McpMessageNotification
170
+ hook.mcp_message_notification(notification)
171
+ end
172
+
126
173
  # Generated from_h raises on a missing key or a value of the wrong shape.
127
174
  #
128
- # @rbs type: ACP::AgentConnection::_Parser
129
- # @rbs &handle: (untyped) -> (ACP::AgentConnection::_Response | ACP::RequestError | ACP::Transport::Reply)
130
- # @rbs return: ^(untyped) -> (ACP::AgentConnection::_Response | ACP::RequestError | ACP::Transport::Reply)
175
+ # @rbs type: ACP::Transport::_Parser
176
+ # @rbs &handle: (untyped) -> (ACP::Transport::_Response | ACP::RequestError | ACP::Transport::Reply)
177
+ # @rbs return: ^(untyped) -> (ACP::Transport::_Response | ACP::RequestError | ACP::Transport::Reply)
131
178
  def route(type, &)
132
179
  lambda do |params|
133
180
  request = type.from_h(params)
@@ -153,7 +200,7 @@ class ACP::AgentConnection
153
200
  # @rbs return: ACP::Types::InitializeResponse
154
201
  def initialize_response(auth_methods)
155
202
  ACP::Types::InitializeResponse.new(
156
- protocol_version: PROTOCOL_VERSION,
203
+ protocol_version: ACP::Protocol::PROTOCOL_VERSION,
157
204
  agent_capabilities: @capabilities,
158
205
  auth_methods:,
159
206
  agent_info: @agent_info
@@ -2,8 +2,6 @@
2
2
 
3
3
  require 'open3'
4
4
 
5
- # A child agent process and the ACP::ClientConnection wired to its stdio.
6
- # Spawn one with ACP::AgentProcess.spawn.
7
5
  class ACP::AgentProcess
8
6
  # @rbs @connection: ACP::ClientConnection
9
7
  # @rbs @reader: Thread
@@ -21,16 +19,9 @@ class ACP::AgentProcess
21
19
  # @dynamic connection
22
20
  attr_reader :connection #: ACP::ClientConnection
23
21
 
24
- # Child stderr captured when spawn was given stderr: :capture, else nil.
25
- #
26
22
  # @dynamic stderr
27
23
  attr_reader :stderr #: String?
28
24
 
29
- # Starts a command with args, env and cwd, wires its stdio to a new
30
- # ACP::ClientConnection and starts it. With a block, yields the connection
31
- # and this process, then terminates the child when the block exits; without
32
- # one, returns the process and terminate is the caller's job.
33
- #
34
25
  # @rbs command: String
35
26
  # @rbs *args: String
36
27
  # @rbs env: Hash[String, String]?
@@ -72,10 +63,6 @@ class ACP::AgentProcess
72
63
  end
73
64
  end
74
65
 
75
- # Closes stdin, waits for the child to exit — sending TERM and then KILL if
76
- # it will not — and joins the transport reader before closing the pipe it
77
- # reads.
78
- #
79
66
  # @rbs return: void
80
67
  def terminate
81
68
  @stdin.close unless @stdin.closed?