acp-sdk 0.10.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 +21 -0
- data/README.md +15 -5
- data/lib/acp/agent_connection/client.rb +7 -19
- 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 +17 -40
- data/lib/acp/agent_process.rb +0 -13
- data/lib/acp/client_connection/streams.rb +183 -0
- data/lib/acp/client_connection.rb +227 -384
- data/lib/acp/protocol.rb +33 -0
- data/lib/acp/request_error.rb +7 -17
- data/lib/acp/transport/cancellation.rb +0 -7
- data/lib/acp/transport/reply.rb +0 -2
- data/lib/acp/transport/stderr_logger.rb +1 -2
- data/lib/acp/transport/stdio.rb +16 -22
- 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/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 +10 -19
- 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 +16 -24
- 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 +100 -159
- data/sig/generated/acp/protocol.rbs +9 -0
- data/sig/generated/acp/request_error.rbs +7 -17
- data/sig/generated/acp/transport/cancellation.rbs +0 -7
- 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 +9 -21
- 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 +0 -30
- 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 +16 -40
- 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/disable_provider.rb +0 -23
- data/lib/acp/agent_connection/optional_method/fork_session.rb +0 -23
- data/lib/acp/agent_connection/optional_method/list_providers.rb +0 -23
- 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/agent_connection/optional_method/set_provider.rb +0 -23
- 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/disable_provider.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/fork_session.rbs +0 -13
- data/sig/generated/acp/agent_connection/optional_method/list_providers.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/agent_connection/optional_method/set_provider.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/disable_provider.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/fork_session.rbs +0 -6
- data/sig/manual/acp/agent_connection/optional_method/list_providers.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
- data/sig/manual/acp/agent_connection/optional_method/set_provider.rbs +0 -6
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class ACP::ClientConnection
|
|
4
|
-
# @rbs @transport: ACP::
|
|
4
|
+
# @rbs @transport: ACP::Transport::_Transport
|
|
5
5
|
# @rbs @permission: ACP::ClientConnection::_PermissionHandler
|
|
6
6
|
# @rbs @read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
7
7
|
# @rbs @write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
|
@@ -13,8 +13,6 @@ class ACP::ClientConnection
|
|
|
13
13
|
# @rbs @release_terminal: (^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError))?
|
|
14
14
|
# @rbs @elicitation: (^(ACP::Types::CreateElicitationRequest) ->
|
|
15
15
|
# (ACP::Types::CreateElicitationResponse | ACP::RequestError))?
|
|
16
|
-
# @rbs @complete_elicitation: (^(ACP::Types::CompleteElicitationNotification) -> void)?
|
|
17
|
-
# @rbs @updates: ACP::ClientConnection::_UpdateHandler
|
|
18
16
|
# @rbs @mcp_message: (^(ACP::Types::Unstable::MessageMcpRequest) ->
|
|
19
17
|
# (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError))?
|
|
20
18
|
# @rbs @mcp_advertised: bool?
|
|
@@ -23,21 +21,131 @@ class ACP::ClientConnection
|
|
|
23
21
|
# @rbs @elicitation_capabilities: ACP::Types::ElicitationCapabilities?
|
|
24
22
|
# @rbs @extension_requests: Hash[String, ^(untyped) -> untyped]
|
|
25
23
|
# @rbs @extension_notifications: Hash[String, ^(untyped) -> void]
|
|
26
|
-
# @rbs @
|
|
27
|
-
# @rbs @streams: Hash[String, Thread::Queue]
|
|
28
|
-
# @rbs @pending_permissions: Hash[String, Array[Thread::Queue]]
|
|
29
|
-
# @rbs @outstanding_elicitations: Hash[String, bool]
|
|
24
|
+
# @rbs @streams: ACP::ClientConnection::Streams
|
|
30
25
|
# @rbs @logger: ACP::Transport::_Logger
|
|
31
26
|
|
|
32
|
-
PROTOCOL_VERSION = ACP::AgentConnection::PROTOCOL_VERSION #: Integer
|
|
33
|
-
|
|
34
27
|
IGNORE = ->(_notification) {} #: ^(ACP::Types::SessionNotification) -> void
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
class Route
|
|
30
|
+
# @rbs @wire: String
|
|
31
|
+
# @rbs @type: ACP::Transport::_Parser
|
|
32
|
+
# @rbs @handler: Symbol
|
|
33
|
+
# @rbs @family: Symbol
|
|
34
|
+
# @rbs @advertised: ^(untyped) -> (bool | nil)
|
|
35
|
+
# @rbs @refuse: Symbol
|
|
36
|
+
# @rbs @key: String
|
|
37
|
+
# @rbs @label: String
|
|
38
|
+
|
|
39
|
+
# @rbs wire: String
|
|
40
|
+
# @rbs type: ACP::Transport::_Parser
|
|
41
|
+
# @rbs handler: Symbol
|
|
42
|
+
# @rbs family: Symbol
|
|
43
|
+
# @rbs key: String
|
|
44
|
+
# @rbs advertised: ^(untyped) -> (bool | nil)
|
|
45
|
+
# @rbs refuse: Symbol
|
|
46
|
+
# @rbs label: String?
|
|
47
|
+
# @rbs return: void
|
|
48
|
+
def initialize(
|
|
49
|
+
wire:,
|
|
50
|
+
type:,
|
|
51
|
+
handler:,
|
|
52
|
+
family:,
|
|
53
|
+
key:,
|
|
54
|
+
advertised: ->(caps) { caps },
|
|
55
|
+
refuse: :unadvertised,
|
|
56
|
+
label: nil
|
|
57
|
+
)
|
|
58
|
+
@wire = wire
|
|
59
|
+
@type = type
|
|
60
|
+
@handler = handler
|
|
61
|
+
@family = family
|
|
62
|
+
@advertised = advertised
|
|
63
|
+
@refuse = refuse
|
|
64
|
+
@key = key
|
|
65
|
+
# The capability key connect's pre-flight names; it differs from the
|
|
66
|
+
# refusal key where a family gates several routes at once.
|
|
67
|
+
@label = label || key
|
|
68
|
+
freeze
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @dynamic wire, type, handler, family, advertised, refuse, key, label
|
|
72
|
+
attr_reader :wire #: String
|
|
73
|
+
attr_reader :type #: ACP::Transport::_Parser
|
|
74
|
+
attr_reader :handler #: Symbol
|
|
75
|
+
attr_reader :family #: Symbol
|
|
76
|
+
attr_reader :advertised #: ^(untyped) -> (bool | nil)
|
|
77
|
+
attr_reader :refuse #: Symbol
|
|
78
|
+
attr_reader :key #: String
|
|
79
|
+
attr_reader :label #: String
|
|
80
|
+
end
|
|
39
81
|
|
|
40
|
-
#
|
|
82
|
+
# The routes are registered at start, before connect records the
|
|
83
|
+
# advertisement, so each one answers its refusal until then. The terminal
|
|
84
|
+
# capability is a single bool, so its five routes share one gate.
|
|
85
|
+
#
|
|
86
|
+
# The terminal handlers may block for as long as the command runs, so the
|
|
87
|
+
# transport must serve each request on its own thread for this not to hold
|
|
88
|
+
# up others.
|
|
89
|
+
SERVE = [
|
|
90
|
+
Route.new(
|
|
91
|
+
wire: 'fs/read_text_file',
|
|
92
|
+
type: ACP::Types::ReadTextFileRequest,
|
|
93
|
+
handler: :@read_text_file,
|
|
94
|
+
family: :@fs_capabilities,
|
|
95
|
+
advertised: ->(caps) { caps&.read_text_file },
|
|
96
|
+
key: 'fs.readTextFile'
|
|
97
|
+
),
|
|
98
|
+
Route.new(
|
|
99
|
+
wire: 'fs/write_text_file',
|
|
100
|
+
type: ACP::Types::WriteTextFileRequest,
|
|
101
|
+
handler: :@write_text_file,
|
|
102
|
+
family: :@fs_capabilities,
|
|
103
|
+
advertised: ->(caps) { caps&.write_text_file },
|
|
104
|
+
key: 'fs.writeTextFile'
|
|
105
|
+
),
|
|
106
|
+
Route.new(
|
|
107
|
+
wire: 'terminal/create',
|
|
108
|
+
type: ACP::Types::CreateTerminalRequest,
|
|
109
|
+
handler: :@create_terminal,
|
|
110
|
+
family: :@terminal,
|
|
111
|
+
key: 'terminal',
|
|
112
|
+
label: 'terminal.create'
|
|
113
|
+
),
|
|
114
|
+
Route.new(
|
|
115
|
+
wire: 'terminal/output',
|
|
116
|
+
type: ACP::Types::TerminalOutputRequest,
|
|
117
|
+
handler: :@terminal_output,
|
|
118
|
+
family: :@terminal,
|
|
119
|
+
key: 'terminal',
|
|
120
|
+
label: 'terminal.output'
|
|
121
|
+
),
|
|
122
|
+
Route.new(
|
|
123
|
+
wire: 'terminal/wait_for_exit',
|
|
124
|
+
type: ACP::Types::WaitForTerminalExitRequest,
|
|
125
|
+
handler: :@wait_for_terminal_exit,
|
|
126
|
+
family: :@terminal,
|
|
127
|
+
key: 'terminal',
|
|
128
|
+
label: 'terminal.wait_for_exit'
|
|
129
|
+
),
|
|
130
|
+
Route.new(
|
|
131
|
+
wire: 'terminal/kill',
|
|
132
|
+
type: ACP::Types::KillTerminalRequest,
|
|
133
|
+
handler: :@kill_terminal,
|
|
134
|
+
family: :@terminal,
|
|
135
|
+
key: 'terminal',
|
|
136
|
+
label: 'terminal.kill'
|
|
137
|
+
),
|
|
138
|
+
Route.new(
|
|
139
|
+
wire: 'terminal/release',
|
|
140
|
+
type: ACP::Types::ReleaseTerminalRequest,
|
|
141
|
+
handler: :@release_terminal,
|
|
142
|
+
family: :@terminal,
|
|
143
|
+
key: 'terminal',
|
|
144
|
+
label: 'terminal.release'
|
|
145
|
+
)
|
|
146
|
+
].freeze #: Array[ACP::ClientConnection::Route]
|
|
147
|
+
|
|
148
|
+
# @rbs transport: ACP::Transport::_Transport
|
|
41
149
|
# @rbs permission: ACP::ClientConnection::_PermissionHandler
|
|
42
150
|
# @rbs read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
43
151
|
# @rbs write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
|
@@ -55,6 +163,7 @@ class ACP::ClientConnection
|
|
|
55
163
|
# (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError))?
|
|
56
164
|
# @rbs extension_requests: Hash[String, ^(untyped) -> untyped]
|
|
57
165
|
# @rbs extension_notifications: Hash[String, ^(untyped) -> void]
|
|
166
|
+
# @rbs streams: ACP::ClientConnection::Streams?
|
|
58
167
|
# @rbs logger: ACP::Transport::_Logger
|
|
59
168
|
# @rbs return: void
|
|
60
169
|
def initialize(
|
|
@@ -73,10 +182,10 @@ class ACP::ClientConnection
|
|
|
73
182
|
mcp_message: nil,
|
|
74
183
|
extension_requests: {},
|
|
75
184
|
extension_notifications: {},
|
|
185
|
+
streams: nil,
|
|
76
186
|
logger: ACP::Transport::StderrLogger.new
|
|
77
187
|
)
|
|
78
188
|
@transport = transport
|
|
79
|
-
@permission = permission
|
|
80
189
|
@read_text_file = read_text_file
|
|
81
190
|
@write_text_file = write_text_file
|
|
82
191
|
@create_terminal = create_terminal
|
|
@@ -85,8 +194,6 @@ class ACP::ClientConnection
|
|
|
85
194
|
@kill_terminal = kill_terminal
|
|
86
195
|
@release_terminal = release_terminal
|
|
87
196
|
@elicitation = elicitation
|
|
88
|
-
@complete_elicitation = complete_elicitation
|
|
89
|
-
@updates = updates
|
|
90
197
|
@mcp_message = mcp_message
|
|
91
198
|
@mcp_advertised = nil
|
|
92
199
|
@fs_capabilities = nil
|
|
@@ -96,10 +203,7 @@ class ACP::ClientConnection
|
|
|
96
203
|
@extension_notifications = extension_notifications
|
|
97
204
|
extension_requests.each_key { |name| ACP::Extensions.validate_name(name) }
|
|
98
205
|
extension_notifications.each_key { |name| ACP::Extensions.validate_name(name) }
|
|
99
|
-
@
|
|
100
|
-
@streams = {}
|
|
101
|
-
@pending_permissions = {}
|
|
102
|
-
@outstanding_elicitations = {}
|
|
206
|
+
@streams = streams || Streams.new(sender: transport, permission:, updates:, logger:, complete_elicitation:)
|
|
103
207
|
@logger = logger
|
|
104
208
|
end
|
|
105
209
|
|
|
@@ -107,18 +211,13 @@ class ACP::ClientConnection
|
|
|
107
211
|
def start
|
|
108
212
|
@transport.start(
|
|
109
213
|
requests: {
|
|
110
|
-
'session/request_permission' => method(:request_permission),
|
|
111
|
-
'fs/read_text_file' => method(:read_text_file),
|
|
112
|
-
'fs/write_text_file' => method(:write_text_file),
|
|
113
|
-
'terminal/create' => method(:create_terminal),
|
|
114
|
-
'terminal/output' => method(:terminal_output),
|
|
115
|
-
'terminal/wait_for_exit' => method(:wait_for_terminal_exit),
|
|
116
|
-
'terminal/kill' => method(:kill_terminal),
|
|
117
|
-
'terminal/release' => method(:release_terminal),
|
|
214
|
+
'session/request_permission' => @streams.method(:request_permission),
|
|
118
215
|
'elicitation/create' => method(:create_elicitation)
|
|
119
|
-
}.merge(mcp_requests).merge(@extension_requests),
|
|
120
|
-
notifications: {
|
|
121
|
-
|
|
216
|
+
}.merge(SERVE.to_h { |route| [route.wire, serve(route)] }).merge(mcp_requests).merge(@extension_requests),
|
|
217
|
+
notifications: {
|
|
218
|
+
'session/update' => @streams.method(:dispatch),
|
|
219
|
+
'elicitation/complete' => @streams.method(:complete_elicitation)
|
|
220
|
+
}.merge(@extension_notifications)
|
|
122
221
|
)
|
|
123
222
|
end
|
|
124
223
|
|
|
@@ -128,33 +227,30 @@ class ACP::ClientConnection
|
|
|
128
227
|
# @rbs request: ACP::Types::InitializeRequest
|
|
129
228
|
# @rbs return: ACP::Types::InitializeResponse | ACP::RequestError
|
|
130
229
|
def connect(request)
|
|
131
|
-
capabilities = request.client_capabilities
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
230
|
+
capabilities = request.client_capabilities
|
|
231
|
+
fs = capabilities&.fs
|
|
232
|
+
terminal = capabilities&.terminal
|
|
233
|
+
check_served('fs', :@fs_capabilities, fs)
|
|
234
|
+
check_served('terminal', :@terminal, terminal)
|
|
136
235
|
|
|
137
|
-
|
|
138
|
-
unserved = unserved_terminal_methods(terminal)
|
|
139
|
-
unless unserved.empty?
|
|
140
|
-
raise ArgumentError, "initialize advertises terminal methods no handler serves: #{unserved.join(', ')}"
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
elicitation = request.client_capabilities&.elicitation
|
|
236
|
+
elicitation = capabilities&.elicitation
|
|
144
237
|
unserved = unserved_elicitation_modes(elicitation)
|
|
145
238
|
unless unserved.empty?
|
|
146
239
|
raise ArgumentError, "initialize advertises elicitation modes no handler serves: #{unserved.join(', ')}"
|
|
147
240
|
end
|
|
148
241
|
|
|
149
|
-
@fs_capabilities =
|
|
242
|
+
@fs_capabilities = fs
|
|
150
243
|
@terminal = terminal
|
|
151
244
|
@elicitation_capabilities = elicitation
|
|
152
245
|
result = @transport.request('initialize', request.to_h)
|
|
153
246
|
response = parse(ACP::Types::InitializeResponse, result)
|
|
154
247
|
return response if response.is_a?(ACP::RequestError)
|
|
155
248
|
|
|
156
|
-
|
|
157
|
-
|
|
249
|
+
# Safe: parse only calls from_h on a Hash, and its RequestError answer
|
|
250
|
+
# short-circuited above.
|
|
251
|
+
reply = result #: Hash[String, untyped]
|
|
252
|
+
@mcp_advertised = ACP::Protocol.mcp_acp_advertised?(reply['agentCapabilities'] || {})
|
|
253
|
+
return response if response.protocol_version == ACP::Protocol::PROTOCOL_VERSION
|
|
158
254
|
|
|
159
255
|
ACP::RequestError.unsupported_protocol_version(
|
|
160
256
|
requested: request.protocol_version,
|
|
@@ -164,91 +260,91 @@ class ACP::ClientConnection
|
|
|
164
260
|
|
|
165
261
|
# @rbs request: ACP::Types::NewSessionRequest
|
|
166
262
|
# @rbs return: ACP::Types::NewSessionResponse | ACP::RequestError
|
|
167
|
-
def
|
|
263
|
+
def new_session(request)
|
|
168
264
|
parse(ACP::Types::NewSessionResponse, @transport.request('session/new', request.to_h))
|
|
169
265
|
end
|
|
170
266
|
|
|
171
267
|
# @rbs request: ACP::Types::PromptRequest
|
|
172
268
|
# @rbs &block: (ACP::Types::SessionUpdate::t) -> void
|
|
173
269
|
# @rbs return: ACP::Types::PromptResponse | ACP::RequestError
|
|
174
|
-
def
|
|
175
|
-
parse(ACP::Types::PromptResponse, stream(request.session_id, 'session/prompt', request.to_h, &))
|
|
270
|
+
def prompt_session(request, &)
|
|
271
|
+
parse(ACP::Types::PromptResponse, @streams.stream(request.session_id, 'session/prompt', request.to_h, &))
|
|
176
272
|
end
|
|
177
273
|
|
|
178
274
|
# @rbs request: ACP::Types::LoadSessionRequest
|
|
179
275
|
# @rbs &block: (ACP::Types::SessionUpdate::t) -> void
|
|
180
276
|
# @rbs return: ACP::Types::LoadSessionResponse | ACP::RequestError
|
|
181
|
-
def
|
|
182
|
-
parse(ACP::Types::LoadSessionResponse, stream(request.session_id, 'session/load', request.to_h, &))
|
|
277
|
+
def load_session(request, &)
|
|
278
|
+
parse(ACP::Types::LoadSessionResponse, @streams.stream(request.session_id, 'session/load', request.to_h, &))
|
|
183
279
|
end
|
|
184
280
|
|
|
185
281
|
# @rbs request: ACP::Types::ListSessionsRequest
|
|
186
282
|
# @rbs return: ACP::Types::ListSessionsResponse | ACP::RequestError
|
|
187
|
-
def
|
|
283
|
+
def list_sessions(request)
|
|
188
284
|
parse(ACP::Types::ListSessionsResponse, @transport.request('session/list', request.to_h))
|
|
189
285
|
end
|
|
190
286
|
|
|
191
|
-
# The agent does not replay history on resume, so unlike
|
|
287
|
+
# The agent does not replay history on resume, so unlike load_session there
|
|
192
288
|
# is nothing to stream and no block is taken.
|
|
193
289
|
#
|
|
194
290
|
# @rbs request: ACP::Types::ResumeSessionRequest
|
|
195
291
|
# @rbs return: ACP::Types::ResumeSessionResponse | ACP::RequestError
|
|
196
|
-
def
|
|
292
|
+
def resume_session(request)
|
|
197
293
|
parse(ACP::Types::ResumeSessionResponse, @transport.request('session/resume', request.to_h))
|
|
198
294
|
end
|
|
199
295
|
|
|
200
296
|
# @rbs request: ACP::Types::CloseSessionRequest
|
|
201
297
|
# @rbs return: ACP::Types::CloseSessionResponse | ACP::RequestError
|
|
202
|
-
def
|
|
298
|
+
def close_session(request)
|
|
203
299
|
parse(ACP::Types::CloseSessionResponse, @transport.request('session/close', request.to_h))
|
|
204
300
|
end
|
|
205
301
|
|
|
206
302
|
# @rbs request: ACP::Types::DeleteSessionRequest
|
|
207
303
|
# @rbs return: ACP::Types::DeleteSessionResponse | ACP::RequestError
|
|
208
|
-
def
|
|
304
|
+
def delete_session(request)
|
|
209
305
|
parse(ACP::Types::DeleteSessionResponse, @transport.request('session/delete', request.to_h))
|
|
210
306
|
end
|
|
211
307
|
|
|
212
308
|
# @rbs request: ACP::Types::Unstable::ForkSessionRequest
|
|
213
309
|
# @rbs return: (ACP::Types::Unstable::ForkSessionResponse | ACP::RequestError)
|
|
214
|
-
def
|
|
310
|
+
def fork_session(request)
|
|
215
311
|
parse(ACP::Types::Unstable::ForkSessionResponse, @transport.request('session/fork', request.to_h))
|
|
216
312
|
end
|
|
217
313
|
|
|
218
314
|
# @rbs request: ACP::Types::Unstable::ListProvidersRequest
|
|
219
315
|
# @rbs return: (ACP::Types::Unstable::ListProvidersResponse | ACP::RequestError)
|
|
220
|
-
def
|
|
316
|
+
def list_providers(request)
|
|
221
317
|
parse(ACP::Types::Unstable::ListProvidersResponse, @transport.request('providers/list', request.to_h))
|
|
222
318
|
end
|
|
223
319
|
|
|
224
320
|
# @rbs request: ACP::Types::Unstable::SetProviderRequest
|
|
225
321
|
# @rbs return: (ACP::Types::Unstable::SetProviderResponse | ACP::RequestError)
|
|
226
|
-
|
|
322
|
+
# The name is the wire-mandated providers/set, not an attribute writer.
|
|
323
|
+
def set_provider(request) # rubocop:disable Naming/AccessorMethodName
|
|
227
324
|
parse(ACP::Types::Unstable::SetProviderResponse, @transport.request('providers/set', request.to_h))
|
|
228
325
|
end
|
|
229
326
|
|
|
230
327
|
# @rbs request: ACP::Types::Unstable::DisableProviderRequest
|
|
231
328
|
# @rbs return: (ACP::Types::Unstable::DisableProviderResponse | ACP::RequestError)
|
|
232
|
-
def
|
|
329
|
+
def disable_provider(request)
|
|
233
330
|
parse(ACP::Types::Unstable::DisableProviderResponse, @transport.request('providers/disable', request.to_h))
|
|
234
331
|
end
|
|
235
332
|
|
|
236
333
|
# The spec requires a pending session/request_permission to be answered with
|
|
237
|
-
# the cancelled outcome once the turn is cancelled
|
|
238
|
-
#
|
|
239
|
-
# so the wire order is cancel, then the cancelled replies.
|
|
334
|
+
# the cancelled outcome once the turn is cancelled. The notification goes
|
|
335
|
+
# first so the wire order is cancel, then the cancelled replies.
|
|
240
336
|
#
|
|
241
337
|
# @rbs notification: ACP::Types::CancelNotification
|
|
242
338
|
# @rbs return: void
|
|
243
|
-
def
|
|
339
|
+
def cancel_session(notification)
|
|
244
340
|
@transport.notify('session/cancel', notification.to_h)
|
|
245
|
-
|
|
246
|
-
pending&.each { |replies| replies << [:value, CANCELLED] }
|
|
341
|
+
@streams.cancel(notification.session_id)
|
|
247
342
|
end
|
|
248
343
|
|
|
249
344
|
# @rbs request: ACP::Types::SetSessionModeRequest
|
|
250
345
|
# @rbs return: ACP::Types::SetSessionModeResponse | ACP::RequestError
|
|
251
|
-
|
|
346
|
+
# The name is the wire-mandated session/set_mode, not an attribute writer.
|
|
347
|
+
def set_session_mode(request) # rubocop:disable Naming/AccessorMethodName
|
|
252
348
|
parse(ACP::Types::SetSessionModeResponse, @transport.request('session/set_mode', request.to_h))
|
|
253
349
|
end
|
|
254
350
|
|
|
@@ -257,7 +353,8 @@ class ACP::ClientConnection
|
|
|
257
353
|
#
|
|
258
354
|
# @rbs request: ACP::Types::SetSessionConfigOptionRequest
|
|
259
355
|
# @rbs return: ACP::Types::SetSessionConfigOptionResponse | ACP::RequestError
|
|
260
|
-
|
|
356
|
+
# The name is the wire-mandated session/set_config_option, not an attribute writer.
|
|
357
|
+
def set_session_config_option(request) # rubocop:disable Naming/AccessorMethodName
|
|
261
358
|
parse(ACP::Types::SetSessionConfigOptionResponse, @transport.request('session/set_config_option', request.to_h))
|
|
262
359
|
end
|
|
263
360
|
|
|
@@ -273,9 +370,7 @@ class ACP::ClientConnection
|
|
|
273
370
|
parse(ACP::Types::LogoutResponse, @transport.request('logout', request.to_h))
|
|
274
371
|
end
|
|
275
372
|
|
|
276
|
-
#
|
|
277
|
-
# returns the agent's reply as-is: extension methods have no schema to
|
|
278
|
-
# parse the result into.
|
|
373
|
+
# Extension methods have no schema to parse the result into.
|
|
279
374
|
#
|
|
280
375
|
# @rbs method: String
|
|
281
376
|
# @rbs params: untyped
|
|
@@ -312,290 +407,89 @@ class ACP::ClientConnection
|
|
|
312
407
|
|
|
313
408
|
private
|
|
314
409
|
|
|
315
|
-
#
|
|
316
|
-
#
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
# @rbs return: bool
|
|
331
|
-
def agent_mcp_advertised?(result)
|
|
332
|
-
return false unless defined?(ACP::Types::Unstable)
|
|
333
|
-
return false unless result.is_a?(Hash)
|
|
334
|
-
|
|
335
|
-
mcp = ACP::Types::Unstable::McpCapabilities.from_h(result['agentCapabilities']['mcpCapabilities'] || {})
|
|
336
|
-
mcp.acp ? true : false
|
|
337
|
-
rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError
|
|
338
|
-
false
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
# @rbs type: ACP::AgentConnection::_Parser
|
|
342
|
-
# @rbs result: (Hash[String, untyped] | ACP::RequestError)
|
|
343
|
-
# @rbs return: untyped
|
|
344
|
-
def parse(type, result)
|
|
345
|
-
return result if result.is_a?(ACP::RequestError)
|
|
346
|
-
|
|
347
|
-
type.from_h(result)
|
|
348
|
-
rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError
|
|
349
|
-
ACP::RequestError.invalid_response(result)
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
# The request waits on its own thread so the caller's thread can yield each
|
|
353
|
-
# update as it arrives. The reader thread queues an update before it settles
|
|
354
|
-
# a reply sent after it, so the queue closes only after the last one.
|
|
355
|
-
#
|
|
356
|
-
# @rbs session_id: String
|
|
357
|
-
# @rbs method: String
|
|
358
|
-
# @rbs params: Hash[String, untyped]
|
|
359
|
-
# @rbs &block: (ACP::Types::SessionUpdate::t) -> void
|
|
360
|
-
# @rbs return: (Hash[String, untyped] | ACP::RequestError)
|
|
361
|
-
def stream(session_id, method, params)
|
|
362
|
-
queue = Thread::Queue.new
|
|
363
|
-
# The check and the insert share one lock acquisition so a concurrent
|
|
364
|
-
# stream cannot register between them.
|
|
365
|
-
claimed = @lock.synchronize do
|
|
366
|
-
if @streams.key?(session_id)
|
|
367
|
-
false
|
|
368
|
-
else
|
|
369
|
-
@streams[session_id] = queue
|
|
370
|
-
true
|
|
371
|
-
end
|
|
372
|
-
end
|
|
373
|
-
unless claimed
|
|
374
|
-
return ACP::RequestError.new(
|
|
375
|
-
code: ACP::RequestError::INVALID_REQUEST, message: "A stream is already open for session #{session_id}"
|
|
376
|
-
)
|
|
377
|
-
end
|
|
378
|
-
requester = Thread.new do
|
|
379
|
-
@transport.request(method, params)
|
|
380
|
-
ensure
|
|
381
|
-
@lock.synchronize { @streams.delete(session_id)&.close }
|
|
382
|
-
end
|
|
383
|
-
while (update = queue.pop)
|
|
384
|
-
yield update
|
|
410
|
+
# @rbs route: ACP::ClientConnection::Route
|
|
411
|
+
# @rbs return: ^(untyped) -> (untyped | ACP::RequestError)
|
|
412
|
+
def serve(route)
|
|
413
|
+
lambda do |params|
|
|
414
|
+
next refuse(route) unless route.advertised.call(instance_variable_get(route.family))
|
|
415
|
+
|
|
416
|
+
request = route.type.from_h(params)
|
|
417
|
+
rescue ACP::Types::ParseError => e
|
|
418
|
+
ACP::RequestError.invalid_params([e.message])
|
|
419
|
+
rescue KeyError, TypeError, NoMethodError
|
|
420
|
+
ACP::RequestError.invalid_params
|
|
421
|
+
else
|
|
422
|
+
# Safe: connect refuses an advertised capability no handler serves, and
|
|
423
|
+
# the mcp route is only registered when one is injected.
|
|
424
|
+
instance_variable_get(route.handler).call(request)
|
|
385
425
|
end
|
|
386
|
-
requester.value
|
|
387
|
-
end
|
|
388
|
-
|
|
389
|
-
# Pushes under the lock so a stream cannot close its queue between the
|
|
390
|
-
# lookup and the push. A malformed notification has no reply to carry the
|
|
391
|
-
# failure.
|
|
392
|
-
#
|
|
393
|
-
# @rbs params: untyped
|
|
394
|
-
# @rbs return: void
|
|
395
|
-
def dispatch(params)
|
|
396
|
-
notification = ACP::Types::SessionNotification.from_h(params)
|
|
397
|
-
rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError => e
|
|
398
|
-
@logger.warn("dropped malformed session/update: #{e.class}: #{e.message}")
|
|
399
|
-
else
|
|
400
|
-
queue = @lock.synchronize { @streams[notification.session_id]&.push(notification.update) }
|
|
401
|
-
@updates.call(notification) unless queue
|
|
402
426
|
end
|
|
403
427
|
|
|
404
|
-
#
|
|
405
|
-
#
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
# @rbs params: untyped
|
|
409
|
-
# @rbs return: (ACP::Types::RequestPermissionResponse | ACP::RequestError)
|
|
410
|
-
def request_permission(params)
|
|
411
|
-
request = ACP::Types::RequestPermissionRequest.from_h(params)
|
|
412
|
-
rescue ACP::Types::ParseError => e
|
|
413
|
-
ACP::RequestError.invalid_params([e.message])
|
|
414
|
-
rescue KeyError, TypeError, NoMethodError
|
|
415
|
-
ACP::RequestError.invalid_params
|
|
416
|
-
else
|
|
417
|
-
replies = Thread::Queue.new
|
|
418
|
-
register_permission(request.session_id, replies)
|
|
419
|
-
# The handler runs on this connection's own thread, so the serving
|
|
420
|
-
# identity must be carried over for cancelled? to see the cancel there.
|
|
421
|
-
serving = @transport.cancellation
|
|
422
|
-
Thread.new do
|
|
423
|
-
ACP::Transport::Cancellation.current = serving
|
|
424
|
-
# A raised StandardError is not a returned ACP::RequestError, so the two
|
|
425
|
-
# need distinct shapes on the queue.
|
|
426
|
-
replies << begin
|
|
427
|
-
[:value, @permission.call(request)]
|
|
428
|
-
rescue StandardError => e
|
|
429
|
-
[:raised, e]
|
|
430
|
-
end
|
|
431
|
-
ensure
|
|
432
|
-
unregister_permission(request.session_id, replies)
|
|
433
|
-
end
|
|
434
|
-
settled = replies.pop
|
|
435
|
-
# Re-raised so the transport's invoke turns it into an error reply, as it
|
|
436
|
-
# would if the handler ran on the serve thread itself.
|
|
437
|
-
raise settled[1] if settled[0] == :raise
|
|
438
|
-
|
|
439
|
-
settled[1]
|
|
428
|
+
# @rbs route: ACP::ClientConnection::Route
|
|
429
|
+
# @rbs return: ACP::RequestError
|
|
430
|
+
def refuse(route)
|
|
431
|
+
ACP::RequestError.public_send(route.refuse, route.key)
|
|
440
432
|
end
|
|
441
433
|
|
|
442
|
-
# @rbs
|
|
443
|
-
# @rbs
|
|
434
|
+
# @rbs name: String
|
|
435
|
+
# @rbs family: Symbol
|
|
436
|
+
# @rbs capabilities: untyped
|
|
444
437
|
# @rbs return: void
|
|
445
|
-
def
|
|
446
|
-
|
|
447
|
-
|
|
438
|
+
def check_served(name, family, capabilities)
|
|
439
|
+
unserved = SERVE.filter_map do |route|
|
|
440
|
+
next unless route.family == family
|
|
441
|
+
next if instance_variable_get(route.handler)
|
|
442
|
+
next unless route.advertised.call(capabilities)
|
|
448
443
|
|
|
449
|
-
|
|
450
|
-
# @rbs replies: Thread::Queue
|
|
451
|
-
# @rbs return: void
|
|
452
|
-
def unregister_permission(session_id, replies)
|
|
453
|
-
@lock.synchronize do
|
|
454
|
-
pending = @pending_permissions[session_id]
|
|
455
|
-
if pending
|
|
456
|
-
pending.delete(replies)
|
|
457
|
-
@pending_permissions.delete(session_id) if pending.empty?
|
|
458
|
-
end
|
|
444
|
+
route.label
|
|
459
445
|
end
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
# The routes are registered at start, before connect records the advertised
|
|
463
|
-
# capabilities, so each one answers -32601 until then.
|
|
464
|
-
#
|
|
465
|
-
# @rbs params: untyped
|
|
466
|
-
# @rbs return: (ACP::Types::ReadTextFileResponse | ACP::RequestError)
|
|
467
|
-
def read_text_file(params)
|
|
468
|
-
return ACP::RequestError.unadvertised('fs.readTextFile') unless @fs_capabilities&.read_text_file
|
|
469
|
-
|
|
470
|
-
request = ACP::Types::ReadTextFileRequest.from_h(params)
|
|
471
|
-
rescue ACP::Types::ParseError => e
|
|
472
|
-
ACP::RequestError.invalid_params([e.message])
|
|
473
|
-
rescue KeyError, TypeError, NoMethodError
|
|
474
|
-
ACP::RequestError.invalid_params
|
|
475
|
-
else
|
|
476
|
-
# Safe: connect refuses an advertised capability no handler serves.
|
|
477
|
-
handler = @read_text_file #: ^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError)
|
|
478
|
-
handler.call(request)
|
|
479
|
-
end
|
|
480
|
-
|
|
481
|
-
# @rbs params: untyped
|
|
482
|
-
# @rbs return: (ACP::Types::WriteTextFileResponse | ACP::RequestError)
|
|
483
|
-
def write_text_file(params)
|
|
484
|
-
return ACP::RequestError.unadvertised('fs.writeTextFile') unless @fs_capabilities&.write_text_file
|
|
485
|
-
|
|
486
|
-
request = ACP::Types::WriteTextFileRequest.from_h(params)
|
|
487
|
-
rescue ACP::Types::ParseError => e
|
|
488
|
-
ACP::RequestError.invalid_params([e.message])
|
|
489
|
-
rescue KeyError, TypeError, NoMethodError
|
|
490
|
-
ACP::RequestError.invalid_params
|
|
491
|
-
else
|
|
492
|
-
# Safe: connect refuses an advertised capability no handler serves.
|
|
493
|
-
handler = @write_text_file #: ^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError)
|
|
494
|
-
handler.call(request)
|
|
495
|
-
end
|
|
496
|
-
|
|
497
|
-
# The terminal routes are registered at start, before connect records the
|
|
498
|
-
# advertised capability, so each one answers -32601 until then.
|
|
499
|
-
#
|
|
500
|
-
# @rbs params: untyped
|
|
501
|
-
# @rbs return: (ACP::Types::CreateTerminalResponse | ACP::RequestError)
|
|
502
|
-
def create_terminal(params)
|
|
503
|
-
return ACP::RequestError.unadvertised('terminal') unless @terminal
|
|
446
|
+
return if unserved.empty?
|
|
504
447
|
|
|
505
|
-
|
|
506
|
-
rescue KeyError, TypeError, NoMethodError
|
|
507
|
-
ACP::RequestError.invalid_params
|
|
508
|
-
else
|
|
509
|
-
# Safe: connect refuses an advertised capability no handler serves.
|
|
510
|
-
handler = @create_terminal #: ^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError)
|
|
511
|
-
handler.call(request)
|
|
448
|
+
raise ArgumentError, "initialize advertises #{name} methods no handler serves: #{unserved.join(', ')}"
|
|
512
449
|
end
|
|
513
450
|
|
|
514
|
-
#
|
|
515
|
-
#
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
request = ACP::Types::TerminalOutputRequest.from_h(params)
|
|
520
|
-
rescue KeyError, TypeError, NoMethodError
|
|
521
|
-
ACP::RequestError.invalid_params
|
|
522
|
-
else
|
|
523
|
-
# Safe: connect refuses an advertised capability no handler serves.
|
|
524
|
-
handler = @terminal_output #: ^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError)
|
|
525
|
-
handler.call(request)
|
|
526
|
-
end
|
|
527
|
-
|
|
528
|
-
# The handler may block for as long as the command runs, so the transport
|
|
529
|
-
# must serve each request on its own thread for this not to hold up others.
|
|
451
|
+
# The unstable types are referenced while the row is built, so the route can
|
|
452
|
+
# only be served when the caller opted in with `require 'acp/types/unstable'`.
|
|
453
|
+
# It is registered only when a handler serves it: unlike the fs and terminal
|
|
454
|
+
# gates, the mcp gate reads the agent's advertisement, so nothing forces an
|
|
455
|
+
# advertised request to have a handler.
|
|
530
456
|
#
|
|
531
|
-
# @rbs
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
return ACP::
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
# @rbs params: untyped
|
|
546
|
-
# @rbs return: (ACP::Types::KillTerminalResponse | ACP::RequestError)
|
|
547
|
-
def kill_terminal(params)
|
|
548
|
-
return ACP::RequestError.unadvertised('terminal') unless @terminal
|
|
549
|
-
|
|
550
|
-
request = ACP::Types::KillTerminalRequest.from_h(params)
|
|
551
|
-
rescue KeyError, TypeError, NoMethodError
|
|
552
|
-
ACP::RequestError.invalid_params
|
|
553
|
-
else
|
|
554
|
-
# Safe: connect refuses an advertised capability no handler serves.
|
|
555
|
-
handler = @kill_terminal #: ^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError)
|
|
556
|
-
handler.call(request)
|
|
557
|
-
end
|
|
558
|
-
|
|
559
|
-
# @rbs params: untyped
|
|
560
|
-
# @rbs return: (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
|
|
561
|
-
def release_terminal(params)
|
|
562
|
-
return ACP::RequestError.unadvertised('terminal') unless @terminal
|
|
563
|
-
|
|
564
|
-
request = ACP::Types::ReleaseTerminalRequest.from_h(params)
|
|
565
|
-
rescue KeyError, TypeError, NoMethodError
|
|
566
|
-
ACP::RequestError.invalid_params
|
|
567
|
-
else
|
|
568
|
-
# Safe: connect refuses an advertised capability no handler serves.
|
|
569
|
-
handler = @release_terminal #: ^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
|
|
570
|
-
handler.call(request)
|
|
457
|
+
# @rbs return: Hash[String, ^(untyped) -> (untyped | ACP::RequestError)]
|
|
458
|
+
def mcp_requests
|
|
459
|
+
return {} unless @mcp_message
|
|
460
|
+
return {} unless ACP::Types.unstable_enabled?
|
|
461
|
+
|
|
462
|
+
route = Route.new(
|
|
463
|
+
wire: 'mcp/message',
|
|
464
|
+
type: ACP::Types::Unstable::MessageMcpRequest,
|
|
465
|
+
handler: :@mcp_message,
|
|
466
|
+
family: :@mcp_advertised,
|
|
467
|
+
refuse: :unadvertised_agent,
|
|
468
|
+
key: 'mcpCapabilities.acp'
|
|
469
|
+
)
|
|
470
|
+
{ 'mcp/message' => serve(route) }
|
|
571
471
|
end
|
|
572
472
|
|
|
573
|
-
#
|
|
574
|
-
#
|
|
575
|
-
#
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
def mcp_message(params)
|
|
579
|
-
return ACP::RequestError.unadvertised_agent('mcpCapabilities.acp') unless @mcp_advertised
|
|
473
|
+
# @rbs type: ACP::Transport::_Parser
|
|
474
|
+
# @rbs result: (Hash[String, untyped] | ACP::RequestError)
|
|
475
|
+
# @rbs return: untyped
|
|
476
|
+
def parse(type, result)
|
|
477
|
+
return result if result.is_a?(ACP::RequestError)
|
|
580
478
|
|
|
581
|
-
|
|
582
|
-
rescue ACP::Types::ParseError
|
|
583
|
-
ACP::RequestError.
|
|
584
|
-
rescue KeyError, TypeError, NoMethodError
|
|
585
|
-
ACP::RequestError.invalid_params
|
|
586
|
-
else
|
|
587
|
-
# Safe: the route is only registered when a handler serves it.
|
|
588
|
-
handler = @mcp_message #: ^(ACP::Types::Unstable::MessageMcpRequest) -> (ACP::Types::Unstable::MessageMcpResponse::t | ACP::RequestError)
|
|
589
|
-
handler.call(request)
|
|
479
|
+
type.from_h(result)
|
|
480
|
+
rescue ACP::Types::ParseError, KeyError, TypeError, NoMethodError
|
|
481
|
+
ACP::RequestError.invalid_response(result)
|
|
590
482
|
end
|
|
591
483
|
|
|
592
|
-
# The
|
|
484
|
+
# The route is registered at start, before connect records the advertised
|
|
593
485
|
# capabilities, so a request for an unadvertised mode answers -32602.
|
|
594
486
|
#
|
|
595
487
|
# @rbs params: untyped
|
|
596
488
|
# @rbs return: (ACP::Types::CreateElicitationResponse | ACP::RequestError)
|
|
597
489
|
def create_elicitation(params)
|
|
598
490
|
request = ACP::Types::CreateElicitationRequest.from_h(params)
|
|
491
|
+
rescue ACP::Types::ParseError => e
|
|
492
|
+
ACP::RequestError.invalid_params([e.message])
|
|
599
493
|
rescue KeyError, TypeError, NoMethodError
|
|
600
494
|
ACP::RequestError.invalid_params
|
|
601
495
|
else
|
|
@@ -605,7 +499,7 @@ class ACP::ClientConnection
|
|
|
605
499
|
when ACP::Types::CreateElicitationRequest::Mode::Url
|
|
606
500
|
return ACP::RequestError.unadvertised_mode('elicitation.url') unless @elicitation_capabilities&.url
|
|
607
501
|
|
|
608
|
-
register_elicitation(request.mode.elicitation_id)
|
|
502
|
+
@streams.register_elicitation(request.mode.elicitation_id)
|
|
609
503
|
else
|
|
610
504
|
return ACP::RequestError.unadvertised_mode("elicitation.#{request.mode['mode']}")
|
|
611
505
|
end
|
|
@@ -615,57 +509,6 @@ class ACP::ClientConnection
|
|
|
615
509
|
handler.call(request)
|
|
616
510
|
end
|
|
617
511
|
|
|
618
|
-
# The spec requires a completion for an unknown or already-completed
|
|
619
|
-
# elicitation id to be ignored, so only an id registered by a served
|
|
620
|
-
# url-mode request reaches the handler. It runs on the transport's reader
|
|
621
|
-
# thread, so it must return quickly.
|
|
622
|
-
#
|
|
623
|
-
# @rbs params: untyped
|
|
624
|
-
# @rbs return: void
|
|
625
|
-
def complete_elicitation(params)
|
|
626
|
-
notification = ACP::Types::CompleteElicitationNotification.from_h(params)
|
|
627
|
-
rescue KeyError, TypeError, NoMethodError => e
|
|
628
|
-
@logger.warn("dropped malformed elicitation/complete: #{e.class}: #{e.message}")
|
|
629
|
-
else
|
|
630
|
-
known = @lock.synchronize { @outstanding_elicitations.delete(notification.elicitation_id) }
|
|
631
|
-
@complete_elicitation&.call(notification) if known
|
|
632
|
-
end
|
|
633
|
-
|
|
634
|
-
# @rbs elicitation_id: String
|
|
635
|
-
# @rbs return: void
|
|
636
|
-
def register_elicitation(elicitation_id)
|
|
637
|
-
@lock.synchronize { @outstanding_elicitations[elicitation_id] = true }
|
|
638
|
-
end
|
|
639
|
-
|
|
640
|
-
# The fs capabilities initialize advertises that no injected handler serves.
|
|
641
|
-
#
|
|
642
|
-
# @rbs capabilities: ACP::Types::FileSystemCapabilities?
|
|
643
|
-
# @rbs return: Array[String]
|
|
644
|
-
def unserved_fs_methods(capabilities)
|
|
645
|
-
{
|
|
646
|
-
'fs.readTextFile' => capabilities&.read_text_file && !@read_text_file,
|
|
647
|
-
'fs.writeTextFile' => capabilities&.write_text_file && !@write_text_file
|
|
648
|
-
}.select { |_, unserved| unserved }.keys
|
|
649
|
-
end
|
|
650
|
-
|
|
651
|
-
# The terminal capabilities initialize advertises that no injected handler
|
|
652
|
-
# serves.
|
|
653
|
-
#
|
|
654
|
-
# @rbs capabilities: bool?
|
|
655
|
-
# @rbs return: Array[String]
|
|
656
|
-
def unserved_terminal_methods(capabilities)
|
|
657
|
-
{
|
|
658
|
-
'terminal.create' => capabilities && !@create_terminal,
|
|
659
|
-
'terminal.output' => capabilities && !@terminal_output,
|
|
660
|
-
'terminal.wait_for_exit' => capabilities && !@wait_for_terminal_exit,
|
|
661
|
-
'terminal.kill' => capabilities && !@kill_terminal,
|
|
662
|
-
'terminal.release' => capabilities && !@release_terminal
|
|
663
|
-
}.select { |_, unserved| unserved }.keys
|
|
664
|
-
end
|
|
665
|
-
|
|
666
|
-
# The elicitation capabilities initialize advertises that no injected
|
|
667
|
-
# handler serves.
|
|
668
|
-
#
|
|
669
512
|
# @rbs capabilities: ACP::Types::ElicitationCapabilities?
|
|
670
513
|
# @rbs return: Array[String]
|
|
671
514
|
def unserved_elicitation_modes(capabilities)
|