Class: Copilot::CopilotSession
- Inherits:
-
Object
- Object
- Copilot::CopilotSession
- Defined in:
- lib/copilot/session.rb
Overview
Represents a single conversation session with the Copilot CLI.
A session maintains conversation state, handles events, and manages tool execution. Sessions are created via Copilot::CopilotClient#create_session or resumed via Copilot::CopilotClient#resume_session.
Instance Attribute Summary collapse
-
#session_id ⇒ String
readonly
The unique session identifier.
-
#workspace_path ⇒ String?
readonly
Workspace path when infinite sessions are enabled.
Instance Method Summary collapse
- #_dispatch_event(event) ⇒ Object private
- #_get_tool_handler(name) ⇒ Object private
- #_handle_exit_plan_mode_request(params) ⇒ Object private
- #_handle_hooks_invoke(hook_type, input_data) ⇒ Object private
- #_handle_permission_request(request) ⇒ Object private
- #_handle_user_input_request(params) ⇒ Object private
- #_register_exit_plan_mode_handler(handler) ⇒ Object private
- #_register_hooks(hooks) ⇒ Object private
- #_register_permission_handler(handler) ⇒ Object private
- #_register_tools(tools) ⇒ Object private
- #_register_trace_context_provider(provider) ⇒ Object private
- #_register_user_input_handler(handler) ⇒ Object private
-
#abort ⇒ Object
Abort the currently processing message.
-
#destroy ⇒ Object
Destroy this session and release associated resources.
-
#get_messages ⇒ Array<SessionEvent>
Retrieve all events/messages from this session's history.
-
#get_metadata ⇒ Hash
Retrieve metadata for this session.
-
#initialize(session_id, rpc_client, workspace_path = nil) ⇒ CopilotSession
constructor
private
A new instance of CopilotSession.
-
#on(event_type = nil, &handler) ⇒ Object
Subscribe to events from this session.
-
#send(prompt:, attachments: nil, mode: nil, agent_mode: nil, display_prompt: nil, request_headers: nil, response_format: nil, image_options: nil) ⇒ String
Send a message to this session.
-
#send_and_wait(prompt:, attachments: nil, mode: nil, agent_mode: nil, display_prompt: nil, request_headers: nil, response_format: nil, image_options: nil, timeout: 60) ⇒ SessionEvent?
Send a message and wait until the session becomes idle.
Constructor Details
#initialize(session_id, rpc_client, workspace_path = nil) ⇒ CopilotSession
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns a new instance of CopilotSession.
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/copilot/session.rb', line 36 def initialize(session_id, rpc_client, workspace_path = nil) @session_id = session_id @rpc_client = rpc_client @workspace_path = workspace_path @event_handlers = [] @typed_event_handlers = {} # type => [handler] @event_handlers_lock = Mutex.new @tool_handlers = {} @tool_handlers_lock = Mutex.new @permission_handler = nil @permission_handler_lock = Mutex.new @user_input_handler = nil @user_input_handler_lock = Mutex.new @hooks = nil @hooks_lock = Mutex.new @exit_plan_mode_handler = nil @exit_plan_mode_handler_lock = Mutex.new @trace_context_provider = nil end |
Instance Attribute Details
#session_id ⇒ String (readonly)
Returns the unique session identifier.
27 28 29 |
# File 'lib/copilot/session.rb', line 27 def session_id @session_id end |
#workspace_path ⇒ String? (readonly)
Returns workspace path when infinite sessions are enabled.
30 31 32 |
# File 'lib/copilot/session.rb', line 30 def workspace_path @workspace_path end |
Instance Method Details
#_dispatch_event(event) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
243 244 245 246 247 248 249 250 251 252 253 254 255 |
# File 'lib/copilot/session.rb', line 243 def _dispatch_event(event) handlers = @event_handlers_lock.synchronize do typed = @typed_event_handlers[event.type]&.dup || [] wildcard = @event_handlers.dup typed + wildcard end handlers.each do |handler| handler.call(event) rescue StandardError => e $stderr.puts("[CopilotSDK] Session event handler error: #{e.class}: #{e.}") end end |
#_get_tool_handler(name) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
272 273 274 |
# File 'lib/copilot/session.rb', line 272 def _get_tool_handler(name) @tool_handlers_lock.synchronize { @tool_handlers[name] } end |
#_handle_exit_plan_mode_request(params) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
323 324 325 326 327 328 329 330 331 332 333 334 335 336 |
# File 'lib/copilot/session.rb', line 323 def _handle_exit_plan_mode_request(params) handler = @exit_plan_mode_handler_lock.synchronize { @exit_plan_mode_handler } unless handler return { approved: true } end begin request = ExitPlanModeRequest.from_hash(params) result = handler.call(request) result.is_a?(ExitPlanModeResponse) ? result.to_h : result rescue StandardError { approved: true } end end |
#_handle_hooks_invoke(hook_type, input_data) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 |
# File 'lib/copilot/session.rb', line 344 def _handle_hooks_invoke(hook_type, input_data) hooks = @hooks_lock.synchronize { @hooks } return nil unless hooks handler_map = { "preToolUse" => hooks.on_pre_tool_use, "postToolUse" => hooks.on_post_tool_use, "userPromptSubmitted" => hooks.on_user_prompt_submitted, "sessionStart" => hooks.on_session_start, "sessionEnd" => hooks.on_session_end, "errorOccurred" => hooks.on_error_occurred, } handler = handler_map[hook_type] return nil unless handler begin handler.call(input_data, { session_id: @session_id }) rescue StandardError nil end end |
#_handle_permission_request(request) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
282 283 284 285 286 287 288 289 290 291 292 293 294 295 |
# File 'lib/copilot/session.rb', line 282 def (request) handler = @permission_handler_lock.synchronize { @permission_handler } unless handler return { kind: PermissionKind::DENIED_NO_APPROVAL } end begin perm_request = PermissionRequest.from_hash(request) result = handler.call(perm_request, { session_id: @session_id }) result.is_a?(PermissionRequestResult) ? result.to_h : result rescue StandardError { kind: PermissionKind::DENIED_NO_APPROVAL } end end |
#_handle_user_input_request(params) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
303 304 305 306 307 308 309 310 |
# File 'lib/copilot/session.rb', line 303 def _handle_user_input_request(params) handler = @user_input_handler_lock.synchronize { @user_input_handler } raise "User input requested but no handler registered" unless handler request = UserInputRequest.from_hash(params) result = handler.call(request, { session_id: @session_id }) result.is_a?(UserInputResponse) ? result.to_h : result end |
#_register_exit_plan_mode_handler(handler) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
318 319 320 |
# File 'lib/copilot/session.rb', line 318 def _register_exit_plan_mode_handler(handler) @exit_plan_mode_handler_lock.synchronize { @exit_plan_mode_handler = handler } end |
#_register_hooks(hooks) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
313 314 315 |
# File 'lib/copilot/session.rb', line 313 def _register_hooks(hooks) @hooks_lock.synchronize { @hooks = hooks } end |
#_register_permission_handler(handler) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
277 278 279 |
# File 'lib/copilot/session.rb', line 277 def (handler) @permission_handler_lock.synchronize { @permission_handler = handler } end |
#_register_tools(tools) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
258 259 260 261 262 263 264 265 266 267 268 269 |
# File 'lib/copilot/session.rb', line 258 def _register_tools(tools) @tool_handlers_lock.synchronize do @tool_handlers.clear return unless tools tools.each do |tool| next unless tool.name && tool.handler @tool_handlers[tool.name] = tool.handler end end end |
#_register_trace_context_provider(provider) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
339 340 341 |
# File 'lib/copilot/session.rb', line 339 def _register_trace_context_provider(provider) @trace_context_provider = provider end |
#_register_user_input_handler(handler) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
298 299 300 |
# File 'lib/copilot/session.rb', line 298 def _register_user_input_handler(handler) @user_input_handler_lock.synchronize { @user_input_handler = handler } end |
#abort ⇒ Object
Abort the currently processing message.
236 237 238 |
# File 'lib/copilot/session.rb', line 236 def abort @rpc_client.request("session.abort", { sessionId: @session_id }) end |
#destroy ⇒ Object
Destroy this session and release associated resources.
221 222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'lib/copilot/session.rb', line 221 def destroy @rpc_client.request("session.destroy", { sessionId: @session_id }) @event_handlers_lock.synchronize do @event_handlers.clear @typed_event_handlers.clear end @tool_handlers_lock.synchronize { @tool_handlers.clear } @permission_handler_lock.synchronize { @permission_handler = nil } @user_input_handler_lock.synchronize { @user_input_handler = nil } @hooks_lock.synchronize { @hooks = nil } @exit_plan_mode_handler_lock.synchronize { @exit_plan_mode_handler = nil } @trace_context_provider = nil end |
#get_messages ⇒ Array<SessionEvent>
Retrieve all events/messages from this session's history.
207 208 209 210 211 |
# File 'lib/copilot/session.rb', line 207 def response = @rpc_client.request("session.getMessages", { sessionId: @session_id }) events = response["events"] || [] events.map { |e| SessionEvent.from_hash(e) } end |
#get_metadata ⇒ Hash
Retrieve metadata for this session.
216 217 218 |
# File 'lib/copilot/session.rb', line 216 def @rpc_client.request("session.getMetadata", { sessionId: @session_id }) end |
#on {|event| ... } ⇒ Proc #on(event_type) {|event| ... } ⇒ Proc
Subscribe to events from this session.
When called with a block only, subscribes to all events. When called with an event type and a block, subscribes to that specific type.
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
# File 'lib/copilot/session.rb', line 182 def on(event_type = nil, &handler) raise ArgumentError, "Block required" unless handler @event_handlers_lock.synchronize do if event_type (@typed_event_handlers[event_type] ||= []) << handler else @event_handlers << handler end end -> { @event_handlers_lock.synchronize do if event_type @typed_event_handlers[event_type]&.delete(handler) else @event_handlers.delete(handler) end end } end |
#send(prompt:, attachments: nil, mode: nil, agent_mode: nil, display_prompt: nil, request_headers: nil, response_format: nil, image_options: nil) ⇒ String
Send a message to this session.
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/copilot/session.rb', line 71 def send(prompt:, attachments: nil, mode: nil, agent_mode: nil, display_prompt: nil, request_headers: nil, response_format: nil, image_options: nil) payload = { sessionId: @session_id, prompt: prompt } payload[:attachments] = if payload[:mode] = mode if mode payload[:agentMode] = agent_mode if agent_mode payload[:displayPrompt] = display_prompt if display_prompt payload[:requestHeaders] = request_headers if request_headers payload[:responseFormat] = response_format if response_format payload[:imageOptions] = .to_h if # Inject trace context if provider is available if @trace_context_provider begin tc = @trace_context_provider.call if tc tp = tc.respond_to?(:traceparent) ? tc.traceparent : tc[:traceparent] ts = tc.respond_to?(:tracestate) ? tc.tracestate : tc[:tracestate] payload[:traceparent] = tp if tp payload[:tracestate] = ts if ts end rescue StandardError # ignore trace context errors end end response = @rpc_client.request("session.send", payload) response["messageId"] end |
#send_and_wait(prompt:, attachments: nil, mode: nil, agent_mode: nil, display_prompt: nil, request_headers: nil, response_format: nil, image_options: nil, timeout: 60) ⇒ SessionEvent?
Send a message and wait until the session becomes idle.
This is a convenience method that combines #send with waiting for
the session.idle event.
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/copilot/session.rb', line 114 def send_and_wait(prompt:, attachments: nil, mode: nil, agent_mode: nil, display_prompt: nil, request_headers: nil, response_format: nil, image_options: nil, timeout: 60) idle_mutex = Mutex.new idle_cv = ConditionVariable.new idle_fired = false = nil error_event = nil # Register BEFORE send to avoid race condition unsub = on do |event| case event.type when SessionEventType::ASSISTANT_MESSAGE = event when SessionEventType::SESSION_IDLE idle_mutex.synchronize do idle_fired = true idle_cv.signal end when SessionEventType::SESSION_ERROR error_event = RuntimeError.new( event.data.is_a?(Hash) ? event.data["message"] : event.data.to_s ) idle_mutex.synchronize do idle_fired = true idle_cv.signal end end end begin self.send(prompt: prompt, attachments: , mode: mode, agent_mode: agent_mode, display_prompt: display_prompt, request_headers: request_headers, response_format: response_format, image_options: ) idle_mutex.synchronize do unless idle_fired idle_cv.wait(idle_mutex, timeout) end end raise error_event if error_event unless idle_fired raise Timeout::Error, "Timeout after #{timeout}s waiting for session.idle" end ensure unsub.call end end |