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
- 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) ⇒ Array<Ask::Agent::ApprovalQueue::Action>
Approve one queued tool action.
-
#approve_all(session_id) ⇒ 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.
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 101 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.
350 351 352 353 354 355 356 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 350 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.
313 314 315 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 313 def abort(session_id) session_entry(session_id)[:session]&.abort end |
#approve_action(session_id, action_id) ⇒ Array<Ask::Agent::ApprovalQueue::Action>
Approve one queued tool action. Continues the turn (follow-up turns run in this thread and stream to active subscribers).
266 267 268 269 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 266 def approve_action(session_id, action_id) queue = approval_queue(session_id) queue ? queue.approve(action_id) : [] end |
#approve_all(session_id) ⇒ Object
Approve all pending tool actions.
277 278 279 280 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 277 def approve_all(session_id) queue = approval_queue(session_id) queue ? queue.approve_all : [] end |
#approve_plan(session_id) ⇒ Object
Approve / reject the proposed plan (plan mode).
303 304 305 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 303 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).
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 158 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
317 318 319 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 317 def get_events(session_id, after_seq:, limit: nil) { "events" => [] } end |
#get_workspace_state(workspace_path) ⇒ Object
325 326 327 328 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 325 def get_workspace_state(workspace_path) # No workspace state to report {} end |
#list_sessions(workspace_path: nil, limit: 20) ⇒ Object
190 191 192 193 194 195 196 197 198 199 200 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 190 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.
288 289 290 291 292 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 288 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.
295 296 297 298 299 300 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 295 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
271 272 273 274 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 271 def reject_action(session_id, action_id) queue = approval_queue(session_id) queue ? queue.reject(action_id) : [] end |
#reject_all(session_id) ⇒ Object
282 283 284 285 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 282 def reject_all(session_id) queue = approval_queue(session_id) queue ? queue.reject_all : [] end |
#reject_plan(session_id) ⇒ Object
307 308 309 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 307 def reject_plan(session_id) session_entry(session_id)[:session]&.plan_queue&.reject_all || [] end |
#respond(request_id, result) ⇒ Object
321 322 323 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 321 def respond(request_id, result) # No reverse requests in basic mode end |
#resume_session(session_id) ⇒ Object
178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 178 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
140 141 142 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 140 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.
224 225 226 227 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 258 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 224 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
207 208 209 210 211 212 213 214 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 207 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
330 331 332 333 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 330 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.
337 338 339 340 341 342 343 344 345 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 337 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
122 123 124 125 126 127 128 129 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 122 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
131 132 133 134 135 136 137 138 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 131 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
202 203 204 205 |
# File 'lib/ask/coding_providers/ask_agent/adapter.rb', line 202 def subscribe(session_id, after_seq: 0) ensure_started { "eventSeq" => session_entry(session_id)[:seq] } end |