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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -0
- data/README.md +20 -8
- data/lib/acp/agent_connection/client.rb +31 -13
- data/lib/acp/agent_connection/optional_method/spec.rb +42 -0
- data/lib/acp/agent_connection/optional_method.rb +36 -0
- data/lib/acp/agent_connection.rb +67 -20
- data/lib/acp/agent_process.rb +0 -13
- data/lib/acp/client_connection/streams.rb +183 -0
- data/lib/acp/client_connection.rb +280 -338
- data/lib/acp/protocol.rb +33 -0
- data/lib/acp/request_error.rb +13 -13
- data/lib/acp/transport/cancellation.rb +13 -3
- data/lib/acp/transport/reply.rb +0 -2
- data/lib/acp/transport/stderr_logger.rb +1 -2
- data/lib/acp/transport/stdio.rb +25 -26
- data/lib/acp/types/audio_content.rb +3 -46
- data/lib/acp/types/auth_method_terminal.rb +3 -59
- data/lib/acp/types/available_commands_update.rb +3 -32
- data/lib/acp/types/boolean_property_schema.rb +3 -41
- data/lib/acp/types/check.rb +210 -0
- data/lib/acp/types/config_option_update.rb +3 -32
- data/lib/acp/types/content.rb +3 -29
- data/lib/acp/types/{elicitation_url_mode → create_elicitation_request/mode/form}/scope.rb +1 -1
- data/lib/acp/types/create_elicitation_request/mode/form.rb +3 -3
- data/lib/acp/types/{elicitation_form_mode → create_elicitation_request/mode/url}/scope.rb +1 -1
- data/lib/acp/types/create_elicitation_request/mode/url.rb +3 -3
- data/lib/acp/types/current_mode_update.rb +3 -29
- data/lib/acp/types/diff.rb +3 -41
- data/lib/acp/types/elicitation_accept_action.rb +3 -26
- data/lib/acp/types/elicitation_form_mode.rb +3 -33
- data/lib/acp/types/elicitation_url_mode.rb +3 -35
- data/lib/acp/types/embedded_resource.rb +3 -44
- data/lib/acp/types/image_content.rb +3 -58
- data/lib/acp/types/integer_property_schema.rb +3 -60
- data/lib/acp/types/mcp_server_http.rb +3 -44
- data/lib/acp/types/mcp_server_sse.rb +3 -44
- data/lib/acp/types/multi_select_property_schema.rb +3 -70
- data/lib/acp/types/number_property_schema.rb +3 -60
- data/lib/acp/types/parse_error.rb +37 -0
- data/lib/acp/types/plan.rb +3 -29
- data/lib/acp/types/resource_link.rb +3 -79
- data/lib/acp/types/selected_permission_outcome.rb +3 -26
- data/lib/acp/types/session_config_boolean.rb +3 -21
- data/lib/acp/types/session_config_select.rb +3 -33
- data/lib/acp/types/session_info_update.rb +3 -42
- data/lib/acp/types/string_multi_select_items.rb +3 -29
- data/lib/acp/types/string_property_schema.rb +3 -105
- data/lib/acp/types/terminal.rb +3 -29
- data/lib/acp/types/text_content.rb +3 -40
- data/lib/acp/types/tool_call.rb +3 -115
- data/lib/acp/types/unstable/compaction_summary_chunk.rb +3 -35
- data/lib/acp/types/unstable/compaction_update.rb +3 -56
- data/lib/acp/types/unstable/idle_state_update.rb +3 -40
- data/lib/acp/types/unstable/mcp_server_acp.rb +3 -35
- data/lib/acp/types/unstable/message_mcp_response/result.rb +1 -1
- data/lib/acp/types/unstable/nes_edit_suggestion.rb +3 -61
- data/lib/acp/types/unstable/nes_jump_suggestion.rb +3 -45
- data/lib/acp/types/unstable/nes_rename_suggestion.rb +3 -51
- data/lib/acp/types/unstable/nes_search_and_replace_suggestion.rb +3 -60
- data/lib/acp/types/unstable/notice.rb +3 -41
- data/lib/acp/types/unstable/plan_file.rb +3 -35
- data/lib/acp/types/unstable/plan_items.rb +3 -38
- data/lib/acp/types/unstable/plan_markdown.rb +3 -35
- data/lib/acp/types/unstable/plan_removed.rb +3 -26
- data/lib/acp/types/unstable/plan_update.rb +3 -33
- data/lib/acp/types/unstable/requires_action_state_update.rb +3 -21
- data/lib/acp/types/unstable/running_state_update.rb +3 -21
- data/lib/acp/types/unstable/session_message.rb +3 -56
- data/lib/acp/types/unstable/session_message_chunk.rb +3 -53
- data/lib/acp/types/unstable/subagent_update.rb +3 -65
- data/lib/acp/types/unstable/unknown_state_update.rb +3 -21
- data/lib/acp/types/unstable.rb +20 -20
- data/lib/acp/types/usage_update.rb +3 -41
- data/lib/acp/types.rb +7 -246
- data/lib/acp/version.rb +1 -1
- data/sig/generated/acp/agent_connection/client.rbs +25 -15
- data/sig/generated/acp/agent_connection/optional_method/spec.rbs +32 -0
- data/sig/generated/acp/agent_connection/optional_method.rbs +3 -0
- data/sig/generated/acp/agent_connection.rbs +34 -13
- data/sig/generated/acp/agent_process.rbs +0 -13
- data/sig/generated/acp/client_connection/streams.rbs +94 -0
- data/sig/generated/acp/client_connection.rbs +128 -131
- data/sig/generated/acp/protocol.rbs +9 -0
- data/sig/generated/acp/request_error.rbs +11 -13
- data/sig/generated/acp/transport/cancellation.rbs +9 -3
- data/sig/generated/acp/transport/reply.rbs +0 -2
- data/sig/generated/acp/transport/stderr_logger.rbs +1 -2
- data/sig/generated/acp/transport/stdio.rbs +14 -22
- data/sig/generated/acp/types/audio_content.rbs +1 -27
- data/sig/generated/acp/types/auth_method_terminal.rbs +1 -35
- data/sig/generated/acp/types/available_commands_update.rbs +1 -19
- data/sig/generated/acp/types/boolean_property_schema.rbs +1 -27
- data/sig/generated/acp/types/check.rbs +115 -0
- data/sig/generated/acp/types/config_option_update.rbs +1 -19
- data/sig/generated/acp/types/content.rbs +1 -19
- data/sig/generated/acp/types/{elicitation_url_mode → create_elicitation_request/mode/form}/scope.rbs +2 -2
- data/sig/generated/acp/types/create_elicitation_request/mode/form.rbs +3 -3
- data/sig/generated/acp/types/{elicitation_form_mode → create_elicitation_request/mode/url}/scope.rbs +2 -2
- data/sig/generated/acp/types/create_elicitation_request/mode/url.rbs +3 -3
- data/sig/generated/acp/types/current_mode_update.rbs +1 -19
- data/sig/generated/acp/types/diff.rbs +1 -27
- data/sig/generated/acp/types/elicitation_accept_action.rbs +1 -15
- data/sig/generated/acp/types/elicitation_form_mode.rbs +1 -19
- data/sig/generated/acp/types/elicitation_url_mode.rbs +1 -23
- data/sig/generated/acp/types/embedded_resource.rbs +1 -23
- data/sig/generated/acp/types/image_content.rbs +1 -31
- data/sig/generated/acp/types/integer_property_schema.rbs +1 -35
- data/sig/generated/acp/types/mcp_server_http.rbs +1 -27
- data/sig/generated/acp/types/mcp_server_sse.rbs +1 -27
- data/sig/generated/acp/types/multi_select_property_schema.rbs +1 -39
- data/sig/generated/acp/types/number_property_schema.rbs +1 -35
- data/sig/generated/acp/types/parse_error.rbs +22 -0
- data/sig/generated/acp/types/plan.rbs +1 -19
- data/sig/generated/acp/types/resource_link.rbs +1 -43
- data/sig/generated/acp/types/selected_permission_outcome.rbs +1 -19
- data/sig/generated/acp/types/session_config_boolean.rbs +1 -15
- data/sig/generated/acp/types/session_config_select.rbs +1 -19
- data/sig/generated/acp/types/session_info_update.rbs +1 -23
- data/sig/generated/acp/types/string_multi_select_items.rbs +1 -19
- data/sig/generated/acp/types/string_property_schema.rbs +1 -51
- data/sig/generated/acp/types/terminal.rbs +1 -19
- data/sig/generated/acp/types/text_content.rbs +1 -23
- data/sig/generated/acp/types/tool_call.rbs +1 -51
- data/sig/generated/acp/types/unstable/compaction_summary_chunk.rbs +1 -23
- data/sig/generated/acp/types/unstable/compaction_update.rbs +1 -31
- data/sig/generated/acp/types/unstable/idle_state_update.rbs +1 -23
- data/sig/generated/acp/types/unstable/mcp_server_acp.rbs +1 -23
- data/sig/generated/acp/types/unstable/nes_edit_suggestion.rbs +1 -31
- data/sig/generated/acp/types/unstable/nes_jump_suggestion.rbs +1 -27
- data/sig/generated/acp/types/unstable/nes_rename_suggestion.rbs +1 -31
- data/sig/generated/acp/types/unstable/nes_search_and_replace_suggestion.rbs +1 -35
- data/sig/generated/acp/types/unstable/notice.rbs +1 -27
- data/sig/generated/acp/types/unstable/plan_file.rbs +1 -23
- data/sig/generated/acp/types/unstable/plan_items.rbs +1 -23
- data/sig/generated/acp/types/unstable/plan_markdown.rbs +1 -23
- data/sig/generated/acp/types/unstable/plan_removed.rbs +1 -19
- data/sig/generated/acp/types/unstable/plan_update.rbs +1 -19
- data/sig/generated/acp/types/unstable/requires_action_state_update.rbs +1 -15
- data/sig/generated/acp/types/unstable/running_state_update.rbs +1 -15
- data/sig/generated/acp/types/unstable/session_message.rbs +1 -31
- data/sig/generated/acp/types/unstable/session_message_chunk.rbs +1 -31
- data/sig/generated/acp/types/unstable/subagent_update.rbs +1 -35
- data/sig/generated/acp/types/unstable/unknown_state_update.rbs +1 -15
- data/sig/generated/acp/types/usage_update.rbs +1 -27
- data/sig/generated/acp/types.rbs +6 -143
- data/sig/manual/acp/agent_connection.rbs +28 -28
- data/sig/manual/acp/client_connection.rbs +7 -2
- data/sig/manual/acp/{agent_connection/optional_method/logout.rbs → protocol.rbs} +3 -3
- data/sig/manual/acp/transport/stdio.rbs +0 -2
- data/sig/manual/acp/transport.rbs +25 -2
- metadata +17 -29
- data/lib/acp/agent_connection/optional_method/authenticate.rb +0 -21
- data/lib/acp/agent_connection/optional_method/close_session.rb +0 -21
- data/lib/acp/agent_connection/optional_method/delete_session.rb +0 -21
- data/lib/acp/agent_connection/optional_method/list_sessions.rb +0 -21
- data/lib/acp/agent_connection/optional_method/load_session.rb +0 -21
- data/lib/acp/agent_connection/optional_method/logout.rb +0 -21
- data/lib/acp/agent_connection/optional_method/resume_session.rb +0 -21
- data/lib/acp/types/content_chunk.rb +0 -40
- data/sig/generated/acp/agent_connection/optional_method/authenticate.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/close_session.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/delete_session.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/list_sessions.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/load_session.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/logout.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/resume_session.rbs +0 -13
- data/sig/generated/acp/types/content_chunk.rbs +0 -25
- data/sig/manual/acp/agent_connection/optional_method/authenticate.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/close_session.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/delete_session.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/list_sessions.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/load_session.rbs +0 -6
- 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
|