acp-sdk 0.9.0 → 0.11.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 (174) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/README.md +20 -8
  4. data/lib/acp/agent_connection/client.rb +31 -13
  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 +67 -20
  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 +280 -338
  11. data/lib/acp/protocol.rb +33 -0
  12. data/lib/acp/request_error.rb +13 -13
  13. data/lib/acp/transport/cancellation.rb +13 -3
  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.rb +25 -26
  17. data/lib/acp/types/audio_content.rb +3 -46
  18. data/lib/acp/types/auth_method_terminal.rb +3 -59
  19. data/lib/acp/types/available_commands_update.rb +3 -32
  20. data/lib/acp/types/boolean_property_schema.rb +3 -41
  21. data/lib/acp/types/check.rb +210 -0
  22. data/lib/acp/types/config_option_update.rb +3 -32
  23. data/lib/acp/types/content.rb +3 -29
  24. data/lib/acp/types/{elicitation_url_mode → create_elicitation_request/mode/form}/scope.rb +1 -1
  25. data/lib/acp/types/create_elicitation_request/mode/form.rb +3 -3
  26. data/lib/acp/types/{elicitation_form_mode → create_elicitation_request/mode/url}/scope.rb +1 -1
  27. data/lib/acp/types/create_elicitation_request/mode/url.rb +3 -3
  28. data/lib/acp/types/current_mode_update.rb +3 -29
  29. data/lib/acp/types/diff.rb +3 -41
  30. data/lib/acp/types/elicitation_accept_action.rb +3 -26
  31. data/lib/acp/types/elicitation_form_mode.rb +3 -33
  32. data/lib/acp/types/elicitation_url_mode.rb +3 -35
  33. data/lib/acp/types/embedded_resource.rb +3 -44
  34. data/lib/acp/types/image_content.rb +3 -58
  35. data/lib/acp/types/integer_property_schema.rb +3 -60
  36. data/lib/acp/types/mcp_server_http.rb +3 -44
  37. data/lib/acp/types/mcp_server_sse.rb +3 -44
  38. data/lib/acp/types/multi_select_property_schema.rb +3 -70
  39. data/lib/acp/types/number_property_schema.rb +3 -60
  40. data/lib/acp/types/parse_error.rb +37 -0
  41. data/lib/acp/types/plan.rb +3 -29
  42. data/lib/acp/types/resource_link.rb +3 -79
  43. data/lib/acp/types/selected_permission_outcome.rb +3 -26
  44. data/lib/acp/types/session_config_boolean.rb +3 -21
  45. data/lib/acp/types/session_config_select.rb +3 -33
  46. data/lib/acp/types/session_info_update.rb +3 -42
  47. data/lib/acp/types/string_multi_select_items.rb +3 -29
  48. data/lib/acp/types/string_property_schema.rb +3 -105
  49. data/lib/acp/types/terminal.rb +3 -29
  50. data/lib/acp/types/text_content.rb +3 -40
  51. data/lib/acp/types/tool_call.rb +3 -115
  52. data/lib/acp/types/unstable/compaction_summary_chunk.rb +3 -35
  53. data/lib/acp/types/unstable/compaction_update.rb +3 -56
  54. data/lib/acp/types/unstable/idle_state_update.rb +3 -40
  55. data/lib/acp/types/unstable/mcp_server_acp.rb +3 -35
  56. data/lib/acp/types/unstable/message_mcp_response/result.rb +1 -1
  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 +25 -15
  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 +34 -13
  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 +128 -131
  84. data/sig/generated/acp/protocol.rbs +9 -0
  85. data/sig/generated/acp/request_error.rbs +11 -13
  86. data/sig/generated/acp/transport/cancellation.rbs +9 -3
  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.rbs +14 -22
  90. data/sig/generated/acp/types/audio_content.rbs +1 -27
  91. data/sig/generated/acp/types/auth_method_terminal.rbs +1 -35
  92. data/sig/generated/acp/types/available_commands_update.rbs +1 -19
  93. data/sig/generated/acp/types/boolean_property_schema.rbs +1 -27
  94. data/sig/generated/acp/types/check.rbs +115 -0
  95. data/sig/generated/acp/types/config_option_update.rbs +1 -19
  96. data/sig/generated/acp/types/content.rbs +1 -19
  97. data/sig/generated/acp/types/{elicitation_url_mode → create_elicitation_request/mode/form}/scope.rbs +2 -2
  98. data/sig/generated/acp/types/create_elicitation_request/mode/form.rbs +3 -3
  99. data/sig/generated/acp/types/{elicitation_form_mode → create_elicitation_request/mode/url}/scope.rbs +2 -2
  100. data/sig/generated/acp/types/create_elicitation_request/mode/url.rbs +3 -3
  101. data/sig/generated/acp/types/current_mode_update.rbs +1 -19
  102. data/sig/generated/acp/types/diff.rbs +1 -27
  103. data/sig/generated/acp/types/elicitation_accept_action.rbs +1 -15
  104. data/sig/generated/acp/types/elicitation_form_mode.rbs +1 -19
  105. data/sig/generated/acp/types/elicitation_url_mode.rbs +1 -23
  106. data/sig/generated/acp/types/embedded_resource.rbs +1 -23
  107. data/sig/generated/acp/types/image_content.rbs +1 -31
  108. data/sig/generated/acp/types/integer_property_schema.rbs +1 -35
  109. data/sig/generated/acp/types/mcp_server_http.rbs +1 -27
  110. data/sig/generated/acp/types/mcp_server_sse.rbs +1 -27
  111. data/sig/generated/acp/types/multi_select_property_schema.rbs +1 -39
  112. data/sig/generated/acp/types/number_property_schema.rbs +1 -35
  113. data/sig/generated/acp/types/parse_error.rbs +22 -0
  114. data/sig/generated/acp/types/plan.rbs +1 -19
  115. data/sig/generated/acp/types/resource_link.rbs +1 -43
  116. data/sig/generated/acp/types/selected_permission_outcome.rbs +1 -19
  117. data/sig/generated/acp/types/session_config_boolean.rbs +1 -15
  118. data/sig/generated/acp/types/session_config_select.rbs +1 -19
  119. data/sig/generated/acp/types/session_info_update.rbs +1 -23
  120. data/sig/generated/acp/types/string_multi_select_items.rbs +1 -19
  121. data/sig/generated/acp/types/string_property_schema.rbs +1 -51
  122. data/sig/generated/acp/types/terminal.rbs +1 -19
  123. data/sig/generated/acp/types/text_content.rbs +1 -23
  124. data/sig/generated/acp/types/tool_call.rbs +1 -51
  125. data/sig/generated/acp/types/unstable/compaction_summary_chunk.rbs +1 -23
  126. data/sig/generated/acp/types/unstable/compaction_update.rbs +1 -31
  127. data/sig/generated/acp/types/unstable/idle_state_update.rbs +1 -23
  128. data/sig/generated/acp/types/unstable/mcp_server_acp.rbs +1 -23
  129. data/sig/generated/acp/types/unstable/nes_edit_suggestion.rbs +1 -31
  130. data/sig/generated/acp/types/unstable/nes_jump_suggestion.rbs +1 -27
  131. data/sig/generated/acp/types/unstable/nes_rename_suggestion.rbs +1 -31
  132. data/sig/generated/acp/types/unstable/nes_search_and_replace_suggestion.rbs +1 -35
  133. data/sig/generated/acp/types/unstable/notice.rbs +1 -27
  134. data/sig/generated/acp/types/unstable/plan_file.rbs +1 -23
  135. data/sig/generated/acp/types/unstable/plan_items.rbs +1 -23
  136. data/sig/generated/acp/types/unstable/plan_markdown.rbs +1 -23
  137. data/sig/generated/acp/types/unstable/plan_removed.rbs +1 -19
  138. data/sig/generated/acp/types/unstable/plan_update.rbs +1 -19
  139. data/sig/generated/acp/types/unstable/requires_action_state_update.rbs +1 -15
  140. data/sig/generated/acp/types/unstable/running_state_update.rbs +1 -15
  141. data/sig/generated/acp/types/unstable/session_message.rbs +1 -31
  142. data/sig/generated/acp/types/unstable/session_message_chunk.rbs +1 -31
  143. data/sig/generated/acp/types/unstable/subagent_update.rbs +1 -35
  144. data/sig/generated/acp/types/unstable/unknown_state_update.rbs +1 -15
  145. data/sig/generated/acp/types/usage_update.rbs +1 -27
  146. data/sig/generated/acp/types.rbs +6 -143
  147. data/sig/manual/acp/agent_connection.rbs +28 -28
  148. data/sig/manual/acp/client_connection.rbs +7 -2
  149. data/sig/manual/acp/{agent_connection/optional_method/logout.rbs → protocol.rbs} +3 -3
  150. data/sig/manual/acp/transport/stdio.rbs +0 -2
  151. data/sig/manual/acp/transport.rbs +25 -2
  152. metadata +17 -29
  153. data/lib/acp/agent_connection/optional_method/authenticate.rb +0 -21
  154. data/lib/acp/agent_connection/optional_method/close_session.rb +0 -21
  155. data/lib/acp/agent_connection/optional_method/delete_session.rb +0 -21
  156. data/lib/acp/agent_connection/optional_method/list_sessions.rb +0 -21
  157. data/lib/acp/agent_connection/optional_method/load_session.rb +0 -21
  158. data/lib/acp/agent_connection/optional_method/logout.rb +0 -21
  159. data/lib/acp/agent_connection/optional_method/resume_session.rb +0 -21
  160. data/lib/acp/types/content_chunk.rb +0 -40
  161. data/sig/generated/acp/agent_connection/optional_method/authenticate.rbs +0 -13
  162. data/sig/generated/acp/agent_connection/optional_method/close_session.rbs +0 -13
  163. data/sig/generated/acp/agent_connection/optional_method/delete_session.rbs +0 -13
  164. data/sig/generated/acp/agent_connection/optional_method/list_sessions.rbs +0 -13
  165. data/sig/generated/acp/agent_connection/optional_method/load_session.rbs +0 -13
  166. data/sig/generated/acp/agent_connection/optional_method/logout.rbs +0 -13
  167. data/sig/generated/acp/agent_connection/optional_method/resume_session.rbs +0 -13
  168. data/sig/generated/acp/types/content_chunk.rbs +0 -25
  169. data/sig/manual/acp/agent_connection/optional_method/authenticate.rbs +0 -6
  170. data/sig/manual/acp/agent_connection/optional_method/close_session.rbs +0 -6
  171. data/sig/manual/acp/agent_connection/optional_method/delete_session.rbs +0 -6
  172. data/sig/manual/acp/agent_connection/optional_method/list_sessions.rbs +0 -6
  173. data/sig/manual/acp/agent_connection/optional_method/load_session.rbs +0 -6
  174. data/sig/manual/acp/agent_connection/optional_method/resume_session.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
@@ -60,9 +58,8 @@ class ACP::RequestError
60
58
  new(code: INVALID_PARAMS, message: 'Invalid params', data: errors && { errors: errors })
61
59
  end
62
60
 
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.
61
+ # The message names the capability so the refusal can be told apart from an
62
+ # unknown method, though both use the -32601 code.
66
63
  #
67
64
  # @rbs capability: String
68
65
  # @rbs return: ACP::RequestError
@@ -70,11 +67,9 @@ class ACP::RequestError
70
67
  new(code: METHOD_NOT_FOUND, message: "Client does not advertise #{capability}")
71
68
  end
72
69
 
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.
70
+ # The message names the mode so the refusal can be told apart from malformed
71
+ # params. The spec gives elicitation a mode in its params, so an unadvertised
72
+ # one is invalid params rather than method not found.
78
73
  #
79
74
  # @rbs mode: String
80
75
  # @rbs return: ACP::RequestError
@@ -82,6 +77,12 @@ class ACP::RequestError
82
77
  new(code: INVALID_PARAMS, message: "Client does not advertise #{mode}")
83
78
  end
84
79
 
80
+ # @rbs capability: String
81
+ # @rbs return: ACP::RequestError
82
+ def self.unadvertised_agent(capability)
83
+ new(code: METHOD_NOT_FOUND, message: "Agent does not advertise #{capability}")
84
+ end
85
+
85
86
  # @rbs return: ACP::RequestError
86
87
  def self.internal_error
87
88
  new(code: INTERNAL_ERROR, message: 'Internal error')
@@ -96,8 +97,7 @@ class ACP::RequestError
96
97
  new(code: INTERNAL_ERROR, message: 'Invalid response', data: response)
97
98
  end
98
99
 
99
- # A local refusal of an initialize reply that names a version the SDK does
100
- # not speak; the message names both versions so logs can tell it from a
100
+ # The message names both versions so logs can tell this local refusal from a
101
101
  # peer's error.
102
102
  #
103
103
  # @rbs requested: Integer
@@ -1,9 +1,19 @@
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
4
+ KEY = :acp_serving_cancellation #: Symbol
5
+
6
+ # @rbs return: ACP::Transport::Cancellation?
7
+ def self.current
8
+ Thread.current[KEY] #: ACP::Transport::Cancellation?
9
+ end
10
+
11
+ # @rbs cancellation: ACP::Transport::Cancellation?
12
+ # @rbs return: void
13
+ def self.current=(cancellation)
14
+ Thread.current[KEY] = cancellation
15
+ end
16
+
7
17
  # @rbs @lock: Thread::Mutex
8
18
  # @rbs @cancelled: bool
9
19
 
@@ -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
@@ -2,11 +2,10 @@
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
@@ -22,9 +21,6 @@ class ACP::Transport::Stdio
22
21
  code: ACP::RequestError::INTERNAL_ERROR, message: 'Connection closed'
23
22
  ) #: ACP::RequestError
24
23
 
25
- SERVING_CANCEL = :acp_serving_cancellation #: Symbol
26
- private_constant :SERVING_CANCEL
27
-
28
24
  # @rbs input: _Reader
29
25
  # @rbs output: _Writer
30
26
  # @rbs logger: ACP::Transport::_Logger
@@ -41,9 +37,6 @@ class ACP::Transport::Stdio
41
37
  @logger = logger
42
38
  end
43
39
 
44
- # Returns the reader thread, which ends at EOF on input after releasing any
45
- # pending outbound requests.
46
- #
47
40
  # @rbs requests: Hash[String, _Handler]
48
41
  # @rbs notifications: Hash[String, _NotificationHandler]
49
42
  # @rbs return: Thread
@@ -58,14 +51,16 @@ class ACP::Transport::Stdio
58
51
 
59
52
  # @rbs method: String
60
53
  # @rbs params: untyped
61
- # @rbs return: (Hash[String, untyped] | ACP::RequestError)
54
+ # @rbs return: untyped
62
55
  def request(method, params = nil)
63
56
  queue = Thread::Queue.new
64
57
  id = register(queue)
65
58
  return CONNECTION_CLOSED unless id
66
59
 
67
60
  write({ 'jsonrpc' => '2.0', 'id' => id, 'method' => method, 'params' => params }.compact)
68
- queue.pop || CONNECTION_CLOSED
61
+ result = queue.pop
62
+ # A null result pops nil just as a closed queue does.
63
+ queue.closed? ? CONNECTION_CLOSED : result
69
64
  end
70
65
 
71
66
  # @rbs method: String
@@ -85,14 +80,17 @@ class ACP::Transport::Stdio
85
80
  notify('$/cancel_request', { 'requestId' => id }) if pending
86
81
  end
87
82
 
88
- # The cancellation of the request the calling thread is serving, or nil off
89
- # a serve thread. A handler observes the peer's $/cancel_request through it
90
- # and can end early with ACP::RequestError.request_cancelled; whatever it
91
- # returns, the transport sends exactly one response.
83
+ # Each id is re-checked against the pending table, so one that settled
84
+ # between the snapshot and its cancel is not cancelled.
92
85
  #
86
+ # @rbs return: void
87
+ def cancel_requests
88
+ @lock.synchronize { @pending.keys }.each { |id| cancel(id) }
89
+ end
90
+
93
91
  # @rbs return: ACP::Transport::Cancellation?
94
92
  def cancellation
95
- Thread.current[SERVING_CANCEL] #: ACP::Transport::Cancellation?
93
+ ACP::Transport::Cancellation.current
96
94
  end
97
95
 
98
96
  private
@@ -132,8 +130,12 @@ class ACP::Transport::Stdio
132
130
  elsif error.is_a?(Hash) && error['code'].is_a?(Integer) && error['message'].is_a?(String)
133
131
  code, text, data = error.values_at('code', 'message', 'data')
134
132
  settle(message['id'], ACP::RequestError.new(code:, message: text, data:))
135
- else
133
+ elsif message.key?('method')
136
134
  reply(detectable_id(message), ACP::RequestError.invalid_request)
135
+ elsif !settle(message['id'], ACP::RequestError.invalid_response(message))
136
+ # JSON-RPC responses only answer requests, so a malformed one is never
137
+ # replied to: the error could settle an unrelated request on the peer.
138
+ @logger.warn('dropped malformed response: no pending request for its id')
137
139
  end
138
140
  end
139
141
 
@@ -167,7 +169,7 @@ class ACP::Transport::Stdio
167
169
  def serve(handler, id, params, method)
168
170
  cancellation = register_serving(id)
169
171
  Thread.new do
170
- Thread.current[SERVING_CANCEL] = cancellation
172
+ ACP::Transport::Cancellation.current = cancellation
171
173
  outcome = handler ? invoke(handler, params) : ACP::RequestError.method_not_found(method)
172
174
  case outcome
173
175
  when ACP::Transport::Reply
@@ -241,9 +243,6 @@ class ACP::Transport::Stdio
241
243
  ACP::RequestError.internal_error
242
244
  end
243
245
 
244
- # The success arm is whatever the handler returned, which the transport only
245
- # knows serializes with to_h; RequestError picks the error arm of the reply.
246
- #
247
246
  # @rbs id: untyped
248
247
  # @rbs outcome: (_ToH | ACP::RequestError)
249
248
  # @rbs return: void
@@ -268,10 +267,10 @@ class ACP::Transport::Stdio
268
267
  end
269
268
 
270
269
  # @rbs id: untyped
271
- # @rbs result: (Hash[String, untyped] | ACP::RequestError)
272
- # @rbs return: void
270
+ # @rbs result: untyped
271
+ # @rbs return: Thread::Queue?
273
272
  def settle(id, result)
274
- @lock.synchronize { @pending.delete(id) }&.push(result)
273
+ @lock.synchronize { @pending.delete(id) }&.tap { |queue| queue.push(result) }
275
274
  end
276
275
 
277
276
  # @rbs return: void
@@ -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
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Hand-authored support for the generated types: the generator neither emits
4
+ # nor deletes this file (see TypeGenerator::SUPPORT_FILES).
5
+
6
+ module ACP::Types::Check
7
+ # @rbs hash: Hash[String, untyped]
8
+ # @rbs key: String
9
+ # @rbs return: untyped
10
+ def self.key(hash, key)
11
+ hash.fetch(key)
12
+ rescue KeyError
13
+ raise ACP::Types::ParseError.new([key], 'is required')
14
+ end
15
+
16
+ # @rbs value: untyped
17
+ # @rbs path: String | Array[String | Integer]
18
+ # @rbs allow_nil: bool
19
+ # @rbs allow_unset: bool
20
+ # @rbs return: untyped
21
+ def self.string(value, path, allow_nil: false, allow_unset: false)
22
+ return value if value.is_a?(String) || skipped?(value, allow_nil, allow_unset)
23
+
24
+ raise ACP::Types::ParseError.new(segments(path), "expected String, got #{describe(value)}")
25
+ end
26
+
27
+ # @rbs value: untyped
28
+ # @rbs path: String | Array[String | Integer]
29
+ # @rbs allow_nil: bool
30
+ # @rbs allow_unset: bool
31
+ # @rbs return: untyped
32
+ def self.boolean(value, path, allow_nil: false, allow_unset: false)
33
+ return value if value == true || value == false || skipped?(value, allow_nil, allow_unset)
34
+
35
+ raise ACP::Types::ParseError.new(segments(path), "expected boolean, got #{describe(value)}")
36
+ end
37
+
38
+ # @rbs value: untyped
39
+ # @rbs path: String | Array[String | Integer]
40
+ # @rbs min: Integer?
41
+ # @rbs max: Integer?
42
+ # @rbs allow_nil: bool
43
+ # @rbs allow_unset: bool
44
+ # @rbs return: untyped
45
+ def self.integer(value, path, min: nil, max: nil, allow_nil: false, allow_unset: false)
46
+ return value if (value.is_a?(Integer) && within?(value, min, max)) || skipped?(value, allow_nil, allow_unset)
47
+
48
+ raise ACP::Types::ParseError.new(segments(path), "expected #{bounded('Integer', min, max)}, got #{describe(value)}")
49
+ end
50
+
51
+ # @rbs value: untyped
52
+ # @rbs path: String | Array[String | Integer]
53
+ # @rbs min: Integer?
54
+ # @rbs max: Integer?
55
+ # @rbs allow_nil: bool
56
+ # @rbs allow_unset: bool
57
+ # @rbs return: untyped
58
+ def self.number(value, path, min: nil, max: nil, allow_nil: false, allow_unset: false)
59
+ return value if (value.is_a?(Numeric) && within?(value, min, max)) || skipped?(value, allow_nil, allow_unset)
60
+
61
+ raise ACP::Types::ParseError.new(segments(path), "expected #{bounded('Numeric', min, max)}, got #{describe(value)}")
62
+ end
63
+
64
+ # @rbs value: untyped
65
+ # @rbs path: String | Array[String | Integer]
66
+ # @rbs values: Array[untyped]
67
+ # @rbs allow_nil: bool
68
+ # @rbs allow_unset: bool
69
+ # @rbs return: untyped
70
+ def self.enum(value, path, values, allow_nil: false, allow_unset: false)
71
+ return value if values.include?(value) || skipped?(value, allow_nil, allow_unset)
72
+
73
+ expected = values.map(&:inspect).join(', ')
74
+ raise ACP::Types::ParseError.new(segments(path), "expected one of #{expected}, got #{describe(value)}")
75
+ end
76
+
77
+ # @rbs value: untyped
78
+ # @rbs path: String | Array[String | Integer]
79
+ # @rbs classes: Array[untyped]
80
+ # @rbs allow_nil: bool
81
+ # @rbs allow_unset: bool
82
+ # @rbs ?&items: (untyped, Array[String | Integer]) -> void
83
+ # @rbs return: untyped
84
+ def self.one_of(value, path, classes, allow_nil: false, allow_unset: false, &items)
85
+ return value if skipped?(value, allow_nil, allow_unset)
86
+
87
+ unless classes.any? { |klass| value.is_a?(klass) }
88
+ expected = classes.map(&:name).join(' | ')
89
+ raise ACP::Types::ParseError.new(segments(path), "expected #{expected}, got #{describe(value)}")
90
+ end
91
+
92
+ return value unless value.is_a?(Array) && items
93
+
94
+ value.each_with_index { |item, index| yield(item, [*segments(path), index]) }
95
+ value
96
+ end
97
+
98
+ # @rbs value: untyped
99
+ # @rbs path: String | Array[String | Integer]
100
+ # @rbs allow_nil: bool
101
+ # @rbs allow_unset: bool
102
+ # @rbs &check: (untyped, Array[String | Integer]) -> void
103
+ # @rbs return: untyped
104
+ def self.hash(value, path, allow_nil: false, allow_unset: false, &check)
105
+ return value if skipped?(value, allow_nil, allow_unset)
106
+ raise ACP::Types::ParseError.new(segments(path), "expected object, got #{describe(value)}") unless value.is_a?(Hash)
107
+
108
+ parsed = {} #: Hash[String, untyped]
109
+ value.each { |key, item| parsed[key] = yield(item, [*segments(path), key]) }
110
+ parsed
111
+ end
112
+
113
+ # @rbs value: untyped
114
+ # @rbs path: String | Array[String | Integer]
115
+ # @rbs allow_nil: bool
116
+ # @rbs allow_unset: bool
117
+ # @rbs &check: (untyped, Array[String | Integer]) -> void
118
+ # @rbs return: untyped
119
+ def self.array(value, path, allow_nil: false, allow_unset: false, &check)
120
+ return value if skipped?(value, allow_nil, allow_unset)
121
+ raise ACP::Types::ParseError.new(segments(path), "expected array, got #{describe(value)}") unless value.is_a?(Array)
122
+
123
+ value.each_with_index.map { |item, index| yield(item, [*segments(path), index]) }
124
+ end
125
+
126
+ # @rbs value: untyped
127
+ # @rbs path: String | Array[String | Integer]
128
+ # @rbs klass: untyped
129
+ # @rbs allow_nil: bool
130
+ # @rbs allow_unset: bool
131
+ # @rbs return: untyped
132
+ def self.object(value, path, klass, allow_nil: false, allow_unset: false)
133
+ return value if skipped?(value, allow_nil, allow_unset)
134
+ raise ACP::Types::ParseError.new(segments(path), "expected object, got #{describe(value)}") unless value.is_a?(Hash)
135
+
136
+ begin
137
+ klass.from_h(value)
138
+ rescue ACP::Types::ParseError => e
139
+ raise e.prefix(*segments(path))
140
+ end
141
+ end
142
+
143
+ # @rbs value: untyped
144
+ # @rbs path: String | Array[String | Integer]
145
+ # @rbs klass: untyped
146
+ # @rbs allow_nil: bool
147
+ # @rbs allow_unset: bool
148
+ # @rbs return: untyped
149
+ def self.objects(value, path, klass, allow_nil: false, allow_unset: false)
150
+ return value if skipped?(value, allow_nil, allow_unset)
151
+ raise ACP::Types::ParseError.new(segments(path), "expected array, got #{describe(value)}") unless value.is_a?(Array)
152
+
153
+ value.each_with_index do |item, index|
154
+ next if item.is_a?(Hash)
155
+
156
+ raise ACP::Types::ParseError.new([*segments(path), index], "expected object, got #{describe(item)}")
157
+ end
158
+
159
+ begin
160
+ klass.from_a(value)
161
+ rescue ACP::Types::ParseError => e
162
+ raise e.prefix(*segments(path))
163
+ end
164
+ end
165
+
166
+ # @rbs value: untyped
167
+ # @rbs allow_nil: bool
168
+ # @rbs allow_unset: bool
169
+ # @rbs return: bool
170
+ def self.skipped?(value, allow_nil, allow_unset)
171
+ (value.nil? && allow_nil) || (allow_unset && value.equal?(:unset))
172
+ end
173
+
174
+ # @rbs path: String | Array[String | Integer]
175
+ # @rbs return: Array[String | Integer]
176
+ def self.segments(path)
177
+ path.is_a?(Array) ? path : [path]
178
+ end
179
+
180
+ # @rbs value: untyped
181
+ # @rbs return: String
182
+ def self.describe(value)
183
+ case value
184
+ when nil then 'null'
185
+ when true then 'true'
186
+ when false then 'false'
187
+ else value.class.name
188
+ end
189
+ end
190
+
191
+ # @rbs value: Numeric
192
+ # @rbs min: Integer?
193
+ # @rbs max: Integer?
194
+ # @rbs return: bool
195
+ def self.within?(value, min, max)
196
+ (min.nil? || value >= min) && (max.nil? || value <= max)
197
+ end
198
+
199
+ # @rbs name: String
200
+ # @rbs min: Integer?
201
+ # @rbs max: Integer?
202
+ # @rbs return: String
203
+ def self.bounded(name, min, max)
204
+ if min && max then "#{name} between #{min} and #{max}"
205
+ elsif min then "#{name} >= #{min}"
206
+ elsif max then "#{name} <= #{max}"
207
+ else name
208
+ end
209
+ end
210
+ end