acp-sdk 0.10.0 → 0.12.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 +29 -0
- data/README.md +19 -5
- data/lib/acp/agent_connection/client.rb +7 -19
- 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 +17 -40
- data/lib/acp/agent_process.rb +0 -13
- data/lib/acp/client_connection/streams.rb +183 -0
- data/lib/acp/client_connection.rb +227 -384
- data/lib/acp/protocol.rb +33 -0
- data/lib/acp/request_error.rb +17 -17
- data/lib/acp/transport/cancellation.rb +0 -7
- data/lib/acp/transport/reply.rb +0 -2
- data/lib/acp/transport/stderr_logger.rb +1 -2
- data/lib/acp/transport/stdio/serving.rb +18 -0
- data/lib/acp/transport/stdio.rb +67 -59
- 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/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 +10 -19
- 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 +16 -24
- 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 +100 -159
- data/sig/generated/acp/protocol.rbs +9 -0
- data/sig/generated/acp/request_error.rbs +15 -17
- data/sig/generated/acp/transport/cancellation.rbs +0 -7
- 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/serving.rbs +14 -0
- data/sig/generated/acp/transport/stdio.rbs +31 -35
- 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 +0 -30
- 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 +18 -40
- 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/disable_provider.rb +0 -23
- data/lib/acp/agent_connection/optional_method/fork_session.rb +0 -23
- data/lib/acp/agent_connection/optional_method/list_providers.rb +0 -23
- 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/agent_connection/optional_method/set_provider.rb +0 -23
- 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/disable_provider.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/fork_session.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/list_providers.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/agent_connection/optional_method/set_provider.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/disable_provider.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/fork_session.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/list_providers.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
- data/sig/manual/acp/agent_connection/optional_method/set_provider.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: 25494cd3370ba23c2d4e0055acbe0cc5ca7c30dba75abdb58ed1c5a9659ba471
|
|
4
|
+
data.tar.gz: 71522f1fd9a8bf5370f97719a78bd88149c9dfecbaa986031788693cde1e1c88
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a29109841f97f7de1f5c1951e82379179741ba08100e6514949a17033e85b646eb35781b7bf01f26eb754831e510bb7de07be13cf0fa02d0817b102f5f370097
|
|
7
|
+
data.tar.gz: 594b91f54fb5722b3d884d210e485606c13a7902f1300e4fe42a467d1b2ed5725b087ad12346b4fe4e46df3fbf2d0a1d82100e6ea47091237f5017755f4264f3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.12.0](https://github.com/bottrall/acp-sdk/compare/v0.11.0...v0.12.0) (2026-10-13)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add RequestError#connection_closed? predicate ([#145](https://github.com/bottrall/acp-sdk/issues/145)) ([16692be](https://github.com/bottrall/acp-sdk/commit/16692be92531e2ddbf14d6547a1bbb431cc151d3)), closes [#108](https://github.com/bottrall/acp-sdk/issues/108)
|
|
9
|
+
* expose Stdio serving state and drain ([#148](https://github.com/bottrall/acp-sdk/issues/148)) ([e8169c1](https://github.com/bottrall/acp-sdk/commit/e8169c1902d8f8ab185cc809e14f08ea98300007)), closes [#107](https://github.com/bottrall/acp-sdk/issues/107)
|
|
10
|
+
|
|
11
|
+
## [0.11.0](https://github.com/bottrall/acp-sdk/compare/v0.10.0...v0.11.0) (2026-10-12)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* 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))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* 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)
|
|
22
|
+
* 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)
|
|
23
|
+
* 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)
|
|
24
|
+
* **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)
|
|
25
|
+
* **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)
|
|
26
|
+
* **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)
|
|
27
|
+
* 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)
|
|
28
|
+
* 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)
|
|
29
|
+
* 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)
|
|
30
|
+
* 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)
|
|
31
|
+
|
|
3
32
|
## [0.10.0](https://github.com/bottrall/acp-sdk/compare/v0.9.0...v0.10.0) (2026-10-07)
|
|
4
33
|
|
|
5
34
|
|
data/README.md
CHANGED
|
@@ -58,14 +58,17 @@ The agent's contract:
|
|
|
58
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.
|
|
59
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.
|
|
60
60
|
- `client.create_elicitation` takes an `ACP::Types::CreateElicitationRequest` and returns an `ACP::Types::CreateElicitationResponse` or an `ACP::RequestError`. Unless `client.capabilities` advertises the `elicitation.form` or `elicitation.url` mode the request asks for, it returns `-32601` without sending anything to the client. A response action the SDK does not know comes through as the raw hash the client sent. `client.complete_elicitation` takes an `ACP::Types::CompleteElicitationNotification` and sends nothing, returning `-32601`, unless the client advertises `elicitation.url`, since only URL-mode elicitations are completed.
|
|
61
|
+
- Once the transport's input ends, a `client` request still waiting for its answer and every one made afterwards return an `ACP::RequestError` whose `connection_closed?` is true. It is false for every error the client sent, even one with the same `-32603` code and `Connection closed` message, so a handler can tell a client that is gone from one that refused without matching the message.
|
|
61
62
|
|
|
62
63
|
Extensions are `_`-prefixed methods outside the spec. `AgentConnection` takes `extension_requests` and `extension_notifications` hashes keyed by the raw wire name, and `client.ext_request` and `client.ext_notify` send them to the client; a handler name without the `_` prefix raises `ArgumentError`, and an extension request with no handler is answered with `-32601`.
|
|
63
64
|
|
|
65
|
+
An embedder that manages the connection's lifecycle keeps a reference to the transport it built: `transport.serving_ids` returns the ids of the requests whose handlers are still running, and `transport.drain` blocks, with no timeout, until those handlers have replied and any `session_created` hook has run. `drain` waits only for the requests in flight when it is called, so join the reader thread `start` returns first — once the client hangs up nothing new arrives — and close the IOs after it returns. A write after either end of the output has closed is logged to the transport's logger as a warning and dropped rather than raised.
|
|
66
|
+
|
|
64
67
|
[`examples/echo_agent.rb`](https://github.com/bottrall/acp-sdk/blob/main/examples/echo_agent.rb) is a complete agent that streams updates, asks permission, handles cancellation, reads and writes files and runs commands through the client (`/read <path>`, `/write <path> <text>` and `/run <command> [args]`, each advertised as a slash command only when the client supports it), and supports `session/load`, `session/list`, `session/resume`, `session/close` and `session/delete`.
|
|
65
68
|
|
|
66
69
|
## Driving an agent
|
|
67
70
|
|
|
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. `
|
|
71
|
+
`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.
|
|
69
72
|
|
|
70
73
|
`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.
|
|
71
74
|
|
|
@@ -81,9 +84,9 @@ response = ACP::AgentProcess.spawn(
|
|
|
81
84
|
updates: ->(notification) { show(notification) }
|
|
82
85
|
) do |connection|
|
|
83
86
|
connection.connect(ACP::Types::InitializeRequest.new(protocol_version: 1))
|
|
84
|
-
session = connection.
|
|
87
|
+
session = connection.new_session(ACP::Types::NewSessionRequest.new(cwd: Dir.pwd, mcp_servers: []))
|
|
85
88
|
prompt = [ACP::Types::ContentBlock::Text.new(text: 'hello')]
|
|
86
|
-
connection.
|
|
89
|
+
connection.prompt_session(ACP::Types::PromptRequest.new(session_id: session.session_id, prompt:)) do |update|
|
|
87
90
|
print update.content.text if update.is_a?(ACP::Types::SessionUpdate::AgentMessageChunk)
|
|
88
91
|
end
|
|
89
92
|
end
|
|
@@ -104,15 +107,24 @@ connection = ACP::ClientConnection.new(
|
|
|
104
107
|
)
|
|
105
108
|
```
|
|
106
109
|
|
|
107
|
-
- `
|
|
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 `
|
|
110
|
+
- `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.
|
|
111
|
+
- `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
112
|
- 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.
|
|
110
113
|
- `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.
|
|
111
114
|
- `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.
|
|
112
115
|
- `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.
|
|
116
|
+
- Once the transport's input ends, a request still waiting for its answer and every one made afterwards return an `ACP::RequestError` whose `connection_closed?` is true. It is false for every error the agent sent, so a closed agent is told apart from a refusal without matching the message.
|
|
113
117
|
|
|
114
118
|
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`.
|
|
115
119
|
|
|
120
|
+
## Types
|
|
121
|
+
|
|
122
|
+
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:
|
|
123
|
+
|
|
124
|
+
- `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.
|
|
125
|
+
- 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.
|
|
126
|
+
- 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.
|
|
127
|
+
|
|
116
128
|
## Unstable types
|
|
117
129
|
|
|
118
130
|
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:
|
|
@@ -125,6 +137,8 @@ Until then, referencing `ACP::Types::Unstable` is a `NameError`, and the stable
|
|
|
125
137
|
|
|
126
138
|
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.
|
|
127
139
|
|
|
140
|
+
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.
|
|
141
|
+
|
|
128
142
|
## Contributing
|
|
129
143
|
|
|
130
144
|
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,20 +1,17 @@
|
|
|
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
5
|
# @rbs @mcp_advertised: bool
|
|
6
6
|
|
|
7
7
|
# @dynamic capabilities, capabilities=
|
|
8
8
|
attr_accessor :capabilities #: ACP::Types::ClientCapabilities?
|
|
9
9
|
|
|
10
|
-
# @rbs peer: ACP::
|
|
10
|
+
# @rbs peer: ACP::Transport::_Peer
|
|
11
11
|
# @rbs mcp_advertised: bool
|
|
12
12
|
# @rbs return: void
|
|
13
13
|
def initialize(peer:, mcp_advertised: false)
|
|
14
14
|
@peer = peer
|
|
15
|
-
# The agent's own initialize advertisement of mcpCapabilities.acp, which
|
|
16
|
-
# licenses this side's mcp/message sends; the client's advertisement is
|
|
17
|
-
# the separate capabilities accessor.
|
|
18
15
|
@mcp_advertised = mcp_advertised
|
|
19
16
|
@capabilities = nil
|
|
20
17
|
end
|
|
@@ -33,9 +30,6 @@ class ACP::AgentConnection::Client
|
|
|
33
30
|
update(session_id, ACP::Types::SessionUpdate::AvailableCommandsUpdate.new(available_commands: commands))
|
|
34
31
|
end
|
|
35
32
|
|
|
36
|
-
# Blocks the calling thread until the client answers or the connection
|
|
37
|
-
# closes.
|
|
38
|
-
#
|
|
39
33
|
# @rbs request: ACP::Types::RequestPermissionRequest
|
|
40
34
|
# @rbs return: ACP::Types::RequestPermissionResponse | ACP::RequestError
|
|
41
35
|
def request_permission(request)
|
|
@@ -123,10 +117,6 @@ class ACP::AgentConnection::Client
|
|
|
123
117
|
@peer.notify('elicitation/complete', notification.to_h)
|
|
124
118
|
end
|
|
125
119
|
|
|
126
|
-
# Sends one inner MCP operation to the client's declared ACP MCP server. The
|
|
127
|
-
# reply's result may be null, and a reply matching neither carrier branch
|
|
128
|
-
# comes back as the raw hash.
|
|
129
|
-
#
|
|
130
120
|
# @rbs request: ACP::Types::Unstable::MessageMcpRequest
|
|
131
121
|
# @rbs return: (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError)
|
|
132
122
|
def mcp_message(request)
|
|
@@ -135,9 +125,7 @@ class ACP::AgentConnection::Client
|
|
|
135
125
|
call('mcp/message', ACP::Types::Unstable::MessageMcpResponse, request)
|
|
136
126
|
end
|
|
137
127
|
|
|
138
|
-
#
|
|
139
|
-
# returns the client's reply as-is: extension methods have no schema to
|
|
140
|
-
# parse the result into.
|
|
128
|
+
# Extension methods have no schema to parse the result into.
|
|
141
129
|
#
|
|
142
130
|
# @rbs method: String
|
|
143
131
|
# @rbs params: untyped
|
|
@@ -158,8 +146,8 @@ class ACP::AgentConnection::Client
|
|
|
158
146
|
private
|
|
159
147
|
|
|
160
148
|
# @rbs rpc_method: String
|
|
161
|
-
# @rbs type: ACP::
|
|
162
|
-
# @rbs request: ACP::
|
|
149
|
+
# @rbs type: ACP::Transport::_Parser
|
|
150
|
+
# @rbs request: ACP::Transport::_Response
|
|
163
151
|
# @rbs return: untyped
|
|
164
152
|
def call(rpc_method, type, request)
|
|
165
153
|
result = @peer.request(rpc_method, request.to_h)
|
|
@@ -171,8 +159,8 @@ class ACP::AgentConnection::Client
|
|
|
171
159
|
end
|
|
172
160
|
|
|
173
161
|
# @rbs rpc_method: String
|
|
174
|
-
# @rbs type: ACP::
|
|
175
|
-
# @rbs request: ACP::
|
|
162
|
+
# @rbs type: ACP::Transport::_Parser
|
|
163
|
+
# @rbs request: ACP::Transport::_Response
|
|
176
164
|
# @rbs return: untyped
|
|
177
165
|
def terminal_call(rpc_method, type, request)
|
|
178
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]
|
|
@@ -87,7 +77,7 @@ class ACP::AgentConnection
|
|
|
87
77
|
|
|
88
78
|
# @rbs agent: ACP::AgentConnection::_Agent
|
|
89
79
|
# @rbs client: ACP::AgentConnection::Client
|
|
90
|
-
# @rbs return: Hash[String, ^(untyped) -> (ACP::
|
|
80
|
+
# @rbs return: Hash[String, ^(untyped) -> (ACP::Transport::_Response | ACP::RequestError | ACP::Transport::Reply)]
|
|
91
81
|
def requests(agent, client)
|
|
92
82
|
# Safe: start drops the optional handlers initialize does not advertise
|
|
93
83
|
# or the agent does not define, and checks the agent defines the rest.
|
|
@@ -115,9 +105,9 @@ class ACP::AgentConnection
|
|
|
115
105
|
# advertises fork or providers.
|
|
116
106
|
#
|
|
117
107
|
# @rbs full: ACP::AgentConnection::_FullAgent
|
|
118
|
-
# @rbs return: Hash[String, ^(untyped) -> (ACP::
|
|
108
|
+
# @rbs return: Hash[String, ^(untyped) -> (ACP::Transport::_Response | ACP::RequestError | ACP::Transport::Reply)]
|
|
119
109
|
def unstable_requests(full)
|
|
120
|
-
return {} unless
|
|
110
|
+
return {} unless ACP::Types.unstable_enabled?
|
|
121
111
|
|
|
122
112
|
{
|
|
123
113
|
'session/fork' => route(ACP::Types::Unstable::ForkSessionRequest) { |request| full.fork_session(request) },
|
|
@@ -127,20 +117,16 @@ class ACP::AgentConnection
|
|
|
127
117
|
}
|
|
128
118
|
end
|
|
129
119
|
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
# `require 'acp/types/unstable'`.
|
|
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'`.
|
|
133
123
|
#
|
|
134
|
-
# @rbs return: Array[ACP::AgentConnection::
|
|
124
|
+
# @rbs return: Array[ACP::AgentConnection::OptionalMethod::Spec]
|
|
135
125
|
def optional_methods
|
|
136
|
-
|
|
126
|
+
specs = ACP::AgentConnection::OptionalMethod::SPECS
|
|
127
|
+
return specs unless ACP::Types.unstable_enabled?
|
|
137
128
|
|
|
138
|
-
|
|
139
|
-
ACP::AgentConnection::OptionalMethod::ForkSession,
|
|
140
|
-
ACP::AgentConnection::OptionalMethod::ListProviders,
|
|
141
|
-
ACP::AgentConnection::OptionalMethod::SetProvider,
|
|
142
|
-
ACP::AgentConnection::OptionalMethod::DisableProvider
|
|
143
|
-
]
|
|
129
|
+
specs + ACP::AgentConnection::OptionalMethod::UNSTABLE_SPECS
|
|
144
130
|
end
|
|
145
131
|
|
|
146
132
|
# @rbs agent: ACP::AgentConnection::_Agent
|
|
@@ -165,18 +151,9 @@ class ACP::AgentConnection
|
|
|
165
151
|
agent.cancel(notification)
|
|
166
152
|
end
|
|
167
153
|
|
|
168
|
-
# The unstable gates reference the unstable types, so they can only be
|
|
169
|
-
# probed when the caller opted in with `require 'acp/types/unstable'`; the
|
|
170
|
-
# stable McpCapabilities has no acp field at all.
|
|
171
|
-
#
|
|
172
154
|
# @rbs return: bool
|
|
173
155
|
def mcp_acp_advertised?
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
mcp = @capabilities.mcp_capabilities #: ACP::Types::McpCapabilities | ACP::Types::Unstable::McpCapabilities?
|
|
177
|
-
return false unless mcp.is_a?(ACP::Types::Unstable::McpCapabilities)
|
|
178
|
-
|
|
179
|
-
mcp.acp ? true : false
|
|
156
|
+
ACP::Protocol.mcp_acp_advertised?(@capabilities)
|
|
180
157
|
end
|
|
181
158
|
|
|
182
159
|
# A notification has no reply to carry a parse failure.
|
|
@@ -195,9 +172,9 @@ class ACP::AgentConnection
|
|
|
195
172
|
|
|
196
173
|
# Generated from_h raises on a missing key or a value of the wrong shape.
|
|
197
174
|
#
|
|
198
|
-
# @rbs type: ACP::
|
|
199
|
-
# @rbs &handle: (untyped) -> (ACP::
|
|
200
|
-
# @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)
|
|
201
178
|
def route(type, &)
|
|
202
179
|
lambda do |params|
|
|
203
180
|
request = type.from_h(params)
|
|
@@ -223,7 +200,7 @@ class ACP::AgentConnection
|
|
|
223
200
|
# @rbs return: ACP::Types::InitializeResponse
|
|
224
201
|
def initialize_response(auth_methods)
|
|
225
202
|
ACP::Types::InitializeResponse.new(
|
|
226
|
-
protocol_version: PROTOCOL_VERSION,
|
|
203
|
+
protocol_version: ACP::Protocol::PROTOCOL_VERSION,
|
|
227
204
|
agent_capabilities: @capabilities,
|
|
228
205
|
auth_methods:,
|
|
229
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?
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# It holds no send facade of its own — requests go through the sender and
|
|
4
|
+
# answers to the agent ride the connection's routes — so it is unit-testable
|
|
5
|
+
# without a transport.
|
|
6
|
+
class ACP::ClientConnection::Streams
|
|
7
|
+
CANCELLED = ACP::Types::RequestPermissionResponse.new(
|
|
8
|
+
outcome: ACP::Types::RequestPermissionOutcome::Cancelled.new
|
|
9
|
+
) #: ACP::Types::RequestPermissionResponse
|
|
10
|
+
|
|
11
|
+
# @rbs @sender: ACP::ClientConnection::Streams::_Sender
|
|
12
|
+
# @rbs @permission: ACP::ClientConnection::_PermissionHandler
|
|
13
|
+
# @rbs @updates: ACP::ClientConnection::_UpdateHandler
|
|
14
|
+
# @rbs @complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?
|
|
15
|
+
# @rbs @logger: ACP::Transport::_Logger
|
|
16
|
+
# @rbs @lock: Thread::Mutex
|
|
17
|
+
# @rbs @streams: Hash[String, Thread::Queue]
|
|
18
|
+
# @rbs @pending_permissions: Hash[String, Array[Thread::Queue]]
|
|
19
|
+
# @rbs @outstanding_elicitations: Hash[String, bool]
|
|
20
|
+
|
|
21
|
+
# @rbs sender: ACP::ClientConnection::Streams::_Sender
|
|
22
|
+
# @rbs permission: ACP::ClientConnection::_PermissionHandler
|
|
23
|
+
# @rbs updates: ACP::ClientConnection::_UpdateHandler
|
|
24
|
+
# @rbs logger: ACP::Transport::_Logger
|
|
25
|
+
# @rbs complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?
|
|
26
|
+
# @rbs return: void
|
|
27
|
+
def initialize(sender:, permission:, updates:, logger:, complete_elicitation: nil)
|
|
28
|
+
@sender = sender
|
|
29
|
+
@permission = permission
|
|
30
|
+
@updates = updates
|
|
31
|
+
@complete_elicitation = complete_elicitation
|
|
32
|
+
@logger = logger
|
|
33
|
+
@lock = Mutex.new
|
|
34
|
+
@streams = {}
|
|
35
|
+
@pending_permissions = {}
|
|
36
|
+
@outstanding_elicitations = {}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The request waits on its own thread so the caller's thread can yield each
|
|
40
|
+
# update as it arrives. The reader thread queues an update before it settles
|
|
41
|
+
# a reply sent after it, so the queue closes only after the last one.
|
|
42
|
+
#
|
|
43
|
+
# @rbs session_id: String
|
|
44
|
+
# @rbs method: String
|
|
45
|
+
# @rbs params: Hash[String, untyped]
|
|
46
|
+
# @rbs &block: (ACP::Types::SessionUpdate::t) -> void
|
|
47
|
+
# @rbs return: (Hash[String, untyped] | ACP::RequestError)
|
|
48
|
+
def stream(session_id, method, params, &)
|
|
49
|
+
queue = Thread::Queue.new
|
|
50
|
+
# The check and the insert share one lock acquisition so a concurrent
|
|
51
|
+
# stream cannot register between them.
|
|
52
|
+
claimed = @lock.synchronize do
|
|
53
|
+
if @streams.key?(session_id)
|
|
54
|
+
false
|
|
55
|
+
else
|
|
56
|
+
@streams[session_id] = queue
|
|
57
|
+
true
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
unless claimed
|
|
61
|
+
return ACP::RequestError.new(
|
|
62
|
+
code: ACP::RequestError::INVALID_REQUEST, message: "A stream is already open for session #{session_id}"
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
requester = Thread.new do
|
|
66
|
+
@sender.request(method, params)
|
|
67
|
+
ensure
|
|
68
|
+
@lock.synchronize { @streams.delete(session_id)&.close }
|
|
69
|
+
end
|
|
70
|
+
while (update = queue.pop)
|
|
71
|
+
yield update
|
|
72
|
+
end
|
|
73
|
+
requester.value
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Pushes under the lock so a stream cannot close its queue between the
|
|
77
|
+
# lookup and the push. A malformed notification has no reply to carry the
|
|
78
|
+
# failure.
|
|
79
|
+
#
|
|
80
|
+
# @rbs params: untyped
|
|
81
|
+
# @rbs return: void
|
|
82
|
+
def dispatch(params)
|
|
83
|
+
notification = ACP::Types::SessionNotification.from_h(params)
|
|
84
|
+
rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError => e
|
|
85
|
+
@logger.warn("dropped malformed session/update: #{e.class}: #{e.message}")
|
|
86
|
+
else
|
|
87
|
+
queue = @lock.synchronize { @streams[notification.session_id]&.push(notification.update) }
|
|
88
|
+
@updates.call(notification) unless queue
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The handler runs on its own thread so a cancelled request is answered
|
|
92
|
+
# without waiting for it, and whatever it returns after that is pushed to a
|
|
93
|
+
# queue nobody reads.
|
|
94
|
+
#
|
|
95
|
+
# @rbs params: untyped
|
|
96
|
+
# @rbs return: (ACP::Types::RequestPermissionResponse | ACP::RequestError)
|
|
97
|
+
def request_permission(params)
|
|
98
|
+
request = ACP::Types::RequestPermissionRequest.from_h(params)
|
|
99
|
+
rescue ACP::Types::ParseError => e
|
|
100
|
+
ACP::RequestError.invalid_params([e.message])
|
|
101
|
+
rescue KeyError, TypeError, NoMethodError
|
|
102
|
+
ACP::RequestError.invalid_params
|
|
103
|
+
else
|
|
104
|
+
replies = Thread::Queue.new
|
|
105
|
+
register_permission(request.session_id, replies)
|
|
106
|
+
# The handler runs on the transport's serve thread, so the serving
|
|
107
|
+
# identity must be carried over for cancelled? to see the cancel there.
|
|
108
|
+
serving = @sender.cancellation
|
|
109
|
+
Thread.new do
|
|
110
|
+
ACP::Transport::Cancellation.current = serving
|
|
111
|
+
# A raised StandardError is not a returned ACP::RequestError, so the two
|
|
112
|
+
# need distinct shapes on the queue.
|
|
113
|
+
replies << begin
|
|
114
|
+
[:value, @permission.call(request)]
|
|
115
|
+
rescue StandardError => e
|
|
116
|
+
[:raised, e]
|
|
117
|
+
end
|
|
118
|
+
ensure
|
|
119
|
+
unregister_permission(request.session_id, replies)
|
|
120
|
+
end
|
|
121
|
+
settled = replies.pop
|
|
122
|
+
# Re-raised so the transport's invoke turns it into an error reply, as it
|
|
123
|
+
# would if the handler ran on the serve thread itself.
|
|
124
|
+
raise settled[1] if settled[0] == :raised
|
|
125
|
+
|
|
126
|
+
settled[1]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# The spec requires a pending session/request_permission to be answered with
|
|
130
|
+
# the cancelled outcome once the turn is cancelled, so every queue registered
|
|
131
|
+
# for the session gets one before this returns.
|
|
132
|
+
#
|
|
133
|
+
# @rbs session_id: String
|
|
134
|
+
# @rbs return: void
|
|
135
|
+
def cancel(session_id)
|
|
136
|
+
pending = @lock.synchronize { @pending_permissions.delete(session_id) }
|
|
137
|
+
pending&.each { |replies| replies << [:value, CANCELLED] }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @rbs elicitation_id: String
|
|
141
|
+
# @rbs return: void
|
|
142
|
+
def register_elicitation(elicitation_id)
|
|
143
|
+
@lock.synchronize { @outstanding_elicitations[elicitation_id] = true }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# The spec requires a completion for an unknown or already-completed
|
|
147
|
+
# elicitation id to be ignored, so only an id registered by a served
|
|
148
|
+
# url-mode request reaches the handler. It runs on the transport's reader
|
|
149
|
+
# thread, so it must return quickly.
|
|
150
|
+
#
|
|
151
|
+
# @rbs params: untyped
|
|
152
|
+
# @rbs return: void
|
|
153
|
+
def complete_elicitation(params)
|
|
154
|
+
notification = ACP::Types::CompleteElicitationNotification.from_h(params)
|
|
155
|
+
rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError => e
|
|
156
|
+
@logger.warn("dropped malformed elicitation/complete: #{e.class}: #{e.message}")
|
|
157
|
+
else
|
|
158
|
+
known = @lock.synchronize { @outstanding_elicitations.delete(notification.elicitation_id) }
|
|
159
|
+
@complete_elicitation&.call(notification) if known
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
# @rbs session_id: String
|
|
165
|
+
# @rbs replies: Thread::Queue
|
|
166
|
+
# @rbs return: void
|
|
167
|
+
def register_permission(session_id, replies)
|
|
168
|
+
@lock.synchronize { (@pending_permissions[session_id] ||= []) << replies }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# @rbs session_id: String
|
|
172
|
+
# @rbs replies: Thread::Queue
|
|
173
|
+
# @rbs return: void
|
|
174
|
+
def unregister_permission(session_id, replies)
|
|
175
|
+
@lock.synchronize do
|
|
176
|
+
pending = @pending_permissions[session_id]
|
|
177
|
+
if pending
|
|
178
|
+
pending.delete(replies)
|
|
179
|
+
@pending_permissions.delete(session_id) if pending.empty?
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|