Class: Ask::CodingProviders::AskAgent::Adapter
- Inherits:
-
Ask::CodingProviders::Adapter
- Object
- Ask::CodingProviders::Adapter
- Ask::CodingProviders::AskAgent::Adapter
- Defined in:
- lib/ask/coding_providers/ask_agent/adapter.rb
Overview
Adapter that wraps Ask::Agent::Session directly (in-process).
Sessions persist across turns: each session id maps to one Ask::Agent::Session instance whose conversation history accumulates. Every agent event is translated and streamed to subscribers, including thinking deltas, tool executions, approvals, plans, and todos. When a tool queues for human approval, the turn pauses; approving or rejecting from any thread continues the turn (follow-up turns run inside ask-agent and their events reach the same subscribers).
Constant Summary collapse
- APPROVAL_OFF =
Approval modes: :off disables the queue, :require gates approval_required tools behind human review, :auto keeps the queue (visible/inspectable) but never blocks.
:off- APPROVAL_REQUIRE =
:require- APPROVAL_AUTO =
:auto- APPROVAL_MODES =
[APPROVAL_OFF, APPROVAL_REQUIRE, APPROVAL_AUTO].freeze
- APPROVAL_SCOPES =
Approval scopes this adapter accepts. :once approves only the named action; :session records a session grant (applied by Ask::Agent::Session before the approved action runs) so matching actions later in the session are auto-approved. :project is deliberately excluded: this adapter never injects project grants into the queue, so accepting it would be a silent no-op.
%i[once session].freeze
- SETTLE_POLL_INTERVAL =
How long a turn stays settled before it is considered complete (protects against follow-up turns starting right after the queue drains).
0.05- SETTLE_POLLS =
seconds
4
Class Method Summary collapse
-
.from_config(model: nil, llm_provider: nil, max_turns: nil) ⇒ Object
Build an AskAgent adapter from config.
Instance Method Summary collapse
-
#abort(session_id) ⇒ Object
Abort the current turn.
-
#approve_action(session_id, action_id, scope: :once) ⇒ Array<Ask::Permissions::Action>
Approve one queued tool action.
-
#approve_all(session_id, scope: :once) ⇒ Object
Approve all pending tool actions.
-
#approve_plan(session_id) ⇒ Object
Approve / reject the proposed plan (plan mode).
-
#create_session(workspace_path, mode: nil, model: nil, system_prompt: nil, agent: nil) ⇒ Object
Create a new conversation session for a workspace.
- #get_events(session_id, after_seq:, limit: nil) ⇒ Object
- #get_workspace_state(workspace_path) ⇒ Object
-
#initialize(model:, provider:, tools: [], max_turns: 25, approval: APPROVAL_OFF, approval_required: nil, plan_mode: false, todos: false, **session_opts) ⇒ Adapter
constructor
A new instance of Adapter.
- #list_sessions(workspace_path: nil, limit: 20) ⇒ Object
-
#pending_approvals(session_id) ⇒ Object
Actions still awaiting a decision, as plain hashes.
-
#pending_plan(session_id) ⇒ Object
The pending plan awaiting approval (plan mode), or nil.
- #reject_action(session_id, action_id) ⇒ Object
- #reject_all(session_id) ⇒ Object
- #reject_plan(session_id) ⇒ Object
- #respond(request_id, result) ⇒ Object
- #resume_session(session_id) ⇒ Object
- #running? ⇒ Boolean
-
#send_and_stream(session_id, content, turn_timeout: 600.0, attachments: nil) {|Hash| ... } ⇒ Object
Send a message and stream translated events to the block.
- #send_message(session_id, content, attachments: nil) ⇒ Object
- #session_directory(session_id) ⇒ Object
-
#session_history(session_id, limit: 100) ⇒ Object
Message history for a session, newest first.
- #start ⇒ Object
- #stop ⇒ Object
- #subscribe(session_id, after_seq: 0) ⇒ Object
Methods inherited from Ask::CodingProviders::Adapter
#find_recent_session, #find_recent_tui_session, #find_sessions, #handle_session_error, #list_projects, #recent_sessions
Constructor Details
#initialize(model:, provider:, tools: [], max_turns: 25, approval: APPROVAL_OFF, approval_required: nil, plan_mode: false, todos: false, **session_opts) ⇒ Adapter
Returns a new instance of Adapter.
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 119 def initialize(model:, provider:, tools: [], max_turns: 25, approval: APPROVAL_OFF, approval_required: nil, plan_mode: false, todos: false, **session_opts) @model_id = model @provider_slug = provider @tools = Array(tools) @max_turns = max_turns @approval = approval @approval_required = Array(approval_required) @plan_mode = !!plan_mode @todos = !!todos @session_opts = session_opts @started = false @provider = nil @sessions = {} @mutex = Mutex.new unless APPROVAL_MODES.include?(approval) raise ArgumentError, "approval must be one of #{APPROVAL_MODES.inspect}, got #{approval.inspect}" end end |
Class Method Details
.from_config(model: nil, llm_provider: nil, max_turns: nil) ⇒ Object
Build an AskAgent adapter from config. Reads ASK_AGENT_MODEL, ASK_AGENT_LLM_PROVIDER, ASK_AGENT_MAX_TURNS from ENV.
377 378 379 380 381 382 383 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 377 def self.from_config(model: nil, llm_provider: nil, max_turns: nil, **) new( model: model || ENV.fetch("ASK_AGENT_MODEL", "deepseek-v4-flash"), provider: llm_provider || ENV.fetch("ASK_AGENT_LLM_PROVIDER", "opencode_go"), max_turns: (max_turns || ENV.fetch("ASK_AGENT_MAX_TURNS", "10")).to_i ) end |
Instance Method Details
#abort(session_id) ⇒ Object
Abort the current turn. The loop exits at the next checkpoint and the stream emits turn.aborted.
340 341 342 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 340 def abort(session_id) session_entry(session_id)[:session]&.abort end |
#approve_action(session_id, action_id, scope: :once) ⇒ Array<Ask::Permissions::Action>
Approve one queued tool action. Continues the turn (follow-up turns run in this thread and stream to active subscribers).
288 289 290 291 292 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 288 def approve_action(session_id, action_id, scope: :once) validate_approval_scope!(scope) queue = approval_queue(session_id) queue ? queue.approve(action_id, scope: scope) : [] end |
#approve_all(session_id, scope: :once) ⇒ Object
Approve all pending tool actions.
303 304 305 306 307 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 303 def approve_all(session_id, scope: :once) validate_approval_scope!(scope) queue = approval_queue(session_id) queue ? queue.approve_all(scope: scope) : [] end |
#approve_plan(session_id) ⇒ Object
Approve / reject the proposed plan (plan mode).
330 331 332 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 330 def approve_plan(session_id) session_entry(session_id)[:session]&.plan_queue&.approve_all || [] end |
#create_session(workspace_path, mode: nil, model: nil, system_prompt: nil, agent: nil) ⇒ Object
Create a new conversation session for a workspace. Returns a session ID (UUID).
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 176 def create_session(workspace_path, mode: nil, model: nil, system_prompt: nil, agent: nil) ensure_started sid = "sess_#{SecureRandom.uuid}" @mutex.synchronize do @sessions[sid] = { workspace: workspace_path, mode: mode, model: model || @model_id, system_prompt: system_prompt, agent: agent, created_at: Time.now, session: nil, subscribers: [], seq: 0, turn_active: false } end sid end |
#get_events(session_id, after_seq:, limit: nil) ⇒ Object
344 345 346 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 344 def get_events(session_id, after_seq:, limit: nil) { "events" => [] } end |
#get_workspace_state(workspace_path) ⇒ Object
352 353 354 355 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 352 def get_workspace_state(workspace_path) # No workspace state to report {} end |
#list_sessions(workspace_path: nil, limit: 20) ⇒ Object
208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 208 def list_sessions(workspace_path: nil, limit: 20) ensure_started @mutex.synchronize do @sessions .select { |_sid, e| workspace_path.nil? || e[:workspace] == workspace_path } .sort_by { |sid, e| [e[:created_at], sid] } .reverse .first(limit) .map { |sid, e| { session_id: sid, workspace: e[:workspace], created_at: e[:created_at].iso8601 } } end end |
#pending_approvals(session_id) ⇒ Object
Actions still awaiting a decision, as plain hashes.
315 316 317 318 319 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 315 def pending_approvals(session_id) queue = approval_queue(session_id) return [] unless queue queue.pending_actions.map { |a| action_hash(a) } end |
#pending_plan(session_id) ⇒ Object
The pending plan awaiting approval (plan mode), or nil.
322 323 324 325 326 327 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 322 def pending_plan(session_id) session = session_entry(session_id)[:session] return nil unless session&.plan_queue action = session.plan_queue.pending_actions.first action && action_hash(action) end |
#reject_action(session_id, action_id) ⇒ Object
294 295 296 297 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 294 def reject_action(session_id, action_id) queue = approval_queue(session_id) queue ? queue.reject(action_id) : [] end |
#reject_all(session_id) ⇒ Object
309 310 311 312 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 309 def reject_all(session_id) queue = approval_queue(session_id) queue ? queue.reject_all : [] end |
#reject_plan(session_id) ⇒ Object
334 335 336 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 334 def reject_plan(session_id) session_entry(session_id)[:session]&.plan_queue&.reject_all || [] end |
#respond(request_id, result) ⇒ Object
348 349 350 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 348 def respond(request_id, result) # No reverse requests in basic mode end |
#resume_session(session_id) ⇒ Object
196 197 198 199 200 201 202 203 204 205 206 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 196 def resume_session(session_id) ensure_started entry = @sessions[session_id] return {} unless entry { "session_id" => session_id, "workspace" => entry[:workspace], "model" => entry[:model], "created_at" => entry[:created_at].iso8601 } end |
#running? ⇒ Boolean
158 159 160 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 158 def running? @started end |
#send_and_stream(session_id, content, turn_timeout: 600.0, attachments: nil) {|Hash| ... } ⇒ Object
Send a message and stream translated events to the block.
Runs the session's loop synchronously; when tools queue for approval the turn pauses and this method waits (up to turn_timeout) for the queue to drain, so subscribers receive the full turn — including follow-up turns that run when approvals resolve.
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 242 def send_and_stream(session_id, content, turn_timeout: 600.0, attachments: nil, &block) return enum_for(:send_and_stream, session_id, content, turn_timeout: turn_timeout, attachments: ) unless block ensure_started entry = session_entry(session_id) session = (entry[:session] ||= build_session(entry)) subscription = subscribe_session(entry, &block) emit(entry, { type: "turn.started", seq: next_seq(entry), payload: { "sessionId" => session_id } }) begin result = run_with_approvals(entry, session, content, attachments: , turn_timeout: turn_timeout) if session.abort_requested? emit(entry, { type: "turn.aborted", seq: next_seq(entry), payload: { "sessionId" => session_id } }) else emit(entry, { type: "turn.completed", seq: next_seq(entry), payload: { "response" => (result || accumulated_text(entry)).to_s, "sessionId" => session_id, "tokenCount" => session.total_input_tokens + session.total_output_tokens } }) end rescue => e emit(entry, { type: "turn.failed", seq: next_seq(entry), payload: { "error" => { "message" => e. }, "sessionId" => session_id } }) ensure unsubscribe_session(entry, subscription) entry[:turn_active] = false end nil end |
#send_message(session_id, content, attachments: nil) ⇒ Object
225 226 227 228 229 230 231 232 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 225 def (session_id, content, attachments: nil) ensure_started result = nil send_and_stream(session_id, content, attachments: ) do |ev| result = ev.dig(:payload, "response") if ev[:type] == "turn.completed" end { "response" => result } end |
#session_directory(session_id) ⇒ Object
357 358 359 360 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 357 def session_directory(session_id) entry = @mutex.synchronize { @sessions[session_id] } entry && entry[:workspace] end |
#session_history(session_id, limit: 100) ⇒ Object
Message history for a session, newest first. Empty for unknown or not-yet-run sessions.
364 365 366 367 368 369 370 371 372 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 364 def session_history(session_id, limit: 100) entry = @mutex.synchronize { @sessions[session_id] } return [] unless entry session = entry[:session] return [] unless session session..last(limit).reverse.map do |m| { text: m.content.to_s, role: m.role.to_s, origin: "ask_agent" } end end |
#start ⇒ Object
140 141 142 143 144 145 146 147 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 140 def start return if @started klass = Ask::Provider.resolve(@provider_slug) compat = klass.respond_to?(:compat_config) ? klass.compat_config : {} api_key = ENV[compat[:alternate_env].to_s] || ENV[compat[:api_key_env].to_s] || ENV["#{@provider_slug.upcase}_API_KEY"] @provider = klass.new(api_key: api_key) @started = true end |
#stop ⇒ Object
149 150 151 152 153 154 155 156 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 149 def stop @mutex.synchronize do @sessions.each_value { |entry| entry[:session]&.abort } @sessions.clear end @started = false @provider = nil end |
#subscribe(session_id, after_seq: 0) ⇒ Object
220 221 222 223 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 220 def subscribe(session_id, after_seq: 0) ensure_started { "eventSeq" => session_entry(session_id)[:seq] } end |