acp-sdk 0.6.0 → 0.8.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 +25 -0
- data/README.md +5 -2
- data/lib/acp/agent_connection/client.rb +1 -1
- data/lib/acp/agent_connection.rb +3 -1
- data/lib/acp/client_connection.rb +220 -4
- data/lib/acp/request_error.rb +18 -4
- data/lib/acp/transport/stdio.rb +21 -6
- data/lib/acp/types/agent_auth_capabilities.rb +4 -1
- data/lib/acp/types/agent_capabilities.rb +20 -5
- data/lib/acp/types/annotations.rb +6 -3
- data/lib/acp/types/audio_content.rb +8 -3
- data/lib/acp/types/auth_capabilities.rb +1 -1
- data/lib/acp/types/auth_method/terminal.rb +11 -5
- data/lib/acp/types/auth_method_agent.rb +6 -1
- data/lib/acp/types/auth_method_terminal.rb +11 -5
- data/lib/acp/types/authenticate_request.rb +1 -1
- data/lib/acp/types/available_command.rb +3 -3
- data/lib/acp/types/available_commands_update.rb +7 -4
- data/lib/acp/types/blob_resource_contents.rb +6 -1
- data/lib/acp/types/boolean_property_schema.rb +6 -1
- data/lib/acp/types/cancel_notification.rb +4 -1
- data/lib/acp/types/cancel_request_notification.rb +9 -1
- data/lib/acp/types/client_capabilities.rb +15 -5
- data/lib/acp/types/client_session_capabilities.rb +6 -3
- data/lib/acp/types/close_session_request.rb +4 -1
- data/lib/acp/types/complete_elicitation_notification.rb +4 -1
- data/lib/acp/types/config_option_update.rb +7 -4
- data/lib/acp/types/content.rb +4 -1
- data/lib/acp/types/content_block/audio.rb +8 -3
- data/lib/acp/types/content_block/image.rb +9 -4
- data/lib/acp/types/content_block/resource.rb +11 -2
- data/lib/acp/types/content_block/resource_link.rb +12 -7
- data/lib/acp/types/content_block/text.rb +7 -2
- data/lib/acp/types/content_chunk.rb +2 -2
- data/lib/acp/types/cost.rb +5 -1
- data/lib/acp/types/create_elicitation_request/mode/form.rb +5 -1
- data/lib/acp/types/create_elicitation_request/mode/url.rb +2 -2
- data/lib/acp/types/create_elicitation_request.rb +1 -1
- data/lib/acp/types/create_elicitation_response/action/accept.rb +6 -1
- data/lib/acp/types/create_terminal_request.rb +12 -6
- data/lib/acp/types/create_terminal_response.rb +4 -1
- data/lib/acp/types/current_mode_update.rb +4 -1
- data/lib/acp/types/delete_session_request.rb +4 -1
- data/lib/acp/types/diff.rb +6 -1
- data/lib/acp/types/elicitation_accept_action.rb +6 -1
- data/lib/acp/types/elicitation_capabilities.rb +2 -2
- data/lib/acp/types/elicitation_form_mode.rb +5 -1
- data/lib/acp/types/elicitation_property_schema/boolean_property_schema.rb +6 -1
- data/lib/acp/types/elicitation_property_schema/integer_property_schema.rb +5 -5
- data/lib/acp/types/elicitation_property_schema/multi_select_property_schema.rb +9 -6
- data/lib/acp/types/elicitation_property_schema/number_property_schema.rb +5 -5
- data/lib/acp/types/elicitation_property_schema/string_property_schema.rb +15 -9
- data/lib/acp/types/elicitation_request_scope.rb +8 -1
- data/lib/acp/types/elicitation_schema.rb +11 -5
- data/lib/acp/types/elicitation_session_scope.rb +4 -1
- data/lib/acp/types/elicitation_url_mode.rb +2 -2
- data/lib/acp/types/embedded_resource.rb +11 -2
- data/lib/acp/types/enum_option.rb +6 -1
- data/lib/acp/types/env_variable.rb +5 -1
- data/lib/acp/types/file_system_capabilities.rb +5 -1
- data/lib/acp/types/http_header.rb +5 -1
- data/lib/acp/types/image_content.rb +9 -4
- data/lib/acp/types/implementation.rb +6 -1
- data/lib/acp/types/initialize_request.rb +18 -3
- data/lib/acp/types/initialize_response.rb +17 -4
- data/lib/acp/types/integer_property_schema.rb +5 -5
- data/lib/acp/types/kill_terminal_request.rb +5 -1
- data/lib/acp/types/list_sessions_request.rb +5 -1
- data/lib/acp/types/list_sessions_response.rb +5 -2
- data/lib/acp/types/load_session_request.rb +14 -4
- data/lib/acp/types/load_session_response.rb +5 -2
- data/lib/acp/types/mcp_capabilities.rb +5 -1
- data/lib/acp/types/mcp_server/http.rb +6 -3
- data/lib/acp/types/mcp_server/sse.rb +6 -3
- data/lib/acp/types/mcp_server_http.rb +6 -3
- data/lib/acp/types/mcp_server_sse.rb +6 -3
- data/lib/acp/types/mcp_server_stdio.rb +10 -4
- data/lib/acp/types/multi_select_items/string_multi_select_items.rb +4 -1
- data/lib/acp/types/multi_select_property_schema.rb +9 -6
- data/lib/acp/types/new_session_request.rb +13 -3
- data/lib/acp/types/new_session_response.rb +6 -3
- data/lib/acp/types/number_property_schema.rb +5 -5
- data/lib/acp/types/permission_option.rb +10 -1
- data/lib/acp/types/plan.rb +4 -1
- data/lib/acp/types/plan_entry.rb +7 -3
- data/lib/acp/types/prompt_capabilities.rb +6 -1
- data/lib/acp/types/prompt_request.rb +5 -2
- data/lib/acp/types/prompt_response.rb +8 -1
- data/lib/acp/types/read_text_file_request.rb +4 -4
- data/lib/acp/types/read_text_file_response.rb +1 -1
- data/lib/acp/types/release_terminal_request.rb +5 -1
- data/lib/acp/types/request_permission_outcome/selected.rb +1 -1
- data/lib/acp/types/request_permission_request.rb +10 -3
- data/lib/acp/types/request_permission_response.rb +8 -1
- data/lib/acp/types/resource_link.rb +12 -7
- data/lib/acp/types/resume_session_request.rb +14 -4
- data/lib/acp/types/resume_session_response.rb +5 -2
- data/lib/acp/types/selected_permission_outcome.rb +1 -1
- data/lib/acp/types/session_capabilities.rb +20 -7
- data/lib/acp/types/session_config_boolean.rb +1 -1
- data/lib/acp/types/session_config_option/kind/boolean.rb +1 -1
- data/lib/acp/types/session_config_option/kind/select.rb +6 -2
- data/lib/acp/types/session_config_option.rb +3 -3
- data/lib/acp/types/session_config_options_capabilities.rb +6 -1
- data/lib/acp/types/session_config_select.rb +6 -2
- data/lib/acp/types/session_config_select_group.rb +6 -3
- data/lib/acp/types/session_config_select_option.rb +6 -1
- data/lib/acp/types/session_config_select_options.rb +6 -2
- data/lib/acp/types/session_info.rb +12 -5
- data/lib/acp/types/session_info_update.rb +10 -1
- data/lib/acp/types/session_mode.rb +6 -1
- data/lib/acp/types/session_mode_state.rb +8 -2
- data/lib/acp/types/session_notification.rb +2 -2
- data/lib/acp/types/session_update/agent_message_chunk.rb +2 -2
- data/lib/acp/types/session_update/agent_thought_chunk.rb +2 -2
- data/lib/acp/types/session_update/available_commands_update.rb +7 -4
- data/lib/acp/types/session_update/config_option_update.rb +7 -4
- data/lib/acp/types/session_update/current_mode_update.rb +4 -1
- data/lib/acp/types/session_update/plan.rb +4 -1
- data/lib/acp/types/session_update/session_info_update.rb +10 -1
- data/lib/acp/types/session_update/tool_call.rb +23 -7
- data/lib/acp/types/session_update/tool_call_update.rb +23 -7
- data/lib/acp/types/session_update/usage_update.rb +3 -3
- data/lib/acp/types/session_update/user_message_chunk.rb +2 -2
- data/lib/acp/types/set_session_config_option_request/value/boolean.rb +1 -1
- data/lib/acp/types/set_session_config_option_request/value/value_id.rb +1 -1
- data/lib/acp/types/set_session_config_option_request.rb +2 -2
- data/lib/acp/types/set_session_config_option_response.rb +7 -4
- data/lib/acp/types/set_session_mode_request.rb +5 -1
- data/lib/acp/types/string_multi_select_items.rb +4 -1
- data/lib/acp/types/string_property_schema.rb +15 -9
- data/lib/acp/types/terminal.rb +4 -1
- data/lib/acp/types/terminal_exit_status.rb +5 -1
- data/lib/acp/types/terminal_output_request.rb +5 -1
- data/lib/acp/types/terminal_output_response.rb +8 -3
- data/lib/acp/types/text_content.rb +7 -2
- data/lib/acp/types/text_resource_contents.rb +6 -1
- data/lib/acp/types/titled_multi_select_items.rb +4 -1
- data/lib/acp/types/tool_call.rb +23 -7
- data/lib/acp/types/tool_call_content/content.rb +4 -1
- data/lib/acp/types/tool_call_content/diff.rb +6 -1
- data/lib/acp/types/tool_call_content/terminal.rb +4 -1
- data/lib/acp/types/tool_call_location.rb +5 -1
- data/lib/acp/types/tool_call_update.rb +23 -7
- data/lib/acp/types/unstructured_command_input.rb +1 -1
- data/lib/acp/types/usage_update.rb +3 -3
- data/lib/acp/types/wait_for_terminal_exit_request.rb +5 -1
- data/lib/acp/types/wait_for_terminal_exit_response.rb +5 -1
- data/lib/acp/types/write_text_file_request.rb +3 -3
- data/lib/acp/types.rb +247 -0
- data/lib/acp/version.rb +1 -1
- data/sig/generated/acp/client_connection.rbs +101 -12
- data/sig/generated/acp/request_error.rbs +14 -2
- data/sig/generated/acp/transport/stdio.rbs +13 -2
- data/sig/generated/acp/types.rbs +143 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e0b8b181c4cbd41a411868aecdf9f34d0356b490526c446a962287a5a3ae01cc
|
|
4
|
+
data.tar.gz: 1376e6d42bc4369de51a645ed423a563e7aa9b35c4c49b060c77a983cb2c603c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6c5d6271be17f17e203c178664ad6de2234c4b1a7b1857663c1ebb20bdbe1fa3c282db70ac6fe3c3e6b9647dfaadbe761bd018972943393a0cb3dec3d10bb2a3
|
|
7
|
+
data.tar.gz: 9ca3cae203c38496096dd020403054daaa4cb6797183c95b04a75c045f4a8aff89754a07af0fd74eb4c2a45f48dbc6d7c64352faf572000f118130d473283f0b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.0](https://github.com/bottrall/acp-sdk/compare/v0.7.0...v0.8.0) (2026-10-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* elicitation handlers on ACP::ClientConnection ([#78](https://github.com/bottrall/acp-sdk/issues/78)) ([94ee653](https://github.com/bottrall/acp-sdk/commit/94ee65374a0b463ccbd7ed57e05fe1ad76b08a62))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* validate primitive and enum types when parsing params ([#79](https://github.com/bottrall/acp-sdk/issues/79)) ([624be63](https://github.com/bottrall/acp-sdk/commit/624be63f667468fba7c1a61064b84f43c226b060))
|
|
14
|
+
|
|
15
|
+
## [0.7.0](https://github.com/bottrall/acp-sdk/compare/v0.6.0...v0.7.0) (2026-10-05)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* terminal handlers on ACP::ClientConnection ([#77](https://github.com/bottrall/acp-sdk/issues/77)) ([8a42829](https://github.com/bottrall/acp-sdk/commit/8a428295d5cd7c92b56ec1b3ed1c900b485bbf01)), closes [#40](https://github.com/bottrall/acp-sdk/issues/40)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* attach data to method-not-found and invalid-request replies ([#75](https://github.com/bottrall/acp-sdk/issues/75)) ([9bc201c](https://github.com/bottrall/acp-sdk/commit/9bc201c53d86f506bedb5b49183b865cf9b8f4fe)), closes [#29](https://github.com/bottrall/acp-sdk/issues/29)
|
|
26
|
+
* don't send handler exception messages to the peer ([#74](https://github.com/bottrall/acp-sdk/issues/74)) ([af66b48](https://github.com/bottrall/acp-sdk/commit/af66b48cbab4a99244c28a9c6f6a12c9ea23efc7)), closes [#28](https://github.com/bottrall/acp-sdk/issues/28)
|
|
27
|
+
|
|
3
28
|
## [0.6.0](https://github.com/bottrall/acp-sdk/compare/v0.5.0...v0.6.0) (2026-10-04)
|
|
4
29
|
|
|
5
30
|
|
data/README.md
CHANGED
|
@@ -49,7 +49,8 @@ connection.start.join
|
|
|
49
49
|
The agent's contract:
|
|
50
50
|
|
|
51
51
|
- It defines `new_session`, `prompt` and `cancel`. `load_session`, `list_sessions`, `resume_session`, `close_session` and `delete_session` are required only when `capabilities` advertises `load_session` or `session_capabilities.list`, `.resume`, `.close` or `.delete`, and `authenticate` only when `auth_methods` is non-empty, and `logout` only when `capabilities` advertises `auth.logout`; `start` raises `ArgumentError` when one is advertised but missing, and a method that is not advertised is answered with `-32601`. `change_session_mode` and `change_session_config_option` are optional, since modes and config options are offered per session rather than in `initialize`; `session/set_mode` and `session/set_config_option` are answered with `-32601` when the agent does not define them. `session_created`, if defined, runs right after the `session/new` reply is sent.
|
|
52
|
-
- 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.
|
|
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.
|
|
53
54
|
- `cancel` runs on the transport's reader thread, so it must return quickly: set a flag and let the prompt notice it.
|
|
54
55
|
- After a cancel, the agent must itself end the turn with `stopReason: cancelled`. `ACP::AgentConnection` does not enforce it.
|
|
55
56
|
- `client.capabilities` is `nil` until the client sends `initialize`.
|
|
@@ -63,7 +64,7 @@ Extensions are `_`-prefixed methods outside the spec. `AgentConnection` takes `e
|
|
|
63
64
|
|
|
64
65
|
## Driving an agent
|
|
65
66
|
|
|
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.
|
|
67
|
+
`ACP::ClientConnection` is the other side of the same transport. Each method takes the request's generated `ACP::Types` object and returns its response type or the agent's `ACP::RequestError`. `connect` sends `initialize`, since Ruby reserves that name for the constructor, and `authenticate` and `logout` send their namesake methods. `session_set_mode` sends `session/set_mode`, and `session_set_config_option` sends `session/set_config_option` and returns the session's complete option list, which replaces any state held from an earlier reply. A handler that raises is answered with `-32603` and the message `Internal error`, and the exception is only logged to the transport's logger.
|
|
67
68
|
|
|
68
69
|
`ACP::AgentProcess.spawn` starts a command with args, env and cwd, wires its stdio to a new `ACP::ClientConnection` and starts it. The block receives the connection and the process handle (`pid`, plus `stderr` when captured), and the child is terminated — after its stdin is closed, with TERM and then KILL if it will not exit — when the block exits, so a raised block cannot leak the process. Without a block, `spawn` returns the process and calling `terminate` is yours. `stderr` is `:inherit` by default, so the child writes to your stderr; pass `:capture` to collect it on the process instead.
|
|
69
70
|
|
|
@@ -105,6 +106,8 @@ connection = ACP::ClientConnection.new(
|
|
|
105
106
|
- `session_prompt` and `session_load` yield the session's updates on the calling thread as they arrive and return once the agent replies. Updates outside those calls, such as the available commands after `session/new`, go to `updates`, which runs on the reader thread and must return quickly. `session_resume` does not take a block: the agent must not replay history on resume, so unlike `session_load` there is nothing to stream. `session_close` and `session_delete` end the session, discarding it on the agent, and return once the agent replies.
|
|
106
107
|
- `permission` answers `session/request_permission` with an `ACP::Types::RequestPermissionResponse` or an `ACP::RequestError`. It runs on its own thread, so it may block while the user decides, and `session_cancel` can be sent meanwhile.
|
|
107
108
|
- `read_text_file` and `write_text_file` answer the agent's `fs/read_text_file` and `fs/write_text_file` with an `ACP::Types::ReadTextFileResponse`, an `ACP::Types::WriteTextFileResponse` or an `ACP::RequestError`. They are routed by the `fs` capabilities `connect` sends: a method the client does not advertise is answered with `-32601` without reaching the handler, and `connect` raises `ArgumentError` — before sending `initialize` — when the request advertises a capability that has no handler.
|
|
109
|
+
- `create_terminal`, `terminal_output`, `wait_for_terminal_exit`, `kill_terminal` and `release_terminal` answer the agent's `terminal/create`, `terminal/output`, `terminal/wait_for_exit`, `terminal/kill` and `terminal/release` with the matching `ACP::Types` response or an `ACP::RequestError`. They are routed by the `terminal` capability `connect` sends, the same way as `fs`: a method the client does not advertise is answered with `-32601` without reaching the handler, and `connect` raises `ArgumentError` — before sending `initialize` — when the request advertises `terminal` with any of the five missing. The transport serves each request on its own thread, so `wait_for_terminal_exit` may block without holding up the others.
|
|
110
|
+
- `elicitation` answers the agent's `elicitation/create` with an `ACP::Types::CreateElicitationResponse` or an `ACP::RequestError`, serving both form and url mode, and `complete_elicitation` receives the agent's `elicitation/complete` notification. They are routed by the `elicitation` capabilities `connect` sends: a request whose mode the client does not advertise is answered with `-32602` without reaching the handler, and `connect` raises `ArgumentError` — before sending `initialize` — when the request advertises `elicitation.form` or `elicitation.url` with no `elicitation` handler. The client tracks the elicitation ids of the url-mode requests it serves, so a completion for an unknown or already-completed id is ignored. `complete_elicitation` runs on the reader thread and must return quickly; it may be omitted, in which case completions are only tracked.
|
|
108
111
|
|
|
109
112
|
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`.
|
|
110
113
|
|
|
@@ -136,7 +136,7 @@ class ACP::AgentConnection::Client
|
|
|
136
136
|
return result if result.is_a?(ACP::RequestError)
|
|
137
137
|
|
|
138
138
|
type.from_h(result)
|
|
139
|
-
rescue KeyError, TypeError, NoMethodError
|
|
139
|
+
rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError
|
|
140
140
|
ACP::RequestError.invalid_response(result)
|
|
141
141
|
end
|
|
142
142
|
|
data/lib/acp/agent_connection.rb
CHANGED
|
@@ -117,7 +117,7 @@ class ACP::AgentConnection
|
|
|
117
117
|
# @rbs return: void
|
|
118
118
|
def cancel(agent, params)
|
|
119
119
|
notification = ACP::Types::CancelNotification.from_h(params)
|
|
120
|
-
rescue KeyError, TypeError, NoMethodError => e
|
|
120
|
+
rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError => e
|
|
121
121
|
@logger.warn("dropped malformed session/cancel: #{e.class}: #{e.message}")
|
|
122
122
|
else
|
|
123
123
|
agent.cancel(notification)
|
|
@@ -131,6 +131,8 @@ class ACP::AgentConnection
|
|
|
131
131
|
def route(type, &)
|
|
132
132
|
lambda do |params|
|
|
133
133
|
request = type.from_h(params)
|
|
134
|
+
rescue ACP::Types::ParseError => e
|
|
135
|
+
ACP::RequestError.invalid_params([e.message])
|
|
134
136
|
rescue KeyError, TypeError, NoMethodError
|
|
135
137
|
ACP::RequestError.invalid_params
|
|
136
138
|
else
|
|
@@ -5,13 +5,25 @@ class ACP::ClientConnection
|
|
|
5
5
|
# @rbs @permission: ACP::ClientConnection::_PermissionHandler
|
|
6
6
|
# @rbs @read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
7
7
|
# @rbs @write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
|
8
|
+
# @rbs @create_terminal: (^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError))?
|
|
9
|
+
# @rbs @terminal_output: (^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError))?
|
|
10
|
+
# @rbs @wait_for_terminal_exit: (^(ACP::Types::WaitForTerminalExitRequest) ->
|
|
11
|
+
# (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError))?
|
|
12
|
+
# @rbs @kill_terminal: (^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError))?
|
|
13
|
+
# @rbs @release_terminal: (^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError))?
|
|
14
|
+
# @rbs @elicitation: (^(ACP::Types::CreateElicitationRequest) ->
|
|
15
|
+
# (ACP::Types::CreateElicitationResponse | ACP::RequestError))?
|
|
16
|
+
# @rbs @complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?
|
|
8
17
|
# @rbs @updates: ACP::ClientConnection::_UpdateHandler
|
|
9
18
|
# @rbs @fs_capabilities: ACP::Types::FileSystemCapabilities?
|
|
19
|
+
# @rbs @terminal: bool?
|
|
20
|
+
# @rbs @elicitation_capabilities: ACP::Types::ElicitationCapabilities?
|
|
10
21
|
# @rbs @extension_requests: Hash[String, ^(untyped) -> untyped]
|
|
11
22
|
# @rbs @extension_notifications: Hash[String, ^(untyped) -> void]
|
|
12
23
|
# @rbs @lock: Thread::Mutex
|
|
13
24
|
# @rbs @streams: Hash[String, Thread::Queue]
|
|
14
25
|
# @rbs @pending_permissions: Hash[String, Array[Thread::Queue]]
|
|
26
|
+
# @rbs @outstanding_elicitations: Hash[String, bool]
|
|
15
27
|
# @rbs @logger: ACP::Transport::_Logger
|
|
16
28
|
|
|
17
29
|
PROTOCOL_VERSION = ACP::AgentConnection::PROTOCOL_VERSION #: Integer
|
|
@@ -26,6 +38,15 @@ class ACP::ClientConnection
|
|
|
26
38
|
# @rbs permission: ACP::ClientConnection::_PermissionHandler
|
|
27
39
|
# @rbs read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
28
40
|
# @rbs write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
|
41
|
+
# @rbs create_terminal: (^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError))?
|
|
42
|
+
# @rbs terminal_output: (^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError))?
|
|
43
|
+
# @rbs wait_for_terminal_exit: (^(ACP::Types::WaitForTerminalExitRequest) ->
|
|
44
|
+
# (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError))?
|
|
45
|
+
# @rbs kill_terminal: (^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError))?
|
|
46
|
+
# @rbs release_terminal: (^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError))?
|
|
47
|
+
# @rbs elicitation: (^(ACP::Types::CreateElicitationRequest) ->
|
|
48
|
+
# (ACP::Types::CreateElicitationResponse | ACP::RequestError))?
|
|
49
|
+
# @rbs complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?
|
|
29
50
|
# @rbs updates: ACP::ClientConnection::_UpdateHandler
|
|
30
51
|
# @rbs extension_requests: Hash[String, ^(untyped) -> untyped]
|
|
31
52
|
# @rbs extension_notifications: Hash[String, ^(untyped) -> void]
|
|
@@ -36,6 +57,13 @@ class ACP::ClientConnection
|
|
|
36
57
|
permission:,
|
|
37
58
|
read_text_file: nil,
|
|
38
59
|
write_text_file: nil,
|
|
60
|
+
create_terminal: nil,
|
|
61
|
+
terminal_output: nil,
|
|
62
|
+
wait_for_terminal_exit: nil,
|
|
63
|
+
kill_terminal: nil,
|
|
64
|
+
release_terminal: nil,
|
|
65
|
+
elicitation: nil,
|
|
66
|
+
complete_elicitation: nil,
|
|
39
67
|
updates: IGNORE,
|
|
40
68
|
extension_requests: {},
|
|
41
69
|
extension_notifications: {},
|
|
@@ -45,8 +73,17 @@ class ACP::ClientConnection
|
|
|
45
73
|
@permission = permission
|
|
46
74
|
@read_text_file = read_text_file
|
|
47
75
|
@write_text_file = write_text_file
|
|
76
|
+
@create_terminal = create_terminal
|
|
77
|
+
@terminal_output = terminal_output
|
|
78
|
+
@wait_for_terminal_exit = wait_for_terminal_exit
|
|
79
|
+
@kill_terminal = kill_terminal
|
|
80
|
+
@release_terminal = release_terminal
|
|
81
|
+
@elicitation = elicitation
|
|
82
|
+
@complete_elicitation = complete_elicitation
|
|
48
83
|
@updates = updates
|
|
49
84
|
@fs_capabilities = nil
|
|
85
|
+
@terminal = nil
|
|
86
|
+
@elicitation_capabilities = nil
|
|
50
87
|
@extension_requests = extension_requests
|
|
51
88
|
@extension_notifications = extension_notifications
|
|
52
89
|
extension_requests.each_key { |name| ACP::Extensions.validate_name(name) }
|
|
@@ -54,6 +91,7 @@ class ACP::ClientConnection
|
|
|
54
91
|
@lock = Mutex.new
|
|
55
92
|
@streams = {}
|
|
56
93
|
@pending_permissions = {}
|
|
94
|
+
@outstanding_elicitations = {}
|
|
57
95
|
@logger = logger
|
|
58
96
|
end
|
|
59
97
|
|
|
@@ -63,9 +101,16 @@ class ACP::ClientConnection
|
|
|
63
101
|
requests: {
|
|
64
102
|
'session/request_permission' => method(:request_permission),
|
|
65
103
|
'fs/read_text_file' => method(:read_text_file),
|
|
66
|
-
'fs/write_text_file' => method(:write_text_file)
|
|
104
|
+
'fs/write_text_file' => method(:write_text_file),
|
|
105
|
+
'terminal/create' => method(:create_terminal),
|
|
106
|
+
'terminal/output' => method(:terminal_output),
|
|
107
|
+
'terminal/wait_for_exit' => method(:wait_for_terminal_exit),
|
|
108
|
+
'terminal/kill' => method(:kill_terminal),
|
|
109
|
+
'terminal/release' => method(:release_terminal),
|
|
110
|
+
'elicitation/create' => method(:create_elicitation)
|
|
67
111
|
}.merge(@extension_requests),
|
|
68
|
-
notifications: { 'session/update' => method(:dispatch)
|
|
112
|
+
notifications: { 'session/update' => method(:dispatch),
|
|
113
|
+
'elicitation/complete' => method(:complete_elicitation) }.merge(@extension_notifications)
|
|
69
114
|
)
|
|
70
115
|
end
|
|
71
116
|
|
|
@@ -81,7 +126,21 @@ class ACP::ClientConnection
|
|
|
81
126
|
raise ArgumentError, "initialize advertises fs methods no handler serves: #{unserved.join(', ')}"
|
|
82
127
|
end
|
|
83
128
|
|
|
129
|
+
terminal = request.client_capabilities&.terminal
|
|
130
|
+
unserved = unserved_terminal_methods(terminal)
|
|
131
|
+
unless unserved.empty?
|
|
132
|
+
raise ArgumentError, "initialize advertises terminal methods no handler serves: #{unserved.join(', ')}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
elicitation = request.client_capabilities&.elicitation
|
|
136
|
+
unserved = unserved_elicitation_modes(elicitation)
|
|
137
|
+
unless unserved.empty?
|
|
138
|
+
raise ArgumentError, "initialize advertises elicitation modes no handler serves: #{unserved.join(', ')}"
|
|
139
|
+
end
|
|
140
|
+
|
|
84
141
|
@fs_capabilities = capabilities
|
|
142
|
+
@terminal = terminal
|
|
143
|
+
@elicitation_capabilities = elicitation
|
|
85
144
|
parse(ACP::Types::InitializeResponse, @transport.request('initialize', request.to_h)).then do |response|
|
|
86
145
|
next response if response.is_a?(ACP::RequestError)
|
|
87
146
|
|
|
@@ -210,7 +269,7 @@ class ACP::ClientConnection
|
|
|
210
269
|
return result if result.is_a?(ACP::RequestError)
|
|
211
270
|
|
|
212
271
|
type.from_h(result)
|
|
213
|
-
rescue KeyError, TypeError, NoMethodError
|
|
272
|
+
rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError
|
|
214
273
|
ACP::RequestError.invalid_response(result)
|
|
215
274
|
end
|
|
216
275
|
|
|
@@ -259,7 +318,7 @@ class ACP::ClientConnection
|
|
|
259
318
|
# @rbs return: void
|
|
260
319
|
def dispatch(params)
|
|
261
320
|
notification = ACP::Types::SessionNotification.from_h(params)
|
|
262
|
-
rescue KeyError, TypeError, NoMethodError => e
|
|
321
|
+
rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError => e
|
|
263
322
|
@logger.warn("dropped malformed session/update: #{e.class}: #{e.message}")
|
|
264
323
|
else
|
|
265
324
|
queue = @lock.synchronize { @streams[notification.session_id]&.push(notification.update) }
|
|
@@ -274,6 +333,8 @@ class ACP::ClientConnection
|
|
|
274
333
|
# @rbs return: (ACP::Types::RequestPermissionResponse | ACP::RequestError)
|
|
275
334
|
def request_permission(params)
|
|
276
335
|
request = ACP::Types::RequestPermissionRequest.from_h(params)
|
|
336
|
+
rescue ACP::Types::ParseError => e
|
|
337
|
+
ACP::RequestError.invalid_params([e.message])
|
|
277
338
|
rescue KeyError, TypeError, NoMethodError
|
|
278
339
|
ACP::RequestError.invalid_params
|
|
279
340
|
else
|
|
@@ -327,6 +388,8 @@ class ACP::ClientConnection
|
|
|
327
388
|
return ACP::RequestError.unadvertised('fs.readTextFile') unless @fs_capabilities&.read_text_file
|
|
328
389
|
|
|
329
390
|
request = ACP::Types::ReadTextFileRequest.from_h(params)
|
|
391
|
+
rescue ACP::Types::ParseError => e
|
|
392
|
+
ACP::RequestError.invalid_params([e.message])
|
|
330
393
|
rescue KeyError, TypeError, NoMethodError
|
|
331
394
|
ACP::RequestError.invalid_params
|
|
332
395
|
else
|
|
@@ -341,6 +404,8 @@ class ACP::ClientConnection
|
|
|
341
404
|
return ACP::RequestError.unadvertised('fs.writeTextFile') unless @fs_capabilities&.write_text_file
|
|
342
405
|
|
|
343
406
|
request = ACP::Types::WriteTextFileRequest.from_h(params)
|
|
407
|
+
rescue ACP::Types::ParseError => e
|
|
408
|
+
ACP::RequestError.invalid_params([e.message])
|
|
344
409
|
rescue KeyError, TypeError, NoMethodError
|
|
345
410
|
ACP::RequestError.invalid_params
|
|
346
411
|
else
|
|
@@ -349,6 +414,130 @@ class ACP::ClientConnection
|
|
|
349
414
|
handler.call(request)
|
|
350
415
|
end
|
|
351
416
|
|
|
417
|
+
# The terminal routes are registered at start, before connect records the
|
|
418
|
+
# advertised capability, so each one answers -32601 until then.
|
|
419
|
+
#
|
|
420
|
+
# @rbs params: untyped
|
|
421
|
+
# @rbs return: (ACP::Types::CreateTerminalResponse | ACP::RequestError)
|
|
422
|
+
def create_terminal(params)
|
|
423
|
+
return ACP::RequestError.unadvertised('terminal') unless @terminal
|
|
424
|
+
|
|
425
|
+
request = ACP::Types::CreateTerminalRequest.from_h(params)
|
|
426
|
+
rescue KeyError, TypeError, NoMethodError
|
|
427
|
+
ACP::RequestError.invalid_params
|
|
428
|
+
else
|
|
429
|
+
# Safe: connect refuses an advertised capability no handler serves.
|
|
430
|
+
handler = @create_terminal #: ^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError)
|
|
431
|
+
handler.call(request)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# @rbs params: untyped
|
|
435
|
+
# @rbs return: (ACP::Types::TerminalOutputResponse | ACP::RequestError)
|
|
436
|
+
def terminal_output(params)
|
|
437
|
+
return ACP::RequestError.unadvertised('terminal') unless @terminal
|
|
438
|
+
|
|
439
|
+
request = ACP::Types::TerminalOutputRequest.from_h(params)
|
|
440
|
+
rescue KeyError, TypeError, NoMethodError
|
|
441
|
+
ACP::RequestError.invalid_params
|
|
442
|
+
else
|
|
443
|
+
# Safe: connect refuses an advertised capability no handler serves.
|
|
444
|
+
handler = @terminal_output #: ^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError)
|
|
445
|
+
handler.call(request)
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# The handler may block for as long as the command runs, so the transport
|
|
449
|
+
# must serve each request on its own thread for this not to hold up others.
|
|
450
|
+
#
|
|
451
|
+
# @rbs params: untyped
|
|
452
|
+
# @rbs return: (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError)
|
|
453
|
+
def wait_for_terminal_exit(params)
|
|
454
|
+
return ACP::RequestError.unadvertised('terminal') unless @terminal
|
|
455
|
+
|
|
456
|
+
request = ACP::Types::WaitForTerminalExitRequest.from_h(params)
|
|
457
|
+
rescue KeyError, TypeError, NoMethodError
|
|
458
|
+
ACP::RequestError.invalid_params
|
|
459
|
+
else
|
|
460
|
+
# Safe: connect refuses an advertised capability no handler serves.
|
|
461
|
+
handler = @wait_for_terminal_exit #: ^(ACP::Types::WaitForTerminalExitRequest) -> (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError)
|
|
462
|
+
handler.call(request)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# @rbs params: untyped
|
|
466
|
+
# @rbs return: (ACP::Types::KillTerminalResponse | ACP::RequestError)
|
|
467
|
+
def kill_terminal(params)
|
|
468
|
+
return ACP::RequestError.unadvertised('terminal') unless @terminal
|
|
469
|
+
|
|
470
|
+
request = ACP::Types::KillTerminalRequest.from_h(params)
|
|
471
|
+
rescue KeyError, TypeError, NoMethodError
|
|
472
|
+
ACP::RequestError.invalid_params
|
|
473
|
+
else
|
|
474
|
+
# Safe: connect refuses an advertised capability no handler serves.
|
|
475
|
+
handler = @kill_terminal #: ^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError)
|
|
476
|
+
handler.call(request)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# @rbs params: untyped
|
|
480
|
+
# @rbs return: (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
|
|
481
|
+
def release_terminal(params)
|
|
482
|
+
return ACP::RequestError.unadvertised('terminal') unless @terminal
|
|
483
|
+
|
|
484
|
+
request = ACP::Types::ReleaseTerminalRequest.from_h(params)
|
|
485
|
+
rescue KeyError, TypeError, NoMethodError
|
|
486
|
+
ACP::RequestError.invalid_params
|
|
487
|
+
else
|
|
488
|
+
# Safe: connect refuses an advertised capability no handler serves.
|
|
489
|
+
handler = @release_terminal #: ^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
|
|
490
|
+
handler.call(request)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# The routes are registered at start, before connect records the advertised
|
|
494
|
+
# capabilities, so a request for an unadvertised mode answers -32602.
|
|
495
|
+
#
|
|
496
|
+
# @rbs params: untyped
|
|
497
|
+
# @rbs return: (ACP::Types::CreateElicitationResponse | ACP::RequestError)
|
|
498
|
+
def create_elicitation(params)
|
|
499
|
+
request = ACP::Types::CreateElicitationRequest.from_h(params)
|
|
500
|
+
rescue KeyError, TypeError, NoMethodError
|
|
501
|
+
ACP::RequestError.invalid_params
|
|
502
|
+
else
|
|
503
|
+
case request.mode
|
|
504
|
+
when ACP::Types::CreateElicitationRequest::Mode::Form
|
|
505
|
+
return ACP::RequestError.unadvertised_mode('elicitation.form') unless @elicitation_capabilities&.form
|
|
506
|
+
when ACP::Types::CreateElicitationRequest::Mode::Url
|
|
507
|
+
return ACP::RequestError.unadvertised_mode('elicitation.url') unless @elicitation_capabilities&.url
|
|
508
|
+
|
|
509
|
+
register_elicitation(request.mode.elicitation_id)
|
|
510
|
+
else
|
|
511
|
+
return ACP::RequestError.unadvertised_mode("elicitation.#{request.mode['mode']}")
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# Safe: connect refuses an advertised mode no handler serves.
|
|
515
|
+
handler = @elicitation #: ^(ACP::Types::CreateElicitationRequest) -> (ACP::Types::CreateElicitationResponse | ACP::RequestError)
|
|
516
|
+
handler.call(request)
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# The spec requires a completion for an unknown or already-completed
|
|
520
|
+
# elicitation id to be ignored, so only an id registered by a served
|
|
521
|
+
# url-mode request reaches the handler. It runs on the transport's reader
|
|
522
|
+
# thread, so it must return quickly.
|
|
523
|
+
#
|
|
524
|
+
# @rbs params: untyped
|
|
525
|
+
# @rbs return: void
|
|
526
|
+
def complete_elicitation(params)
|
|
527
|
+
notification = ACP::Types::CompleteElicitationNotification.from_h(params)
|
|
528
|
+
rescue KeyError, TypeError, NoMethodError => e
|
|
529
|
+
@logger.warn("dropped malformed elicitation/complete: #{e.class}: #{e.message}")
|
|
530
|
+
else
|
|
531
|
+
known = @lock.synchronize { @outstanding_elicitations.delete(notification.elicitation_id) }
|
|
532
|
+
@complete_elicitation&.call(notification) if known
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# @rbs elicitation_id: String
|
|
536
|
+
# @rbs return: void
|
|
537
|
+
def register_elicitation(elicitation_id)
|
|
538
|
+
@lock.synchronize { @outstanding_elicitations[elicitation_id] = true }
|
|
539
|
+
end
|
|
540
|
+
|
|
352
541
|
# The fs capabilities initialize advertises that no injected handler serves.
|
|
353
542
|
#
|
|
354
543
|
# @rbs capabilities: ACP::Types::FileSystemCapabilities?
|
|
@@ -359,4 +548,31 @@ class ACP::ClientConnection
|
|
|
359
548
|
'fs.writeTextFile' => capabilities&.write_text_file && !@write_text_file
|
|
360
549
|
}.select { |_, unserved| unserved }.keys
|
|
361
550
|
end
|
|
551
|
+
|
|
552
|
+
# The terminal capabilities initialize advertises that no injected handler
|
|
553
|
+
# serves.
|
|
554
|
+
#
|
|
555
|
+
# @rbs capabilities: bool?
|
|
556
|
+
# @rbs return: Array[String]
|
|
557
|
+
def unserved_terminal_methods(capabilities)
|
|
558
|
+
{
|
|
559
|
+
'terminal.create' => capabilities && !@create_terminal,
|
|
560
|
+
'terminal.output' => capabilities && !@terminal_output,
|
|
561
|
+
'terminal.wait_for_exit' => capabilities && !@wait_for_terminal_exit,
|
|
562
|
+
'terminal.kill' => capabilities && !@kill_terminal,
|
|
563
|
+
'terminal.release' => capabilities && !@release_terminal
|
|
564
|
+
}.select { |_, unserved| unserved }.keys
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
# The elicitation capabilities initialize advertises that no injected
|
|
568
|
+
# handler serves.
|
|
569
|
+
#
|
|
570
|
+
# @rbs capabilities: ACP::Types::ElicitationCapabilities?
|
|
571
|
+
# @rbs return: Array[String]
|
|
572
|
+
def unserved_elicitation_modes(capabilities)
|
|
573
|
+
{
|
|
574
|
+
'elicitation.form' => capabilities&.form && !@elicitation,
|
|
575
|
+
'elicitation.url' => capabilities&.url && !@elicitation
|
|
576
|
+
}.select { |_, unserved| unserved }.keys
|
|
577
|
+
end
|
|
362
578
|
end
|
data/lib/acp/request_error.rb
CHANGED
|
@@ -48,14 +48,16 @@ class ACP::RequestError
|
|
|
48
48
|
new(code: INVALID_REQUEST, message: 'Invalid request')
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
# @rbs method: String?
|
|
51
52
|
# @rbs return: ACP::RequestError
|
|
52
|
-
def self.method_not_found
|
|
53
|
-
new(code: METHOD_NOT_FOUND, message: 'Method not found')
|
|
53
|
+
def self.method_not_found(method = nil)
|
|
54
|
+
new(code: METHOD_NOT_FOUND, message: 'Method not found', data: method && { method: method })
|
|
54
55
|
end
|
|
55
56
|
|
|
57
|
+
# @rbs errors: Array[String]?
|
|
56
58
|
# @rbs return: ACP::RequestError
|
|
57
|
-
def self.invalid_params
|
|
58
|
-
new(code: INVALID_PARAMS, message: 'Invalid params')
|
|
59
|
+
def self.invalid_params(errors = nil)
|
|
60
|
+
new(code: INVALID_PARAMS, message: 'Invalid params', data: errors && { errors: errors })
|
|
59
61
|
end
|
|
60
62
|
|
|
61
63
|
# The method-not-found code for a method the client's capabilities do not
|
|
@@ -68,6 +70,18 @@ class ACP::RequestError
|
|
|
68
70
|
new(code: METHOD_NOT_FOUND, message: "Client does not advertise #{capability}")
|
|
69
71
|
end
|
|
70
72
|
|
|
73
|
+
# The invalid-params code for a request whose mode the client's
|
|
74
|
+
# capabilities do not advertise, with a message that names the mode so it
|
|
75
|
+
# can be told apart from malformed params. The spec gives elicitation a
|
|
76
|
+
# mode in its params, so an unadvertised one is invalid params rather than
|
|
77
|
+
# method not found.
|
|
78
|
+
#
|
|
79
|
+
# @rbs mode: String
|
|
80
|
+
# @rbs return: ACP::RequestError
|
|
81
|
+
def self.unadvertised_mode(mode)
|
|
82
|
+
new(code: INVALID_PARAMS, message: "Client does not advertise #{mode}")
|
|
83
|
+
end
|
|
84
|
+
|
|
71
85
|
# @rbs return: ACP::RequestError
|
|
72
86
|
def self.internal_error
|
|
73
87
|
new(code: INTERNAL_ERROR, message: 'Internal error')
|
data/lib/acp/transport/stdio.rb
CHANGED
|
@@ -92,7 +92,7 @@ class ACP::Transport::Stdio
|
|
|
92
92
|
method = message['method']
|
|
93
93
|
error = message['error']
|
|
94
94
|
if method.is_a?(String) && message.key?('id')
|
|
95
|
-
serve(requests[method], message['id'], message['params'])
|
|
95
|
+
serve(requests[method], message['id'], message['params'], method)
|
|
96
96
|
elsif method.is_a?(String)
|
|
97
97
|
# Run on the reader thread so notifications (session/update) keep their
|
|
98
98
|
# order; a JSON-RPC notification has no reply to carry a handler error.
|
|
@@ -104,10 +104,21 @@ class ACP::Transport::Stdio
|
|
|
104
104
|
code, text, data = error.values_at('code', 'message', 'data')
|
|
105
105
|
settle(message['id'], ACP::RequestError.new(code:, message: text, data:))
|
|
106
106
|
else
|
|
107
|
-
reply(
|
|
107
|
+
reply(detectable_id(message), ACP::RequestError.invalid_request)
|
|
108
108
|
end
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
+
# JSON-RPC 2.0 says an error reply echoes the request id whenever it can be
|
|
112
|
+
# detected; ids outside the spec's String/Number/Null types are not, so
|
|
113
|
+
# those replies keep id null.
|
|
114
|
+
#
|
|
115
|
+
# @rbs message: Hash[String, untyped]
|
|
116
|
+
# @rbs return: untyped
|
|
117
|
+
def detectable_id(message)
|
|
118
|
+
id = message['id']
|
|
119
|
+
id.is_a?(String) || id.is_a?(Numeric) ? id : nil
|
|
120
|
+
end
|
|
121
|
+
|
|
111
122
|
# @rbs line: String
|
|
112
123
|
# @rbs return: Hash[String, untyped] | ACP::RequestError
|
|
113
124
|
def parse(line)
|
|
@@ -122,10 +133,11 @@ class ACP::Transport::Stdio
|
|
|
122
133
|
# @rbs handler: _Handler?
|
|
123
134
|
# @rbs id: untyped
|
|
124
135
|
# @rbs params: untyped
|
|
136
|
+
# @rbs method: String
|
|
125
137
|
# @rbs return: void
|
|
126
|
-
def serve(handler, id, params)
|
|
138
|
+
def serve(handler, id, params, method)
|
|
127
139
|
Thread.new do
|
|
128
|
-
outcome = handler ? invoke(handler, params) : ACP::RequestError.method_not_found
|
|
140
|
+
outcome = handler ? invoke(handler, params) : ACP::RequestError.method_not_found(method)
|
|
129
141
|
case outcome
|
|
130
142
|
when ACP::Transport::Reply
|
|
131
143
|
reply(id, outcome.result)
|
|
@@ -149,7 +161,9 @@ class ACP::Transport::Stdio
|
|
|
149
161
|
end
|
|
150
162
|
|
|
151
163
|
# Handlers are application code at the protocol boundary: an exception
|
|
152
|
-
# becomes an error response instead of killing the thread serving it.
|
|
164
|
+
# becomes an error response instead of killing the thread serving it. The
|
|
165
|
+
# exception is only logged, not sent: its message can carry paths, SQL or
|
|
166
|
+
# anything else the handler interpolated, which must not reach the peer.
|
|
153
167
|
#
|
|
154
168
|
# @rbs handler: _Handler
|
|
155
169
|
# @rbs params: untyped
|
|
@@ -157,7 +171,8 @@ class ACP::Transport::Stdio
|
|
|
157
171
|
def invoke(handler, params)
|
|
158
172
|
handler.call(params)
|
|
159
173
|
rescue StandardError => e
|
|
160
|
-
|
|
174
|
+
@logger.error("request handler raised #{e.class}: #{e.message}")
|
|
175
|
+
ACP::RequestError.internal_error
|
|
161
176
|
end
|
|
162
177
|
|
|
163
178
|
# The success arm is whatever the handler returned, which the transport only
|
|
@@ -21,7 +21,10 @@ class ACP::Types::AgentAuthCapabilities
|
|
|
21
21
|
# @rbs hash: Hash[String, untyped]
|
|
22
22
|
# @rbs return: ACP::Types::AgentAuthCapabilities
|
|
23
23
|
def self.from_h(hash)
|
|
24
|
-
new(
|
|
24
|
+
new(
|
|
25
|
+
logout: ACP::Types::Check.object(hash['logout'], 'logout', ACP::Types::LogoutCapabilities, allow_nil: true),
|
|
26
|
+
meta: hash['_meta']
|
|
27
|
+
)
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
# @rbs return: Hash[String, untyped]
|
|
@@ -49,11 +49,26 @@ class ACP::Types::AgentCapabilities
|
|
|
49
49
|
# @rbs return: ACP::Types::AgentCapabilities
|
|
50
50
|
def self.from_h(hash)
|
|
51
51
|
new(
|
|
52
|
-
load_session: hash['loadSession'],
|
|
53
|
-
prompt_capabilities:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
load_session: ACP::Types::Check.boolean(hash['loadSession'], 'loadSession', allow_nil: true),
|
|
53
|
+
prompt_capabilities: ACP::Types::Check.object(
|
|
54
|
+
hash['promptCapabilities'],
|
|
55
|
+
'promptCapabilities',
|
|
56
|
+
ACP::Types::PromptCapabilities,
|
|
57
|
+
allow_nil: true
|
|
58
|
+
),
|
|
59
|
+
mcp_capabilities: ACP::Types::Check.object(
|
|
60
|
+
hash['mcpCapabilities'],
|
|
61
|
+
'mcpCapabilities',
|
|
62
|
+
ACP::Types::McpCapabilities,
|
|
63
|
+
allow_nil: true
|
|
64
|
+
),
|
|
65
|
+
session_capabilities: ACP::Types::Check.object(
|
|
66
|
+
hash['sessionCapabilities'],
|
|
67
|
+
'sessionCapabilities',
|
|
68
|
+
ACP::Types::SessionCapabilities,
|
|
69
|
+
allow_nil: true
|
|
70
|
+
),
|
|
71
|
+
auth: ACP::Types::Check.object(hash['auth'], 'auth', ACP::Types::AgentAuthCapabilities, allow_nil: true),
|
|
57
72
|
meta: hash['_meta']
|
|
58
73
|
)
|
|
59
74
|
end
|
|
@@ -31,10 +31,13 @@ class ACP::Types::Annotations
|
|
|
31
31
|
# @rbs hash: Hash[String, untyped]
|
|
32
32
|
# @rbs return: ACP::Types::Annotations
|
|
33
33
|
def self.from_h(hash)
|
|
34
|
+
audience = ACP::Types::Check.array(hash['audience'], 'audience', allow_nil: true) do |item, path|
|
|
35
|
+
ACP::Types::Check.enum(item, path, %w[assistant user])
|
|
36
|
+
end
|
|
34
37
|
new(
|
|
35
|
-
audience
|
|
36
|
-
last_modified: hash['lastModified'],
|
|
37
|
-
priority: hash['priority'],
|
|
38
|
+
audience:,
|
|
39
|
+
last_modified: ACP::Types::Check.string(hash['lastModified'], 'lastModified', allow_nil: true),
|
|
40
|
+
priority: ACP::Types::Check.number(hash['priority'], 'priority', allow_nil: true),
|
|
38
41
|
meta: hash['_meta']
|
|
39
42
|
)
|
|
40
43
|
end
|
|
@@ -32,9 +32,14 @@ class ACP::Types::AudioContent
|
|
|
32
32
|
# @rbs return: ACP::Types::AudioContent
|
|
33
33
|
def self.from_h(hash)
|
|
34
34
|
new(
|
|
35
|
-
annotations:
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
annotations: ACP::Types::Check.object(
|
|
36
|
+
hash['annotations'],
|
|
37
|
+
'annotations',
|
|
38
|
+
ACP::Types::Annotations,
|
|
39
|
+
allow_nil: true
|
|
40
|
+
),
|
|
41
|
+
data: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'data'), 'data'),
|
|
42
|
+
mime_type: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'mimeType'), 'mimeType'),
|
|
38
43
|
meta: hash['_meta']
|
|
39
44
|
)
|
|
40
45
|
end
|
|
@@ -21,7 +21,7 @@ class ACP::Types::AuthCapabilities
|
|
|
21
21
|
# @rbs hash: Hash[String, untyped]
|
|
22
22
|
# @rbs return: ACP::Types::AuthCapabilities
|
|
23
23
|
def self.from_h(hash)
|
|
24
|
-
new(terminal: hash['terminal'], meta: hash['_meta'])
|
|
24
|
+
new(terminal: ACP::Types::Check.boolean(hash['terminal'], 'terminal', allow_nil: true), meta: hash['_meta'])
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# @rbs return: Hash[String, untyped]
|
|
@@ -41,12 +41,18 @@ class ACP::Types::AuthMethod::Terminal
|
|
|
41
41
|
# @rbs hash: Hash[String, untyped]
|
|
42
42
|
# @rbs return: ACP::Types::AuthMethod::Terminal
|
|
43
43
|
def self.from_h(hash)
|
|
44
|
+
args = ACP::Types::Check.array(hash['args'], 'args', allow_nil: true) do |item, path|
|
|
45
|
+
ACP::Types::Check.string(item, path)
|
|
46
|
+
end
|
|
47
|
+
env = ACP::Types::Check.hash(hash['env'], 'env', allow_nil: true) do |value, path|
|
|
48
|
+
ACP::Types::Check.string(value, path)
|
|
49
|
+
end
|
|
44
50
|
new(
|
|
45
|
-
id:
|
|
46
|
-
name:
|
|
47
|
-
description: hash['description'],
|
|
48
|
-
args
|
|
49
|
-
env
|
|
51
|
+
id: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'id'), 'id'),
|
|
52
|
+
name: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'name'), 'name'),
|
|
53
|
+
description: ACP::Types::Check.string(hash['description'], 'description', allow_nil: true),
|
|
54
|
+
args:,
|
|
55
|
+
env:,
|
|
50
56
|
meta: hash['_meta']
|
|
51
57
|
)
|
|
52
58
|
end
|