acp-sdk 0.4.0 → 0.5.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 +20 -0
- data/README.md +10 -3
- data/lib/acp/agent_connection/client.rb +26 -14
- data/lib/acp/agent_connection.rb +63 -12
- data/lib/acp/client_connection.rb +146 -5
- data/lib/acp/request_error.rb +21 -0
- data/lib/acp/transport/stderr_logger.rb +17 -0
- data/lib/acp/transport/stdio.rb +15 -6
- data/lib/acp/version.rb +1 -1
- data/sig/generated/acp/agent_connection/client.rbs +8 -7
- data/sig/generated/acp/agent_connection.rbs +30 -4
- data/sig/generated/acp/client_connection.rbs +67 -2
- data/sig/generated/acp/request_error.rbs +17 -0
- data/sig/generated/acp/transport/stderr_logger.rbs +13 -0
- data/sig/generated/acp/transport/stdio.rbs +9 -2
- data/sig/manual/acp/agent_connection.rbs +2 -0
- data/sig/manual/acp/transport.rbs +8 -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: 77ac970489b956c7216d4c94a8d5cc6e7bf3fe5bfd10110cb8558c119f58f69d
|
|
4
|
+
data.tar.gz: 29ec919463eb48d06811aa8b6c736163fd7872348ce99f3f8cdc10c5670bf8c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: acf89beeb444ff33ac273aececd55d645792fe591f346cbdce0a099b6b6bff4eedb4025cceadd3a46ec62253c85b111c0149466960707b3feaa92623dab600a8
|
|
7
|
+
data.tar.gz: 92fba44ccd9334876fa8cb689728a7a19cc3d473bf1e7ee27fe215d72e48952e04bc9c0428609e31e883bdef6986e1ec7b4c9383d8a37271646593b33ff069bd
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0](https://github.com/bottrall/acp-sdk/compare/v0.4.0...v0.5.0) (2026-10-04)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add authenticate and logout to ACP::ClientConnection ([#63](https://github.com/bottrall/acp-sdk/issues/63)) ([460585a](https://github.com/bottrall/acp-sdk/commit/460585a9a9401e35725ea379432694d9b76f35b6))
|
|
9
|
+
* add session_set_mode and session_set_config_option to ACP::ClientConnection ([#66](https://github.com/bottrall/acp-sdk/issues/66)) ([994179f](https://github.com/bottrall/acp-sdk/commit/994179f9ab51918bee7d29c8b473e0f05a55a7ad))
|
|
10
|
+
* add session/resume, session/close and session/delete to the client ([#65](https://github.com/bottrall/acp-sdk/issues/65)) ([4af834a](https://github.com/bottrall/acp-sdk/commit/4af834a5bc519753bad36d41efba902bcddc296b)), closes [#37](https://github.com/bottrall/acp-sdk/issues/37)
|
|
11
|
+
* elicitation on ACP::AgentConnection::Client ([#67](https://github.com/bottrall/acp-sdk/issues/67)) ([7d3212a](https://github.com/bottrall/acp-sdk/commit/7d3212a82ec6392351d8df1349b2fc21bf0bf030)), closes [#41](https://github.com/bottrall/acp-sdk/issues/41)
|
|
12
|
+
* filesystem handlers on ACP::ClientConnection ([#68](https://github.com/bottrall/acp-sdk/issues/68)) ([053f091](https://github.com/bottrall/acp-sdk/commit/053f0910adc3a7016bf1769099a54655f41bb9a6))
|
|
13
|
+
* injectable logger for dropped messages and swallowed errors ([#59](https://github.com/bottrall/acp-sdk/issues/59)) ([e66a306](https://github.com/bottrall/acp-sdk/commit/e66a30622922717d1be6d27c5b9a2931643fcd6b))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* call ACP::RequestError.unadvertised in elicitation capability checks ([#69](https://github.com/bottrall/acp-sdk/issues/69)) ([fda6dad](https://github.com/bottrall/acp-sdk/commit/fda6dad13e7fe308b5ff63e4c9e6fa9eef6e8274))
|
|
19
|
+
* only advertise terminal auth methods to clients that support them ([#61](https://github.com/bottrall/acp-sdk/issues/61)) ([1d2674a](https://github.com/bottrall/acp-sdk/commit/1d2674a978d938834d69d2c68df4f2c918ea801c)), closes [#33](https://github.com/bottrall/acp-sdk/issues/33)
|
|
20
|
+
* refuse overlapping streaming calls for the same session ([#64](https://github.com/bottrall/acp-sdk/issues/64)) ([a3a5b34](https://github.com/bottrall/acp-sdk/commit/a3a5b343f0ba2555b62643577226166fada27485))
|
|
21
|
+
* reject an unsupported protocol version in ACP::ClientConnection#connect ([#62](https://github.com/bottrall/acp-sdk/issues/62)) ([6980b32](https://github.com/bottrall/acp-sdk/commit/6980b328bcec997ae68c6af9077d623a73c15d39)), closes [#34](https://github.com/bottrall/acp-sdk/issues/34)
|
|
22
|
+
|
|
3
23
|
## [0.4.0](https://github.com/bottrall/acp-sdk/compare/v0.3.0...v0.4.0) (2026-10-02)
|
|
4
24
|
|
|
5
25
|
|
data/README.md
CHANGED
|
@@ -55,12 +55,13 @@ The agent's contract:
|
|
|
55
55
|
- `client.capabilities` is `nil` until the client sends `initialize`.
|
|
56
56
|
- `client.read_text_file` and `client.write_text_file` take an `ACP::Types::ReadTextFileRequest` or `ACP::Types::WriteTextFileRequest` and return the response type or an `ACP::RequestError`. Unless `client.capabilities` advertises `fs.read_text_file` or `fs.write_text_file`, they return `-32601` without sending anything to the client.
|
|
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
|
+
- `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.
|
|
58
59
|
|
|
59
|
-
[`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` and `session/
|
|
60
|
+
[`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`.
|
|
60
61
|
|
|
61
62
|
## Driving an agent
|
|
62
63
|
|
|
63
|
-
`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.
|
|
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.
|
|
64
65
|
|
|
65
66
|
`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.
|
|
66
67
|
|
|
@@ -90,12 +91,18 @@ Any transport over a pair of IOs works too, so the connection can still be wired
|
|
|
90
91
|
connection = ACP::ClientConnection.new(
|
|
91
92
|
transport: ACP::Transport::Stdio.new(input: some_io, output: other_io),
|
|
92
93
|
permission: ->(request) { ask_the_user(request) },
|
|
94
|
+
read_text_file: ->(request) { ACP::Types::ReadTextFileResponse.new(content: File.read(request.path)) },
|
|
95
|
+
write_text_file: ->(request) {
|
|
96
|
+
File.write(request.path, request.content)
|
|
97
|
+
ACP::Types::WriteTextFileResponse.new
|
|
98
|
+
},
|
|
93
99
|
updates: ->(notification) { show(notification) }
|
|
94
100
|
)
|
|
95
101
|
```
|
|
96
102
|
|
|
97
|
-
- `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.
|
|
103
|
+
- `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.
|
|
98
104
|
- `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
|
+
- `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.
|
|
99
106
|
|
|
100
107
|
## Contributing
|
|
101
108
|
|
|
@@ -39,7 +39,7 @@ class ACP::AgentConnection::Client
|
|
|
39
39
|
# @rbs request: ACP::Types::ReadTextFileRequest
|
|
40
40
|
# @rbs return: ACP::Types::ReadTextFileResponse | ACP::RequestError
|
|
41
41
|
def read_text_file(request)
|
|
42
|
-
return unadvertised('fs.readTextFile') unless capabilities&.fs&.read_text_file
|
|
42
|
+
return ACP::RequestError.unadvertised('fs.readTextFile') unless capabilities&.fs&.read_text_file
|
|
43
43
|
|
|
44
44
|
call('fs/read_text_file', ACP::Types::ReadTextFileResponse, request)
|
|
45
45
|
end
|
|
@@ -47,7 +47,7 @@ class ACP::AgentConnection::Client
|
|
|
47
47
|
# @rbs request: ACP::Types::WriteTextFileRequest
|
|
48
48
|
# @rbs return: ACP::Types::WriteTextFileResponse | ACP::RequestError
|
|
49
49
|
def write_text_file(request)
|
|
50
|
-
return unadvertised('fs.writeTextFile') unless capabilities&.fs&.write_text_file
|
|
50
|
+
return ACP::RequestError.unadvertised('fs.writeTextFile') unless capabilities&.fs&.write_text_file
|
|
51
51
|
|
|
52
52
|
call('fs/write_text_file', ACP::Types::WriteTextFileResponse, request)
|
|
53
53
|
end
|
|
@@ -82,6 +82,29 @@ class ACP::AgentConnection::Client
|
|
|
82
82
|
terminal_call('terminal/release', ACP::Types::ReleaseTerminalResponse, request)
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
+
# @rbs request: ACP::Types::CreateElicitationRequest
|
|
86
|
+
# @rbs return: ACP::Types::CreateElicitationResponse | ACP::RequestError
|
|
87
|
+
def create_elicitation(request)
|
|
88
|
+
case request.mode
|
|
89
|
+
when ACP::Types::CreateElicitationRequest::Mode::Form
|
|
90
|
+
return ACP::RequestError.unadvertised('elicitation.form') unless capabilities&.elicitation&.form
|
|
91
|
+
when ACP::Types::CreateElicitationRequest::Mode::Url
|
|
92
|
+
return ACP::RequestError.unadvertised('elicitation.url') unless capabilities&.elicitation&.url
|
|
93
|
+
else
|
|
94
|
+
return ACP::RequestError.unadvertised("elicitation.#{request.mode['mode']}")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
call('elicitation/create', ACP::Types::CreateElicitationResponse, request)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @rbs notification: ACP::Types::CompleteElicitationNotification
|
|
101
|
+
# @rbs return: void
|
|
102
|
+
def complete_elicitation(notification)
|
|
103
|
+
return ACP::RequestError.unadvertised('elicitation.url') unless capabilities&.elicitation&.url
|
|
104
|
+
|
|
105
|
+
@peer.notify('elicitation/complete', notification.to_h)
|
|
106
|
+
end
|
|
107
|
+
|
|
85
108
|
private
|
|
86
109
|
|
|
87
110
|
# @rbs rpc_method: String
|
|
@@ -102,19 +125,8 @@ class ACP::AgentConnection::Client
|
|
|
102
125
|
# @rbs request: ACP::AgentConnection::_Response
|
|
103
126
|
# @rbs return: untyped
|
|
104
127
|
def terminal_call(rpc_method, type, request)
|
|
105
|
-
return unadvertised('terminal') unless capabilities&.terminal
|
|
128
|
+
return ACP::RequestError.unadvertised('terminal') unless capabilities&.terminal
|
|
106
129
|
|
|
107
130
|
call(rpc_method, type, request)
|
|
108
131
|
end
|
|
109
|
-
|
|
110
|
-
# Same method-not-found code a peer would send, but with a message that says
|
|
111
|
-
# the refusal was local, so logs can tell the two cases apart.
|
|
112
|
-
#
|
|
113
|
-
# @rbs capability: String
|
|
114
|
-
# @rbs return: ACP::RequestError
|
|
115
|
-
def unadvertised(capability)
|
|
116
|
-
ACP::RequestError.new(
|
|
117
|
-
code: ACP::RequestError::METHOD_NOT_FOUND, message: "Client does not advertise #{capability}"
|
|
118
|
-
)
|
|
119
|
-
end
|
|
120
132
|
end
|
data/lib/acp/agent_connection.rb
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
class ACP::AgentConnection
|
|
4
4
|
# @rbs @transport: ACP::AgentConnection::_Transport
|
|
5
|
-
# @rbs @
|
|
5
|
+
# @rbs @capabilities: ACP::Types::AgentCapabilities
|
|
6
|
+
# @rbs @agent_info: ACP::Types::Implementation?
|
|
7
|
+
# @rbs @auth_methods: Array[ACP::Types::AuthMethod::t]
|
|
6
8
|
# @rbs @factory: ^(ACP::AgentConnection::Client) -> ACP::AgentConnection::_Agent
|
|
9
|
+
# @rbs @logger: ACP::Transport::_Logger
|
|
7
10
|
|
|
8
11
|
PROTOCOL_VERSION = 1 #: Integer
|
|
9
12
|
OPTIONAL = [
|
|
@@ -27,16 +30,22 @@ class ACP::AgentConnection
|
|
|
27
30
|
# @rbs capabilities: ACP::Types::AgentCapabilities
|
|
28
31
|
# @rbs agent_info: ACP::Types::Implementation?
|
|
29
32
|
# @rbs auth_methods: Array[ACP::Types::AuthMethod::t]
|
|
33
|
+
# @rbs logger: ACP::Transport::_Logger
|
|
30
34
|
# @rbs &factory: (ACP::AgentConnection::Client) -> ACP::AgentConnection::_Agent
|
|
31
35
|
# @rbs return: void
|
|
32
|
-
def initialize(
|
|
36
|
+
def initialize(
|
|
37
|
+
transport:,
|
|
38
|
+
capabilities:,
|
|
39
|
+
agent_info: nil,
|
|
40
|
+
auth_methods: [],
|
|
41
|
+
logger: ACP::Transport::StderrLogger.new,
|
|
42
|
+
&factory
|
|
43
|
+
)
|
|
33
44
|
@transport = transport
|
|
34
|
-
@
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
agent_info:
|
|
39
|
-
)
|
|
45
|
+
@logger = logger
|
|
46
|
+
@capabilities = capabilities
|
|
47
|
+
@agent_info = agent_info
|
|
48
|
+
@auth_methods = auth_methods
|
|
40
49
|
@factory = factory
|
|
41
50
|
end
|
|
42
51
|
|
|
@@ -44,7 +53,10 @@ class ACP::AgentConnection
|
|
|
44
53
|
def start
|
|
45
54
|
client = ACP::AgentConnection::Client.new(peer: @transport)
|
|
46
55
|
agent = @factory.call(client)
|
|
47
|
-
|
|
56
|
+
# Routes are fixed before a client connects, so the check sees the
|
|
57
|
+
# unfiltered auth methods: a terminal-capable client still needs
|
|
58
|
+
# authenticate routed.
|
|
59
|
+
advertised, unadvertised = OPTIONAL.partition { |method| method.advertised?(initialize_response(@auth_methods)) }
|
|
48
60
|
missing = advertised.map(&:agent_method).reject { |name| agent.respond_to?(name) }
|
|
49
61
|
raise ArgumentError, "initialize advertises methods the agent lacks: #{missing.join(', ')}" unless missing.empty?
|
|
50
62
|
|
|
@@ -82,7 +94,20 @@ class ACP::AgentConnection
|
|
|
82
94
|
# @rbs agent: ACP::AgentConnection::_Agent
|
|
83
95
|
# @rbs return: Hash[String, ^(untyped) -> void]
|
|
84
96
|
def notifications(agent)
|
|
85
|
-
{ 'session/cancel' => ->(params) {
|
|
97
|
+
{ 'session/cancel' => ->(params) { cancel(agent, params) } }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# A notification has no reply to carry a parse failure.
|
|
101
|
+
#
|
|
102
|
+
# @rbs agent: ACP::AgentConnection::_Agent
|
|
103
|
+
# @rbs params: untyped
|
|
104
|
+
# @rbs return: void
|
|
105
|
+
def cancel(agent, params)
|
|
106
|
+
notification = ACP::Types::CancelNotification.from_h(params)
|
|
107
|
+
rescue KeyError, TypeError, NoMethodError => e
|
|
108
|
+
@logger.warn("dropped malformed session/cancel: #{e.class}: #{e.message}")
|
|
109
|
+
else
|
|
110
|
+
agent.cancel(notification)
|
|
86
111
|
end
|
|
87
112
|
|
|
88
113
|
# Generated from_h raises on a missing key or a value of the wrong shape.
|
|
@@ -104,8 +129,34 @@ class ACP::AgentConnection
|
|
|
104
129
|
# @rbs request: ACP::Types::InitializeRequest
|
|
105
130
|
# @rbs return: ACP::Types::InitializeResponse
|
|
106
131
|
def connect(client, request)
|
|
107
|
-
|
|
108
|
-
|
|
132
|
+
capabilities = request.client_capabilities || ACP::Types::ClientCapabilities.new
|
|
133
|
+
client.capabilities = capabilities
|
|
134
|
+
initialize_response(auth_methods(capabilities))
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# @rbs auth_methods: Array[ACP::Types::AuthMethod::t]
|
|
138
|
+
# @rbs return: ACP::Types::InitializeResponse
|
|
139
|
+
def initialize_response(auth_methods)
|
|
140
|
+
ACP::Types::InitializeResponse.new(
|
|
141
|
+
protocol_version: PROTOCOL_VERSION,
|
|
142
|
+
agent_capabilities: @capabilities,
|
|
143
|
+
auth_methods:,
|
|
144
|
+
agent_info: @agent_info
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @rbs capabilities: ACP::Types::ClientCapabilities
|
|
149
|
+
# @rbs return: Array[ACP::Types::AuthMethod::t]
|
|
150
|
+
def auth_methods(capabilities)
|
|
151
|
+
return @auth_methods if capabilities.auth&.terminal
|
|
152
|
+
|
|
153
|
+
@auth_methods.reject { |method| terminal_auth_method?(method) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# @rbs method: ACP::Types::AuthMethod::t
|
|
157
|
+
# @rbs return: bool
|
|
158
|
+
def terminal_auth_method?(method)
|
|
159
|
+
method.is_a?(ACP::Types::AuthMethod::Terminal) || (method.is_a?(Hash) && method['type'] == 'terminal')
|
|
109
160
|
end
|
|
110
161
|
|
|
111
162
|
# session_created runs after the reply because the client must know the
|
|
@@ -3,28 +3,45 @@
|
|
|
3
3
|
class ACP::ClientConnection
|
|
4
4
|
# @rbs @transport: ACP::AgentConnection::_Transport
|
|
5
5
|
# @rbs @permission: ACP::ClientConnection::_PermissionHandler
|
|
6
|
+
# @rbs @read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
7
|
+
# @rbs @write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
|
6
8
|
# @rbs @updates: ACP::ClientConnection::_UpdateHandler
|
|
9
|
+
# @rbs @fs_capabilities: ACP::Types::FileSystemCapabilities?
|
|
7
10
|
# @rbs @lock: Thread::Mutex
|
|
8
11
|
# @rbs @streams: Hash[String, Thread::Queue]
|
|
12
|
+
# @rbs @logger: ACP::Transport::_Logger
|
|
13
|
+
|
|
14
|
+
PROTOCOL_VERSION = ACP::AgentConnection::PROTOCOL_VERSION #: Integer
|
|
9
15
|
|
|
10
16
|
IGNORE = ->(_notification) {} #: ^(ACP::Types::SessionNotification) -> void
|
|
11
17
|
|
|
12
18
|
# @rbs transport: ACP::AgentConnection::_Transport
|
|
13
19
|
# @rbs permission: ACP::ClientConnection::_PermissionHandler
|
|
20
|
+
# @rbs read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
21
|
+
# @rbs write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
|
14
22
|
# @rbs updates: ACP::ClientConnection::_UpdateHandler
|
|
23
|
+
# @rbs logger: ACP::Transport::_Logger
|
|
15
24
|
# @rbs return: void
|
|
16
|
-
def initialize(transport:, permission:, updates: IGNORE)
|
|
25
|
+
def initialize(transport:, permission:, read_text_file: nil, write_text_file: nil, updates: IGNORE, logger: ACP::Transport::StderrLogger.new)
|
|
17
26
|
@transport = transport
|
|
18
27
|
@permission = permission
|
|
28
|
+
@read_text_file = read_text_file
|
|
29
|
+
@write_text_file = write_text_file
|
|
19
30
|
@updates = updates
|
|
31
|
+
@fs_capabilities = nil
|
|
20
32
|
@lock = Mutex.new
|
|
21
33
|
@streams = {}
|
|
34
|
+
@logger = logger
|
|
22
35
|
end
|
|
23
36
|
|
|
24
37
|
# @rbs return: Thread
|
|
25
38
|
def start
|
|
26
39
|
@transport.start(
|
|
27
|
-
requests: {
|
|
40
|
+
requests: {
|
|
41
|
+
'session/request_permission' => method(:request_permission),
|
|
42
|
+
'fs/read_text_file' => method(:read_text_file),
|
|
43
|
+
'fs/write_text_file' => method(:write_text_file)
|
|
44
|
+
},
|
|
28
45
|
notifications: { 'session/update' => method(:dispatch) }
|
|
29
46
|
)
|
|
30
47
|
end
|
|
@@ -35,7 +52,23 @@ class ACP::ClientConnection
|
|
|
35
52
|
# @rbs request: ACP::Types::InitializeRequest
|
|
36
53
|
# @rbs return: ACP::Types::InitializeResponse | ACP::RequestError
|
|
37
54
|
def connect(request)
|
|
38
|
-
|
|
55
|
+
capabilities = request.client_capabilities&.fs
|
|
56
|
+
unserved = unserved_fs_methods(capabilities)
|
|
57
|
+
unless unserved.empty?
|
|
58
|
+
raise ArgumentError, "initialize advertises fs methods no handler serves: #{unserved.join(', ')}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
@fs_capabilities = capabilities
|
|
62
|
+
parse(ACP::Types::InitializeResponse, @transport.request('initialize', request.to_h)).then do |response|
|
|
63
|
+
next response if response.is_a?(ACP::RequestError)
|
|
64
|
+
|
|
65
|
+
next response if response.protocol_version == PROTOCOL_VERSION
|
|
66
|
+
|
|
67
|
+
ACP::RequestError.unsupported_protocol_version(
|
|
68
|
+
requested: request.protocol_version,
|
|
69
|
+
returned: response.protocol_version
|
|
70
|
+
)
|
|
71
|
+
end
|
|
39
72
|
end
|
|
40
73
|
|
|
41
74
|
# @rbs request: ACP::Types::NewSessionRequest
|
|
@@ -64,12 +97,60 @@ class ACP::ClientConnection
|
|
|
64
97
|
parse(ACP::Types::ListSessionsResponse, @transport.request('session/list', request.to_h))
|
|
65
98
|
end
|
|
66
99
|
|
|
100
|
+
# The agent does not replay history on resume, so unlike session_load there
|
|
101
|
+
# is nothing to stream and no block is taken.
|
|
102
|
+
#
|
|
103
|
+
# @rbs request: ACP::Types::ResumeSessionRequest
|
|
104
|
+
# @rbs return: ACP::Types::ResumeSessionResponse | ACP::RequestError
|
|
105
|
+
def session_resume(request)
|
|
106
|
+
parse(ACP::Types::ResumeSessionResponse, @transport.request('session/resume', request.to_h))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @rbs request: ACP::Types::CloseSessionRequest
|
|
110
|
+
# @rbs return: ACP::Types::CloseSessionResponse | ACP::RequestError
|
|
111
|
+
def session_close(request)
|
|
112
|
+
parse(ACP::Types::CloseSessionResponse, @transport.request('session/close', request.to_h))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @rbs request: ACP::Types::DeleteSessionRequest
|
|
116
|
+
# @rbs return: ACP::Types::DeleteSessionResponse | ACP::RequestError
|
|
117
|
+
def session_delete(request)
|
|
118
|
+
parse(ACP::Types::DeleteSessionResponse, @transport.request('session/delete', request.to_h))
|
|
119
|
+
end
|
|
120
|
+
|
|
67
121
|
# @rbs notification: ACP::Types::CancelNotification
|
|
68
122
|
# @rbs return: void
|
|
69
123
|
def session_cancel(notification)
|
|
70
124
|
@transport.notify('session/cancel', notification.to_h)
|
|
71
125
|
end
|
|
72
126
|
|
|
127
|
+
# @rbs request: ACP::Types::SetSessionModeRequest
|
|
128
|
+
# @rbs return: ACP::Types::SetSessionModeResponse | ACP::RequestError
|
|
129
|
+
def session_set_mode(request)
|
|
130
|
+
parse(ACP::Types::SetSessionModeResponse, @transport.request('session/set_mode', request.to_h))
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# The reply carries the session's complete option list, so it replaces any
|
|
134
|
+
# state held from an earlier reply rather than merging into it.
|
|
135
|
+
#
|
|
136
|
+
# @rbs request: ACP::Types::SetSessionConfigOptionRequest
|
|
137
|
+
# @rbs return: ACP::Types::SetSessionConfigOptionResponse | ACP::RequestError
|
|
138
|
+
def session_set_config_option(request)
|
|
139
|
+
parse(ACP::Types::SetSessionConfigOptionResponse, @transport.request('session/set_config_option', request.to_h))
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# @rbs request: ACP::Types::AuthenticateRequest
|
|
143
|
+
# @rbs return: ACP::Types::AuthenticateResponse | ACP::RequestError
|
|
144
|
+
def authenticate(request)
|
|
145
|
+
parse(ACP::Types::AuthenticateResponse, @transport.request('authenticate', request.to_h))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @rbs request: ACP::Types::LogoutRequest
|
|
149
|
+
# @rbs return: ACP::Types::LogoutResponse | ACP::RequestError
|
|
150
|
+
def logout(request)
|
|
151
|
+
parse(ACP::Types::LogoutResponse, @transport.request('logout', request.to_h))
|
|
152
|
+
end
|
|
153
|
+
|
|
73
154
|
private
|
|
74
155
|
|
|
75
156
|
# @rbs type: ACP::AgentConnection::_Parser
|
|
@@ -94,7 +175,21 @@ class ACP::ClientConnection
|
|
|
94
175
|
# @rbs return: (Hash[String, untyped] | ACP::RequestError)
|
|
95
176
|
def stream(session_id, method, params)
|
|
96
177
|
queue = Thread::Queue.new
|
|
97
|
-
|
|
178
|
+
# The check and the insert share one lock acquisition so a concurrent
|
|
179
|
+
# stream cannot register between them.
|
|
180
|
+
claimed = @lock.synchronize do
|
|
181
|
+
if @streams.key?(session_id)
|
|
182
|
+
false
|
|
183
|
+
else
|
|
184
|
+
@streams[session_id] = queue
|
|
185
|
+
true
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
unless claimed
|
|
189
|
+
return ACP::RequestError.new(
|
|
190
|
+
code: ACP::RequestError::INVALID_REQUEST, message: "A stream is already open for session #{session_id}"
|
|
191
|
+
)
|
|
192
|
+
end
|
|
98
193
|
requester = Thread.new do
|
|
99
194
|
@transport.request(method, params)
|
|
100
195
|
ensure
|
|
@@ -107,12 +202,16 @@ class ACP::ClientConnection
|
|
|
107
202
|
end
|
|
108
203
|
|
|
109
204
|
# Pushes under the lock so a stream cannot close its queue between the
|
|
110
|
-
# lookup and the push.
|
|
205
|
+
# lookup and the push. A malformed notification has no reply to carry the
|
|
206
|
+
# failure.
|
|
111
207
|
#
|
|
112
208
|
# @rbs params: untyped
|
|
113
209
|
# @rbs return: void
|
|
114
210
|
def dispatch(params)
|
|
115
211
|
notification = ACP::Types::SessionNotification.from_h(params)
|
|
212
|
+
rescue KeyError, TypeError, NoMethodError => e
|
|
213
|
+
@logger.warn("dropped malformed session/update: #{e.class}: #{e.message}")
|
|
214
|
+
else
|
|
116
215
|
queue = @lock.synchronize { @streams[notification.session_id]&.push(notification.update) }
|
|
117
216
|
@updates.call(notification) unless queue
|
|
118
217
|
end
|
|
@@ -126,4 +225,46 @@ class ACP::ClientConnection
|
|
|
126
225
|
else
|
|
127
226
|
@permission.call(request)
|
|
128
227
|
end
|
|
228
|
+
|
|
229
|
+
# The routes are registered at start, before connect records the advertised
|
|
230
|
+
# capabilities, so each one answers -32601 until then.
|
|
231
|
+
#
|
|
232
|
+
# @rbs params: untyped
|
|
233
|
+
# @rbs return: (ACP::Types::ReadTextFileResponse | ACP::RequestError)
|
|
234
|
+
def read_text_file(params)
|
|
235
|
+
return ACP::RequestError.unadvertised('fs.readTextFile') unless @fs_capabilities&.read_text_file
|
|
236
|
+
|
|
237
|
+
request = ACP::Types::ReadTextFileRequest.from_h(params)
|
|
238
|
+
rescue KeyError, TypeError, NoMethodError
|
|
239
|
+
ACP::RequestError.invalid_params
|
|
240
|
+
else
|
|
241
|
+
# Safe: connect refuses an advertised capability no handler serves.
|
|
242
|
+
handler = @read_text_file #: ^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError)
|
|
243
|
+
handler.call(request)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# @rbs params: untyped
|
|
247
|
+
# @rbs return: (ACP::Types::WriteTextFileResponse | ACP::RequestError)
|
|
248
|
+
def write_text_file(params)
|
|
249
|
+
return ACP::RequestError.unadvertised('fs.writeTextFile') unless @fs_capabilities&.write_text_file
|
|
250
|
+
|
|
251
|
+
request = ACP::Types::WriteTextFileRequest.from_h(params)
|
|
252
|
+
rescue KeyError, TypeError, NoMethodError
|
|
253
|
+
ACP::RequestError.invalid_params
|
|
254
|
+
else
|
|
255
|
+
# Safe: connect refuses an advertised capability no handler serves.
|
|
256
|
+
handler = @write_text_file #: ^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError)
|
|
257
|
+
handler.call(request)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# The fs capabilities initialize advertises that no injected handler serves.
|
|
261
|
+
#
|
|
262
|
+
# @rbs capabilities: ACP::Types::FileSystemCapabilities?
|
|
263
|
+
# @rbs return: Array[String]
|
|
264
|
+
def unserved_fs_methods(capabilities)
|
|
265
|
+
{
|
|
266
|
+
'fs.readTextFile' => capabilities&.read_text_file && !@read_text_file,
|
|
267
|
+
'fs.writeTextFile' => capabilities&.write_text_file && !@write_text_file
|
|
268
|
+
}.select { |_, unserved| unserved }.keys
|
|
269
|
+
end
|
|
129
270
|
end
|
data/lib/acp/request_error.rb
CHANGED
|
@@ -58,6 +58,16 @@ class ACP::RequestError
|
|
|
58
58
|
new(code: INVALID_PARAMS, message: 'Invalid params')
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
# The method-not-found code for a method the client's capabilities do not
|
|
62
|
+
# advertise, with a message that names the capability so it can be told
|
|
63
|
+
# apart from an unknown method.
|
|
64
|
+
#
|
|
65
|
+
# @rbs capability: String
|
|
66
|
+
# @rbs return: ACP::RequestError
|
|
67
|
+
def self.unadvertised(capability)
|
|
68
|
+
new(code: METHOD_NOT_FOUND, message: "Client does not advertise #{capability}")
|
|
69
|
+
end
|
|
70
|
+
|
|
61
71
|
# @rbs return: ACP::RequestError
|
|
62
72
|
def self.internal_error
|
|
63
73
|
new(code: INTERNAL_ERROR, message: 'Internal error')
|
|
@@ -72,6 +82,17 @@ class ACP::RequestError
|
|
|
72
82
|
new(code: INTERNAL_ERROR, message: 'Invalid response', data: response)
|
|
73
83
|
end
|
|
74
84
|
|
|
85
|
+
# A local refusal of an initialize reply that names a version the SDK does
|
|
86
|
+
# not speak; the message names both versions so logs can tell it from a
|
|
87
|
+
# peer's error.
|
|
88
|
+
#
|
|
89
|
+
# @rbs requested: Integer
|
|
90
|
+
# @rbs returned: Integer
|
|
91
|
+
# @rbs return: ACP::RequestError
|
|
92
|
+
def self.unsupported_protocol_version(requested:, returned:)
|
|
93
|
+
new(code: INTERNAL_ERROR, message: "Unsupported protocol version: requested #{requested}, returned #{returned}")
|
|
94
|
+
end
|
|
95
|
+
|
|
75
96
|
# @rbs return: ACP::RequestError
|
|
76
97
|
def self.request_cancelled
|
|
77
98
|
new(code: REQUEST_CANCELLED, message: 'Request cancelled')
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The default sink for dropped messages and swallowed errors: the spec
|
|
4
|
+
# sanctions agents logging to stderr.
|
|
5
|
+
class ACP::Transport::StderrLogger
|
|
6
|
+
# @rbs message: String
|
|
7
|
+
# @rbs return: void
|
|
8
|
+
def warn(message)
|
|
9
|
+
Kernel.warn("acp: WARN: #{message}")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @rbs message: String
|
|
13
|
+
# @rbs return: void
|
|
14
|
+
def error(message)
|
|
15
|
+
Kernel.warn("acp: ERROR: #{message}")
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/acp/transport/stdio.rb
CHANGED
|
@@ -15,6 +15,7 @@ class ACP::Transport::Stdio
|
|
|
15
15
|
# @rbs @pending: Hash[untyped, Thread::Queue]
|
|
16
16
|
# @rbs @next_id: Integer
|
|
17
17
|
# @rbs @closed: bool
|
|
18
|
+
# @rbs @logger: ACP::Transport::_Logger
|
|
18
19
|
|
|
19
20
|
CONNECTION_CLOSED = ACP::RequestError.new(
|
|
20
21
|
code: ACP::RequestError::INTERNAL_ERROR, message: 'Connection closed'
|
|
@@ -22,8 +23,9 @@ class ACP::Transport::Stdio
|
|
|
22
23
|
|
|
23
24
|
# @rbs input: _Reader
|
|
24
25
|
# @rbs output: _Writer
|
|
26
|
+
# @rbs logger: ACP::Transport::_Logger
|
|
25
27
|
# @rbs return: void
|
|
26
|
-
def initialize(input:, output:)
|
|
28
|
+
def initialize(input:, output:, logger: ACP::Transport::StderrLogger.new)
|
|
27
29
|
@input = input
|
|
28
30
|
@output = output
|
|
29
31
|
@write_lock = Mutex.new
|
|
@@ -31,6 +33,7 @@ class ACP::Transport::Stdio
|
|
|
31
33
|
@pending = {}
|
|
32
34
|
@next_id = 0
|
|
33
35
|
@closed = false
|
|
36
|
+
@logger = logger
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
# Returns the reader thread, which ends at EOF on input after releasing any
|
|
@@ -94,7 +97,7 @@ class ACP::Transport::Stdio
|
|
|
94
97
|
# Run on the reader thread so notifications (session/update) keep their
|
|
95
98
|
# order; a JSON-RPC notification has no reply to carry a handler error.
|
|
96
99
|
handler = notifications[method]
|
|
97
|
-
quietly { handler.call(message['params']) } if handler
|
|
100
|
+
quietly('notification handler') { handler.call(message['params']) } if handler
|
|
98
101
|
elsif message.key?('result')
|
|
99
102
|
settle(message['id'], message['result'])
|
|
100
103
|
elsif error.is_a?(Hash) && error['code'].is_a?(Integer) && error['message'].is_a?(String)
|
|
@@ -126,7 +129,7 @@ class ACP::Transport::Stdio
|
|
|
126
129
|
case outcome
|
|
127
130
|
when ACP::Transport::Reply
|
|
128
131
|
reply(id, outcome.result)
|
|
129
|
-
quietly(&outcome.after)
|
|
132
|
+
quietly('reply after callback', &outcome.after)
|
|
130
133
|
else reply(id, outcome)
|
|
131
134
|
end
|
|
132
135
|
end
|
|
@@ -136,12 +139,13 @@ class ACP::Transport::Stdio
|
|
|
136
139
|
# Reply's after once the reply is on the wire. An exception must not kill
|
|
137
140
|
# the reader thread or print a thread report.
|
|
138
141
|
#
|
|
142
|
+
# @rbs label: String
|
|
139
143
|
# @rbs &block: () -> void
|
|
140
144
|
# @rbs return: void
|
|
141
|
-
def quietly
|
|
145
|
+
def quietly(label)
|
|
142
146
|
yield
|
|
143
|
-
rescue StandardError
|
|
144
|
-
|
|
147
|
+
rescue StandardError => e
|
|
148
|
+
@logger.error("#{label} raised #{e.class}: #{e.message}")
|
|
145
149
|
end
|
|
146
150
|
|
|
147
151
|
# Handlers are application code at the protocol boundary: an exception
|
|
@@ -198,6 +202,9 @@ class ACP::Transport::Stdio
|
|
|
198
202
|
end
|
|
199
203
|
end
|
|
200
204
|
|
|
205
|
+
# A closed peer must not kill the thread writing the reply: the reader has
|
|
206
|
+
# to keep draining input so pending requests still settle at EOF.
|
|
207
|
+
#
|
|
201
208
|
# @rbs message: Hash[String, untyped]
|
|
202
209
|
# @rbs return: void
|
|
203
210
|
def write(message)
|
|
@@ -206,5 +213,7 @@ class ACP::Transport::Stdio
|
|
|
206
213
|
@output.write(line)
|
|
207
214
|
@output.flush
|
|
208
215
|
end
|
|
216
|
+
rescue Errno::EPIPE => e
|
|
217
|
+
@logger.warn("write to closed peer: #{e.class}: #{e.message}")
|
|
209
218
|
end
|
|
210
219
|
end
|
data/lib/acp/version.rb
CHANGED
|
@@ -55,6 +55,14 @@ class ACP::AgentConnection::Client
|
|
|
55
55
|
# @rbs return: ACP::Types::ReleaseTerminalResponse | ACP::RequestError
|
|
56
56
|
def release_terminal: (ACP::Types::ReleaseTerminalRequest request) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
|
|
57
57
|
|
|
58
|
+
# @rbs request: ACP::Types::CreateElicitationRequest
|
|
59
|
+
# @rbs return: ACP::Types::CreateElicitationResponse | ACP::RequestError
|
|
60
|
+
def create_elicitation: (ACP::Types::CreateElicitationRequest request) -> (ACP::Types::CreateElicitationResponse | ACP::RequestError)
|
|
61
|
+
|
|
62
|
+
# @rbs notification: ACP::Types::CompleteElicitationNotification
|
|
63
|
+
# @rbs return: void
|
|
64
|
+
def complete_elicitation: (ACP::Types::CompleteElicitationNotification notification) -> void
|
|
65
|
+
|
|
58
66
|
private
|
|
59
67
|
|
|
60
68
|
# @rbs rpc_method: String
|
|
@@ -68,11 +76,4 @@ class ACP::AgentConnection::Client
|
|
|
68
76
|
# @rbs request: ACP::AgentConnection::_Response
|
|
69
77
|
# @rbs return: untyped
|
|
70
78
|
def terminal_call: (String rpc_method, ACP::AgentConnection::_Parser type, ACP::AgentConnection::_Response request) -> untyped
|
|
71
|
-
|
|
72
|
-
# Same method-not-found code a peer would send, but with a message that says
|
|
73
|
-
# the refusal was local, so logs can tell the two cases apart.
|
|
74
|
-
#
|
|
75
|
-
# @rbs capability: String
|
|
76
|
-
# @rbs return: ACP::RequestError
|
|
77
|
-
def unadvertised: (String capability) -> ACP::RequestError
|
|
78
79
|
end
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
# Generated from lib/acp/agent_connection.rb with RBS::Inline
|
|
2
2
|
|
|
3
3
|
class ACP::AgentConnection
|
|
4
|
-
@
|
|
5
|
-
|
|
6
|
-
@initialize_response: ACP::Types::InitializeResponse
|
|
4
|
+
@logger: ACP::Transport::_Logger
|
|
7
5
|
|
|
8
6
|
@factory: ^(ACP::AgentConnection::Client) -> ACP::AgentConnection::_Agent
|
|
9
7
|
|
|
8
|
+
@auth_methods: Array[ACP::Types::AuthMethod::t]
|
|
9
|
+
|
|
10
|
+
@agent_info: ACP::Types::Implementation?
|
|
11
|
+
|
|
12
|
+
@capabilities: ACP::Types::AgentCapabilities
|
|
13
|
+
|
|
14
|
+
@transport: ACP::AgentConnection::_Transport
|
|
15
|
+
|
|
10
16
|
PROTOCOL_VERSION: Integer
|
|
11
17
|
|
|
12
18
|
OPTIONAL: Array[ACP::AgentConnection::_OptionalMethod]
|
|
@@ -20,9 +26,10 @@ class ACP::AgentConnection
|
|
|
20
26
|
# @rbs capabilities: ACP::Types::AgentCapabilities
|
|
21
27
|
# @rbs agent_info: ACP::Types::Implementation?
|
|
22
28
|
# @rbs auth_methods: Array[ACP::Types::AuthMethod::t]
|
|
29
|
+
# @rbs logger: ACP::Transport::_Logger
|
|
23
30
|
# @rbs &factory: (ACP::AgentConnection::Client) -> ACP::AgentConnection::_Agent
|
|
24
31
|
# @rbs return: void
|
|
25
|
-
def initialize: (transport: ACP::AgentConnection::_Transport, capabilities: ACP::Types::AgentCapabilities, ?agent_info: ACP::Types::Implementation?, ?auth_methods: Array[ACP::Types::AuthMethod::t]) { (ACP::AgentConnection::Client) -> ACP::AgentConnection::_Agent } -> 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
|
|
26
33
|
|
|
27
34
|
# @rbs return: Thread
|
|
28
35
|
def start: () -> Thread
|
|
@@ -38,6 +45,13 @@ class ACP::AgentConnection
|
|
|
38
45
|
# @rbs return: Hash[String, ^(untyped) -> void]
|
|
39
46
|
def notifications: (ACP::AgentConnection::_Agent agent) -> Hash[String, ^(untyped) -> void]
|
|
40
47
|
|
|
48
|
+
# A notification has no reply to carry a parse failure.
|
|
49
|
+
#
|
|
50
|
+
# @rbs agent: ACP::AgentConnection::_Agent
|
|
51
|
+
# @rbs params: untyped
|
|
52
|
+
# @rbs return: void
|
|
53
|
+
def cancel: (ACP::AgentConnection::_Agent agent, untyped params) -> void
|
|
54
|
+
|
|
41
55
|
# Generated from_h raises on a missing key or a value of the wrong shape.
|
|
42
56
|
#
|
|
43
57
|
# @rbs type: ACP::AgentConnection::_Parser
|
|
@@ -50,6 +64,18 @@ class ACP::AgentConnection
|
|
|
50
64
|
# @rbs return: ACP::Types::InitializeResponse
|
|
51
65
|
def connect: (ACP::AgentConnection::Client client, ACP::Types::InitializeRequest request) -> ACP::Types::InitializeResponse
|
|
52
66
|
|
|
67
|
+
# @rbs auth_methods: Array[ACP::Types::AuthMethod::t]
|
|
68
|
+
# @rbs return: ACP::Types::InitializeResponse
|
|
69
|
+
def initialize_response: (Array[ACP::Types::AuthMethod::t] auth_methods) -> ACP::Types::InitializeResponse
|
|
70
|
+
|
|
71
|
+
# @rbs capabilities: ACP::Types::ClientCapabilities
|
|
72
|
+
# @rbs return: Array[ACP::Types::AuthMethod::t]
|
|
73
|
+
def auth_methods: (ACP::Types::ClientCapabilities capabilities) -> Array[ACP::Types::AuthMethod::t]
|
|
74
|
+
|
|
75
|
+
# @rbs method: ACP::Types::AuthMethod::t
|
|
76
|
+
# @rbs return: bool
|
|
77
|
+
def terminal_auth_method?: (ACP::Types::AuthMethod::t method) -> bool
|
|
78
|
+
|
|
53
79
|
# session_created runs after the reply because the client must know the
|
|
54
80
|
# session id before updates for it (e.g. available commands) arrive.
|
|
55
81
|
#
|
|
@@ -1,23 +1,36 @@
|
|
|
1
1
|
# Generated from lib/acp/client_connection.rb with RBS::Inline
|
|
2
2
|
|
|
3
3
|
class ACP::ClientConnection
|
|
4
|
+
@logger: ACP::Transport::_Logger
|
|
5
|
+
|
|
4
6
|
@streams: Hash[String, Thread::Queue]
|
|
5
7
|
|
|
6
8
|
@lock: Thread::Mutex
|
|
7
9
|
|
|
10
|
+
@fs_capabilities: ACP::Types::FileSystemCapabilities?
|
|
11
|
+
|
|
8
12
|
@updates: ACP::ClientConnection::_UpdateHandler
|
|
9
13
|
|
|
14
|
+
@write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
|
15
|
+
|
|
16
|
+
@read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
17
|
+
|
|
10
18
|
@permission: ACP::ClientConnection::_PermissionHandler
|
|
11
19
|
|
|
12
20
|
@transport: ACP::AgentConnection::_Transport
|
|
13
21
|
|
|
22
|
+
PROTOCOL_VERSION: Integer
|
|
23
|
+
|
|
14
24
|
IGNORE: ^(ACP::Types::SessionNotification) -> void
|
|
15
25
|
|
|
16
26
|
# @rbs transport: ACP::AgentConnection::_Transport
|
|
17
27
|
# @rbs permission: ACP::ClientConnection::_PermissionHandler
|
|
28
|
+
# @rbs read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
29
|
+
# @rbs write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
|
18
30
|
# @rbs updates: ACP::ClientConnection::_UpdateHandler
|
|
31
|
+
# @rbs logger: ACP::Transport::_Logger
|
|
19
32
|
# @rbs return: void
|
|
20
|
-
def initialize: (transport: ACP::AgentConnection::_Transport, permission: ACP::ClientConnection::_PermissionHandler, ?updates: ACP::ClientConnection::_UpdateHandler) -> 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
|
|
21
34
|
|
|
22
35
|
# @rbs return: Thread
|
|
23
36
|
def start: () -> Thread
|
|
@@ -47,10 +60,44 @@ class ACP::ClientConnection
|
|
|
47
60
|
# @rbs return: ACP::Types::ListSessionsResponse | ACP::RequestError
|
|
48
61
|
def session_list: (ACP::Types::ListSessionsRequest request) -> (ACP::Types::ListSessionsResponse | ACP::RequestError)
|
|
49
62
|
|
|
63
|
+
# The agent does not replay history on resume, so unlike session_load there
|
|
64
|
+
# is nothing to stream and no block is taken.
|
|
65
|
+
#
|
|
66
|
+
# @rbs request: ACP::Types::ResumeSessionRequest
|
|
67
|
+
# @rbs return: ACP::Types::ResumeSessionResponse | ACP::RequestError
|
|
68
|
+
def session_resume: (ACP::Types::ResumeSessionRequest request) -> (ACP::Types::ResumeSessionResponse | ACP::RequestError)
|
|
69
|
+
|
|
70
|
+
# @rbs request: ACP::Types::CloseSessionRequest
|
|
71
|
+
# @rbs return: ACP::Types::CloseSessionResponse | ACP::RequestError
|
|
72
|
+
def session_close: (ACP::Types::CloseSessionRequest request) -> (ACP::Types::CloseSessionResponse | ACP::RequestError)
|
|
73
|
+
|
|
74
|
+
# @rbs request: ACP::Types::DeleteSessionRequest
|
|
75
|
+
# @rbs return: ACP::Types::DeleteSessionResponse | ACP::RequestError
|
|
76
|
+
def session_delete: (ACP::Types::DeleteSessionRequest request) -> (ACP::Types::DeleteSessionResponse | ACP::RequestError)
|
|
77
|
+
|
|
50
78
|
# @rbs notification: ACP::Types::CancelNotification
|
|
51
79
|
# @rbs return: void
|
|
52
80
|
def session_cancel: (ACP::Types::CancelNotification notification) -> void
|
|
53
81
|
|
|
82
|
+
# @rbs request: ACP::Types::SetSessionModeRequest
|
|
83
|
+
# @rbs return: ACP::Types::SetSessionModeResponse | ACP::RequestError
|
|
84
|
+
def session_set_mode: (ACP::Types::SetSessionModeRequest request) -> (ACP::Types::SetSessionModeResponse | ACP::RequestError)
|
|
85
|
+
|
|
86
|
+
# The reply carries the session's complete option list, so it replaces any
|
|
87
|
+
# state held from an earlier reply rather than merging into it.
|
|
88
|
+
#
|
|
89
|
+
# @rbs request: ACP::Types::SetSessionConfigOptionRequest
|
|
90
|
+
# @rbs return: ACP::Types::SetSessionConfigOptionResponse | ACP::RequestError
|
|
91
|
+
def session_set_config_option: (ACP::Types::SetSessionConfigOptionRequest request) -> (ACP::Types::SetSessionConfigOptionResponse | ACP::RequestError)
|
|
92
|
+
|
|
93
|
+
# @rbs request: ACP::Types::AuthenticateRequest
|
|
94
|
+
# @rbs return: ACP::Types::AuthenticateResponse | ACP::RequestError
|
|
95
|
+
def authenticate: (ACP::Types::AuthenticateRequest request) -> (ACP::Types::AuthenticateResponse | ACP::RequestError)
|
|
96
|
+
|
|
97
|
+
# @rbs request: ACP::Types::LogoutRequest
|
|
98
|
+
# @rbs return: ACP::Types::LogoutResponse | ACP::RequestError
|
|
99
|
+
def logout: (ACP::Types::LogoutRequest request) -> (ACP::Types::LogoutResponse | ACP::RequestError)
|
|
100
|
+
|
|
54
101
|
private
|
|
55
102
|
|
|
56
103
|
# @rbs type: ACP::AgentConnection::_Parser
|
|
@@ -70,7 +117,8 @@ class ACP::ClientConnection
|
|
|
70
117
|
def stream: (String session_id, String method, Hash[String, untyped] params) { (ACP::Types::SessionUpdate::t) -> void } -> (Hash[String, untyped] | ACP::RequestError)
|
|
71
118
|
|
|
72
119
|
# Pushes under the lock so a stream cannot close its queue between the
|
|
73
|
-
# lookup and the push.
|
|
120
|
+
# lookup and the push. A malformed notification has no reply to carry the
|
|
121
|
+
# failure.
|
|
74
122
|
#
|
|
75
123
|
# @rbs params: untyped
|
|
76
124
|
# @rbs return: void
|
|
@@ -79,4 +127,21 @@ class ACP::ClientConnection
|
|
|
79
127
|
# @rbs params: untyped
|
|
80
128
|
# @rbs return: (ACP::Types::RequestPermissionResponse | ACP::RequestError)
|
|
81
129
|
def request_permission: (untyped params) -> (ACP::Types::RequestPermissionResponse | ACP::RequestError)
|
|
130
|
+
|
|
131
|
+
# The routes are registered at start, before connect records the advertised
|
|
132
|
+
# capabilities, so each one answers -32601 until then.
|
|
133
|
+
#
|
|
134
|
+
# @rbs params: untyped
|
|
135
|
+
# @rbs return: (ACP::Types::ReadTextFileResponse | ACP::RequestError)
|
|
136
|
+
def read_text_file: (untyped params) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError)
|
|
137
|
+
|
|
138
|
+
# @rbs params: untyped
|
|
139
|
+
# @rbs return: (ACP::Types::WriteTextFileResponse | ACP::RequestError)
|
|
140
|
+
def write_text_file: (untyped params) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError)
|
|
141
|
+
|
|
142
|
+
# The fs capabilities initialize advertises that no injected handler serves.
|
|
143
|
+
#
|
|
144
|
+
# @rbs capabilities: ACP::Types::FileSystemCapabilities?
|
|
145
|
+
# @rbs return: Array[String]
|
|
146
|
+
def unserved_fs_methods: (ACP::Types::FileSystemCapabilities? capabilities) -> Array[String]
|
|
82
147
|
end
|
|
@@ -50,6 +50,14 @@ class ACP::RequestError
|
|
|
50
50
|
# @rbs return: ACP::RequestError
|
|
51
51
|
def self.invalid_params: () -> ACP::RequestError
|
|
52
52
|
|
|
53
|
+
# The method-not-found code for a method the client's capabilities do not
|
|
54
|
+
# advertise, with a message that names the capability so it can be told
|
|
55
|
+
# apart from an unknown method.
|
|
56
|
+
#
|
|
57
|
+
# @rbs capability: String
|
|
58
|
+
# @rbs return: ACP::RequestError
|
|
59
|
+
def self.unadvertised: (String capability) -> ACP::RequestError
|
|
60
|
+
|
|
53
61
|
# @rbs return: ACP::RequestError
|
|
54
62
|
def self.internal_error: () -> ACP::RequestError
|
|
55
63
|
|
|
@@ -60,6 +68,15 @@ class ACP::RequestError
|
|
|
60
68
|
# @rbs return: ACP::RequestError
|
|
61
69
|
def self.invalid_response: (untyped response) -> ACP::RequestError
|
|
62
70
|
|
|
71
|
+
# A local refusal of an initialize reply that names a version the SDK does
|
|
72
|
+
# not speak; the message names both versions so logs can tell it from a
|
|
73
|
+
# peer's error.
|
|
74
|
+
#
|
|
75
|
+
# @rbs requested: Integer
|
|
76
|
+
# @rbs returned: Integer
|
|
77
|
+
# @rbs return: ACP::RequestError
|
|
78
|
+
def self.unsupported_protocol_version: (requested: Integer, returned: Integer) -> ACP::RequestError
|
|
79
|
+
|
|
63
80
|
# @rbs return: ACP::RequestError
|
|
64
81
|
def self.request_cancelled: () -> ACP::RequestError
|
|
65
82
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Generated from lib/acp/transport/stderr_logger.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
# The default sink for dropped messages and swallowed errors: the spec
|
|
4
|
+
# sanctions agents logging to stderr.
|
|
5
|
+
class ACP::Transport::StderrLogger
|
|
6
|
+
# @rbs message: String
|
|
7
|
+
# @rbs return: void
|
|
8
|
+
def warn: (String message) -> void
|
|
9
|
+
|
|
10
|
+
# @rbs message: String
|
|
11
|
+
# @rbs return: void
|
|
12
|
+
def error: (String message) -> void
|
|
13
|
+
end
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
# outbound request mid-turn and still see notifications (e.g. session/cancel)
|
|
7
7
|
# that arrive meanwhile.
|
|
8
8
|
class ACP::Transport::Stdio
|
|
9
|
+
@logger: ACP::Transport::_Logger
|
|
10
|
+
|
|
9
11
|
@closed: bool
|
|
10
12
|
|
|
11
13
|
@next_id: Integer
|
|
@@ -24,8 +26,9 @@ class ACP::Transport::Stdio
|
|
|
24
26
|
|
|
25
27
|
# @rbs input: _Reader
|
|
26
28
|
# @rbs output: _Writer
|
|
29
|
+
# @rbs logger: ACP::Transport::_Logger
|
|
27
30
|
# @rbs return: void
|
|
28
|
-
def initialize: (input: _Reader, output: _Writer) -> void
|
|
31
|
+
def initialize: (input: _Reader, output: _Writer, ?logger: ACP::Transport::_Logger) -> void
|
|
29
32
|
|
|
30
33
|
# Returns the reader thread, which ends at EOF on input after releasing any
|
|
31
34
|
# pending outbound requests.
|
|
@@ -73,9 +76,10 @@ class ACP::Transport::Stdio
|
|
|
73
76
|
# Reply's after once the reply is on the wire. An exception must not kill
|
|
74
77
|
# the reader thread or print a thread report.
|
|
75
78
|
#
|
|
79
|
+
# @rbs label: String
|
|
76
80
|
# @rbs &block: () -> void
|
|
77
81
|
# @rbs return: void
|
|
78
|
-
def quietly: () { () -> void } -> void
|
|
82
|
+
def quietly: (String label) { () -> void } -> void
|
|
79
83
|
|
|
80
84
|
# Handlers are application code at the protocol boundary: an exception
|
|
81
85
|
# becomes an error response instead of killing the thread serving it.
|
|
@@ -105,6 +109,9 @@ class ACP::Transport::Stdio
|
|
|
105
109
|
# @rbs return: void
|
|
106
110
|
def close: () -> void
|
|
107
111
|
|
|
112
|
+
# A closed peer must not kill the thread writing the reply: the reader has
|
|
113
|
+
# to keep draining input so pending requests still settle at EOF.
|
|
114
|
+
#
|
|
108
115
|
# @rbs message: Hash[String, untyped]
|
|
109
116
|
# @rbs return: void
|
|
110
117
|
def write: (Hash[String, untyped] message) -> void
|
|
@@ -79,6 +79,8 @@ class ACP::AgentConnection
|
|
|
79
79
|
def wait_for_terminal_exit: (ACP::Types::WaitForTerminalExitRequest) -> (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError)
|
|
80
80
|
def kill_terminal: (ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError)
|
|
81
81
|
def release_terminal: (ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
|
|
82
|
+
def create_elicitation: (ACP::Types::CreateElicitationRequest request) -> (ACP::Types::CreateElicitationResponse | ACP::RequestError)
|
|
83
|
+
def complete_elicitation: (ACP::Types::CompleteElicitationNotification notification) -> void
|
|
82
84
|
def capabilities: () -> ACP::Types::ClientCapabilities?
|
|
83
85
|
end
|
|
84
86
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
module ACP::Transport
|
|
2
|
+
# Receives the reports the SDK logs for dropped messages and swallowed
|
|
3
|
+
# errors; a log message is a single line with no trailing newline.
|
|
4
|
+
interface _Logger
|
|
5
|
+
def warn: (String message) -> void
|
|
6
|
+
def error: (String message) -> void
|
|
7
|
+
end
|
|
8
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: acp-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jake Bottrall
|
|
@@ -48,6 +48,7 @@ files:
|
|
|
48
48
|
- lib/acp/sdk.rb
|
|
49
49
|
- lib/acp/transport.rb
|
|
50
50
|
- lib/acp/transport/reply.rb
|
|
51
|
+
- lib/acp/transport/stderr_logger.rb
|
|
51
52
|
- lib/acp/transport/stdio.rb
|
|
52
53
|
- lib/acp/types.rb
|
|
53
54
|
- lib/acp/types/agent_auth_capabilities.rb
|
|
@@ -262,6 +263,7 @@ files:
|
|
|
262
263
|
- sig/generated/acp/sdk.rbs
|
|
263
264
|
- sig/generated/acp/transport.rbs
|
|
264
265
|
- sig/generated/acp/transport/reply.rbs
|
|
266
|
+
- sig/generated/acp/transport/stderr_logger.rbs
|
|
265
267
|
- sig/generated/acp/transport/stdio.rbs
|
|
266
268
|
- sig/generated/acp/types.rbs
|
|
267
269
|
- sig/generated/acp/types/agent_auth_capabilities.rbs
|
|
@@ -470,6 +472,7 @@ files:
|
|
|
470
472
|
- sig/manual/acp/agent_connection/optional_method/resume_session.rbs
|
|
471
473
|
- sig/manual/acp/agent_process.rbs
|
|
472
474
|
- sig/manual/acp/client_connection.rbs
|
|
475
|
+
- sig/manual/acp/transport.rbs
|
|
473
476
|
- sig/manual/acp/transport/stdio.rbs
|
|
474
477
|
homepage: https://github.com/bottrall/acp-sdk
|
|
475
478
|
licenses:
|