acp-sdk 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -0
- data/README.md +20 -8
- data/lib/acp/agent_connection/client.rb +31 -13
- data/lib/acp/agent_connection/optional_method/spec.rb +42 -0
- data/lib/acp/agent_connection/optional_method.rb +36 -0
- data/lib/acp/agent_connection.rb +67 -20
- data/lib/acp/agent_process.rb +0 -13
- data/lib/acp/client_connection/streams.rb +183 -0
- data/lib/acp/client_connection.rb +280 -338
- data/lib/acp/protocol.rb +33 -0
- data/lib/acp/request_error.rb +13 -13
- data/lib/acp/transport/cancellation.rb +13 -3
- data/lib/acp/transport/reply.rb +0 -2
- data/lib/acp/transport/stderr_logger.rb +1 -2
- data/lib/acp/transport/stdio.rb +25 -26
- data/lib/acp/types/audio_content.rb +3 -46
- data/lib/acp/types/auth_method_terminal.rb +3 -59
- data/lib/acp/types/available_commands_update.rb +3 -32
- data/lib/acp/types/boolean_property_schema.rb +3 -41
- data/lib/acp/types/check.rb +210 -0
- data/lib/acp/types/config_option_update.rb +3 -32
- data/lib/acp/types/content.rb +3 -29
- data/lib/acp/types/{elicitation_url_mode → create_elicitation_request/mode/form}/scope.rb +1 -1
- data/lib/acp/types/create_elicitation_request/mode/form.rb +3 -3
- data/lib/acp/types/{elicitation_form_mode → create_elicitation_request/mode/url}/scope.rb +1 -1
- data/lib/acp/types/create_elicitation_request/mode/url.rb +3 -3
- data/lib/acp/types/current_mode_update.rb +3 -29
- data/lib/acp/types/diff.rb +3 -41
- data/lib/acp/types/elicitation_accept_action.rb +3 -26
- data/lib/acp/types/elicitation_form_mode.rb +3 -33
- data/lib/acp/types/elicitation_url_mode.rb +3 -35
- data/lib/acp/types/embedded_resource.rb +3 -44
- data/lib/acp/types/image_content.rb +3 -58
- data/lib/acp/types/integer_property_schema.rb +3 -60
- data/lib/acp/types/mcp_server_http.rb +3 -44
- data/lib/acp/types/mcp_server_sse.rb +3 -44
- data/lib/acp/types/multi_select_property_schema.rb +3 -70
- data/lib/acp/types/number_property_schema.rb +3 -60
- data/lib/acp/types/parse_error.rb +37 -0
- data/lib/acp/types/plan.rb +3 -29
- data/lib/acp/types/resource_link.rb +3 -79
- data/lib/acp/types/selected_permission_outcome.rb +3 -26
- data/lib/acp/types/session_config_boolean.rb +3 -21
- data/lib/acp/types/session_config_select.rb +3 -33
- data/lib/acp/types/session_info_update.rb +3 -42
- data/lib/acp/types/string_multi_select_items.rb +3 -29
- data/lib/acp/types/string_property_schema.rb +3 -105
- data/lib/acp/types/terminal.rb +3 -29
- data/lib/acp/types/text_content.rb +3 -40
- data/lib/acp/types/tool_call.rb +3 -115
- data/lib/acp/types/unstable/compaction_summary_chunk.rb +3 -35
- data/lib/acp/types/unstable/compaction_update.rb +3 -56
- data/lib/acp/types/unstable/idle_state_update.rb +3 -40
- data/lib/acp/types/unstable/mcp_server_acp.rb +3 -35
- data/lib/acp/types/unstable/message_mcp_response/result.rb +1 -1
- data/lib/acp/types/unstable/nes_edit_suggestion.rb +3 -61
- data/lib/acp/types/unstable/nes_jump_suggestion.rb +3 -45
- data/lib/acp/types/unstable/nes_rename_suggestion.rb +3 -51
- data/lib/acp/types/unstable/nes_search_and_replace_suggestion.rb +3 -60
- data/lib/acp/types/unstable/notice.rb +3 -41
- data/lib/acp/types/unstable/plan_file.rb +3 -35
- data/lib/acp/types/unstable/plan_items.rb +3 -38
- data/lib/acp/types/unstable/plan_markdown.rb +3 -35
- data/lib/acp/types/unstable/plan_removed.rb +3 -26
- data/lib/acp/types/unstable/plan_update.rb +3 -33
- data/lib/acp/types/unstable/requires_action_state_update.rb +3 -21
- data/lib/acp/types/unstable/running_state_update.rb +3 -21
- data/lib/acp/types/unstable/session_message.rb +3 -56
- data/lib/acp/types/unstable/session_message_chunk.rb +3 -53
- data/lib/acp/types/unstable/subagent_update.rb +3 -65
- data/lib/acp/types/unstable/unknown_state_update.rb +3 -21
- data/lib/acp/types/unstable.rb +20 -20
- data/lib/acp/types/usage_update.rb +3 -41
- data/lib/acp/types.rb +7 -246
- data/lib/acp/version.rb +1 -1
- data/sig/generated/acp/agent_connection/client.rbs +25 -15
- data/sig/generated/acp/agent_connection/optional_method/spec.rbs +32 -0
- data/sig/generated/acp/agent_connection/optional_method.rbs +3 -0
- data/sig/generated/acp/agent_connection.rbs +34 -13
- data/sig/generated/acp/agent_process.rbs +0 -13
- data/sig/generated/acp/client_connection/streams.rbs +94 -0
- data/sig/generated/acp/client_connection.rbs +128 -131
- data/sig/generated/acp/protocol.rbs +9 -0
- data/sig/generated/acp/request_error.rbs +11 -13
- data/sig/generated/acp/transport/cancellation.rbs +9 -3
- data/sig/generated/acp/transport/reply.rbs +0 -2
- data/sig/generated/acp/transport/stderr_logger.rbs +1 -2
- data/sig/generated/acp/transport/stdio.rbs +14 -22
- data/sig/generated/acp/types/audio_content.rbs +1 -27
- data/sig/generated/acp/types/auth_method_terminal.rbs +1 -35
- data/sig/generated/acp/types/available_commands_update.rbs +1 -19
- data/sig/generated/acp/types/boolean_property_schema.rbs +1 -27
- data/sig/generated/acp/types/check.rbs +115 -0
- data/sig/generated/acp/types/config_option_update.rbs +1 -19
- data/sig/generated/acp/types/content.rbs +1 -19
- data/sig/generated/acp/types/{elicitation_url_mode → create_elicitation_request/mode/form}/scope.rbs +2 -2
- data/sig/generated/acp/types/create_elicitation_request/mode/form.rbs +3 -3
- data/sig/generated/acp/types/{elicitation_form_mode → create_elicitation_request/mode/url}/scope.rbs +2 -2
- data/sig/generated/acp/types/create_elicitation_request/mode/url.rbs +3 -3
- data/sig/generated/acp/types/current_mode_update.rbs +1 -19
- data/sig/generated/acp/types/diff.rbs +1 -27
- data/sig/generated/acp/types/elicitation_accept_action.rbs +1 -15
- data/sig/generated/acp/types/elicitation_form_mode.rbs +1 -19
- data/sig/generated/acp/types/elicitation_url_mode.rbs +1 -23
- data/sig/generated/acp/types/embedded_resource.rbs +1 -23
- data/sig/generated/acp/types/image_content.rbs +1 -31
- data/sig/generated/acp/types/integer_property_schema.rbs +1 -35
- data/sig/generated/acp/types/mcp_server_http.rbs +1 -27
- data/sig/generated/acp/types/mcp_server_sse.rbs +1 -27
- data/sig/generated/acp/types/multi_select_property_schema.rbs +1 -39
- data/sig/generated/acp/types/number_property_schema.rbs +1 -35
- data/sig/generated/acp/types/parse_error.rbs +22 -0
- data/sig/generated/acp/types/plan.rbs +1 -19
- data/sig/generated/acp/types/resource_link.rbs +1 -43
- data/sig/generated/acp/types/selected_permission_outcome.rbs +1 -19
- data/sig/generated/acp/types/session_config_boolean.rbs +1 -15
- data/sig/generated/acp/types/session_config_select.rbs +1 -19
- data/sig/generated/acp/types/session_info_update.rbs +1 -23
- data/sig/generated/acp/types/string_multi_select_items.rbs +1 -19
- data/sig/generated/acp/types/string_property_schema.rbs +1 -51
- data/sig/generated/acp/types/terminal.rbs +1 -19
- data/sig/generated/acp/types/text_content.rbs +1 -23
- data/sig/generated/acp/types/tool_call.rbs +1 -51
- data/sig/generated/acp/types/unstable/compaction_summary_chunk.rbs +1 -23
- data/sig/generated/acp/types/unstable/compaction_update.rbs +1 -31
- data/sig/generated/acp/types/unstable/idle_state_update.rbs +1 -23
- data/sig/generated/acp/types/unstable/mcp_server_acp.rbs +1 -23
- data/sig/generated/acp/types/unstable/nes_edit_suggestion.rbs +1 -31
- data/sig/generated/acp/types/unstable/nes_jump_suggestion.rbs +1 -27
- data/sig/generated/acp/types/unstable/nes_rename_suggestion.rbs +1 -31
- data/sig/generated/acp/types/unstable/nes_search_and_replace_suggestion.rbs +1 -35
- data/sig/generated/acp/types/unstable/notice.rbs +1 -27
- data/sig/generated/acp/types/unstable/plan_file.rbs +1 -23
- data/sig/generated/acp/types/unstable/plan_items.rbs +1 -23
- data/sig/generated/acp/types/unstable/plan_markdown.rbs +1 -23
- data/sig/generated/acp/types/unstable/plan_removed.rbs +1 -19
- data/sig/generated/acp/types/unstable/plan_update.rbs +1 -19
- data/sig/generated/acp/types/unstable/requires_action_state_update.rbs +1 -15
- data/sig/generated/acp/types/unstable/running_state_update.rbs +1 -15
- data/sig/generated/acp/types/unstable/session_message.rbs +1 -31
- data/sig/generated/acp/types/unstable/session_message_chunk.rbs +1 -31
- data/sig/generated/acp/types/unstable/subagent_update.rbs +1 -35
- data/sig/generated/acp/types/unstable/unknown_state_update.rbs +1 -15
- data/sig/generated/acp/types/usage_update.rbs +1 -27
- data/sig/generated/acp/types.rbs +6 -143
- data/sig/manual/acp/agent_connection.rbs +28 -28
- data/sig/manual/acp/client_connection.rbs +7 -2
- data/sig/manual/acp/{agent_connection/optional_method/logout.rbs → protocol.rbs} +3 -3
- data/sig/manual/acp/transport/stdio.rbs +0 -2
- data/sig/manual/acp/transport.rbs +25 -2
- metadata +17 -29
- data/lib/acp/agent_connection/optional_method/authenticate.rb +0 -21
- data/lib/acp/agent_connection/optional_method/close_session.rb +0 -21
- data/lib/acp/agent_connection/optional_method/delete_session.rb +0 -21
- data/lib/acp/agent_connection/optional_method/list_sessions.rb +0 -21
- data/lib/acp/agent_connection/optional_method/load_session.rb +0 -21
- data/lib/acp/agent_connection/optional_method/logout.rb +0 -21
- data/lib/acp/agent_connection/optional_method/resume_session.rb +0 -21
- data/lib/acp/types/content_chunk.rb +0 -40
- data/sig/generated/acp/agent_connection/optional_method/authenticate.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/close_session.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/delete_session.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/list_sessions.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/load_session.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/logout.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/resume_session.rbs +0 -13
- data/sig/generated/acp/types/content_chunk.rbs +0 -25
- data/sig/manual/acp/agent_connection/optional_method/authenticate.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/close_session.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/delete_session.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/list_sessions.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/load_session.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/resume_session.rbs +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 96418a5250fccc2c0eb4380e15309f7658f347bc91d5df3c1218cc3794389f48
|
|
4
|
+
data.tar.gz: 52c089fd1c4238da195880f4fcbf823452272de94a6c3f6ad3a2d7c06f0580d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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. `
|
|
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.
|
|
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.
|
|
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
|
-
- `
|
|
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 `
|
|
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::
|
|
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::
|
|
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
|
-
#
|
|
109
|
-
#
|
|
110
|
-
|
|
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::
|
|
132
|
-
# @rbs request: ACP::
|
|
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::
|
|
145
|
-
# @rbs request: ACP::
|
|
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
|
data/lib/acp/agent_connection.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class ACP::AgentConnection
|
|
4
|
-
# @rbs @transport: ACP::
|
|
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::
|
|
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 =
|
|
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::
|
|
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::
|
|
129
|
-
# @rbs &handle: (untyped) -> (ACP::
|
|
130
|
-
# @rbs return: ^(untyped) -> (ACP::
|
|
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
|
data/lib/acp/agent_process.rb
CHANGED
|
@@ -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?
|