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.
Files changed (157) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +5 -2
  4. data/lib/acp/agent_connection/client.rb +1 -1
  5. data/lib/acp/agent_connection.rb +3 -1
  6. data/lib/acp/client_connection.rb +220 -4
  7. data/lib/acp/request_error.rb +18 -4
  8. data/lib/acp/transport/stdio.rb +21 -6
  9. data/lib/acp/types/agent_auth_capabilities.rb +4 -1
  10. data/lib/acp/types/agent_capabilities.rb +20 -5
  11. data/lib/acp/types/annotations.rb +6 -3
  12. data/lib/acp/types/audio_content.rb +8 -3
  13. data/lib/acp/types/auth_capabilities.rb +1 -1
  14. data/lib/acp/types/auth_method/terminal.rb +11 -5
  15. data/lib/acp/types/auth_method_agent.rb +6 -1
  16. data/lib/acp/types/auth_method_terminal.rb +11 -5
  17. data/lib/acp/types/authenticate_request.rb +1 -1
  18. data/lib/acp/types/available_command.rb +3 -3
  19. data/lib/acp/types/available_commands_update.rb +7 -4
  20. data/lib/acp/types/blob_resource_contents.rb +6 -1
  21. data/lib/acp/types/boolean_property_schema.rb +6 -1
  22. data/lib/acp/types/cancel_notification.rb +4 -1
  23. data/lib/acp/types/cancel_request_notification.rb +9 -1
  24. data/lib/acp/types/client_capabilities.rb +15 -5
  25. data/lib/acp/types/client_session_capabilities.rb +6 -3
  26. data/lib/acp/types/close_session_request.rb +4 -1
  27. data/lib/acp/types/complete_elicitation_notification.rb +4 -1
  28. data/lib/acp/types/config_option_update.rb +7 -4
  29. data/lib/acp/types/content.rb +4 -1
  30. data/lib/acp/types/content_block/audio.rb +8 -3
  31. data/lib/acp/types/content_block/image.rb +9 -4
  32. data/lib/acp/types/content_block/resource.rb +11 -2
  33. data/lib/acp/types/content_block/resource_link.rb +12 -7
  34. data/lib/acp/types/content_block/text.rb +7 -2
  35. data/lib/acp/types/content_chunk.rb +2 -2
  36. data/lib/acp/types/cost.rb +5 -1
  37. data/lib/acp/types/create_elicitation_request/mode/form.rb +5 -1
  38. data/lib/acp/types/create_elicitation_request/mode/url.rb +2 -2
  39. data/lib/acp/types/create_elicitation_request.rb +1 -1
  40. data/lib/acp/types/create_elicitation_response/action/accept.rb +6 -1
  41. data/lib/acp/types/create_terminal_request.rb +12 -6
  42. data/lib/acp/types/create_terminal_response.rb +4 -1
  43. data/lib/acp/types/current_mode_update.rb +4 -1
  44. data/lib/acp/types/delete_session_request.rb +4 -1
  45. data/lib/acp/types/diff.rb +6 -1
  46. data/lib/acp/types/elicitation_accept_action.rb +6 -1
  47. data/lib/acp/types/elicitation_capabilities.rb +2 -2
  48. data/lib/acp/types/elicitation_form_mode.rb +5 -1
  49. data/lib/acp/types/elicitation_property_schema/boolean_property_schema.rb +6 -1
  50. data/lib/acp/types/elicitation_property_schema/integer_property_schema.rb +5 -5
  51. data/lib/acp/types/elicitation_property_schema/multi_select_property_schema.rb +9 -6
  52. data/lib/acp/types/elicitation_property_schema/number_property_schema.rb +5 -5
  53. data/lib/acp/types/elicitation_property_schema/string_property_schema.rb +15 -9
  54. data/lib/acp/types/elicitation_request_scope.rb +8 -1
  55. data/lib/acp/types/elicitation_schema.rb +11 -5
  56. data/lib/acp/types/elicitation_session_scope.rb +4 -1
  57. data/lib/acp/types/elicitation_url_mode.rb +2 -2
  58. data/lib/acp/types/embedded_resource.rb +11 -2
  59. data/lib/acp/types/enum_option.rb +6 -1
  60. data/lib/acp/types/env_variable.rb +5 -1
  61. data/lib/acp/types/file_system_capabilities.rb +5 -1
  62. data/lib/acp/types/http_header.rb +5 -1
  63. data/lib/acp/types/image_content.rb +9 -4
  64. data/lib/acp/types/implementation.rb +6 -1
  65. data/lib/acp/types/initialize_request.rb +18 -3
  66. data/lib/acp/types/initialize_response.rb +17 -4
  67. data/lib/acp/types/integer_property_schema.rb +5 -5
  68. data/lib/acp/types/kill_terminal_request.rb +5 -1
  69. data/lib/acp/types/list_sessions_request.rb +5 -1
  70. data/lib/acp/types/list_sessions_response.rb +5 -2
  71. data/lib/acp/types/load_session_request.rb +14 -4
  72. data/lib/acp/types/load_session_response.rb +5 -2
  73. data/lib/acp/types/mcp_capabilities.rb +5 -1
  74. data/lib/acp/types/mcp_server/http.rb +6 -3
  75. data/lib/acp/types/mcp_server/sse.rb +6 -3
  76. data/lib/acp/types/mcp_server_http.rb +6 -3
  77. data/lib/acp/types/mcp_server_sse.rb +6 -3
  78. data/lib/acp/types/mcp_server_stdio.rb +10 -4
  79. data/lib/acp/types/multi_select_items/string_multi_select_items.rb +4 -1
  80. data/lib/acp/types/multi_select_property_schema.rb +9 -6
  81. data/lib/acp/types/new_session_request.rb +13 -3
  82. data/lib/acp/types/new_session_response.rb +6 -3
  83. data/lib/acp/types/number_property_schema.rb +5 -5
  84. data/lib/acp/types/permission_option.rb +10 -1
  85. data/lib/acp/types/plan.rb +4 -1
  86. data/lib/acp/types/plan_entry.rb +7 -3
  87. data/lib/acp/types/prompt_capabilities.rb +6 -1
  88. data/lib/acp/types/prompt_request.rb +5 -2
  89. data/lib/acp/types/prompt_response.rb +8 -1
  90. data/lib/acp/types/read_text_file_request.rb +4 -4
  91. data/lib/acp/types/read_text_file_response.rb +1 -1
  92. data/lib/acp/types/release_terminal_request.rb +5 -1
  93. data/lib/acp/types/request_permission_outcome/selected.rb +1 -1
  94. data/lib/acp/types/request_permission_request.rb +10 -3
  95. data/lib/acp/types/request_permission_response.rb +8 -1
  96. data/lib/acp/types/resource_link.rb +12 -7
  97. data/lib/acp/types/resume_session_request.rb +14 -4
  98. data/lib/acp/types/resume_session_response.rb +5 -2
  99. data/lib/acp/types/selected_permission_outcome.rb +1 -1
  100. data/lib/acp/types/session_capabilities.rb +20 -7
  101. data/lib/acp/types/session_config_boolean.rb +1 -1
  102. data/lib/acp/types/session_config_option/kind/boolean.rb +1 -1
  103. data/lib/acp/types/session_config_option/kind/select.rb +6 -2
  104. data/lib/acp/types/session_config_option.rb +3 -3
  105. data/lib/acp/types/session_config_options_capabilities.rb +6 -1
  106. data/lib/acp/types/session_config_select.rb +6 -2
  107. data/lib/acp/types/session_config_select_group.rb +6 -3
  108. data/lib/acp/types/session_config_select_option.rb +6 -1
  109. data/lib/acp/types/session_config_select_options.rb +6 -2
  110. data/lib/acp/types/session_info.rb +12 -5
  111. data/lib/acp/types/session_info_update.rb +10 -1
  112. data/lib/acp/types/session_mode.rb +6 -1
  113. data/lib/acp/types/session_mode_state.rb +8 -2
  114. data/lib/acp/types/session_notification.rb +2 -2
  115. data/lib/acp/types/session_update/agent_message_chunk.rb +2 -2
  116. data/lib/acp/types/session_update/agent_thought_chunk.rb +2 -2
  117. data/lib/acp/types/session_update/available_commands_update.rb +7 -4
  118. data/lib/acp/types/session_update/config_option_update.rb +7 -4
  119. data/lib/acp/types/session_update/current_mode_update.rb +4 -1
  120. data/lib/acp/types/session_update/plan.rb +4 -1
  121. data/lib/acp/types/session_update/session_info_update.rb +10 -1
  122. data/lib/acp/types/session_update/tool_call.rb +23 -7
  123. data/lib/acp/types/session_update/tool_call_update.rb +23 -7
  124. data/lib/acp/types/session_update/usage_update.rb +3 -3
  125. data/lib/acp/types/session_update/user_message_chunk.rb +2 -2
  126. data/lib/acp/types/set_session_config_option_request/value/boolean.rb +1 -1
  127. data/lib/acp/types/set_session_config_option_request/value/value_id.rb +1 -1
  128. data/lib/acp/types/set_session_config_option_request.rb +2 -2
  129. data/lib/acp/types/set_session_config_option_response.rb +7 -4
  130. data/lib/acp/types/set_session_mode_request.rb +5 -1
  131. data/lib/acp/types/string_multi_select_items.rb +4 -1
  132. data/lib/acp/types/string_property_schema.rb +15 -9
  133. data/lib/acp/types/terminal.rb +4 -1
  134. data/lib/acp/types/terminal_exit_status.rb +5 -1
  135. data/lib/acp/types/terminal_output_request.rb +5 -1
  136. data/lib/acp/types/terminal_output_response.rb +8 -3
  137. data/lib/acp/types/text_content.rb +7 -2
  138. data/lib/acp/types/text_resource_contents.rb +6 -1
  139. data/lib/acp/types/titled_multi_select_items.rb +4 -1
  140. data/lib/acp/types/tool_call.rb +23 -7
  141. data/lib/acp/types/tool_call_content/content.rb +4 -1
  142. data/lib/acp/types/tool_call_content/diff.rb +6 -1
  143. data/lib/acp/types/tool_call_content/terminal.rb +4 -1
  144. data/lib/acp/types/tool_call_location.rb +5 -1
  145. data/lib/acp/types/tool_call_update.rb +23 -7
  146. data/lib/acp/types/unstructured_command_input.rb +1 -1
  147. data/lib/acp/types/usage_update.rb +3 -3
  148. data/lib/acp/types/wait_for_terminal_exit_request.rb +5 -1
  149. data/lib/acp/types/wait_for_terminal_exit_response.rb +5 -1
  150. data/lib/acp/types/write_text_file_request.rb +3 -3
  151. data/lib/acp/types.rb +247 -0
  152. data/lib/acp/version.rb +1 -1
  153. data/sig/generated/acp/client_connection.rbs +101 -12
  154. data/sig/generated/acp/request_error.rbs +14 -2
  155. data/sig/generated/acp/transport/stdio.rbs +13 -2
  156. data/sig/generated/acp/types.rbs +143 -0
  157. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 367678f3b99c82b2f768879be0308249d9b32ce8ce94c3d4e6de6c12c4a1c043
4
- data.tar.gz: 071e457b8e00fa19be760fff65cfd69998102e54b4b3d47f9b3cc0d80c359fc3
3
+ metadata.gz: e0b8b181c4cbd41a411868aecdf9f34d0356b490526c446a962287a5a3ae01cc
4
+ data.tar.gz: 1376e6d42bc4369de51a645ed423a563e7aa9b35c4c49b060c77a983cb2c603c
5
5
  SHA512:
6
- metadata.gz: 706206bd29062ef6ecf1856b9e9671113e02bb6eb6ac837c2815bfcdbbff4bc07ac8d2c6394ec6f7b33c37a3832a9e0c4bb5c4e5c4699c1cdc129e875f693605
7
- data.tar.gz: 5a73a945d099dedcf5837aa1c897b1dca0313a49d131337756b26c28731ddedb14b96c21722c7d6ecff3a21e6bed811e036551724c12bebcdf53f88a56121f8e
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
 
@@ -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) }.merge(@extension_notifications)
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
@@ -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')
@@ -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(nil, ACP::RequestError.invalid_request)
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
- ACP::RequestError.new(code: ACP::RequestError::INTERNAL_ERROR, message: e.message)
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(logout: hash['logout']&.then { |value| ACP::Types::LogoutCapabilities.from_h(value) }, meta: hash['_meta'])
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: hash['promptCapabilities']&.then { |value| ACP::Types::PromptCapabilities.from_h(value) },
54
- mcp_capabilities: hash['mcpCapabilities']&.then { |value| ACP::Types::McpCapabilities.from_h(value) },
55
- session_capabilities: hash['sessionCapabilities']&.then { |value| ACP::Types::SessionCapabilities.from_h(value) },
56
- auth: hash['auth']&.then { |value| ACP::Types::AgentAuthCapabilities.from_h(value) },
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: hash['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: hash['annotations']&.then { |value| ACP::Types::Annotations.from_h(value) },
36
- data: hash.fetch('data'),
37
- mime_type: hash.fetch('mimeType'),
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: hash.fetch('id'),
46
- name: hash.fetch('name'),
47
- description: hash['description'],
48
- args: hash['args'],
49
- env: hash['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