acp-sdk 0.1.0 → 0.3.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 +18 -0
- data/README.md +66 -41
- data/lib/acp/agent_connection/client.rb +82 -3
- data/lib/acp/agent_connection/optional_method/authenticate.rb +21 -0
- data/lib/acp/agent_connection/optional_method/close_session.rb +21 -0
- data/lib/acp/agent_connection/optional_method/delete_session.rb +21 -0
- data/lib/acp/agent_connection/optional_method/list_sessions.rb +21 -0
- data/lib/acp/agent_connection/optional_method/load_session.rb +21 -0
- data/lib/acp/agent_connection/optional_method/resume_session.rb +21 -0
- data/lib/acp/agent_connection/optional_method.rb +1 -25
- data/lib/acp/agent_connection.rb +47 -47
- data/lib/acp/agent_process.rb +119 -0
- data/lib/acp/client_connection.rb +13 -15
- data/lib/acp/request_error.rb +81 -0
- data/lib/acp/transport/reply.rb +4 -4
- data/lib/acp/transport/stdio.rb +28 -25
- data/lib/acp/types.rb +4 -0
- data/lib/acp/version.rb +1 -1
- data/sig/generated/acp/agent_connection/client.rbs +51 -2
- data/sig/generated/acp/agent_connection/optional_method/authenticate.rbs +13 -0
- data/sig/generated/acp/agent_connection/optional_method/close_session.rbs +13 -0
- data/sig/generated/acp/agent_connection/optional_method/delete_session.rbs +13 -0
- data/sig/generated/acp/agent_connection/optional_method/list_sessions.rbs +13 -0
- data/sig/generated/acp/agent_connection/optional_method/load_session.rbs +13 -0
- data/sig/generated/acp/agent_connection/optional_method/resume_session.rbs +13 -0
- data/sig/generated/acp/agent_connection/optional_method.rbs +1 -18
- data/sig/generated/acp/agent_connection.rbs +17 -22
- data/sig/generated/acp/agent_process.rbs +72 -0
- data/sig/generated/acp/client_connection.rbs +16 -16
- data/sig/generated/acp/request_error.rbs +65 -0
- data/sig/generated/acp/transport/reply.rbs +5 -5
- data/sig/generated/acp/transport/stdio.rbs +14 -19
- data/sig/generated/acp/types.rbs +4 -0
- data/sig/manual/acp/agent_connection/optional_method/authenticate.rbs +6 -0
- data/sig/manual/acp/agent_connection/optional_method/close_session.rbs +6 -0
- data/sig/manual/acp/agent_connection/optional_method/delete_session.rbs +6 -0
- data/sig/manual/acp/agent_connection/optional_method/list_sessions.rbs +6 -0
- data/sig/manual/acp/agent_connection/optional_method/load_session.rbs +6 -0
- data/sig/manual/acp/agent_connection/optional_method/resume_session.rbs +6 -0
- data/sig/manual/acp/agent_connection.rbs +55 -6
- data/sig/manual/acp/agent_process.rbs +7 -0
- data/sig/manual/acp/client_connection.rbs +1 -1
- data/sig/manual/acp/transport/stdio.rbs +7 -1
- metadata +26 -8
- data/lib/acp/transport/response_error.rb +0 -28
- data/lib/acp/transport/result.rb +0 -35
- data/sig/generated/acp/transport/response_error.rbs +0 -21
- data/sig/generated/acp/transport/result.rbs +0 -25
- data/sig/manual/acp/types.rbs +0 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 59dca9a568751a3b4c29c1ff4afdd9ad12e997689d5eb3a4782f2a4c5ced81b0
|
|
4
|
+
data.tar.gz: 99bde6bbc81d16012a6d9b3654bf15f15a35a40713f3d6dbbe686557f182ac50
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f803ca1e9d3dae3b791cebe930d49c221b2cd3f68df6cc5734bc3e40103af0d101db4447851596c2f6834b855f08a110c24455b06c517aa62e51b95a23cbdfd2
|
|
7
|
+
data.tar.gz: 844f53d23060359a77314f196fdeab023a7ded7c67fdea9f1a75b80bb28db3efb2421977001c13b3ec37f84479f6b9bf816e53a4a8da92c89bc58ee211b88ab8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0](https://github.com/bottrall/acp-sdk/compare/v0.2.0...v0.3.0) (2026-10-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* named protocol error codes ([#52](https://github.com/bottrall/acp-sdk/issues/52)) ([0a81700](https://github.com/bottrall/acp-sdk/commit/0a81700e3fa095279a77e2f161a7d6369c46792f))
|
|
9
|
+
* spawn an agent process for ACP::ClientConnection ([#54](https://github.com/bottrall/acp-sdk/issues/54)) ([e01d2d2](https://github.com/bottrall/acp-sdk/commit/e01d2d280e9cfd08c203f6dccd1ce602979e21c8))
|
|
10
|
+
|
|
11
|
+
## [0.2.0](https://github.com/bottrall/acp-sdk/compare/v0.1.0...v0.2.0) (2026-09-30)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* authenticate on ACP::AgentConnection ([#23](https://github.com/bottrall/acp-sdk/issues/23)) ([aac2bdd](https://github.com/bottrall/acp-sdk/commit/aac2bddbada2108bad3397d8f2c7bcdd617e8e0f))
|
|
17
|
+
* filesystem methods on ACP::AgentConnection::Client ([#20](https://github.com/bottrall/acp-sdk/issues/20)) ([a15d942](https://github.com/bottrall/acp-sdk/commit/a15d942d1fe48cd7042732f64ea6d94cdd191549)), closes [#10](https://github.com/bottrall/acp-sdk/issues/10)
|
|
18
|
+
* optional session methods on ACP::AgentConnection ([#24](https://github.com/bottrall/acp-sdk/issues/24)) ([952f906](https://github.com/bottrall/acp-sdk/commit/952f906e32bf455bac40effeb3d32fa6dea95904))
|
|
19
|
+
* terminal methods on ACP::AgentConnection::Client ([#22](https://github.com/bottrall/acp-sdk/issues/22)) ([b4108b8](https://github.com/bottrall/acp-sdk/commit/b4108b8570a36d78a2c1231c04f7c1f697848318))
|
|
20
|
+
|
|
3
21
|
## 0.1.0 (2026-09-30)
|
|
4
22
|
|
|
5
23
|
|
data/README.md
CHANGED
|
@@ -1,20 +1,43 @@
|
|
|
1
1
|
# acp-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://rubygems.org/gems/acp-sdk)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A Ruby SDK for the [Agent Client Protocol](https://agentclientprotocol.com) (ACP). It covers both sides of the protocol:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- **Agent side** (`ACP::AgentConnection`): expose your agent to ACP clients such as Zed and JetBrains.
|
|
8
|
+
- **Client side** (`ACP::ClientConnection`): drive ACP agents from Ruby, for an editor integration or for tests.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
Every ACP request, response and notification is a generated Ruby class under `ACP::Types`, and messages travel as JSON-RPC over stdio. The SDK speaks ACP protocol version 1.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Requires Ruby 4.0 or later.
|
|
15
|
+
|
|
16
|
+
Add the gem to your Gemfile:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
bundle add acp-sdk
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or install it directly:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
gem install acp-sdk
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then require it:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
require 'acp/sdk'
|
|
32
|
+
```
|
|
12
33
|
|
|
13
34
|
## Serving an agent
|
|
14
35
|
|
|
15
|
-
An agent is a plain Ruby object. `ACP::AgentConnection` answers `initialize` itself, turns each request into its generated `ACP::Types` object, calls the agent, and sends back what it returns. The factory block receives an `ACP::AgentConnection::Client` handle, which the agent keeps for sending session updates
|
|
36
|
+
An agent is a plain Ruby object. `ACP::AgentConnection` answers `initialize` itself, turns each request into its generated `ACP::Types` object, calls the agent, and sends back what it returns. The factory block receives an `ACP::AgentConnection::Client` handle, which the agent keeps for sending session updates, asking permission, reading and writing files, and running commands in terminals.
|
|
16
37
|
|
|
17
38
|
```ruby
|
|
39
|
+
require 'acp/sdk'
|
|
40
|
+
|
|
18
41
|
connection = ACP::AgentConnection.new(
|
|
19
42
|
transport: ACP::Transport::Stdio.new(input: $stdin, output: $stdout),
|
|
20
43
|
capabilities: ACP::Types::AgentCapabilities.new(load_session: true),
|
|
@@ -25,57 +48,59 @@ connection.start.join
|
|
|
25
48
|
|
|
26
49
|
The agent's contract:
|
|
27
50
|
|
|
28
|
-
- It defines `new_session`, `prompt` and `cancel`. `load_session` and `
|
|
29
|
-
- Each request method returns its response type or an `ACP::
|
|
51
|
+
- It defines `new_session`, `prompt` and `cancel`. `load_session`, `list_sessions`, `resume_session`, `close_session` and `delete_session` are required only when `capabilities` advertises `load_session` or `session_capabilities.list`, `.resume`, `.close` or `.delete`, and `authenticate` only when `auth_methods` is non-empty; `start` raises `ArgumentError` when one is advertised but missing, and a method that is not advertised is answered with `-32601`. `change_session_mode` and `change_session_config_option` are optional, since modes and config options are offered per session rather than in `initialize`; `session/set_mode` and `session/set_config_option` are answered with `-32601` when the agent does not define them. `session_created`, if defined, runs right after the `session/new` reply is sent.
|
|
52
|
+
- Each request method returns its response type or an `ACP::RequestError`, which is sent as the error reply. Params that do not match the schema are answered with `-32602` before the agent sees them.
|
|
30
53
|
- `cancel` runs on the transport's reader thread, so it must return quickly: set a flag and let the prompt notice it.
|
|
31
54
|
- After a cancel, the agent must itself end the turn with `stopReason: cancelled`. `ACP::AgentConnection` does not enforce it.
|
|
32
55
|
- `client.capabilities` is `nil` until the client sends `initialize`.
|
|
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
|
+
- `client.create_terminal`, `client.terminal_output`, `client.wait_for_terminal_exit`, `client.kill_terminal` and `client.release_terminal` take the matching `ACP::Types` request (`CreateTerminalRequest`, `TerminalOutputRequest`, `WaitForTerminalExitRequest`, `KillTerminalRequest`, `ReleaseTerminalRequest`) and return its response type or an `ACP::RequestError`. Unless `client.capabilities` advertises `terminal`, they return `-32601` without sending anything to the client. The agent must release every terminal it creates.
|
|
58
|
+
|
|
59
|
+
[`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/list`.
|
|
33
60
|
|
|
34
61
|
## Driving an agent
|
|
35
62
|
|
|
36
|
-
`ACP::ClientConnection` is the other side of the same transport
|
|
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
|
+
|
|
65
|
+
`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.
|
|
37
66
|
|
|
38
67
|
```ruby
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
68
|
+
require 'acp/sdk'
|
|
69
|
+
|
|
70
|
+
response = ACP::AgentProcess.spawn(
|
|
71
|
+
'my-agent', '--flag',
|
|
72
|
+
env: { 'API_KEY' => '...' },
|
|
73
|
+
cwd: Dir.pwd,
|
|
74
|
+
stderr: :capture,
|
|
42
75
|
permission: ->(request) { ask_the_user(request) },
|
|
43
76
|
updates: ->(notification) { show(notification) }
|
|
44
|
-
)
|
|
45
|
-
connection.
|
|
46
|
-
connection.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
77
|
+
) do |connection|
|
|
78
|
+
connection.connect(ACP::Types::InitializeRequest.new(protocol_version: 1))
|
|
79
|
+
session = connection.session_new(ACP::Types::NewSessionRequest.new(cwd: Dir.pwd, mcp_servers: []))
|
|
80
|
+
prompt = [ACP::Types::ContentBlock::Text.new(text: 'hello')]
|
|
81
|
+
connection.session_prompt(ACP::Types::PromptRequest.new(session_id: session.session_id, prompt:)) do |update|
|
|
82
|
+
print update.content.text if update.is_a?(ACP::Types::SessionUpdate::AgentMessageChunk)
|
|
83
|
+
end
|
|
51
84
|
end
|
|
52
85
|
```
|
|
53
86
|
|
|
54
|
-
|
|
55
|
-
- `permission` answers `session/request_permission` with an `ACP::Types::RequestPermissionResponse` or an `ACP::Transport::ResponseError`. It runs on its own thread, so it may block while the user decides, and `session_cancel` can be sent meanwhile.
|
|
87
|
+
Any transport over a pair of IOs works too, so the connection can still be wired by hand when the agent is not a child process:
|
|
56
88
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
| `bin/test` | Run the test suite. Pass files and/or Minitest flags: `bin/test test/foo_test.rb -n /pattern/` |
|
|
65
|
-
| `bin/lint` | Run RuboCop. Arguments are forwarded, e.g. `bin/lint -a` |
|
|
66
|
-
| `bin/typecheck` | Check `lib/acp/types`, the rbs collection lockfile and `sig/generated` are current, then type-check with Steep |
|
|
67
|
-
| `bin/rbs` | Regenerate `sig/generated` from the inline annotations in `lib/` |
|
|
68
|
-
| `bin/types` | Regenerate `lib/acp/types` from `schema/schema.json`, then `sig/generated` |
|
|
69
|
-
| `bin/rbs-watch` | Regenerate `sig/generated` whenever `lib/` changes |
|
|
70
|
-
| `bin/ci` | Run everything CI runs, serially. Use before pushing |
|
|
71
|
-
| `bin/build` | Build the gem into `pkg/`; the publish workflow runs this before `gem push` |
|
|
89
|
+
```ruby
|
|
90
|
+
connection = ACP::ClientConnection.new(
|
|
91
|
+
transport: ACP::Transport::Stdio.new(input: some_io, output: other_io),
|
|
92
|
+
permission: ->(request) { ask_the_user(request) },
|
|
93
|
+
updates: ->(notification) { show(notification) }
|
|
94
|
+
)
|
|
95
|
+
```
|
|
72
96
|
|
|
73
|
-
|
|
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.
|
|
98
|
+
- `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.
|
|
74
99
|
|
|
75
|
-
|
|
100
|
+
## Contributing
|
|
76
101
|
|
|
77
|
-
[
|
|
102
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/bottrall/acp-sdk). See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and workflow. Everyone interacting in the project is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
|
|
78
103
|
|
|
79
|
-
##
|
|
104
|
+
## License
|
|
80
105
|
|
|
81
|
-
|
|
106
|
+
Released under the [MIT License](LICENSE.txt).
|
|
@@ -31,9 +31,88 @@ class ACP::AgentConnection::Client
|
|
|
31
31
|
# closes.
|
|
32
32
|
#
|
|
33
33
|
# @rbs request: ACP::Types::RequestPermissionRequest
|
|
34
|
-
# @rbs return: ACP::Types::RequestPermissionResponse | ACP::
|
|
34
|
+
# @rbs return: ACP::Types::RequestPermissionResponse | ACP::RequestError
|
|
35
35
|
def request_permission(request)
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
call('session/request_permission', ACP::Types::RequestPermissionResponse, request)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @rbs request: ACP::Types::ReadTextFileRequest
|
|
40
|
+
# @rbs return: ACP::Types::ReadTextFileResponse | ACP::RequestError
|
|
41
|
+
def read_text_file(request)
|
|
42
|
+
return unadvertised('fs.readTextFile') unless capabilities&.fs&.read_text_file
|
|
43
|
+
|
|
44
|
+
call('fs/read_text_file', ACP::Types::ReadTextFileResponse, request)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @rbs request: ACP::Types::WriteTextFileRequest
|
|
48
|
+
# @rbs return: ACP::Types::WriteTextFileResponse | ACP::RequestError
|
|
49
|
+
def write_text_file(request)
|
|
50
|
+
return unadvertised('fs.writeTextFile') unless capabilities&.fs&.write_text_file
|
|
51
|
+
|
|
52
|
+
call('fs/write_text_file', ACP::Types::WriteTextFileResponse, request)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @rbs request: ACP::Types::CreateTerminalRequest
|
|
56
|
+
# @rbs return: ACP::Types::CreateTerminalResponse | ACP::RequestError
|
|
57
|
+
def create_terminal(request)
|
|
58
|
+
terminal_call('terminal/create', ACP::Types::CreateTerminalResponse, request)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @rbs request: ACP::Types::TerminalOutputRequest
|
|
62
|
+
# @rbs return: ACP::Types::TerminalOutputResponse | ACP::RequestError
|
|
63
|
+
def terminal_output(request)
|
|
64
|
+
terminal_call('terminal/output', ACP::Types::TerminalOutputResponse, request)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @rbs request: ACP::Types::WaitForTerminalExitRequest
|
|
68
|
+
# @rbs return: ACP::Types::WaitForTerminalExitResponse | ACP::RequestError
|
|
69
|
+
def wait_for_terminal_exit(request)
|
|
70
|
+
terminal_call('terminal/wait_for_exit', ACP::Types::WaitForTerminalExitResponse, request)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @rbs request: ACP::Types::KillTerminalRequest
|
|
74
|
+
# @rbs return: ACP::Types::KillTerminalResponse | ACP::RequestError
|
|
75
|
+
def kill_terminal(request)
|
|
76
|
+
terminal_call('terminal/kill', ACP::Types::KillTerminalResponse, request)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @rbs request: ACP::Types::ReleaseTerminalRequest
|
|
80
|
+
# @rbs return: ACP::Types::ReleaseTerminalResponse | ACP::RequestError
|
|
81
|
+
def release_terminal(request)
|
|
82
|
+
terminal_call('terminal/release', ACP::Types::ReleaseTerminalResponse, request)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# @rbs rpc_method: String
|
|
88
|
+
# @rbs type: ACP::AgentConnection::_Parser
|
|
89
|
+
# @rbs request: ACP::AgentConnection::_Response
|
|
90
|
+
# @rbs return: untyped
|
|
91
|
+
def call(rpc_method, type, request)
|
|
92
|
+
result = @peer.request(rpc_method, request.to_h)
|
|
93
|
+
return result if result.is_a?(ACP::RequestError)
|
|
94
|
+
|
|
95
|
+
type.from_h(result)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# @rbs rpc_method: String
|
|
99
|
+
# @rbs type: ACP::AgentConnection::_Parser
|
|
100
|
+
# @rbs request: ACP::AgentConnection::_Response
|
|
101
|
+
# @rbs return: untyped
|
|
102
|
+
def terminal_call(rpc_method, type, request)
|
|
103
|
+
return unadvertised('terminal') unless capabilities&.terminal
|
|
104
|
+
|
|
105
|
+
call(rpc_method, type, request)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Same method-not-found code a peer would send, but with a message that says
|
|
109
|
+
# the refusal was local, so logs can tell the two cases apart.
|
|
110
|
+
#
|
|
111
|
+
# @rbs capability: String
|
|
112
|
+
# @rbs return: ACP::RequestError
|
|
113
|
+
def unadvertised(capability)
|
|
114
|
+
ACP::RequestError.new(
|
|
115
|
+
code: ACP::RequestError::METHOD_NOT_FOUND, message: "Client does not advertise #{capability}"
|
|
116
|
+
)
|
|
38
117
|
end
|
|
39
118
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ACP::AgentConnection::OptionalMethod::Authenticate
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
# @rbs return: String
|
|
7
|
+
def rpc_method
|
|
8
|
+
'authenticate'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @rbs return: Symbol
|
|
12
|
+
def agent_method
|
|
13
|
+
:authenticate
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @rbs initialize_response: ACP::Types::InitializeResponse
|
|
17
|
+
# @rbs return: bool
|
|
18
|
+
def advertised?(initialize_response)
|
|
19
|
+
initialize_response.auth_methods&.any? || false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ACP::AgentConnection::OptionalMethod::CloseSession
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
# @rbs return: String
|
|
7
|
+
def rpc_method
|
|
8
|
+
'session/close'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @rbs return: Symbol
|
|
12
|
+
def agent_method
|
|
13
|
+
:close_session
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @rbs initialize_response: ACP::Types::InitializeResponse
|
|
17
|
+
# @rbs return: bool
|
|
18
|
+
def advertised?(initialize_response)
|
|
19
|
+
initialize_response.agent_capabilities&.session_capabilities&.close ? true : false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ACP::AgentConnection::OptionalMethod::DeleteSession
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
# @rbs return: String
|
|
7
|
+
def rpc_method
|
|
8
|
+
'session/delete'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @rbs return: Symbol
|
|
12
|
+
def agent_method
|
|
13
|
+
:delete_session
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @rbs initialize_response: ACP::Types::InitializeResponse
|
|
17
|
+
# @rbs return: bool
|
|
18
|
+
def advertised?(initialize_response)
|
|
19
|
+
initialize_response.agent_capabilities&.session_capabilities&.delete ? true : false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ACP::AgentConnection::OptionalMethod::ListSessions
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
# @rbs return: String
|
|
7
|
+
def rpc_method
|
|
8
|
+
'session/list'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @rbs return: Symbol
|
|
12
|
+
def agent_method
|
|
13
|
+
:list_sessions
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @rbs initialize_response: ACP::Types::InitializeResponse
|
|
17
|
+
# @rbs return: bool
|
|
18
|
+
def advertised?(initialize_response)
|
|
19
|
+
initialize_response.agent_capabilities&.session_capabilities&.list ? true : false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ACP::AgentConnection::OptionalMethod::LoadSession
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
# @rbs return: String
|
|
7
|
+
def rpc_method
|
|
8
|
+
'session/load'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @rbs return: Symbol
|
|
12
|
+
def agent_method
|
|
13
|
+
:load_session
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @rbs initialize_response: ACP::Types::InitializeResponse
|
|
17
|
+
# @rbs return: bool
|
|
18
|
+
def advertised?(initialize_response)
|
|
19
|
+
initialize_response.agent_capabilities&.load_session ? true : false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ACP::AgentConnection::OptionalMethod::ResumeSession
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
# @rbs return: String
|
|
7
|
+
def rpc_method
|
|
8
|
+
'session/resume'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @rbs return: Symbol
|
|
12
|
+
def agent_method
|
|
13
|
+
:resume_session
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @rbs initialize_response: ACP::Types::InitializeResponse
|
|
17
|
+
# @rbs return: bool
|
|
18
|
+
def advertised?(initialize_response)
|
|
19
|
+
initialize_response.agent_capabilities&.session_capabilities&.resume ? true : false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -1,28 +1,4 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
# @rbs @capability: ^(ACP::Types::AgentCapabilities) -> boolish
|
|
5
|
-
|
|
6
|
-
# @dynamic rpc_method
|
|
7
|
-
attr_reader :rpc_method #: String
|
|
8
|
-
|
|
9
|
-
# @dynamic agent_method
|
|
10
|
-
attr_reader :agent_method #: Symbol
|
|
11
|
-
|
|
12
|
-
# @rbs rpc_method: String
|
|
13
|
-
# @rbs agent_method: Symbol
|
|
14
|
-
# @rbs &capability: (ACP::Types::AgentCapabilities) -> boolish
|
|
15
|
-
# @rbs return: void
|
|
16
|
-
def initialize(rpc_method:, agent_method:, &capability)
|
|
17
|
-
@rpc_method = rpc_method
|
|
18
|
-
@agent_method = agent_method
|
|
19
|
-
@capability = capability
|
|
20
|
-
freeze
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# @rbs capabilities: ACP::Types::AgentCapabilities
|
|
24
|
-
# @rbs return: bool
|
|
25
|
-
def advertised?(capabilities)
|
|
26
|
-
@capability.call(capabilities) ? true : false
|
|
27
|
-
end
|
|
3
|
+
module ACP::AgentConnection::OptionalMethod
|
|
28
4
|
end
|
data/lib/acp/agent_connection.rb
CHANGED
|
@@ -2,19 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
class ACP::AgentConnection
|
|
4
4
|
# @rbs @transport: ACP::AgentConnection::_Transport
|
|
5
|
-
# @rbs @
|
|
6
|
-
# @rbs @agent_info: ACP::Types::Implementation?
|
|
7
|
-
# @rbs @auth_methods: Array[ACP::Types::AuthMethod::t]
|
|
5
|
+
# @rbs @initialize_response: ACP::Types::InitializeResponse
|
|
8
6
|
# @rbs @factory: ^(ACP::AgentConnection::Client) -> ACP::AgentConnection::_Agent
|
|
9
7
|
|
|
10
8
|
PROTOCOL_VERSION = 1 #: Integer
|
|
11
|
-
INVALID_PARAMS = ACP::Transport::ResponseError.new(code: -32_602, message: 'Invalid params') #: ACP::Transport::ResponseError
|
|
12
9
|
OPTIONAL = [
|
|
13
|
-
ACP::AgentConnection::OptionalMethod
|
|
14
|
-
ACP::AgentConnection::OptionalMethod
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
ACP::AgentConnection::OptionalMethod::LoadSession,
|
|
11
|
+
ACP::AgentConnection::OptionalMethod::ListSessions,
|
|
12
|
+
ACP::AgentConnection::OptionalMethod::Authenticate,
|
|
13
|
+
ACP::AgentConnection::OptionalMethod::ResumeSession,
|
|
14
|
+
ACP::AgentConnection::OptionalMethod::CloseSession,
|
|
15
|
+
ACP::AgentConnection::OptionalMethod::DeleteSession
|
|
16
|
+
].freeze #: Array[ACP::AgentConnection::_OptionalMethod]
|
|
17
|
+
# The schema has no initialize capability for modes or config options: an
|
|
18
|
+
# agent offers them per session, in its session responses, so they are
|
|
19
|
+
# routed whenever the agent defines them.
|
|
20
|
+
PER_SESSION = {
|
|
21
|
+
'session/set_mode' => :change_session_mode,
|
|
22
|
+
'session/set_config_option' => :change_session_config_option
|
|
23
|
+
}.freeze #: Hash[String, Symbol]
|
|
18
24
|
|
|
19
25
|
# @rbs transport: ACP::AgentConnection::_Transport
|
|
20
26
|
# @rbs capabilities: ACP::Types::AgentCapabilities
|
|
@@ -24,9 +30,12 @@ class ACP::AgentConnection
|
|
|
24
30
|
# @rbs return: void
|
|
25
31
|
def initialize(transport:, capabilities:, agent_info: nil, auth_methods: [], &factory)
|
|
26
32
|
@transport = transport
|
|
27
|
-
@
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
@initialize_response = ACP::Types::InitializeResponse.new(
|
|
34
|
+
protocol_version: PROTOCOL_VERSION,
|
|
35
|
+
agent_capabilities: capabilities,
|
|
36
|
+
auth_methods:,
|
|
37
|
+
agent_info:
|
|
38
|
+
)
|
|
30
39
|
@factory = factory
|
|
31
40
|
end
|
|
32
41
|
|
|
@@ -34,11 +43,11 @@ class ACP::AgentConnection
|
|
|
34
43
|
def start
|
|
35
44
|
client = ACP::AgentConnection::Client.new(peer: @transport)
|
|
36
45
|
agent = @factory.call(client)
|
|
37
|
-
advertised, unadvertised = OPTIONAL.partition { |method| method.advertised?(@
|
|
46
|
+
advertised, unadvertised = OPTIONAL.partition { |method| method.advertised?(@initialize_response) }
|
|
38
47
|
missing = advertised.map(&:agent_method).reject { |name| agent.respond_to?(name) }
|
|
39
|
-
raise ArgumentError, "
|
|
48
|
+
raise ArgumentError, "initialize advertises methods the agent lacks: #{missing.join(', ')}" unless missing.empty?
|
|
40
49
|
|
|
41
|
-
unrouted = unadvertised.map(&:rpc_method)
|
|
50
|
+
unrouted = unadvertised.map(&:rpc_method) + PER_SESSION.reject { |_, name| agent.respond_to?(name) }.keys
|
|
42
51
|
@transport.start(requests: requests(agent, client).except(*unrouted), notifications: notifications(agent))
|
|
43
52
|
end
|
|
44
53
|
|
|
@@ -46,17 +55,25 @@ class ACP::AgentConnection
|
|
|
46
55
|
|
|
47
56
|
# @rbs agent: ACP::AgentConnection::_Agent
|
|
48
57
|
# @rbs client: ACP::AgentConnection::Client
|
|
49
|
-
# @rbs return: Hash[String, ^(untyped) -> (ACP::
|
|
58
|
+
# @rbs return: Hash[String, ^(untyped) -> (ACP::AgentConnection::_Response | ACP::RequestError | ACP::Transport::Reply)]
|
|
50
59
|
def requests(agent, client)
|
|
51
|
-
# Safe: start drops the
|
|
52
|
-
#
|
|
53
|
-
full = agent #: ACP::AgentConnection::
|
|
60
|
+
# Safe: start drops the optional handlers initialize does not advertise
|
|
61
|
+
# or the agent does not define, and checks the agent defines the rest.
|
|
62
|
+
full = agent #: ACP::AgentConnection::_FullAgent
|
|
54
63
|
{
|
|
55
64
|
'initialize' => route(ACP::Types::InitializeRequest) { |request| connect(client, request) },
|
|
65
|
+
'authenticate' => route(ACP::Types::AuthenticateRequest) { |request| full.authenticate(request) },
|
|
56
66
|
'session/new' => route(ACP::Types::NewSessionRequest) { |request| new_session(agent, request) },
|
|
57
|
-
'session/prompt' => route(ACP::Types::PromptRequest) { |request|
|
|
58
|
-
'session/load' => route(ACP::Types::LoadSessionRequest) { |request|
|
|
59
|
-
'session/list' => route(ACP::Types::ListSessionsRequest) { |request|
|
|
67
|
+
'session/prompt' => route(ACP::Types::PromptRequest) { |request| agent.prompt(request) },
|
|
68
|
+
'session/load' => route(ACP::Types::LoadSessionRequest) { |request| full.load_session(request) },
|
|
69
|
+
'session/list' => route(ACP::Types::ListSessionsRequest) { |request| full.list_sessions(request) },
|
|
70
|
+
'session/resume' => route(ACP::Types::ResumeSessionRequest) { |request| full.resume_session(request) },
|
|
71
|
+
'session/close' => route(ACP::Types::CloseSessionRequest) { |request| full.close_session(request) },
|
|
72
|
+
'session/delete' => route(ACP::Types::DeleteSessionRequest) { |request| full.delete_session(request) },
|
|
73
|
+
'session/set_mode' => route(ACP::Types::SetSessionModeRequest) { |request| full.change_session_mode(request) },
|
|
74
|
+
'session/set_config_option' => route(ACP::Types::SetSessionConfigOptionRequest) do |request|
|
|
75
|
+
full.change_session_config_option(request)
|
|
76
|
+
end
|
|
60
77
|
}
|
|
61
78
|
end
|
|
62
79
|
|
|
@@ -69,40 +86,24 @@ class ACP::AgentConnection
|
|
|
69
86
|
# Generated from_h raises on a missing key or a value of the wrong shape.
|
|
70
87
|
#
|
|
71
88
|
# @rbs type: ACP::AgentConnection::_Parser
|
|
72
|
-
# @rbs &handle: (untyped) -> (ACP::
|
|
73
|
-
# @rbs return: ^(untyped) -> (ACP::
|
|
89
|
+
# @rbs &handle: (untyped) -> (ACP::AgentConnection::_Response | ACP::RequestError | ACP::Transport::Reply)
|
|
90
|
+
# @rbs return: ^(untyped) -> (ACP::AgentConnection::_Response | ACP::RequestError | ACP::Transport::Reply)
|
|
74
91
|
def route(type, &)
|
|
75
92
|
lambda do |params|
|
|
76
93
|
request = type.from_h(params)
|
|
77
94
|
rescue KeyError, TypeError, NoMethodError
|
|
78
|
-
ACP::
|
|
95
|
+
ACP::RequestError.invalid_params
|
|
79
96
|
else
|
|
80
97
|
yield(request)
|
|
81
98
|
end
|
|
82
99
|
end
|
|
83
100
|
|
|
84
|
-
# @rbs response: ACP::AgentConnection::_Response | ACP::Transport::ResponseError
|
|
85
|
-
# @rbs return: ACP::Transport::Result
|
|
86
|
-
def respond(response)
|
|
87
|
-
case response
|
|
88
|
-
when ACP::Transport::ResponseError then ACP::Transport::Result.error(response)
|
|
89
|
-
else ACP::Transport::Result.ok(response.to_h)
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
101
|
# @rbs client: ACP::AgentConnection::Client
|
|
94
102
|
# @rbs request: ACP::Types::InitializeRequest
|
|
95
|
-
# @rbs return: ACP::
|
|
103
|
+
# @rbs return: ACP::Types::InitializeResponse
|
|
96
104
|
def connect(client, request)
|
|
97
105
|
client.capabilities = request.client_capabilities || ACP::Types::ClientCapabilities.new
|
|
98
|
-
|
|
99
|
-
ACP::Types::InitializeResponse.new(
|
|
100
|
-
protocol_version: PROTOCOL_VERSION,
|
|
101
|
-
agent_capabilities: @capabilities,
|
|
102
|
-
auth_methods: @auth_methods,
|
|
103
|
-
agent_info: @agent_info
|
|
104
|
-
)
|
|
105
|
-
)
|
|
106
|
+
@initialize_response
|
|
106
107
|
end
|
|
107
108
|
|
|
108
109
|
# session_created runs after the reply because the client must know the
|
|
@@ -110,13 +111,12 @@ class ACP::AgentConnection
|
|
|
110
111
|
#
|
|
111
112
|
# @rbs agent: ACP::AgentConnection::_Agent
|
|
112
113
|
# @rbs request: ACP::Types::NewSessionRequest
|
|
113
|
-
# @rbs return: ACP::
|
|
114
|
+
# @rbs return: (ACP::Types::NewSessionResponse | ACP::RequestError | ACP::Transport::Reply)
|
|
114
115
|
def new_session(agent, request)
|
|
115
116
|
response = agent.new_session(request)
|
|
116
|
-
|
|
117
|
-
return result unless response.is_a?(ACP::Types::NewSessionResponse) && agent.respond_to?(:session_created)
|
|
117
|
+
return response unless response.is_a?(ACP::Types::NewSessionResponse) && agent.respond_to?(:session_created)
|
|
118
118
|
|
|
119
119
|
hook = agent #: ACP::AgentConnection::_Agent & ACP::AgentConnection::_SessionCreated
|
|
120
|
-
ACP::Transport::Reply.new(
|
|
120
|
+
ACP::Transport::Reply.new(response, after: -> { hook.session_created(response) })
|
|
121
121
|
end
|
|
122
122
|
end
|