acp-sdk 0.11.0 → 0.13.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 +19 -0
- data/README.md +4 -0
- data/lib/acp/agent_connection/client.rb +2 -2
- data/lib/acp/agent_connection.rb +2 -2
- data/lib/acp/client_connection/streams.rb +2 -2
- data/lib/acp/client_connection.rb +2 -2
- data/lib/acp/request_error.rb +10 -0
- data/lib/acp/transport/stdio/serving.rb +18 -0
- data/lib/acp/transport/stdio.rb +52 -38
- data/lib/acp/version.rb +1 -1
- data/sig/generated/acp/agent_connection/client.rbs +3 -3
- data/sig/generated/acp/agent_connection.rbs +3 -3
- data/sig/generated/acp/client_connection/streams.rbs +3 -3
- data/sig/generated/acp/client_connection.rbs +3 -3
- data/sig/generated/acp/request_error.rbs +8 -0
- data/sig/generated/acp/transport/stdio/serving.rbs +14 -0
- data/sig/generated/acp/transport/stdio.rbs +23 -15
- data/sig/manual/acp/agent_connection.rbs +0 -22
- data/sig/manual/acp/client_connection.rbs +0 -7
- data/sig/manual/acp/transport.rbs +0 -16
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fdea95657cb02b7563bbc7730fe4b346cb7e80f08ab78071b0da79e3d46704e3
|
|
4
|
+
data.tar.gz: 57efa048c5f5959d842e89d750ea2bd92e6caf4e4d6fc0c54964f2eaca577776
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d4120fff675aa4c1afa4c9101b5403997f6a281c1eab85c34b2515958e4ee87fd0a13e1083015a66a0549a88e1b18d7aa264b948277a6650ad4062fffef6d046
|
|
7
|
+
data.tar.gz: 3cba7edd6dccf7300211347d17114550588cd1ce7ba581f4da53f4d7623beffe923fd1912dd9a7c42e698d823f2dfad4a9344920a5962c3e9e77c39467d51409
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.13.0](https://github.com/bottrall/acp-sdk/compare/v0.12.0...v0.13.0) (2026-10-13)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* the ACP::Transport::_Transport, ACP::Transport::_Peer, ACP::ClientConnection::Streams::_Sender and ACP::AgentConnection::_Client interfaces no longer exist. Name ACP::Transport::Stdio and ACP::AgentConnection::Client instead.
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* type transport and client seams as concrete classes ([#149](https://github.com/bottrall/acp-sdk/issues/149)) ([89805a0](https://github.com/bottrall/acp-sdk/commit/89805a0a462701a9528a059a63dc5147ba2285c2))
|
|
13
|
+
|
|
14
|
+
## [0.12.0](https://github.com/bottrall/acp-sdk/compare/v0.11.0...v0.12.0) (2026-10-13)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* 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)
|
|
20
|
+
* 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)
|
|
21
|
+
|
|
3
22
|
## [0.11.0](https://github.com/bottrall/acp-sdk/compare/v0.10.0...v0.11.0) (2026-10-12)
|
|
4
23
|
|
|
5
24
|
|
data/README.md
CHANGED
|
@@ -58,9 +58,12 @@ 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
|
|
@@ -110,6 +113,7 @@ connection = ACP::ClientConnection.new(
|
|
|
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
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class ACP::AgentConnection::Client
|
|
4
|
-
# @rbs @peer: ACP::Transport::
|
|
4
|
+
# @rbs @peer: ACP::Transport::Stdio
|
|
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::Transport::
|
|
10
|
+
# @rbs peer: ACP::Transport::Stdio
|
|
11
11
|
# @rbs mcp_advertised: bool
|
|
12
12
|
# @rbs return: void
|
|
13
13
|
def initialize(peer:, mcp_advertised: false)
|
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::Transport::
|
|
4
|
+
# @rbs @transport: ACP::Transport::Stdio
|
|
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]
|
|
@@ -18,7 +18,7 @@ class ACP::AgentConnection
|
|
|
18
18
|
'session/set_config_option' => :change_session_config_option
|
|
19
19
|
}.freeze #: Hash[String, Symbol]
|
|
20
20
|
|
|
21
|
-
# @rbs transport: ACP::Transport::
|
|
21
|
+
# @rbs transport: ACP::Transport::Stdio
|
|
22
22
|
# @rbs capabilities: ACP::Types::AgentCapabilities
|
|
23
23
|
# @rbs agent_info: ACP::Types::Implementation?
|
|
24
24
|
# @rbs auth_methods: Array[ACP::Types::AuthMethod::t]
|
|
@@ -8,7 +8,7 @@ class ACP::ClientConnection::Streams
|
|
|
8
8
|
outcome: ACP::Types::RequestPermissionOutcome::Cancelled.new
|
|
9
9
|
) #: ACP::Types::RequestPermissionResponse
|
|
10
10
|
|
|
11
|
-
# @rbs @sender: ACP::
|
|
11
|
+
# @rbs @sender: ACP::Transport::Stdio
|
|
12
12
|
# @rbs @permission: ACP::ClientConnection::_PermissionHandler
|
|
13
13
|
# @rbs @updates: ACP::ClientConnection::_UpdateHandler
|
|
14
14
|
# @rbs @complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?
|
|
@@ -18,7 +18,7 @@ class ACP::ClientConnection::Streams
|
|
|
18
18
|
# @rbs @pending_permissions: Hash[String, Array[Thread::Queue]]
|
|
19
19
|
# @rbs @outstanding_elicitations: Hash[String, bool]
|
|
20
20
|
|
|
21
|
-
# @rbs sender: ACP::
|
|
21
|
+
# @rbs sender: ACP::Transport::Stdio
|
|
22
22
|
# @rbs permission: ACP::ClientConnection::_PermissionHandler
|
|
23
23
|
# @rbs updates: ACP::ClientConnection::_UpdateHandler
|
|
24
24
|
# @rbs logger: ACP::Transport::_Logger
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class ACP::ClientConnection
|
|
4
|
-
# @rbs @transport: ACP::Transport::
|
|
4
|
+
# @rbs @transport: ACP::Transport::Stdio
|
|
5
5
|
# @rbs @permission: ACP::ClientConnection::_PermissionHandler
|
|
6
6
|
# @rbs @read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
7
7
|
# @rbs @write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
|
@@ -145,7 +145,7 @@ class ACP::ClientConnection
|
|
|
145
145
|
)
|
|
146
146
|
].freeze #: Array[ACP::ClientConnection::Route]
|
|
147
147
|
|
|
148
|
-
# @rbs transport: ACP::Transport::
|
|
148
|
+
# @rbs transport: ACP::Transport::Stdio
|
|
149
149
|
# @rbs permission: ACP::ClientConnection::_PermissionHandler
|
|
150
150
|
# @rbs read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
151
151
|
# @rbs write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
data/lib/acp/request_error.rb
CHANGED
|
@@ -36,6 +36,16 @@ class ACP::RequestError
|
|
|
36
36
|
{ 'code' => code, 'message' => message, 'data' => data }.compact
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
CONNECTION_CLOSED = new(code: INTERNAL_ERROR, message: 'Connection closed') #: ACP::RequestError
|
|
40
|
+
|
|
41
|
+
# Identity rather than code and message: a live peer can answer with the
|
|
42
|
+
# same pair, and that refusal must not read as a closed connection.
|
|
43
|
+
#
|
|
44
|
+
# @rbs return: bool
|
|
45
|
+
def connection_closed?
|
|
46
|
+
equal?(CONNECTION_CLOSED)
|
|
47
|
+
end
|
|
48
|
+
|
|
39
49
|
# @rbs return: ACP::RequestError
|
|
40
50
|
def self.parse_error
|
|
41
51
|
new(code: PARSE_ERROR, message: 'Parse error')
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ACP::Transport::Stdio::Serving
|
|
4
|
+
# @dynamic id
|
|
5
|
+
attr_reader :id #: untyped
|
|
6
|
+
|
|
7
|
+
# @dynamic cancellation
|
|
8
|
+
attr_reader :cancellation #: ACP::Transport::Cancellation
|
|
9
|
+
|
|
10
|
+
# @rbs id: untyped
|
|
11
|
+
# @rbs cancellation: ACP::Transport::Cancellation
|
|
12
|
+
# @rbs return: void
|
|
13
|
+
def initialize(id:, cancellation:)
|
|
14
|
+
@id = id
|
|
15
|
+
@cancellation = cancellation
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/acp/transport/stdio.rb
CHANGED
|
@@ -12,14 +12,12 @@ class ACP::Transport::Stdio
|
|
|
12
12
|
# @rbs @write_lock: Thread::Mutex
|
|
13
13
|
# @rbs @lock: Thread::Mutex
|
|
14
14
|
# @rbs @pending: Hash[untyped, Thread::Queue]
|
|
15
|
-
# @rbs @serving: Hash[
|
|
15
|
+
# @rbs @serving: Hash[Thread, ACP::Transport::Stdio::Serving]
|
|
16
16
|
# @rbs @next_id: Integer
|
|
17
17
|
# @rbs @closed: bool
|
|
18
18
|
# @rbs @logger: ACP::Transport::_Logger
|
|
19
19
|
|
|
20
|
-
CONNECTION_CLOSED = ACP::RequestError
|
|
21
|
-
code: ACP::RequestError::INTERNAL_ERROR, message: 'Connection closed'
|
|
22
|
-
) #: ACP::RequestError
|
|
20
|
+
CONNECTION_CLOSED = ACP::RequestError::CONNECTION_CLOSED #: ACP::RequestError
|
|
23
21
|
|
|
24
22
|
# @rbs input: _Reader
|
|
25
23
|
# @rbs output: _Writer
|
|
@@ -37,6 +35,13 @@ class ACP::Transport::Stdio
|
|
|
37
35
|
@logger = logger
|
|
38
36
|
end
|
|
39
37
|
|
|
38
|
+
# Joining the returned thread does not wait for the requests still being
|
|
39
|
+
# served. EOF cancels none of them (only the peer's $/cancel_request does):
|
|
40
|
+
# each handler runs to completion and its reply is still written, to be
|
|
41
|
+
# warned and dropped if the peer is gone. A caller blocked in #request, a
|
|
42
|
+
# handler waiting on its own request included, is released with
|
|
43
|
+
# CONNECTION_CLOSED.
|
|
44
|
+
#
|
|
40
45
|
# @rbs requests: Hash[String, _Handler]
|
|
41
46
|
# @rbs notifications: Hash[String, _NotificationHandler]
|
|
42
47
|
# @rbs return: Thread
|
|
@@ -93,6 +98,19 @@ class ACP::Transport::Stdio
|
|
|
93
98
|
ACP::Transport::Cancellation.current
|
|
94
99
|
end
|
|
95
100
|
|
|
101
|
+
# @rbs return: Array[untyped]
|
|
102
|
+
def serving_ids
|
|
103
|
+
@lock.synchronize { @serving.each_value.map(&:id) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Joins the requests in flight at the call, not one the reader dispatches
|
|
107
|
+
# meanwhile: join the reader thread first to leave nothing in flight.
|
|
108
|
+
#
|
|
109
|
+
# @rbs return: void
|
|
110
|
+
def drain
|
|
111
|
+
@lock.synchronize { @serving.keys }.each(&:join)
|
|
112
|
+
end
|
|
113
|
+
|
|
96
114
|
private
|
|
97
115
|
|
|
98
116
|
# @rbs line: String
|
|
@@ -167,39 +185,29 @@ class ACP::Transport::Stdio
|
|
|
167
185
|
# @rbs method: String
|
|
168
186
|
# @rbs return: void
|
|
169
187
|
def serve(handler, id, params, method)
|
|
170
|
-
cancellation =
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
188
|
+
cancellation = ACP::Transport::Cancellation.new
|
|
189
|
+
# Registered on the reader thread so a cancel racing the request cannot
|
|
190
|
+
# be missed. The serve thread starts under the lock so it is registered
|
|
191
|
+
# before a drain can miss it or it can unregister itself.
|
|
192
|
+
@lock.synchronize do
|
|
193
|
+
thread = Thread.new do
|
|
194
|
+
ACP::Transport::Cancellation.current = cancellation
|
|
195
|
+
outcome = handler ? invoke(handler, params) : ACP::RequestError.method_not_found(method)
|
|
196
|
+
case outcome
|
|
197
|
+
when ACP::Transport::Reply
|
|
198
|
+
reply(id, outcome.result)
|
|
199
|
+
quietly('reply after callback', &outcome.after)
|
|
200
|
+
else reply(id, outcome)
|
|
201
|
+
end
|
|
202
|
+
ensure
|
|
203
|
+
# Unregistered only after the reply and its after callback, so drain waits
|
|
204
|
+
# for both and later cancels for the id are ignored.
|
|
205
|
+
@lock.synchronize { @serving.delete(Thread.current) }
|
|
179
206
|
end
|
|
180
|
-
|
|
181
|
-
unregister_serving(id)
|
|
207
|
+
@serving[thread] = Serving.new(id:, cancellation:)
|
|
182
208
|
end
|
|
183
209
|
end
|
|
184
210
|
|
|
185
|
-
# Registered before the serve thread starts so a cancel racing the request
|
|
186
|
-
# cannot be missed, and dropped once the response is on the wire so later
|
|
187
|
-
# cancels for the id are ignored.
|
|
188
|
-
#
|
|
189
|
-
# @rbs id: untyped
|
|
190
|
-
# @rbs return: ACP::Transport::Cancellation
|
|
191
|
-
def register_serving(id)
|
|
192
|
-
cancellation = ACP::Transport::Cancellation.new
|
|
193
|
-
@lock.synchronize { @serving[id] = cancellation }
|
|
194
|
-
cancellation
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# @rbs id: untyped
|
|
198
|
-
# @rbs return: void
|
|
199
|
-
def unregister_serving(id)
|
|
200
|
-
@lock.synchronize { @serving.delete(id) }
|
|
201
|
-
end
|
|
202
|
-
|
|
203
211
|
# A notification has no reply to carry a malformed params failure, so one
|
|
204
212
|
# is warned and dropped rather than answered.
|
|
205
213
|
#
|
|
@@ -211,8 +219,10 @@ class ACP::Transport::Stdio
|
|
|
211
219
|
return @logger.warn('dropped malformed $/cancel_request: missing requestId')
|
|
212
220
|
end
|
|
213
221
|
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
cancellations = @lock.synchronize do
|
|
223
|
+
@serving.each_value.filter_map { |entry| entry.cancellation if entry.id == id }
|
|
224
|
+
end
|
|
225
|
+
cancellations.each(&:cancel)
|
|
216
226
|
end
|
|
217
227
|
|
|
218
228
|
# For code with no reply to carry an error: a notification handler, or a
|
|
@@ -273,6 +283,9 @@ class ACP::Transport::Stdio
|
|
|
273
283
|
@lock.synchronize { @pending.delete(id) }&.tap { |queue| queue.push(result) }
|
|
274
284
|
end
|
|
275
285
|
|
|
286
|
+
# Leaves @serving alone on purpose: embedders rely on in-flight handlers
|
|
287
|
+
# running to completion after the input ends.
|
|
288
|
+
#
|
|
276
289
|
# @rbs return: void
|
|
277
290
|
def close
|
|
278
291
|
@lock.synchronize do
|
|
@@ -282,8 +295,9 @@ class ACP::Transport::Stdio
|
|
|
282
295
|
end
|
|
283
296
|
end
|
|
284
297
|
|
|
285
|
-
# A closed peer
|
|
286
|
-
#
|
|
298
|
+
# A closed peer (EPIPE) or an output the embedder already closed (IOError)
|
|
299
|
+
# must not kill the thread writing the reply: the reader has to keep
|
|
300
|
+
# draining input so pending requests still settle at EOF.
|
|
287
301
|
#
|
|
288
302
|
# @rbs message: Hash[String, untyped]
|
|
289
303
|
# @rbs return: void
|
|
@@ -293,7 +307,7 @@ class ACP::Transport::Stdio
|
|
|
293
307
|
@output.write(line)
|
|
294
308
|
@output.flush
|
|
295
309
|
end
|
|
296
|
-
rescue Errno::EPIPE => e
|
|
310
|
+
rescue Errno::EPIPE, IOError => e
|
|
297
311
|
@logger.warn("write to closed peer: #{e.class}: #{e.message}")
|
|
298
312
|
end
|
|
299
313
|
end
|
data/lib/acp/version.rb
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
class ACP::AgentConnection::Client
|
|
4
4
|
@mcp_advertised: bool
|
|
5
5
|
|
|
6
|
-
@peer: ACP::Transport::
|
|
6
|
+
@peer: ACP::Transport::Stdio
|
|
7
7
|
|
|
8
8
|
# @dynamic capabilities, capabilities=
|
|
9
9
|
attr_accessor capabilities: ACP::Types::ClientCapabilities?
|
|
10
10
|
|
|
11
|
-
# @rbs peer: ACP::Transport::
|
|
11
|
+
# @rbs peer: ACP::Transport::Stdio
|
|
12
12
|
# @rbs mcp_advertised: bool
|
|
13
13
|
# @rbs return: void
|
|
14
|
-
def initialize: (peer: ACP::Transport::
|
|
14
|
+
def initialize: (peer: ACP::Transport::Stdio, ?mcp_advertised: bool) -> void
|
|
15
15
|
|
|
16
16
|
# @rbs session_id: String
|
|
17
17
|
# @rbs update: ACP::Types::SessionUpdate::t
|
|
@@ -15,14 +15,14 @@ class ACP::AgentConnection
|
|
|
15
15
|
|
|
16
16
|
@capabilities: ACP::Types::AgentCapabilities
|
|
17
17
|
|
|
18
|
-
@transport: ACP::Transport::
|
|
18
|
+
@transport: ACP::Transport::Stdio
|
|
19
19
|
|
|
20
20
|
# The schema has no initialize capability for modes or config options: an
|
|
21
21
|
# agent offers them per session, in its session responses, so they are
|
|
22
22
|
# routed whenever the agent defines them.
|
|
23
23
|
PER_SESSION: Hash[String, Symbol]
|
|
24
24
|
|
|
25
|
-
# @rbs transport: ACP::Transport::
|
|
25
|
+
# @rbs transport: ACP::Transport::Stdio
|
|
26
26
|
# @rbs capabilities: ACP::Types::AgentCapabilities
|
|
27
27
|
# @rbs agent_info: ACP::Types::Implementation?
|
|
28
28
|
# @rbs auth_methods: Array[ACP::Types::AuthMethod::t]
|
|
@@ -31,7 +31,7 @@ class ACP::AgentConnection
|
|
|
31
31
|
# @rbs logger: ACP::Transport::_Logger
|
|
32
32
|
# @rbs &factory: (ACP::AgentConnection::Client) -> ACP::AgentConnection::_Agent
|
|
33
33
|
# @rbs return: void
|
|
34
|
-
def initialize: (transport: ACP::Transport::
|
|
34
|
+
def initialize: (transport: ACP::Transport::Stdio, capabilities: ACP::Types::AgentCapabilities, ?agent_info: ACP::Types::Implementation?, ?auth_methods: Array[ACP::Types::AuthMethod::t], ?extension_requests: Hash[String, ^(untyped) -> untyped], ?extension_notifications: Hash[String, ^(untyped) -> void], ?logger: ACP::Transport::_Logger) { (ACP::AgentConnection::Client) -> ACP::AgentConnection::_Agent } -> void
|
|
35
35
|
|
|
36
36
|
# @rbs return: Thread
|
|
37
37
|
def start: () -> Thread
|
|
@@ -22,15 +22,15 @@ class ACP::ClientConnection::Streams
|
|
|
22
22
|
|
|
23
23
|
@permission: ACP::ClientConnection::_PermissionHandler
|
|
24
24
|
|
|
25
|
-
@sender: ACP::
|
|
25
|
+
@sender: ACP::Transport::Stdio
|
|
26
26
|
|
|
27
|
-
# @rbs sender: ACP::
|
|
27
|
+
# @rbs sender: ACP::Transport::Stdio
|
|
28
28
|
# @rbs permission: ACP::ClientConnection::_PermissionHandler
|
|
29
29
|
# @rbs updates: ACP::ClientConnection::_UpdateHandler
|
|
30
30
|
# @rbs logger: ACP::Transport::_Logger
|
|
31
31
|
# @rbs complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?
|
|
32
32
|
# @rbs return: void
|
|
33
|
-
def initialize: (sender: ACP::
|
|
33
|
+
def initialize: (sender: ACP::Transport::Stdio, permission: ACP::ClientConnection::_PermissionHandler, updates: ACP::ClientConnection::_UpdateHandler, logger: ACP::Transport::_Logger, ?complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?) -> void
|
|
34
34
|
|
|
35
35
|
# The request waits on its own thread so the caller's thread can yield each
|
|
36
36
|
# update as it arrives. The reader thread queues an update before it settles
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Generated from lib/acp/client_connection.rb with RBS::Inline
|
|
2
2
|
|
|
3
3
|
class ACP::ClientConnection
|
|
4
|
-
@transport: ACP::Transport::
|
|
4
|
+
@transport: ACP::Transport::Stdio
|
|
5
5
|
|
|
6
6
|
@permission: ACP::ClientConnection::_PermissionHandler
|
|
7
7
|
|
|
@@ -96,7 +96,7 @@ class ACP::ClientConnection
|
|
|
96
96
|
# up others.
|
|
97
97
|
SERVE: Array[ACP::ClientConnection::Route]
|
|
98
98
|
|
|
99
|
-
# @rbs transport: ACP::Transport::
|
|
99
|
+
# @rbs transport: ACP::Transport::Stdio
|
|
100
100
|
# @rbs permission: ACP::ClientConnection::_PermissionHandler
|
|
101
101
|
# @rbs read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
102
102
|
# @rbs write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
|
@@ -117,7 +117,7 @@ class ACP::ClientConnection
|
|
|
117
117
|
# @rbs streams: ACP::ClientConnection::Streams?
|
|
118
118
|
# @rbs logger: ACP::Transport::_Logger
|
|
119
119
|
# @rbs return: void
|
|
120
|
-
def initialize: (transport: ACP::Transport::
|
|
120
|
+
def initialize: (transport: ACP::Transport::Stdio, 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], ?streams: ACP::ClientConnection::Streams?, ?logger: ACP::Transport::_Logger) -> void
|
|
121
121
|
|
|
122
122
|
# @rbs return: Thread
|
|
123
123
|
def start: () -> Thread
|
|
@@ -36,6 +36,14 @@ class ACP::RequestError
|
|
|
36
36
|
# @rbs return: Hash[String, untyped]
|
|
37
37
|
def to_h: () -> Hash[String, untyped]
|
|
38
38
|
|
|
39
|
+
CONNECTION_CLOSED: ACP::RequestError
|
|
40
|
+
|
|
41
|
+
# Identity rather than code and message: a live peer can answer with the
|
|
42
|
+
# same pair, and that refusal must not read as a closed connection.
|
|
43
|
+
#
|
|
44
|
+
# @rbs return: bool
|
|
45
|
+
def connection_closed?: () -> bool
|
|
46
|
+
|
|
39
47
|
# @rbs return: ACP::RequestError
|
|
40
48
|
def self.parse_error: () -> ACP::RequestError
|
|
41
49
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Generated from lib/acp/transport/stdio/serving.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class ACP::Transport::Stdio::Serving
|
|
4
|
+
# @dynamic id
|
|
5
|
+
attr_reader id: untyped
|
|
6
|
+
|
|
7
|
+
# @dynamic cancellation
|
|
8
|
+
attr_reader cancellation: ACP::Transport::Cancellation
|
|
9
|
+
|
|
10
|
+
# @rbs id: untyped
|
|
11
|
+
# @rbs cancellation: ACP::Transport::Cancellation
|
|
12
|
+
# @rbs return: void
|
|
13
|
+
def initialize: (id: untyped, cancellation: ACP::Transport::Cancellation) -> void
|
|
14
|
+
end
|
|
@@ -11,7 +11,7 @@ class ACP::Transport::Stdio
|
|
|
11
11
|
|
|
12
12
|
@next_id: Integer
|
|
13
13
|
|
|
14
|
-
@serving: Hash[
|
|
14
|
+
@serving: Hash[Thread, ACP::Transport::Stdio::Serving]
|
|
15
15
|
|
|
16
16
|
@pending: Hash[untyped, Thread::Queue]
|
|
17
17
|
|
|
@@ -31,6 +31,13 @@ class ACP::Transport::Stdio
|
|
|
31
31
|
# @rbs return: void
|
|
32
32
|
def initialize: (input: _Reader, output: _Writer, ?logger: ACP::Transport::_Logger) -> void
|
|
33
33
|
|
|
34
|
+
# Joining the returned thread does not wait for the requests still being
|
|
35
|
+
# served. EOF cancels none of them (only the peer's $/cancel_request does):
|
|
36
|
+
# each handler runs to completion and its reply is still written, to be
|
|
37
|
+
# warned and dropped if the peer is gone. A caller blocked in #request, a
|
|
38
|
+
# handler waiting on its own request included, is released with
|
|
39
|
+
# CONNECTION_CLOSED.
|
|
40
|
+
#
|
|
34
41
|
# @rbs requests: Hash[String, _Handler]
|
|
35
42
|
# @rbs notifications: Hash[String, _NotificationHandler]
|
|
36
43
|
# @rbs return: Thread
|
|
@@ -62,6 +69,15 @@ class ACP::Transport::Stdio
|
|
|
62
69
|
# @rbs return: ACP::Transport::Cancellation?
|
|
63
70
|
def cancellation: () -> ACP::Transport::Cancellation?
|
|
64
71
|
|
|
72
|
+
# @rbs return: Array[untyped]
|
|
73
|
+
def serving_ids: () -> Array[untyped]
|
|
74
|
+
|
|
75
|
+
# Joins the requests in flight at the call, not one the reader dispatches
|
|
76
|
+
# meanwhile: join the reader thread first to leave nothing in flight.
|
|
77
|
+
#
|
|
78
|
+
# @rbs return: void
|
|
79
|
+
def drain: () -> void
|
|
80
|
+
|
|
65
81
|
private
|
|
66
82
|
|
|
67
83
|
# @rbs line: String
|
|
@@ -95,18 +111,6 @@ class ACP::Transport::Stdio
|
|
|
95
111
|
# @rbs return: void
|
|
96
112
|
def serve: (_Handler? handler, untyped id, untyped params, String method) -> void
|
|
97
113
|
|
|
98
|
-
# Registered before the serve thread starts so a cancel racing the request
|
|
99
|
-
# cannot be missed, and dropped once the response is on the wire so later
|
|
100
|
-
# cancels for the id are ignored.
|
|
101
|
-
#
|
|
102
|
-
# @rbs id: untyped
|
|
103
|
-
# @rbs return: ACP::Transport::Cancellation
|
|
104
|
-
def register_serving: (untyped id) -> ACP::Transport::Cancellation
|
|
105
|
-
|
|
106
|
-
# @rbs id: untyped
|
|
107
|
-
# @rbs return: void
|
|
108
|
-
def unregister_serving: (untyped id) -> void
|
|
109
|
-
|
|
110
114
|
# A notification has no reply to carry a malformed params failure, so one
|
|
111
115
|
# is warned and dropped rather than answered.
|
|
112
116
|
#
|
|
@@ -147,11 +151,15 @@ class ACP::Transport::Stdio
|
|
|
147
151
|
# @rbs return: Thread::Queue?
|
|
148
152
|
def settle: (untyped id, untyped result) -> Thread::Queue?
|
|
149
153
|
|
|
154
|
+
# Leaves @serving alone on purpose: embedders rely on in-flight handlers
|
|
155
|
+
# running to completion after the input ends.
|
|
156
|
+
#
|
|
150
157
|
# @rbs return: void
|
|
151
158
|
def close: () -> void
|
|
152
159
|
|
|
153
|
-
# A closed peer
|
|
154
|
-
#
|
|
160
|
+
# A closed peer (EPIPE) or an output the embedder already closed (IOError)
|
|
161
|
+
# must not kill the thread writing the reply: the reader has to keep
|
|
162
|
+
# draining input so pending requests still settle at EOF.
|
|
155
163
|
#
|
|
156
164
|
# @rbs message: Hash[String, untyped]
|
|
157
165
|
# @rbs return: void
|
|
@@ -85,26 +85,4 @@ class ACP::AgentConnection
|
|
|
85
85
|
# Runs on the transport's reader thread, so it must return quickly.
|
|
86
86
|
def mcp_message_notification: (ACP::Types::Unstable::MessageMcpNotification notification) -> void
|
|
87
87
|
end
|
|
88
|
-
|
|
89
|
-
# What an agent sees of ACP::AgentConnection::Client, so tests can pass a fake.
|
|
90
|
-
interface _Client
|
|
91
|
-
def update: (String session_id, ACP::Types::SessionUpdate::t update) -> void
|
|
92
|
-
def available_commands: (String session_id, Array[ACP::Types::AvailableCommand] commands) -> void
|
|
93
|
-
def request_permission: (ACP::Types::RequestPermissionRequest) -> (ACP::Types::RequestPermissionResponse | ACP::RequestError)
|
|
94
|
-
def cancel_requests: () -> void
|
|
95
|
-
def cancelled?: () -> bool
|
|
96
|
-
def read_text_file: (ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError)
|
|
97
|
-
def write_text_file: (ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError)
|
|
98
|
-
def create_terminal: (ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError)
|
|
99
|
-
def terminal_output: (ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError)
|
|
100
|
-
def wait_for_terminal_exit: (ACP::Types::WaitForTerminalExitRequest) -> (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError)
|
|
101
|
-
def kill_terminal: (ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError)
|
|
102
|
-
def release_terminal: (ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
|
|
103
|
-
def create_elicitation: (ACP::Types::CreateElicitationRequest request) -> (ACP::Types::CreateElicitationResponse | ACP::RequestError)
|
|
104
|
-
def complete_elicitation: (ACP::Types::CompleteElicitationNotification notification) -> void
|
|
105
|
-
def mcp_message: (ACP::Types::Unstable::MessageMcpRequest request) -> (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError)
|
|
106
|
-
def ext_request: (String method, ?untyped params) -> (Hash[String, untyped] | ACP::RequestError)
|
|
107
|
-
def ext_notify: (String method, ?untyped params) -> void
|
|
108
|
-
def capabilities: () -> ACP::Types::ClientCapabilities?
|
|
109
|
-
end
|
|
110
88
|
end
|
|
@@ -9,10 +9,3 @@ class ACP::ClientConnection
|
|
|
9
9
|
def call: (ACP::Types::SessionNotification) -> void
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
|
-
|
|
13
|
-
class ACP::ClientConnection::Streams
|
|
14
|
-
interface _Sender
|
|
15
|
-
def request: (String method, ?untyped params) -> (Hash[String, untyped] | ACP::RequestError)
|
|
16
|
-
def cancellation: () -> ACP::Transport::Cancellation?
|
|
17
|
-
end
|
|
18
|
-
end
|
|
@@ -5,22 +5,6 @@ module ACP::Transport
|
|
|
5
5
|
def error: (String message) -> void
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
-
interface _Peer
|
|
9
|
-
def request: (String method, ?untyped params) -> (Hash[String, untyped] | ACP::RequestError)
|
|
10
|
-
def notify: (String method, ?untyped params) -> void
|
|
11
|
-
def cancellation: () -> ACP::Transport::Cancellation?
|
|
12
|
-
def cancel_requests: () -> void
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
interface _Transport
|
|
16
|
-
include _Peer
|
|
17
|
-
|
|
18
|
-
def start: (
|
|
19
|
-
requests: Hash[String, ACP::Transport::Stdio::_Handler],
|
|
20
|
-
notifications: Hash[String, ACP::Transport::Stdio::_NotificationHandler]
|
|
21
|
-
) -> Thread
|
|
22
|
-
end
|
|
23
|
-
|
|
24
8
|
interface _Parser
|
|
25
9
|
def from_h: (Hash[String, untyped]) -> untyped
|
|
26
10
|
end
|
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.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jake Bottrall
|
|
@@ -48,6 +48,7 @@ files:
|
|
|
48
48
|
- lib/acp/transport/reply.rb
|
|
49
49
|
- lib/acp/transport/stderr_logger.rb
|
|
50
50
|
- lib/acp/transport/stdio.rb
|
|
51
|
+
- lib/acp/transport/stdio/serving.rb
|
|
51
52
|
- lib/acp/types.rb
|
|
52
53
|
- lib/acp/types/agent_auth_capabilities.rb
|
|
53
54
|
- lib/acp/types/agent_capabilities.rb
|
|
@@ -405,6 +406,7 @@ files:
|
|
|
405
406
|
- sig/generated/acp/transport/reply.rbs
|
|
406
407
|
- sig/generated/acp/transport/stderr_logger.rbs
|
|
407
408
|
- sig/generated/acp/transport/stdio.rbs
|
|
409
|
+
- sig/generated/acp/transport/stdio/serving.rbs
|
|
408
410
|
- sig/generated/acp/types.rbs
|
|
409
411
|
- sig/generated/acp/types/agent_auth_capabilities.rbs
|
|
410
412
|
- sig/generated/acp/types/agent_capabilities.rbs
|