acp-sdk 0.7.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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +2 -1
  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 +93 -4
  7. data/lib/acp/request_error.rb +15 -2
  8. data/lib/acp/types/agent_auth_capabilities.rb +4 -1
  9. data/lib/acp/types/agent_capabilities.rb +20 -5
  10. data/lib/acp/types/annotations.rb +6 -3
  11. data/lib/acp/types/audio_content.rb +8 -3
  12. data/lib/acp/types/auth_capabilities.rb +1 -1
  13. data/lib/acp/types/auth_method/terminal.rb +11 -5
  14. data/lib/acp/types/auth_method_agent.rb +6 -1
  15. data/lib/acp/types/auth_method_terminal.rb +11 -5
  16. data/lib/acp/types/authenticate_request.rb +1 -1
  17. data/lib/acp/types/available_command.rb +3 -3
  18. data/lib/acp/types/available_commands_update.rb +7 -4
  19. data/lib/acp/types/blob_resource_contents.rb +6 -1
  20. data/lib/acp/types/boolean_property_schema.rb +6 -1
  21. data/lib/acp/types/cancel_notification.rb +4 -1
  22. data/lib/acp/types/cancel_request_notification.rb +9 -1
  23. data/lib/acp/types/client_capabilities.rb +15 -5
  24. data/lib/acp/types/client_session_capabilities.rb +6 -3
  25. data/lib/acp/types/close_session_request.rb +4 -1
  26. data/lib/acp/types/complete_elicitation_notification.rb +4 -1
  27. data/lib/acp/types/config_option_update.rb +7 -4
  28. data/lib/acp/types/content.rb +4 -1
  29. data/lib/acp/types/content_block/audio.rb +8 -3
  30. data/lib/acp/types/content_block/image.rb +9 -4
  31. data/lib/acp/types/content_block/resource.rb +11 -2
  32. data/lib/acp/types/content_block/resource_link.rb +12 -7
  33. data/lib/acp/types/content_block/text.rb +7 -2
  34. data/lib/acp/types/content_chunk.rb +2 -2
  35. data/lib/acp/types/cost.rb +5 -1
  36. data/lib/acp/types/create_elicitation_request/mode/form.rb +5 -1
  37. data/lib/acp/types/create_elicitation_request/mode/url.rb +2 -2
  38. data/lib/acp/types/create_elicitation_request.rb +1 -1
  39. data/lib/acp/types/create_elicitation_response/action/accept.rb +6 -1
  40. data/lib/acp/types/create_terminal_request.rb +12 -6
  41. data/lib/acp/types/create_terminal_response.rb +4 -1
  42. data/lib/acp/types/current_mode_update.rb +4 -1
  43. data/lib/acp/types/delete_session_request.rb +4 -1
  44. data/lib/acp/types/diff.rb +6 -1
  45. data/lib/acp/types/elicitation_accept_action.rb +6 -1
  46. data/lib/acp/types/elicitation_capabilities.rb +2 -2
  47. data/lib/acp/types/elicitation_form_mode.rb +5 -1
  48. data/lib/acp/types/elicitation_property_schema/boolean_property_schema.rb +6 -1
  49. data/lib/acp/types/elicitation_property_schema/integer_property_schema.rb +5 -5
  50. data/lib/acp/types/elicitation_property_schema/multi_select_property_schema.rb +9 -6
  51. data/lib/acp/types/elicitation_property_schema/number_property_schema.rb +5 -5
  52. data/lib/acp/types/elicitation_property_schema/string_property_schema.rb +15 -9
  53. data/lib/acp/types/elicitation_request_scope.rb +8 -1
  54. data/lib/acp/types/elicitation_schema.rb +11 -5
  55. data/lib/acp/types/elicitation_session_scope.rb +4 -1
  56. data/lib/acp/types/elicitation_url_mode.rb +2 -2
  57. data/lib/acp/types/embedded_resource.rb +11 -2
  58. data/lib/acp/types/enum_option.rb +6 -1
  59. data/lib/acp/types/env_variable.rb +5 -1
  60. data/lib/acp/types/file_system_capabilities.rb +5 -1
  61. data/lib/acp/types/http_header.rb +5 -1
  62. data/lib/acp/types/image_content.rb +9 -4
  63. data/lib/acp/types/implementation.rb +6 -1
  64. data/lib/acp/types/initialize_request.rb +18 -3
  65. data/lib/acp/types/initialize_response.rb +17 -4
  66. data/lib/acp/types/integer_property_schema.rb +5 -5
  67. data/lib/acp/types/kill_terminal_request.rb +5 -1
  68. data/lib/acp/types/list_sessions_request.rb +5 -1
  69. data/lib/acp/types/list_sessions_response.rb +5 -2
  70. data/lib/acp/types/load_session_request.rb +14 -4
  71. data/lib/acp/types/load_session_response.rb +5 -2
  72. data/lib/acp/types/mcp_capabilities.rb +5 -1
  73. data/lib/acp/types/mcp_server/http.rb +6 -3
  74. data/lib/acp/types/mcp_server/sse.rb +6 -3
  75. data/lib/acp/types/mcp_server_http.rb +6 -3
  76. data/lib/acp/types/mcp_server_sse.rb +6 -3
  77. data/lib/acp/types/mcp_server_stdio.rb +10 -4
  78. data/lib/acp/types/multi_select_items/string_multi_select_items.rb +4 -1
  79. data/lib/acp/types/multi_select_property_schema.rb +9 -6
  80. data/lib/acp/types/new_session_request.rb +13 -3
  81. data/lib/acp/types/new_session_response.rb +6 -3
  82. data/lib/acp/types/number_property_schema.rb +5 -5
  83. data/lib/acp/types/permission_option.rb +10 -1
  84. data/lib/acp/types/plan.rb +4 -1
  85. data/lib/acp/types/plan_entry.rb +7 -3
  86. data/lib/acp/types/prompt_capabilities.rb +6 -1
  87. data/lib/acp/types/prompt_request.rb +5 -2
  88. data/lib/acp/types/prompt_response.rb +8 -1
  89. data/lib/acp/types/read_text_file_request.rb +4 -4
  90. data/lib/acp/types/read_text_file_response.rb +1 -1
  91. data/lib/acp/types/release_terminal_request.rb +5 -1
  92. data/lib/acp/types/request_permission_outcome/selected.rb +1 -1
  93. data/lib/acp/types/request_permission_request.rb +10 -3
  94. data/lib/acp/types/request_permission_response.rb +8 -1
  95. data/lib/acp/types/resource_link.rb +12 -7
  96. data/lib/acp/types/resume_session_request.rb +14 -4
  97. data/lib/acp/types/resume_session_response.rb +5 -2
  98. data/lib/acp/types/selected_permission_outcome.rb +1 -1
  99. data/lib/acp/types/session_capabilities.rb +20 -7
  100. data/lib/acp/types/session_config_boolean.rb +1 -1
  101. data/lib/acp/types/session_config_option/kind/boolean.rb +1 -1
  102. data/lib/acp/types/session_config_option/kind/select.rb +6 -2
  103. data/lib/acp/types/session_config_option.rb +3 -3
  104. data/lib/acp/types/session_config_options_capabilities.rb +6 -1
  105. data/lib/acp/types/session_config_select.rb +6 -2
  106. data/lib/acp/types/session_config_select_group.rb +6 -3
  107. data/lib/acp/types/session_config_select_option.rb +6 -1
  108. data/lib/acp/types/session_config_select_options.rb +6 -2
  109. data/lib/acp/types/session_info.rb +12 -5
  110. data/lib/acp/types/session_info_update.rb +10 -1
  111. data/lib/acp/types/session_mode.rb +6 -1
  112. data/lib/acp/types/session_mode_state.rb +8 -2
  113. data/lib/acp/types/session_notification.rb +2 -2
  114. data/lib/acp/types/session_update/agent_message_chunk.rb +2 -2
  115. data/lib/acp/types/session_update/agent_thought_chunk.rb +2 -2
  116. data/lib/acp/types/session_update/available_commands_update.rb +7 -4
  117. data/lib/acp/types/session_update/config_option_update.rb +7 -4
  118. data/lib/acp/types/session_update/current_mode_update.rb +4 -1
  119. data/lib/acp/types/session_update/plan.rb +4 -1
  120. data/lib/acp/types/session_update/session_info_update.rb +10 -1
  121. data/lib/acp/types/session_update/tool_call.rb +23 -7
  122. data/lib/acp/types/session_update/tool_call_update.rb +23 -7
  123. data/lib/acp/types/session_update/usage_update.rb +3 -3
  124. data/lib/acp/types/session_update/user_message_chunk.rb +2 -2
  125. data/lib/acp/types/set_session_config_option_request/value/boolean.rb +1 -1
  126. data/lib/acp/types/set_session_config_option_request/value/value_id.rb +1 -1
  127. data/lib/acp/types/set_session_config_option_request.rb +2 -2
  128. data/lib/acp/types/set_session_config_option_response.rb +7 -4
  129. data/lib/acp/types/set_session_mode_request.rb +5 -1
  130. data/lib/acp/types/string_multi_select_items.rb +4 -1
  131. data/lib/acp/types/string_property_schema.rb +15 -9
  132. data/lib/acp/types/terminal.rb +4 -1
  133. data/lib/acp/types/terminal_exit_status.rb +5 -1
  134. data/lib/acp/types/terminal_output_request.rb +5 -1
  135. data/lib/acp/types/terminal_output_response.rb +8 -3
  136. data/lib/acp/types/text_content.rb +7 -2
  137. data/lib/acp/types/text_resource_contents.rb +6 -1
  138. data/lib/acp/types/titled_multi_select_items.rb +4 -1
  139. data/lib/acp/types/tool_call.rb +23 -7
  140. data/lib/acp/types/tool_call_content/content.rb +4 -1
  141. data/lib/acp/types/tool_call_content/diff.rb +6 -1
  142. data/lib/acp/types/tool_call_content/terminal.rb +4 -1
  143. data/lib/acp/types/tool_call_location.rb +5 -1
  144. data/lib/acp/types/tool_call_update.rb +23 -7
  145. data/lib/acp/types/unstructured_command_input.rb +1 -1
  146. data/lib/acp/types/usage_update.rb +3 -3
  147. data/lib/acp/types/wait_for_terminal_exit_request.rb +5 -1
  148. data/lib/acp/types/wait_for_terminal_exit_response.rb +5 -1
  149. data/lib/acp/types/write_text_file_request.rb +3 -3
  150. data/lib/acp/types.rb +247 -0
  151. data/lib/acp/version.rb +1 -1
  152. data/sig/generated/acp/client_connection.rbs +39 -1
  153. data/sig/generated/acp/request_error.rbs +12 -1
  154. data/sig/generated/acp/types.rbs +143 -0
  155. metadata +1 -1
@@ -26,7 +26,11 @@ class ACP::Types::TerminalExitStatus
26
26
  # @rbs hash: Hash[String, untyped]
27
27
  # @rbs return: ACP::Types::TerminalExitStatus
28
28
  def self.from_h(hash)
29
- new(exit_code: hash['exitCode'], signal: hash['signal'], meta: hash['_meta'])
29
+ new(
30
+ exit_code: ACP::Types::Check.integer(hash['exitCode'], 'exitCode', min: 0, allow_nil: true),
31
+ signal: ACP::Types::Check.string(hash['signal'], 'signal', allow_nil: true),
32
+ meta: hash['_meta']
33
+ )
30
34
  end
31
35
 
32
36
  # @rbs return: Hash[String, untyped]
@@ -26,7 +26,11 @@ class ACP::Types::TerminalOutputRequest
26
26
  # @rbs hash: Hash[String, untyped]
27
27
  # @rbs return: ACP::Types::TerminalOutputRequest
28
28
  def self.from_h(hash)
29
- new(session_id: hash.fetch('sessionId'), terminal_id: hash.fetch('terminalId'), meta: hash['_meta'])
29
+ new(
30
+ session_id: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'sessionId'), 'sessionId'),
31
+ terminal_id: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'terminalId'), 'terminalId'),
32
+ meta: hash['_meta']
33
+ )
30
34
  end
31
35
 
32
36
  # @rbs return: Hash[String, untyped]
@@ -32,9 +32,14 @@ class ACP::Types::TerminalOutputResponse
32
32
  # @rbs return: ACP::Types::TerminalOutputResponse
33
33
  def self.from_h(hash)
34
34
  new(
35
- output: hash.fetch('output'),
36
- truncated: hash.fetch('truncated'),
37
- exit_status: hash['exitStatus']&.then { |value| ACP::Types::TerminalExitStatus.from_h(value) },
35
+ output: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'output'), 'output'),
36
+ truncated: ACP::Types::Check.boolean(ACP::Types::Check.key(hash, 'truncated'), 'truncated'),
37
+ exit_status: ACP::Types::Check.object(
38
+ hash['exitStatus'],
39
+ 'exitStatus',
40
+ ACP::Types::TerminalExitStatus,
41
+ allow_nil: true
42
+ ),
38
43
  meta: hash['_meta']
39
44
  )
40
45
  end
@@ -27,8 +27,13 @@ class ACP::Types::TextContent
27
27
  # @rbs return: ACP::Types::TextContent
28
28
  def self.from_h(hash)
29
29
  new(
30
- annotations: hash['annotations']&.then { |value| ACP::Types::Annotations.from_h(value) },
31
- text: hash.fetch('text'),
30
+ annotations: ACP::Types::Check.object(
31
+ hash['annotations'],
32
+ 'annotations',
33
+ ACP::Types::Annotations,
34
+ allow_nil: true
35
+ ),
36
+ text: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'text'), 'text'),
32
37
  meta: hash['_meta']
33
38
  )
34
39
  end
@@ -31,7 +31,12 @@ class ACP::Types::TextResourceContents
31
31
  # @rbs hash: Hash[String, untyped]
32
32
  # @rbs return: ACP::Types::TextResourceContents
33
33
  def self.from_h(hash)
34
- new(mime_type: hash['mimeType'], text: hash.fetch('text'), uri: hash.fetch('uri'), meta: hash['_meta'])
34
+ new(
35
+ mime_type: ACP::Types::Check.string(hash['mimeType'], 'mimeType', allow_nil: true),
36
+ text: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'text'), 'text'),
37
+ uri: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'uri'), 'uri'),
38
+ meta: hash['_meta']
39
+ )
35
40
  end
36
41
 
37
42
  # @rbs return: Hash[String, untyped]
@@ -21,7 +21,10 @@ class ACP::Types::TitledMultiSelectItems
21
21
  # @rbs hash: Hash[String, untyped]
22
22
  # @rbs return: ACP::Types::TitledMultiSelectItems
23
23
  def self.from_h(hash)
24
- new(any_of: hash.fetch('anyOf').map { |item| ACP::Types::EnumOption.from_h(item) }, meta: hash['_meta'])
24
+ any_of = ACP::Types::Check.array(ACP::Types::Check.key(hash, 'anyOf'), 'anyOf') do |item, path|
25
+ ACP::Types::Check.object(item, path, ACP::Types::EnumOption)
26
+ end
27
+ new(any_of:, meta: hash['_meta'])
25
28
  end
26
29
 
27
30
  # @rbs return: Hash[String, untyped]
@@ -72,14 +72,30 @@ class ACP::Types::ToolCall
72
72
  # @rbs hash: Hash[String, untyped]
73
73
  # @rbs return: ACP::Types::ToolCall
74
74
  def self.from_h(hash)
75
+ content = ACP::Types::Check.array(hash['content'], 'content', allow_nil: true) do |item, path|
76
+ ACP::Types::Check.object(item, path, ACP::Types::ToolCallContent)
77
+ end
78
+ locations = ACP::Types::Check.array(hash['locations'], 'locations', allow_nil: true) do |item, path|
79
+ ACP::Types::Check.object(item, path, ACP::Types::ToolCallLocation)
80
+ end
75
81
  new(
76
- tool_call_id: hash.fetch('toolCallId'),
77
- title: hash.fetch('title'),
78
- name: hash['name'],
79
- kind: hash['kind'],
80
- status: hash['status'],
81
- content: hash['content']&.map { |item| ACP::Types::ToolCallContent.from_h(item) },
82
- locations: hash['locations']&.map { |item| ACP::Types::ToolCallLocation.from_h(item) },
82
+ tool_call_id: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'toolCallId'), 'toolCallId'),
83
+ title: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'title'), 'title'),
84
+ name: ACP::Types::Check.string(hash['name'], 'name', allow_nil: true),
85
+ kind: ACP::Types::Check.enum(
86
+ hash['kind'],
87
+ 'kind',
88
+ %w[read edit delete move search execute think fetch switch_mode other],
89
+ allow_nil: true
90
+ ),
91
+ status: ACP::Types::Check.enum(
92
+ hash['status'],
93
+ 'status',
94
+ %w[pending in_progress completed failed],
95
+ allow_nil: true
96
+ ),
97
+ content:,
98
+ locations:,
83
99
  raw_input: hash['rawInput'],
84
100
  raw_output: hash['rawOutput'],
85
101
  meta: hash['_meta']
@@ -21,7 +21,10 @@ class ACP::Types::ToolCallContent::Content
21
21
  # @rbs hash: Hash[String, untyped]
22
22
  # @rbs return: ACP::Types::ToolCallContent::Content
23
23
  def self.from_h(hash)
24
- new(content: ACP::Types::ContentBlock.from_h(hash.fetch('content')), meta: hash['_meta'])
24
+ new(
25
+ content: ACP::Types::Check.object(ACP::Types::Check.key(hash, 'content'), 'content', ACP::Types::ContentBlock),
26
+ meta: hash['_meta']
27
+ )
25
28
  end
26
29
 
27
30
  # @rbs return: Hash[String, untyped]
@@ -31,7 +31,12 @@ class ACP::Types::ToolCallContent::Diff
31
31
  # @rbs hash: Hash[String, untyped]
32
32
  # @rbs return: ACP::Types::ToolCallContent::Diff
33
33
  def self.from_h(hash)
34
- new(path: hash.fetch('path'), old_text: hash['oldText'], new_text: hash.fetch('newText'), meta: hash['_meta'])
34
+ new(
35
+ path: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'path'), 'path'),
36
+ old_text: ACP::Types::Check.string(hash['oldText'], 'oldText', allow_nil: true),
37
+ new_text: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'newText'), 'newText'),
38
+ meta: hash['_meta']
39
+ )
35
40
  end
36
41
 
37
42
  # @rbs return: Hash[String, untyped]
@@ -21,7 +21,10 @@ class ACP::Types::ToolCallContent::Terminal
21
21
  # @rbs hash: Hash[String, untyped]
22
22
  # @rbs return: ACP::Types::ToolCallContent::Terminal
23
23
  def self.from_h(hash)
24
- new(terminal_id: hash.fetch('terminalId'), meta: hash['_meta'])
24
+ new(
25
+ terminal_id: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'terminalId'), 'terminalId'),
26
+ meta: hash['_meta']
27
+ )
25
28
  end
26
29
 
27
30
  # @rbs return: Hash[String, untyped]
@@ -26,7 +26,11 @@ class ACP::Types::ToolCallLocation
26
26
  # @rbs hash: Hash[String, untyped]
27
27
  # @rbs return: ACP::Types::ToolCallLocation
28
28
  def self.from_h(hash)
29
- new(path: hash.fetch('path'), line: hash['line'], meta: hash['_meta'])
29
+ new(
30
+ path: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'path'), 'path'),
31
+ line: ACP::Types::Check.integer(hash['line'], 'line', min: 0, allow_nil: true),
32
+ meta: hash['_meta']
33
+ )
30
34
  end
31
35
 
32
36
  # @rbs return: Hash[String, untyped]
@@ -72,14 +72,30 @@ class ACP::Types::ToolCallUpdate
72
72
  # @rbs hash: Hash[String, untyped]
73
73
  # @rbs return: ACP::Types::ToolCallUpdate
74
74
  def self.from_h(hash)
75
+ content = ACP::Types::Check.array(hash['content'], 'content', allow_nil: true) do |item, path|
76
+ ACP::Types::Check.object(item, path, ACP::Types::ToolCallContent)
77
+ end
78
+ locations = ACP::Types::Check.array(hash['locations'], 'locations', allow_nil: true) do |item, path|
79
+ ACP::Types::Check.object(item, path, ACP::Types::ToolCallLocation)
80
+ end
75
81
  new(
76
- tool_call_id: hash.fetch('toolCallId'),
77
- kind: hash['kind'],
78
- status: hash['status'],
79
- title: hash['title'],
80
- name: hash['name'],
81
- content: hash['content']&.map { |item| ACP::Types::ToolCallContent.from_h(item) },
82
- locations: hash['locations']&.map { |item| ACP::Types::ToolCallLocation.from_h(item) },
82
+ tool_call_id: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'toolCallId'), 'toolCallId'),
83
+ kind: ACP::Types::Check.enum(
84
+ hash['kind'],
85
+ 'kind',
86
+ %w[read edit delete move search execute think fetch switch_mode other],
87
+ allow_nil: true
88
+ ),
89
+ status: ACP::Types::Check.enum(
90
+ hash['status'],
91
+ 'status',
92
+ %w[pending in_progress completed failed],
93
+ allow_nil: true
94
+ ),
95
+ title: ACP::Types::Check.string(hash['title'], 'title', allow_nil: true),
96
+ name: ACP::Types::Check.string(hash['name'], 'name', allow_nil: true),
97
+ content:,
98
+ locations:,
83
99
  raw_input: hash['rawInput'],
84
100
  raw_output: hash['rawOutput'],
85
101
  meta: hash['_meta']
@@ -21,7 +21,7 @@ class ACP::Types::UnstructuredCommandInput
21
21
  # @rbs hash: Hash[String, untyped]
22
22
  # @rbs return: ACP::Types::UnstructuredCommandInput
23
23
  def self.from_h(hash)
24
- new(hint: hash.fetch('hint'), meta: hash['_meta'])
24
+ new(hint: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'hint'), 'hint'), meta: hash['_meta'])
25
25
  end
26
26
 
27
27
  # @rbs return: Hash[String, untyped]
@@ -32,9 +32,9 @@ class ACP::Types::UsageUpdate
32
32
  # @rbs return: ACP::Types::UsageUpdate
33
33
  def self.from_h(hash)
34
34
  new(
35
- used: hash.fetch('used'),
36
- size: hash.fetch('size'),
37
- cost: hash['cost']&.then { |value| ACP::Types::Cost.from_h(value) },
35
+ used: ACP::Types::Check.integer(ACP::Types::Check.key(hash, 'used'), 'used', min: 0),
36
+ size: ACP::Types::Check.integer(ACP::Types::Check.key(hash, 'size'), 'size', min: 0),
37
+ cost: ACP::Types::Check.object(hash['cost'], 'cost', ACP::Types::Cost, allow_nil: true),
38
38
  meta: hash['_meta']
39
39
  )
40
40
  end
@@ -26,7 +26,11 @@ class ACP::Types::WaitForTerminalExitRequest
26
26
  # @rbs hash: Hash[String, untyped]
27
27
  # @rbs return: ACP::Types::WaitForTerminalExitRequest
28
28
  def self.from_h(hash)
29
- new(session_id: hash.fetch('sessionId'), terminal_id: hash.fetch('terminalId'), meta: hash['_meta'])
29
+ new(
30
+ session_id: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'sessionId'), 'sessionId'),
31
+ terminal_id: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'terminalId'), 'terminalId'),
32
+ meta: hash['_meta']
33
+ )
30
34
  end
31
35
 
32
36
  # @rbs return: Hash[String, untyped]
@@ -26,7 +26,11 @@ class ACP::Types::WaitForTerminalExitResponse
26
26
  # @rbs hash: Hash[String, untyped]
27
27
  # @rbs return: ACP::Types::WaitForTerminalExitResponse
28
28
  def self.from_h(hash)
29
- new(exit_code: hash['exitCode'], signal: hash['signal'], meta: hash['_meta'])
29
+ new(
30
+ exit_code: ACP::Types::Check.integer(hash['exitCode'], 'exitCode', min: 0, allow_nil: true),
31
+ signal: ACP::Types::Check.string(hash['signal'], 'signal', allow_nil: true),
32
+ meta: hash['_meta']
33
+ )
30
34
  end
31
35
 
32
36
  # @rbs return: Hash[String, untyped]
@@ -32,9 +32,9 @@ class ACP::Types::WriteTextFileRequest
32
32
  # @rbs return: ACP::Types::WriteTextFileRequest
33
33
  def self.from_h(hash)
34
34
  new(
35
- session_id: hash.fetch('sessionId'),
36
- path: hash.fetch('path'),
37
- content: hash.fetch('content'),
35
+ session_id: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'sessionId'), 'sessionId'),
36
+ path: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'path'), 'path'),
37
+ content: ACP::Types::Check.string(ACP::Types::Check.key(hash, 'content'), 'content'),
38
38
  meta: hash['_meta']
39
39
  )
40
40
  end
data/lib/acp/types.rb CHANGED
@@ -1,4 +1,251 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ACP::Types
4
+ # Raised by the generated from_h when a param does not match its schema
5
+ # type; the connection boundaries rescue it into an invalid-params reply
6
+ # that names the failing path.
7
+ class ParseError < StandardError
8
+ # @rbs @segments: Array[String | Integer]
9
+ # @rbs @detail: String
10
+
11
+ # @rbs segments: Array[String | Integer]
12
+ # @rbs detail: String
13
+ # @rbs return: void
14
+ def initialize(segments, detail)
15
+ @segments = segments.freeze
16
+ @detail = detail
17
+ super("#{path}: #{detail}")
18
+ end
19
+
20
+ # @dynamic segments
21
+ attr_reader :segments #: Array[String | Integer]
22
+
23
+ # @rbs return: String
24
+ def path
25
+ segments.each_with_index.map do |segment, index|
26
+ if segment.is_a?(Integer) then "[#{segment}]"
27
+ elsif index.zero? then segment
28
+ else ".#{segment}"
29
+ end
30
+ end.join
31
+ end
32
+
33
+ # Prepends a container's segments so nested failures report an absolute path.
34
+ #
35
+ # @rbs *segments: (String | Integer)
36
+ # @rbs return: ACP::Types::ParseError
37
+ def prefix(*segments)
38
+ ParseError.new([*segments, *@segments], @detail)
39
+ end
40
+ end
41
+
42
+ # The guards the generated from_h methods call. Each takes the fetched value
43
+ # and the path to it, and raises ParseError naming that path when the value
44
+ # does not match the schema type. Nested from_h failures come back with the
45
+ # field path prepended, so the reported path is absolute.
46
+ module Check
47
+ # @rbs hash: Hash[String, untyped]
48
+ # @rbs key: String
49
+ # @rbs return: untyped
50
+ def self.key(hash, key)
51
+ hash.fetch(key)
52
+ rescue KeyError
53
+ raise ParseError.new([key], 'is required')
54
+ end
55
+
56
+ # @rbs value: untyped
57
+ # @rbs path: String | Array[String | Integer]
58
+ # @rbs allow_nil: bool
59
+ # @rbs allow_unset: bool
60
+ # @rbs return: untyped
61
+ def self.string(value, path, allow_nil: false, allow_unset: false)
62
+ return value if value.is_a?(String) || skipped?(value, allow_nil, allow_unset)
63
+
64
+ raise ParseError.new(segments(path), "expected String, got #{describe(value)}")
65
+ end
66
+
67
+ # @rbs value: untyped
68
+ # @rbs path: String | Array[String | Integer]
69
+ # @rbs allow_nil: bool
70
+ # @rbs allow_unset: bool
71
+ # @rbs return: untyped
72
+ def self.boolean(value, path, allow_nil: false, allow_unset: false)
73
+ return value if value == true || value == false || skipped?(value, allow_nil, allow_unset)
74
+
75
+ raise ParseError.new(segments(path), "expected boolean, got #{describe(value)}")
76
+ end
77
+
78
+ # @rbs value: untyped
79
+ # @rbs path: String | Array[String | Integer]
80
+ # @rbs min: Integer?
81
+ # @rbs max: Integer?
82
+ # @rbs allow_nil: bool
83
+ # @rbs allow_unset: bool
84
+ # @rbs return: untyped
85
+ def self.integer(value, path, min: nil, max: nil, allow_nil: false, allow_unset: false)
86
+ return value if (value.is_a?(Integer) && within?(value, min, max)) || skipped?(value, allow_nil, allow_unset)
87
+
88
+ raise ParseError.new(segments(path), "expected #{bounded('Integer', min, max)}, got #{describe(value)}")
89
+ end
90
+
91
+ # @rbs value: untyped
92
+ # @rbs path: String | Array[String | Integer]
93
+ # @rbs min: Integer?
94
+ # @rbs max: Integer?
95
+ # @rbs allow_nil: bool
96
+ # @rbs allow_unset: bool
97
+ # @rbs return: untyped
98
+ def self.number(value, path, min: nil, max: nil, allow_nil: false, allow_unset: false)
99
+ return value if (value.is_a?(Numeric) && within?(value, min, max)) || skipped?(value, allow_nil, allow_unset)
100
+
101
+ raise ParseError.new(segments(path), "expected #{bounded('Numeric', min, max)}, got #{describe(value)}")
102
+ end
103
+
104
+ # @rbs value: untyped
105
+ # @rbs path: String | Array[String | Integer]
106
+ # @rbs values: Array[untyped]
107
+ # @rbs allow_nil: bool
108
+ # @rbs allow_unset: bool
109
+ # @rbs return: untyped
110
+ def self.enum(value, path, values, allow_nil: false, allow_unset: false)
111
+ return value if values.include?(value) || skipped?(value, allow_nil, allow_unset)
112
+
113
+ expected = values.map(&:inspect).join(', ')
114
+ raise ParseError.new(segments(path), "expected one of #{expected}, got #{describe(value)}")
115
+ end
116
+
117
+ # @rbs value: untyped
118
+ # @rbs path: String | Array[String | Integer]
119
+ # @rbs classes: Array[untyped]
120
+ # @rbs allow_nil: bool
121
+ # @rbs allow_unset: bool
122
+ # @rbs ?&items: (untyped, Array[String | Integer]) -> void
123
+ # @rbs return: untyped
124
+ def self.one_of(value, path, classes, allow_nil: false, allow_unset: false, &items)
125
+ return value if skipped?(value, allow_nil, allow_unset)
126
+
127
+ matched = classes.any? { |klass| value.is_a?(klass) }
128
+ unless matched
129
+ raise ParseError.new(segments(path), "expected #{classes.map(&:name).join(' | ')}, got #{describe(value)}")
130
+ end
131
+
132
+ return value unless value.is_a?(Array) && items
133
+
134
+ value.each_with_index { |item, index| yield(item, [*segments(path), index]) }
135
+ value
136
+ end
137
+
138
+ # @rbs value: untyped
139
+ # @rbs path: String | Array[String | Integer]
140
+ # @rbs allow_nil: bool
141
+ # @rbs allow_unset: bool
142
+ # @rbs &check: (untyped, Array[String | Integer]) -> void
143
+ # @rbs return: untyped
144
+ def self.hash(value, path, allow_nil: false, allow_unset: false, &check)
145
+ return value if skipped?(value, allow_nil, allow_unset)
146
+ raise ParseError.new(segments(path), "expected object, got #{describe(value)}") unless value.is_a?(Hash)
147
+
148
+ parsed = {} #: Hash[String, untyped]
149
+ value.each { |key, item| parsed[key] = yield(item, [*segments(path), key]) }
150
+ parsed
151
+ end
152
+
153
+ # @rbs value: untyped
154
+ # @rbs path: String | Array[String | Integer]
155
+ # @rbs allow_nil: bool
156
+ # @rbs allow_unset: bool
157
+ # @rbs &check: (untyped, Array[String | Integer]) -> void
158
+ # @rbs return: untyped
159
+ def self.array(value, path, allow_nil: false, allow_unset: false, &check)
160
+ return value if skipped?(value, allow_nil, allow_unset)
161
+ raise ParseError.new(segments(path), "expected array, got #{describe(value)}") unless value.is_a?(Array)
162
+
163
+ value.each_with_index.map { |item, index| yield(item, [*segments(path), index]) }
164
+ end
165
+
166
+ # @rbs value: untyped
167
+ # @rbs path: String | Array[String | Integer]
168
+ # @rbs klass: untyped
169
+ # @rbs allow_nil: bool
170
+ # @rbs allow_unset: bool
171
+ # @rbs return: untyped
172
+ def self.object(value, path, klass, allow_nil: false, allow_unset: false)
173
+ return value if skipped?(value, allow_nil, allow_unset)
174
+ raise ParseError.new(segments(path), "expected object, got #{describe(value)}") unless value.is_a?(Hash)
175
+
176
+ begin
177
+ klass.from_h(value)
178
+ rescue ParseError => e
179
+ raise e.prefix(*segments(path))
180
+ end
181
+ end
182
+
183
+ # @rbs value: untyped
184
+ # @rbs path: String | Array[String | Integer]
185
+ # @rbs klass: untyped
186
+ # @rbs allow_nil: bool
187
+ # @rbs allow_unset: bool
188
+ # @rbs return: untyped
189
+ def self.objects(value, path, klass, allow_nil: false, allow_unset: false)
190
+ return value if skipped?(value, allow_nil, allow_unset)
191
+ raise ParseError.new(segments(path), "expected array, got #{describe(value)}") unless value.is_a?(Array)
192
+
193
+ value.each_with_index do |item, index|
194
+ next if item.is_a?(Hash)
195
+
196
+ raise ParseError.new([*segments(path), index], "expected object, got #{describe(item)}")
197
+ end
198
+
199
+ begin
200
+ klass.from_a(value)
201
+ rescue ParseError => e
202
+ raise e.prefix(*segments(path))
203
+ end
204
+ end
205
+
206
+ # @rbs value: untyped
207
+ # @rbs allow_nil: bool
208
+ # @rbs allow_unset: bool
209
+ # @rbs return: bool
210
+ def self.skipped?(value, allow_nil, allow_unset)
211
+ (value.nil? && allow_nil) || (allow_unset && value.equal?(:unset))
212
+ end
213
+
214
+ # @rbs path: String | Array[String | Integer]
215
+ # @rbs return: Array[String | Integer]
216
+ def self.segments(path)
217
+ path.is_a?(Array) ? path : [path]
218
+ end
219
+
220
+ # @rbs value: untyped
221
+ # @rbs return: String
222
+ def self.describe(value)
223
+ case value
224
+ when nil then 'null'
225
+ when true then 'true'
226
+ when false then 'false'
227
+ else value.class.name
228
+ end
229
+ end
230
+
231
+ # @rbs value: Numeric
232
+ # @rbs min: Integer?
233
+ # @rbs max: Integer?
234
+ # @rbs return: bool
235
+ def self.within?(value, min, max)
236
+ (min.nil? || value >= min) && (max.nil? || value <= max)
237
+ end
238
+
239
+ # @rbs name: String
240
+ # @rbs min: Integer?
241
+ # @rbs max: Integer?
242
+ # @rbs return: String
243
+ def self.bounded(name, min, max)
244
+ if min && max then "#{name} between #{min} and #{max}"
245
+ elsif min then "#{name} >= #{min}"
246
+ elsif max then "#{name} <= #{max}"
247
+ else name
248
+ end
249
+ end
250
+ end
4
251
  end
data/lib/acp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ACP
4
- VERSION = '0.7.0'
4
+ VERSION = '0.8.0'
5
5
  end
@@ -19,12 +19,18 @@ class ACP::ClientConnection
19
19
 
20
20
  @release_terminal: (^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError))?
21
21
 
22
+ @elicitation: (^(ACP::Types::CreateElicitationRequest) -> (ACP::Types::CreateElicitationResponse | ACP::RequestError))?
23
+
24
+ @complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?
25
+
22
26
  @updates: ACP::ClientConnection::_UpdateHandler
23
27
 
24
28
  @fs_capabilities: ACP::Types::FileSystemCapabilities?
25
29
 
26
30
  @terminal: bool?
27
31
 
32
+ @elicitation_capabilities: ACP::Types::ElicitationCapabilities?
33
+
28
34
  @extension_requests: Hash[String, ^(untyped) -> untyped]
29
35
 
30
36
  @extension_notifications: Hash[String, ^(untyped) -> void]
@@ -35,6 +41,8 @@ class ACP::ClientConnection
35
41
 
36
42
  @pending_permissions: Hash[String, Array[Thread::Queue]]
37
43
 
44
+ @outstanding_elicitations: Hash[String, bool]
45
+
38
46
  @logger: ACP::Transport::_Logger
39
47
 
40
48
  PROTOCOL_VERSION: Integer
@@ -53,12 +61,15 @@ class ACP::ClientConnection
53
61
  # (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError))?
54
62
  # @rbs kill_terminal: (^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError))?
55
63
  # @rbs release_terminal: (^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError))?
64
+ # @rbs elicitation: (^(ACP::Types::CreateElicitationRequest) ->
65
+ # (ACP::Types::CreateElicitationResponse | ACP::RequestError))?
66
+ # @rbs complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?
56
67
  # @rbs updates: ACP::ClientConnection::_UpdateHandler
57
68
  # @rbs extension_requests: Hash[String, ^(untyped) -> untyped]
58
69
  # @rbs extension_notifications: Hash[String, ^(untyped) -> void]
59
70
  # @rbs logger: ACP::Transport::_Logger
60
71
  # @rbs return: void
61
- def initialize: (transport: ACP::AgentConnection::_Transport, permission: ACP::ClientConnection::_PermissionHandler, ?read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?, ?write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?, ?create_terminal: (^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError))?, ?terminal_output: (^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError))?, ?wait_for_terminal_exit: (^(ACP::Types::WaitForTerminalExitRequest) -> (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError))?, ?kill_terminal: (^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError))?, ?release_terminal: (^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError))?, ?updates: ACP::ClientConnection::_UpdateHandler, ?extension_requests: Hash[String, ^(untyped) -> untyped], ?extension_notifications: Hash[String, ^(untyped) -> void], ?logger: ACP::Transport::_Logger) -> void
72
+ def initialize: (transport: ACP::AgentConnection::_Transport, permission: ACP::ClientConnection::_PermissionHandler, ?read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?, ?write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?, ?create_terminal: (^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError))?, ?terminal_output: (^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError))?, ?wait_for_terminal_exit: (^(ACP::Types::WaitForTerminalExitRequest) -> (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError))?, ?kill_terminal: (^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError))?, ?release_terminal: (^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError))?, ?elicitation: (^(ACP::Types::CreateElicitationRequest) -> (ACP::Types::CreateElicitationResponse | ACP::RequestError))?, ?complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?, ?updates: ACP::ClientConnection::_UpdateHandler, ?extension_requests: Hash[String, ^(untyped) -> untyped], ?extension_notifications: Hash[String, ^(untyped) -> void], ?logger: ACP::Transport::_Logger) -> void
62
73
 
63
74
  # @rbs return: Thread
64
75
  def start: () -> Thread
@@ -226,6 +237,26 @@ class ACP::ClientConnection
226
237
  # @rbs return: (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
227
238
  def release_terminal: (untyped params) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
228
239
 
240
+ # The routes are registered at start, before connect records the advertised
241
+ # capabilities, so a request for an unadvertised mode answers -32602.
242
+ #
243
+ # @rbs params: untyped
244
+ # @rbs return: (ACP::Types::CreateElicitationResponse | ACP::RequestError)
245
+ def create_elicitation: (untyped params) -> (ACP::Types::CreateElicitationResponse | ACP::RequestError)
246
+
247
+ # The spec requires a completion for an unknown or already-completed
248
+ # elicitation id to be ignored, so only an id registered by a served
249
+ # url-mode request reaches the handler. It runs on the transport's reader
250
+ # thread, so it must return quickly.
251
+ #
252
+ # @rbs params: untyped
253
+ # @rbs return: void
254
+ def complete_elicitation: (untyped params) -> void
255
+
256
+ # @rbs elicitation_id: String
257
+ # @rbs return: void
258
+ def register_elicitation: (String elicitation_id) -> void
259
+
229
260
  # The fs capabilities initialize advertises that no injected handler serves.
230
261
  #
231
262
  # @rbs capabilities: ACP::Types::FileSystemCapabilities?
@@ -238,4 +269,11 @@ class ACP::ClientConnection
238
269
  # @rbs capabilities: bool?
239
270
  # @rbs return: Array[String]
240
271
  def unserved_terminal_methods: (bool? capabilities) -> Array[String]
272
+
273
+ # The elicitation capabilities initialize advertises that no injected
274
+ # handler serves.
275
+ #
276
+ # @rbs capabilities: ACP::Types::ElicitationCapabilities?
277
+ # @rbs return: Array[String]
278
+ def unserved_elicitation_modes: (ACP::Types::ElicitationCapabilities? capabilities) -> Array[String]
241
279
  end
@@ -48,8 +48,9 @@ class ACP::RequestError
48
48
  # @rbs return: ACP::RequestError
49
49
  def self.method_not_found: (?String? method) -> ACP::RequestError
50
50
 
51
+ # @rbs errors: Array[String]?
51
52
  # @rbs return: ACP::RequestError
52
- def self.invalid_params: () -> ACP::RequestError
53
+ def self.invalid_params: (?Array[String]? errors) -> ACP::RequestError
53
54
 
54
55
  # The method-not-found code for a method the client's capabilities do not
55
56
  # advertise, with a message that names the capability so it can be told
@@ -59,6 +60,16 @@ class ACP::RequestError
59
60
  # @rbs return: ACP::RequestError
60
61
  def self.unadvertised: (String capability) -> ACP::RequestError
61
62
 
63
+ # The invalid-params code for a request whose mode the client's
64
+ # capabilities do not advertise, with a message that names the mode so it
65
+ # can be told apart from malformed params. The spec gives elicitation a
66
+ # mode in its params, so an unadvertised one is invalid params rather than
67
+ # method not found.
68
+ #
69
+ # @rbs mode: String
70
+ # @rbs return: ACP::RequestError
71
+ def self.unadvertised_mode: (String mode) -> ACP::RequestError
72
+
62
73
  # @rbs return: ACP::RequestError
63
74
  def self.internal_error: () -> ACP::RequestError
64
75