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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96418a5250fccc2c0eb4380e15309f7658f347bc91d5df3c1218cc3794389f48
4
- data.tar.gz: 52c089fd1c4238da195880f4fcbf823452272de94a6c3f6ad3a2d7c06f0580d1
3
+ metadata.gz: fdea95657cb02b7563bbc7730fe4b346cb7e80f08ab78071b0da79e3d46704e3
4
+ data.tar.gz: 57efa048c5f5959d842e89d750ea2bd92e6caf4e4d6fc0c54964f2eaca577776
5
5
  SHA512:
6
- metadata.gz: 84820ff6950c13e9d2fd30f5e5936f7951700af3622bfdc84af49cfcd6b7f24465e00d4889d2d451eaa01fc5091b6af7ecc103579c175d683f451e7ac27a86ce
7
- data.tar.gz: 696efcc33f82fd2bc5ecd1a8e9843138c5be690e13ed4a72c955202c95af404c43a7c794868a1a0dad712174bb6ceaf71b0ab9d51bcf86e5954f6f6acdee3d61
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::_Peer
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::_Peer
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)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ACP::AgentConnection
4
- # @rbs @transport: ACP::Transport::_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::_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::ClientConnection::Streams::_Sender
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::ClientConnection::Streams::_Sender
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::_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::_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))?
@@ -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
@@ -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[untyped, ACP::Transport::Cancellation]
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.new(
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 = register_serving(id)
171
- Thread.new do
172
- ACP::Transport::Cancellation.current = cancellation
173
- outcome = handler ? invoke(handler, params) : ACP::RequestError.method_not_found(method)
174
- case outcome
175
- when ACP::Transport::Reply
176
- reply(id, outcome.result)
177
- quietly('reply after callback', &outcome.after)
178
- else reply(id, outcome)
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
- ensure
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
- cancellation = @lock.synchronize { @serving[id] }
215
- cancellation&.cancel
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 must not kill the thread writing the reply: the reader has
286
- # to keep draining input so pending requests still settle at EOF.
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ACP
4
- VERSION = '0.11.0'
4
+ VERSION = '0.13.0'
5
5
  end
@@ -3,15 +3,15 @@
3
3
  class ACP::AgentConnection::Client
4
4
  @mcp_advertised: bool
5
5
 
6
- @peer: ACP::Transport::_Peer
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::_Peer
11
+ # @rbs peer: ACP::Transport::Stdio
12
12
  # @rbs mcp_advertised: bool
13
13
  # @rbs return: void
14
- def initialize: (peer: ACP::Transport::_Peer, ?mcp_advertised: bool) -> void
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::_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::_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::_Transport, 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
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::ClientConnection::Streams::_Sender
25
+ @sender: ACP::Transport::Stdio
26
26
 
27
- # @rbs sender: ACP::ClientConnection::Streams::_Sender
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::ClientConnection::Streams::_Sender, permission: ACP::ClientConnection::_PermissionHandler, updates: ACP::ClientConnection::_UpdateHandler, logger: ACP::Transport::_Logger, ?complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?) -> void
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::_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::_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::_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], ?streams: ACP::ClientConnection::Streams?, ?logger: ACP::Transport::_Logger) -> void
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[untyped, ACP::Transport::Cancellation]
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 must not kill the thread writing the reply: the reader has
154
- # to keep draining input so pending requests still settle at EOF.
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.11.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