Class: Ask::AppServer::SessionManager

Inherits:
Object
  • Object
show all
Defined in:
lib/ask/app_server/session_manager.rb

Overview

Manages the lifecycle of agent sessions exposed through the app-server.

Orchestrates creation, resumption, subscription, messaging, aborting, and event polling across AgentAdapter instances stored in SessionStore.

Constant Summary collapse

DEFAULT_TOOLS =

Default tools if none specified.

%w[bash read write edit glob grep].freeze
DEFAULT_MODEL =

Default model if none specified.

ENV.fetch("ASK_APP_SERVER_MODEL", "opencode_go/deepseek-v4-flash")

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(store: nil, permission_mode: :on_request, blocked_tools: nil, permission_timeout: 300) ⇒ SessionManager

Returns a new instance of SessionManager.



21
22
23
24
25
26
27
28
29
# File 'lib/ask/app_server/session_manager.rb', line 21

def initialize(store: nil, permission_mode: :on_request, blocked_tools: nil, permission_timeout: 300)
  @store = store || SessionStore.new
  @permission_mode = permission_mode
  @blocked_tools = blocked_tools
  @permission_timeout = permission_timeout
  @permission_handlers = {}  # session_id => PermissionHandler
  @on_new_handler = nil  # callback for new permission handlers
  @logger = Logger.new($stdout, level: ENV["DEBUG"] ? Logger::DEBUG || Logger::DEBUG : Logger::WARN)
end

Instance Attribute Details

#blocked_toolsObject (readonly)

Returns the value of attribute blocked_tools.



18
19
20
# File 'lib/ask/app_server/session_manager.rb', line 18

def blocked_tools
  @blocked_tools
end

#permission_modeObject

Returns the value of attribute permission_mode.



17
18
19
# File 'lib/ask/app_server/session_manager.rb', line 17

def permission_mode
  @permission_mode
end

#permission_timeoutObject (readonly)

Returns the value of attribute permission_timeout.



19
20
21
# File 'lib/ask/app_server/session_manager.rb', line 19

def permission_timeout
  @permission_timeout
end

#storeObject (readonly)

Returns the value of attribute store.



16
17
18
# File 'lib/ask/app_server/session_manager.rb', line 16

def store
  @store
end

Instance Method Details

#create_session(workspace_path: nil, mode: nil, model: nil, tools: nil, system_prompt: nil) ⇒ Object

Create a new session. Returns the session ID.



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/ask/app_server/session_manager.rb', line 44

def create_session(workspace_path: nil, mode: nil, model: nil, tools: nil, system_prompt: nil)
  permission_mode = resolve_permission_mode(mode)

  # Extract provider prefix from model string (e.g., "opencode_go/deepseek-v4-flash")
  model_id, model_provider = parse_model_string(model || DEFAULT_MODEL)
  model_for_agent = model_provider ? model_id : (model || DEFAULT_MODEL)

  # Create the permission handler if needed
  permission_handler = if permission_mode == :on_request
    handler_opts = { mode: :on_request }
    handler_opts[:blocked_tools] = @blocked_tools if @blocked_tools
    handler_opts[:timeout] = @permission_timeout if @permission_timeout

    handler = PermissionHandler.new(**handler_opts)
    @permission_handlers[session_id_cache] = handler if session_id_cache
    handler
  end

  hooks = {}
  hooks[:before_tool] = [permission_handler] if permission_handler

  adapter = AgentAdapter.new(
    model: model_for_agent,
    tools: tools || DEFAULT_TOOLS,
    system_prompt: system_prompt || build_default_system_prompt(workspace_path),
    agent_dir: workspace_path,
    hooks: hooks
  )

  session_id = adapter.start_session
  @store.add(session_id, adapter)

  # Register the permission handler by session ID
  if permission_handler
    @permission_handlers[session_id] = permission_handler
    @on_new_handler&.call(permission_handler)
  end

  @logger.info("Created session #{session_id} (model=#{model || DEFAULT_MODEL}, permission_mode=#{permission_mode})")

  session_id
end

#destroy_session(session_id) ⇒ Object

Remove a session.



93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/ask/app_server/session_manager.rb', line 93

def destroy_session(session_id)
  adapter = @store.get(session_id)
  return false unless adapter

  # Cancel any pending permission requests
  handler = @permission_handlers.delete(session_id)
  handler&.cancel_all!

  adapter.abort_turn! if adapter.running
  @store.remove(session_id)
  @logger.info("Destroyed session #{session_id}")
  true
end

#get(session_id) ⇒ Object

Get an adapter by session ID.



108
109
110
# File 'lib/ask/app_server/session_manager.rb', line 108

def get(session_id)
  @store.get(session_id)
end

#get_events(session_id, after_seq:, limit: nil) ⇒ Object

Get events for a session after a sequence number.



142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/ask/app_server/session_manager.rb', line 142

def get_events(session_id, after_seq:, limit: nil)
  adapter = @store.get(session_id)
  raise Ask::AppServer::SessionNotFound, "Session #{session_id} not found" unless adapter

  events = adapter.events_after(after_seq.to_i)
  events = events.first(limit) if limit && limit > 0

  {
    sessionId: session_id,
    events: events,
    hasMore: limit && limit > 0 && events.length >= limit
  }
end

#list_sessions(limit: 20) ⇒ Object

List active sessions.



113
114
115
# File 'lib/ask/app_server/session_manager.rb', line 113

def list_sessions(limit: 20)
  @store.list(limit: limit)
end

#on_new_permission_handler(&block) ⇒ Object

Register a callback that fires for every new PermissionHandler created. The server uses this to wire its protocol sender callback.



33
34
35
# File 'lib/ask/app_server/session_manager.rb', line 33

def on_new_permission_handler(&block)
  @on_new_handler = block
end

#pending_notifications(session_id) ⇒ Object

Notify subscribers of new events.



187
188
189
190
191
192
# File 'lib/ask/app_server/session_manager.rb', line 187

def pending_notifications(session_id)
  adapter = @store.get(session_id)
  return [] unless adapter && subscribed?(session_id)

  adapter.drain_events
end

#permission_handler_for(session_id) ⇒ Object

Get the permission handler for a session, if any.



88
89
90
# File 'lib/ask/app_server/session_manager.rb', line 88

def permission_handler_for(session_id)
  @permission_handlers[session_id]
end

#read_workspace_state(session_id = nil) ⇒ Object

Read workspace state.



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/ask/app_server/session_manager.rb', line 157

def read_workspace_state(session_id = nil)
  settings = {
    model: {
      current: {
        modelId: ENV.fetch("ASK_APP_SERVER_MODEL", DEFAULT_MODEL),
        providerId: resolve_provider_id
      }
    },
    permissions: {
      mode: @permission_mode.to_s
    }
  }

  if session_id
    adapter = @store.get(session_id)
    if adapter
      settings[:sessionId] = session_id
      settings[:running] = adapter.running
    end
  end

  { settings: settings }
end

#send_message(session_id, content) ⇒ Object

Send a message to a session.



118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/ask/app_server/session_manager.rb', line 118

def send_message(session_id, content)
  adapter = @store.get(session_id)
  raise Ask::AppServer::SessionNotFound, "Session #{session_id} not found" unless adapter

  if adapter.running
    @logger.info("Session #{session_id} busy, injecting message")
    adapter.inject_message(content)
  else
    adapter.send_message(content)
  end

  true
end

#subscribe(session_id, delivery_kind: "web-remote-replayable") ⇒ Object

Subscribe to a session's events.



133
134
135
136
137
138
139
# File 'lib/ask/app_server/session_manager.rb', line 133

def subscribe(session_id, delivery_kind: "web-remote-replayable")
  adapter = @store.get(session_id)
  raise Ask::AppServer::SessionNotFound, "Session #{session_id} not found" unless adapter

  @store.subscribe(session_id, delivery_kind: delivery_kind)
  { subscribed: true, sessionId: session_id, deliveryKind: delivery_kind }
end

#subscribed?(session_id) ⇒ Boolean

Check if a session is subscribed.

Returns:

  • (Boolean)


182
183
184
# File 'lib/ask/app_server/session_manager.rb', line 182

def subscribed?(session_id)
  @store.subscribed?(session_id)
end