Class: Copilot::CopilotSession

Inherits:
Object
  • Object
show all
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.

Examples:

Basic usage

session = client.create_session(model: "gpt-4")

# Subscribe to all events
unsub = session.on { |event| puts event.type }

# Send a message and wait for completion
response = session.send_and_wait(prompt: "Hello!")
puts response&.data&.dig("content")

session.destroy

Instance Attribute Summary collapse

Instance Method Summary collapse

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.

Parameters:

  • session_id (String)
  • rpc_client (JsonRpcClient)
  • workspace_path (String, nil) (defaults to: nil)


36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 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
end

Instance Attribute Details

#session_idString (readonly)

Returns the unique session identifier.

Returns:

  • (String)

    the unique session identifier



27
28
29
# File 'lib/copilot/session.rb', line 27

def session_id
  @session_id
end

#workspace_pathString? (readonly)

Returns workspace path when infinite sessions are enabled.

Returns:

  • (String, nil)

    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.



215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/copilot/session.rb', line 215

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.message}")
  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.



244
245
246
# File 'lib/copilot/session.rb', line 244

def _get_tool_handler(name)
  @tool_handlers_lock.synchronize { @tool_handlers[name] }
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.



290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/copilot/session.rb', line 290

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.,
    "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.



254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/copilot/session.rb', line 254

def _handle_permission_request(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.



275
276
277
278
279
280
281
282
# File 'lib/copilot/session.rb', line 275

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_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.



285
286
287
# File 'lib/copilot/session.rb', line 285

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.



249
250
251
# File 'lib/copilot/session.rb', line 249

def _register_permission_handler(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.



230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/copilot/session.rb', line 230

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_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.



270
271
272
# File 'lib/copilot/session.rb', line 270

def _register_user_input_handler(handler)
  @user_input_handler_lock.synchronize { @user_input_handler = handler }
end

#abortObject

Abort the currently processing message.



208
209
210
# File 'lib/copilot/session.rb', line 208

def abort
  @rpc_client.request("session.abort", { sessionId: @session_id })
end

#destroyObject

Destroy this session and release associated resources.



195
196
197
198
199
200
201
202
203
204
205
# File 'lib/copilot/session.rb', line 195

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 }
end

#get_messagesArray<SessionEvent>

Retrieve all events/messages from this session’s history.

Returns:



181
182
183
184
185
# File 'lib/copilot/session.rb', line 181

def get_messages
  response = @rpc_client.request("session.getMessages", { sessionId: @session_id })
  events = response["events"] || []
  events.map { |e| SessionEvent.from_hash(e) }
end

#get_metadataHash

Retrieve metadata for this session.

Returns:

  • (Hash)

    the session metadata



190
191
192
# File 'lib/copilot/session.rb', line 190

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.

Overloads:

  • #on {|event| ... } ⇒ Proc

    Returns unsubscribe function.

    Yields:

    • (event)

      called for every session event

    Yield Parameters:

    Returns:

    • (Proc)

      unsubscribe function

  • #on(event_type) {|event| ... } ⇒ Proc

    Returns unsubscribe function.

    Parameters:

    • event_type (String)

      the specific event type to listen for

    Yields:

    • (event)

      called only for events matching the type

    Yield Parameters:

    Returns:

    • (Proc)

      unsubscribe function

Raises:

  • (ArgumentError)


156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/copilot/session.rb', line 156

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, response_format: nil, image_options: nil) ⇒ String

Send a message to this session.

Parameters:

  • prompt (String)

    the message text

  • attachments (Array, nil) (defaults to: nil)

    optional file/directory/selection attachments

  • mode (String, nil) (defaults to: nil)

    “enqueue” (default) or “immediate”

  • response_format (String, nil) (defaults to: nil)

    desired response format (“text”, “image”, “json_object”)

  • image_options (ImageOptions, nil) (defaults to: nil)

    options for image generation

Returns:

  • (String)

    the message ID



66
67
68
69
70
71
72
73
74
# File 'lib/copilot/session.rb', line 66

def send(prompt:, attachments: nil, mode: nil, response_format: nil, image_options: nil)
  payload = { sessionId: @session_id, prompt: prompt }
  payload[:attachments] = attachments if attachments
  payload[:mode] = mode if mode
  payload[:responseFormat] = response_format if response_format
  payload[:imageOptions] = image_options.to_h if image_options
  response = @rpc_client.request("session.send", payload)
  response["messageId"]
end

#send_and_wait(prompt:, attachments: nil, mode: 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.

Parameters:

  • prompt (String)

    the message text

  • attachments (Array, nil) (defaults to: nil)

    optional attachments

  • mode (String, nil) (defaults to: nil)

    delivery mode

  • response_format (String, nil) (defaults to: nil)

    desired response format (“text”, “image”, “json_object”)

  • image_options (ImageOptions, nil) (defaults to: nil)

    options for image generation

  • timeout (Numeric) (defaults to: 60)

    seconds to wait (default 60)

Returns:

  • (SessionEvent, nil)

    the final assistant.message event, or nil

Raises:

  • (Timeout::Error)

    if the timeout expires before session.idle

  • (RuntimeError)

    if the session emits a session.error event



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
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
# File 'lib/copilot/session.rb', line 90

def send_and_wait(prompt:, attachments: nil, mode: nil, response_format: nil, image_options: nil, timeout: 60)
  idle_mutex = Mutex.new
  idle_cv    = ConditionVariable.new
  idle_fired = false

  last_assistant_message = nil
  error_event = nil

  # Register BEFORE send to avoid race condition
  unsub = on do |event|
    case event.type
    when SessionEventType::ASSISTANT_MESSAGE
      last_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: attachments, mode: mode,
              response_format: response_format, image_options: 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

    last_assistant_message
  ensure
    unsub.call
  end
end