ask-decisions 0.2.4 → 0.2.5
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 +17 -0
- data/README.md +25 -0
- data/lib/ask/decisions/compactor.rb +57 -15
- data/lib/ask/decisions/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: 9969a46203cf5653cacf020b9e5399945d137c846b88fcc52b1b6d91828de484
|
|
4
|
+
data.tar.gz: e172bd808e1a61019d9dd65dc2073f41776d46ee64e0233fa5333017a2c6945b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2e9547a817b4b6e6dc2d21b00c4ca0118fb087505781937e9ad1e591eb4737b60c3eb156a32f76a346120bd0344ba376678522dd30151739bb25153adb0d7ff6
|
|
7
|
+
data.tar.gz: bea3578951f0d0df5473f8a65412021cc040c044f9244ba484d1e2dc91577065aedf87b457176b8397f933150ed1cb1e760a187ca545af2f6c93d8db44cf48f2
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
+
## [0.2.5] - 2026-09-18
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Dropping one of several tool calls no longer orphans its siblings.**
|
|
12
|
+
When an assistant message carried multiple tool calls and Jev dropped
|
|
13
|
+
one, the Compactor removed the whole message — including the calls Jev
|
|
14
|
+
said to keep, stranding their results without a call. A drop now removes
|
|
15
|
+
the dropped call from its message; the message survives when it still
|
|
16
|
+
has text or surviving calls, and disappears only when the drop empties it.
|
|
17
|
+
- **A drop touching a pinned message keeps the compactor's promises.** A
|
|
18
|
+
result is never left without its call, and a pinned message is never
|
|
19
|
+
removed — a drop against either now downgrades to keeping the pair
|
|
20
|
+
(truncating the result when only the call is pinned), and the stats say
|
|
21
|
+
what actually happened: fully pinned pairs report `pinned` instead of
|
|
22
|
+
counting as evaluated work.
|
|
23
|
+
|
|
7
24
|
## [0.2.4] - 2026-09-18
|
|
8
25
|
|
|
9
26
|
### Fixed
|
data/README.md
CHANGED
|
@@ -213,6 +213,31 @@ policy.evaluate(tool: "bash", confidence: 0.6).action # => :act
|
|
|
213
213
|
policy.evaluate(tool: "rm", confidence: 0.8).action # => :review
|
|
214
214
|
```
|
|
215
215
|
|
|
216
|
+
## Compactor
|
|
217
|
+
|
|
218
|
+
Decision-based conversation compaction. Summaries are lossy — a file path,
|
|
219
|
+
an exact error, a one-time constraint can vanish. The Compactor never
|
|
220
|
+
rewrites anything: Jev scores every tool call and result, and the irrelevant
|
|
221
|
+
ones are dropped or truncated. Everything kept stays verbatim.
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
compactor = Ask::Decisions::Compactor.new(provider, preserve_recent: 6)
|
|
225
|
+
result = compactor.compact(session.messages)
|
|
226
|
+
|
|
227
|
+
result.messages # => pruned conversation
|
|
228
|
+
result.stats # => { kept: 12, dropped: 5, truncated: 3, reduction_ratio: 0.29, ... }
|
|
229
|
+
result.reduction_ratio # => 0.29
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Three tiers per tool call: result still needed → keep both verbatim; call
|
|
233
|
+
matters, result doesn't → keep the call, truncate the result to a short
|
|
234
|
+
head; neither matters → remove both. The first message and the most recent
|
|
235
|
+
`preserve_recent:` messages are pinned — never touched, never scored.
|
|
236
|
+
|
|
237
|
+
Fail open: a failed decision call means keep the transcript and fall back to
|
|
238
|
+
summarization. A compaction that doesn't happen this turn happens next turn;
|
|
239
|
+
a lost constraint is gone for good.
|
|
240
|
+
|
|
216
241
|
## Calibration
|
|
217
242
|
|
|
218
243
|
Measure whether confidence is trustworthy on YOUR decisions:
|
|
@@ -81,6 +81,7 @@ module Ask
|
|
|
81
81
|
questions = build_questions(pairs, pinned)
|
|
82
82
|
batch = batch_and_execute(state, questions)
|
|
83
83
|
decisions = extract_decisions(batch, pairs)
|
|
84
|
+
mark_pinned_pairs(decisions, pairs, pinned)
|
|
84
85
|
pruned = apply_decisions(messages, decisions, pinned)
|
|
85
86
|
stats = compute_stats(messages, pruned, decisions)
|
|
86
87
|
|
|
@@ -145,8 +146,8 @@ module Ask
|
|
|
145
146
|
end
|
|
146
147
|
|
|
147
148
|
# First message and the most recent +preserve_recent+ messages are pinned.
|
|
148
|
-
# Pinned messages are never removed,
|
|
149
|
-
#
|
|
149
|
+
# Pinned messages are never removed, and a pair living entirely inside
|
|
150
|
+
# them is never scored — Jev does not see pinned content.
|
|
150
151
|
def compute_pinned(messages)
|
|
151
152
|
pinned = Set.new([0])
|
|
152
153
|
start = [messages.size - @preserve_recent, 1].max
|
|
@@ -154,11 +155,6 @@ module Ask
|
|
|
154
155
|
pinned
|
|
155
156
|
end
|
|
156
157
|
|
|
157
|
-
# Whether a message index is safe to remove (not pinned).
|
|
158
|
-
def removable?(idx, pinned)
|
|
159
|
-
!pinned.include?(idx)
|
|
160
|
-
end
|
|
161
|
-
|
|
162
158
|
# ── State ────────────────────────────────────────────────────────
|
|
163
159
|
|
|
164
160
|
# Build the state Jev sees: full conversation with tool results replaced
|
|
@@ -276,6 +272,19 @@ module Ask
|
|
|
276
272
|
|
|
277
273
|
# ── Decisions ────────────────────────────────────────────────────
|
|
278
274
|
|
|
275
|
+
# A pair whose call and result both live in pinned messages was never
|
|
276
|
+
# scored. Record that as its own action so the stats describe Jev's
|
|
277
|
+
# work, not the pinning.
|
|
278
|
+
def mark_pinned_pairs(decisions, pairs, pinned)
|
|
279
|
+
pairs.each do |pair|
|
|
280
|
+
next unless pinned.include?(pair[:call_msg_idx]) && pinned.include?(pair[:result_msg_idx])
|
|
281
|
+
|
|
282
|
+
if (decision = decisions[pair[:call_id]])
|
|
283
|
+
decision[:action] = :pinned
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
279
288
|
def extract_decisions(batch, pairs)
|
|
280
289
|
decisions = {}
|
|
281
290
|
pairs.each do |pair|
|
|
@@ -312,6 +321,9 @@ module Ask
|
|
|
312
321
|
pairs = build_pairs(messages)
|
|
313
322
|
remove_indices = Set.new
|
|
314
323
|
truncate_indices = {}
|
|
324
|
+
# call_id => true when the call survives although its result does not
|
|
325
|
+
# (a :drop whose result is pinned downgrades to keeping the call).
|
|
326
|
+
dropped_results = Set.new
|
|
315
327
|
|
|
316
328
|
pairs.each do |pair|
|
|
317
329
|
decision = decisions[pair[:call_id]]
|
|
@@ -319,13 +331,24 @@ module Ask
|
|
|
319
331
|
|
|
320
332
|
case decision[:action]
|
|
321
333
|
when :drop
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
334
|
+
call_pinned = pinned.include?(pair[:call_msg_idx])
|
|
335
|
+
result_pinned = pinned.include?(pair[:result_msg_idx])
|
|
336
|
+
|
|
337
|
+
if call_pinned || result_pinned
|
|
338
|
+
# A result is never left without its call, and a pinned message
|
|
339
|
+
# is never removed. A drop touching a pinned message keeps the
|
|
340
|
+
# pair — the call verbatim, the result truncated to its head.
|
|
341
|
+
if result_pinned
|
|
342
|
+
decision[:action] = :keep
|
|
343
|
+
else
|
|
344
|
+
truncate_indices[pair[:result_msg_idx]] = pair[:call]
|
|
345
|
+
decision[:action] = :truncate
|
|
346
|
+
end
|
|
347
|
+
next
|
|
328
348
|
end
|
|
349
|
+
|
|
350
|
+
remove_indices.add(pair[:result_msg_idx])
|
|
351
|
+
dropped_results.add(pair[:call_id])
|
|
329
352
|
when :truncate
|
|
330
353
|
truncate_indices[pair[:result_msg_idx]] = pair[:call]
|
|
331
354
|
end
|
|
@@ -337,11 +360,29 @@ module Ask
|
|
|
337
360
|
if truncate_indices[idx]
|
|
338
361
|
truncate_message(msg, truncate_indices[idx])
|
|
339
362
|
else
|
|
340
|
-
msg
|
|
363
|
+
prune_dropped_calls(msg, dropped_results)
|
|
341
364
|
end
|
|
342
365
|
end
|
|
343
366
|
end
|
|
344
367
|
|
|
368
|
+
# A drop removes the tool result message; the call itself leaves the
|
|
369
|
+
# assistant message that carried it. A message survives when it has text
|
|
370
|
+
# or surviving calls — it is removed only when the drop empties it.
|
|
371
|
+
def prune_dropped_calls(msg, dropped_results)
|
|
372
|
+
return msg unless msg[:tool_calls] && dropped_results.any?
|
|
373
|
+
|
|
374
|
+
surviving = msg[:tool_calls].reject { |tc| dropped_results.include?(tc[:id]) }
|
|
375
|
+
return msg if surviving.size == msg[:tool_calls].size
|
|
376
|
+
|
|
377
|
+
if surviving.empty? && msg[:content].to_s.empty?
|
|
378
|
+
nil
|
|
379
|
+
elsif surviving.empty?
|
|
380
|
+
msg.except(:tool_calls)
|
|
381
|
+
else
|
|
382
|
+
msg.merge(tool_calls: surviving)
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
345
386
|
def truncate_message(msg, call)
|
|
346
387
|
original = msg[:content].to_s
|
|
347
388
|
return msg if original.length <= @truncate_head_chars
|
|
@@ -364,7 +405,8 @@ module Ask
|
|
|
364
405
|
kept: action_counts[:keep] || 0,
|
|
365
406
|
truncated: action_counts[:truncate] || 0,
|
|
366
407
|
dropped: action_counts[:drop] || 0,
|
|
367
|
-
|
|
408
|
+
pinned: action_counts[:pinned] || 0,
|
|
409
|
+
tool_pairs_evaluated: decisions.size - (action_counts[:pinned] || 0),
|
|
368
410
|
chars_before: original_chars,
|
|
369
411
|
chars_after: pruned_chars,
|
|
370
412
|
reduction_ratio: original_chars > 0 ? (1.0 - pruned_chars.to_f / original_chars) : 0.0
|