acp-sdk 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +6 -4
  4. data/lib/acp/agent_connection/client.rb +31 -1
  5. data/lib/acp/agent_connection/optional_method/disable_provider.rb +23 -0
  6. data/lib/acp/agent_connection/optional_method/fork_session.rb +23 -0
  7. data/lib/acp/agent_connection/optional_method/list_providers.rb +23 -0
  8. data/lib/acp/agent_connection/optional_method/set_provider.rb +23 -0
  9. data/lib/acp/agent_connection.rb +73 -3
  10. data/lib/acp/client_connection.rb +108 -9
  11. data/lib/acp/request_error.rb +10 -0
  12. data/lib/acp/transport/cancellation.rb +17 -0
  13. data/lib/acp/transport/stdio.rb +10 -5
  14. data/lib/acp/types/unstable/message_mcp_response/result.rb +1 -1
  15. data/lib/acp/version.rb +1 -1
  16. data/sig/generated/acp/agent_connection/client.rbs +20 -1
  17. data/sig/generated/acp/agent_connection/optional_method/disable_provider.rbs +13 -0
  18. data/sig/generated/acp/agent_connection/optional_method/fork_session.rbs +13 -0
  19. data/sig/generated/acp/agent_connection/optional_method/list_providers.rbs +13 -0
  20. data/sig/generated/acp/agent_connection/optional_method/set_provider.rbs +13 -0
  21. data/sig/generated/acp/agent_connection.rbs +29 -0
  22. data/sig/generated/acp/client_connection.rbs +57 -1
  23. data/sig/generated/acp/request_error.rbs +8 -0
  24. data/sig/generated/acp/transport/cancellation.rbs +13 -0
  25. data/sig/generated/acp/transport/stdio.rbs +6 -2
  26. data/sig/manual/acp/agent_connection/optional_method/disable_provider.rbs +6 -0
  27. data/sig/manual/acp/agent_connection/optional_method/fork_session.rbs +6 -0
  28. data/sig/manual/acp/agent_connection/optional_method/list_providers.rbs +6 -0
  29. data/sig/manual/acp/agent_connection/optional_method/set_provider.rbs +6 -0
  30. data/sig/manual/acp/agent_connection.rbs +30 -0
  31. metadata +14 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e8a4bb93508ff8a9737b9b5b044e4b63e19ba87393490f10f522b9b63aee4396
4
- data.tar.gz: 9e62dd726471fd718c2a234b9ea65bc90ca473c67d979e207b2702edd33b30b5
3
+ metadata.gz: e0d983006184ce11ac51e9361062aa6878c01d348645f41fe226423db5d72e8b
4
+ data.tar.gz: 0d50ff69cb0c6cc743d75dcd3fd943a9ddeaa4469a8f1e8d6e54be811e3c3b39
5
5
  SHA512:
6
- metadata.gz: 50ef3954c1104ef512b757421bb0df10d3eb0be7f245c5a26c8641e815fa54d160a1d4d2c8369b9b41eb4b64affce206c584627650a5ce3446be09f88f2239b4
7
- data.tar.gz: 0e505cd00fd0fef743dc1c047892db07b1ea4bf0999b3c0bff402fc10f73609a23fcbd4310b4a8deb7822fd06d8a64d1bab517426302004c205b3eee2972a104
6
+ metadata.gz: 4296fbbf1fa985cb3913f2019528c06658fd8fcac285a238abf79073d2d5eb13d081e69da57f05476569967189f732c804f6dc851e291709c0b4052572b17d9f
7
+ data.tar.gz: 8f0a5bedfa32615f867111297c4fc593f2b567fd77a55e76dda4eabe57bd8494e225de93b470f1ac294f4b99a57797fda4d7f69704693fb8cd322e65fe650f59
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.0](https://github.com/bottrall/acp-sdk/compare/v0.9.0...v0.10.0) (2026-10-07)
4
+
5
+
6
+ ### Features
7
+
8
+ * MCP-over-ACP mcp/message (unstable) ([#89](https://github.com/bottrall/acp-sdk/issues/89)) ([3d6d30c](https://github.com/bottrall/acp-sdk/commit/3d6d30c6e31e88dd0fe78e08a5bc4308948cacdd))
9
+ * 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)
10
+ * 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))
11
+ * 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)
12
+ * 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)
13
+
3
14
  ## [0.9.0](https://github.com/bottrall/acp-sdk/compare/v0.8.0...v0.9.0) (2026-10-07)
4
15
 
5
16
 
data/README.md CHANGED
@@ -11,7 +11,7 @@ Every ACP request, response and notification is a generated Ruby class under `AC
11
11
 
12
12
  ## Installation
13
13
 
14
- Requires Ruby 4.0 or later.
14
+ Requires Ruby 3.4 or later (CI tests 3.4 and 4.0).
15
15
 
16
16
  Add the gem to your Gemfile:
17
17
 
@@ -48,11 +48,12 @@ connection.start.join
48
48
 
49
49
  The agent's contract:
50
50
 
51
- - It defines `new_session`, `prompt` and `cancel`. `load_session`, `list_sessions`, `resume_session`, `close_session` and `delete_session` are required only when `capabilities` advertises `load_session` or `session_capabilities.list`, `.resume`, `.close` or `.delete`, and `authenticate` only when `auth_methods` is non-empty, and `logout` only when `capabilities` advertises `auth.logout`; `start` raises `ArgumentError` when one is advertised but missing, and a method that is not advertised is answered with `-32601`. `change_session_mode` and `change_session_config_option` are optional, since modes and config options are offered per session rather than in `initialize`; `session/set_mode` and `session/set_config_option` are answered with `-32601` when the agent does not define them. `session_created`, if defined, runs right after the `session/new` reply is sent.
51
+ - It defines `new_session`, `prompt` and `cancel`. `load_session`, `list_sessions`, `resume_session`, `close_session` and `delete_session` are required only when `capabilities` advertises `load_session` or `session_capabilities.list`, `.resume`, `.close` or `.delete`, and `authenticate` only when `auth_methods` is non-empty, and `logout` only when `capabilities` advertises `auth.logout`; `start` raises `ArgumentError` when one is advertised but missing, and a method that is not advertised is answered with `-32601`. `change_session_mode` and `change_session_config_option` are optional, since modes and config options are offered per session rather than in `initialize`; `session/set_mode` and `session/set_config_option` are answered with `-32601` when the agent does not define them. `session_created`, if defined, runs right after the `session/new` reply is sent. `fork_session` is the same shape but unstable: `session/fork` routes only when the caller has opted into the unstable types with `require 'acp/types/unstable'` and `capabilities` advertises `session_capabilities.fork`, and is answered with `-32601` otherwise. `list_providers`, `set_provider` and `disable_provider` are the same shape but unstable too: `providers/list`, `providers/set` and `providers/disable` route only when the caller has opted into the unstable types with `require 'acp/types/unstable'` and `capabilities` advertises `providers`, and are answered with `-32601` otherwise.
52
52
  - Each request method returns its response type or an `ACP::RequestError`, which is sent as the error reply. Params that do not match the schema are answered with `-32602` before the agent sees them, with `data` naming the failing path, e.g. `{ errors: ['sessionId: expected String, got Integer'] }`.
53
53
  - A handler that raises is answered with `-32603` and the message `Internal error`. Nothing from the exception reaches the peer — its class and message are logged to the transport's logger instead, since messages can interpolate paths, queries or credentials. Return an `ACP::RequestError` from the handler when the peer should see a real error message.
54
- - `cancel` runs on the transport's reader thread, so it must return quickly: set a flag and let the prompt notice it.
54
+ - `cancel` runs on the transport's reader thread, so it must return quickly: set a flag and let the prompt notice it, and call `client.cancel_requests` to cancel the connection's outstanding requests to the client — a blocked `request_permission` or `wait_for_terminal_exit`, say. That sends `$/cancel_request` for each one, and the blocked call returns once the client answers it, with its response or the `-32800` error.
55
55
  - After a cancel, the agent must itself end the turn with `stopReason: cancelled`. `ACP::AgentConnection` does not enforce it.
56
+ - `client.cancelled?` is true in a handler when the client cancelled the request it is serving with `$/cancel_request`. The handler can end early by returning `ACP::RequestError.request_cancelled`, which is answered as `-32800`, or return a normal response, which is sent as-is — the transport sends exactly one response either way.
56
57
  - `client.capabilities` is `nil` until the client sends `initialize`.
57
58
  - `client.read_text_file` and `client.write_text_file` take an `ACP::Types::ReadTextFileRequest` or `ACP::Types::WriteTextFileRequest` and return the response type or an `ACP::RequestError`. Unless `client.capabilities` advertises `fs.read_text_file` or `fs.write_text_file`, they return `-32601` without sending anything to the client.
58
59
  - `client.create_terminal`, `client.terminal_output`, `client.wait_for_terminal_exit`, `client.kill_terminal` and `client.release_terminal` take the matching `ACP::Types` request (`CreateTerminalRequest`, `TerminalOutputRequest`, `WaitForTerminalExitRequest`, `KillTerminalRequest`, `ReleaseTerminalRequest`) and return its response type or an `ACP::RequestError`. Unless `client.capabilities` advertises `terminal`, they return `-32601` without sending anything to the client. The agent must release every terminal it creates.
@@ -64,7 +65,7 @@ Extensions are `_`-prefixed methods outside the spec. `AgentConnection` takes `e
64
65
 
65
66
  ## Driving an agent
66
67
 
67
- `ACP::ClientConnection` is the other side of the same transport. Each method takes the request's generated `ACP::Types` object and returns its response type or the agent's `ACP::RequestError`. `connect` sends `initialize`, since Ruby reserves that name for the constructor, and `authenticate` and `logout` send their namesake methods. `session_set_mode` sends `session/set_mode`, and `session_set_config_option` sends `session/set_config_option` and returns the session's complete option list, which replaces any state held from an earlier reply. A handler that raises is answered with `-32603` and the message `Internal error`, and the exception is only logged to the transport's logger.
68
+ `ACP::ClientConnection` is the other side of the same transport. Each method takes the request's generated `ACP::Types` object and returns its response type or the agent's `ACP::RequestError`. `connect` sends `initialize`, since Ruby reserves that name for the constructor, and `authenticate` and `logout` send their namesake methods. `session_set_mode` sends `session/set_mode`, and `session_set_config_option` sends `session/set_config_option` and returns the session's complete option list, which replaces any state held from an earlier reply. `session_fork` 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`. `providers_list`, `providers_set` and `providers_disable` 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
 
@@ -105,6 +106,7 @@ connection = ACP::ClientConnection.new(
105
106
 
106
107
  - `session_prompt` and `session_load` yield the session's updates on the calling thread as they arrive and return once the agent replies. Updates outside those calls, such as the available commands after `session/new`, go to `updates`, which runs on the reader thread and must return quickly. `session_resume` does not take a block: the agent must not replay history on resume, so unlike `session_load` there is nothing to stream. `session_close` and `session_delete` end the session, discarding it on the agent, and return once the agent replies.
107
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 `session_cancel` 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.
@@ -2,14 +2,20 @@
2
2
 
3
3
  class ACP::AgentConnection::Client
4
4
  # @rbs @peer: ACP::AgentConnection::_Peer
5
+ # @rbs @mcp_advertised: bool
5
6
 
6
7
  # @dynamic capabilities, capabilities=
7
8
  attr_accessor :capabilities #: ACP::Types::ClientCapabilities?
8
9
 
9
10
  # @rbs peer: ACP::AgentConnection::_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
+ # 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
+ @mcp_advertised = mcp_advertised
13
19
  @capabilities = nil
14
20
  end
15
21
 
@@ -36,6 +42,18 @@ class ACP::AgentConnection::Client
36
42
  call('session/request_permission', ACP::Types::RequestPermissionResponse, request)
37
43
  end
38
44
 
45
+ # @rbs return: void
46
+ def cancel_requests
47
+ @peer.cancel_requests
48
+ end
49
+
50
+ # Always false off a handler thread.
51
+ #
52
+ # @rbs return: bool
53
+ def cancelled?
54
+ !!@peer.cancellation&.cancelled?
55
+ end
56
+
39
57
  # @rbs request: ACP::Types::ReadTextFileRequest
40
58
  # @rbs return: ACP::Types::ReadTextFileResponse | ACP::RequestError
41
59
  def read_text_file(request)
@@ -105,6 +123,18 @@ class ACP::AgentConnection::Client
105
123
  @peer.notify('elicitation/complete', notification.to_h)
106
124
  end
107
125
 
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
+ # @rbs request: ACP::Types::Unstable::MessageMcpRequest
131
+ # @rbs return: (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError)
132
+ def mcp_message(request)
133
+ return ACP::RequestError.unadvertised_agent('mcpCapabilities.acp') unless @mcp_advertised
134
+
135
+ call('mcp/message', ACP::Types::Unstable::MessageMcpResponse, request)
136
+ end
137
+
108
138
  # Sends an extension request, keyed by its raw `_`-prefixed wire name, and
109
139
  # returns the client's reply as-is: extension methods have no schema to
110
140
  # parse the result into.
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ACP::AgentConnection::OptionalMethod::DisableProvider
4
+ extend self
5
+
6
+ # @rbs return: String
7
+ def rpc_method
8
+ 'providers/disable'
9
+ end
10
+
11
+ # @rbs return: Symbol
12
+ def agent_method
13
+ :disable_provider
14
+ end
15
+
16
+ # @rbs initialize_response: ACP::Types::InitializeResponse
17
+ # @rbs return: bool
18
+ def advertised?(initialize_response)
19
+ capabilities = initialize_response.agent_capabilities #: ACP::Types::AgentCapabilities | ACP::Types::Unstable::AgentCapabilities?
20
+ providers = capabilities.is_a?(ACP::Types::Unstable::AgentCapabilities) ? capabilities.providers : nil #: ACP::Types::Unstable::ProvidersCapabilities?
21
+ providers ? true : false
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ACP::AgentConnection::OptionalMethod::ForkSession
4
+ extend self
5
+
6
+ # @rbs return: String
7
+ def rpc_method
8
+ 'session/fork'
9
+ end
10
+
11
+ # @rbs return: Symbol
12
+ def agent_method
13
+ :fork_session
14
+ end
15
+
16
+ # @rbs initialize_response: ACP::Types::InitializeResponse
17
+ # @rbs return: bool
18
+ def advertised?(initialize_response)
19
+ session = initialize_response.agent_capabilities&.session_capabilities #: ACP::Types::SessionCapabilities | ACP::Types::Unstable::SessionCapabilities?
20
+ fork = session.is_a?(ACP::Types::Unstable::SessionCapabilities) ? session.fork : nil #: ACP::Types::Unstable::SessionForkCapabilities?
21
+ fork ? true : false
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ACP::AgentConnection::OptionalMethod::ListProviders
4
+ extend self
5
+
6
+ # @rbs return: String
7
+ def rpc_method
8
+ 'providers/list'
9
+ end
10
+
11
+ # @rbs return: Symbol
12
+ def agent_method
13
+ :list_providers
14
+ end
15
+
16
+ # @rbs initialize_response: ACP::Types::InitializeResponse
17
+ # @rbs return: bool
18
+ def advertised?(initialize_response)
19
+ capabilities = initialize_response.agent_capabilities #: ACP::Types::AgentCapabilities | ACP::Types::Unstable::AgentCapabilities?
20
+ providers = capabilities.is_a?(ACP::Types::Unstable::AgentCapabilities) ? capabilities.providers : nil #: ACP::Types::Unstable::ProvidersCapabilities?
21
+ providers ? true : false
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ACP::AgentConnection::OptionalMethod::SetProvider
4
+ extend self
5
+
6
+ # @rbs return: String
7
+ def rpc_method
8
+ 'providers/set'
9
+ end
10
+
11
+ # @rbs return: Symbol
12
+ def agent_method
13
+ :set_provider
14
+ end
15
+
16
+ # @rbs initialize_response: ACP::Types::InitializeResponse
17
+ # @rbs return: bool
18
+ def advertised?(initialize_response)
19
+ capabilities = initialize_response.agent_capabilities #: ACP::Types::AgentCapabilities | ACP::Types::Unstable::AgentCapabilities?
20
+ providers = capabilities.is_a?(ACP::Types::Unstable::AgentCapabilities) ? capabilities.providers : nil #: ACP::Types::Unstable::ProvidersCapabilities?
21
+ providers ? true : false
22
+ end
23
+ end
@@ -61,15 +61,21 @@ class ACP::AgentConnection
61
61
 
62
62
  # @rbs return: Thread
63
63
  def start
64
- client = ACP::AgentConnection::Client.new(peer: @transport)
64
+ client = ACP::AgentConnection::Client.new(peer: @transport, mcp_advertised: mcp_acp_advertised?)
65
65
  agent = @factory.call(client)
66
66
  # Routes are fixed before a client connects, so the check sees the
67
67
  # unfiltered auth methods: a terminal-capable client still needs
68
68
  # authenticate routed.
69
- advertised, unadvertised = OPTIONAL.partition { |method| method.advertised?(initialize_response(@auth_methods)) }
69
+ advertised, unadvertised = optional_methods.partition do |method|
70
+ method.advertised?(initialize_response(@auth_methods))
71
+ end
70
72
  missing = advertised.map(&:agent_method).reject { |name| agent.respond_to?(name) }
71
73
  raise ArgumentError, "initialize advertises methods the agent lacks: #{missing.join(', ')}" unless missing.empty?
72
74
 
75
+ if mcp_acp_advertised? && !agent.respond_to?(:mcp_message_notification)
76
+ raise ArgumentError, 'initialize advertises mcpCapabilities.acp but the agent lacks mcp_message_notification'
77
+ end
78
+
73
79
  unrouted = unadvertised.map(&:rpc_method) + PER_SESSION.reject { |_, name| agent.respond_to?(name) }.keys
74
80
  @transport.start(
75
81
  requests: requests(agent, client).except(*unrouted).merge(@extension_requests),
@@ -101,13 +107,49 @@ class ACP::AgentConnection
101
107
  'session/set_config_option' => route(ACP::Types::SetSessionConfigOptionRequest) do |request|
102
108
  full.change_session_config_option(request)
103
109
  end
110
+ }.merge(unstable_requests(full))
111
+ end
112
+
113
+ # The unstable request types are referenced while the hash is built, so
114
+ # without the guard start would raise NameError even for an agent that never
115
+ # advertises fork or providers.
116
+ #
117
+ # @rbs full: ACP::AgentConnection::_FullAgent
118
+ # @rbs return: Hash[String, ^(untyped) -> (ACP::AgentConnection::_Response | ACP::RequestError | ACP::Transport::Reply)]
119
+ def unstable_requests(full)
120
+ return {} unless defined?(ACP::Types::Unstable)
121
+
122
+ {
123
+ 'session/fork' => route(ACP::Types::Unstable::ForkSessionRequest) { |request| full.fork_session(request) },
124
+ 'providers/list' => route(ACP::Types::Unstable::ListProvidersRequest) { |request| full.list_providers(request) },
125
+ 'providers/set' => route(ACP::Types::Unstable::SetProviderRequest) { |request| full.set_provider(request) },
126
+ 'providers/disable' => route(ACP::Types::Unstable::DisableProviderRequest) { |request| full.disable_provider(request) }
104
127
  }
105
128
  end
106
129
 
130
+ # ForkSession's and the provider methods' advertised? reference the unstable
131
+ # types, so they can only be probed when the caller opted in with
132
+ # `require 'acp/types/unstable'`.
133
+ #
134
+ # @rbs return: Array[ACP::AgentConnection::_OptionalMethod]
135
+ def optional_methods
136
+ return OPTIONAL unless defined?(ACP::Types::Unstable)
137
+
138
+ OPTIONAL + [
139
+ ACP::AgentConnection::OptionalMethod::ForkSession,
140
+ ACP::AgentConnection::OptionalMethod::ListProviders,
141
+ ACP::AgentConnection::OptionalMethod::SetProvider,
142
+ ACP::AgentConnection::OptionalMethod::DisableProvider
143
+ ]
144
+ end
145
+
107
146
  # @rbs agent: ACP::AgentConnection::_Agent
108
147
  # @rbs return: Hash[String, ^(untyped) -> void]
109
148
  def notifications(agent)
110
- { 'session/cancel' => ->(params) { cancel(agent, params) } }
149
+ routes = { 'session/cancel' => ->(params) { cancel(agent, params) } }
150
+ return routes unless mcp_acp_advertised?
151
+
152
+ routes.merge('mcp/message' => ->(params) { mcp_message_notification(agent, params) })
111
153
  end
112
154
 
113
155
  # A notification has no reply to carry a parse failure.
@@ -123,6 +165,34 @@ class ACP::AgentConnection
123
165
  agent.cancel(notification)
124
166
  end
125
167
 
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
+ # @rbs return: bool
173
+ def mcp_acp_advertised?
174
+ return false unless defined?(ACP::Types::Unstable)
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
180
+ end
181
+
182
+ # A notification has no reply to carry a parse failure.
183
+ #
184
+ # @rbs agent: ACP::AgentConnection::_Agent
185
+ # @rbs params: untyped
186
+ # @rbs return: void
187
+ def mcp_message_notification(agent, params)
188
+ notification = ACP::Types::Unstable::MessageMcpNotification.from_h(params)
189
+ rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError => e
190
+ @logger.warn("dropped malformed mcp/message: #{e.class}: #{e.message}")
191
+ else
192
+ hook = agent #: ACP::AgentConnection::_Agent & ACP::AgentConnection::_McpMessageNotification
193
+ hook.mcp_message_notification(notification)
194
+ end
195
+
126
196
  # Generated from_h raises on a missing key or a value of the wrong shape.
127
197
  #
128
198
  # @rbs type: ACP::AgentConnection::_Parser
@@ -15,6 +15,9 @@ class ACP::ClientConnection
15
15
  # (ACP::Types::CreateElicitationResponse | ACP::RequestError))?
16
16
  # @rbs @complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?
17
17
  # @rbs @updates: ACP::ClientConnection::_UpdateHandler
18
+ # @rbs @mcp_message: (^(ACP::Types::Unstable::MessageMcpRequest) ->
19
+ # (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError))?
20
+ # @rbs @mcp_advertised: bool?
18
21
  # @rbs @fs_capabilities: ACP::Types::FileSystemCapabilities?
19
22
  # @rbs @terminal: bool?
20
23
  # @rbs @elicitation_capabilities: ACP::Types::ElicitationCapabilities?
@@ -48,6 +51,8 @@ class ACP::ClientConnection
48
51
  # (ACP::Types::CreateElicitationResponse | ACP::RequestError))?
49
52
  # @rbs complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?
50
53
  # @rbs updates: ACP::ClientConnection::_UpdateHandler
54
+ # @rbs mcp_message: (^(ACP::Types::Unstable::MessageMcpRequest) ->
55
+ # (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError))?
51
56
  # @rbs extension_requests: Hash[String, ^(untyped) -> untyped]
52
57
  # @rbs extension_notifications: Hash[String, ^(untyped) -> void]
53
58
  # @rbs logger: ACP::Transport::_Logger
@@ -65,6 +70,7 @@ class ACP::ClientConnection
65
70
  elicitation: nil,
66
71
  complete_elicitation: nil,
67
72
  updates: IGNORE,
73
+ mcp_message: nil,
68
74
  extension_requests: {},
69
75
  extension_notifications: {},
70
76
  logger: ACP::Transport::StderrLogger.new
@@ -81,6 +87,8 @@ class ACP::ClientConnection
81
87
  @elicitation = elicitation
82
88
  @complete_elicitation = complete_elicitation
83
89
  @updates = updates
90
+ @mcp_message = mcp_message
91
+ @mcp_advertised = nil
84
92
  @fs_capabilities = nil
85
93
  @terminal = nil
86
94
  @elicitation_capabilities = nil
@@ -108,7 +116,7 @@ class ACP::ClientConnection
108
116
  'terminal/kill' => method(:kill_terminal),
109
117
  'terminal/release' => method(:release_terminal),
110
118
  'elicitation/create' => method(:create_elicitation)
111
- }.merge(@extension_requests),
119
+ }.merge(mcp_requests).merge(@extension_requests),
112
120
  notifications: { 'session/update' => method(:dispatch),
113
121
  'elicitation/complete' => method(:complete_elicitation) }.merge(@extension_notifications)
114
122
  )
@@ -141,16 +149,17 @@ class ACP::ClientConnection
141
149
  @fs_capabilities = capabilities
142
150
  @terminal = terminal
143
151
  @elicitation_capabilities = elicitation
144
- parse(ACP::Types::InitializeResponse, @transport.request('initialize', request.to_h)).then do |response|
145
- next response if response.is_a?(ACP::RequestError)
152
+ result = @transport.request('initialize', request.to_h)
153
+ response = parse(ACP::Types::InitializeResponse, result)
154
+ return response if response.is_a?(ACP::RequestError)
146
155
 
147
- next response if response.protocol_version == PROTOCOL_VERSION
156
+ @mcp_advertised = agent_mcp_advertised?(result)
157
+ return response if response.protocol_version == PROTOCOL_VERSION
148
158
 
149
- ACP::RequestError.unsupported_protocol_version(
150
- requested: request.protocol_version,
151
- returned: response.protocol_version
152
- )
153
- end
159
+ ACP::RequestError.unsupported_protocol_version(
160
+ requested: request.protocol_version,
161
+ returned: response.protocol_version
162
+ )
154
163
  end
155
164
 
156
165
  # @rbs request: ACP::Types::NewSessionRequest
@@ -200,6 +209,30 @@ class ACP::ClientConnection
200
209
  parse(ACP::Types::DeleteSessionResponse, @transport.request('session/delete', request.to_h))
201
210
  end
202
211
 
212
+ # @rbs request: ACP::Types::Unstable::ForkSessionRequest
213
+ # @rbs return: (ACP::Types::Unstable::ForkSessionResponse | ACP::RequestError)
214
+ def session_fork(request)
215
+ parse(ACP::Types::Unstable::ForkSessionResponse, @transport.request('session/fork', request.to_h))
216
+ end
217
+
218
+ # @rbs request: ACP::Types::Unstable::ListProvidersRequest
219
+ # @rbs return: (ACP::Types::Unstable::ListProvidersResponse | ACP::RequestError)
220
+ def providers_list(request)
221
+ parse(ACP::Types::Unstable::ListProvidersResponse, @transport.request('providers/list', request.to_h))
222
+ end
223
+
224
+ # @rbs request: ACP::Types::Unstable::SetProviderRequest
225
+ # @rbs return: (ACP::Types::Unstable::SetProviderResponse | ACP::RequestError)
226
+ def providers_set(request)
227
+ parse(ACP::Types::Unstable::SetProviderResponse, @transport.request('providers/set', request.to_h))
228
+ end
229
+
230
+ # @rbs request: ACP::Types::Unstable::DisableProviderRequest
231
+ # @rbs return: (ACP::Types::Unstable::DisableProviderResponse | ACP::RequestError)
232
+ def providers_disable(request)
233
+ parse(ACP::Types::Unstable::DisableProviderResponse, @transport.request('providers/disable', request.to_h))
234
+ end
235
+
203
236
  # The spec requires a pending session/request_permission to be answered with
204
237
  # the cancelled outcome once the turn is cancelled, so every queue registered
205
238
  # for the session gets one before this returns. The notification goes first
@@ -252,6 +285,13 @@ class ACP::ClientConnection
252
285
  @transport.request(method, params)
253
286
  end
254
287
 
288
+ # Always false off a handler thread.
289
+ #
290
+ # @rbs return: bool
291
+ def cancelled?
292
+ !!@transport.cancellation&.cancelled?
293
+ end
294
+
255
295
  # @rbs method: String
256
296
  # @rbs params: untyped
257
297
  # @rbs return: void
@@ -260,8 +300,44 @@ class ACP::ClientConnection
260
300
  @transport.notify(method, params)
261
301
  end
262
302
 
303
+ # Unlike the mcp/message serve route, this send is not gated: a notification
304
+ # has no reply to carry a refusal, and the agent drops what it did not
305
+ # advertise.
306
+ #
307
+ # @rbs notification: ACP::Types::Unstable::MessageMcpNotification
308
+ # @rbs return: void
309
+ def mcp_message_notification(notification)
310
+ @transport.notify('mcp/message', notification.to_h)
311
+ end
312
+
263
313
  private
264
314
 
315
+ # The route is only registered when a handler serves it, but the agent's
316
+ # advertisement is only known after connect, so an unadvertised request
317
+ # still reaches the route and is refused there.
318
+ #
319
+ # @rbs return: Hash[String, ACP::Transport::Stdio::_Handler]
320
+ def mcp_requests
321
+ return {} unless @mcp_message
322
+
323
+ { 'mcp/message' => method(:mcp_message) }
324
+ end
325
+
326
+ # The stable McpCapabilities has no acp field, so the gate re-reads the raw
327
+ # initialize reply with the unstable types when they are opted into.
328
+ #
329
+ # @rbs result: (Hash[String, untyped] | ACP::RequestError)
330
+ # @rbs return: bool
331
+ def agent_mcp_advertised?(result)
332
+ return false unless defined?(ACP::Types::Unstable)
333
+ return false unless result.is_a?(Hash)
334
+
335
+ mcp = ACP::Types::Unstable::McpCapabilities.from_h(result['agentCapabilities']['mcpCapabilities'] || {})
336
+ mcp.acp ? true : false
337
+ rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError
338
+ false
339
+ end
340
+
265
341
  # @rbs type: ACP::AgentConnection::_Parser
266
342
  # @rbs result: (Hash[String, untyped] | ACP::RequestError)
267
343
  # @rbs return: untyped
@@ -340,7 +416,11 @@ class ACP::ClientConnection
340
416
  else
341
417
  replies = Thread::Queue.new
342
418
  register_permission(request.session_id, replies)
419
+ # The handler runs on this connection's own thread, so the serving
420
+ # identity must be carried over for cancelled? to see the cancel there.
421
+ serving = @transport.cancellation
343
422
  Thread.new do
423
+ ACP::Transport::Cancellation.current = serving
344
424
  # A raised StandardError is not a returned ACP::RequestError, so the two
345
425
  # need distinct shapes on the queue.
346
426
  replies << begin
@@ -490,6 +570,25 @@ class ACP::ClientConnection
490
570
  handler.call(request)
491
571
  end
492
572
 
573
+ # The route is registered at start, before connect records the agent's
574
+ # advertisement, so an unadvertised request answers -32601 until then.
575
+ #
576
+ # @rbs params: untyped
577
+ # @rbs return: (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError)
578
+ def mcp_message(params)
579
+ return ACP::RequestError.unadvertised_agent('mcpCapabilities.acp') unless @mcp_advertised
580
+
581
+ request = ACP::Types::Unstable::MessageMcpRequest.from_h(params)
582
+ rescue ACP::Types::ParseError => e
583
+ ACP::RequestError.invalid_params([e.message])
584
+ rescue KeyError, TypeError, NoMethodError
585
+ ACP::RequestError.invalid_params
586
+ else
587
+ # Safe: the route is only registered when a handler serves it.
588
+ handler = @mcp_message #: ^(ACP::Types::Unstable::MessageMcpRequest) -> (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError)
589
+ handler.call(request)
590
+ end
591
+
493
592
  # The routes are registered at start, before connect records the advertised
494
593
  # capabilities, so a request for an unadvertised mode answers -32602.
495
594
  #
@@ -82,6 +82,16 @@ class ACP::RequestError
82
82
  new(code: INVALID_PARAMS, message: "Client does not advertise #{mode}")
83
83
  end
84
84
 
85
+ # unadvertised names a capability of the client's, this one a capability of
86
+ # the agent's: the same method-not-found code, with a message naming which
87
+ # side's advertisement is missing.
88
+ #
89
+ # @rbs capability: String
90
+ # @rbs return: ACP::RequestError
91
+ def self.unadvertised_agent(capability)
92
+ new(code: METHOD_NOT_FOUND, message: "Agent does not advertise #{capability}")
93
+ end
94
+
85
95
  # @rbs return: ACP::RequestError
86
96
  def self.internal_error
87
97
  new(code: INTERNAL_ERROR, message: 'Internal error')
@@ -4,6 +4,23 @@
4
4
  # request, marks it when the peer's $/cancel_request arrives, and hands it to
5
5
  # the serving thread, whose handler can observe it and end the request early.
6
6
  class ACP::Transport::Cancellation
7
+ KEY = :acp_serving_cancellation #: Symbol
8
+
9
+ # Connections read this to expose the serving state to their handlers and
10
+ # write it to carry serving identity onto a thread they spawn; nil off a
11
+ # serve thread.
12
+ #
13
+ # @rbs return: ACP::Transport::Cancellation?
14
+ def self.current
15
+ Thread.current[KEY] #: ACP::Transport::Cancellation?
16
+ end
17
+
18
+ # @rbs cancellation: ACP::Transport::Cancellation?
19
+ # @rbs return: void
20
+ def self.current=(cancellation)
21
+ Thread.current[KEY] = cancellation
22
+ end
23
+
7
24
  # @rbs @lock: Thread::Mutex
8
25
  # @rbs @cancelled: bool
9
26
 
@@ -22,9 +22,6 @@ class ACP::Transport::Stdio
22
22
  code: ACP::RequestError::INTERNAL_ERROR, message: 'Connection closed'
23
23
  ) #: ACP::RequestError
24
24
 
25
- SERVING_CANCEL = :acp_serving_cancellation #: Symbol
26
- private_constant :SERVING_CANCEL
27
-
28
25
  # @rbs input: _Reader
29
26
  # @rbs output: _Writer
30
27
  # @rbs logger: ACP::Transport::_Logger
@@ -85,6 +82,14 @@ class ACP::Transport::Stdio
85
82
  notify('$/cancel_request', { 'requestId' => id }) if pending
86
83
  end
87
84
 
85
+ # Each id is re-checked against the pending table, so one that settled
86
+ # between the snapshot and its cancel is not cancelled.
87
+ #
88
+ # @rbs return: void
89
+ def cancel_requests
90
+ @lock.synchronize { @pending.keys }.each { |id| cancel(id) }
91
+ end
92
+
88
93
  # The cancellation of the request the calling thread is serving, or nil off
89
94
  # a serve thread. A handler observes the peer's $/cancel_request through it
90
95
  # and can end early with ACP::RequestError.request_cancelled; whatever it
@@ -92,7 +97,7 @@ class ACP::Transport::Stdio
92
97
  #
93
98
  # @rbs return: ACP::Transport::Cancellation?
94
99
  def cancellation
95
- Thread.current[SERVING_CANCEL] #: ACP::Transport::Cancellation?
100
+ ACP::Transport::Cancellation.current
96
101
  end
97
102
 
98
103
  private
@@ -167,7 +172,7 @@ class ACP::Transport::Stdio
167
172
  def serve(handler, id, params, method)
168
173
  cancellation = register_serving(id)
169
174
  Thread.new do
170
- Thread.current[SERVING_CANCEL] = cancellation
175
+ ACP::Transport::Cancellation.current = cancellation
171
176
  outcome = handler ? invoke(handler, params) : ACP::RequestError.method_not_found(method)
172
177
  case outcome
173
178
  when ACP::Transport::Reply
@@ -27,6 +27,6 @@ class ACP::Types::Unstable::MessageMcpResponse::Result
27
27
 
28
28
  # @rbs return: Hash[String, untyped]
29
29
  def to_h
30
- { 'result' => result, '_meta' => meta }.compact
30
+ { '_meta' => meta }.compact.merge('result' => result)
31
31
  end
32
32
  end
data/lib/acp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ACP
4
- VERSION = '0.9.0'
4
+ VERSION = '0.10.0'
5
5
  end
@@ -1,14 +1,17 @@
1
1
  # Generated from lib/acp/agent_connection/client.rb with RBS::Inline
2
2
 
3
3
  class ACP::AgentConnection::Client
4
+ @mcp_advertised: bool
5
+
4
6
  @peer: ACP::AgentConnection::_Peer
5
7
 
6
8
  # @dynamic capabilities, capabilities=
7
9
  attr_accessor capabilities: ACP::Types::ClientCapabilities?
8
10
 
9
11
  # @rbs peer: ACP::AgentConnection::_Peer
12
+ # @rbs mcp_advertised: bool
10
13
  # @rbs return: void
11
- def initialize: (peer: ACP::AgentConnection::_Peer) -> void
14
+ def initialize: (peer: ACP::AgentConnection::_Peer, ?mcp_advertised: bool) -> void
12
15
 
13
16
  # @rbs session_id: String
14
17
  # @rbs update: ACP::Types::SessionUpdate::t
@@ -27,6 +30,14 @@ class ACP::AgentConnection::Client
27
30
  # @rbs return: ACP::Types::RequestPermissionResponse | ACP::RequestError
28
31
  def request_permission: (ACP::Types::RequestPermissionRequest request) -> (ACP::Types::RequestPermissionResponse | ACP::RequestError)
29
32
 
33
+ # @rbs return: void
34
+ def cancel_requests: () -> void
35
+
36
+ # Always false off a handler thread.
37
+ #
38
+ # @rbs return: bool
39
+ def cancelled?: () -> bool
40
+
30
41
  # @rbs request: ACP::Types::ReadTextFileRequest
31
42
  # @rbs return: ACP::Types::ReadTextFileResponse | ACP::RequestError
32
43
  def read_text_file: (ACP::Types::ReadTextFileRequest request) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError)
@@ -63,6 +74,14 @@ class ACP::AgentConnection::Client
63
74
  # @rbs return: void
64
75
  def complete_elicitation: (ACP::Types::CompleteElicitationNotification notification) -> void
65
76
 
77
+ # Sends one inner MCP operation to the client's declared ACP MCP server. The
78
+ # reply's result may be null, and a reply matching neither carrier branch
79
+ # comes back as the raw hash.
80
+ #
81
+ # @rbs request: ACP::Types::Unstable::MessageMcpRequest
82
+ # @rbs return: (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError)
83
+ def mcp_message: (ACP::Types::Unstable::MessageMcpRequest request) -> (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError)
84
+
66
85
  # Sends an extension request, keyed by its raw `_`-prefixed wire name, and
67
86
  # returns the client's reply as-is: extension methods have no schema to
68
87
  # parse the result into.
@@ -0,0 +1,13 @@
1
+ # Generated from lib/acp/agent_connection/optional_method/disable_provider.rb with RBS::Inline
2
+
3
+ module ACP::AgentConnection::OptionalMethod::DisableProvider
4
+ # @rbs return: String
5
+ def rpc_method: () -> String
6
+
7
+ # @rbs return: Symbol
8
+ def agent_method: () -> Symbol
9
+
10
+ # @rbs initialize_response: ACP::Types::InitializeResponse
11
+ # @rbs return: bool
12
+ def advertised?: (ACP::Types::InitializeResponse initialize_response) -> bool
13
+ end
@@ -0,0 +1,13 @@
1
+ # Generated from lib/acp/agent_connection/optional_method/fork_session.rb with RBS::Inline
2
+
3
+ module ACP::AgentConnection::OptionalMethod::ForkSession
4
+ # @rbs return: String
5
+ def rpc_method: () -> String
6
+
7
+ # @rbs return: Symbol
8
+ def agent_method: () -> Symbol
9
+
10
+ # @rbs initialize_response: ACP::Types::InitializeResponse
11
+ # @rbs return: bool
12
+ def advertised?: (ACP::Types::InitializeResponse initialize_response) -> bool
13
+ end
@@ -0,0 +1,13 @@
1
+ # Generated from lib/acp/agent_connection/optional_method/list_providers.rb with RBS::Inline
2
+
3
+ module ACP::AgentConnection::OptionalMethod::ListProviders
4
+ # @rbs return: String
5
+ def rpc_method: () -> String
6
+
7
+ # @rbs return: Symbol
8
+ def agent_method: () -> Symbol
9
+
10
+ # @rbs initialize_response: ACP::Types::InitializeResponse
11
+ # @rbs return: bool
12
+ def advertised?: (ACP::Types::InitializeResponse initialize_response) -> bool
13
+ end
@@ -0,0 +1,13 @@
1
+ # Generated from lib/acp/agent_connection/optional_method/set_provider.rb with RBS::Inline
2
+
3
+ module ACP::AgentConnection::OptionalMethod::SetProvider
4
+ # @rbs return: String
5
+ def rpc_method: () -> String
6
+
7
+ # @rbs return: Symbol
8
+ def agent_method: () -> Symbol
9
+
10
+ # @rbs initialize_response: ACP::Types::InitializeResponse
11
+ # @rbs return: bool
12
+ def advertised?: (ACP::Types::InitializeResponse initialize_response) -> bool
13
+ end
@@ -47,6 +47,21 @@ class ACP::AgentConnection
47
47
  # @rbs return: Hash[String, ^(untyped) -> (ACP::AgentConnection::_Response | ACP::RequestError | ACP::Transport::Reply)]
48
48
  def requests: (ACP::AgentConnection::_Agent agent, ACP::AgentConnection::Client client) -> Hash[String, ^(untyped) -> (ACP::AgentConnection::_Response | ACP::RequestError | ACP::Transport::Reply)]
49
49
 
50
+ # The unstable request types are referenced while the hash is built, so
51
+ # without the guard start would raise NameError even for an agent that never
52
+ # advertises fork or providers.
53
+ #
54
+ # @rbs full: ACP::AgentConnection::_FullAgent
55
+ # @rbs return: Hash[String, ^(untyped) -> (ACP::AgentConnection::_Response | ACP::RequestError | ACP::Transport::Reply)]
56
+ def unstable_requests: (ACP::AgentConnection::_FullAgent full) -> Hash[String, ^(untyped) -> (ACP::AgentConnection::_Response | ACP::RequestError | ACP::Transport::Reply)]
57
+
58
+ # ForkSession's and the provider methods' advertised? reference the unstable
59
+ # types, so they can only be probed when the caller opted in with
60
+ # `require 'acp/types/unstable'`.
61
+ #
62
+ # @rbs return: Array[ACP::AgentConnection::_OptionalMethod]
63
+ def optional_methods: () -> Array[ACP::AgentConnection::_OptionalMethod]
64
+
50
65
  # @rbs agent: ACP::AgentConnection::_Agent
51
66
  # @rbs return: Hash[String, ^(untyped) -> void]
52
67
  def notifications: (ACP::AgentConnection::_Agent agent) -> Hash[String, ^(untyped) -> void]
@@ -58,6 +73,20 @@ class ACP::AgentConnection
58
73
  # @rbs return: void
59
74
  def cancel: (ACP::AgentConnection::_Agent agent, untyped params) -> void
60
75
 
76
+ # The unstable gates reference the unstable types, so they can only be
77
+ # probed when the caller opted in with `require 'acp/types/unstable'`; the
78
+ # stable McpCapabilities has no acp field at all.
79
+ #
80
+ # @rbs return: bool
81
+ def mcp_acp_advertised?: () -> bool
82
+
83
+ # A notification has no reply to carry a parse failure.
84
+ #
85
+ # @rbs agent: ACP::AgentConnection::_Agent
86
+ # @rbs params: untyped
87
+ # @rbs return: void
88
+ def mcp_message_notification: (ACP::AgentConnection::_Agent agent, untyped params) -> void
89
+
61
90
  # Generated from_h raises on a missing key or a value of the wrong shape.
62
91
  #
63
92
  # @rbs type: ACP::AgentConnection::_Parser
@@ -25,6 +25,10 @@ class ACP::ClientConnection
25
25
 
26
26
  @updates: ACP::ClientConnection::_UpdateHandler
27
27
 
28
+ @mcp_message: (^(ACP::Types::Unstable::MessageMcpRequest) -> (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError))?
29
+
30
+ @mcp_advertised: bool?
31
+
28
32
  @fs_capabilities: ACP::Types::FileSystemCapabilities?
29
33
 
30
34
  @terminal: bool?
@@ -65,11 +69,13 @@ class ACP::ClientConnection
65
69
  # (ACP::Types::CreateElicitationResponse | ACP::RequestError))?
66
70
  # @rbs complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?
67
71
  # @rbs updates: ACP::ClientConnection::_UpdateHandler
72
+ # @rbs mcp_message: (^(ACP::Types::Unstable::MessageMcpRequest) ->
73
+ # (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError))?
68
74
  # @rbs extension_requests: Hash[String, ^(untyped) -> untyped]
69
75
  # @rbs extension_notifications: Hash[String, ^(untyped) -> void]
70
76
  # @rbs logger: ACP::Transport::_Logger
71
77
  # @rbs return: void
72
- def initialize: (transport: ACP::AgentConnection::_Transport, permission: ACP::ClientConnection::_PermissionHandler, ?read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?, ?write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?, ?create_terminal: (^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError))?, ?terminal_output: (^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError))?, ?wait_for_terminal_exit: (^(ACP::Types::WaitForTerminalExitRequest) -> (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError))?, ?kill_terminal: (^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError))?, ?release_terminal: (^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError))?, ?elicitation: (^(ACP::Types::CreateElicitationRequest) -> (ACP::Types::CreateElicitationResponse | ACP::RequestError))?, ?complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?, ?updates: ACP::ClientConnection::_UpdateHandler, ?extension_requests: Hash[String, ^(untyped) -> untyped], ?extension_notifications: Hash[String, ^(untyped) -> void], ?logger: ACP::Transport::_Logger) -> void
78
+ def initialize: (transport: ACP::AgentConnection::_Transport, permission: ACP::ClientConnection::_PermissionHandler, ?read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?, ?write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?, ?create_terminal: (^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError))?, ?terminal_output: (^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError))?, ?wait_for_terminal_exit: (^(ACP::Types::WaitForTerminalExitRequest) -> (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError))?, ?kill_terminal: (^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError))?, ?release_terminal: (^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError))?, ?elicitation: (^(ACP::Types::CreateElicitationRequest) -> (ACP::Types::CreateElicitationResponse | ACP::RequestError))?, ?complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?, ?updates: ACP::ClientConnection::_UpdateHandler, ?mcp_message: (^(ACP::Types::Unstable::MessageMcpRequest) -> (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError))?, ?extension_requests: Hash[String, ^(untyped) -> untyped], ?extension_notifications: Hash[String, ^(untyped) -> void], ?logger: ACP::Transport::_Logger) -> void
73
79
 
74
80
  # @rbs return: Thread
75
81
  def start: () -> Thread
@@ -114,6 +120,22 @@ class ACP::ClientConnection
114
120
  # @rbs return: ACP::Types::DeleteSessionResponse | ACP::RequestError
115
121
  def session_delete: (ACP::Types::DeleteSessionRequest request) -> (ACP::Types::DeleteSessionResponse | ACP::RequestError)
116
122
 
123
+ # @rbs request: ACP::Types::Unstable::ForkSessionRequest
124
+ # @rbs return: (ACP::Types::Unstable::ForkSessionResponse | ACP::RequestError)
125
+ def session_fork: (ACP::Types::Unstable::ForkSessionRequest request) -> (ACP::Types::Unstable::ForkSessionResponse | ACP::RequestError)
126
+
127
+ # @rbs request: ACP::Types::Unstable::ListProvidersRequest
128
+ # @rbs return: (ACP::Types::Unstable::ListProvidersResponse | ACP::RequestError)
129
+ def providers_list: (ACP::Types::Unstable::ListProvidersRequest request) -> (ACP::Types::Unstable::ListProvidersResponse | ACP::RequestError)
130
+
131
+ # @rbs request: ACP::Types::Unstable::SetProviderRequest
132
+ # @rbs return: (ACP::Types::Unstable::SetProviderResponse | ACP::RequestError)
133
+ def providers_set: (ACP::Types::Unstable::SetProviderRequest request) -> (ACP::Types::Unstable::SetProviderResponse | ACP::RequestError)
134
+
135
+ # @rbs request: ACP::Types::Unstable::DisableProviderRequest
136
+ # @rbs return: (ACP::Types::Unstable::DisableProviderResponse | ACP::RequestError)
137
+ def providers_disable: (ACP::Types::Unstable::DisableProviderRequest request) -> (ACP::Types::Unstable::DisableProviderResponse | ACP::RequestError)
138
+
117
139
  # The spec requires a pending session/request_permission to be answered with
118
140
  # the cancelled outcome once the turn is cancelled, so every queue registered
119
141
  # for the session gets one before this returns. The notification goes first
@@ -151,13 +173,40 @@ class ACP::ClientConnection
151
173
  # @rbs return: (Hash[String, untyped] | ACP::RequestError)
152
174
  def ext_request: (String method, ?untyped params) -> (Hash[String, untyped] | ACP::RequestError)
153
175
 
176
+ # Always false off a handler thread.
177
+ #
178
+ # @rbs return: bool
179
+ def cancelled?: () -> bool
180
+
154
181
  # @rbs method: String
155
182
  # @rbs params: untyped
156
183
  # @rbs return: void
157
184
  def ext_notify: (String method, ?untyped params) -> void
158
185
 
186
+ # Unlike the mcp/message serve route, this send is not gated: a notification
187
+ # has no reply to carry a refusal, and the agent drops what it did not
188
+ # advertise.
189
+ #
190
+ # @rbs notification: ACP::Types::Unstable::MessageMcpNotification
191
+ # @rbs return: void
192
+ def mcp_message_notification: (ACP::Types::Unstable::MessageMcpNotification notification) -> void
193
+
159
194
  private
160
195
 
196
+ # The route is only registered when a handler serves it, but the agent's
197
+ # advertisement is only known after connect, so an unadvertised request
198
+ # still reaches the route and is refused there.
199
+ #
200
+ # @rbs return: Hash[String, ACP::Transport::Stdio::_Handler]
201
+ def mcp_requests: () -> Hash[String, ACP::Transport::Stdio::_Handler]
202
+
203
+ # The stable McpCapabilities has no acp field, so the gate re-reads the raw
204
+ # initialize reply with the unstable types when they are opted into.
205
+ #
206
+ # @rbs result: (Hash[String, untyped] | ACP::RequestError)
207
+ # @rbs return: bool
208
+ def agent_mcp_advertised?: (Hash[String, untyped] | ACP::RequestError result) -> bool
209
+
161
210
  # @rbs type: ACP::AgentConnection::_Parser
162
211
  # @rbs result: (Hash[String, untyped] | ACP::RequestError)
163
212
  # @rbs return: untyped
@@ -237,6 +286,13 @@ class ACP::ClientConnection
237
286
  # @rbs return: (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
238
287
  def release_terminal: (untyped params) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
239
288
 
289
+ # The route is registered at start, before connect records the agent's
290
+ # advertisement, so an unadvertised request answers -32601 until then.
291
+ #
292
+ # @rbs params: untyped
293
+ # @rbs return: (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError)
294
+ def mcp_message: (untyped params) -> (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError)
295
+
240
296
  # The routes are registered at start, before connect records the advertised
241
297
  # capabilities, so a request for an unadvertised mode answers -32602.
242
298
  #
@@ -70,6 +70,14 @@ class ACP::RequestError
70
70
  # @rbs return: ACP::RequestError
71
71
  def self.unadvertised_mode: (String mode) -> ACP::RequestError
72
72
 
73
+ # unadvertised names a capability of the client's, this one a capability of
74
+ # the agent's: the same method-not-found code, with a message naming which
75
+ # side's advertisement is missing.
76
+ #
77
+ # @rbs capability: String
78
+ # @rbs return: ACP::RequestError
79
+ def self.unadvertised_agent: (String capability) -> ACP::RequestError
80
+
73
81
  # @rbs return: ACP::RequestError
74
82
  def self.internal_error: () -> ACP::RequestError
75
83
 
@@ -4,6 +4,19 @@
4
4
  # request, marks it when the peer's $/cancel_request arrives, and hands it to
5
5
  # the serving thread, whose handler can observe it and end the request early.
6
6
  class ACP::Transport::Cancellation
7
+ KEY: Symbol
8
+
9
+ # Connections read this to expose the serving state to their handlers and
10
+ # write it to carry serving identity onto a thread they spawn; nil off a
11
+ # serve thread.
12
+ #
13
+ # @rbs return: ACP::Transport::Cancellation?
14
+ def self.current: () -> ACP::Transport::Cancellation?
15
+
16
+ # @rbs cancellation: ACP::Transport::Cancellation?
17
+ # @rbs return: void
18
+ def self.current=: (ACP::Transport::Cancellation? cancellation) -> void
19
+
7
20
  @lock: Thread::Mutex
8
21
 
9
22
  @cancelled: bool
@@ -26,8 +26,6 @@ class ACP::Transport::Stdio
26
26
 
27
27
  CONNECTION_CLOSED: ACP::RequestError
28
28
 
29
- SERVING_CANCEL: Symbol
30
-
31
29
  # @rbs input: _Reader
32
30
  # @rbs output: _Writer
33
31
  # @rbs logger: ACP::Transport::_Logger
@@ -59,6 +57,12 @@ class ACP::Transport::Stdio
59
57
  # @rbs return: void
60
58
  def cancel: (untyped id) -> void
61
59
 
60
+ # Each id is re-checked against the pending table, so one that settled
61
+ # between the snapshot and its cancel is not cancelled.
62
+ #
63
+ # @rbs return: void
64
+ def cancel_requests: () -> void
65
+
62
66
  # The cancellation of the request the calling thread is serving, or nil off
63
67
  # a serve thread. A handler observes the peer's $/cancel_request through it
64
68
  # and can end early with ACP::RequestError.request_cancelled; whatever it
@@ -0,0 +1,6 @@
1
+ # rbs-inline does not emit `extend self`, so without this reopen the module's
2
+ # methods aren't visible as singleton methods.
3
+
4
+ module ACP::AgentConnection::OptionalMethod::DisableProvider
5
+ extend ::ACP::AgentConnection::OptionalMethod::DisableProvider
6
+ end
@@ -0,0 +1,6 @@
1
+ # rbs-inline does not emit `extend self`, so without this reopen the module's
2
+ # methods aren't visible as singleton methods.
3
+
4
+ module ACP::AgentConnection::OptionalMethod::ForkSession
5
+ extend ::ACP::AgentConnection::OptionalMethod::ForkSession
6
+ end
@@ -0,0 +1,6 @@
1
+ # rbs-inline does not emit `extend self`, so without this reopen the module's
2
+ # methods aren't visible as singleton methods.
3
+
4
+ module ACP::AgentConnection::OptionalMethod::ListProviders
5
+ extend ::ACP::AgentConnection::OptionalMethod::ListProviders
6
+ end
@@ -0,0 +1,6 @@
1
+ # rbs-inline does not emit `extend self`, so without this reopen the module's
2
+ # methods aren't visible as singleton methods.
3
+
4
+ module ACP::AgentConnection::OptionalMethod::SetProvider
5
+ extend ::ACP::AgentConnection::OptionalMethod::SetProvider
6
+ end
@@ -36,6 +36,22 @@ class ACP::AgentConnection
36
36
  def logout: (ACP::Types::LogoutRequest) -> (ACP::Types::LogoutResponse | ACP::RequestError)
37
37
  end
38
38
 
39
+ interface _ForkSession
40
+ def fork_session: (ACP::Types::Unstable::ForkSessionRequest) -> (ACP::Types::Unstable::ForkSessionResponse | ACP::RequestError)
41
+ end
42
+
43
+ interface _ListProviders
44
+ def list_providers: (ACP::Types::Unstable::ListProvidersRequest) -> (ACP::Types::Unstable::ListProvidersResponse | ACP::RequestError)
45
+ end
46
+
47
+ interface _SetProvider
48
+ def set_provider: (ACP::Types::Unstable::SetProviderRequest) -> (ACP::Types::Unstable::SetProviderResponse | ACP::RequestError)
49
+ end
50
+
51
+ interface _DisableProvider
52
+ def disable_provider: (ACP::Types::Unstable::DisableProviderRequest) -> (ACP::Types::Unstable::DisableProviderResponse | ACP::RequestError)
53
+ end
54
+
39
55
  interface _ChangeSessionMode
40
56
  def change_session_mode: (ACP::Types::SetSessionModeRequest) -> (ACP::Types::SetSessionModeResponse | ACP::RequestError)
41
57
  end
@@ -53,6 +69,10 @@ class ACP::AgentConnection
53
69
  include _CloseSession
54
70
  include _DeleteSession
55
71
  include _Logout
72
+ include _ForkSession
73
+ include _ListProviders
74
+ include _SetProvider
75
+ include _DisableProvider
56
76
  include _ChangeSessionMode
57
77
  include _ChangeSessionConfigOption
58
78
  end
@@ -67,11 +87,18 @@ class ACP::AgentConnection
67
87
  def session_created: (ACP::Types::NewSessionResponse) -> void
68
88
  end
69
89
 
90
+ interface _McpMessageNotification
91
+ # Runs on the transport's reader thread, so it must return quickly.
92
+ def mcp_message_notification: (ACP::Types::Unstable::MessageMcpNotification notification) -> void
93
+ end
94
+
70
95
  # What an agent sees of ACP::AgentConnection::Client, so tests can pass a fake.
71
96
  interface _Client
72
97
  def update: (String session_id, ACP::Types::SessionUpdate::t update) -> void
73
98
  def available_commands: (String session_id, Array[ACP::Types::AvailableCommand] commands) -> void
74
99
  def request_permission: (ACP::Types::RequestPermissionRequest) -> (ACP::Types::RequestPermissionResponse | ACP::RequestError)
100
+ def cancel_requests: () -> void
101
+ def cancelled?: () -> bool
75
102
  def read_text_file: (ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError)
76
103
  def write_text_file: (ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError)
77
104
  def create_terminal: (ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError)
@@ -81,6 +108,7 @@ class ACP::AgentConnection
81
108
  def release_terminal: (ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
82
109
  def create_elicitation: (ACP::Types::CreateElicitationRequest request) -> (ACP::Types::CreateElicitationResponse | ACP::RequestError)
83
110
  def complete_elicitation: (ACP::Types::CompleteElicitationNotification notification) -> void
111
+ def mcp_message: (ACP::Types::Unstable::MessageMcpRequest request) -> (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError)
84
112
  def ext_request: (String method, ?untyped params) -> (Hash[String, untyped] | ACP::RequestError)
85
113
  def ext_notify: (String method, ?untyped params) -> void
86
114
  def capabilities: () -> ACP::Types::ClientCapabilities?
@@ -89,6 +117,8 @@ class ACP::AgentConnection
89
117
  interface _Peer
90
118
  def request: (String method, ?untyped params) -> (Hash[String, untyped] | ACP::RequestError)
91
119
  def notify: (String method, ?untyped params) -> void
120
+ def cancellation: () -> ACP::Transport::Cancellation?
121
+ def cancel_requests: () -> void
92
122
  end
93
123
 
94
124
  interface _Transport
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acp-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jake Bottrall
@@ -38,10 +38,14 @@ files:
38
38
  - lib/acp/agent_connection/optional_method/authenticate.rb
39
39
  - lib/acp/agent_connection/optional_method/close_session.rb
40
40
  - lib/acp/agent_connection/optional_method/delete_session.rb
41
+ - lib/acp/agent_connection/optional_method/disable_provider.rb
42
+ - lib/acp/agent_connection/optional_method/fork_session.rb
43
+ - lib/acp/agent_connection/optional_method/list_providers.rb
41
44
  - lib/acp/agent_connection/optional_method/list_sessions.rb
42
45
  - lib/acp/agent_connection/optional_method/load_session.rb
43
46
  - lib/acp/agent_connection/optional_method/logout.rb
44
47
  - lib/acp/agent_connection/optional_method/resume_session.rb
48
+ - lib/acp/agent_connection/optional_method/set_provider.rb
45
49
  - lib/acp/agent_process.rb
46
50
  - lib/acp/client_connection.rb
47
51
  - lib/acp/extensions.rb
@@ -398,10 +402,14 @@ files:
398
402
  - sig/generated/acp/agent_connection/optional_method/authenticate.rbs
399
403
  - sig/generated/acp/agent_connection/optional_method/close_session.rbs
400
404
  - sig/generated/acp/agent_connection/optional_method/delete_session.rbs
405
+ - sig/generated/acp/agent_connection/optional_method/disable_provider.rbs
406
+ - sig/generated/acp/agent_connection/optional_method/fork_session.rbs
407
+ - sig/generated/acp/agent_connection/optional_method/list_providers.rbs
401
408
  - sig/generated/acp/agent_connection/optional_method/list_sessions.rbs
402
409
  - sig/generated/acp/agent_connection/optional_method/load_session.rbs
403
410
  - sig/generated/acp/agent_connection/optional_method/logout.rbs
404
411
  - sig/generated/acp/agent_connection/optional_method/resume_session.rbs
412
+ - sig/generated/acp/agent_connection/optional_method/set_provider.rbs
405
413
  - sig/generated/acp/agent_process.rbs
406
414
  - sig/generated/acp/client_connection.rbs
407
415
  - sig/generated/acp/extensions.rbs
@@ -756,10 +764,14 @@ files:
756
764
  - sig/manual/acp/agent_connection/optional_method/authenticate.rbs
757
765
  - sig/manual/acp/agent_connection/optional_method/close_session.rbs
758
766
  - sig/manual/acp/agent_connection/optional_method/delete_session.rbs
767
+ - sig/manual/acp/agent_connection/optional_method/disable_provider.rbs
768
+ - sig/manual/acp/agent_connection/optional_method/fork_session.rbs
769
+ - sig/manual/acp/agent_connection/optional_method/list_providers.rbs
759
770
  - sig/manual/acp/agent_connection/optional_method/list_sessions.rbs
760
771
  - sig/manual/acp/agent_connection/optional_method/load_session.rbs
761
772
  - sig/manual/acp/agent_connection/optional_method/logout.rbs
762
773
  - sig/manual/acp/agent_connection/optional_method/resume_session.rbs
774
+ - sig/manual/acp/agent_connection/optional_method/set_provider.rbs
763
775
  - sig/manual/acp/agent_process.rbs
764
776
  - sig/manual/acp/client_connection.rbs
765
777
  - sig/manual/acp/extensions.rbs
@@ -779,7 +791,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
779
791
  requirements:
780
792
  - - ">="
781
793
  - !ruby/object:Gem::Version
782
- version: '4.0'
794
+ version: '3.4'
783
795
  required_rubygems_version: !ruby/object:Gem::Requirement
784
796
  requirements:
785
797
  - - ">="