acp-sdk 0.10.0 → 0.12.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 (187) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/README.md +19 -5
  4. data/lib/acp/agent_connection/client.rb +7 -19
  5. data/lib/acp/agent_connection/optional_method/spec.rb +42 -0
  6. data/lib/acp/agent_connection/optional_method.rb +36 -0
  7. data/lib/acp/agent_connection.rb +17 -40
  8. data/lib/acp/agent_process.rb +0 -13
  9. data/lib/acp/client_connection/streams.rb +183 -0
  10. data/lib/acp/client_connection.rb +227 -384
  11. data/lib/acp/protocol.rb +33 -0
  12. data/lib/acp/request_error.rb +17 -17
  13. data/lib/acp/transport/cancellation.rb +0 -7
  14. data/lib/acp/transport/reply.rb +0 -2
  15. data/lib/acp/transport/stderr_logger.rb +1 -2
  16. data/lib/acp/transport/stdio/serving.rb +18 -0
  17. data/lib/acp/transport/stdio.rb +67 -59
  18. data/lib/acp/types/audio_content.rb +3 -46
  19. data/lib/acp/types/auth_method_terminal.rb +3 -59
  20. data/lib/acp/types/available_commands_update.rb +3 -32
  21. data/lib/acp/types/boolean_property_schema.rb +3 -41
  22. data/lib/acp/types/check.rb +210 -0
  23. data/lib/acp/types/config_option_update.rb +3 -32
  24. data/lib/acp/types/content.rb +3 -29
  25. data/lib/acp/types/{elicitation_url_mode → create_elicitation_request/mode/form}/scope.rb +1 -1
  26. data/lib/acp/types/create_elicitation_request/mode/form.rb +3 -3
  27. data/lib/acp/types/{elicitation_form_mode → create_elicitation_request/mode/url}/scope.rb +1 -1
  28. data/lib/acp/types/create_elicitation_request/mode/url.rb +3 -3
  29. data/lib/acp/types/current_mode_update.rb +3 -29
  30. data/lib/acp/types/diff.rb +3 -41
  31. data/lib/acp/types/elicitation_accept_action.rb +3 -26
  32. data/lib/acp/types/elicitation_form_mode.rb +3 -33
  33. data/lib/acp/types/elicitation_url_mode.rb +3 -35
  34. data/lib/acp/types/embedded_resource.rb +3 -44
  35. data/lib/acp/types/image_content.rb +3 -58
  36. data/lib/acp/types/integer_property_schema.rb +3 -60
  37. data/lib/acp/types/mcp_server_http.rb +3 -44
  38. data/lib/acp/types/mcp_server_sse.rb +3 -44
  39. data/lib/acp/types/multi_select_property_schema.rb +3 -70
  40. data/lib/acp/types/number_property_schema.rb +3 -60
  41. data/lib/acp/types/parse_error.rb +37 -0
  42. data/lib/acp/types/plan.rb +3 -29
  43. data/lib/acp/types/resource_link.rb +3 -79
  44. data/lib/acp/types/selected_permission_outcome.rb +3 -26
  45. data/lib/acp/types/session_config_boolean.rb +3 -21
  46. data/lib/acp/types/session_config_select.rb +3 -33
  47. data/lib/acp/types/session_info_update.rb +3 -42
  48. data/lib/acp/types/string_multi_select_items.rb +3 -29
  49. data/lib/acp/types/string_property_schema.rb +3 -105
  50. data/lib/acp/types/terminal.rb +3 -29
  51. data/lib/acp/types/text_content.rb +3 -40
  52. data/lib/acp/types/tool_call.rb +3 -115
  53. data/lib/acp/types/unstable/compaction_summary_chunk.rb +3 -35
  54. data/lib/acp/types/unstable/compaction_update.rb +3 -56
  55. data/lib/acp/types/unstable/idle_state_update.rb +3 -40
  56. data/lib/acp/types/unstable/mcp_server_acp.rb +3 -35
  57. data/lib/acp/types/unstable/nes_edit_suggestion.rb +3 -61
  58. data/lib/acp/types/unstable/nes_jump_suggestion.rb +3 -45
  59. data/lib/acp/types/unstable/nes_rename_suggestion.rb +3 -51
  60. data/lib/acp/types/unstable/nes_search_and_replace_suggestion.rb +3 -60
  61. data/lib/acp/types/unstable/notice.rb +3 -41
  62. data/lib/acp/types/unstable/plan_file.rb +3 -35
  63. data/lib/acp/types/unstable/plan_items.rb +3 -38
  64. data/lib/acp/types/unstable/plan_markdown.rb +3 -35
  65. data/lib/acp/types/unstable/plan_removed.rb +3 -26
  66. data/lib/acp/types/unstable/plan_update.rb +3 -33
  67. data/lib/acp/types/unstable/requires_action_state_update.rb +3 -21
  68. data/lib/acp/types/unstable/running_state_update.rb +3 -21
  69. data/lib/acp/types/unstable/session_message.rb +3 -56
  70. data/lib/acp/types/unstable/session_message_chunk.rb +3 -53
  71. data/lib/acp/types/unstable/subagent_update.rb +3 -65
  72. data/lib/acp/types/unstable/unknown_state_update.rb +3 -21
  73. data/lib/acp/types/unstable.rb +20 -20
  74. data/lib/acp/types/usage_update.rb +3 -41
  75. data/lib/acp/types.rb +7 -246
  76. data/lib/acp/version.rb +1 -1
  77. data/sig/generated/acp/agent_connection/client.rbs +10 -19
  78. data/sig/generated/acp/agent_connection/optional_method/spec.rbs +32 -0
  79. data/sig/generated/acp/agent_connection/optional_method.rbs +3 -0
  80. data/sig/generated/acp/agent_connection.rbs +16 -24
  81. data/sig/generated/acp/agent_process.rbs +0 -13
  82. data/sig/generated/acp/client_connection/streams.rbs +94 -0
  83. data/sig/generated/acp/client_connection.rbs +100 -159
  84. data/sig/generated/acp/protocol.rbs +9 -0
  85. data/sig/generated/acp/request_error.rbs +15 -17
  86. data/sig/generated/acp/transport/cancellation.rbs +0 -7
  87. data/sig/generated/acp/transport/reply.rbs +0 -2
  88. data/sig/generated/acp/transport/stderr_logger.rbs +1 -2
  89. data/sig/generated/acp/transport/stdio/serving.rbs +14 -0
  90. data/sig/generated/acp/transport/stdio.rbs +31 -35
  91. data/sig/generated/acp/types/audio_content.rbs +1 -27
  92. data/sig/generated/acp/types/auth_method_terminal.rbs +1 -35
  93. data/sig/generated/acp/types/available_commands_update.rbs +1 -19
  94. data/sig/generated/acp/types/boolean_property_schema.rbs +1 -27
  95. data/sig/generated/acp/types/check.rbs +115 -0
  96. data/sig/generated/acp/types/config_option_update.rbs +1 -19
  97. data/sig/generated/acp/types/content.rbs +1 -19
  98. data/sig/generated/acp/types/{elicitation_url_mode → create_elicitation_request/mode/form}/scope.rbs +2 -2
  99. data/sig/generated/acp/types/create_elicitation_request/mode/form.rbs +3 -3
  100. data/sig/generated/acp/types/{elicitation_form_mode → create_elicitation_request/mode/url}/scope.rbs +2 -2
  101. data/sig/generated/acp/types/create_elicitation_request/mode/url.rbs +3 -3
  102. data/sig/generated/acp/types/current_mode_update.rbs +1 -19
  103. data/sig/generated/acp/types/diff.rbs +1 -27
  104. data/sig/generated/acp/types/elicitation_accept_action.rbs +1 -15
  105. data/sig/generated/acp/types/elicitation_form_mode.rbs +1 -19
  106. data/sig/generated/acp/types/elicitation_url_mode.rbs +1 -23
  107. data/sig/generated/acp/types/embedded_resource.rbs +1 -23
  108. data/sig/generated/acp/types/image_content.rbs +1 -31
  109. data/sig/generated/acp/types/integer_property_schema.rbs +1 -35
  110. data/sig/generated/acp/types/mcp_server_http.rbs +1 -27
  111. data/sig/generated/acp/types/mcp_server_sse.rbs +1 -27
  112. data/sig/generated/acp/types/multi_select_property_schema.rbs +1 -39
  113. data/sig/generated/acp/types/number_property_schema.rbs +1 -35
  114. data/sig/generated/acp/types/parse_error.rbs +22 -0
  115. data/sig/generated/acp/types/plan.rbs +1 -19
  116. data/sig/generated/acp/types/resource_link.rbs +1 -43
  117. data/sig/generated/acp/types/selected_permission_outcome.rbs +1 -19
  118. data/sig/generated/acp/types/session_config_boolean.rbs +1 -15
  119. data/sig/generated/acp/types/session_config_select.rbs +1 -19
  120. data/sig/generated/acp/types/session_info_update.rbs +1 -23
  121. data/sig/generated/acp/types/string_multi_select_items.rbs +1 -19
  122. data/sig/generated/acp/types/string_property_schema.rbs +1 -51
  123. data/sig/generated/acp/types/terminal.rbs +1 -19
  124. data/sig/generated/acp/types/text_content.rbs +1 -23
  125. data/sig/generated/acp/types/tool_call.rbs +1 -51
  126. data/sig/generated/acp/types/unstable/compaction_summary_chunk.rbs +1 -23
  127. data/sig/generated/acp/types/unstable/compaction_update.rbs +1 -31
  128. data/sig/generated/acp/types/unstable/idle_state_update.rbs +1 -23
  129. data/sig/generated/acp/types/unstable/mcp_server_acp.rbs +1 -23
  130. data/sig/generated/acp/types/unstable/nes_edit_suggestion.rbs +1 -31
  131. data/sig/generated/acp/types/unstable/nes_jump_suggestion.rbs +1 -27
  132. data/sig/generated/acp/types/unstable/nes_rename_suggestion.rbs +1 -31
  133. data/sig/generated/acp/types/unstable/nes_search_and_replace_suggestion.rbs +1 -35
  134. data/sig/generated/acp/types/unstable/notice.rbs +1 -27
  135. data/sig/generated/acp/types/unstable/plan_file.rbs +1 -23
  136. data/sig/generated/acp/types/unstable/plan_items.rbs +1 -23
  137. data/sig/generated/acp/types/unstable/plan_markdown.rbs +1 -23
  138. data/sig/generated/acp/types/unstable/plan_removed.rbs +1 -19
  139. data/sig/generated/acp/types/unstable/plan_update.rbs +1 -19
  140. data/sig/generated/acp/types/unstable/requires_action_state_update.rbs +1 -15
  141. data/sig/generated/acp/types/unstable/running_state_update.rbs +1 -15
  142. data/sig/generated/acp/types/unstable/session_message.rbs +1 -31
  143. data/sig/generated/acp/types/unstable/session_message_chunk.rbs +1 -31
  144. data/sig/generated/acp/types/unstable/subagent_update.rbs +1 -35
  145. data/sig/generated/acp/types/unstable/unknown_state_update.rbs +1 -15
  146. data/sig/generated/acp/types/usage_update.rbs +1 -27
  147. data/sig/generated/acp/types.rbs +6 -143
  148. data/sig/manual/acp/agent_connection.rbs +0 -30
  149. data/sig/manual/acp/client_connection.rbs +7 -2
  150. data/sig/manual/acp/{agent_connection/optional_method/logout.rbs → protocol.rbs} +3 -3
  151. data/sig/manual/acp/transport/stdio.rbs +0 -2
  152. data/sig/manual/acp/transport.rbs +25 -2
  153. metadata +18 -40
  154. data/lib/acp/agent_connection/optional_method/authenticate.rb +0 -21
  155. data/lib/acp/agent_connection/optional_method/close_session.rb +0 -21
  156. data/lib/acp/agent_connection/optional_method/delete_session.rb +0 -21
  157. data/lib/acp/agent_connection/optional_method/disable_provider.rb +0 -23
  158. data/lib/acp/agent_connection/optional_method/fork_session.rb +0 -23
  159. data/lib/acp/agent_connection/optional_method/list_providers.rb +0 -23
  160. data/lib/acp/agent_connection/optional_method/list_sessions.rb +0 -21
  161. data/lib/acp/agent_connection/optional_method/load_session.rb +0 -21
  162. data/lib/acp/agent_connection/optional_method/logout.rb +0 -21
  163. data/lib/acp/agent_connection/optional_method/resume_session.rb +0 -21
  164. data/lib/acp/agent_connection/optional_method/set_provider.rb +0 -23
  165. data/lib/acp/types/content_chunk.rb +0 -40
  166. data/sig/generated/acp/agent_connection/optional_method/authenticate.rbs +0 -13
  167. data/sig/generated/acp/agent_connection/optional_method/close_session.rbs +0 -13
  168. data/sig/generated/acp/agent_connection/optional_method/delete_session.rbs +0 -13
  169. data/sig/generated/acp/agent_connection/optional_method/disable_provider.rbs +0 -13
  170. data/sig/generated/acp/agent_connection/optional_method/fork_session.rbs +0 -13
  171. data/sig/generated/acp/agent_connection/optional_method/list_providers.rbs +0 -13
  172. data/sig/generated/acp/agent_connection/optional_method/list_sessions.rbs +0 -13
  173. data/sig/generated/acp/agent_connection/optional_method/load_session.rbs +0 -13
  174. data/sig/generated/acp/agent_connection/optional_method/logout.rbs +0 -13
  175. data/sig/generated/acp/agent_connection/optional_method/resume_session.rbs +0 -13
  176. data/sig/generated/acp/agent_connection/optional_method/set_provider.rbs +0 -13
  177. data/sig/generated/acp/types/content_chunk.rbs +0 -25
  178. data/sig/manual/acp/agent_connection/optional_method/authenticate.rbs +0 -6
  179. data/sig/manual/acp/agent_connection/optional_method/close_session.rbs +0 -6
  180. data/sig/manual/acp/agent_connection/optional_method/delete_session.rbs +0 -6
  181. data/sig/manual/acp/agent_connection/optional_method/disable_provider.rbs +0 -6
  182. data/sig/manual/acp/agent_connection/optional_method/fork_session.rbs +0 -6
  183. data/sig/manual/acp/agent_connection/optional_method/list_providers.rbs +0 -6
  184. data/sig/manual/acp/agent_connection/optional_method/list_sessions.rbs +0 -6
  185. data/sig/manual/acp/agent_connection/optional_method/load_session.rbs +0 -6
  186. data/sig/manual/acp/agent_connection/optional_method/resume_session.rbs +0 -6
  187. data/sig/manual/acp/agent_connection/optional_method/set_provider.rbs +0 -6
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ACP::Protocol
4
+ PROTOCOL_VERSION = 1 #: Integer
5
+
6
+ extend self
7
+
8
+ # @rbs capabilities: (ACP::Types::AgentCapabilities | ACP::Types::Unstable::AgentCapabilities | Hash[String, untyped])
9
+ # @rbs return: bool
10
+ def mcp_acp_advertised?(capabilities)
11
+ # The stable McpCapabilities has no acp field, so the gate can only read it
12
+ # when the caller opted in with `require 'acp/types/unstable'`.
13
+ return false unless ACP::Types.unstable_enabled?
14
+
15
+ mcp =
16
+ case capabilities
17
+ when ACP::Types::AgentCapabilities, ACP::Types::Unstable::AgentCapabilities
18
+ capabilities.mcp_capabilities #: ACP::Types::McpCapabilities | ACP::Types::Unstable::McpCapabilities?
19
+ when Hash
20
+ # The client's stable parse of the initialize reply drops acp, so
21
+ # mcpCapabilities is re-parsed from the raw hash with the unstable
22
+ # type. Only that sub-object: the other unstable fields arrive
23
+ # unvalidated and must not decide whether MCP is advertised.
24
+ raw = capabilities['mcpCapabilities']
25
+ ACP::Types::Unstable::McpCapabilities.from_h(raw) if raw.is_a?(Hash)
26
+ end
27
+ return false unless mcp.is_a?(ACP::Types::Unstable::McpCapabilities)
28
+
29
+ mcp.acp ? true : false
30
+ rescue ACP::Types::ParseError
31
+ false
32
+ end
33
+ end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # The schema's JSON-RPC Error object, named after the official SDKs'
4
- # RequestError: built by handlers and local refusals, and decoded from a
5
- # peer's error replies by the transport.
3
+ # Named after the official SDKs' RequestError rather than the schema's Error.
6
4
  class ACP::RequestError
7
5
  PARSE_ERROR = -32_700 #: Integer
8
6
  INVALID_REQUEST = -32_600 #: Integer
@@ -38,6 +36,16 @@ class ACP::RequestError
38
36
  { 'code' => code, 'message' => message, 'data' => data }.compact
39
37
  end
40
38
 
39
+ CONNECTION_CLOSED = new(code: INTERNAL_ERROR, message: 'Connection closed') #: ACP::RequestError
40
+
41
+ # Identity rather than code and message: a live peer can answer with the
42
+ # same pair, and that refusal must not read as a closed connection.
43
+ #
44
+ # @rbs return: bool
45
+ def connection_closed?
46
+ equal?(CONNECTION_CLOSED)
47
+ end
48
+
41
49
  # @rbs return: ACP::RequestError
42
50
  def self.parse_error
43
51
  new(code: PARSE_ERROR, message: 'Parse error')
@@ -60,9 +68,8 @@ class ACP::RequestError
60
68
  new(code: INVALID_PARAMS, message: 'Invalid params', data: errors && { errors: errors })
61
69
  end
62
70
 
63
- # The method-not-found code for a method the client's capabilities do not
64
- # advertise, with a message that names the capability so it can be told
65
- # apart from an unknown method.
71
+ # The message names the capability so the refusal can be told apart from an
72
+ # unknown method, though both use the -32601 code.
66
73
  #
67
74
  # @rbs capability: String
68
75
  # @rbs return: ACP::RequestError
@@ -70,11 +77,9 @@ class ACP::RequestError
70
77
  new(code: METHOD_NOT_FOUND, message: "Client does not advertise #{capability}")
71
78
  end
72
79
 
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.
80
+ # The message names the mode so the refusal can be told apart from malformed
81
+ # params. The spec gives elicitation a mode in its params, so an unadvertised
82
+ # one is invalid params rather than method not found.
78
83
  #
79
84
  # @rbs mode: String
80
85
  # @rbs return: ACP::RequestError
@@ -82,10 +87,6 @@ class ACP::RequestError
82
87
  new(code: INVALID_PARAMS, message: "Client does not advertise #{mode}")
83
88
  end
84
89
 
85
- # unadvertised names a capability of the client's, this one a capability of
86
- # the agent's: the same method-not-found code, with a message naming which
87
- # side's advertisement is missing.
88
- #
89
90
  # @rbs capability: String
90
91
  # @rbs return: ACP::RequestError
91
92
  def self.unadvertised_agent(capability)
@@ -106,8 +107,7 @@ class ACP::RequestError
106
107
  new(code: INTERNAL_ERROR, message: 'Invalid response', data: response)
107
108
  end
108
109
 
109
- # A local refusal of an initialize reply that names a version the SDK does
110
- # not speak; the message names both versions so logs can tell it from a
110
+ # The message names both versions so logs can tell this local refusal from a
111
111
  # peer's error.
112
112
  #
113
113
  # @rbs requested: Integer
@@ -1,15 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Whether the peer cancelled one request. The transport creates one per served
4
- # request, marks it when the peer's $/cancel_request arrives, and hands it to
5
- # the serving thread, whose handler can observe it and end the request early.
6
3
  class ACP::Transport::Cancellation
7
4
  KEY = :acp_serving_cancellation #: Symbol
8
5
 
9
- # Connections read this to expose the serving state to their handlers and
10
- # write it to carry serving identity onto a thread they spawn; nil off a
11
- # serve thread.
12
- #
13
6
  # @rbs return: ACP::Transport::Cancellation?
14
7
  def self.current
15
8
  Thread.current[KEY] #: ACP::Transport::Cancellation?
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # A request handler's outcome plus a callback the transport runs once that
4
- # outcome is on the wire, for messages the peer must only see after the reply.
5
3
  class ACP::Transport::Reply
6
4
  # @dynamic result
7
5
  attr_reader :result #: ACP::Transport::Stdio::_ToH
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # The default sink for dropped messages and swallowed errors: the spec
4
- # sanctions agents logging to stderr.
3
+ # The spec sanctions agents logging to stderr.
5
4
  class ACP::Transport::StderrLogger
6
5
  # @rbs message: String
7
6
  # @rbs return: void
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ACP::Transport::Stdio::Serving
4
+ # @dynamic id
5
+ attr_reader :id #: untyped
6
+
7
+ # @dynamic cancellation
8
+ attr_reader :cancellation #: ACP::Transport::Cancellation
9
+
10
+ # @rbs id: untyped
11
+ # @rbs cancellation: ACP::Transport::Cancellation
12
+ # @rbs return: void
13
+ def initialize(id:, cancellation:)
14
+ @id = id
15
+ @cancellation = cancellation
16
+ freeze
17
+ end
18
+ end
@@ -2,25 +2,22 @@
2
2
 
3
3
  require 'json'
4
4
 
5
- # JSON-RPC 2.0 over newline-delimited JSON on an IO pair. A reader thread
6
- # dispatches inbound messages while outbound requests are pending, and each
7
- # inbound request is served on its own thread, so a handler can make an
8
- # outbound request mid-turn and still see notifications (e.g. session/cancel)
9
- # that arrive meanwhile.
5
+ # A reader thread dispatches inbound messages while outbound requests are
6
+ # pending, and each inbound request is served on its own thread, so a handler
7
+ # can make an outbound request mid-turn and still see notifications (e.g.
8
+ # session/cancel) that arrive meanwhile.
10
9
  class ACP::Transport::Stdio
11
10
  # @rbs @input: _Reader
12
11
  # @rbs @output: _Writer
13
12
  # @rbs @write_lock: Thread::Mutex
14
13
  # @rbs @lock: Thread::Mutex
15
14
  # @rbs @pending: Hash[untyped, Thread::Queue]
16
- # @rbs @serving: Hash[untyped, ACP::Transport::Cancellation]
15
+ # @rbs @serving: Hash[Thread, ACP::Transport::Stdio::Serving]
17
16
  # @rbs @next_id: Integer
18
17
  # @rbs @closed: bool
19
18
  # @rbs @logger: ACP::Transport::_Logger
20
19
 
21
- CONNECTION_CLOSED = ACP::RequestError.new(
22
- code: ACP::RequestError::INTERNAL_ERROR, message: 'Connection closed'
23
- ) #: ACP::RequestError
20
+ CONNECTION_CLOSED = ACP::RequestError::CONNECTION_CLOSED #: ACP::RequestError
24
21
 
25
22
  # @rbs input: _Reader
26
23
  # @rbs output: _Writer
@@ -38,8 +35,12 @@ class ACP::Transport::Stdio
38
35
  @logger = logger
39
36
  end
40
37
 
41
- # Returns the reader thread, which ends at EOF on input after releasing any
42
- # pending outbound requests.
38
+ # Joining the returned thread does not wait for the requests still being
39
+ # served. EOF cancels none of them (only the peer's $/cancel_request does):
40
+ # each handler runs to completion and its reply is still written, to be
41
+ # warned and dropped if the peer is gone. A caller blocked in #request, a
42
+ # handler waiting on its own request included, is released with
43
+ # CONNECTION_CLOSED.
43
44
  #
44
45
  # @rbs requests: Hash[String, _Handler]
45
46
  # @rbs notifications: Hash[String, _NotificationHandler]
@@ -55,14 +56,16 @@ class ACP::Transport::Stdio
55
56
 
56
57
  # @rbs method: String
57
58
  # @rbs params: untyped
58
- # @rbs return: (Hash[String, untyped] | ACP::RequestError)
59
+ # @rbs return: untyped
59
60
  def request(method, params = nil)
60
61
  queue = Thread::Queue.new
61
62
  id = register(queue)
62
63
  return CONNECTION_CLOSED unless id
63
64
 
64
65
  write({ 'jsonrpc' => '2.0', 'id' => id, 'method' => method, 'params' => params }.compact)
65
- queue.pop || CONNECTION_CLOSED
66
+ result = queue.pop
67
+ # A null result pops nil just as a closed queue does.
68
+ queue.closed? ? CONNECTION_CLOSED : result
66
69
  end
67
70
 
68
71
  # @rbs method: String
@@ -90,16 +93,24 @@ class ACP::Transport::Stdio
90
93
  @lock.synchronize { @pending.keys }.each { |id| cancel(id) }
91
94
  end
92
95
 
93
- # The cancellation of the request the calling thread is serving, or nil off
94
- # a serve thread. A handler observes the peer's $/cancel_request through it
95
- # and can end early with ACP::RequestError.request_cancelled; whatever it
96
- # returns, the transport sends exactly one response.
97
- #
98
96
  # @rbs return: ACP::Transport::Cancellation?
99
97
  def cancellation
100
98
  ACP::Transport::Cancellation.current
101
99
  end
102
100
 
101
+ # @rbs return: Array[untyped]
102
+ def serving_ids
103
+ @lock.synchronize { @serving.each_value.map(&:id) }
104
+ end
105
+
106
+ # Joins the requests in flight at the call, not one the reader dispatches
107
+ # meanwhile: join the reader thread first to leave nothing in flight.
108
+ #
109
+ # @rbs return: void
110
+ def drain
111
+ @lock.synchronize { @serving.keys }.each(&:join)
112
+ end
113
+
103
114
  private
104
115
 
105
116
  # @rbs line: String
@@ -137,8 +148,12 @@ class ACP::Transport::Stdio
137
148
  elsif error.is_a?(Hash) && error['code'].is_a?(Integer) && error['message'].is_a?(String)
138
149
  code, text, data = error.values_at('code', 'message', 'data')
139
150
  settle(message['id'], ACP::RequestError.new(code:, message: text, data:))
140
- else
151
+ elsif message.key?('method')
141
152
  reply(detectable_id(message), ACP::RequestError.invalid_request)
153
+ elsif !settle(message['id'], ACP::RequestError.invalid_response(message))
154
+ # JSON-RPC responses only answer requests, so a malformed one is never
155
+ # replied to: the error could settle an unrelated request on the peer.
156
+ @logger.warn('dropped malformed response: no pending request for its id')
142
157
  end
143
158
  end
144
159
 
@@ -170,39 +185,29 @@ class ACP::Transport::Stdio
170
185
  # @rbs method: String
171
186
  # @rbs return: void
172
187
  def serve(handler, id, params, method)
173
- cancellation = register_serving(id)
174
- Thread.new do
175
- ACP::Transport::Cancellation.current = cancellation
176
- outcome = handler ? invoke(handler, params) : ACP::RequestError.method_not_found(method)
177
- case outcome
178
- when ACP::Transport::Reply
179
- reply(id, outcome.result)
180
- quietly('reply after callback', &outcome.after)
181
- else reply(id, outcome)
188
+ cancellation = ACP::Transport::Cancellation.new
189
+ # Registered on the reader thread so a cancel racing the request cannot
190
+ # be missed. The serve thread starts under the lock so it is registered
191
+ # before a drain can miss it or it can unregister itself.
192
+ @lock.synchronize do
193
+ thread = Thread.new do
194
+ ACP::Transport::Cancellation.current = cancellation
195
+ outcome = handler ? invoke(handler, params) : ACP::RequestError.method_not_found(method)
196
+ case outcome
197
+ when ACP::Transport::Reply
198
+ reply(id, outcome.result)
199
+ quietly('reply after callback', &outcome.after)
200
+ else reply(id, outcome)
201
+ end
202
+ ensure
203
+ # Unregistered only after the reply and its after callback, so drain waits
204
+ # for both and later cancels for the id are ignored.
205
+ @lock.synchronize { @serving.delete(Thread.current) }
182
206
  end
183
- ensure
184
- unregister_serving(id)
207
+ @serving[thread] = Serving.new(id:, cancellation:)
185
208
  end
186
209
  end
187
210
 
188
- # Registered before the serve thread starts so a cancel racing the request
189
- # cannot be missed, and dropped once the response is on the wire so later
190
- # cancels for the id are ignored.
191
- #
192
- # @rbs id: untyped
193
- # @rbs return: ACP::Transport::Cancellation
194
- def register_serving(id)
195
- cancellation = ACP::Transport::Cancellation.new
196
- @lock.synchronize { @serving[id] = cancellation }
197
- cancellation
198
- end
199
-
200
- # @rbs id: untyped
201
- # @rbs return: void
202
- def unregister_serving(id)
203
- @lock.synchronize { @serving.delete(id) }
204
- end
205
-
206
211
  # A notification has no reply to carry a malformed params failure, so one
207
212
  # is warned and dropped rather than answered.
208
213
  #
@@ -214,8 +219,10 @@ class ACP::Transport::Stdio
214
219
  return @logger.warn('dropped malformed $/cancel_request: missing requestId')
215
220
  end
216
221
 
217
- cancellation = @lock.synchronize { @serving[id] }
218
- cancellation&.cancel
222
+ cancellations = @lock.synchronize do
223
+ @serving.each_value.filter_map { |entry| entry.cancellation if entry.id == id }
224
+ end
225
+ cancellations.each(&:cancel)
219
226
  end
220
227
 
221
228
  # For code with no reply to carry an error: a notification handler, or a
@@ -246,9 +253,6 @@ class ACP::Transport::Stdio
246
253
  ACP::RequestError.internal_error
247
254
  end
248
255
 
249
- # The success arm is whatever the handler returned, which the transport only
250
- # knows serializes with to_h; RequestError picks the error arm of the reply.
251
- #
252
256
  # @rbs id: untyped
253
257
  # @rbs outcome: (_ToH | ACP::RequestError)
254
258
  # @rbs return: void
@@ -273,12 +277,15 @@ class ACP::Transport::Stdio
273
277
  end
274
278
 
275
279
  # @rbs id: untyped
276
- # @rbs result: (Hash[String, untyped] | ACP::RequestError)
277
- # @rbs return: void
280
+ # @rbs result: untyped
281
+ # @rbs return: Thread::Queue?
278
282
  def settle(id, result)
279
- @lock.synchronize { @pending.delete(id) }&.push(result)
283
+ @lock.synchronize { @pending.delete(id) }&.tap { |queue| queue.push(result) }
280
284
  end
281
285
 
286
+ # Leaves @serving alone on purpose: embedders rely on in-flight handlers
287
+ # running to completion after the input ends.
288
+ #
282
289
  # @rbs return: void
283
290
  def close
284
291
  @lock.synchronize do
@@ -288,8 +295,9 @@ class ACP::Transport::Stdio
288
295
  end
289
296
  end
290
297
 
291
- # A closed peer must not kill the thread writing the reply: the reader has
292
- # to keep draining input so pending requests still settle at EOF.
298
+ # A closed peer (EPIPE) or an output the embedder already closed (IOError)
299
+ # must not kill the thread writing the reply: the reader has to keep
300
+ # draining input so pending requests still settle at EOF.
293
301
  #
294
302
  # @rbs message: Hash[String, untyped]
295
303
  # @rbs return: void
@@ -299,7 +307,7 @@ class ACP::Transport::Stdio
299
307
  @output.write(line)
300
308
  @output.flush
301
309
  end
302
- rescue Errno::EPIPE => e
310
+ rescue Errno::EPIPE, IOError => e
303
311
  @logger.warn("write to closed peer: #{e.class}: #{e.message}")
304
312
  end
305
313
  end
@@ -2,50 +2,7 @@
2
2
 
3
3
  # Generated by bin/types from schema/schema.json. Do not edit.
4
4
 
5
- class ACP::Types::AudioContent
6
- # @dynamic annotations
7
- attr_reader :annotations #: ACP::Types::Annotations?
5
+ # @rbs!
6
+ # class ACP::Types::AudioContent = ACP::Types::ContentBlock::Audio
8
7
 
9
- # @dynamic data
10
- attr_reader :data #: String
11
-
12
- # @dynamic mime_type
13
- attr_reader :mime_type #: String
14
-
15
- # @dynamic meta
16
- attr_reader :meta #: Hash[String, untyped]?
17
-
18
- # @rbs annotations: ACP::Types::Annotations?
19
- # @rbs data: String
20
- # @rbs mime_type: String
21
- # @rbs meta: Hash[String, untyped]?
22
- # @rbs return: void
23
- def initialize(data:, mime_type:, annotations: nil, meta: nil)
24
- @annotations = annotations
25
- @data = data
26
- @mime_type = mime_type
27
- @meta = meta
28
- freeze
29
- end
30
-
31
- # @rbs hash: Hash[String, untyped]
32
- # @rbs return: ACP::Types::AudioContent
33
- def self.from_h(hash)
34
- new(
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'),
43
- meta: hash['_meta']
44
- )
45
- end
46
-
47
- # @rbs return: Hash[String, untyped]
48
- def to_h
49
- { 'data' => data, 'mimeType' => mime_type, 'annotations' => annotations&.to_h, '_meta' => meta }.compact
50
- end
51
- end
8
+ ACP::Types::AudioContent = ACP::Types::ContentBlock::Audio
@@ -2,63 +2,7 @@
2
2
 
3
3
  # Generated by bin/types from schema/schema.json. Do not edit.
4
4
 
5
- class ACP::Types::AuthMethodTerminal
6
- # @dynamic id
7
- attr_reader :id #: String
5
+ # @rbs!
6
+ # class ACP::Types::AuthMethodTerminal = ACP::Types::AuthMethod::Terminal
8
7
 
9
- # @dynamic name
10
- attr_reader :name #: String
11
-
12
- # @dynamic description
13
- attr_reader :description #: String?
14
-
15
- # @dynamic args
16
- attr_reader :args #: Array[String]?
17
-
18
- # @dynamic env
19
- attr_reader :env #: Hash[String, String]?
20
-
21
- # @dynamic meta
22
- attr_reader :meta #: Hash[String, untyped]?
23
-
24
- # @rbs id: String
25
- # @rbs name: String
26
- # @rbs description: String?
27
- # @rbs args: Array[String]?
28
- # @rbs env: Hash[String, String]?
29
- # @rbs meta: Hash[String, untyped]?
30
- # @rbs return: void
31
- def initialize(id:, name:, description: nil, args: nil, env: nil, meta: nil)
32
- @id = id
33
- @name = name
34
- @description = description
35
- @args = args
36
- @env = env
37
- @meta = meta
38
- freeze
39
- end
40
-
41
- # @rbs hash: Hash[String, untyped]
42
- # @rbs return: ACP::Types::AuthMethodTerminal
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
50
- new(
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:,
56
- meta: hash['_meta']
57
- )
58
- end
59
-
60
- # @rbs return: Hash[String, untyped]
61
- def to_h
62
- { 'id' => id, 'name' => name, 'description' => description, 'args' => args, 'env' => env, '_meta' => meta }.compact
63
- end
64
- end
8
+ ACP::Types::AuthMethodTerminal = ACP::Types::AuthMethod::Terminal
@@ -2,36 +2,7 @@
2
2
 
3
3
  # Generated by bin/types from schema/schema.json. Do not edit.
4
4
 
5
- class ACP::Types::AvailableCommandsUpdate
6
- # @dynamic available_commands
7
- attr_reader :available_commands #: Array[ACP::Types::AvailableCommand]
5
+ # @rbs!
6
+ # class ACP::Types::AvailableCommandsUpdate = ACP::Types::SessionUpdate::AvailableCommandsUpdate
8
7
 
9
- # @dynamic meta
10
- attr_reader :meta #: Hash[String, untyped]?
11
-
12
- # @rbs available_commands: Array[ACP::Types::AvailableCommand]
13
- # @rbs meta: Hash[String, untyped]?
14
- # @rbs return: void
15
- def initialize(available_commands:, meta: nil)
16
- @available_commands = available_commands
17
- @meta = meta
18
- freeze
19
- end
20
-
21
- # @rbs hash: Hash[String, untyped]
22
- # @rbs return: ACP::Types::AvailableCommandsUpdate
23
- def self.from_h(hash)
24
- available_commands = ACP::Types::Check.array(
25
- ACP::Types::Check.key(hash, 'availableCommands'),
26
- 'availableCommands'
27
- ) do |item, path|
28
- ACP::Types::Check.object(item, path, ACP::Types::AvailableCommand)
29
- end
30
- new(available_commands:, meta: hash['_meta'])
31
- end
32
-
33
- # @rbs return: Hash[String, untyped]
34
- def to_h
35
- { 'availableCommands' => available_commands.map(&:to_h), '_meta' => meta }.compact
36
- end
37
- end
8
+ ACP::Types::AvailableCommandsUpdate = ACP::Types::SessionUpdate::AvailableCommandsUpdate
@@ -2,45 +2,7 @@
2
2
 
3
3
  # Generated by bin/types from schema/schema.json. Do not edit.
4
4
 
5
- class ACP::Types::BooleanPropertySchema
6
- # @dynamic title
7
- attr_reader :title #: String?
5
+ # @rbs!
6
+ # class ACP::Types::BooleanPropertySchema = ACP::Types::ElicitationPropertySchema::BooleanPropertySchema
8
7
 
9
- # @dynamic description
10
- attr_reader :description #: String?
11
-
12
- # @dynamic default
13
- attr_reader :default #: bool?
14
-
15
- # @dynamic meta
16
- attr_reader :meta #: Hash[String, untyped]?
17
-
18
- # @rbs title: String?
19
- # @rbs description: String?
20
- # @rbs default: bool?
21
- # @rbs meta: Hash[String, untyped]?
22
- # @rbs return: void
23
- def initialize(title: nil, description: nil, default: nil, meta: nil)
24
- @title = title
25
- @description = description
26
- @default = default
27
- @meta = meta
28
- freeze
29
- end
30
-
31
- # @rbs hash: Hash[String, untyped]
32
- # @rbs return: ACP::Types::BooleanPropertySchema
33
- def self.from_h(hash)
34
- new(
35
- title: ACP::Types::Check.string(hash['title'], 'title', allow_nil: true),
36
- description: ACP::Types::Check.string(hash['description'], 'description', allow_nil: true),
37
- default: ACP::Types::Check.boolean(hash['default'], 'default', allow_nil: true),
38
- meta: hash['_meta']
39
- )
40
- end
41
-
42
- # @rbs return: Hash[String, untyped]
43
- def to_h
44
- { 'title' => title, 'description' => description, 'default' => default, '_meta' => meta }.compact
45
- end
46
- end
8
+ ACP::Types::BooleanPropertySchema = ACP::Types::ElicitationPropertySchema::BooleanPropertySchema