Class: Ask::Agent::ApprovalQueue
- Inherits:
-
Object
- Object
- Ask::Agent::ApprovalQueue
- 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
-
#on_approve ⇒ Object
Called with an Action when it is approved and applied.
-
#on_reject ⇒ Object
Called with an Action when it is rejected.
-
#on_submit ⇒ Object
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.
Instance Method Summary collapse
-
#[](id) ⇒ Action?
Look up an action by id.
-
#any_pending? ⇒ Boolean
True while at least one action awaits a decision.
-
#approve(*ids) ⇒ Array<Action>
Approve specific actions (by id), applying them in id order.
-
#approve_all ⇒ Array<Action>
Approve all currently pending actions, in id order.
-
#initialize(on_approve: nil, on_reject: nil, auto_approve: nil, on_submit: nil) ⇒ ApprovalQueue
constructor
A new instance of ApprovalQueue.
-
#pending?(id) ⇒ Boolean
Whether the action is still awaiting a decision.
-
#pending_actions ⇒ Array<Action>
All actions still awaiting a decision, in id order.
-
#reject(*ids) ⇒ Array<Action>
Reject specific actions (by id).
-
#reject_all ⇒ Array<Action>
Reject all currently pending actions, in id order.
-
#submit(tool_call_id:, tool_name:, args: {}, auto_approvable: false, message: nil) ⇒ Integer
Queue a tool action for approval.
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_approve ⇒ Object
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_reject ⇒ Object
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_submit ⇒ Object
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.
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.
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.
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_all ⇒ Array<Action>
Approve all currently pending actions, in id order.
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.
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_actions ⇒ Array<Action>
All actions still awaiting a decision, in id order.
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).
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_all ⇒ Array<Action>
Reject all currently pending actions, in id order.
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.
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: ) @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 |