acp-sdk 0.5.0 → 0.6.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: 77ac970489b956c7216d4c94a8d5cc6e7bf3fe5bfd10110cb8558c119f58f69d
4
- data.tar.gz: 29ec919463eb48d06811aa8b6c736163fd7872348ce99f3f8cdc10c5670bf8c4
3
+ metadata.gz: 367678f3b99c82b2f768879be0308249d9b32ce8ce94c3d4e6de6c12c4a1c043
4
+ data.tar.gz: 071e457b8e00fa19be760fff65cfd69998102e54b4b3d47f9b3cc0d80c359fc3
5
5
  SHA512:
6
- metadata.gz: acf89beeb444ff33ac273aececd55d645792fe591f346cbdce0a099b6b6bff4eedb4025cceadd3a46ec62253c85b111c0149466960707b3feaa92623dab600a8
7
- data.tar.gz: 92fba44ccd9334876fa8cb689728a7a19cc3d473bf1e7ee27fe215d72e48952e04bc9c0428609e31e883bdef6986e1ec7b4c9383d8a37271646593b33ff069bd
6
+ metadata.gz: 706206bd29062ef6ecf1856b9e9671113e02bb6eb6ac837c2815bfcdbbff4bc07ac8d2c6394ec6f7b33c37a3832a9e0c4bb5c4e5c4699c1cdc129e875f693605
7
+ data.tar.gz: 5a73a945d099dedcf5837aa1c897b1dca0313a49d131337756b26c28731ddedb14b96c21722c7d6ecff3a21e6bed811e036551724c12bebcdf53f88a56121f8e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.0](https://github.com/bottrall/acp-sdk/compare/v0.5.0...v0.6.0) (2026-10-04)
4
+
5
+
6
+ ### Features
7
+
8
+ * answer pending permission requests with cancelled after session_cancel ([#70](https://github.com/bottrall/acp-sdk/issues/70)) ([06a218b](https://github.com/bottrall/acp-sdk/commit/06a218b6262ff409e301d08a43330af5b60837e0))
9
+ * extension methods on both connections ([#72](https://github.com/bottrall/acp-sdk/issues/72)) ([d1faa1a](https://github.com/bottrall/acp-sdk/commit/d1faa1a738019e73a4dbad5b59f480795f8dd4bf))
10
+
3
11
  ## [0.5.0](https://github.com/bottrall/acp-sdk/compare/v0.4.0...v0.5.0) (2026-10-04)
4
12
 
5
13
 
data/README.md CHANGED
@@ -57,6 +57,8 @@ The agent's contract:
57
57
  - `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.
58
58
  - `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.
59
59
 
60
+ 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`.
61
+
60
62
  [`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`.
61
63
 
62
64
  ## Driving an agent
@@ -104,6 +106,8 @@ connection = ACP::ClientConnection.new(
104
106
  - `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.
105
107
  - `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.
106
108
 
109
+ 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`.
110
+
107
111
  ## Contributing
108
112
 
109
113
  Bug reports and pull requests are welcome on [GitHub](https://github.com/bottrall/acp-sdk). See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and workflow. Everyone interacting in the project is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
@@ -105,6 +105,26 @@ class ACP::AgentConnection::Client
105
105
  @peer.notify('elicitation/complete', notification.to_h)
106
106
  end
107
107
 
108
+ # Sends an extension request, keyed by its raw `_`-prefixed wire name, and
109
+ # returns the client's reply as-is: extension methods have no schema to
110
+ # parse the result into.
111
+ #
112
+ # @rbs method: String
113
+ # @rbs params: untyped
114
+ # @rbs return: (Hash[String, untyped] | ACP::RequestError)
115
+ def ext_request(method, params = nil)
116
+ ACP::Extensions.validate_name(method)
117
+ @peer.request(method, params)
118
+ end
119
+
120
+ # @rbs method: String
121
+ # @rbs params: untyped
122
+ # @rbs return: void
123
+ def ext_notify(method, params = nil)
124
+ ACP::Extensions.validate_name(method)
125
+ @peer.notify(method, params)
126
+ end
127
+
108
128
  private
109
129
 
110
130
  # @rbs rpc_method: String
@@ -6,6 +6,8 @@ class ACP::AgentConnection
6
6
  # @rbs @agent_info: ACP::Types::Implementation?
7
7
  # @rbs @auth_methods: Array[ACP::Types::AuthMethod::t]
8
8
  # @rbs @factory: ^(ACP::AgentConnection::Client) -> ACP::AgentConnection::_Agent
9
+ # @rbs @extension_requests: Hash[String, ^(untyped) -> untyped]
10
+ # @rbs @extension_notifications: Hash[String, ^(untyped) -> void]
9
11
  # @rbs @logger: ACP::Transport::_Logger
10
12
 
11
13
  PROTOCOL_VERSION = 1 #: Integer
@@ -30,6 +32,8 @@ class ACP::AgentConnection
30
32
  # @rbs capabilities: ACP::Types::AgentCapabilities
31
33
  # @rbs agent_info: ACP::Types::Implementation?
32
34
  # @rbs auth_methods: Array[ACP::Types::AuthMethod::t]
35
+ # @rbs extension_requests: Hash[String, ^(untyped) -> untyped]
36
+ # @rbs extension_notifications: Hash[String, ^(untyped) -> void]
33
37
  # @rbs logger: ACP::Transport::_Logger
34
38
  # @rbs &factory: (ACP::AgentConnection::Client) -> ACP::AgentConnection::_Agent
35
39
  # @rbs return: void
@@ -38,6 +42,8 @@ class ACP::AgentConnection
38
42
  capabilities:,
39
43
  agent_info: nil,
40
44
  auth_methods: [],
45
+ extension_requests: {},
46
+ extension_notifications: {},
41
47
  logger: ACP::Transport::StderrLogger.new,
42
48
  &factory
43
49
  )
@@ -46,6 +52,10 @@ class ACP::AgentConnection
46
52
  @capabilities = capabilities
47
53
  @agent_info = agent_info
48
54
  @auth_methods = auth_methods
55
+ @extension_requests = extension_requests
56
+ @extension_notifications = extension_notifications
57
+ extension_requests.each_key { |name| ACP::Extensions.validate_name(name) }
58
+ extension_notifications.each_key { |name| ACP::Extensions.validate_name(name) }
49
59
  @factory = factory
50
60
  end
51
61
 
@@ -61,7 +71,10 @@ class ACP::AgentConnection
61
71
  raise ArgumentError, "initialize advertises methods the agent lacks: #{missing.join(', ')}" unless missing.empty?
62
72
 
63
73
  unrouted = unadvertised.map(&:rpc_method) + PER_SESSION.reject { |_, name| agent.respond_to?(name) }.keys
64
- @transport.start(requests: requests(agent, client).except(*unrouted), notifications: notifications(agent))
74
+ @transport.start(
75
+ requests: requests(agent, client).except(*unrouted).merge(@extension_requests),
76
+ notifications: notifications(agent).merge(@extension_notifications)
77
+ )
65
78
  end
66
79
 
67
80
  private
@@ -7,30 +7,53 @@ class ACP::ClientConnection
7
7
  # @rbs @write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
8
8
  # @rbs @updates: ACP::ClientConnection::_UpdateHandler
9
9
  # @rbs @fs_capabilities: ACP::Types::FileSystemCapabilities?
10
+ # @rbs @extension_requests: Hash[String, ^(untyped) -> untyped]
11
+ # @rbs @extension_notifications: Hash[String, ^(untyped) -> void]
10
12
  # @rbs @lock: Thread::Mutex
11
13
  # @rbs @streams: Hash[String, Thread::Queue]
14
+ # @rbs @pending_permissions: Hash[String, Array[Thread::Queue]]
12
15
  # @rbs @logger: ACP::Transport::_Logger
13
16
 
14
17
  PROTOCOL_VERSION = ACP::AgentConnection::PROTOCOL_VERSION #: Integer
15
18
 
16
19
  IGNORE = ->(_notification) {} #: ^(ACP::Types::SessionNotification) -> void
17
20
 
21
+ CANCELLED = ACP::Types::RequestPermissionResponse.new(
22
+ outcome: ACP::Types::RequestPermissionOutcome::Cancelled.new
23
+ ) #: ACP::Types::RequestPermissionResponse
24
+
18
25
  # @rbs transport: ACP::AgentConnection::_Transport
19
26
  # @rbs permission: ACP::ClientConnection::_PermissionHandler
20
27
  # @rbs read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
21
28
  # @rbs write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
22
29
  # @rbs updates: ACP::ClientConnection::_UpdateHandler
30
+ # @rbs extension_requests: Hash[String, ^(untyped) -> untyped]
31
+ # @rbs extension_notifications: Hash[String, ^(untyped) -> void]
23
32
  # @rbs logger: ACP::Transport::_Logger
24
33
  # @rbs return: void
25
- def initialize(transport:, permission:, read_text_file: nil, write_text_file: nil, updates: IGNORE, logger: ACP::Transport::StderrLogger.new)
34
+ def initialize(
35
+ transport:,
36
+ permission:,
37
+ read_text_file: nil,
38
+ write_text_file: nil,
39
+ updates: IGNORE,
40
+ extension_requests: {},
41
+ extension_notifications: {},
42
+ logger: ACP::Transport::StderrLogger.new
43
+ )
26
44
  @transport = transport
27
45
  @permission = permission
28
46
  @read_text_file = read_text_file
29
47
  @write_text_file = write_text_file
30
48
  @updates = updates
31
49
  @fs_capabilities = nil
50
+ @extension_requests = extension_requests
51
+ @extension_notifications = extension_notifications
52
+ extension_requests.each_key { |name| ACP::Extensions.validate_name(name) }
53
+ extension_notifications.each_key { |name| ACP::Extensions.validate_name(name) }
32
54
  @lock = Mutex.new
33
55
  @streams = {}
56
+ @pending_permissions = {}
34
57
  @logger = logger
35
58
  end
36
59
 
@@ -41,8 +64,8 @@ class ACP::ClientConnection
41
64
  'session/request_permission' => method(:request_permission),
42
65
  'fs/read_text_file' => method(:read_text_file),
43
66
  'fs/write_text_file' => method(:write_text_file)
44
- },
45
- notifications: { 'session/update' => method(:dispatch) }
67
+ }.merge(@extension_requests),
68
+ notifications: { 'session/update' => method(:dispatch) }.merge(@extension_notifications)
46
69
  )
47
70
  end
48
71
 
@@ -118,10 +141,17 @@ class ACP::ClientConnection
118
141
  parse(ACP::Types::DeleteSessionResponse, @transport.request('session/delete', request.to_h))
119
142
  end
120
143
 
144
+ # The spec requires a pending session/request_permission to be answered with
145
+ # the cancelled outcome once the turn is cancelled, so every queue registered
146
+ # for the session gets one before this returns. The notification goes first
147
+ # so the wire order is cancel, then the cancelled replies.
148
+ #
121
149
  # @rbs notification: ACP::Types::CancelNotification
122
150
  # @rbs return: void
123
151
  def session_cancel(notification)
124
152
  @transport.notify('session/cancel', notification.to_h)
153
+ pending = @lock.synchronize { @pending_permissions.delete(notification.session_id) }
154
+ pending&.each { |replies| replies << [:value, CANCELLED] }
125
155
  end
126
156
 
127
157
  # @rbs request: ACP::Types::SetSessionModeRequest
@@ -151,6 +181,26 @@ class ACP::ClientConnection
151
181
  parse(ACP::Types::LogoutResponse, @transport.request('logout', request.to_h))
152
182
  end
153
183
 
184
+ # Sends an extension request, keyed by its raw `_`-prefixed wire name, and
185
+ # returns the agent's reply as-is: extension methods have no schema to
186
+ # parse the result into.
187
+ #
188
+ # @rbs method: String
189
+ # @rbs params: untyped
190
+ # @rbs return: (Hash[String, untyped] | ACP::RequestError)
191
+ def ext_request(method, params = nil)
192
+ ACP::Extensions.validate_name(method)
193
+ @transport.request(method, params)
194
+ end
195
+
196
+ # @rbs method: String
197
+ # @rbs params: untyped
198
+ # @rbs return: void
199
+ def ext_notify(method, params = nil)
200
+ ACP::Extensions.validate_name(method)
201
+ @transport.notify(method, params)
202
+ end
203
+
154
204
  private
155
205
 
156
206
  # @rbs type: ACP::AgentConnection::_Parser
@@ -216,6 +266,10 @@ class ACP::ClientConnection
216
266
  @updates.call(notification) unless queue
217
267
  end
218
268
 
269
+ # The handler runs on its own thread so a cancelled request is answered
270
+ # without waiting for it, and whatever it returns after that is pushed to a
271
+ # queue nobody reads.
272
+ #
219
273
  # @rbs params: untyped
220
274
  # @rbs return: (ACP::Types::RequestPermissionResponse | ACP::RequestError)
221
275
  def request_permission(params)
@@ -223,7 +277,45 @@ class ACP::ClientConnection
223
277
  rescue KeyError, TypeError, NoMethodError
224
278
  ACP::RequestError.invalid_params
225
279
  else
226
- @permission.call(request)
280
+ replies = Thread::Queue.new
281
+ register_permission(request.session_id, replies)
282
+ Thread.new do
283
+ # A raised StandardError is not a returned ACP::RequestError, so the two
284
+ # need distinct shapes on the queue.
285
+ replies << begin
286
+ [:value, @permission.call(request)]
287
+ rescue StandardError => e
288
+ [:raised, e]
289
+ end
290
+ ensure
291
+ unregister_permission(request.session_id, replies)
292
+ end
293
+ settled = replies.pop
294
+ # Re-raised so the transport's invoke turns it into an error reply, as it
295
+ # would if the handler ran on the serve thread itself.
296
+ raise settled[1] if settled[0] == :raise
297
+
298
+ settled[1]
299
+ end
300
+
301
+ # @rbs session_id: String
302
+ # @rbs replies: Thread::Queue
303
+ # @rbs return: void
304
+ def register_permission(session_id, replies)
305
+ @lock.synchronize { (@pending_permissions[session_id] ||= []) << replies }
306
+ end
307
+
308
+ # @rbs session_id: String
309
+ # @rbs replies: Thread::Queue
310
+ # @rbs return: void
311
+ def unregister_permission(session_id, replies)
312
+ @lock.synchronize do
313
+ pending = @pending_permissions[session_id]
314
+ if pending
315
+ pending.delete(replies)
316
+ @pending_permissions.delete(session_id) if pending.empty?
317
+ end
318
+ end
227
319
  end
228
320
 
229
321
  # The routes are registered at start, before connect records the advertised
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shared by the connections' handler maps and send paths, so a plain method
4
+ # name cannot slip into the extension routes.
5
+ module ACP::Extensions
6
+ extend self
7
+
8
+ # @rbs name: String
9
+ # @rbs return: void
10
+ def validate_name(name)
11
+ return if name.start_with?('_')
12
+
13
+ raise ArgumentError, "extension method names must start with '_' (got #{name.inspect})"
14
+ end
15
+ 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.5.0'
4
+ VERSION = '0.6.0'
5
5
  end
@@ -63,6 +63,20 @@ class ACP::AgentConnection::Client
63
63
  # @rbs return: void
64
64
  def complete_elicitation: (ACP::Types::CompleteElicitationNotification notification) -> void
65
65
 
66
+ # Sends an extension request, keyed by its raw `_`-prefixed wire name, and
67
+ # returns the client's reply as-is: extension methods have no schema to
68
+ # parse the result into.
69
+ #
70
+ # @rbs method: String
71
+ # @rbs params: untyped
72
+ # @rbs return: (Hash[String, untyped] | ACP::RequestError)
73
+ def ext_request: (String method, ?untyped params) -> (Hash[String, untyped] | ACP::RequestError)
74
+
75
+ # @rbs method: String
76
+ # @rbs params: untyped
77
+ # @rbs return: void
78
+ def ext_notify: (String method, ?untyped params) -> void
79
+
66
80
  private
67
81
 
68
82
  # @rbs rpc_method: String
@@ -3,6 +3,10 @@
3
3
  class ACP::AgentConnection
4
4
  @logger: ACP::Transport::_Logger
5
5
 
6
+ @extension_notifications: Hash[String, ^(untyped) -> void]
7
+
8
+ @extension_requests: Hash[String, ^(untyped) -> untyped]
9
+
6
10
  @factory: ^(ACP::AgentConnection::Client) -> ACP::AgentConnection::_Agent
7
11
 
8
12
  @auth_methods: Array[ACP::Types::AuthMethod::t]
@@ -26,10 +30,12 @@ class ACP::AgentConnection
26
30
  # @rbs capabilities: ACP::Types::AgentCapabilities
27
31
  # @rbs agent_info: ACP::Types::Implementation?
28
32
  # @rbs auth_methods: Array[ACP::Types::AuthMethod::t]
33
+ # @rbs extension_requests: Hash[String, ^(untyped) -> untyped]
34
+ # @rbs extension_notifications: Hash[String, ^(untyped) -> void]
29
35
  # @rbs logger: ACP::Transport::_Logger
30
36
  # @rbs &factory: (ACP::AgentConnection::Client) -> ACP::AgentConnection::_Agent
31
37
  # @rbs return: void
32
- def initialize: (transport: ACP::AgentConnection::_Transport, capabilities: ACP::Types::AgentCapabilities, ?agent_info: ACP::Types::Implementation?, ?auth_methods: Array[ACP::Types::AuthMethod::t], ?logger: ACP::Transport::_Logger) { (ACP::AgentConnection::Client) -> ACP::AgentConnection::_Agent } -> void
38
+ def initialize: (transport: ACP::AgentConnection::_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
33
39
 
34
40
  # @rbs return: Thread
35
41
  def start: () -> Thread
@@ -3,10 +3,16 @@
3
3
  class ACP::ClientConnection
4
4
  @logger: ACP::Transport::_Logger
5
5
 
6
+ @pending_permissions: Hash[String, Array[Thread::Queue]]
7
+
6
8
  @streams: Hash[String, Thread::Queue]
7
9
 
8
10
  @lock: Thread::Mutex
9
11
 
12
+ @extension_notifications: Hash[String, ^(untyped) -> void]
13
+
14
+ @extension_requests: Hash[String, ^(untyped) -> untyped]
15
+
10
16
  @fs_capabilities: ACP::Types::FileSystemCapabilities?
11
17
 
12
18
  @updates: ACP::ClientConnection::_UpdateHandler
@@ -23,14 +29,18 @@ class ACP::ClientConnection
23
29
 
24
30
  IGNORE: ^(ACP::Types::SessionNotification) -> void
25
31
 
32
+ CANCELLED: ACP::Types::RequestPermissionResponse
33
+
26
34
  # @rbs transport: ACP::AgentConnection::_Transport
27
35
  # @rbs permission: ACP::ClientConnection::_PermissionHandler
28
36
  # @rbs read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
29
37
  # @rbs write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
30
38
  # @rbs updates: ACP::ClientConnection::_UpdateHandler
39
+ # @rbs extension_requests: Hash[String, ^(untyped) -> untyped]
40
+ # @rbs extension_notifications: Hash[String, ^(untyped) -> void]
31
41
  # @rbs logger: ACP::Transport::_Logger
32
42
  # @rbs return: void
33
- 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))?, ?updates: ACP::ClientConnection::_UpdateHandler, ?logger: ACP::Transport::_Logger) -> void
43
+ 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))?, ?updates: ACP::ClientConnection::_UpdateHandler, ?extension_requests: Hash[String, ^(untyped) -> untyped], ?extension_notifications: Hash[String, ^(untyped) -> void], ?logger: ACP::Transport::_Logger) -> void
34
44
 
35
45
  # @rbs return: Thread
36
46
  def start: () -> Thread
@@ -75,6 +85,11 @@ class ACP::ClientConnection
75
85
  # @rbs return: ACP::Types::DeleteSessionResponse | ACP::RequestError
76
86
  def session_delete: (ACP::Types::DeleteSessionRequest request) -> (ACP::Types::DeleteSessionResponse | ACP::RequestError)
77
87
 
88
+ # The spec requires a pending session/request_permission to be answered with
89
+ # the cancelled outcome once the turn is cancelled, so every queue registered
90
+ # for the session gets one before this returns. The notification goes first
91
+ # so the wire order is cancel, then the cancelled replies.
92
+ #
78
93
  # @rbs notification: ACP::Types::CancelNotification
79
94
  # @rbs return: void
80
95
  def session_cancel: (ACP::Types::CancelNotification notification) -> void
@@ -98,6 +113,20 @@ class ACP::ClientConnection
98
113
  # @rbs return: ACP::Types::LogoutResponse | ACP::RequestError
99
114
  def logout: (ACP::Types::LogoutRequest request) -> (ACP::Types::LogoutResponse | ACP::RequestError)
100
115
 
116
+ # Sends an extension request, keyed by its raw `_`-prefixed wire name, and
117
+ # returns the agent's reply as-is: extension methods have no schema to
118
+ # parse the result into.
119
+ #
120
+ # @rbs method: String
121
+ # @rbs params: untyped
122
+ # @rbs return: (Hash[String, untyped] | ACP::RequestError)
123
+ def ext_request: (String method, ?untyped params) -> (Hash[String, untyped] | ACP::RequestError)
124
+
125
+ # @rbs method: String
126
+ # @rbs params: untyped
127
+ # @rbs return: void
128
+ def ext_notify: (String method, ?untyped params) -> void
129
+
101
130
  private
102
131
 
103
132
  # @rbs type: ACP::AgentConnection::_Parser
@@ -124,10 +153,24 @@ class ACP::ClientConnection
124
153
  # @rbs return: void
125
154
  def dispatch: (untyped params) -> void
126
155
 
156
+ # The handler runs on its own thread so a cancelled request is answered
157
+ # without waiting for it, and whatever it returns after that is pushed to a
158
+ # queue nobody reads.
159
+ #
127
160
  # @rbs params: untyped
128
161
  # @rbs return: (ACP::Types::RequestPermissionResponse | ACP::RequestError)
129
162
  def request_permission: (untyped params) -> (ACP::Types::RequestPermissionResponse | ACP::RequestError)
130
163
 
164
+ # @rbs session_id: String
165
+ # @rbs replies: Thread::Queue
166
+ # @rbs return: void
167
+ def register_permission: (String session_id, Thread::Queue replies) -> void
168
+
169
+ # @rbs session_id: String
170
+ # @rbs replies: Thread::Queue
171
+ # @rbs return: void
172
+ def unregister_permission: (String session_id, Thread::Queue replies) -> void
173
+
131
174
  # The routes are registered at start, before connect records the advertised
132
175
  # capabilities, so each one answers -32601 until then.
133
176
  #
@@ -0,0 +1,9 @@
1
+ # Generated from lib/acp/extensions.rb with RBS::Inline
2
+
3
+ # Shared by the connections' handler maps and send paths, so a plain method
4
+ # name cannot slip into the extension routes.
5
+ module ACP::Extensions
6
+ # @rbs name: String
7
+ # @rbs return: void
8
+ def validate_name: (String name) -> void
9
+ end
@@ -81,6 +81,8 @@ class ACP::AgentConnection
81
81
  def release_terminal: (ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
82
82
  def create_elicitation: (ACP::Types::CreateElicitationRequest request) -> (ACP::Types::CreateElicitationResponse | ACP::RequestError)
83
83
  def complete_elicitation: (ACP::Types::CompleteElicitationNotification notification) -> void
84
+ def ext_request: (String method, ?untyped params) -> (Hash[String, untyped] | ACP::RequestError)
85
+ def ext_notify: (String method, ?untyped params) -> void
84
86
  def capabilities: () -> ACP::Types::ClientCapabilities?
85
87
  end
86
88
 
@@ -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::Extensions
5
+ extend ::ACP::Extensions
6
+ 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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jake Bottrall
@@ -44,6 +44,7 @@ files:
44
44
  - lib/acp/agent_connection/optional_method/resume_session.rb
45
45
  - lib/acp/agent_process.rb
46
46
  - lib/acp/client_connection.rb
47
+ - lib/acp/extensions.rb
47
48
  - lib/acp/request_error.rb
48
49
  - lib/acp/sdk.rb
49
50
  - lib/acp/transport.rb
@@ -259,6 +260,7 @@ files:
259
260
  - sig/generated/acp/agent_connection/optional_method/resume_session.rbs
260
261
  - sig/generated/acp/agent_process.rbs
261
262
  - sig/generated/acp/client_connection.rbs
263
+ - sig/generated/acp/extensions.rbs
262
264
  - sig/generated/acp/request_error.rbs
263
265
  - sig/generated/acp/sdk.rbs
264
266
  - sig/generated/acp/transport.rbs
@@ -472,6 +474,7 @@ files:
472
474
  - sig/manual/acp/agent_connection/optional_method/resume_session.rbs
473
475
  - sig/manual/acp/agent_process.rbs
474
476
  - sig/manual/acp/client_connection.rbs
477
+ - sig/manual/acp/extensions.rbs
475
478
  - sig/manual/acp/transport.rbs
476
479
  - sig/manual/acp/transport/stdio.rbs
477
480
  homepage: https://github.com/bottrall/acp-sdk