acp-sdk 0.5.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 +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +7 -1
- data/lib/acp/agent_connection/client.rb +20 -0
- data/lib/acp/agent_connection.rb +14 -1
- data/lib/acp/client_connection.rb +224 -5
- data/lib/acp/extensions.rb +15 -0
- data/lib/acp/request_error.rb +3 -2
- data/lib/acp/transport/stdio.rb +21 -6
- data/lib/acp/version.rb +1 -1
- data/sig/generated/acp/agent_connection/client.rbs +14 -0
- data/sig/generated/acp/agent_connection.rbs +7 -1
- data/sig/generated/acp/client_connection.rbs +103 -9
- data/sig/generated/acp/extensions.rbs +9 -0
- data/sig/generated/acp/request_error.rbs +2 -1
- data/sig/generated/acp/transport/stdio.rbs +13 -2
- data/sig/manual/acp/agent_connection.rbs +2 -0
- data/sig/manual/acp/extensions.rbs +6 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 978b245a94b3cd60d3858ee42c3deaf9b687c8a6a1701ce54c684a5e434b2bce
|
|
4
|
+
data.tar.gz: b83adfe811bc7873455f9f81993061e7b2ea66d0a75b78645807fc44e08cd58b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2907a71594882b7afd3ed8fe89b2e9520f139dc1c235a4e43bf836a2f127ae6264ce948d86885577daa6cd320078c6da831f4b8df89225201287276e17710c4e
|
|
7
|
+
data.tar.gz: 9191c0362932d37aaeb55666fb0b892505e448949f13e4173d0d9a0196ccebd16e56324f42db33f6c0d47fbec658bce6aac6e1bec4e4e9d4bdd4d6a1237ffc11
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
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
|
+
|
|
16
|
+
## [0.6.0](https://github.com/bottrall/acp-sdk/compare/v0.5.0...v0.6.0) (2026-10-04)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* 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))
|
|
22
|
+
* extension methods on both connections ([#72](https://github.com/bottrall/acp-sdk/issues/72)) ([d1faa1a](https://github.com/bottrall/acp-sdk/commit/d1faa1a738019e73a4dbad5b59f480795f8dd4bf))
|
|
23
|
+
|
|
3
24
|
## [0.5.0](https://github.com/bottrall/acp-sdk/compare/v0.4.0...v0.5.0) (2026-10-04)
|
|
4
25
|
|
|
5
26
|
|
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`.
|
|
@@ -57,11 +58,13 @@ The agent's contract:
|
|
|
57
58
|
- `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
59
|
- `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
60
|
|
|
61
|
+
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`.
|
|
62
|
+
|
|
60
63
|
[`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
64
|
|
|
62
65
|
## Driving an agent
|
|
63
66
|
|
|
64
|
-
`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.
|
|
65
68
|
|
|
66
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.
|
|
67
70
|
|
|
@@ -103,6 +106,9 @@ connection = ACP::ClientConnection.new(
|
|
|
103
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.
|
|
104
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.
|
|
105
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.
|
|
110
|
+
|
|
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`.
|
|
106
112
|
|
|
107
113
|
## Contributing
|
|
108
114
|
|
|
@@ -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
|
data/lib/acp/agent_connection.rb
CHANGED
|
@@ -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(
|
|
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
|
|
@@ -5,32 +5,79 @@ 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?
|
|
17
|
+
# @rbs @extension_requests: Hash[String, ^(untyped) -> untyped]
|
|
18
|
+
# @rbs @extension_notifications: Hash[String, ^(untyped) -> void]
|
|
10
19
|
# @rbs @lock: Thread::Mutex
|
|
11
20
|
# @rbs @streams: Hash[String, Thread::Queue]
|
|
21
|
+
# @rbs @pending_permissions: Hash[String, Array[Thread::Queue]]
|
|
12
22
|
# @rbs @logger: ACP::Transport::_Logger
|
|
13
23
|
|
|
14
24
|
PROTOCOL_VERSION = ACP::AgentConnection::PROTOCOL_VERSION #: Integer
|
|
15
25
|
|
|
16
26
|
IGNORE = ->(_notification) {} #: ^(ACP::Types::SessionNotification) -> void
|
|
17
27
|
|
|
28
|
+
CANCELLED = ACP::Types::RequestPermissionResponse.new(
|
|
29
|
+
outcome: ACP::Types::RequestPermissionOutcome::Cancelled.new
|
|
30
|
+
) #: ACP::Types::RequestPermissionResponse
|
|
31
|
+
|
|
18
32
|
# @rbs transport: ACP::AgentConnection::_Transport
|
|
19
33
|
# @rbs permission: ACP::ClientConnection::_PermissionHandler
|
|
20
34
|
# @rbs read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
21
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))?
|
|
22
42
|
# @rbs updates: ACP::ClientConnection::_UpdateHandler
|
|
43
|
+
# @rbs extension_requests: Hash[String, ^(untyped) -> untyped]
|
|
44
|
+
# @rbs extension_notifications: Hash[String, ^(untyped) -> void]
|
|
23
45
|
# @rbs logger: ACP::Transport::_Logger
|
|
24
46
|
# @rbs return: void
|
|
25
|
-
def initialize(
|
|
47
|
+
def initialize(
|
|
48
|
+
transport:,
|
|
49
|
+
permission:,
|
|
50
|
+
read_text_file: nil,
|
|
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,
|
|
57
|
+
updates: IGNORE,
|
|
58
|
+
extension_requests: {},
|
|
59
|
+
extension_notifications: {},
|
|
60
|
+
logger: ACP::Transport::StderrLogger.new
|
|
61
|
+
)
|
|
26
62
|
@transport = transport
|
|
27
63
|
@permission = permission
|
|
28
64
|
@read_text_file = read_text_file
|
|
29
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
|
|
30
71
|
@updates = updates
|
|
31
72
|
@fs_capabilities = nil
|
|
73
|
+
@terminal = nil
|
|
74
|
+
@extension_requests = extension_requests
|
|
75
|
+
@extension_notifications = extension_notifications
|
|
76
|
+
extension_requests.each_key { |name| ACP::Extensions.validate_name(name) }
|
|
77
|
+
extension_notifications.each_key { |name| ACP::Extensions.validate_name(name) }
|
|
32
78
|
@lock = Mutex.new
|
|
33
79
|
@streams = {}
|
|
80
|
+
@pending_permissions = {}
|
|
34
81
|
@logger = logger
|
|
35
82
|
end
|
|
36
83
|
|
|
@@ -40,9 +87,14 @@ class ACP::ClientConnection
|
|
|
40
87
|
requests: {
|
|
41
88
|
'session/request_permission' => method(:request_permission),
|
|
42
89
|
'fs/read_text_file' => method(:read_text_file),
|
|
43
|
-
'fs/write_text_file' => method(:write_text_file)
|
|
44
|
-
|
|
45
|
-
|
|
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)
|
|
96
|
+
}.merge(@extension_requests),
|
|
97
|
+
notifications: { 'session/update' => method(:dispatch) }.merge(@extension_notifications)
|
|
46
98
|
)
|
|
47
99
|
end
|
|
48
100
|
|
|
@@ -58,7 +110,14 @@ class ACP::ClientConnection
|
|
|
58
110
|
raise ArgumentError, "initialize advertises fs methods no handler serves: #{unserved.join(', ')}"
|
|
59
111
|
end
|
|
60
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
|
+
|
|
61
119
|
@fs_capabilities = capabilities
|
|
120
|
+
@terminal = terminal
|
|
62
121
|
parse(ACP::Types::InitializeResponse, @transport.request('initialize', request.to_h)).then do |response|
|
|
63
122
|
next response if response.is_a?(ACP::RequestError)
|
|
64
123
|
|
|
@@ -118,10 +177,17 @@ class ACP::ClientConnection
|
|
|
118
177
|
parse(ACP::Types::DeleteSessionResponse, @transport.request('session/delete', request.to_h))
|
|
119
178
|
end
|
|
120
179
|
|
|
180
|
+
# The spec requires a pending session/request_permission to be answered with
|
|
181
|
+
# the cancelled outcome once the turn is cancelled, so every queue registered
|
|
182
|
+
# for the session gets one before this returns. The notification goes first
|
|
183
|
+
# so the wire order is cancel, then the cancelled replies.
|
|
184
|
+
#
|
|
121
185
|
# @rbs notification: ACP::Types::CancelNotification
|
|
122
186
|
# @rbs return: void
|
|
123
187
|
def session_cancel(notification)
|
|
124
188
|
@transport.notify('session/cancel', notification.to_h)
|
|
189
|
+
pending = @lock.synchronize { @pending_permissions.delete(notification.session_id) }
|
|
190
|
+
pending&.each { |replies| replies << [:value, CANCELLED] }
|
|
125
191
|
end
|
|
126
192
|
|
|
127
193
|
# @rbs request: ACP::Types::SetSessionModeRequest
|
|
@@ -151,6 +217,26 @@ class ACP::ClientConnection
|
|
|
151
217
|
parse(ACP::Types::LogoutResponse, @transport.request('logout', request.to_h))
|
|
152
218
|
end
|
|
153
219
|
|
|
220
|
+
# Sends an extension request, keyed by its raw `_`-prefixed wire name, and
|
|
221
|
+
# returns the agent's reply as-is: extension methods have no schema to
|
|
222
|
+
# parse the result into.
|
|
223
|
+
#
|
|
224
|
+
# @rbs method: String
|
|
225
|
+
# @rbs params: untyped
|
|
226
|
+
# @rbs return: (Hash[String, untyped] | ACP::RequestError)
|
|
227
|
+
def ext_request(method, params = nil)
|
|
228
|
+
ACP::Extensions.validate_name(method)
|
|
229
|
+
@transport.request(method, params)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# @rbs method: String
|
|
233
|
+
# @rbs params: untyped
|
|
234
|
+
# @rbs return: void
|
|
235
|
+
def ext_notify(method, params = nil)
|
|
236
|
+
ACP::Extensions.validate_name(method)
|
|
237
|
+
@transport.notify(method, params)
|
|
238
|
+
end
|
|
239
|
+
|
|
154
240
|
private
|
|
155
241
|
|
|
156
242
|
# @rbs type: ACP::AgentConnection::_Parser
|
|
@@ -216,6 +302,10 @@ class ACP::ClientConnection
|
|
|
216
302
|
@updates.call(notification) unless queue
|
|
217
303
|
end
|
|
218
304
|
|
|
305
|
+
# The handler runs on its own thread so a cancelled request is answered
|
|
306
|
+
# without waiting for it, and whatever it returns after that is pushed to a
|
|
307
|
+
# queue nobody reads.
|
|
308
|
+
#
|
|
219
309
|
# @rbs params: untyped
|
|
220
310
|
# @rbs return: (ACP::Types::RequestPermissionResponse | ACP::RequestError)
|
|
221
311
|
def request_permission(params)
|
|
@@ -223,7 +313,45 @@ class ACP::ClientConnection
|
|
|
223
313
|
rescue KeyError, TypeError, NoMethodError
|
|
224
314
|
ACP::RequestError.invalid_params
|
|
225
315
|
else
|
|
226
|
-
|
|
316
|
+
replies = Thread::Queue.new
|
|
317
|
+
register_permission(request.session_id, replies)
|
|
318
|
+
Thread.new do
|
|
319
|
+
# A raised StandardError is not a returned ACP::RequestError, so the two
|
|
320
|
+
# need distinct shapes on the queue.
|
|
321
|
+
replies << begin
|
|
322
|
+
[:value, @permission.call(request)]
|
|
323
|
+
rescue StandardError => e
|
|
324
|
+
[:raised, e]
|
|
325
|
+
end
|
|
326
|
+
ensure
|
|
327
|
+
unregister_permission(request.session_id, replies)
|
|
328
|
+
end
|
|
329
|
+
settled = replies.pop
|
|
330
|
+
# Re-raised so the transport's invoke turns it into an error reply, as it
|
|
331
|
+
# would if the handler ran on the serve thread itself.
|
|
332
|
+
raise settled[1] if settled[0] == :raise
|
|
333
|
+
|
|
334
|
+
settled[1]
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# @rbs session_id: String
|
|
338
|
+
# @rbs replies: Thread::Queue
|
|
339
|
+
# @rbs return: void
|
|
340
|
+
def register_permission(session_id, replies)
|
|
341
|
+
@lock.synchronize { (@pending_permissions[session_id] ||= []) << replies }
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# @rbs session_id: String
|
|
345
|
+
# @rbs replies: Thread::Queue
|
|
346
|
+
# @rbs return: void
|
|
347
|
+
def unregister_permission(session_id, replies)
|
|
348
|
+
@lock.synchronize do
|
|
349
|
+
pending = @pending_permissions[session_id]
|
|
350
|
+
if pending
|
|
351
|
+
pending.delete(replies)
|
|
352
|
+
@pending_permissions.delete(session_id) if pending.empty?
|
|
353
|
+
end
|
|
354
|
+
end
|
|
227
355
|
end
|
|
228
356
|
|
|
229
357
|
# The routes are registered at start, before connect records the advertised
|
|
@@ -257,6 +385,82 @@ class ACP::ClientConnection
|
|
|
257
385
|
handler.call(request)
|
|
258
386
|
end
|
|
259
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
|
+
|
|
260
464
|
# The fs capabilities initialize advertises that no injected handler serves.
|
|
261
465
|
#
|
|
262
466
|
# @rbs capabilities: ACP::Types::FileSystemCapabilities?
|
|
@@ -267,4 +471,19 @@ class ACP::ClientConnection
|
|
|
267
471
|
'fs.writeTextFile' => capabilities&.write_text_file && !@write_text_file
|
|
268
472
|
}.select { |_, unserved| unserved }.keys
|
|
269
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
|
|
270
489
|
end
|
|
@@ -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/request_error.rb
CHANGED
|
@@ -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
|
data/lib/acp/transport/stdio.rb
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
@@ -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
|
|
@@ -1,36 +1,64 @@
|
|
|
1
1
|
# Generated from lib/acp/client_connection.rb with RBS::Inline
|
|
2
2
|
|
|
3
3
|
class ACP::ClientConnection
|
|
4
|
-
@
|
|
4
|
+
@transport: ACP::AgentConnection::_Transport
|
|
5
5
|
|
|
6
|
-
@
|
|
6
|
+
@permission: ACP::ClientConnection::_PermissionHandler
|
|
7
7
|
|
|
8
|
-
@
|
|
8
|
+
@read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
9
9
|
|
|
10
|
-
@
|
|
10
|
+
@write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
|
11
|
+
|
|
12
|
+
@create_terminal: (^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError))?
|
|
13
|
+
|
|
14
|
+
@terminal_output: (^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError))?
|
|
15
|
+
|
|
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))?
|
|
11
21
|
|
|
12
22
|
@updates: ACP::ClientConnection::_UpdateHandler
|
|
13
23
|
|
|
14
|
-
@
|
|
24
|
+
@fs_capabilities: ACP::Types::FileSystemCapabilities?
|
|
15
25
|
|
|
16
|
-
@
|
|
26
|
+
@terminal: bool?
|
|
17
27
|
|
|
18
|
-
@
|
|
28
|
+
@extension_requests: Hash[String, ^(untyped) -> untyped]
|
|
19
29
|
|
|
20
|
-
@
|
|
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
|
|
21
39
|
|
|
22
40
|
PROTOCOL_VERSION: Integer
|
|
23
41
|
|
|
24
42
|
IGNORE: ^(ACP::Types::SessionNotification) -> void
|
|
25
43
|
|
|
44
|
+
CANCELLED: ACP::Types::RequestPermissionResponse
|
|
45
|
+
|
|
26
46
|
# @rbs transport: ACP::AgentConnection::_Transport
|
|
27
47
|
# @rbs permission: ACP::ClientConnection::_PermissionHandler
|
|
28
48
|
# @rbs read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
29
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))?
|
|
30
56
|
# @rbs updates: ACP::ClientConnection::_UpdateHandler
|
|
57
|
+
# @rbs extension_requests: Hash[String, ^(untyped) -> untyped]
|
|
58
|
+
# @rbs extension_notifications: Hash[String, ^(untyped) -> void]
|
|
31
59
|
# @rbs logger: ACP::Transport::_Logger
|
|
32
60
|
# @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
|
|
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
|
|
34
62
|
|
|
35
63
|
# @rbs return: Thread
|
|
36
64
|
def start: () -> Thread
|
|
@@ -75,6 +103,11 @@ class ACP::ClientConnection
|
|
|
75
103
|
# @rbs return: ACP::Types::DeleteSessionResponse | ACP::RequestError
|
|
76
104
|
def session_delete: (ACP::Types::DeleteSessionRequest request) -> (ACP::Types::DeleteSessionResponse | ACP::RequestError)
|
|
77
105
|
|
|
106
|
+
# The spec requires a pending session/request_permission to be answered with
|
|
107
|
+
# the cancelled outcome once the turn is cancelled, so every queue registered
|
|
108
|
+
# for the session gets one before this returns. The notification goes first
|
|
109
|
+
# so the wire order is cancel, then the cancelled replies.
|
|
110
|
+
#
|
|
78
111
|
# @rbs notification: ACP::Types::CancelNotification
|
|
79
112
|
# @rbs return: void
|
|
80
113
|
def session_cancel: (ACP::Types::CancelNotification notification) -> void
|
|
@@ -98,6 +131,20 @@ class ACP::ClientConnection
|
|
|
98
131
|
# @rbs return: ACP::Types::LogoutResponse | ACP::RequestError
|
|
99
132
|
def logout: (ACP::Types::LogoutRequest request) -> (ACP::Types::LogoutResponse | ACP::RequestError)
|
|
100
133
|
|
|
134
|
+
# Sends an extension request, keyed by its raw `_`-prefixed wire name, and
|
|
135
|
+
# returns the agent's reply as-is: extension methods have no schema to
|
|
136
|
+
# parse the result into.
|
|
137
|
+
#
|
|
138
|
+
# @rbs method: String
|
|
139
|
+
# @rbs params: untyped
|
|
140
|
+
# @rbs return: (Hash[String, untyped] | ACP::RequestError)
|
|
141
|
+
def ext_request: (String method, ?untyped params) -> (Hash[String, untyped] | ACP::RequestError)
|
|
142
|
+
|
|
143
|
+
# @rbs method: String
|
|
144
|
+
# @rbs params: untyped
|
|
145
|
+
# @rbs return: void
|
|
146
|
+
def ext_notify: (String method, ?untyped params) -> void
|
|
147
|
+
|
|
101
148
|
private
|
|
102
149
|
|
|
103
150
|
# @rbs type: ACP::AgentConnection::_Parser
|
|
@@ -124,10 +171,24 @@ class ACP::ClientConnection
|
|
|
124
171
|
# @rbs return: void
|
|
125
172
|
def dispatch: (untyped params) -> void
|
|
126
173
|
|
|
174
|
+
# The handler runs on its own thread so a cancelled request is answered
|
|
175
|
+
# without waiting for it, and whatever it returns after that is pushed to a
|
|
176
|
+
# queue nobody reads.
|
|
177
|
+
#
|
|
127
178
|
# @rbs params: untyped
|
|
128
179
|
# @rbs return: (ACP::Types::RequestPermissionResponse | ACP::RequestError)
|
|
129
180
|
def request_permission: (untyped params) -> (ACP::Types::RequestPermissionResponse | ACP::RequestError)
|
|
130
181
|
|
|
182
|
+
# @rbs session_id: String
|
|
183
|
+
# @rbs replies: Thread::Queue
|
|
184
|
+
# @rbs return: void
|
|
185
|
+
def register_permission: (String session_id, Thread::Queue replies) -> void
|
|
186
|
+
|
|
187
|
+
# @rbs session_id: String
|
|
188
|
+
# @rbs replies: Thread::Queue
|
|
189
|
+
# @rbs return: void
|
|
190
|
+
def unregister_permission: (String session_id, Thread::Queue replies) -> void
|
|
191
|
+
|
|
131
192
|
# The routes are registered at start, before connect records the advertised
|
|
132
193
|
# capabilities, so each one answers -32601 until then.
|
|
133
194
|
#
|
|
@@ -139,9 +200,42 @@ class ACP::ClientConnection
|
|
|
139
200
|
# @rbs return: (ACP::Types::WriteTextFileResponse | ACP::RequestError)
|
|
140
201
|
def write_text_file: (untyped params) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError)
|
|
141
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
|
+
|
|
142
229
|
# The fs capabilities initialize advertises that no injected handler serves.
|
|
143
230
|
#
|
|
144
231
|
# @rbs capabilities: ACP::Types::FileSystemCapabilities?
|
|
145
232
|
# @rbs return: Array[String]
|
|
146
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]
|
|
147
241
|
end
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: acp-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.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
|