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,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ # It holds no send facade of its own — requests go through the sender and
4
+ # answers to the agent ride the connection's routes — so it is unit-testable
5
+ # without a transport.
6
+ class ACP::ClientConnection::Streams
7
+ CANCELLED = ACP::Types::RequestPermissionResponse.new(
8
+ outcome: ACP::Types::RequestPermissionOutcome::Cancelled.new
9
+ ) #: ACP::Types::RequestPermissionResponse
10
+
11
+ # @rbs @sender: ACP::ClientConnection::Streams::_Sender
12
+ # @rbs @permission: ACP::ClientConnection::_PermissionHandler
13
+ # @rbs @updates: ACP::ClientConnection::_UpdateHandler
14
+ # @rbs @complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?
15
+ # @rbs @logger: ACP::Transport::_Logger
16
+ # @rbs @lock: Thread::Mutex
17
+ # @rbs @streams: Hash[String, Thread::Queue]
18
+ # @rbs @pending_permissions: Hash[String, Array[Thread::Queue]]
19
+ # @rbs @outstanding_elicitations: Hash[String, bool]
20
+
21
+ # @rbs sender: ACP::ClientConnection::Streams::_Sender
22
+ # @rbs permission: ACP::ClientConnection::_PermissionHandler
23
+ # @rbs updates: ACP::ClientConnection::_UpdateHandler
24
+ # @rbs logger: ACP::Transport::_Logger
25
+ # @rbs complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?
26
+ # @rbs return: void
27
+ def initialize(sender:, permission:, updates:, logger:, complete_elicitation: nil)
28
+ @sender = sender
29
+ @permission = permission
30
+ @updates = updates
31
+ @complete_elicitation = complete_elicitation
32
+ @logger = logger
33
+ @lock = Mutex.new
34
+ @streams = {}
35
+ @pending_permissions = {}
36
+ @outstanding_elicitations = {}
37
+ end
38
+
39
+ # The request waits on its own thread so the caller's thread can yield each
40
+ # update as it arrives. The reader thread queues an update before it settles
41
+ # a reply sent after it, so the queue closes only after the last one.
42
+ #
43
+ # @rbs session_id: String
44
+ # @rbs method: String
45
+ # @rbs params: Hash[String, untyped]
46
+ # @rbs &block: (ACP::Types::SessionUpdate::t) -> void
47
+ # @rbs return: (Hash[String, untyped] | ACP::RequestError)
48
+ def stream(session_id, method, params, &)
49
+ queue = Thread::Queue.new
50
+ # The check and the insert share one lock acquisition so a concurrent
51
+ # stream cannot register between them.
52
+ claimed = @lock.synchronize do
53
+ if @streams.key?(session_id)
54
+ false
55
+ else
56
+ @streams[session_id] = queue
57
+ true
58
+ end
59
+ end
60
+ unless claimed
61
+ return ACP::RequestError.new(
62
+ code: ACP::RequestError::INVALID_REQUEST, message: "A stream is already open for session #{session_id}"
63
+ )
64
+ end
65
+ requester = Thread.new do
66
+ @sender.request(method, params)
67
+ ensure
68
+ @lock.synchronize { @streams.delete(session_id)&.close }
69
+ end
70
+ while (update = queue.pop)
71
+ yield update
72
+ end
73
+ requester.value
74
+ end
75
+
76
+ # Pushes under the lock so a stream cannot close its queue between the
77
+ # lookup and the push. A malformed notification has no reply to carry the
78
+ # failure.
79
+ #
80
+ # @rbs params: untyped
81
+ # @rbs return: void
82
+ def dispatch(params)
83
+ notification = ACP::Types::SessionNotification.from_h(params)
84
+ rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError => e
85
+ @logger.warn("dropped malformed session/update: #{e.class}: #{e.message}")
86
+ else
87
+ queue = @lock.synchronize { @streams[notification.session_id]&.push(notification.update) }
88
+ @updates.call(notification) unless queue
89
+ end
90
+
91
+ # The handler runs on its own thread so a cancelled request is answered
92
+ # without waiting for it, and whatever it returns after that is pushed to a
93
+ # queue nobody reads.
94
+ #
95
+ # @rbs params: untyped
96
+ # @rbs return: (ACP::Types::RequestPermissionResponse | ACP::RequestError)
97
+ def request_permission(params)
98
+ request = ACP::Types::RequestPermissionRequest.from_h(params)
99
+ rescue ACP::Types::ParseError => e
100
+ ACP::RequestError.invalid_params([e.message])
101
+ rescue KeyError, TypeError, NoMethodError
102
+ ACP::RequestError.invalid_params
103
+ else
104
+ replies = Thread::Queue.new
105
+ register_permission(request.session_id, replies)
106
+ # The handler runs on the transport's serve thread, so the serving
107
+ # identity must be carried over for cancelled? to see the cancel there.
108
+ serving = @sender.cancellation
109
+ Thread.new do
110
+ ACP::Transport::Cancellation.current = serving
111
+ # A raised StandardError is not a returned ACP::RequestError, so the two
112
+ # need distinct shapes on the queue.
113
+ replies << begin
114
+ [:value, @permission.call(request)]
115
+ rescue StandardError => e
116
+ [:raised, e]
117
+ end
118
+ ensure
119
+ unregister_permission(request.session_id, replies)
120
+ end
121
+ settled = replies.pop
122
+ # Re-raised so the transport's invoke turns it into an error reply, as it
123
+ # would if the handler ran on the serve thread itself.
124
+ raise settled[1] if settled[0] == :raised
125
+
126
+ settled[1]
127
+ end
128
+
129
+ # The spec requires a pending session/request_permission to be answered with
130
+ # the cancelled outcome once the turn is cancelled, so every queue registered
131
+ # for the session gets one before this returns.
132
+ #
133
+ # @rbs session_id: String
134
+ # @rbs return: void
135
+ def cancel(session_id)
136
+ pending = @lock.synchronize { @pending_permissions.delete(session_id) }
137
+ pending&.each { |replies| replies << [:value, CANCELLED] }
138
+ end
139
+
140
+ # @rbs elicitation_id: String
141
+ # @rbs return: void
142
+ def register_elicitation(elicitation_id)
143
+ @lock.synchronize { @outstanding_elicitations[elicitation_id] = true }
144
+ end
145
+
146
+ # The spec requires a completion for an unknown or already-completed
147
+ # elicitation id to be ignored, so only an id registered by a served
148
+ # url-mode request reaches the handler. It runs on the transport's reader
149
+ # thread, so it must return quickly.
150
+ #
151
+ # @rbs params: untyped
152
+ # @rbs return: void
153
+ def complete_elicitation(params)
154
+ notification = ACP::Types::CompleteElicitationNotification.from_h(params)
155
+ rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError => e
156
+ @logger.warn("dropped malformed elicitation/complete: #{e.class}: #{e.message}")
157
+ else
158
+ known = @lock.synchronize { @outstanding_elicitations.delete(notification.elicitation_id) }
159
+ @complete_elicitation&.call(notification) if known
160
+ end
161
+
162
+ private
163
+
164
+ # @rbs session_id: String
165
+ # @rbs replies: Thread::Queue
166
+ # @rbs return: void
167
+ def register_permission(session_id, replies)
168
+ @lock.synchronize { (@pending_permissions[session_id] ||= []) << replies }
169
+ end
170
+
171
+ # @rbs session_id: String
172
+ # @rbs replies: Thread::Queue
173
+ # @rbs return: void
174
+ def unregister_permission(session_id, replies)
175
+ @lock.synchronize do
176
+ pending = @pending_permissions[session_id]
177
+ if pending
178
+ pending.delete(replies)
179
+ @pending_permissions.delete(session_id) if pending.empty?
180
+ end
181
+ end
182
+ end
183
+ end