Class: Ask::AppServer::AgentAdapter

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

Overview

Wraps an Ask::Agent::Session and translates its events into canonical Ask::SessionProtocol events via EventTranslator.

Each wrapper is associated with one session and maintains an EventTranslator that clients poll or subscribe to. The adapter also owns the approval queue wiring (approval events surface as approval.required / approval.updated) and the interaction controls (approve/reject by id, plan approve/reject) that any client can call.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model:, tools: nil, system_prompt: nil, agent_dir: nil, approval: :off, require_approval: nil, **session_opts) ⇒ AgentAdapter

Returns a new instance of AgentAdapter.

Parameters:

  • model (String)

    model identifier

  • tools (Array<String, Class>) (defaults to: nil)

    tool names or classes

  • system_prompt (String, nil) (defaults to: nil)
  • agent_dir (String, nil) (defaults to: nil)

    workspace path

  • approval (Symbol) (defaults to: :off)

    :off, :require, or :auto

  • require_approval (Array<String>, nil) (defaults to: nil)

    tool names gated behind human approval when approval is :require

  • session_opts (Hash)

    remaining options passed to Ask::Agent::Session.new (hooks, plan_mode, todos, ...)



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/ask/app_server/agent_adapter.rb', line 38

def initialize(model:, tools: nil, system_prompt: nil, agent_dir: nil,
               approval: :off, require_approval: nil, **session_opts)
  @model = model
  @system_prompt = system_prompt
  @tools = resolve_tools(tools)
  @session_opts = session_opts
  @approval = approval
  @require_approval = require_approval
  @agent_dir = agent_dir
  @session = nil
  @translator = nil
  @on_event_block = nil
  @session_id = nil
  @running = false
  @running_mutex = Mutex.new
  @run_thread = nil
  @abort_requested = false
  @created_at = Time.now
  @logger = Logger.new($stdout, level: ENV["DEBUG"] ? Logger::DEBUG : Logger::WARN)
end

Instance Attribute Details

#created_atObject (readonly)

When the session was created.



27
28
29
# File 'lib/ask/app_server/agent_adapter.rb', line 27

def created_at
  @created_at
end

#runningObject (readonly)

Whether a turn is currently in progress.



24
25
26
# File 'lib/ask/app_server/agent_adapter.rb', line 24

def running
  @running
end

#sessionObject (readonly)

The underlying ask-agent session.



15
16
17
# File 'lib/ask/app_server/agent_adapter.rb', line 15

def session
  @session
end

#session_idObject (readonly)

The session ID (same as ask-agent session id).



21
22
23
# File 'lib/ask/app_server/agent_adapter.rb', line 21

def session_id
  @session_id
end

#translatorObject (readonly)

The event translator that accumulates protocol events.



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

def translator
  @translator
end

Instance Method Details

#abort_turn!Object

Request abort of the current turn.



117
118
119
120
# File 'lib/ask/app_server/agent_adapter.rb', line 117

def abort_turn!
  @abort_requested = true
  @session&.abort if @session
end

#approve_all_interactionsInteger

Approve every pending approval interaction.

Returns:

  • (Integer)

    number approved



169
170
171
172
173
174
# File 'lib/ask/app_server/agent_adapter.rb', line 169

def approve_all_interactions
  queue = @session&.approval_queue
  return 0 unless queue

  queue.approve_all.size
end

#approve_interaction(interaction_id) ⇒ Boolean

Approve a pending approval interaction by canonical id ("act_N").

Returns:

  • (Boolean)

    whether an action was approved



157
158
159
# File 'lib/ask/app_server/agent_adapter.rb', line 157

def approve_interaction(interaction_id)
  apply_interaction(interaction_id) { |queue, id| queue.approve(id) }
end

#close!Object

Close the session: delete its state and emit session.ended.



208
209
210
211
212
# File 'lib/ask/app_server/agent_adapter.rb', line 208

def close!
  @session&.delete if @session.respond_to?(:delete)
  @translator&.session_ended(@session_id, reason: "closed")
  true
end

#drain_eventsObject

Drain and return pending events.



225
226
227
# File 'lib/ask/app_server/agent_adapter.rb', line 225

def drain_events
  @translator&.drain_events || []
end

#events_after(after_seq) ⇒ Object

Events after a given sequence number.



235
236
237
# File 'lib/ask/app_server/agent_adapter.rb', line 235

def events_after(after_seq)
  pending_events.select { |e| e.seq > after_seq }
end

#idle?Boolean

Whether this session is idle (no turn running).

Returns:

  • (Boolean)


133
134
135
# File 'lib/ask/app_server/agent_adapter.rb', line 133

def idle?
  !@running
end

#last_seqObject

Last sequence number.



230
231
232
# File 'lib/ask/app_server/agent_adapter.rb', line 230

def last_seq
  @translator&.last_seq || 0
end

#on_event(&block) ⇒ Object

Register an observer for every canonical event this session emits (translations plus approval/plan/session-lifecycle emissions). May be called before start_session; the block is applied when the translator exists so the observer sees session.created.



85
86
87
88
89
90
91
# File 'lib/ask/app_server/agent_adapter.rb', line 85

def on_event(&block)
  if @translator
    @translator.on_event = block
  else
    @on_event_block = block
  end
end

#pending_eventsObject

All events since last drain.



220
221
222
# File 'lib/ask/app_server/agent_adapter.rb', line 220

def pending_events
  @translator&.pending_events || []
end

#pending_interactionsObject

Pending approval interactions, as canonical Interaction objects.



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

def pending_interactions
  queue = @session&.approval_queue
  return [] unless queue

  queue.pending_actions.map do |action|
    payload = { "toolName" => action.tool_name.to_s }
    payload["args"] = action.args if action.args
    payload["message"] = action.message if action.message
    payload["autoApprovable"] = action.auto_approvable unless action.auto_approvable.nil?
    Ask::SessionProtocol::Interactions.interaction(
      id: "act_#{action.id}", kind: "approval", status: "pending", payload: payload
    )
  end
end

#plan_approveBoolean

Approve the pending plan proposal.

Returns:

  • (Boolean)


189
190
191
192
193
194
# File 'lib/ask/app_server/agent_adapter.rb', line 189

def plan_approve
  queue = @session&.plan_queue
  return false unless queue

  queue.approve_all.any?
end

#plan_rejectBoolean

Reject the pending plan proposal; the agent stays in plan mode.

Returns:

  • (Boolean)


198
199
200
201
202
203
# File 'lib/ask/app_server/agent_adapter.rb', line 198

def plan_reject
  queue = @session&.plan_queue
  return false unless queue

  queue.reject_all.any?
end

#reject_all_interactionsInteger

Reject every pending approval interaction.

Returns:

  • (Integer)

    number rejected



178
179
180
181
182
183
# File 'lib/ask/app_server/agent_adapter.rb', line 178

def reject_all_interactions
  queue = @session&.approval_queue
  return 0 unless queue

  queue.reject_all.size
end

#reject_interaction(interaction_id) ⇒ Boolean

Reject a pending approval interaction by canonical id ("act_N").

Returns:

  • (Boolean)

    whether an action was rejected



163
164
165
# File 'lib/ask/app_server/agent_adapter.rb', line 163

def reject_interaction(interaction_id)
  apply_interaction(interaction_id) { |queue, id| queue.reject(id) }
end

#resume(session) ⇒ Object

Resume an existing session (re-attach event handler).



72
73
74
75
76
77
78
79
# File 'lib/ask/app_server/agent_adapter.rb', line 72

def resume(session)
  @session = session
  @session_id = session.id
  @translator = EventTranslator.new
  @translator.on_event = @on_event_block if @on_event_block
  @session.on_event { |event| handle_agent_event(event) }
  @session_id
end

#send_message(content, expected_turn_id: nil) ⇒ Hash

Send a message and start processing (idle session) or inject it mid-run (running session). Uses ask-agent's steer semantics:

steered — the message was added to the conversation immediately
queued  — the session is running; the message is queued for the
        next turn boundary (no abort)
stale   — the caller's expected_turn_id no longer matches

Parameters:

  • content (String)
  • expected_turn_id (String, nil) (defaults to: nil)

    staleness guard

Returns:

  • (Hash)

    { status: "steered"|"queued"|"stale", turn_id: }



104
105
106
107
108
109
110
111
112
113
114
# File 'lib/ask/app_server/agent_adapter.rb', line 104

def send_message(content, expected_turn_id: nil)
  raise "Session not started" unless @session

  if @running
    result = @session.steer(content, expected_turn_id: expected_turn_id)
    { status: result[:status].to_s, turn_id: result[:turn_id] }
  else
    start_run(content)
    { status: "steered", turn_id: @session.turn_id }
  end
end

#start_sessionObject

Start a new ask-agent session. Returns the session ID.



61
62
63
64
65
66
67
68
69
# File 'lib/ask/app_server/agent_adapter.rb', line 61

def start_session
  @translator = EventTranslator.new
  @translator.on_event = @on_event_block if @on_event_block
  @session = build_session
  @session_id = @session.id
  @session.on_event { |event| handle_agent_event(event) }
  @translator.session_created(@session_id)
  @session_id
end

#streaming_textObject

The accumulated streaming text from the current/last turn.



215
216
217
# File 'lib/ask/app_server/agent_adapter.rb', line 215

def streaming_text
  @translator&.instance_variable_get(:@streaming_text).to_s
end

#wait_for_turn(timeout: 600) ⇒ Object

Wait for the current turn to complete (with timeout). Returns true if completed, false if timed out.



124
125
126
127
128
129
130
# File 'lib/ask/app_server/agent_adapter.rb', line 124

def wait_for_turn(timeout: 600)
  thread = @run_thread
  return true unless thread

  thread.join(timeout)
  !thread.alive?
end