acp-sdk 0.6.0 → 0.7.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: 367678f3b99c82b2f768879be0308249d9b32ce8ce94c3d4e6de6c12c4a1c043
4
- data.tar.gz: 071e457b8e00fa19be760fff65cfd69998102e54b4b3d47f9b3cc0d80c359fc3
3
+ metadata.gz: 978b245a94b3cd60d3858ee42c3deaf9b687c8a6a1701ce54c684a5e434b2bce
4
+ data.tar.gz: b83adfe811bc7873455f9f81993061e7b2ea66d0a75b78645807fc44e08cd58b
5
5
  SHA512:
6
- metadata.gz: 706206bd29062ef6ecf1856b9e9671113e02bb6eb6ac837c2815bfcdbbff4bc07ac8d2c6394ec6f7b33c37a3832a9e0c4bb5c4e5c4699c1cdc129e875f693605
7
- data.tar.gz: 5a73a945d099dedcf5837aa1c897b1dca0313a49d131337756b26c28731ddedb14b96c21722c7d6ecff3a21e6bed811e036551724c12bebcdf53f88a56121f8e
6
+ metadata.gz: 2907a71594882b7afd3ed8fe89b2e9520f139dc1c235a4e43bf836a2f127ae6264ce948d86885577daa6cd320078c6da831f4b8df89225201287276e17710c4e
7
+ data.tar.gz: 9191c0362932d37aaeb55666fb0b892505e448949f13e4173d0d9a0196ccebd16e56324f42db33f6c0d47fbec658bce6aac6e1bec4e4e9d4bdd4d6a1237ffc11
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.0](https://github.com/bottrall/acp-sdk/compare/v0.6.0...v0.7.0) (2026-10-05)
4
+
5
+
6
+ ### Features
7
+
8
+ * terminal handlers on ACP::ClientConnection ([#77](https://github.com/bottrall/acp-sdk/issues/77)) ([8a42829](https://github.com/bottrall/acp-sdk/commit/8a428295d5cd7c92b56ec1b3ed1c900b485bbf01)), closes [#40](https://github.com/bottrall/acp-sdk/issues/40)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * attach data to method-not-found and invalid-request replies ([#75](https://github.com/bottrall/acp-sdk/issues/75)) ([9bc201c](https://github.com/bottrall/acp-sdk/commit/9bc201c53d86f506bedb5b49183b865cf9b8f4fe)), closes [#29](https://github.com/bottrall/acp-sdk/issues/29)
14
+ * don't send handler exception messages to the peer ([#74](https://github.com/bottrall/acp-sdk/issues/74)) ([af66b48](https://github.com/bottrall/acp-sdk/commit/af66b48cbab4a99244c28a9c6f6a12c9ea23efc7)), closes [#28](https://github.com/bottrall/acp-sdk/issues/28)
15
+
3
16
  ## [0.6.0](https://github.com/bottrall/acp-sdk/compare/v0.5.0...v0.6.0) (2026-10-04)
4
17
 
5
18
 
data/README.md CHANGED
@@ -50,6 +50,7 @@ The agent's contract:
50
50
 
51
51
  - It defines `new_session`, `prompt` and `cancel`. `load_session`, `list_sessions`, `resume_session`, `close_session` and `delete_session` are required only when `capabilities` advertises `load_session` or `session_capabilities.list`, `.resume`, `.close` or `.delete`, and `authenticate` only when `auth_methods` is non-empty, and `logout` only when `capabilities` advertises `auth.logout`; `start` raises `ArgumentError` when one is advertised but missing, and a method that is not advertised is answered with `-32601`. `change_session_mode` and `change_session_config_option` are optional, since modes and config options are offered per session rather than in `initialize`; `session/set_mode` and `session/set_config_option` are answered with `-32601` when the agent does not define them. `session_created`, if defined, runs right after the `session/new` reply is sent.
52
52
  - Each request method returns its response type or an `ACP::RequestError`, which is sent as the error reply. Params that do not match the schema are answered with `-32602` before the agent sees them.
53
+ - A handler that raises is answered with `-32603` and the message `Internal error`. Nothing from the exception reaches the peer — its class and message are logged to the transport's logger instead, since messages can interpolate paths, queries or credentials. Return an `ACP::RequestError` from the handler when the peer should see a real error message.
53
54
  - `cancel` runs on the transport's reader thread, so it must return quickly: set a flag and let the prompt notice it.
54
55
  - After a cancel, the agent must itself end the turn with `stopReason: cancelled`. `ACP::AgentConnection` does not enforce it.
55
56
  - `client.capabilities` is `nil` until the client sends `initialize`.
@@ -63,7 +64,7 @@ Extensions are `_`-prefixed methods outside the spec. `AgentConnection` takes `e
63
64
 
64
65
  ## Driving an agent
65
66
 
66
- `ACP::ClientConnection` is the other side of the same transport. Each method takes the request's generated `ACP::Types` object and returns its response type or the agent's `ACP::RequestError`. `connect` sends `initialize`, since Ruby reserves that name for the constructor, and `authenticate` and `logout` send their namesake methods. `session_set_mode` sends `session/set_mode`, and `session_set_config_option` sends `session/set_config_option` and returns the session's complete option list, which replaces any state held from an earlier reply.
67
+ `ACP::ClientConnection` is the other side of the same transport. Each method takes the request's generated `ACP::Types` object and returns its response type or the agent's `ACP::RequestError`. `connect` sends `initialize`, since Ruby reserves that name for the constructor, and `authenticate` and `logout` send their namesake methods. `session_set_mode` sends `session/set_mode`, and `session_set_config_option` sends `session/set_config_option` and returns the session's complete option list, which replaces any state held from an earlier reply. A handler that raises is answered with `-32603` and the message `Internal error`, and the exception is only logged to the transport's logger.
67
68
 
68
69
  `ACP::AgentProcess.spawn` starts a command with args, env and cwd, wires its stdio to a new `ACP::ClientConnection` and starts it. The block receives the connection and the process handle (`pid`, plus `stderr` when captured), and the child is terminated — after its stdin is closed, with TERM and then KILL if it will not exit — when the block exits, so a raised block cannot leak the process. Without a block, `spawn` returns the process and calling `terminate` is yours. `stderr` is `:inherit` by default, so the child writes to your stderr; pass `:capture` to collect it on the process instead.
69
70
 
@@ -105,6 +106,7 @@ connection = ACP::ClientConnection.new(
105
106
  - `session_prompt` and `session_load` yield the session's updates on the calling thread as they arrive and return once the agent replies. Updates outside those calls, such as the available commands after `session/new`, go to `updates`, which runs on the reader thread and must return quickly. `session_resume` does not take a block: the agent must not replay history on resume, so unlike `session_load` there is nothing to stream. `session_close` and `session_delete` end the session, discarding it on the agent, and return once the agent replies.
106
107
  - `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.
107
108
  - `read_text_file` and `write_text_file` answer the agent's `fs/read_text_file` and `fs/write_text_file` with an `ACP::Types::ReadTextFileResponse`, an `ACP::Types::WriteTextFileResponse` or an `ACP::RequestError`. They are routed by the `fs` capabilities `connect` sends: a method the client does not advertise is answered with `-32601` without reaching the handler, and `connect` raises `ArgumentError` — before sending `initialize` — when the request advertises a capability that has no handler.
109
+ - `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.
108
110
 
109
111
  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
112
 
@@ -5,8 +5,15 @@ class ACP::ClientConnection
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))?
8
+ # @rbs @create_terminal: (^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError))?
9
+ # @rbs @terminal_output: (^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError))?
10
+ # @rbs @wait_for_terminal_exit: (^(ACP::Types::WaitForTerminalExitRequest) ->
11
+ # (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError))?
12
+ # @rbs @kill_terminal: (^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError))?
13
+ # @rbs @release_terminal: (^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError))?
8
14
  # @rbs @updates: ACP::ClientConnection::_UpdateHandler
9
15
  # @rbs @fs_capabilities: ACP::Types::FileSystemCapabilities?
16
+ # @rbs @terminal: bool?
10
17
  # @rbs @extension_requests: Hash[String, ^(untyped) -> untyped]
11
18
  # @rbs @extension_notifications: Hash[String, ^(untyped) -> void]
12
19
  # @rbs @lock: Thread::Mutex
@@ -26,6 +33,12 @@ class ACP::ClientConnection
26
33
  # @rbs permission: ACP::ClientConnection::_PermissionHandler
27
34
  # @rbs read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
28
35
  # @rbs write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
36
+ # @rbs create_terminal: (^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError))?
37
+ # @rbs terminal_output: (^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError))?
38
+ # @rbs wait_for_terminal_exit: (^(ACP::Types::WaitForTerminalExitRequest) ->
39
+ # (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError))?
40
+ # @rbs kill_terminal: (^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError))?
41
+ # @rbs release_terminal: (^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError))?
29
42
  # @rbs updates: ACP::ClientConnection::_UpdateHandler
30
43
  # @rbs extension_requests: Hash[String, ^(untyped) -> untyped]
31
44
  # @rbs extension_notifications: Hash[String, ^(untyped) -> void]
@@ -36,6 +49,11 @@ class ACP::ClientConnection
36
49
  permission:,
37
50
  read_text_file: nil,
38
51
  write_text_file: nil,
52
+ create_terminal: nil,
53
+ terminal_output: nil,
54
+ wait_for_terminal_exit: nil,
55
+ kill_terminal: nil,
56
+ release_terminal: nil,
39
57
  updates: IGNORE,
40
58
  extension_requests: {},
41
59
  extension_notifications: {},
@@ -45,8 +63,14 @@ class ACP::ClientConnection
45
63
  @permission = permission
46
64
  @read_text_file = read_text_file
47
65
  @write_text_file = write_text_file
66
+ @create_terminal = create_terminal
67
+ @terminal_output = terminal_output
68
+ @wait_for_terminal_exit = wait_for_terminal_exit
69
+ @kill_terminal = kill_terminal
70
+ @release_terminal = release_terminal
48
71
  @updates = updates
49
72
  @fs_capabilities = nil
73
+ @terminal = nil
50
74
  @extension_requests = extension_requests
51
75
  @extension_notifications = extension_notifications
52
76
  extension_requests.each_key { |name| ACP::Extensions.validate_name(name) }
@@ -63,7 +87,12 @@ class ACP::ClientConnection
63
87
  requests: {
64
88
  'session/request_permission' => method(:request_permission),
65
89
  'fs/read_text_file' => method(:read_text_file),
66
- 'fs/write_text_file' => method(:write_text_file)
90
+ 'fs/write_text_file' => method(:write_text_file),
91
+ 'terminal/create' => method(:create_terminal),
92
+ 'terminal/output' => method(:terminal_output),
93
+ 'terminal/wait_for_exit' => method(:wait_for_terminal_exit),
94
+ 'terminal/kill' => method(:kill_terminal),
95
+ 'terminal/release' => method(:release_terminal)
67
96
  }.merge(@extension_requests),
68
97
  notifications: { 'session/update' => method(:dispatch) }.merge(@extension_notifications)
69
98
  )
@@ -81,7 +110,14 @@ class ACP::ClientConnection
81
110
  raise ArgumentError, "initialize advertises fs methods no handler serves: #{unserved.join(', ')}"
82
111
  end
83
112
 
113
+ terminal = request.client_capabilities&.terminal
114
+ unserved = unserved_terminal_methods(terminal)
115
+ unless unserved.empty?
116
+ raise ArgumentError, "initialize advertises terminal methods no handler serves: #{unserved.join(', ')}"
117
+ end
118
+
84
119
  @fs_capabilities = capabilities
120
+ @terminal = terminal
85
121
  parse(ACP::Types::InitializeResponse, @transport.request('initialize', request.to_h)).then do |response|
86
122
  next response if response.is_a?(ACP::RequestError)
87
123
 
@@ -349,6 +385,82 @@ class ACP::ClientConnection
349
385
  handler.call(request)
350
386
  end
351
387
 
388
+ # The terminal routes are registered at start, before connect records the
389
+ # advertised capability, so each one answers -32601 until then.
390
+ #
391
+ # @rbs params: untyped
392
+ # @rbs return: (ACP::Types::CreateTerminalResponse | ACP::RequestError)
393
+ def create_terminal(params)
394
+ return ACP::RequestError.unadvertised('terminal') unless @terminal
395
+
396
+ request = ACP::Types::CreateTerminalRequest.from_h(params)
397
+ rescue KeyError, TypeError, NoMethodError
398
+ ACP::RequestError.invalid_params
399
+ else
400
+ # Safe: connect refuses an advertised capability no handler serves.
401
+ handler = @create_terminal #: ^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError)
402
+ handler.call(request)
403
+ end
404
+
405
+ # @rbs params: untyped
406
+ # @rbs return: (ACP::Types::TerminalOutputResponse | ACP::RequestError)
407
+ def terminal_output(params)
408
+ return ACP::RequestError.unadvertised('terminal') unless @terminal
409
+
410
+ request = ACP::Types::TerminalOutputRequest.from_h(params)
411
+ rescue KeyError, TypeError, NoMethodError
412
+ ACP::RequestError.invalid_params
413
+ else
414
+ # Safe: connect refuses an advertised capability no handler serves.
415
+ handler = @terminal_output #: ^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError)
416
+ handler.call(request)
417
+ end
418
+
419
+ # The handler may block for as long as the command runs, so the transport
420
+ # must serve each request on its own thread for this not to hold up others.
421
+ #
422
+ # @rbs params: untyped
423
+ # @rbs return: (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError)
424
+ def wait_for_terminal_exit(params)
425
+ return ACP::RequestError.unadvertised('terminal') unless @terminal
426
+
427
+ request = ACP::Types::WaitForTerminalExitRequest.from_h(params)
428
+ rescue KeyError, TypeError, NoMethodError
429
+ ACP::RequestError.invalid_params
430
+ else
431
+ # Safe: connect refuses an advertised capability no handler serves.
432
+ handler = @wait_for_terminal_exit #: ^(ACP::Types::WaitForTerminalExitRequest) -> (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError)
433
+ handler.call(request)
434
+ end
435
+
436
+ # @rbs params: untyped
437
+ # @rbs return: (ACP::Types::KillTerminalResponse | ACP::RequestError)
438
+ def kill_terminal(params)
439
+ return ACP::RequestError.unadvertised('terminal') unless @terminal
440
+
441
+ request = ACP::Types::KillTerminalRequest.from_h(params)
442
+ rescue KeyError, TypeError, NoMethodError
443
+ ACP::RequestError.invalid_params
444
+ else
445
+ # Safe: connect refuses an advertised capability no handler serves.
446
+ handler = @kill_terminal #: ^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError)
447
+ handler.call(request)
448
+ end
449
+
450
+ # @rbs params: untyped
451
+ # @rbs return: (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
452
+ def release_terminal(params)
453
+ return ACP::RequestError.unadvertised('terminal') unless @terminal
454
+
455
+ request = ACP::Types::ReleaseTerminalRequest.from_h(params)
456
+ rescue KeyError, TypeError, NoMethodError
457
+ ACP::RequestError.invalid_params
458
+ else
459
+ # Safe: connect refuses an advertised capability no handler serves.
460
+ handler = @release_terminal #: ^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
461
+ handler.call(request)
462
+ end
463
+
352
464
  # The fs capabilities initialize advertises that no injected handler serves.
353
465
  #
354
466
  # @rbs capabilities: ACP::Types::FileSystemCapabilities?
@@ -359,4 +471,19 @@ class ACP::ClientConnection
359
471
  'fs.writeTextFile' => capabilities&.write_text_file && !@write_text_file
360
472
  }.select { |_, unserved| unserved }.keys
361
473
  end
474
+
475
+ # The terminal capabilities initialize advertises that no injected handler
476
+ # serves.
477
+ #
478
+ # @rbs capabilities: bool?
479
+ # @rbs return: Array[String]
480
+ def unserved_terminal_methods(capabilities)
481
+ {
482
+ 'terminal.create' => capabilities && !@create_terminal,
483
+ 'terminal.output' => capabilities && !@terminal_output,
484
+ 'terminal.wait_for_exit' => capabilities && !@wait_for_terminal_exit,
485
+ 'terminal.kill' => capabilities && !@kill_terminal,
486
+ 'terminal.release' => capabilities && !@release_terminal
487
+ }.select { |_, unserved| unserved }.keys
488
+ end
362
489
  end
@@ -48,9 +48,10 @@ class ACP::RequestError
48
48
  new(code: INVALID_REQUEST, message: 'Invalid request')
49
49
  end
50
50
 
51
+ # @rbs method: String?
51
52
  # @rbs return: ACP::RequestError
52
- def self.method_not_found
53
- new(code: METHOD_NOT_FOUND, message: 'Method not found')
53
+ def self.method_not_found(method = nil)
54
+ new(code: METHOD_NOT_FOUND, message: 'Method not found', data: method && { method: method })
54
55
  end
55
56
 
56
57
  # @rbs return: ACP::RequestError
@@ -92,7 +92,7 @@ class ACP::Transport::Stdio
92
92
  method = message['method']
93
93
  error = message['error']
94
94
  if method.is_a?(String) && message.key?('id')
95
- serve(requests[method], message['id'], message['params'])
95
+ serve(requests[method], message['id'], message['params'], method)
96
96
  elsif method.is_a?(String)
97
97
  # Run on the reader thread so notifications (session/update) keep their
98
98
  # order; a JSON-RPC notification has no reply to carry a handler error.
@@ -104,10 +104,21 @@ class ACP::Transport::Stdio
104
104
  code, text, data = error.values_at('code', 'message', 'data')
105
105
  settle(message['id'], ACP::RequestError.new(code:, message: text, data:))
106
106
  else
107
- reply(nil, ACP::RequestError.invalid_request)
107
+ reply(detectable_id(message), ACP::RequestError.invalid_request)
108
108
  end
109
109
  end
110
110
 
111
+ # JSON-RPC 2.0 says an error reply echoes the request id whenever it can be
112
+ # detected; ids outside the spec's String/Number/Null types are not, so
113
+ # those replies keep id null.
114
+ #
115
+ # @rbs message: Hash[String, untyped]
116
+ # @rbs return: untyped
117
+ def detectable_id(message)
118
+ id = message['id']
119
+ id.is_a?(String) || id.is_a?(Numeric) ? id : nil
120
+ end
121
+
111
122
  # @rbs line: String
112
123
  # @rbs return: Hash[String, untyped] | ACP::RequestError
113
124
  def parse(line)
@@ -122,10 +133,11 @@ class ACP::Transport::Stdio
122
133
  # @rbs handler: _Handler?
123
134
  # @rbs id: untyped
124
135
  # @rbs params: untyped
136
+ # @rbs method: String
125
137
  # @rbs return: void
126
- def serve(handler, id, params)
138
+ def serve(handler, id, params, method)
127
139
  Thread.new do
128
- outcome = handler ? invoke(handler, params) : ACP::RequestError.method_not_found
140
+ outcome = handler ? invoke(handler, params) : ACP::RequestError.method_not_found(method)
129
141
  case outcome
130
142
  when ACP::Transport::Reply
131
143
  reply(id, outcome.result)
@@ -149,7 +161,9 @@ class ACP::Transport::Stdio
149
161
  end
150
162
 
151
163
  # Handlers are application code at the protocol boundary: an exception
152
- # becomes an error response instead of killing the thread serving it.
164
+ # becomes an error response instead of killing the thread serving it. The
165
+ # exception is only logged, not sent: its message can carry paths, SQL or
166
+ # anything else the handler interpolated, which must not reach the peer.
153
167
  #
154
168
  # @rbs handler: _Handler
155
169
  # @rbs params: untyped
@@ -157,7 +171,8 @@ class ACP::Transport::Stdio
157
171
  def invoke(handler, params)
158
172
  handler.call(params)
159
173
  rescue StandardError => e
160
- ACP::RequestError.new(code: ACP::RequestError::INTERNAL_ERROR, message: e.message)
174
+ @logger.error("request handler raised #{e.class}: #{e.message}")
175
+ ACP::RequestError.internal_error
161
176
  end
162
177
 
163
178
  # The success arm is whatever the handler returned, which the transport only
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.6.0'
4
+ VERSION = '0.7.0'
5
5
  end
@@ -1,29 +1,41 @@
1
1
  # Generated from lib/acp/client_connection.rb with RBS::Inline
2
2
 
3
3
  class ACP::ClientConnection
4
- @logger: ACP::Transport::_Logger
4
+ @transport: ACP::AgentConnection::_Transport
5
5
 
6
- @pending_permissions: Hash[String, Array[Thread::Queue]]
6
+ @permission: ACP::ClientConnection::_PermissionHandler
7
7
 
8
- @streams: Hash[String, Thread::Queue]
8
+ @read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
9
9
 
10
- @lock: Thread::Mutex
10
+ @write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
11
11
 
12
- @extension_notifications: Hash[String, ^(untyped) -> void]
12
+ @create_terminal: (^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError))?
13
13
 
14
- @extension_requests: Hash[String, ^(untyped) -> untyped]
14
+ @terminal_output: (^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError))?
15
15
 
16
- @fs_capabilities: ACP::Types::FileSystemCapabilities?
16
+ @wait_for_terminal_exit: (^(ACP::Types::WaitForTerminalExitRequest) -> (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError))?
17
+
18
+ @kill_terminal: (^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError))?
19
+
20
+ @release_terminal: (^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError))?
17
21
 
18
22
  @updates: ACP::ClientConnection::_UpdateHandler
19
23
 
20
- @write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
24
+ @fs_capabilities: ACP::Types::FileSystemCapabilities?
21
25
 
22
- @read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
26
+ @terminal: bool?
23
27
 
24
- @permission: ACP::ClientConnection::_PermissionHandler
28
+ @extension_requests: Hash[String, ^(untyped) -> untyped]
25
29
 
26
- @transport: ACP::AgentConnection::_Transport
30
+ @extension_notifications: Hash[String, ^(untyped) -> void]
31
+
32
+ @lock: Thread::Mutex
33
+
34
+ @streams: Hash[String, Thread::Queue]
35
+
36
+ @pending_permissions: Hash[String, Array[Thread::Queue]]
37
+
38
+ @logger: ACP::Transport::_Logger
27
39
 
28
40
  PROTOCOL_VERSION: Integer
29
41
 
@@ -35,12 +47,18 @@ class ACP::ClientConnection
35
47
  # @rbs permission: ACP::ClientConnection::_PermissionHandler
36
48
  # @rbs read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
37
49
  # @rbs write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
50
+ # @rbs create_terminal: (^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError))?
51
+ # @rbs terminal_output: (^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError))?
52
+ # @rbs wait_for_terminal_exit: (^(ACP::Types::WaitForTerminalExitRequest) ->
53
+ # (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError))?
54
+ # @rbs kill_terminal: (^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError))?
55
+ # @rbs release_terminal: (^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError))?
38
56
  # @rbs updates: ACP::ClientConnection::_UpdateHandler
39
57
  # @rbs extension_requests: Hash[String, ^(untyped) -> untyped]
40
58
  # @rbs extension_notifications: Hash[String, ^(untyped) -> void]
41
59
  # @rbs logger: ACP::Transport::_Logger
42
60
  # @rbs return: 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
61
+ def initialize: (transport: ACP::AgentConnection::_Transport, permission: ACP::ClientConnection::_PermissionHandler, ?read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?, ?write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?, ?create_terminal: (^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError))?, ?terminal_output: (^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError))?, ?wait_for_terminal_exit: (^(ACP::Types::WaitForTerminalExitRequest) -> (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError))?, ?kill_terminal: (^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError))?, ?release_terminal: (^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError))?, ?updates: ACP::ClientConnection::_UpdateHandler, ?extension_requests: Hash[String, ^(untyped) -> untyped], ?extension_notifications: Hash[String, ^(untyped) -> void], ?logger: ACP::Transport::_Logger) -> void
44
62
 
45
63
  # @rbs return: Thread
46
64
  def start: () -> Thread
@@ -182,9 +200,42 @@ class ACP::ClientConnection
182
200
  # @rbs return: (ACP::Types::WriteTextFileResponse | ACP::RequestError)
183
201
  def write_text_file: (untyped params) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError)
184
202
 
203
+ # The terminal routes are registered at start, before connect records the
204
+ # advertised capability, so each one answers -32601 until then.
205
+ #
206
+ # @rbs params: untyped
207
+ # @rbs return: (ACP::Types::CreateTerminalResponse | ACP::RequestError)
208
+ def create_terminal: (untyped params) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError)
209
+
210
+ # @rbs params: untyped
211
+ # @rbs return: (ACP::Types::TerminalOutputResponse | ACP::RequestError)
212
+ def terminal_output: (untyped params) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError)
213
+
214
+ # The handler may block for as long as the command runs, so the transport
215
+ # must serve each request on its own thread for this not to hold up others.
216
+ #
217
+ # @rbs params: untyped
218
+ # @rbs return: (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError)
219
+ def wait_for_terminal_exit: (untyped params) -> (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError)
220
+
221
+ # @rbs params: untyped
222
+ # @rbs return: (ACP::Types::KillTerminalResponse | ACP::RequestError)
223
+ def kill_terminal: (untyped params) -> (ACP::Types::KillTerminalResponse | ACP::RequestError)
224
+
225
+ # @rbs params: untyped
226
+ # @rbs return: (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
227
+ def release_terminal: (untyped params) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
228
+
185
229
  # The fs capabilities initialize advertises that no injected handler serves.
186
230
  #
187
231
  # @rbs capabilities: ACP::Types::FileSystemCapabilities?
188
232
  # @rbs return: Array[String]
189
233
  def unserved_fs_methods: (ACP::Types::FileSystemCapabilities? capabilities) -> Array[String]
234
+
235
+ # The terminal capabilities initialize advertises that no injected handler
236
+ # serves.
237
+ #
238
+ # @rbs capabilities: bool?
239
+ # @rbs return: Array[String]
240
+ def unserved_terminal_methods: (bool? capabilities) -> Array[String]
190
241
  end
@@ -44,8 +44,9 @@ class ACP::RequestError
44
44
  # @rbs return: ACP::RequestError
45
45
  def self.invalid_request: () -> ACP::RequestError
46
46
 
47
+ # @rbs method: String?
47
48
  # @rbs return: ACP::RequestError
48
- def self.method_not_found: () -> ACP::RequestError
49
+ def self.method_not_found: (?String? method) -> ACP::RequestError
49
50
 
50
51
  # @rbs return: ACP::RequestError
51
52
  def self.invalid_params: () -> ACP::RequestError
@@ -62,6 +62,14 @@ class ACP::Transport::Stdio
62
62
  # @rbs return: void
63
63
  def route: (Hash[String, untyped] message, Hash[String, _Handler] requests, Hash[String, _NotificationHandler] notifications) -> void
64
64
 
65
+ # JSON-RPC 2.0 says an error reply echoes the request id whenever it can be
66
+ # detected; ids outside the spec's String/Number/Null types are not, so
67
+ # those replies keep id null.
68
+ #
69
+ # @rbs message: Hash[String, untyped]
70
+ # @rbs return: untyped
71
+ def detectable_id: (Hash[String, untyped] message) -> untyped
72
+
65
73
  # @rbs line: String
66
74
  # @rbs return: Hash[String, untyped] | ACP::RequestError
67
75
  def parse: (String line) -> (Hash[String, untyped] | ACP::RequestError)
@@ -69,8 +77,9 @@ class ACP::Transport::Stdio
69
77
  # @rbs handler: _Handler?
70
78
  # @rbs id: untyped
71
79
  # @rbs params: untyped
80
+ # @rbs method: String
72
81
  # @rbs return: void
73
- def serve: (_Handler? handler, untyped id, untyped params) -> void
82
+ def serve: (_Handler? handler, untyped id, untyped params, String method) -> void
74
83
 
75
84
  # For code with no reply to carry an error: a notification handler, or a
76
85
  # Reply's after once the reply is on the wire. An exception must not kill
@@ -82,7 +91,9 @@ class ACP::Transport::Stdio
82
91
  def quietly: (String label) { () -> void } -> void
83
92
 
84
93
  # Handlers are application code at the protocol boundary: an exception
85
- # becomes an error response instead of killing the thread serving it.
94
+ # becomes an error response instead of killing the thread serving it. The
95
+ # exception is only logged, not sent: its message can carry paths, SQL or
96
+ # anything else the handler interpolated, which must not reach the peer.
86
97
  #
87
98
  # @rbs handler: _Handler
88
99
  # @rbs params: untyped
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.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jake Bottrall