Class: Ask::AppServer::SessionManager
- Inherits:
-
Object
- Object
- Ask::AppServer::SessionManager
- Defined in:
- lib/ask/app_server/session_manager.rb
Overview
Manages the lifecycle of agent sessions exposed through the session host (ask-app-server).
Orchestrates creation, resumption, subscription, messaging, aborting, interaction resolution (approvals, plan), closing, 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")
- DEFAULT_REQUIRE_APPROVAL =
Tools gated behind human approval when approval is :require.
%w[write edit bash destroy].freeze
Instance Attribute Summary collapse
-
#blocked_tools ⇒ Object
readonly
Returns the value of attribute blocked_tools.
-
#permission_mode ⇒ Object
Returns the value of attribute permission_mode.
-
#permission_timeout ⇒ Object
readonly
Returns the value of attribute permission_timeout.
-
#store ⇒ Object
readonly
Returns the value of attribute store.
Instance Method Summary collapse
-
#approval_mode ⇒ Object
The approval mode new sessions get, translated to the canonical vocabulary (:off, :require, :auto).
-
#approve_all_interactions(session_id) ⇒ Integer
Approve every pending approval interaction.
-
#approve_interaction(session_id, interaction_id) ⇒ Boolean
Approve a pending approval interaction by canonical id.
-
#close_session(session_id) ⇒ Object
Close a session (protocol alias of destroy_session).
-
#create_session(workspace_path: nil, mode: nil, model: nil, tools: nil, system_prompt: nil) ⇒ Object
Create a new session.
-
#destroy_session(session_id) ⇒ Object
Remove a session.
-
#get(session_id) ⇒ Object
Get an adapter by session ID.
-
#get_events(session_id, after_seq:, limit: nil) ⇒ Object
Get events for a session after a sequence number.
-
#initialize(store: nil, permission_mode: :on_request, blocked_tools: nil, permission_timeout: 300) ⇒ SessionManager
constructor
A new instance of SessionManager.
-
#list_sessions(limit: 20) ⇒ Object
List active sessions.
-
#on_session_event(&block) ⇒ Object
Register an observer for canonical events across all sessions.
-
#pending_interactions(session_id) ⇒ Object
Pending approval interactions for a session.
-
#plan_approve(session_id) ⇒ Boolean
Approve the pending plan proposal.
-
#plan_reject(session_id) ⇒ Boolean
Reject the pending plan proposal; the agent stays in plan mode.
-
#read_workspace_state(session_id = nil) ⇒ Object
Read workspace state.
-
#reject_all_interactions(session_id) ⇒ Integer
Reject every pending approval interaction.
-
#reject_interaction(session_id, interaction_id) ⇒ Boolean
Reject a pending approval interaction by canonical id.
-
#send_message(session_id, content, expected_turn_id: nil) ⇒ Hash
Send a message to a session (prompt when idle, mid-execution injection when running).
-
#subscribe(session_id, delivery_kind: "replay") ⇒ Object
Subscribe to a session's events.
-
#subscribed?(session_id) ⇒ Boolean
Check if a session is subscribed.
Constructor Details
#initialize(store: nil, permission_mode: :on_request, blocked_tools: nil, permission_timeout: 300) ⇒ SessionManager
Returns a new instance of SessionManager.
26 27 28 29 30 31 32 33 |
# File 'lib/ask/app_server/session_manager.rb', line 26 def initialize(store: nil, permission_mode: :on_request, blocked_tools: nil, permission_timeout: 300) @store = store || SessionStore.new @permission_mode = @blocked_tools = (blocked_tools || DEFAULT_REQUIRE_APPROVAL).map(&:to_s) @permission_timeout = @session_event_observers = [] @logger = Logger.new($stdout, level: ENV["DEBUG"] ? Logger::DEBUG || Logger::DEBUG : Logger::WARN) end |
Instance Attribute Details
#blocked_tools ⇒ Object (readonly)
Returns the value of attribute blocked_tools.
23 24 25 |
# File 'lib/ask/app_server/session_manager.rb', line 23 def blocked_tools @blocked_tools end |
#permission_mode ⇒ Object
Returns the value of attribute permission_mode.
22 23 24 |
# File 'lib/ask/app_server/session_manager.rb', line 22 def @permission_mode end |
#permission_timeout ⇒ Object (readonly)
Returns the value of attribute permission_timeout.
24 25 26 |
# File 'lib/ask/app_server/session_manager.rb', line 24 def @permission_timeout end |
#store ⇒ Object (readonly)
Returns the value of attribute store.
21 22 23 |
# File 'lib/ask/app_server/session_manager.rb', line 21 def store @store end |
Instance Method Details
#approval_mode ⇒ Object
The approval mode new sessions get, translated to the canonical vocabulary (:off, :require, :auto).
49 50 51 |
# File 'lib/ask/app_server/session_manager.rb', line 49 def approval_mode (@permission_mode) end |
#approve_all_interactions(session_id) ⇒ Integer
Approve every pending approval interaction.
165 166 167 168 169 170 |
# File 'lib/ask/app_server/session_manager.rb', line 165 def approve_all_interactions(session_id) adapter = @store.get(session_id) raise Ask::AppServer::SessionNotFound, "Session #{session_id} not found" unless adapter adapter.approve_all_interactions end |
#approve_interaction(session_id, interaction_id) ⇒ Boolean
Approve a pending approval interaction by canonical id.
147 148 149 150 151 152 |
# File 'lib/ask/app_server/session_manager.rb', line 147 def approve_interaction(session_id, interaction_id) adapter = @store.get(session_id) raise Ask::AppServer::SessionNotFound, "Session #{session_id} not found" unless adapter adapter.approve_interaction(interaction_id) end |
#close_session(session_id) ⇒ Object
Close a session (protocol alias of destroy_session).
105 106 107 |
# File 'lib/ask/app_server/session_manager.rb', line 105 def close_session(session_id) destroy_session(session_id) end |
#create_session(workspace_path: nil, mode: nil, model: nil, tools: nil, system_prompt: nil) ⇒ Object
Create a new session. Returns the session ID.
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 86 87 88 89 90 |
# File 'lib/ask/app_server/session_manager.rb', line 60 def create_session(workspace_path: nil, mode: nil, model: nil, tools: nil, system_prompt: nil) approval_opts, plan_mode = resolve_approval(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) 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, approval: approval_opts[:mode], require_approval: approval_opts[:require_approval], plan_mode: plan_mode ) session_id = adapter.start_session @store.add(session_id, adapter) # Attach observers after registration so they see a settled store # (e.g. the pane reporter's session-count metadata), then replay # the session.created event the adapter buffered during start. adapter.on_event { |event| notify_session_event(adapter.session_id, event) } created = adapter.pending_events.find { |e| e.type == "session.created" } notify_session_event(session_id, created) if created @logger.info("Created session #{session_id} (model=#{model || DEFAULT_MODEL}, approval=#{approval_opts[:mode]})") session_id end |
#destroy_session(session_id) ⇒ Object
Remove a session.
93 94 95 96 97 98 99 100 101 102 |
# File 'lib/ask/app_server/session_manager.rb', line 93 def destroy_session(session_id) adapter = @store.get(session_id) return false unless adapter adapter.abort_turn! if adapter.running adapter.close! @store.remove(session_id) @logger.info("Destroyed session #{session_id}") true end |
#get(session_id) ⇒ Object
Get an adapter by session ID.
110 111 112 |
# File 'lib/ask/app_server/session_manager.rb', line 110 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.
213 214 215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/ask/app_server/session_manager.rb', line 213 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.
115 116 117 |
# File 'lib/ask/app_server/session_manager.rb', line 115 def list_sessions(limit: 20) @store.list(limit: limit) end |
#on_session_event(&block) ⇒ Object
Register an observer for canonical events across all sessions. Called with (session_id, Ask::SessionProtocol::Events::Event) for every emitted event, as it happens.
38 39 40 |
# File 'lib/ask/app_server/session_manager.rb', line 38 def on_session_event(&block) @session_event_observers << block end |
#pending_interactions(session_id) ⇒ Object
Pending approval interactions for a session.
138 139 140 141 142 143 |
# File 'lib/ask/app_server/session_manager.rb', line 138 def pending_interactions(session_id) adapter = @store.get(session_id) raise Ask::AppServer::SessionNotFound, "Session #{session_id} not found" unless adapter adapter.pending_interactions end |
#plan_approve(session_id) ⇒ Boolean
Approve the pending plan proposal.
185 186 187 188 189 190 |
# File 'lib/ask/app_server/session_manager.rb', line 185 def plan_approve(session_id) adapter = @store.get(session_id) raise Ask::AppServer::SessionNotFound, "Session #{session_id} not found" unless adapter adapter.plan_approve end |
#plan_reject(session_id) ⇒ Boolean
Reject the pending plan proposal; the agent stays in plan mode.
194 195 196 197 198 199 |
# File 'lib/ask/app_server/session_manager.rb', line 194 def plan_reject(session_id) adapter = @store.get(session_id) raise Ask::AppServer::SessionNotFound, "Session #{session_id} not found" unless adapter adapter.plan_reject end |
#read_workspace_state(session_id = nil) ⇒ Object
Read workspace state.
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 |
# File 'lib/ask/app_server/session_manager.rb', line 228 def read_workspace_state(session_id = nil) workspace = { path: nil, name: nil, gitBranch: nil, mode: approval_mode.to_s } if session_id adapter = @store.get(session_id) if adapter workspace[:sessionId] = session_id workspace[:running] = adapter.running end end settings = { model: { current: { modelId: ENV.fetch("ASK_APP_SERVER_MODEL", DEFAULT_MODEL), providerId: resolve_provider_id } }, permissions: { mode: @permission_mode.to_s } } { workspace: workspace, settings: settings } end |
#reject_all_interactions(session_id) ⇒ Integer
Reject every pending approval interaction.
174 175 176 177 178 179 |
# File 'lib/ask/app_server/session_manager.rb', line 174 def reject_all_interactions(session_id) adapter = @store.get(session_id) raise Ask::AppServer::SessionNotFound, "Session #{session_id} not found" unless adapter adapter.reject_all_interactions end |
#reject_interaction(session_id, interaction_id) ⇒ Boolean
Reject a pending approval interaction by canonical id.
156 157 158 159 160 161 |
# File 'lib/ask/app_server/session_manager.rb', line 156 def reject_interaction(session_id, interaction_id) adapter = @store.get(session_id) raise Ask::AppServer::SessionNotFound, "Session #{session_id} not found" unless adapter adapter.reject_interaction(interaction_id) end |
#send_message(session_id, content, expected_turn_id: nil) ⇒ Hash
Send a message to a session (prompt when idle, mid-execution injection when running).
123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/ask/app_server/session_manager.rb', line 123 def (session_id, content, expected_turn_id: nil) adapter = @store.get(session_id) raise Ask::AppServer::SessionNotFound, "Session #{session_id} not found" unless adapter result = adapter.(content, expected_turn_id: expected_turn_id) { accepted: result[:status] != "stale", status: result[:status], turnId: result[:turn_id] } end |
#subscribe(session_id, delivery_kind: "replay") ⇒ Object
Subscribe to a session's events.
204 205 206 207 208 209 210 |
# File 'lib/ask/app_server/session_manager.rb', line 204 def subscribe(session_id, delivery_kind: "replay") adapter = @store.get(session_id) raise Ask::AppServer::SessionNotFound, "Session #{session_id} not found" unless adapter @store.subscribe(session_id, delivery_kind: delivery_kind) { subscription: { sessionId: session_id, deliveryKind: delivery_kind } } end |
#subscribed?(session_id) ⇒ Boolean
Check if a session is subscribed.
260 261 262 |
# File 'lib/ask/app_server/session_manager.rb', line 260 def subscribed?(session_id) @store.subscribed?(session_id) end |