ask-agent 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d32526c4ac8d8daa60b4f4b63e5787617c2070154cb854f6fff61222f880185d
4
- data.tar.gz: cd528117e7577374e8c41a423ec72c9fd1560980017f968d215f674b1a916e3c
3
+ metadata.gz: f67174ee80499aad6a21ff621f02c341daf0e992392996d577218aec2d8987ab
4
+ data.tar.gz: 25b21cc738b1e106730330b3ec28b118545f96a296898e4e9b3e337b4a2bf9a0
5
5
  SHA512:
6
- metadata.gz: 4c210ccc2a08d0ed0d2bb60a560aac06635633c816bfd4d5b9ae21b2139af3ae5729b37fb46b795e48b0bbc2975c33e855cea695b507ad306ce04187330605a7
7
- data.tar.gz: fb3954229781a8e34a517a0e8d81369e55579ae4c18f041ffb4c99b3295c628bb0e5b49d9f08d080b4cd6d556614ddb5b5cac685b9c7d5813bfb0371281d1e54
6
+ metadata.gz: 74fdca8fecf20a3707bf162a319a0b4fa910f3718745fa8276235200e66e98ddd9a005cd7d4ce4bb5fc7d74321a1a86983934cc17041e6a20e58bce48156ce75
7
+ data.tar.gz: 78250a88b07fe2265e276ab5f156dd1c712315585eb906d81a198825c63450cf470ec48702bfbab2ac4b4d757523ff53a86d28646293e21809891626af4616c7
data/CHANGELOG.md CHANGED
@@ -1,3 +1,56 @@
1
+ ## [0.27.0] — 2026-08-06
2
+
3
+ ### Added
4
+
5
+ - **Human-in-the-loop tool approval — `Ask::Agent::ApprovalQueue`.** Tools
6
+ declared `approval_required` (ask-tools) are queued instead of executed:
7
+ the agent receives a pending result and continues, and the tool runs only
8
+ after a human approves it. Built on the async-tools seam
9
+ (`Ask::Result.pending` → `register_pending_tool` → `complete_pending_tool`).
10
+
11
+ ```ruby
12
+ session = Ask::Agent::Session.new(
13
+ model: "gpt-4o",
14
+ tools: [SendEmail], # SendEmail.approval_required true
15
+ approval: { auto_approve: {} }
16
+ )
17
+ session.run("Email bob about the launch")
18
+
19
+ # Later, when the user decides:
20
+ session.approval_queue.pending_actions # inspect what's waiting
21
+ session.approval_queue.approve_all # or approve(id) / reject(id)
22
+ ```
23
+
24
+ - `approval: true` enables with defaults; a Hash accepts
25
+ `require_approval:` (tool names / regexps / `:all`) and `auto_approve:`
26
+ (user-enabled rules keyed by tool name). A custom `ApprovalQueue`
27
+ instance is accepted too.
28
+ - **Auto-approval is a dual signal** — a tool marked `auto_approvable`
29
+ AND a user rule enabling it. Nothing is silently applied past a manual
30
+ (non-auto-approvable) gate; eligible actions drain in id order with a
31
+ single-flight guard (no double-apply).
32
+ - **Approving executes the real tool** and feeds the result into the
33
+ conversation; **rejecting** injects a "rejected by the user" message and
34
+ the agent adapts. Failed applies leave the action pending for retry.
35
+ - `Session#approval_queue` returns the queue (nil when approval is off).
36
+
37
+ - **`Ask::Agent::Extensions::ApprovalPolicy`** — the classification hook.
38
+ Queues calls for tools whose class declares `approval_required`, or whose
39
+ name matches rule-based lists, or (with `require_approval: :all`) every
40
+ call. Usable standalone as a `before_tool` hook.
41
+
42
+ ### Changed
43
+
44
+ - `ToolExecutor` before-tool hooks now support a `:pending` action alongside
45
+ `:block` and `:short_circuit`, returning a pending tool result.
46
+
47
+ ## [0.26.1] - 2026-08-05
48
+
49
+ ### Added
50
+
51
+ - **`Session#pending_tools?`** — true while any async tool is still running;
52
+ the voice seam uses it to tell the worker a completion is coming.
53
+
1
54
  ## [0.26.0] - 2026-08-05
2
55
 
3
56
  ### Added
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Agent
5
+ # A queue of tool actions awaiting human approval.
6
+ #
7
+ # When an agent calls a tool that requires approval, the action is queued
8
+ # instead of executed — the agent gets a pending result and continues.
9
+ # The user approves or rejects actions later, in bulk or one-by-one.
10
+ #
11
+ # queue = Ask::Agent::ApprovalQueue.new
12
+ # id = queue.submit(tool_call_id: "call_1", tool_name: "send_email",
13
+ # args: { "to" => "x@y.com" }, auto_approvable: false)
14
+ # queue.pending_actions # => [{id: 1, tool_name: "send_email", ...}]
15
+ # queue.approve(1) # applies action 1 via the on_approve callback
16
+ # queue.reject(1)
17
+ #
18
+ # Auto-approval is a dual signal: the action must be marked
19
+ # +auto_approvable+ (by the tool) AND the queue must have a matching
20
+ # auto-approval rule enabled by the user. Otherwise the action queues for
21
+ # human review.
22
+ #
23
+ # Actions are applied in id order and the drainer never applies past a
24
+ # non-auto-approvable (manual) gate — nothing is silently approved.
25
+ class ApprovalQueue
26
+ # One queued action.
27
+ #
28
+ # @!attribute [r] id
29
+ # @return [Integer] sequential id assigned by the queue
30
+ # @!attribute [r] tool_call_id
31
+ # @return [String] the original tool call id from the LLM
32
+ # @!attribute [r] tool_name
33
+ # @return [String] tool name
34
+ # @!attribute [r] args
35
+ # @return [Hash] arguments to pass to the tool when applied
36
+ # @!attribute [r] auto_approvable
37
+ # @return [Boolean] per-action verdict (tool's declaration)
38
+ # @!attribute [r] status
39
+ # @return [Symbol] :pending, :applying, :approved, :rejected
40
+ # @!attribute [r] submitted_at
41
+ # @return [Time]
42
+ # @!attribute [r] message
43
+ # @return [String, nil] human-readable description of the action
44
+ Action = Data.define(:id, :tool_call_id, :tool_name, :args,
45
+ :auto_approvable, :status, :submitted_at, :message)
46
+
47
+ # Create a new approval queue.
48
+ #
49
+ # @param on_approve [Proc, nil] called with an {Action} when it is
50
+ # approved and applied. The session wires this to execute the real
51
+ # tool call.
52
+ # @param on_reject [Proc, nil] called with an {Action} when it is
53
+ # rejected. The session wires this to notify the conversation.
54
+ # @param auto_approve [Hash{String => Boolean}, nil] user-enabled
55
+ # auto-approval rules keyed by tool name. An action is auto-applied
56
+ # only when its tool is listed here with +true+ AND the action itself
57
+ # is marked auto_approvable.
58
+ def initialize(on_approve: nil, on_reject: nil, auto_approve: nil)
59
+ @on_approve = on_approve
60
+ @on_reject = on_reject
61
+ @auto_approve = auto_approve || {}
62
+ @actions = {}
63
+ @next_id = 1
64
+ @mutex = Mutex.new
65
+ @draining = false
66
+ end
67
+
68
+ # Queue a tool action for approval.
69
+ #
70
+ # @param tool_call_id [String] original tool call id from the LLM
71
+ # @param tool_name [String] tool name
72
+ # @param args [Hash] arguments for the tool
73
+ # @param auto_approvable [Boolean] whether the tool permits auto-approval
74
+ # @param message [String, nil] human-readable description
75
+ # @return [Integer] the action id
76
+ def submit(tool_call_id:, tool_name:, args: {}, auto_approvable: false, message: nil)
77
+ action = @mutex.synchronize do
78
+ action = Action.new(
79
+ id: @next_id,
80
+ tool_call_id: tool_call_id,
81
+ tool_name: tool_name,
82
+ args: args || {},
83
+ auto_approvable: auto_approvable,
84
+ status: :pending,
85
+ submitted_at: Time.now,
86
+ message: message
87
+ )
88
+ @next_id += 1
89
+ @actions[action.id] = action
90
+ action
91
+ end
92
+
93
+ drain
94
+ action.id
95
+ end
96
+
97
+ # All actions still awaiting a decision, in id order.
98
+ #
99
+ # @return [Array<Action>]
100
+ def pending_actions
101
+ @mutex.synchronize do
102
+ @actions.values.select { |a| a.status == :pending }.sort_by(&:id)
103
+ end
104
+ end
105
+
106
+ # @param id [Integer]
107
+ # @return [Boolean] whether the action is still awaiting a decision
108
+ def pending?(id)
109
+ @mutex.synchronize do
110
+ a = @actions[id]
111
+ a && a.status == :pending
112
+ end
113
+ end
114
+
115
+ # @return [Boolean] true while at least one action awaits a decision
116
+ def any_pending?
117
+ pending_actions.any?
118
+ end
119
+
120
+ # Look up an action by id.
121
+ #
122
+ # @param id [Integer]
123
+ # @return [Action, nil]
124
+ def [](id)
125
+ @mutex.synchronize { @actions[id] }
126
+ end
127
+
128
+ # Approve specific actions (by id), applying them in id order.
129
+ #
130
+ # @param ids [Array<Integer>]
131
+ # @return [Array<Action>] the actions that were approved
132
+ def approve(*ids)
133
+ actions = ids.flatten.filter_map { |id| @mutex.synchronize { @actions[id] } }
134
+ actions.select! { |a| a.status == :pending }
135
+ actions.sort_by!(&:id)
136
+ actions.each { |a| apply(a) }
137
+ actions
138
+ end
139
+
140
+ # Reject specific actions (by id).
141
+ #
142
+ # @param ids [Array<Integer>]
143
+ # @return [Array<Action>] the actions that were rejected
144
+ def reject(*ids)
145
+ actions = ids.flatten.filter_map { |id| @mutex.synchronize { @actions[id] } }
146
+ actions.select! { |a| a.status == :pending }
147
+ actions.sort_by!(&:id)
148
+ actions.each { |a| reject_action(a) }
149
+ actions
150
+ end
151
+
152
+ # Approve all currently pending actions, in id order.
153
+ #
154
+ # @return [Array<Action>]
155
+ def approve_all
156
+ approve(*pending_actions.map(&:id))
157
+ end
158
+
159
+ # Reject all currently pending actions, in id order.
160
+ #
161
+ # @return [Array<Action>]
162
+ def reject_all
163
+ reject(*pending_actions.map(&:id))
164
+ end
165
+
166
+ private
167
+
168
+ # Apply eligible pending actions in id order, stopping at the first
169
+ # action that is NOT auto-eligible (a manual gate) — nothing is
170
+ # silently applied past a human review point. Single-flight guard so
171
+ # concurrent drains cannot double-apply.
172
+ def drain
173
+ return if @mutex.synchronize { @draining }
174
+ @mutex.synchronize { @draining = true }
175
+
176
+ begin
177
+ loop do
178
+ action = @mutex.synchronize do
179
+ @actions.values.select { |a| a.status == :pending }
180
+ .sort_by(&:id)
181
+ .first
182
+ end
183
+ break unless action
184
+
185
+ if auto_approvable?(action)
186
+ apply(action)
187
+ else
188
+ break # manual gate — stop, never skip ahead
189
+ end
190
+ end
191
+ ensure
192
+ @mutex.synchronize { @draining = false }
193
+ end
194
+ end
195
+
196
+ def auto_approvable?(action)
197
+ action.auto_approvable && @auto_approve[action.tool_name] == true
198
+ end
199
+
200
+ def apply(action)
201
+ claimed = @mutex.synchronize do
202
+ return false unless action.status == :pending
203
+ @actions[action.id] = action.with(status: :applying)
204
+ end
205
+ @on_approve&.call(claimed)
206
+ @mutex.synchronize do
207
+ @actions[action.id] = claimed.with(status: :approved)
208
+ end
209
+ true
210
+ rescue StandardError
211
+ # Failed apply: leave the action pending so the user can retry or
212
+ # reject it. Re-raise so the caller (approve/drain) surfaces it.
213
+ @mutex.synchronize do
214
+ @actions[action.id] = action.with(status: :pending) if @actions[action.id]&.status == :applying
215
+ end
216
+ raise
217
+ end
218
+
219
+ def reject_action(action)
220
+ return false unless @mutex.synchronize { action.status == :pending }
221
+ @on_reject&.call(action)
222
+ @mutex.synchronize do
223
+ @actions[action.id] = action.with(status: :rejected)
224
+ end
225
+ true
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Agent
5
+ module Extensions
6
+ # Approval policy hook: classifies tool calls as approval-required and
7
+ # routes them into an {Ask::Agent::ApprovalQueue}.
8
+ #
9
+ # Wire it as a +before_tool+ hook on a Session (or enable it with
10
+ # `Session.new(approval: true)`), and any tool whose class declares
11
+ # `approval_required true` — or whose name matches the policy's
12
+ # rule-based lists — is queued for human approval instead of executed.
13
+ # The agent receives a pending result and continues; the tool runs only
14
+ # after a human approves it.
15
+ #
16
+ # @example
17
+ # queue = Ask::Agent::ApprovalQueue.new
18
+ # policy = Ask::Agent::Extensions::ApprovalPolicy.new(queue: queue)
19
+ # session = Ask::Agent::Session.new(
20
+ # model: "gpt-4o",
21
+ # tools: [SendEmail],
22
+ # hooks: { before_tool: [policy.method(:before_tool_call)] }
23
+ # )
24
+ #
25
+ # # Later, when the user decides:
26
+ # session.approval_queue.approve_all
27
+ class ApprovalPolicy
28
+ # @param queue [Ask::Agent::ApprovalQueue] the queue actions go into.
29
+ # The queue owns the user-enabled auto-approval rules; this policy
30
+ # only reports each tool's own declaration.
31
+ # @param require_approval [Array<String, Regexp>, :all, nil] extra
32
+ # rule-based classification on top of the tool's own declaration.
33
+ # Strings match tool names exactly, Regexps match against the name,
34
+ # and :all requires approval for every tool call.
35
+ # @param tools [Array<Object>, nil] the session's resolved tool
36
+ # instances, used to read class-level declarations
37
+ # (`approval_required`, `auto_approvable`). When nil, only the
38
+ # rule-based lists classify calls.
39
+ def initialize(queue:, require_approval: nil, tools: nil)
40
+ @queue = queue
41
+ @require_approval = require_approval
42
+ @tools = Array(tools)
43
+ end
44
+
45
+ # Hook entry point — matches the +before_tool+ hook signature.
46
+ #
47
+ # @param tool_call [Ask::Agent::ToolCallInfo]
48
+ # @param _context [Hash]
49
+ # @return [Hash] {action: :proceed} to run, or
50
+ # {action: :pending, action_id:, reason:} to queue for approval
51
+ def before_tool_call(tool_call, _context)
52
+ return { action: :proceed } unless approval_required?(tool_call.name)
53
+
54
+ auto_approvable = tool_auto_approvable?(tool_call.name)
55
+ action_id = @queue.submit(
56
+ tool_call_id: tool_call.id,
57
+ tool_name: tool_call.name,
58
+ args: tool_call.arguments,
59
+ auto_approvable: auto_approvable,
60
+ message: "Calling \"#{tool_call.name}\" requires approval"
61
+ )
62
+
63
+ { action: :pending, action_id: action_id, reason: "Tool '#{tool_call.name}' requires approval" }
64
+ end
65
+
66
+ private
67
+
68
+ def approval_required?(tool_name)
69
+ return true if @require_approval == :all
70
+
71
+ rule_matches?(@require_approval, tool_name) || tool_declares_approval?(tool_name)
72
+ end
73
+
74
+ def rule_matches?(rules, tool_name)
75
+ Array(rules).any? do |rule|
76
+ rule == tool_name || (rule.is_a?(Regexp) && rule.match?(tool_name))
77
+ end
78
+ end
79
+
80
+ def tool_declares_approval?(tool_name)
81
+ tool = tool_for(tool_name)
82
+ tool&.respond_to?(:approval_required?) && tool.approval_required?
83
+ end
84
+
85
+ def tool_auto_approvable?(tool_name)
86
+ tool = tool_for(tool_name)
87
+ tool&.respond_to?(:auto_approvable?) && tool.auto_approvable?
88
+ end
89
+
90
+ def tool_for(tool_name)
91
+ @tools.find { |t| t.respond_to?(:name) && t.name == tool_name }
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -22,7 +22,7 @@ module Ask
22
22
  id: nil, system_prompt: nil, parallel_tools: true,
23
23
  reflector: nil, telemetry: true, meta_agent: nil,
24
24
  agent_dir: nil, evaluator: nil, audit_log: nil,
25
- skills_disclosure: true, **chat_options)
25
+ skills_disclosure: true, approval: nil, **chat_options)
26
26
  @id = id || SecureRandom.uuid
27
27
  @agent_dir = agent_dir
28
28
  @max_turns = max_turns
@@ -53,6 +53,7 @@ module Ask
53
53
  @compactor = compactor ? build_compactor(compactor) : nil
54
54
  @hooks = Hooks.new(hooks)
55
55
  @audit_log = build_audit_log(audit_log)
56
+ @approval_queue = build_approval(approval)
56
57
 
57
58
  @system_context = build_system_context(system_prompt)
58
59
  apply_system_context
@@ -88,6 +89,13 @@ module Ask
88
89
  end
89
90
  end
90
91
 
92
+ # The approval queue backing this session, or nil when the session was
93
+ # created without approval support. Use it to inspect pending actions
94
+ # and approve/reject them.
95
+ #
96
+ # @return [Ask::Agent::ApprovalQueue, nil]
97
+ attr_reader :approval_queue
98
+
91
99
  def run(message, tools: nil, reset: true)
92
100
  raise "Session deleted" if @deleted
93
101
  raise "Session already running" if @running
@@ -336,6 +344,11 @@ module Ask
336
344
  nil
337
345
  end
338
346
 
347
+ # @return [Boolean] true while at least one async tool is running
348
+ def pending_tools?
349
+ @pending_mutex.synchronize { !@pending_tools.empty? }
350
+ end
351
+
339
352
  # Completes a pending (async) tool call from a background thread.
340
353
  #
341
354
  # Adds the tool result to the conversation and, when the session is
@@ -411,6 +424,48 @@ module Ask
411
424
  Ask::Agent::Extensions::AuditLog.new(self, adapter: config)
412
425
  end
413
426
 
427
+ # Build the approval queue + policy when approval is enabled.
428
+ #
429
+ # `approval` accepts:
430
+ # - true → queue with defaults
431
+ # - a Hash → { require_approval:, auto_approve: } for the policy
432
+ # - an ApprovalQueue → uses it, with policy options from
433
+ # approval[:policy] if given
434
+ #
435
+ # When enabled, an ApprovalPolicy hook is prepended to the session's
436
+ # before_tool hooks so approval-required tools queue instead of running.
437
+ def build_approval(approval)
438
+ return nil unless approval
439
+
440
+ policy_opts = approval.is_a?(Hash) ? approval : {}
441
+
442
+ queue = if approval.is_a?(Ask::Agent::ApprovalQueue)
443
+ approval
444
+ elsif policy_opts[:queue].is_a?(Ask::Agent::ApprovalQueue)
445
+ policy_opts[:queue]
446
+ else
447
+ Ask::Agent::ApprovalQueue.new(
448
+ auto_approve: policy_opts[:auto_approve],
449
+ on_approve: ->(action) { apply_approved_action(action) },
450
+ on_reject: ->(action) { reject_pending_action(action) }
451
+ )
452
+ end
453
+
454
+ policy = Ask::Agent::Extensions::ApprovalPolicy.new(
455
+ queue: queue,
456
+ require_approval: policy_opts[:require_approval],
457
+ tools: @tools
458
+ )
459
+
460
+ # Prepend the approval gate so it runs before user hooks
461
+ @hooks = Hooks.new(
462
+ before_tool: [policy.method(:before_tool_call)] + Array(@hooks.instance_variable_get(:@before_tool)),
463
+ after_tool: @hooks.instance_variable_get(:@after_tool)
464
+ )
465
+
466
+ queue
467
+ end
468
+
414
469
  def build_chat(model, system_prompt, tools, **chat_options)
415
470
  if model.respond_to?(:ask)
416
471
  model
@@ -421,6 +476,48 @@ module Ask
421
476
  end
422
477
  end
423
478
 
479
+ # Execute an approved action's tool call and complete the pending tool,
480
+ # so the follow-up turn voices the outcome.
481
+ def apply_approved_action(action)
482
+ tool = @tools.find { |t| t.name == action.tool_name }
483
+ if tool
484
+ result = tool.call(action.args)
485
+ status = result.respond_to?(:ok?) ? (result.ok? ? "success" : "error") : "success"
486
+ complete_pending_tool(
487
+ tool_call_id: action.tool_call_id,
488
+ result: {
489
+ tool_name: action.tool_name,
490
+ message: result.to_s,
491
+ status: status,
492
+ is_error: status == "error"
493
+ }
494
+ )
495
+ else
496
+ complete_pending_tool(
497
+ tool_call_id: action.tool_call_id,
498
+ result: {
499
+ tool_name: action.tool_name,
500
+ message: "Tool not found: #{action.tool_name}",
501
+ status: "error",
502
+ is_error: true
503
+ }
504
+ )
505
+ end
506
+ end
507
+
508
+ # Notify the conversation that an action was rejected by the user.
509
+ def reject_pending_action(action)
510
+ complete_pending_tool(
511
+ tool_call_id: action.tool_call_id,
512
+ result: {
513
+ tool_name: action.tool_name,
514
+ message: "Action '#{action.tool_name}' was rejected by the user.",
515
+ status: "rejected",
516
+ is_error: false
517
+ }
518
+ )
519
+ end
520
+
424
521
  def resolve_tools(tools)
425
522
  resolved = tools.map do |tool|
426
523
  tool.is_a?(Class) ? tool.new : tool
@@ -115,6 +115,17 @@ module Ask
115
115
  return { tool_name: tool_call.name, message: hook_result[:reason], status: "blocked", is_error: true }
116
116
  when :short_circuit
117
117
  return { tool_name: tool_call.name, **hook_result[:result], status: "short_circuited" }
118
+ when :pending
119
+ # Queued for human approval — the loop hands the turn back with an
120
+ # interim reply; the action runs later via ApprovalQueue#approve.
121
+ return {
122
+ tool_name: tool_call.name,
123
+ message: hook_result[:reason] || "Pending approval",
124
+ status: "pending",
125
+ is_error: false,
126
+ tool_call_id: tool_call.id,
127
+ action_id: hook_result[:action_id]
128
+ }
118
129
  end
119
130
 
120
131
  return aborted_result(tool_call) if abort_controller&.aborted?
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Agent
5
- VERSION = "0.26.0"
5
+ VERSION = "0.27.0"
6
6
  end
7
7
  end
data/lib/ask/agent.rb CHANGED
@@ -25,6 +25,7 @@ module Ask
25
25
  autoload :Permissions, "ask/agent/extensions/permissions"
26
26
  autoload :RateLimiter, "ask/agent/extensions/rate_limiter"
27
27
  autoload :AuditLog, "ask/agent/extensions/audit_log"
28
+ autoload :ApprovalPolicy, "ask/agent/extensions/approval_policy"
28
29
  end
29
30
 
30
31
  module Middleware
@@ -257,6 +258,7 @@ require_relative "agent/evaluator"
257
258
  require_relative "agent/tool_executor"
258
259
  require_relative "agent/compactor"
259
260
  require_relative "agent/hooks"
261
+ require_relative "agent/approval_queue"
260
262
  require_relative "agent/configuration"
261
263
  require_relative "agent/meta_agent"
262
264
  require_relative "agent/persistence/base"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.26.0
4
+ version: 0.27.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -164,6 +164,7 @@ files:
164
164
  - exe/askr
165
165
  - lib/ask-agent.rb
166
166
  - lib/ask/agent.rb
167
+ - lib/ask/agent/approval_queue.rb
167
168
  - lib/ask/agent/chat.rb
168
169
  - lib/ask/agent/cli.rb
169
170
  - lib/ask/agent/compactor.rb
@@ -173,6 +174,7 @@ files:
173
174
  - lib/ask/agent/definition.rb
174
175
  - lib/ask/agent/evaluator.rb
175
176
  - lib/ask/agent/events.rb
177
+ - lib/ask/agent/extensions/approval_policy.rb
176
178
  - lib/ask/agent/extensions/audit_log.rb
177
179
  - lib/ask/agent/extensions/audit_log/active_record_writer.rb
178
180
  - lib/ask/agent/extensions/permissions.rb