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 Method Summary collapse

Constructor Details

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

Create a new approval queue.

Parameters:

  • on_approve (Proc, nil) (defaults to: nil)

    called with an Action when it is approved and applied. The session wires this to execute the real tool call.

  • on_reject (Proc, nil) (defaults to: nil)

    called with an Action when it is rejected. The session wires this to notify the conversation.

  • auto_approve (Hash{String => Boolean}, nil) (defaults to: nil)

    user-enabled auto-approval rules keyed by tool name. An action is auto-applied only when its tool is listed here with true AND the action itself is marked auto_approvable.



58
59
60
61
62
63
64
65
66
# File 'lib/ask/agent/approval_queue.rb', line 58

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

Instance Method Details

#[](id) ⇒ Action?

Look up an action by id.

Parameters:

  • id (Integer)

Returns:



124
125
126
# File 'lib/ask/agent/approval_queue.rb', line 124

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



116
117
118
# File 'lib/ask/agent/approval_queue.rb', line 116

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



132
133
134
135
136
137
138
# File 'lib/ask/agent/approval_queue.rb', line 132

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:



155
156
157
# File 'lib/ask/agent/approval_queue.rb', line 155

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



108
109
110
111
112
113
# File 'lib/ask/agent/approval_queue.rb', line 108

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:



100
101
102
103
104
# File 'lib/ask/agent/approval_queue.rb', line 100

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



144
145
146
147
148
149
150
# File 'lib/ask/agent/approval_queue.rb', line 144

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:



162
163
164
# File 'lib/ask/agent/approval_queue.rb', line 162

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



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/ask/agent/approval_queue.rb', line 76

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

  drain
  action.id
end