Class: Ask::Agent::ApprovalQueue

Inherits:
Object
  • Object
show all
Defined in:
lib/ask/agent/approval_queue.rb

Overview

A queue of tool actions awaiting human approval.

When an agent calls a tool that requires approval, the action is queued instead of executed — the agent gets a pending result and continues. The user approves or rejects actions later, in bulk or one-by-one.

queue = Ask::Agent::ApprovalQueue.new
id = queue.submit(tool_call_id: "call_1", tool_name: "send_email",
                args: { "to" => "x@y.com" }, auto_approvable: false)
queue.pending_actions   # => [{id: 1, tool_name: "send_email", ...}]
queue.approve(1)        # applies action 1 via the on_approve callback
queue.reject(1)

Auto-approval is a dual signal: the action must be marked auto_approvable (by the tool) AND the queue must have a matching auto-approval rule enabled by the user. Otherwise the action queues for human review.

Actions are applied in id order and the drainer never applies past a non-auto-approvable (manual) gate — nothing is silently approved.

Defined Under Namespace

Classes: Action

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(on_approve: nil, on_reject: nil, auto_approve: nil, on_submit: nil) ⇒ ApprovalQueue

Returns a new instance of ApprovalQueue.



72
73
74
75
76
77
78
79
80
81
# File 'lib/ask/agent/approval_queue.rb', line 72

def initialize(on_approve: nil, on_reject: nil, auto_approve: nil, on_submit: nil)
  @on_approve = on_approve
  @on_reject = on_reject
  @on_submit = on_submit
  @auto_approve = auto_approve || {}
  @actions = {}
  @next_id = 1
  @mutex = Mutex.new
  @draining = false
end

Instance Attribute Details

#on_approveObject

Called with an Action when it is approved and applied. The session wires this to execute the real tool call; can be replaced after construction (e.g. by queue subclasses that also emit events).



70
71
72
# File 'lib/ask/agent/approval_queue.rb', line 70

def on_approve
  @on_approve
end

#on_rejectObject

Called with an Action when it is rejected.



70
# File 'lib/ask/agent/approval_queue.rb', line 70

attr_accessor :on_approve, :on_reject, :on_submit

#on_submitObject

Called with the new Action when it is submitted — BEFORE the auto-approval drain runs, so subscribers can register the pending call before it is applied. The session wires this to register the pending tool call, closing the race where an approval lands while the executor is still in flight.



70
# File 'lib/ask/agent/approval_queue.rb', line 70

attr_accessor :on_approve, :on_reject, :on_submit

Instance Method Details

#[](id) ⇒ Action?

Look up an action by id.

Parameters:

  • id (Integer)

Returns:



144
145
146
# File 'lib/ask/agent/approval_queue.rb', line 144

def [](id)
  @mutex.synchronize { @actions[id] }
end

#any_pending?Boolean

Returns true while at least one action awaits a decision.

Returns:

  • (Boolean)

    true while at least one action awaits a decision



136
137
138
# File 'lib/ask/agent/approval_queue.rb', line 136

def any_pending?
  pending_actions.any?
end

#approve(*ids) ⇒ Array<Action>

Approve specific actions (by id), applying them in id order.

Parameters:

  • ids (Array<Integer>)

Returns:

  • (Array<Action>)

    the actions that were approved



152
153
154
155
156
157
158
# File 'lib/ask/agent/approval_queue.rb', line 152

def approve(*ids)
  actions = ids.flatten.filter_map { |id| @mutex.synchronize { @actions[id] } }
  actions.select! { |a| a.status == :pending }
  actions.sort_by!(&:id)
  actions.each { |a| apply(a) }
  actions
end

#approve_allArray<Action>

Approve all currently pending actions, in id order.

Returns:



175
176
177
# File 'lib/ask/agent/approval_queue.rb', line 175

def approve_all
  approve(*pending_actions.map(&:id))
end

#pending?(id) ⇒ Boolean

Returns whether the action is still awaiting a decision.

Parameters:

  • id (Integer)

Returns:

  • (Boolean)

    whether the action is still awaiting a decision



128
129
130
131
132
133
# File 'lib/ask/agent/approval_queue.rb', line 128

def pending?(id)
  @mutex.synchronize do
    a = @actions[id]
    a && a.status == :pending
  end
end

#pending_actionsArray<Action>

All actions still awaiting a decision, in id order.

Returns:



120
121
122
123
124
# File 'lib/ask/agent/approval_queue.rb', line 120

def pending_actions
  @mutex.synchronize do
    @actions.values.select { |a| a.status == :pending }.sort_by(&:id)
  end
end

#reject(*ids) ⇒ Array<Action>

Reject specific actions (by id).

Parameters:

  • ids (Array<Integer>)

Returns:

  • (Array<Action>)

    the actions that were rejected



164
165
166
167
168
169
170
# File 'lib/ask/agent/approval_queue.rb', line 164

def reject(*ids)
  actions = ids.flatten.filter_map { |id| @mutex.synchronize { @actions[id] } }
  actions.select! { |a| a.status == :pending }
  actions.sort_by!(&:id)
  actions.each { |a| reject_action(a) }
  actions
end

#reject_allArray<Action>

Reject all currently pending actions, in id order.

Returns:



182
183
184
# File 'lib/ask/agent/approval_queue.rb', line 182

def reject_all
  reject(*pending_actions.map(&:id))
end

#submit(tool_call_id:, tool_name:, args: {}, auto_approvable: false, message: nil) ⇒ Integer

Queue a tool action for approval.

Parameters:

  • tool_call_id (String)

    original tool call id from the LLM

  • tool_name (String)

    tool name

  • args (Hash) (defaults to: {})

    arguments for the tool

  • auto_approvable (Boolean) (defaults to: false)

    whether the tool permits auto-approval

  • message (String, nil) (defaults to: nil)

    human-readable description

Returns:

  • (Integer)

    the action id



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/ask/agent/approval_queue.rb', line 91

def submit(tool_call_id:, tool_name:, args: {}, auto_approvable: false, message: nil)
  action = @mutex.synchronize do
    action = Action.new(
      id: @next_id,
      tool_call_id: tool_call_id,
      tool_name: tool_name,
      args: args || {},
      auto_approvable: auto_approvable,
      status: :pending,
      submitted_at: Time.now,
      message: message
    )
    @next_id += 1
    @actions[action.id] = action
    action
  end

  # Notify BEFORE the drain: an auto-approvable action is applied
  # (and possibly completed) inside drain, and listeners need to
  # observe the submission first.
  @on_submit&.call(action)

  drain
  action.id
end