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



238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/copilot/session.rb', line 238

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.



267
268
269
# File 'lib/copilot/session.rb', line 267

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.



318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/copilot/session.rb', line 318

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.



339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/copilot/session.rb', line 339

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.



277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/copilot/session.rb', line 277

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.



298
299
300
301
302
303
304
305
# File 'lib/copilot/session.rb', line 298

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.



313
314
315
# File 'lib/copilot/session.rb', line 313

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.



308
309
310
# File 'lib/copilot/session.rb', line 308

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.



272
273
274
# File 'lib/copilot/session.rb', line 272

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.



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

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.



334
335
336
# File 'lib/copilot/session.rb', line 334

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.



293
294
295
# File 'lib/copilot/session.rb', line 293

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

#abortObject

Abort the currently processing message.



231
232
233
# File 'lib/copilot/session.rb', line 231

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

#destroyObject

Destroy this session and release associated resources.



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

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_messagesArray<SessionEvent>

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

Returns:



202
203
204
205
206
# File 'lib/copilot/session.rb', line 202

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



211
212
213
# File 'lib/copilot/session.rb', line 211

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)


177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/copilot/session.rb', line 177

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



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
# File 'lib/copilot/session.rb', line 71

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

  # 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, 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



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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/copilot/session.rb', line 111

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