acp-sdk 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +14 -3
- data/lib/acp/agent_connection/client.rb +46 -14
- data/lib/acp/agent_connection.rb +77 -13
- data/lib/acp/client_connection.rb +240 -7
- data/lib/acp/extensions.rb +15 -0
- 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 +22 -7
- data/sig/generated/acp/agent_connection.rbs +35 -3
- data/sig/generated/acp/client_connection.rbs +110 -2
- data/sig/generated/acp/extensions.rbs +9 -0
- 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 +4 -0
- data/sig/manual/acp/extensions.rbs +6 -0
- data/sig/manual/acp/transport.rbs +8 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 367678f3b99c82b2f768879be0308249d9b32ce8ce94c3d4e6de6c12c4a1c043
|
|
4
|
+
data.tar.gz: 071e457b8e00fa19be760fff65cfd69998102e54b4b3d47f9b3cc0d80c359fc3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 706206bd29062ef6ecf1856b9e9671113e02bb6eb6ac837c2815bfcdbbff4bc07ac8d2c6394ec6f7b33c37a3832a9e0c4bb5c4e5c4699c1cdc129e875f693605
|
|
7
|
+
data.tar.gz: 5a73a945d099dedcf5837aa1c897b1dca0313a49d131337756b26c28731ddedb14b96c21722c7d6ecff3a21e6bed811e036551724c12bebcdf53f88a56121f8e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.0](https://github.com/bottrall/acp-sdk/compare/v0.5.0...v0.6.0) (2026-10-04)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* answer pending permission requests with cancelled after session_cancel ([#70](https://github.com/bottrall/acp-sdk/issues/70)) ([06a218b](https://github.com/bottrall/acp-sdk/commit/06a218b6262ff409e301d08a43330af5b60837e0))
|
|
9
|
+
* extension methods on both connections ([#72](https://github.com/bottrall/acp-sdk/issues/72)) ([d1faa1a](https://github.com/bottrall/acp-sdk/commit/d1faa1a738019e73a4dbad5b59f480795f8dd4bf))
|
|
10
|
+
|
|
11
|
+
## [0.5.0](https://github.com/bottrall/acp-sdk/compare/v0.4.0...v0.5.0) (2026-10-04)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* 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))
|
|
17
|
+
* 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))
|
|
18
|
+
* 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)
|
|
19
|
+
* 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)
|
|
20
|
+
* filesystem handlers on ACP::ClientConnection ([#68](https://github.com/bottrall/acp-sdk/issues/68)) ([053f091](https://github.com/bottrall/acp-sdk/commit/053f0910adc3a7016bf1769099a54655f41bb9a6))
|
|
21
|
+
* 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))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Bug Fixes
|
|
25
|
+
|
|
26
|
+
* 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))
|
|
27
|
+
* 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)
|
|
28
|
+
* 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))
|
|
29
|
+
* 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)
|
|
30
|
+
|
|
3
31
|
## [0.4.0](https://github.com/bottrall/acp-sdk/compare/v0.3.0...v0.4.0) (2026-10-02)
|
|
4
32
|
|
|
5
33
|
|
data/README.md
CHANGED
|
@@ -55,12 +55,15 @@ 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
|
-
|
|
60
|
+
Extensions are `_`-prefixed methods outside the spec. `AgentConnection` takes `extension_requests` and `extension_notifications` hashes keyed by the raw wire name, and `client.ext_request` and `client.ext_notify` send them to the client; a handler name without the `_` prefix raises `ArgumentError`, and an extension request with no handler is answered with `-32601`.
|
|
61
|
+
|
|
62
|
+
[`examples/echo_agent.rb`](https://github.com/bottrall/acp-sdk/blob/main/examples/echo_agent.rb) is a complete agent that streams updates, asks permission, handles cancellation, reads and writes files and runs commands through the client (`/read <path>`, `/write <path> <text>` and `/run <command> [args]`, each advertised as a slash command only when the client supports it), and supports `session/load`, `session/list`, `session/resume`, `session/close` and `session/delete`.
|
|
60
63
|
|
|
61
64
|
## Driving an agent
|
|
62
65
|
|
|
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.
|
|
66
|
+
`ACP::ClientConnection` is the other side of the same transport. Each method takes the request's generated `ACP::Types` object and returns its response type or the agent's `ACP::RequestError`. `connect` sends `initialize`, since Ruby reserves that name for the constructor, and `authenticate` and `logout` send their namesake methods. `session_set_mode` sends `session/set_mode`, and `session_set_config_option` sends `session/set_config_option` and returns the session's complete option list, which replaces any state held from an earlier reply.
|
|
64
67
|
|
|
65
68
|
`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
69
|
|
|
@@ -90,12 +93,20 @@ Any transport over a pair of IOs works too, so the connection can still be wired
|
|
|
90
93
|
connection = ACP::ClientConnection.new(
|
|
91
94
|
transport: ACP::Transport::Stdio.new(input: some_io, output: other_io),
|
|
92
95
|
permission: ->(request) { ask_the_user(request) },
|
|
96
|
+
read_text_file: ->(request) { ACP::Types::ReadTextFileResponse.new(content: File.read(request.path)) },
|
|
97
|
+
write_text_file: ->(request) {
|
|
98
|
+
File.write(request.path, request.content)
|
|
99
|
+
ACP::Types::WriteTextFileResponse.new
|
|
100
|
+
},
|
|
93
101
|
updates: ->(notification) { show(notification) }
|
|
94
102
|
)
|
|
95
103
|
```
|
|
96
104
|
|
|
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.
|
|
105
|
+
- `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
106
|
- `permission` answers `session/request_permission` with an `ACP::Types::RequestPermissionResponse` or an `ACP::RequestError`. It runs on its own thread, so it may block while the user decides, and `session_cancel` can be sent meanwhile.
|
|
107
|
+
- `read_text_file` and `write_text_file` answer the agent's `fs/read_text_file` and `fs/write_text_file` with an `ACP::Types::ReadTextFileResponse`, an `ACP::Types::WriteTextFileResponse` or an `ACP::RequestError`. They are routed by the `fs` capabilities `connect` sends: a method the client does not advertise is answered with `-32601` without reaching the handler, and `connect` raises `ArgumentError` — before sending `initialize` — when the request advertises a capability that has no handler.
|
|
108
|
+
|
|
109
|
+
Extensions work the same way in both directions: `extension_requests` and `extension_notifications` are hashes keyed by the raw `_`-prefixed wire name, `ext_request` sends one to the agent and returns its result as-is, and `ext_notify` sends an extension notification. A handler name without the `_` prefix raises `ArgumentError`, and an extension request nothing answers gets `-32601`.
|
|
99
110
|
|
|
100
111
|
## Contributing
|
|
101
112
|
|
|
@@ -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,49 @@ 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
|
+
|
|
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
|
+
|
|
85
128
|
private
|
|
86
129
|
|
|
87
130
|
# @rbs rpc_method: String
|
|
@@ -102,19 +145,8 @@ class ACP::AgentConnection::Client
|
|
|
102
145
|
# @rbs request: ACP::AgentConnection::_Response
|
|
103
146
|
# @rbs return: untyped
|
|
104
147
|
def terminal_call(rpc_method, type, request)
|
|
105
|
-
return unadvertised('terminal') unless capabilities&.terminal
|
|
148
|
+
return ACP::RequestError.unadvertised('terminal') unless capabilities&.terminal
|
|
106
149
|
|
|
107
150
|
call(rpc_method, type, request)
|
|
108
151
|
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
152
|
end
|
data/lib/acp/agent_connection.rb
CHANGED
|
@@ -2,8 +2,13 @@
|
|
|
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 @extension_requests: Hash[String, ^(untyped) -> untyped]
|
|
10
|
+
# @rbs @extension_notifications: Hash[String, ^(untyped) -> void]
|
|
11
|
+
# @rbs @logger: ACP::Transport::_Logger
|
|
7
12
|
|
|
8
13
|
PROTOCOL_VERSION = 1 #: Integer
|
|
9
14
|
OPTIONAL = [
|
|
@@ -27,16 +32,30 @@ class ACP::AgentConnection
|
|
|
27
32
|
# @rbs capabilities: ACP::Types::AgentCapabilities
|
|
28
33
|
# @rbs agent_info: ACP::Types::Implementation?
|
|
29
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]
|
|
37
|
+
# @rbs logger: ACP::Transport::_Logger
|
|
30
38
|
# @rbs &factory: (ACP::AgentConnection::Client) -> ACP::AgentConnection::_Agent
|
|
31
39
|
# @rbs return: void
|
|
32
|
-
def initialize(
|
|
40
|
+
def initialize(
|
|
41
|
+
transport:,
|
|
42
|
+
capabilities:,
|
|
43
|
+
agent_info: nil,
|
|
44
|
+
auth_methods: [],
|
|
45
|
+
extension_requests: {},
|
|
46
|
+
extension_notifications: {},
|
|
47
|
+
logger: ACP::Transport::StderrLogger.new,
|
|
48
|
+
&factory
|
|
49
|
+
)
|
|
33
50
|
@transport = transport
|
|
34
|
-
@
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
@logger = logger
|
|
52
|
+
@capabilities = capabilities
|
|
53
|
+
@agent_info = agent_info
|
|
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) }
|
|
40
59
|
@factory = factory
|
|
41
60
|
end
|
|
42
61
|
|
|
@@ -44,12 +63,18 @@ class ACP::AgentConnection
|
|
|
44
63
|
def start
|
|
45
64
|
client = ACP::AgentConnection::Client.new(peer: @transport)
|
|
46
65
|
agent = @factory.call(client)
|
|
47
|
-
|
|
66
|
+
# Routes are fixed before a client connects, so the check sees the
|
|
67
|
+
# unfiltered auth methods: a terminal-capable client still needs
|
|
68
|
+
# authenticate routed.
|
|
69
|
+
advertised, unadvertised = OPTIONAL.partition { |method| method.advertised?(initialize_response(@auth_methods)) }
|
|
48
70
|
missing = advertised.map(&:agent_method).reject { |name| agent.respond_to?(name) }
|
|
49
71
|
raise ArgumentError, "initialize advertises methods the agent lacks: #{missing.join(', ')}" unless missing.empty?
|
|
50
72
|
|
|
51
73
|
unrouted = unadvertised.map(&:rpc_method) + PER_SESSION.reject { |_, name| agent.respond_to?(name) }.keys
|
|
52
|
-
@transport.start(
|
|
74
|
+
@transport.start(
|
|
75
|
+
requests: requests(agent, client).except(*unrouted).merge(@extension_requests),
|
|
76
|
+
notifications: notifications(agent).merge(@extension_notifications)
|
|
77
|
+
)
|
|
53
78
|
end
|
|
54
79
|
|
|
55
80
|
private
|
|
@@ -82,7 +107,20 @@ class ACP::AgentConnection
|
|
|
82
107
|
# @rbs agent: ACP::AgentConnection::_Agent
|
|
83
108
|
# @rbs return: Hash[String, ^(untyped) -> void]
|
|
84
109
|
def notifications(agent)
|
|
85
|
-
{ 'session/cancel' => ->(params) {
|
|
110
|
+
{ 'session/cancel' => ->(params) { cancel(agent, params) } }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# A notification has no reply to carry a parse failure.
|
|
114
|
+
#
|
|
115
|
+
# @rbs agent: ACP::AgentConnection::_Agent
|
|
116
|
+
# @rbs params: untyped
|
|
117
|
+
# @rbs return: void
|
|
118
|
+
def cancel(agent, params)
|
|
119
|
+
notification = ACP::Types::CancelNotification.from_h(params)
|
|
120
|
+
rescue KeyError, TypeError, NoMethodError => e
|
|
121
|
+
@logger.warn("dropped malformed session/cancel: #{e.class}: #{e.message}")
|
|
122
|
+
else
|
|
123
|
+
agent.cancel(notification)
|
|
86
124
|
end
|
|
87
125
|
|
|
88
126
|
# Generated from_h raises on a missing key or a value of the wrong shape.
|
|
@@ -104,8 +142,34 @@ class ACP::AgentConnection
|
|
|
104
142
|
# @rbs request: ACP::Types::InitializeRequest
|
|
105
143
|
# @rbs return: ACP::Types::InitializeResponse
|
|
106
144
|
def connect(client, request)
|
|
107
|
-
|
|
108
|
-
|
|
145
|
+
capabilities = request.client_capabilities || ACP::Types::ClientCapabilities.new
|
|
146
|
+
client.capabilities = capabilities
|
|
147
|
+
initialize_response(auth_methods(capabilities))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# @rbs auth_methods: Array[ACP::Types::AuthMethod::t]
|
|
151
|
+
# @rbs return: ACP::Types::InitializeResponse
|
|
152
|
+
def initialize_response(auth_methods)
|
|
153
|
+
ACP::Types::InitializeResponse.new(
|
|
154
|
+
protocol_version: PROTOCOL_VERSION,
|
|
155
|
+
agent_capabilities: @capabilities,
|
|
156
|
+
auth_methods:,
|
|
157
|
+
agent_info: @agent_info
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# @rbs capabilities: ACP::Types::ClientCapabilities
|
|
162
|
+
# @rbs return: Array[ACP::Types::AuthMethod::t]
|
|
163
|
+
def auth_methods(capabilities)
|
|
164
|
+
return @auth_methods if capabilities.auth&.terminal
|
|
165
|
+
|
|
166
|
+
@auth_methods.reject { |method| terminal_auth_method?(method) }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# @rbs method: ACP::Types::AuthMethod::t
|
|
170
|
+
# @rbs return: bool
|
|
171
|
+
def terminal_auth_method?(method)
|
|
172
|
+
method.is_a?(ACP::Types::AuthMethod::Terminal) || (method.is_a?(Hash) && method['type'] == 'terminal')
|
|
109
173
|
end
|
|
110
174
|
|
|
111
175
|
# session_created runs after the reply because the client must know the
|
|
@@ -3,29 +3,69 @@
|
|
|
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?
|
|
10
|
+
# @rbs @extension_requests: Hash[String, ^(untyped) -> untyped]
|
|
11
|
+
# @rbs @extension_notifications: Hash[String, ^(untyped) -> void]
|
|
7
12
|
# @rbs @lock: Thread::Mutex
|
|
8
13
|
# @rbs @streams: Hash[String, Thread::Queue]
|
|
14
|
+
# @rbs @pending_permissions: Hash[String, Array[Thread::Queue]]
|
|
15
|
+
# @rbs @logger: ACP::Transport::_Logger
|
|
16
|
+
|
|
17
|
+
PROTOCOL_VERSION = ACP::AgentConnection::PROTOCOL_VERSION #: Integer
|
|
9
18
|
|
|
10
19
|
IGNORE = ->(_notification) {} #: ^(ACP::Types::SessionNotification) -> void
|
|
11
20
|
|
|
21
|
+
CANCELLED = ACP::Types::RequestPermissionResponse.new(
|
|
22
|
+
outcome: ACP::Types::RequestPermissionOutcome::Cancelled.new
|
|
23
|
+
) #: ACP::Types::RequestPermissionResponse
|
|
24
|
+
|
|
12
25
|
# @rbs transport: ACP::AgentConnection::_Transport
|
|
13
26
|
# @rbs permission: ACP::ClientConnection::_PermissionHandler
|
|
27
|
+
# @rbs read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
28
|
+
# @rbs write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
|
14
29
|
# @rbs updates: ACP::ClientConnection::_UpdateHandler
|
|
30
|
+
# @rbs extension_requests: Hash[String, ^(untyped) -> untyped]
|
|
31
|
+
# @rbs extension_notifications: Hash[String, ^(untyped) -> void]
|
|
32
|
+
# @rbs logger: ACP::Transport::_Logger
|
|
15
33
|
# @rbs return: void
|
|
16
|
-
def initialize(
|
|
34
|
+
def initialize(
|
|
35
|
+
transport:,
|
|
36
|
+
permission:,
|
|
37
|
+
read_text_file: nil,
|
|
38
|
+
write_text_file: nil,
|
|
39
|
+
updates: IGNORE,
|
|
40
|
+
extension_requests: {},
|
|
41
|
+
extension_notifications: {},
|
|
42
|
+
logger: ACP::Transport::StderrLogger.new
|
|
43
|
+
)
|
|
17
44
|
@transport = transport
|
|
18
45
|
@permission = permission
|
|
46
|
+
@read_text_file = read_text_file
|
|
47
|
+
@write_text_file = write_text_file
|
|
19
48
|
@updates = updates
|
|
49
|
+
@fs_capabilities = nil
|
|
50
|
+
@extension_requests = extension_requests
|
|
51
|
+
@extension_notifications = extension_notifications
|
|
52
|
+
extension_requests.each_key { |name| ACP::Extensions.validate_name(name) }
|
|
53
|
+
extension_notifications.each_key { |name| ACP::Extensions.validate_name(name) }
|
|
20
54
|
@lock = Mutex.new
|
|
21
55
|
@streams = {}
|
|
56
|
+
@pending_permissions = {}
|
|
57
|
+
@logger = logger
|
|
22
58
|
end
|
|
23
59
|
|
|
24
60
|
# @rbs return: Thread
|
|
25
61
|
def start
|
|
26
62
|
@transport.start(
|
|
27
|
-
requests: {
|
|
28
|
-
|
|
63
|
+
requests: {
|
|
64
|
+
'session/request_permission' => method(:request_permission),
|
|
65
|
+
'fs/read_text_file' => method(:read_text_file),
|
|
66
|
+
'fs/write_text_file' => method(:write_text_file)
|
|
67
|
+
}.merge(@extension_requests),
|
|
68
|
+
notifications: { 'session/update' => method(:dispatch) }.merge(@extension_notifications)
|
|
29
69
|
)
|
|
30
70
|
end
|
|
31
71
|
|
|
@@ -35,7 +75,23 @@ class ACP::ClientConnection
|
|
|
35
75
|
# @rbs request: ACP::Types::InitializeRequest
|
|
36
76
|
# @rbs return: ACP::Types::InitializeResponse | ACP::RequestError
|
|
37
77
|
def connect(request)
|
|
38
|
-
|
|
78
|
+
capabilities = request.client_capabilities&.fs
|
|
79
|
+
unserved = unserved_fs_methods(capabilities)
|
|
80
|
+
unless unserved.empty?
|
|
81
|
+
raise ArgumentError, "initialize advertises fs methods no handler serves: #{unserved.join(', ')}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
@fs_capabilities = capabilities
|
|
85
|
+
parse(ACP::Types::InitializeResponse, @transport.request('initialize', request.to_h)).then do |response|
|
|
86
|
+
next response if response.is_a?(ACP::RequestError)
|
|
87
|
+
|
|
88
|
+
next response if response.protocol_version == PROTOCOL_VERSION
|
|
89
|
+
|
|
90
|
+
ACP::RequestError.unsupported_protocol_version(
|
|
91
|
+
requested: request.protocol_version,
|
|
92
|
+
returned: response.protocol_version
|
|
93
|
+
)
|
|
94
|
+
end
|
|
39
95
|
end
|
|
40
96
|
|
|
41
97
|
# @rbs request: ACP::Types::NewSessionRequest
|
|
@@ -64,10 +120,85 @@ class ACP::ClientConnection
|
|
|
64
120
|
parse(ACP::Types::ListSessionsResponse, @transport.request('session/list', request.to_h))
|
|
65
121
|
end
|
|
66
122
|
|
|
123
|
+
# The agent does not replay history on resume, so unlike session_load there
|
|
124
|
+
# is nothing to stream and no block is taken.
|
|
125
|
+
#
|
|
126
|
+
# @rbs request: ACP::Types::ResumeSessionRequest
|
|
127
|
+
# @rbs return: ACP::Types::ResumeSessionResponse | ACP::RequestError
|
|
128
|
+
def session_resume(request)
|
|
129
|
+
parse(ACP::Types::ResumeSessionResponse, @transport.request('session/resume', request.to_h))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @rbs request: ACP::Types::CloseSessionRequest
|
|
133
|
+
# @rbs return: ACP::Types::CloseSessionResponse | ACP::RequestError
|
|
134
|
+
def session_close(request)
|
|
135
|
+
parse(ACP::Types::CloseSessionResponse, @transport.request('session/close', request.to_h))
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @rbs request: ACP::Types::DeleteSessionRequest
|
|
139
|
+
# @rbs return: ACP::Types::DeleteSessionResponse | ACP::RequestError
|
|
140
|
+
def session_delete(request)
|
|
141
|
+
parse(ACP::Types::DeleteSessionResponse, @transport.request('session/delete', request.to_h))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# The spec requires a pending session/request_permission to be answered with
|
|
145
|
+
# the cancelled outcome once the turn is cancelled, so every queue registered
|
|
146
|
+
# for the session gets one before this returns. The notification goes first
|
|
147
|
+
# so the wire order is cancel, then the cancelled replies.
|
|
148
|
+
#
|
|
67
149
|
# @rbs notification: ACP::Types::CancelNotification
|
|
68
150
|
# @rbs return: void
|
|
69
151
|
def session_cancel(notification)
|
|
70
152
|
@transport.notify('session/cancel', notification.to_h)
|
|
153
|
+
pending = @lock.synchronize { @pending_permissions.delete(notification.session_id) }
|
|
154
|
+
pending&.each { |replies| replies << [:value, CANCELLED] }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# @rbs request: ACP::Types::SetSessionModeRequest
|
|
158
|
+
# @rbs return: ACP::Types::SetSessionModeResponse | ACP::RequestError
|
|
159
|
+
def session_set_mode(request)
|
|
160
|
+
parse(ACP::Types::SetSessionModeResponse, @transport.request('session/set_mode', request.to_h))
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# The reply carries the session's complete option list, so it replaces any
|
|
164
|
+
# state held from an earlier reply rather than merging into it.
|
|
165
|
+
#
|
|
166
|
+
# @rbs request: ACP::Types::SetSessionConfigOptionRequest
|
|
167
|
+
# @rbs return: ACP::Types::SetSessionConfigOptionResponse | ACP::RequestError
|
|
168
|
+
def session_set_config_option(request)
|
|
169
|
+
parse(ACP::Types::SetSessionConfigOptionResponse, @transport.request('session/set_config_option', request.to_h))
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# @rbs request: ACP::Types::AuthenticateRequest
|
|
173
|
+
# @rbs return: ACP::Types::AuthenticateResponse | ACP::RequestError
|
|
174
|
+
def authenticate(request)
|
|
175
|
+
parse(ACP::Types::AuthenticateResponse, @transport.request('authenticate', request.to_h))
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# @rbs request: ACP::Types::LogoutRequest
|
|
179
|
+
# @rbs return: ACP::Types::LogoutResponse | ACP::RequestError
|
|
180
|
+
def logout(request)
|
|
181
|
+
parse(ACP::Types::LogoutResponse, @transport.request('logout', request.to_h))
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Sends an extension request, keyed by its raw `_`-prefixed wire name, and
|
|
185
|
+
# returns the agent's reply as-is: extension methods have no schema to
|
|
186
|
+
# parse the result into.
|
|
187
|
+
#
|
|
188
|
+
# @rbs method: String
|
|
189
|
+
# @rbs params: untyped
|
|
190
|
+
# @rbs return: (Hash[String, untyped] | ACP::RequestError)
|
|
191
|
+
def ext_request(method, params = nil)
|
|
192
|
+
ACP::Extensions.validate_name(method)
|
|
193
|
+
@transport.request(method, params)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# @rbs method: String
|
|
197
|
+
# @rbs params: untyped
|
|
198
|
+
# @rbs return: void
|
|
199
|
+
def ext_notify(method, params = nil)
|
|
200
|
+
ACP::Extensions.validate_name(method)
|
|
201
|
+
@transport.notify(method, params)
|
|
71
202
|
end
|
|
72
203
|
|
|
73
204
|
private
|
|
@@ -94,7 +225,21 @@ class ACP::ClientConnection
|
|
|
94
225
|
# @rbs return: (Hash[String, untyped] | ACP::RequestError)
|
|
95
226
|
def stream(session_id, method, params)
|
|
96
227
|
queue = Thread::Queue.new
|
|
97
|
-
|
|
228
|
+
# The check and the insert share one lock acquisition so a concurrent
|
|
229
|
+
# stream cannot register between them.
|
|
230
|
+
claimed = @lock.synchronize do
|
|
231
|
+
if @streams.key?(session_id)
|
|
232
|
+
false
|
|
233
|
+
else
|
|
234
|
+
@streams[session_id] = queue
|
|
235
|
+
true
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
unless claimed
|
|
239
|
+
return ACP::RequestError.new(
|
|
240
|
+
code: ACP::RequestError::INVALID_REQUEST, message: "A stream is already open for session #{session_id}"
|
|
241
|
+
)
|
|
242
|
+
end
|
|
98
243
|
requester = Thread.new do
|
|
99
244
|
@transport.request(method, params)
|
|
100
245
|
ensure
|
|
@@ -107,16 +252,24 @@ class ACP::ClientConnection
|
|
|
107
252
|
end
|
|
108
253
|
|
|
109
254
|
# Pushes under the lock so a stream cannot close its queue between the
|
|
110
|
-
# lookup and the push.
|
|
255
|
+
# lookup and the push. A malformed notification has no reply to carry the
|
|
256
|
+
# failure.
|
|
111
257
|
#
|
|
112
258
|
# @rbs params: untyped
|
|
113
259
|
# @rbs return: void
|
|
114
260
|
def dispatch(params)
|
|
115
261
|
notification = ACP::Types::SessionNotification.from_h(params)
|
|
262
|
+
rescue KeyError, TypeError, NoMethodError => e
|
|
263
|
+
@logger.warn("dropped malformed session/update: #{e.class}: #{e.message}")
|
|
264
|
+
else
|
|
116
265
|
queue = @lock.synchronize { @streams[notification.session_id]&.push(notification.update) }
|
|
117
266
|
@updates.call(notification) unless queue
|
|
118
267
|
end
|
|
119
268
|
|
|
269
|
+
# The handler runs on its own thread so a cancelled request is answered
|
|
270
|
+
# without waiting for it, and whatever it returns after that is pushed to a
|
|
271
|
+
# queue nobody reads.
|
|
272
|
+
#
|
|
120
273
|
# @rbs params: untyped
|
|
121
274
|
# @rbs return: (ACP::Types::RequestPermissionResponse | ACP::RequestError)
|
|
122
275
|
def request_permission(params)
|
|
@@ -124,6 +277,86 @@ class ACP::ClientConnection
|
|
|
124
277
|
rescue KeyError, TypeError, NoMethodError
|
|
125
278
|
ACP::RequestError.invalid_params
|
|
126
279
|
else
|
|
127
|
-
|
|
280
|
+
replies = Thread::Queue.new
|
|
281
|
+
register_permission(request.session_id, replies)
|
|
282
|
+
Thread.new do
|
|
283
|
+
# A raised StandardError is not a returned ACP::RequestError, so the two
|
|
284
|
+
# need distinct shapes on the queue.
|
|
285
|
+
replies << begin
|
|
286
|
+
[:value, @permission.call(request)]
|
|
287
|
+
rescue StandardError => e
|
|
288
|
+
[:raised, e]
|
|
289
|
+
end
|
|
290
|
+
ensure
|
|
291
|
+
unregister_permission(request.session_id, replies)
|
|
292
|
+
end
|
|
293
|
+
settled = replies.pop
|
|
294
|
+
# Re-raised so the transport's invoke turns it into an error reply, as it
|
|
295
|
+
# would if the handler ran on the serve thread itself.
|
|
296
|
+
raise settled[1] if settled[0] == :raise
|
|
297
|
+
|
|
298
|
+
settled[1]
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# @rbs session_id: String
|
|
302
|
+
# @rbs replies: Thread::Queue
|
|
303
|
+
# @rbs return: void
|
|
304
|
+
def register_permission(session_id, replies)
|
|
305
|
+
@lock.synchronize { (@pending_permissions[session_id] ||= []) << replies }
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# @rbs session_id: String
|
|
309
|
+
# @rbs replies: Thread::Queue
|
|
310
|
+
# @rbs return: void
|
|
311
|
+
def unregister_permission(session_id, replies)
|
|
312
|
+
@lock.synchronize do
|
|
313
|
+
pending = @pending_permissions[session_id]
|
|
314
|
+
if pending
|
|
315
|
+
pending.delete(replies)
|
|
316
|
+
@pending_permissions.delete(session_id) if pending.empty?
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# The routes are registered at start, before connect records the advertised
|
|
322
|
+
# capabilities, so each one answers -32601 until then.
|
|
323
|
+
#
|
|
324
|
+
# @rbs params: untyped
|
|
325
|
+
# @rbs return: (ACP::Types::ReadTextFileResponse | ACP::RequestError)
|
|
326
|
+
def read_text_file(params)
|
|
327
|
+
return ACP::RequestError.unadvertised('fs.readTextFile') unless @fs_capabilities&.read_text_file
|
|
328
|
+
|
|
329
|
+
request = ACP::Types::ReadTextFileRequest.from_h(params)
|
|
330
|
+
rescue KeyError, TypeError, NoMethodError
|
|
331
|
+
ACP::RequestError.invalid_params
|
|
332
|
+
else
|
|
333
|
+
# Safe: connect refuses an advertised capability no handler serves.
|
|
334
|
+
handler = @read_text_file #: ^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError)
|
|
335
|
+
handler.call(request)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# @rbs params: untyped
|
|
339
|
+
# @rbs return: (ACP::Types::WriteTextFileResponse | ACP::RequestError)
|
|
340
|
+
def write_text_file(params)
|
|
341
|
+
return ACP::RequestError.unadvertised('fs.writeTextFile') unless @fs_capabilities&.write_text_file
|
|
342
|
+
|
|
343
|
+
request = ACP::Types::WriteTextFileRequest.from_h(params)
|
|
344
|
+
rescue KeyError, TypeError, NoMethodError
|
|
345
|
+
ACP::RequestError.invalid_params
|
|
346
|
+
else
|
|
347
|
+
# Safe: connect refuses an advertised capability no handler serves.
|
|
348
|
+
handler = @write_text_file #: ^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError)
|
|
349
|
+
handler.call(request)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# The fs capabilities initialize advertises that no injected handler serves.
|
|
353
|
+
#
|
|
354
|
+
# @rbs capabilities: ACP::Types::FileSystemCapabilities?
|
|
355
|
+
# @rbs return: Array[String]
|
|
356
|
+
def unserved_fs_methods(capabilities)
|
|
357
|
+
{
|
|
358
|
+
'fs.readTextFile' => capabilities&.read_text_file && !@read_text_file,
|
|
359
|
+
'fs.writeTextFile' => capabilities&.write_text_file && !@write_text_file
|
|
360
|
+
}.select { |_, unserved| unserved }.keys
|
|
128
361
|
end
|
|
129
362
|
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
|
@@ -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,28 @@ 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
|
+
|
|
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
|
+
|
|
58
80
|
private
|
|
59
81
|
|
|
60
82
|
# @rbs rpc_method: String
|
|
@@ -68,11 +90,4 @@ class ACP::AgentConnection::Client
|
|
|
68
90
|
# @rbs request: ACP::AgentConnection::_Response
|
|
69
91
|
# @rbs return: untyped
|
|
70
92
|
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
93
|
end
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
# Generated from lib/acp/agent_connection.rb with RBS::Inline
|
|
2
2
|
|
|
3
3
|
class ACP::AgentConnection
|
|
4
|
-
@
|
|
4
|
+
@logger: ACP::Transport::_Logger
|
|
5
|
+
|
|
6
|
+
@extension_notifications: Hash[String, ^(untyped) -> void]
|
|
5
7
|
|
|
6
|
-
@
|
|
8
|
+
@extension_requests: Hash[String, ^(untyped) -> untyped]
|
|
7
9
|
|
|
8
10
|
@factory: ^(ACP::AgentConnection::Client) -> ACP::AgentConnection::_Agent
|
|
9
11
|
|
|
12
|
+
@auth_methods: Array[ACP::Types::AuthMethod::t]
|
|
13
|
+
|
|
14
|
+
@agent_info: ACP::Types::Implementation?
|
|
15
|
+
|
|
16
|
+
@capabilities: ACP::Types::AgentCapabilities
|
|
17
|
+
|
|
18
|
+
@transport: ACP::AgentConnection::_Transport
|
|
19
|
+
|
|
10
20
|
PROTOCOL_VERSION: Integer
|
|
11
21
|
|
|
12
22
|
OPTIONAL: Array[ACP::AgentConnection::_OptionalMethod]
|
|
@@ -20,9 +30,12 @@ class ACP::AgentConnection
|
|
|
20
30
|
# @rbs capabilities: ACP::Types::AgentCapabilities
|
|
21
31
|
# @rbs agent_info: ACP::Types::Implementation?
|
|
22
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]
|
|
35
|
+
# @rbs logger: ACP::Transport::_Logger
|
|
23
36
|
# @rbs &factory: (ACP::AgentConnection::Client) -> ACP::AgentConnection::_Agent
|
|
24
37
|
# @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
|
|
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
|
|
26
39
|
|
|
27
40
|
# @rbs return: Thread
|
|
28
41
|
def start: () -> Thread
|
|
@@ -38,6 +51,13 @@ class ACP::AgentConnection
|
|
|
38
51
|
# @rbs return: Hash[String, ^(untyped) -> void]
|
|
39
52
|
def notifications: (ACP::AgentConnection::_Agent agent) -> Hash[String, ^(untyped) -> void]
|
|
40
53
|
|
|
54
|
+
# A notification has no reply to carry a parse failure.
|
|
55
|
+
#
|
|
56
|
+
# @rbs agent: ACP::AgentConnection::_Agent
|
|
57
|
+
# @rbs params: untyped
|
|
58
|
+
# @rbs return: void
|
|
59
|
+
def cancel: (ACP::AgentConnection::_Agent agent, untyped params) -> void
|
|
60
|
+
|
|
41
61
|
# Generated from_h raises on a missing key or a value of the wrong shape.
|
|
42
62
|
#
|
|
43
63
|
# @rbs type: ACP::AgentConnection::_Parser
|
|
@@ -50,6 +70,18 @@ class ACP::AgentConnection
|
|
|
50
70
|
# @rbs return: ACP::Types::InitializeResponse
|
|
51
71
|
def connect: (ACP::AgentConnection::Client client, ACP::Types::InitializeRequest request) -> ACP::Types::InitializeResponse
|
|
52
72
|
|
|
73
|
+
# @rbs auth_methods: Array[ACP::Types::AuthMethod::t]
|
|
74
|
+
# @rbs return: ACP::Types::InitializeResponse
|
|
75
|
+
def initialize_response: (Array[ACP::Types::AuthMethod::t] auth_methods) -> ACP::Types::InitializeResponse
|
|
76
|
+
|
|
77
|
+
# @rbs capabilities: ACP::Types::ClientCapabilities
|
|
78
|
+
# @rbs return: Array[ACP::Types::AuthMethod::t]
|
|
79
|
+
def auth_methods: (ACP::Types::ClientCapabilities capabilities) -> Array[ACP::Types::AuthMethod::t]
|
|
80
|
+
|
|
81
|
+
# @rbs method: ACP::Types::AuthMethod::t
|
|
82
|
+
# @rbs return: bool
|
|
83
|
+
def terminal_auth_method?: (ACP::Types::AuthMethod::t method) -> bool
|
|
84
|
+
|
|
53
85
|
# session_created runs after the reply because the client must know the
|
|
54
86
|
# session id before updates for it (e.g. available commands) arrive.
|
|
55
87
|
#
|
|
@@ -1,23 +1,46 @@
|
|
|
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
|
+
|
|
6
|
+
@pending_permissions: Hash[String, Array[Thread::Queue]]
|
|
7
|
+
|
|
4
8
|
@streams: Hash[String, Thread::Queue]
|
|
5
9
|
|
|
6
10
|
@lock: Thread::Mutex
|
|
7
11
|
|
|
12
|
+
@extension_notifications: Hash[String, ^(untyped) -> void]
|
|
13
|
+
|
|
14
|
+
@extension_requests: Hash[String, ^(untyped) -> untyped]
|
|
15
|
+
|
|
16
|
+
@fs_capabilities: ACP::Types::FileSystemCapabilities?
|
|
17
|
+
|
|
8
18
|
@updates: ACP::ClientConnection::_UpdateHandler
|
|
9
19
|
|
|
20
|
+
@write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
|
21
|
+
|
|
22
|
+
@read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
23
|
+
|
|
10
24
|
@permission: ACP::ClientConnection::_PermissionHandler
|
|
11
25
|
|
|
12
26
|
@transport: ACP::AgentConnection::_Transport
|
|
13
27
|
|
|
28
|
+
PROTOCOL_VERSION: Integer
|
|
29
|
+
|
|
14
30
|
IGNORE: ^(ACP::Types::SessionNotification) -> void
|
|
15
31
|
|
|
32
|
+
CANCELLED: ACP::Types::RequestPermissionResponse
|
|
33
|
+
|
|
16
34
|
# @rbs transport: ACP::AgentConnection::_Transport
|
|
17
35
|
# @rbs permission: ACP::ClientConnection::_PermissionHandler
|
|
36
|
+
# @rbs read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
37
|
+
# @rbs write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
|
18
38
|
# @rbs updates: ACP::ClientConnection::_UpdateHandler
|
|
39
|
+
# @rbs extension_requests: Hash[String, ^(untyped) -> untyped]
|
|
40
|
+
# @rbs extension_notifications: Hash[String, ^(untyped) -> void]
|
|
41
|
+
# @rbs logger: ACP::Transport::_Logger
|
|
19
42
|
# @rbs return: void
|
|
20
|
-
def initialize: (transport: ACP::AgentConnection::_Transport, permission: ACP::ClientConnection::_PermissionHandler, ?updates: ACP::ClientConnection::_UpdateHandler) -> void
|
|
43
|
+
def initialize: (transport: ACP::AgentConnection::_Transport, permission: ACP::ClientConnection::_PermissionHandler, ?read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?, ?write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?, ?updates: ACP::ClientConnection::_UpdateHandler, ?extension_requests: Hash[String, ^(untyped) -> untyped], ?extension_notifications: Hash[String, ^(untyped) -> void], ?logger: ACP::Transport::_Logger) -> void
|
|
21
44
|
|
|
22
45
|
# @rbs return: Thread
|
|
23
46
|
def start: () -> Thread
|
|
@@ -47,10 +70,63 @@ class ACP::ClientConnection
|
|
|
47
70
|
# @rbs return: ACP::Types::ListSessionsResponse | ACP::RequestError
|
|
48
71
|
def session_list: (ACP::Types::ListSessionsRequest request) -> (ACP::Types::ListSessionsResponse | ACP::RequestError)
|
|
49
72
|
|
|
73
|
+
# The agent does not replay history on resume, so unlike session_load there
|
|
74
|
+
# is nothing to stream and no block is taken.
|
|
75
|
+
#
|
|
76
|
+
# @rbs request: ACP::Types::ResumeSessionRequest
|
|
77
|
+
# @rbs return: ACP::Types::ResumeSessionResponse | ACP::RequestError
|
|
78
|
+
def session_resume: (ACP::Types::ResumeSessionRequest request) -> (ACP::Types::ResumeSessionResponse | ACP::RequestError)
|
|
79
|
+
|
|
80
|
+
# @rbs request: ACP::Types::CloseSessionRequest
|
|
81
|
+
# @rbs return: ACP::Types::CloseSessionResponse | ACP::RequestError
|
|
82
|
+
def session_close: (ACP::Types::CloseSessionRequest request) -> (ACP::Types::CloseSessionResponse | ACP::RequestError)
|
|
83
|
+
|
|
84
|
+
# @rbs request: ACP::Types::DeleteSessionRequest
|
|
85
|
+
# @rbs return: ACP::Types::DeleteSessionResponse | ACP::RequestError
|
|
86
|
+
def session_delete: (ACP::Types::DeleteSessionRequest request) -> (ACP::Types::DeleteSessionResponse | ACP::RequestError)
|
|
87
|
+
|
|
88
|
+
# The spec requires a pending session/request_permission to be answered with
|
|
89
|
+
# the cancelled outcome once the turn is cancelled, so every queue registered
|
|
90
|
+
# for the session gets one before this returns. The notification goes first
|
|
91
|
+
# so the wire order is cancel, then the cancelled replies.
|
|
92
|
+
#
|
|
50
93
|
# @rbs notification: ACP::Types::CancelNotification
|
|
51
94
|
# @rbs return: void
|
|
52
95
|
def session_cancel: (ACP::Types::CancelNotification notification) -> void
|
|
53
96
|
|
|
97
|
+
# @rbs request: ACP::Types::SetSessionModeRequest
|
|
98
|
+
# @rbs return: ACP::Types::SetSessionModeResponse | ACP::RequestError
|
|
99
|
+
def session_set_mode: (ACP::Types::SetSessionModeRequest request) -> (ACP::Types::SetSessionModeResponse | ACP::RequestError)
|
|
100
|
+
|
|
101
|
+
# The reply carries the session's complete option list, so it replaces any
|
|
102
|
+
# state held from an earlier reply rather than merging into it.
|
|
103
|
+
#
|
|
104
|
+
# @rbs request: ACP::Types::SetSessionConfigOptionRequest
|
|
105
|
+
# @rbs return: ACP::Types::SetSessionConfigOptionResponse | ACP::RequestError
|
|
106
|
+
def session_set_config_option: (ACP::Types::SetSessionConfigOptionRequest request) -> (ACP::Types::SetSessionConfigOptionResponse | ACP::RequestError)
|
|
107
|
+
|
|
108
|
+
# @rbs request: ACP::Types::AuthenticateRequest
|
|
109
|
+
# @rbs return: ACP::Types::AuthenticateResponse | ACP::RequestError
|
|
110
|
+
def authenticate: (ACP::Types::AuthenticateRequest request) -> (ACP::Types::AuthenticateResponse | ACP::RequestError)
|
|
111
|
+
|
|
112
|
+
# @rbs request: ACP::Types::LogoutRequest
|
|
113
|
+
# @rbs return: ACP::Types::LogoutResponse | ACP::RequestError
|
|
114
|
+
def logout: (ACP::Types::LogoutRequest request) -> (ACP::Types::LogoutResponse | ACP::RequestError)
|
|
115
|
+
|
|
116
|
+
# Sends an extension request, keyed by its raw `_`-prefixed wire name, and
|
|
117
|
+
# returns the agent's reply as-is: extension methods have no schema to
|
|
118
|
+
# parse the result into.
|
|
119
|
+
#
|
|
120
|
+
# @rbs method: String
|
|
121
|
+
# @rbs params: untyped
|
|
122
|
+
# @rbs return: (Hash[String, untyped] | ACP::RequestError)
|
|
123
|
+
def ext_request: (String method, ?untyped params) -> (Hash[String, untyped] | ACP::RequestError)
|
|
124
|
+
|
|
125
|
+
# @rbs method: String
|
|
126
|
+
# @rbs params: untyped
|
|
127
|
+
# @rbs return: void
|
|
128
|
+
def ext_notify: (String method, ?untyped params) -> void
|
|
129
|
+
|
|
54
130
|
private
|
|
55
131
|
|
|
56
132
|
# @rbs type: ACP::AgentConnection::_Parser
|
|
@@ -70,13 +146,45 @@ class ACP::ClientConnection
|
|
|
70
146
|
def stream: (String session_id, String method, Hash[String, untyped] params) { (ACP::Types::SessionUpdate::t) -> void } -> (Hash[String, untyped] | ACP::RequestError)
|
|
71
147
|
|
|
72
148
|
# Pushes under the lock so a stream cannot close its queue between the
|
|
73
|
-
# lookup and the push.
|
|
149
|
+
# lookup and the push. A malformed notification has no reply to carry the
|
|
150
|
+
# failure.
|
|
74
151
|
#
|
|
75
152
|
# @rbs params: untyped
|
|
76
153
|
# @rbs return: void
|
|
77
154
|
def dispatch: (untyped params) -> void
|
|
78
155
|
|
|
156
|
+
# The handler runs on its own thread so a cancelled request is answered
|
|
157
|
+
# without waiting for it, and whatever it returns after that is pushed to a
|
|
158
|
+
# queue nobody reads.
|
|
159
|
+
#
|
|
79
160
|
# @rbs params: untyped
|
|
80
161
|
# @rbs return: (ACP::Types::RequestPermissionResponse | ACP::RequestError)
|
|
81
162
|
def request_permission: (untyped params) -> (ACP::Types::RequestPermissionResponse | ACP::RequestError)
|
|
163
|
+
|
|
164
|
+
# @rbs session_id: String
|
|
165
|
+
# @rbs replies: Thread::Queue
|
|
166
|
+
# @rbs return: void
|
|
167
|
+
def register_permission: (String session_id, Thread::Queue replies) -> void
|
|
168
|
+
|
|
169
|
+
# @rbs session_id: String
|
|
170
|
+
# @rbs replies: Thread::Queue
|
|
171
|
+
# @rbs return: void
|
|
172
|
+
def unregister_permission: (String session_id, Thread::Queue replies) -> void
|
|
173
|
+
|
|
174
|
+
# The routes are registered at start, before connect records the advertised
|
|
175
|
+
# capabilities, so each one answers -32601 until then.
|
|
176
|
+
#
|
|
177
|
+
# @rbs params: untyped
|
|
178
|
+
# @rbs return: (ACP::Types::ReadTextFileResponse | ACP::RequestError)
|
|
179
|
+
def read_text_file: (untyped params) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError)
|
|
180
|
+
|
|
181
|
+
# @rbs params: untyped
|
|
182
|
+
# @rbs return: (ACP::Types::WriteTextFileResponse | ACP::RequestError)
|
|
183
|
+
def write_text_file: (untyped params) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError)
|
|
184
|
+
|
|
185
|
+
# The fs capabilities initialize advertises that no injected handler serves.
|
|
186
|
+
#
|
|
187
|
+
# @rbs capabilities: ACP::Types::FileSystemCapabilities?
|
|
188
|
+
# @rbs return: Array[String]
|
|
189
|
+
def unserved_fs_methods: (ACP::Types::FileSystemCapabilities? capabilities) -> Array[String]
|
|
82
190
|
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
|
|
@@ -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,10 @@ 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
|
|
84
|
+
def ext_request: (String method, ?untyped params) -> (Hash[String, untyped] | ACP::RequestError)
|
|
85
|
+
def ext_notify: (String method, ?untyped params) -> void
|
|
82
86
|
def capabilities: () -> ACP::Types::ClientCapabilities?
|
|
83
87
|
end
|
|
84
88
|
|
|
@@ -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.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jake Bottrall
|
|
@@ -44,10 +44,12 @@ 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
|
|
50
51
|
- lib/acp/transport/reply.rb
|
|
52
|
+
- lib/acp/transport/stderr_logger.rb
|
|
51
53
|
- lib/acp/transport/stdio.rb
|
|
52
54
|
- lib/acp/types.rb
|
|
53
55
|
- lib/acp/types/agent_auth_capabilities.rb
|
|
@@ -258,10 +260,12 @@ files:
|
|
|
258
260
|
- sig/generated/acp/agent_connection/optional_method/resume_session.rbs
|
|
259
261
|
- sig/generated/acp/agent_process.rbs
|
|
260
262
|
- sig/generated/acp/client_connection.rbs
|
|
263
|
+
- sig/generated/acp/extensions.rbs
|
|
261
264
|
- sig/generated/acp/request_error.rbs
|
|
262
265
|
- sig/generated/acp/sdk.rbs
|
|
263
266
|
- sig/generated/acp/transport.rbs
|
|
264
267
|
- sig/generated/acp/transport/reply.rbs
|
|
268
|
+
- sig/generated/acp/transport/stderr_logger.rbs
|
|
265
269
|
- sig/generated/acp/transport/stdio.rbs
|
|
266
270
|
- sig/generated/acp/types.rbs
|
|
267
271
|
- sig/generated/acp/types/agent_auth_capabilities.rbs
|
|
@@ -470,6 +474,8 @@ files:
|
|
|
470
474
|
- sig/manual/acp/agent_connection/optional_method/resume_session.rbs
|
|
471
475
|
- sig/manual/acp/agent_process.rbs
|
|
472
476
|
- sig/manual/acp/client_connection.rbs
|
|
477
|
+
- sig/manual/acp/extensions.rbs
|
|
478
|
+
- sig/manual/acp/transport.rbs
|
|
473
479
|
- sig/manual/acp/transport/stdio.rbs
|
|
474
480
|
homepage: https://github.com/bottrall/acp-sdk
|
|
475
481
|
licenses:
|