ask-agent 0.39.0 → 0.40.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 +4 -4
- data/CHANGELOG.md +23 -0
- data/lib/ask/agent/approval_queue.rb +21 -1
- data/lib/ask/agent/session.rb +53 -4
- data/lib/ask/agent/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 969e484bfb85499a953c458a677564c9e0419066dcb931d12f451d0297b1c355
|
|
4
|
+
data.tar.gz: 3f13c4c60d09bfe2918f2bd974cb90c82f06224f492575e52a0e8f8aea1ebac4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ee591ac269d606a3b8c07e1ea8dee3407174351efcf7fe45fd2972318382feac61be8175bd9110d4307fcef67bade241fd991ebb121711dae569bd831c08e60
|
|
7
|
+
data.tar.gz: debc87ce3557c9e40883078958fa2614b1c136ae7bd703512f8faa052b865586ccba4d69aab0eed9cb6e4ebc27537724d4bf95a6d5f0bcbb785b92f91861f676
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
## [0.40.0] — 2026-08-10
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **Approval race: completing a queued tool call now always lands.**
|
|
6
|
+
The approval policy queues a tool call inside the executor (before_tool
|
|
7
|
+
hook), but the loop only registered the pending call *after* the executor
|
|
8
|
+
returned. An approval landing in that window found nothing to complete —
|
|
9
|
+
the call was then registered as a ghost pending entry and the turn never
|
|
10
|
+
settled. Two changes close it:
|
|
11
|
+
- `ApprovalQueue` gains an `on_submit` callback that fires **before** the
|
|
12
|
+
auto-approval drain; `Session#build_approval` (and the plan queue) wire
|
|
13
|
+
it to `register_pending_tool`, so the pending call exists the moment the
|
|
14
|
+
action is queued.
|
|
15
|
+
- `Session#register_pending_tool` skips re-registration for calls that
|
|
16
|
+
were already resolved (`action_id` check) or recently completed
|
|
17
|
+
(bounded `@recently_completed` set), so a late loop registration cannot
|
|
18
|
+
resurrect a completed call.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `ApprovalQueue#on_submit` accessor and `on_submit:` initializer argument.
|
|
23
|
+
|
|
1
24
|
## [0.38.0] — 2026-08-07
|
|
2
25
|
|
|
3
26
|
### Added
|
|
@@ -55,9 +55,24 @@ module Ask
|
|
|
55
55
|
# auto-approval rules keyed by tool name. An action is auto-applied
|
|
56
56
|
# only when its tool is listed here with +true+ AND the action itself
|
|
57
57
|
# is marked auto_approvable.
|
|
58
|
-
|
|
58
|
+
# @!attribute [rw] on_approve
|
|
59
|
+
# Called with an {Action} when it is approved and applied. The
|
|
60
|
+
# session wires this to execute the real tool call; can be replaced
|
|
61
|
+
# after construction (e.g. by queue subclasses that also emit events).
|
|
62
|
+
# @!attribute [rw] on_reject
|
|
63
|
+
# Called with an {Action} when it is rejected.
|
|
64
|
+
# @!attribute [rw] on_submit
|
|
65
|
+
# Called with the new {Action} when it is submitted — BEFORE the
|
|
66
|
+
# auto-approval drain runs, so subscribers can register the pending
|
|
67
|
+
# call before it is applied. The session wires this to register the
|
|
68
|
+
# pending tool call, closing the race where an approval lands while
|
|
69
|
+
# the executor is still in flight.
|
|
70
|
+
attr_accessor :on_approve, :on_reject, :on_submit
|
|
71
|
+
|
|
72
|
+
def initialize(on_approve: nil, on_reject: nil, auto_approve: nil, on_submit: nil)
|
|
59
73
|
@on_approve = on_approve
|
|
60
74
|
@on_reject = on_reject
|
|
75
|
+
@on_submit = on_submit
|
|
61
76
|
@auto_approve = auto_approve || {}
|
|
62
77
|
@actions = {}
|
|
63
78
|
@next_id = 1
|
|
@@ -90,6 +105,11 @@ module Ask
|
|
|
90
105
|
action
|
|
91
106
|
end
|
|
92
107
|
|
|
108
|
+
# Notify BEFORE the drain: an auto-approvable action is applied
|
|
109
|
+
# (and possibly completed) inside drain, and listeners need to
|
|
110
|
+
# observe the submission first.
|
|
111
|
+
@on_submit&.call(action)
|
|
112
|
+
|
|
93
113
|
drain
|
|
94
114
|
action.id
|
|
95
115
|
end
|
data/lib/ask/agent/session.rb
CHANGED
|
@@ -6,6 +6,10 @@ require "time"
|
|
|
6
6
|
module Ask
|
|
7
7
|
module Agent
|
|
8
8
|
class Session
|
|
9
|
+
# Max ids remembered as "recently completed" to guard against late
|
|
10
|
+
# loop registrations resurrecting ghost pending calls.
|
|
11
|
+
RECENTLY_COMPLETED_MAX = 200
|
|
12
|
+
|
|
9
13
|
attr_reader :id, :chat, :tools, :turn_count, :created_at, :messages
|
|
10
14
|
attr_reader :tool_calls_made, :total_input_tokens, :total_output_tokens, :total_cost
|
|
11
15
|
|
|
@@ -39,6 +43,7 @@ module Ask
|
|
|
39
43
|
@deleted = false
|
|
40
44
|
@abort_requested = false
|
|
41
45
|
@pending_tools = {}
|
|
46
|
+
@recently_completed = []
|
|
42
47
|
@pending_mutex = Mutex.new
|
|
43
48
|
@followup_pending = false
|
|
44
49
|
@turn_count = 0
|
|
@@ -63,7 +68,6 @@ module Ask
|
|
|
63
68
|
@todos_enabled = !!todos
|
|
64
69
|
@todo_list = TodoList.new if @todos_enabled
|
|
65
70
|
@todo_list&.subscribe { |entries| emit(Events::TodoUpdated.new(todos: entries)) }
|
|
66
|
-
|
|
67
71
|
# Durable memory (memory_write / memory_search tools). An instance
|
|
68
72
|
# with its own namespace and state adapter; nil disables memory.
|
|
69
73
|
@memory = memory
|
|
@@ -107,7 +111,19 @@ module Ask
|
|
|
107
111
|
end
|
|
108
112
|
@plan_queue = ApprovalQueue.new(
|
|
109
113
|
on_approve: ->(action) { approve_plan(action) },
|
|
110
|
-
on_reject: ->(action) { reject_plan(action) }
|
|
114
|
+
on_reject: ->(action) { reject_plan(action) },
|
|
115
|
+
# Same race closure as the tool approval queue: register the
|
|
116
|
+
# pending call at submit time so plan approvals land even when
|
|
117
|
+
# the executor is still in flight.
|
|
118
|
+
on_submit: ->(action) {
|
|
119
|
+
register_pending_tool(action.tool_call_id, {
|
|
120
|
+
tool_name: action.tool_name,
|
|
121
|
+
message: action.message || "Plan awaiting approval",
|
|
122
|
+
status: "pending",
|
|
123
|
+
tool_call_id: action.tool_call_id,
|
|
124
|
+
action_id: action.id
|
|
125
|
+
})
|
|
126
|
+
}
|
|
111
127
|
) if @plan_mode
|
|
112
128
|
|
|
113
129
|
@tools = resolve_tools(tools)
|
|
@@ -689,10 +705,19 @@ end
|
|
|
689
705
|
# --- Async (pending) tools ---
|
|
690
706
|
|
|
691
707
|
# Registers a pending tool call (called by the loop when a tool
|
|
692
|
-
# returned Ask::Result.pending
|
|
693
|
-
# via #complete_pending_tool.
|
|
708
|
+
# returned Ask::Result.pending, or at approval-queue submit time).
|
|
709
|
+
# The background work completes later via #complete_pending_tool.
|
|
710
|
+
#
|
|
711
|
+
# A call can be resolved (approved/rejected) while the executor is
|
|
712
|
+
# still in flight; when the loop then registers the same call, the
|
|
713
|
+
# registration is skipped so no ghost pending entry is left behind.
|
|
694
714
|
def register_pending_tool(tool_call_id, result)
|
|
695
715
|
@pending_mutex.synchronize do
|
|
716
|
+
return if @recently_completed.include?(tool_call_id)
|
|
717
|
+
if (action_id = result[:action_id]) && @approval_queue
|
|
718
|
+
action = @approval_queue[action_id]
|
|
719
|
+
return if action && action.status != :pending
|
|
720
|
+
end
|
|
696
721
|
@pending_tools[tool_call_id] = result
|
|
697
722
|
end
|
|
698
723
|
emit(Events::ToolPending.new(name: result[:tool_name], id: tool_call_id))
|
|
@@ -718,6 +743,12 @@ end
|
|
|
718
743
|
pending = @pending_tools.delete(tool_call_id)
|
|
719
744
|
return false unless pending
|
|
720
745
|
|
|
746
|
+
# Remember the id briefly so a late loop registration (from a
|
|
747
|
+
# completion that landed while the executor was in flight) cannot
|
|
748
|
+
# resurrect it as a ghost pending call.
|
|
749
|
+
@recently_completed << tool_call_id
|
|
750
|
+
@recently_completed.shift if @recently_completed.size > RECENTLY_COMPLETED_MAX
|
|
751
|
+
|
|
721
752
|
@chat.add_message(
|
|
722
753
|
role: :tool,
|
|
723
754
|
content: result[:message].to_s,
|
|
@@ -806,6 +837,24 @@ end
|
|
|
806
837
|
)
|
|
807
838
|
end
|
|
808
839
|
|
|
840
|
+
# Custom queues (subclasses, event-emitting wrappers) may come
|
|
841
|
+
# without callbacks — wire the session's defaults so approvals
|
|
842
|
+
# actually execute the tool call.
|
|
843
|
+
queue.on_approve ||= ->(action) { apply_approved_action(action) }
|
|
844
|
+
queue.on_reject ||= ->(action) { reject_pending_action(action) }
|
|
845
|
+
# Register the pending tool call the moment the action is queued —
|
|
846
|
+
# before the auto-approval drain — so completions always match even
|
|
847
|
+
# when an approval lands while the executor is still in flight.
|
|
848
|
+
queue.on_submit ||= ->(action) {
|
|
849
|
+
register_pending_tool(action.tool_call_id, {
|
|
850
|
+
tool_name: action.tool_name,
|
|
851
|
+
message: action.message || "Pending approval",
|
|
852
|
+
status: "pending",
|
|
853
|
+
tool_call_id: action.tool_call_id,
|
|
854
|
+
action_id: action.id
|
|
855
|
+
})
|
|
856
|
+
}
|
|
857
|
+
|
|
809
858
|
policy = Ask::Agent::Policies::ApprovalPolicy.new(
|
|
810
859
|
queue: queue,
|
|
811
860
|
require_approval: policy_opts[:require_approval],
|
data/lib/ask/agent/version.rb
CHANGED