acp-sdk 0.13.0 → 0.14.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 +16 -0
- data/README.md +141 -69
- data/lib/acp/agent_connection/client.rb +2 -3
- data/lib/acp/agent_connection/optional_route.rb +45 -0
- data/lib/acp/agent_connection.rb +120 -76
- data/lib/acp/agent_process.rb +4 -4
- data/lib/acp/client_connection/elicitations.rb +44 -0
- data/lib/acp/client_connection/inbound/route.rb +29 -0
- data/lib/acp/client_connection/inbound.rb +232 -0
- data/lib/acp/client_connection/streams.rb +20 -52
- data/lib/acp/client_connection.rb +34 -309
- data/lib/acp/extensions.rb +6 -0
- data/lib/acp/protocol.rb +14 -4
- data/lib/acp/transport/reply.rb +2 -2
- data/lib/acp/transport/stdio.rb +2 -2
- data/lib/acp/types/parse_error.rb +1 -1
- data/lib/acp/types.rb +13 -0
- data/lib/acp/version.rb +1 -1
- data/sig/generated/acp/agent_connection/optional_route.rbs +35 -0
- data/sig/generated/acp/agent_connection.rbs +5 -21
- data/sig/generated/acp/client_connection/elicitations.rbs +29 -0
- data/sig/generated/acp/client_connection/inbound/route.rbs +26 -0
- data/sig/generated/acp/client_connection/inbound.rbs +90 -0
- data/sig/generated/acp/client_connection/streams.rbs +7 -28
- data/sig/generated/acp/client_connection.rbs +12 -136
- data/sig/generated/acp/extensions.rbs +4 -0
- data/sig/generated/acp/protocol.rbs +8 -0
- data/sig/generated/acp/transport/reply.rbs +3 -3
- data/sig/generated/acp/transport/stdio.rbs +4 -4
- data/sig/generated/acp/types.rbs +9 -0
- data/sig/manual/acp/agent_connection.rbs +5 -57
- data/sig/manual/acp/client_connection.rbs +27 -0
- data/sig/manual/acp/transport/stdio.rbs +1 -5
- metadata +9 -5
- data/lib/acp/agent_connection/optional_method/spec.rb +0 -42
- data/lib/acp/agent_connection/optional_method.rb +0 -40
- data/sig/generated/acp/agent_connection/optional_method/spec.rbs +0 -32
- data/sig/generated/acp/agent_connection/optional_method.rbs +0 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: baf3230fbed41f9fac37c739b21bfe649fb83566b36d22930fb410a5904fa059
|
|
4
|
+
data.tar.gz: 4a1ff54c9f5d5af5534d37fcd2b2886a0624a8ee582e00392e0952634c3cda56
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4b8547d48ebfdf296f218e1a2c7f1bb9d727bf86b5f02de6b7b2e5230a175d408783dafcde63677c800344bed52003c2ecc58b830a134902daaa600341b9875a
|
|
7
|
+
data.tar.gz: b85b63b6ccccfaab69409933376f7d5e691c0e7619227f92861c417f7fd2517b172a271224225ee40eb1f88c9cba3f55e4dd0c1685e8c9a9663bd1432daec0c3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.14.0](https://github.com/bottrall/acp-sdk/compare/v0.13.0...v0.14.0) (2026-10-24)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* build a client with ACP::ClientConnection.build instead of ClientConnection.new, which now takes only its collaborators (transport:, streams:, inbound:). The seven fs and terminal handler keywords become two handler objects, fs: and terminal:, and the elicitation: and complete_elicitation: callables become one elicitation: object responding to create_elicitation and optionally complete_elicitation. The unserved-capability ArgumentError from connect now names handler methods (write_text_file) rather than capability keys (fs.writeTextFile).
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* split ClientConnection into outbound, inbound and elicitations ([#159](https://github.com/bottrall/acp-sdk/issues/159)) ([3bdd4b1](https://github.com/bottrall/acp-sdk/commit/3bdd4b17f5f3b8aaf95c48d0e39b68710ab04cce))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* reject non-object payloads through one shared parse ([#156](https://github.com/bottrall/acp-sdk/issues/156)) ([930ea26](https://github.com/bottrall/acp-sdk/commit/930ea26d12c67bf72d6638f836eb790c91f05642))
|
|
18
|
+
|
|
3
19
|
## [0.13.0](https://github.com/bottrall/acp-sdk/compare/v0.12.0...v0.13.0) (2026-10-13)
|
|
4
20
|
|
|
5
21
|
|
data/README.md
CHANGED
|
@@ -1,90 +1,117 @@
|
|
|
1
1
|
# acp-sdk
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/acp-sdk)
|
|
4
|
+
[](https://github.com/bottrall/acp-sdk/actions/workflows/ci.yml)
|
|
4
5
|
|
|
5
|
-
A Ruby SDK for the [Agent Client Protocol](https://agentclientprotocol.com) (ACP)
|
|
6
|
+
A Ruby SDK for the [Agent Client Protocol](https://agentclientprotocol.com) (ACP), the protocol editors such as Zed and JetBrains use to talk to coding agents. Use it to:
|
|
6
7
|
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
8
|
+
- **Build an agent** that any ACP client can run.
|
|
9
|
+
- **Drive an agent** from Ruby, for an editor integration, a script or a test suite.
|
|
9
10
|
|
|
10
|
-
Every ACP
|
|
11
|
+
Every ACP message is a typed Ruby object, and the SDK takes care of the wire format, capability negotiation and cancellation. It speaks ACP protocol version 1.
|
|
11
12
|
|
|
12
13
|
## Installation
|
|
13
14
|
|
|
14
|
-
Requires Ruby 3.4 or later
|
|
15
|
-
|
|
16
|
-
Add the gem to your Gemfile:
|
|
15
|
+
Requires Ruby 3.4 or later.
|
|
17
16
|
|
|
18
17
|
```sh
|
|
19
18
|
bundle add acp-sdk
|
|
20
19
|
```
|
|
21
20
|
|
|
22
|
-
Or install it directly:
|
|
23
|
-
|
|
24
|
-
```sh
|
|
25
|
-
gem install acp-sdk
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
Then require it:
|
|
29
|
-
|
|
30
21
|
```ruby
|
|
31
22
|
require 'acp/sdk'
|
|
32
23
|
```
|
|
33
24
|
|
|
34
|
-
##
|
|
25
|
+
## Building an agent
|
|
35
26
|
|
|
36
|
-
An agent is a plain Ruby object. `ACP::AgentConnection`
|
|
27
|
+
An agent is a plain Ruby object. `ACP::AgentConnection` calls its methods with typed requests and sends back what they return. The block receives a `client` handle for talking back to the editor.
|
|
37
28
|
|
|
38
29
|
```ruby
|
|
39
30
|
require 'acp/sdk'
|
|
31
|
+
require 'securerandom'
|
|
32
|
+
|
|
33
|
+
class MyAgent
|
|
34
|
+
def initialize(client:)
|
|
35
|
+
@client = client
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def new_session(_request)
|
|
39
|
+
ACP::Types::NewSessionResponse.new(session_id: SecureRandom.uuid)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def prompt(request)
|
|
43
|
+
reply = ACP::Types::ContentBlock::Text.new(text: 'Hello from Ruby!')
|
|
44
|
+
@client.update(request.session_id, ACP::Types::SessionUpdate::AgentMessageChunk.new(content: reply))
|
|
45
|
+
ACP::Types::PromptResponse.new(stop_reason: ACP::Types::StopReason::END_TURN)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def cancel(_notification); end
|
|
49
|
+
end
|
|
40
50
|
|
|
41
|
-
|
|
51
|
+
ACP::AgentConnection.new(
|
|
42
52
|
transport: ACP::Transport::Stdio.new(input: $stdin, output: $stdout),
|
|
43
|
-
capabilities: ACP::Types::AgentCapabilities.new
|
|
53
|
+
capabilities: ACP::Types::AgentCapabilities.new,
|
|
44
54
|
agent_info: ACP::Types::Implementation.new(name: 'my-agent', version: '1.0.0')
|
|
45
|
-
) { |client| MyAgent.new(client:) }
|
|
46
|
-
connection.start.join
|
|
55
|
+
) { |client| MyAgent.new(client:) }.start.join
|
|
47
56
|
```
|
|
48
57
|
|
|
49
|
-
|
|
58
|
+
Point your editor at that script and you have a working agent. For a fuller one that streams updates, asks permission, handles cancellation, uses files and terminals and persists sessions, see [`examples/echo_agent.rb`](https://github.com/bottrall/acp-sdk/blob/main/examples/echo_agent.rb).
|
|
50
59
|
|
|
51
|
-
|
|
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, with `data` naming the failing path, e.g. `{ errors: ['sessionId: expected String, got Integer'] }`.
|
|
53
|
-
- A handler that raises is answered with `-32603` and the message `Internal error`. Nothing from the exception reaches the peer — its class and message are logged to the transport's logger instead, since messages can interpolate paths, queries or credentials. Return an `ACP::RequestError` from the handler when the peer should see a real error message.
|
|
54
|
-
- `cancel` runs on the transport's reader thread, so it must return quickly: set a flag and let the prompt notice it, and call `client.cancel_requests` to cancel the connection's outstanding requests to the client — a blocked `request_permission` or `wait_for_terminal_exit`, say. That sends `$/cancel_request` for each one, and the blocked call returns once the client answers it, with its response or the `-32800` error.
|
|
55
|
-
- After a cancel, the agent must itself end the turn with `stopReason: cancelled`. `ACP::AgentConnection` does not enforce it.
|
|
56
|
-
- `client.cancelled?` is true in a handler when the client cancelled the request it is serving with `$/cancel_request`. The handler can end early by returning `ACP::RequestError.request_cancelled`, which is answered as `-32800`, or return a normal response, which is sent as-is — the transport sends exactly one response either way.
|
|
57
|
-
- `client.capabilities` is `nil` until the client sends `initialize`.
|
|
58
|
-
- `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.
|
|
59
|
-
- `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.
|
|
60
|
-
- `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.
|
|
61
|
-
- Once the transport's input ends, a `client` request still waiting for its answer and every one made afterwards return an `ACP::RequestError` whose `connection_closed?` is true. It is false for every error the client sent, even one with the same `-32603` code and `Connection closed` message, so a handler can tell a client that is gone from one that refused without matching the message.
|
|
60
|
+
### Agent methods
|
|
62
61
|
|
|
63
|
-
|
|
62
|
+
Each request method returns its response type, or an [`ACP::RequestError`](#errors) to reject the request.
|
|
64
63
|
|
|
65
|
-
|
|
64
|
+
| Method | Implement it when |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| `new_session`, `prompt`, `cancel` | Always |
|
|
67
|
+
| `load_session` | `capabilities` sets `load_session` |
|
|
68
|
+
| `list_sessions`, `resume_session`, `close_session`, `delete_session` | `capabilities.session_capabilities` sets `list`, `resume`, `close` or `delete` |
|
|
69
|
+
| `authenticate` | You pass `auth_methods:` |
|
|
70
|
+
| `logout` | `capabilities.auth` sets `logout` |
|
|
71
|
+
| `change_session_mode`, `change_session_config_option` | Your sessions offer modes or config options |
|
|
72
|
+
| `session_created(response)` | You want a hook after `session/new` is answered, e.g. to send available commands |
|
|
66
73
|
|
|
67
|
-
|
|
74
|
+
`start` raises `ArgumentError` if you advertise a capability without implementing its method. Requests for anything you don't advertise are rejected before they reach your agent.
|
|
68
75
|
|
|
69
|
-
|
|
76
|
+
### Calling the client
|
|
70
77
|
|
|
71
|
-
|
|
78
|
+
| `client.` | Does |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `update(session_id, update)` | Streams a `SessionUpdate`: message chunks, tool calls, plans and so on |
|
|
81
|
+
| `available_commands(session_id, commands)` | Advertises slash commands |
|
|
82
|
+
| `request_permission(request)` | Asks the user to approve a tool call |
|
|
83
|
+
| `read_text_file`, `write_text_file` | Reads and writes files through the editor |
|
|
84
|
+
| `create_terminal`, `terminal_output`, `wait_for_terminal_exit`, `kill_terminal`, `release_terminal` | Runs commands in the editor. Release every terminal you create. |
|
|
85
|
+
| `create_elicitation`, `complete_elicitation` | Asks the user for structured input |
|
|
86
|
+
| `capabilities` | The client's `ClientCapabilities` (`nil` until the client connects) |
|
|
72
87
|
|
|
73
|
-
|
|
88
|
+
File, terminal and elicitation calls need the matching client capability. Without it they return an error without contacting the client, so check `client.capabilities` before offering a feature that depends on one.
|
|
74
89
|
|
|
75
|
-
|
|
76
|
-
|
|
90
|
+
### Cancellation
|
|
91
|
+
|
|
92
|
+
`cancel` must return quickly, so record the cancellation and return. Call `client.cancel_requests` to unblock anything your agent is waiting on (a permission prompt, a terminal), then have `prompt` return `StopReason::CANCELLED`. The SDK doesn't end the turn for you.
|
|
93
|
+
|
|
94
|
+
Inside a request handler, `client.cancelled?` tells you whether the client has cancelled that request. Return `ACP::RequestError.request_cancelled` to stop early.
|
|
95
|
+
|
|
96
|
+
### Shutting down
|
|
77
97
|
|
|
78
|
-
|
|
98
|
+
`start` returns a thread that finishes when the client disconnects. To wait for in-flight handlers before closing your IO, keep a reference to the transport and call `transport.drain` after joining that thread.
|
|
99
|
+
|
|
100
|
+
## Driving an agent
|
|
101
|
+
|
|
102
|
+
`ACP::AgentProcess.spawn` launches an agent as a child process and connects to it. The process is terminated when the block exits, even if the block raises.
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
ACP::AgentProcess.spawn(
|
|
79
106
|
'my-agent', '--flag',
|
|
80
107
|
env: { 'API_KEY' => '...' },
|
|
81
108
|
cwd: Dir.pwd,
|
|
82
|
-
stderr: :capture,
|
|
83
109
|
permission: ->(request) { ask_the_user(request) },
|
|
84
110
|
updates: ->(notification) { show(notification) }
|
|
85
111
|
) do |connection|
|
|
86
112
|
connection.connect(ACP::Types::InitializeRequest.new(protocol_version: 1))
|
|
87
113
|
session = connection.new_session(ACP::Types::NewSessionRequest.new(cwd: Dir.pwd, mcp_servers: []))
|
|
114
|
+
|
|
88
115
|
prompt = [ACP::Types::ContentBlock::Text.new(text: 'hello')]
|
|
89
116
|
connection.prompt_session(ACP::Types::PromptRequest.new(session_id: session.session_id, prompt:)) do |update|
|
|
90
117
|
print update.content.text if update.is_a?(ACP::Types::SessionUpdate::AgentMessageChunk)
|
|
@@ -92,52 +119,97 @@ response = ACP::AgentProcess.spawn(
|
|
|
92
119
|
end
|
|
93
120
|
```
|
|
94
121
|
|
|
95
|
-
|
|
122
|
+
- Connection methods mirror the protocol: `new_session`, `prompt_session`, `cancel_session`, `set_session_mode` and so on. `connect` sends `initialize`, because Ruby reserves that name.
|
|
123
|
+
- `prompt_session` and `load_session` yield the session's updates as they arrive. Updates sent at other times go to `updates:`.
|
|
124
|
+
- Without a block, `spawn` returns the process and you call `terminate` yourself. Pass `stderr: :capture` to collect the agent's stderr on `process.stderr`.
|
|
125
|
+
|
|
126
|
+
### Serving files, terminals and elicitation
|
|
127
|
+
|
|
128
|
+
To let the agent use files, terminals or elicitation, build an `ACP::ClientConnection` with a handler object for each capability and advertise the capability when you connect:
|
|
96
129
|
|
|
97
130
|
```ruby
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
131
|
+
class Files
|
|
132
|
+
def read_text_file(request)
|
|
133
|
+
ACP::Types::ReadTextFileResponse.new(content: File.read(request.path))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def write_text_file(request)
|
|
103
137
|
File.write(request.path, request.content)
|
|
104
138
|
ACP::Types::WriteTextFileResponse.new
|
|
105
|
-
|
|
106
|
-
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
connection = ACP::ClientConnection.build(
|
|
143
|
+
transport: ACP::Transport::Stdio.new(input: agent_stdout, output: agent_stdin),
|
|
144
|
+
permission: ->(request) { ask_the_user(request) },
|
|
145
|
+
fs: Files.new
|
|
146
|
+
)
|
|
147
|
+
connection.start
|
|
148
|
+
connection.connect(
|
|
149
|
+
ACP::Types::InitializeRequest.new(
|
|
150
|
+
protocol_version: 1,
|
|
151
|
+
client_capabilities: ACP::Types::ClientCapabilities.new(
|
|
152
|
+
fs: ACP::Types::FileSystemCapabilities.new(read_text_file: true, write_text_file: true)
|
|
153
|
+
)
|
|
154
|
+
)
|
|
107
155
|
)
|
|
108
156
|
```
|
|
109
157
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
158
|
+
| Keyword | Methods | Capability |
|
|
159
|
+
| --- | --- | --- |
|
|
160
|
+
| `permission` | a callable | Always required |
|
|
161
|
+
| `fs` | `read_text_file`, `write_text_file` | `fs.read_text_file`, `fs.write_text_file` |
|
|
162
|
+
| `terminal` | `create_terminal`, `terminal_output`, `wait_for_terminal_exit`, `kill_terminal`, `release_terminal` | `terminal` |
|
|
163
|
+
| `elicitation` | `create_elicitation`, and optionally `complete_elicitation` for url mode | `elicitation.form` or `elicitation.url` |
|
|
164
|
+
|
|
165
|
+
A handler object needs only the methods of the capabilities you advertise. Each method returns the response type or an `ACP::RequestError`. `connect` raises `ArgumentError` if you advertise a capability whose method the handler doesn't define.
|
|
166
|
+
|
|
167
|
+
`permission` and the request methods each run on their own thread, so they can block while the user decides. `updates` and `complete_elicitation` must return quickly.
|
|
117
168
|
|
|
118
|
-
|
|
169
|
+
## Errors
|
|
170
|
+
|
|
171
|
+
`ACP::RequestError` is what you return to reject a request, and what you get back when the other side rejects yours. Errors are returned, never raised:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
session = connection.new_session(request)
|
|
175
|
+
if session.is_a?(ACP::RequestError)
|
|
176
|
+
warn "#{session.code}: #{session.message}"
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
- Build standard errors with `ACP::RequestError.invalid_params`, `.method_not_found`, `.resource_not_found`, `.auth_required` or `.request_cancelled`, or custom ones with `ACP::RequestError.new(code:, message:, data:)`.
|
|
181
|
+
- If a handler raises, the other side only sees `Internal error`. The exception is logged rather than sent, so a message can't leak paths or secrets. Return a `RequestError` when the other side should see the reason.
|
|
182
|
+
- If the connection drops, pending and later requests return an error whose `connection_closed?` is true.
|
|
119
183
|
|
|
120
184
|
## Types
|
|
121
185
|
|
|
122
|
-
Every ACP
|
|
186
|
+
Every ACP message is a frozen value class under `ACP::Types`, built with snake_case keyword arguments and converted with `from_h` and `to_h`.
|
|
123
187
|
|
|
124
|
-
-
|
|
125
|
-
-
|
|
126
|
-
- The schema's pure string enums are generated as constant modules, for building requests and matching responses without string literals: `ACP::Types::ToolKind::EDIT`, `ACP::Types::StopReason::CANCELLED`, `ACP::Types::PlanEntryPriority::HIGH`, `ACP::Types::Role::USER` and the rest. They carry no validation of their own — the generated `from_h` checks incoming values against the schema regardless of how you build outgoing ones.
|
|
188
|
+
- Unions are namespaces of variant classes, such as `ContentBlock::Text` and `SessionUpdate::ToolCall`. Match on them with `is_a?` or `case`.
|
|
189
|
+
- String enums are modules of constants, such as `StopReason::END_TURN`, `ToolKind::EDIT` and `Role::USER`.
|
|
127
190
|
|
|
128
|
-
##
|
|
191
|
+
## Extensions
|
|
129
192
|
|
|
130
|
-
|
|
193
|
+
Custom methods outside the spec must start with `_`. Both connections accept `extension_requests:` and `extension_notifications:` hashes of handlers keyed by method name, and send their own with `ext_request` and `ext_notify`:
|
|
131
194
|
|
|
132
195
|
```ruby
|
|
133
|
-
|
|
196
|
+
ACP::AgentConnection.new(
|
|
197
|
+
# ...
|
|
198
|
+
extension_requests: { '_my_agent/ping' => ->(params) { { 'pong' => params } } }
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
connection.ext_request('_my_agent/ping', { 'hello' => 'world' }) # => { "pong" => { "hello" => "world" } }
|
|
134
202
|
```
|
|
135
203
|
|
|
136
|
-
|
|
204
|
+
## Unstable features
|
|
205
|
+
|
|
206
|
+
Draft protocol features, such as session forking, providers, usage reporting and plan and compaction updates, live under `ACP::Types::Unstable` and load only when you opt in:
|
|
137
207
|
|
|
138
|
-
|
|
208
|
+
```ruby
|
|
209
|
+
require 'acp/types/unstable'
|
|
210
|
+
```
|
|
139
211
|
|
|
140
|
-
|
|
212
|
+
Once loaded, the connections support `fork_session`, `list_providers`, `set_provider` and `disable_provider`. Unstable features aren't part of the spec yet. Peers may not support them, and they can change or disappear in any release.
|
|
141
213
|
|
|
142
214
|
## Contributing
|
|
143
215
|
|
|
@@ -153,9 +153,8 @@ class ACP::AgentConnection::Client
|
|
|
153
153
|
result = @peer.request(rpc_method, request.to_h)
|
|
154
154
|
return result if result.is_a?(ACP::RequestError)
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
ACP::RequestError.invalid_response(result)
|
|
156
|
+
response = ACP::Types.parse(type, result)
|
|
157
|
+
response.is_a?(ACP::Types::ParseError) ? ACP::RequestError.invalid_response(result) : response
|
|
159
158
|
end
|
|
160
159
|
|
|
161
160
|
# @rbs rpc_method: String
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ACP::AgentConnection::OptionalRoute
|
|
4
|
+
# @dynamic rpc_method
|
|
5
|
+
attr_reader :rpc_method #: String
|
|
6
|
+
# @dynamic type
|
|
7
|
+
attr_reader :type #: ACP::Transport::_Parser
|
|
8
|
+
# @dynamic agent_method
|
|
9
|
+
attr_reader :agent_method #: Symbol
|
|
10
|
+
# @dynamic path
|
|
11
|
+
attr_reader :path #: Array[Symbol]?
|
|
12
|
+
# @dynamic handle
|
|
13
|
+
attr_reader :handle #: ^(ACP::AgentConnection::_FullAgent, untyped) -> (ACP::Transport::_Response | ACP::RequestError)
|
|
14
|
+
|
|
15
|
+
# @rbs rpc_method: String
|
|
16
|
+
# @rbs type: ACP::Transport::_Parser
|
|
17
|
+
# @rbs agent_method: Symbol
|
|
18
|
+
# @rbs path: Array[Symbol]?
|
|
19
|
+
# @rbs &handle: (ACP::AgentConnection::_FullAgent, untyped) -> (ACP::Transport::_Response | ACP::RequestError)
|
|
20
|
+
# @rbs return: void
|
|
21
|
+
def initialize(rpc_method:, type:, agent_method:, path:, &handle)
|
|
22
|
+
@rpc_method = rpc_method
|
|
23
|
+
@type = type
|
|
24
|
+
@agent_method = agent_method
|
|
25
|
+
@path = path&.freeze
|
|
26
|
+
@handle = handle
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @rbs initialize_response: ACP::Types::InitializeResponse
|
|
31
|
+
# @rbs return: bool
|
|
32
|
+
def advertised?(initialize_response)
|
|
33
|
+
steps = path
|
|
34
|
+
steps ? ACP::Protocol.advertised?(initialize_response, steps) : false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @rbs initialize_response: ACP::Types::InitializeResponse
|
|
38
|
+
# @rbs agent: ACP::AgentConnection::_Agent
|
|
39
|
+
# @rbs return: bool
|
|
40
|
+
def routed?(initialize_response, agent)
|
|
41
|
+
# The schema gives a per-session method no initialize capability, since an
|
|
42
|
+
# agent offers it in its session responses.
|
|
43
|
+
path ? advertised?(initialize_response) : agent.respond_to?(agent_method)
|
|
44
|
+
end
|
|
45
|
+
end
|