mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(agent): give [[keep]] turns a budget of their own and stop the fold ceiling where the send-time output check stops
This commit is contained in:
1 parent
15de9ec334
commit
a2aef0b953
8 files changed
+187
-12
No files matched your search
+10
-3
@@ -377,10 +377,17 @@ digest, so a constraint keeps its protection across repeated compaction.
|
||||
|
||||
A turn past those bounds folds like any other content. Prefix it with `[[keep]]`
|
||||
(keep policy `user_marked`, on by default) to hold it verbatim regardless of
|
||||
size. That drop is never silent: compaction telemetry carries `user_kept` and
|
||||
its own size.
|
||||
|
||||
Marked turns have a budget of their own: together they may spend `window×15%`,
|
||||
newest first, and the oldest fold into the digest past it. A kept turn rides
|
||||
into every later fold, so an unbounded mark would grow what each fold carries
|
||||
until its candidate could not land under the trigger.
|
||||
|
||||
That drop is never silent: compaction telemetry carries `user_kept` and
|
||||
`user_dropped` counts, and a committed checkpoint that had to fold one of your
|
||||
turns emits a warning naming `[[keep]]` — the projection reads as complete
|
||||
either way, so the count is the only thing that distinguishes them.
|
||||
turns, marked or not, emits a warning. The projection reads as complete either
|
||||
way, so the count is the only thing that distinguishes them.
|
||||
|
||||
Two properties bound that loss. Each fold re-derives its digest from the
|
||||
canonical transcript rather than from the previous digest, so digests do not
|
||||
|
||||
@@ -139,13 +139,19 @@ func (a *contextWindow) compactTrigger() int {
|
||||
}
|
||||
|
||||
// hardInputCeiling is a physical input-safety boundary, not another user
|
||||
// compaction threshold. Reply budgets are resolved independently at send time.
|
||||
// compaction threshold. On a provider whose reply shares the window it stops
|
||||
// where the send-time output check stops, so a prompt the check would refuse is
|
||||
// one the fold has already been told it must free.
|
||||
func (a *contextWindow) hardInputCeiling() int {
|
||||
window := a.effectiveContextWindow()
|
||||
if window <= 0 {
|
||||
return 0
|
||||
}
|
||||
return max(1, window-protocolReserveTokens)
|
||||
reserve := protocolReserveTokens
|
||||
if sharesContextWindow(a.svc.prov) && a.configuredOutputBudget(0) > 0 {
|
||||
reserve = max(reserve, outputBudgetReserveFor(window))
|
||||
}
|
||||
return max(1, window-reserve)
|
||||
}
|
||||
|
||||
// recentTailBudget is the content-construction budget for the recent verbatim
|
||||
@@ -356,12 +362,15 @@ func (a *contextWindow) keepIndexes(region []provider.Message, policy KeepPolicy
|
||||
// Retention applies only to messages since the latest digest. A failed tool
|
||||
// result is bounded by its own budget, since last fold's survivors sit past
|
||||
// the digest and would otherwise be kept again.
|
||||
var failures []int
|
||||
var failures, marked []int
|
||||
for i, m := range region {
|
||||
if i >= policyStart && shouldKeepMessage(m, policy) {
|
||||
if m.Role == provider.RoleTool {
|
||||
switch m.Role {
|
||||
case provider.RoleTool:
|
||||
failures = append(failures, i)
|
||||
} else {
|
||||
case provider.RoleUser:
|
||||
marked = append(marked, i)
|
||||
default:
|
||||
keep[i] = true
|
||||
}
|
||||
}
|
||||
@@ -372,7 +381,9 @@ func (a *contextWindow) keepIndexes(region []provider.Message, policy KeepPolicy
|
||||
}
|
||||
}
|
||||
a.keepFailuresWithinBudget(region, keep, failures)
|
||||
markedDropped, markedTokens := a.keepMarkedWithinBudget(region, keep, marked)
|
||||
retention := a.keepUserTurns(region, keep)
|
||||
retention.MarkedDropped, retention.MarkedDroppedTokens = markedDropped, markedTokens
|
||||
pinned := slices.Clone(keep)
|
||||
for i, m := range region {
|
||||
if !pinned[i] {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"reasonix/internal/contract/provider"
|
||||
)
|
||||
|
||||
const keptMarkedWindowFrac = 0.15
|
||||
|
||||
// keptMarkedBudget caps what [[keep]] turns may spend of a checkpoint. A turn the
|
||||
// person marked is held verbatim, and a kept turn rides into the next fold's
|
||||
// region, so without its own bound the retention grows by every marked turn
|
||||
// until the candidate cannot land under the trigger and the fold is refused.
|
||||
func (a *contextWindow) keptMarkedBudget() int {
|
||||
window := a.effectiveContextWindow()
|
||||
if window <= 0 {
|
||||
return keptUserTurnsFloorTokens
|
||||
}
|
||||
return max(1, int(float64(window)*keptMarkedWindowFrac))
|
||||
}
|
||||
|
||||
// keepMarkedWithinBudget retains the newest marked turns that fit the budget and
|
||||
// returns the rest, which fold into the digest and are counted.
|
||||
func (a *contextWindow) keepMarkedWithinBudget(region []provider.Message, keep []bool, marked []int) (dropped, droppedTokens int) {
|
||||
budget := a.keptMarkedBudget()
|
||||
for _, i := range slices.Backward(marked) {
|
||||
cost := fixedTokenEstimate(region[i])
|
||||
if cost > budget {
|
||||
dropped++
|
||||
droppedTokens += cost
|
||||
continue
|
||||
}
|
||||
keep[i] = true
|
||||
budget -= cost
|
||||
}
|
||||
return dropped, droppedTokens
|
||||
}
|
||||
@@ -20,9 +20,10 @@ func TestCompactTriggerIndependentOfOutputBudget(t *testing.T) {
|
||||
if got := a.window().compactTrigger(); got != want {
|
||||
t.Fatalf("trigger after larger output budget = %d, want %d", got, want)
|
||||
}
|
||||
// The reply reserve is a constant of the window, never the budget's size.
|
||||
hard := a.window().hardInputCeiling()
|
||||
if hard != 128_000-protocolReserveTokens {
|
||||
t.Fatalf("hard ceiling = %d, want window-protocolReserve", hard)
|
||||
if want := 128_000 - outputBudgetReserveFor(128_000); hard != want {
|
||||
t.Fatalf("hard ceiling = %d, want the window less the output reserve (%d)", hard, want)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ type userTurnRetention struct {
|
||||
Kept int
|
||||
Dropped int
|
||||
DroppedTokens int
|
||||
// MarkedDropped counts [[keep]] turns the marked-turn budget could not hold.
|
||||
MarkedDropped, MarkedDroppedTokens int
|
||||
}
|
||||
|
||||
// keepUserTurns protects the user's own words from summarizer judgement: a
|
||||
@@ -32,8 +34,8 @@ func (a *contextWindow) keepUserTurns(region []provider.Message, keep []bool) us
|
||||
continue
|
||||
}
|
||||
if keep[i] {
|
||||
// Already held by the keep policy — a [[keep]] marker, which is the
|
||||
// documented way past the size budget below.
|
||||
// Already held by the keep policy: a [[keep]] marker, which has a
|
||||
// budget of its own.
|
||||
ret.Kept++
|
||||
continue
|
||||
}
|
||||
@@ -76,6 +78,13 @@ func (a *contextWindow) keptUserTurnsBudget() int {
|
||||
// it the drop is invisible: the projection reads as complete, and the escape
|
||||
// hatch is only useful to someone told it exists at the moment it is needed.
|
||||
func (a *contextWindow) noticeDroppedUserTurns(ret userTurnRetention) {
|
||||
if ret.MarkedDropped > 0 {
|
||||
a.svc.sink.Emit(event.Event{Kind: event.Notice, Level: event.LevelWarn,
|
||||
Text: fmt.Sprintf("%s you marked [[keep]] %s past the room [[keep]] has, so the oldest now survive only through the summary.",
|
||||
pluralTurns(ret.MarkedDropped), wereOrWas(ret.MarkedDropped)),
|
||||
Detail: fmt.Sprintf("compaction folded %d [[keep]] turn(s) (~%d tokens) past the marked-turn budget of %d",
|
||||
ret.MarkedDropped, ret.MarkedDroppedTokens, a.keptMarkedBudget())})
|
||||
}
|
||||
if ret.Dropped == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/contract/provider"
|
||||
)
|
||||
|
||||
func markedTurn(n, chars int) provider.Message {
|
||||
return provider.Message{Role: provider.RoleUser, Content: fmt.Sprintf("[[keep]] turn %d ", n) + strings.Repeat("c", chars)}
|
||||
}
|
||||
|
||||
func keptMarkedTokens(kept []provider.Message) (tokens int, turns map[string]bool) {
|
||||
turns = map[string]bool{}
|
||||
for _, m := range kept {
|
||||
if m.Role == provider.RoleUser && isUserMarked(m) {
|
||||
tokens += fixedTokenEstimate(m)
|
||||
turns[strings.Fields(m.Content)[2]] = true
|
||||
}
|
||||
}
|
||||
return tokens, turns
|
||||
}
|
||||
|
||||
// [[keep]] holds a turn verbatim inside a budget of its own. With no bound a
|
||||
// session that marks turn after turn grows what every fold must carry until the
|
||||
// candidate cannot land under the trigger, and a fold that cannot land is
|
||||
// refused again every round.
|
||||
func TestMarkedTurnsStayWithinTheirBudgetAndTheNewestSurvive(t *testing.T) {
|
||||
a := &Agent{agentConfig: agentConfig{contextWindow: 200_000}, keepPolicy: KeepUserMarked}
|
||||
budget := a.window().keptMarkedBudget()
|
||||
if budget <= 0 {
|
||||
t.Fatalf("budget = %d, want a positive bound", budget)
|
||||
}
|
||||
var region []provider.Message
|
||||
for i := range 20 {
|
||||
region = append(region, markedTurn(i, 40_000))
|
||||
}
|
||||
kept, fold, retention, _ := a.window().partitionFoldWith(region, a.keepPolicy)
|
||||
tokens, turns := keptMarkedTokens(kept)
|
||||
if tokens > budget {
|
||||
t.Fatalf("%d tokens of [[keep]] turns kept, budget %d", tokens, budget)
|
||||
}
|
||||
if !turns["19"] || turns["0"] {
|
||||
t.Fatalf("kept %v, want the newest turn kept and the oldest folded", turns)
|
||||
}
|
||||
if retention.MarkedDropped == 0 || retention.MarkedDropped != 20-len(turns) || len(fold) != retention.MarkedDropped {
|
||||
t.Fatalf("retention reports %d dropped marked turns, fold has %d messages, %d kept", retention.MarkedDropped, len(fold), len(turns))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkedTurnsUnderTheirBudgetAllStay(t *testing.T) {
|
||||
a := &Agent{agentConfig: agentConfig{contextWindow: 200_000}, keepPolicy: KeepUserMarked}
|
||||
var region []provider.Message
|
||||
for i := range 4 {
|
||||
region = append(region, markedTurn(i, 2_000))
|
||||
}
|
||||
kept, fold, retention, _ := a.window().partitionFoldWith(region, a.keepPolicy)
|
||||
if _, turns := keptMarkedTokens(kept); len(turns) != 4 || len(fold) != 0 || retention.MarkedDropped != 0 {
|
||||
t.Fatalf("kept %v, fold %d, dropped %d; want all four kept", turns, len(fold), retention.MarkedDropped)
|
||||
}
|
||||
}
|
||||
|
||||
// Marked turns carried through folds must not accumulate: a turn kept at one
|
||||
// fold rides into the next fold's region.
|
||||
func TestMarkedTurnsCarriedThroughFoldsDoNotAccumulate(t *testing.T) {
|
||||
a := &Agent{agentConfig: agentConfig{contextWindow: 200_000}, keepPolicy: KeepUserMarked}
|
||||
budget := a.window().keptMarkedBudget()
|
||||
var carried []provider.Message
|
||||
next := 0
|
||||
for round := 1; round <= 8; round++ {
|
||||
region := []provider.Message{formatSummaryMessage("earlier work")}
|
||||
region = append(region, carried...)
|
||||
for range 5 {
|
||||
region = append(region, markedTurn(next, 40_000))
|
||||
next++
|
||||
}
|
||||
kept, _, _, _ := a.window().partitionFoldWith(region, a.keepPolicy)
|
||||
if tokens, _ := keptMarkedTokens(kept); tokens > budget {
|
||||
t.Fatalf("round %d: %d tokens kept, budget %d", round, tokens, budget)
|
||||
}
|
||||
carried = kept
|
||||
}
|
||||
}
|
||||
@@ -490,3 +490,25 @@ func TestOutputBudgetKeepsAWindowProportionalMarginOnALargeWindow(t *testing.T)
|
||||
t.Fatalf("a prompt 1%% above its estimate plus the %d-token budget is %d, past the %d window", got, actual+got, window)
|
||||
}
|
||||
}
|
||||
|
||||
// The prompt size at which the send-time output check refuses a request is the
|
||||
// size at which a fold must free room. The fold's hard ceiling used to sit above
|
||||
// it, so a prompt in between was neither folded nor sendable, and the person
|
||||
// saw the refusal beside a fold that declined for being worth too little.
|
||||
func TestTheFoldCeilingStopsWhereTheSendTimeOutputCheckStops(t *testing.T) {
|
||||
prov := &sharedWindowTestProvider{budget: 8 * 1024, shared: true}
|
||||
a := &Agent{agentConfig: agentConfig{contextWindow: 128_000}, svc: agentServices{prov: prov, sink: event.Discard}, sess: sessionRuntime{output: outputBudgetState{outputBudget: prov.budget}}}
|
||||
w := a.window()
|
||||
ceiling := w.hardInputCeiling()
|
||||
atCeiling := provider.Request{Messages: []provider.Message{{Role: provider.RoleUser, Content: strings.Repeat("x", 4)}}}
|
||||
if _, _, err := w.effectiveOutputBudget(atCeiling); err != nil {
|
||||
t.Fatalf("a tiny prompt is refused: %v", err)
|
||||
}
|
||||
if want := 128_000 - outputBudgetReserveFor(128_000); ceiling != want {
|
||||
t.Fatalf("hardInputCeiling = %d, want the window less the output reserve (%d)", ceiling, want)
|
||||
}
|
||||
prov.shared = false
|
||||
if got := w.hardInputCeiling(); got != 128_000-protocolReserveTokens {
|
||||
t.Fatalf("a provider whose reply does not share the window keeps the protocol reserve only, got %d", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
- 修复:`[[keep]]` 标记的回合现在有自己的保留预算(窗口的 15%,新的优先),超出的旧回合并入摘要并提示;共享窗口的模型在回复空间用尽前就会压缩。见 #13320、#12773。
|
||||
- Fixed: [[keep]] turns now have their own budget (15% of the window, newest first); older ones fold with a notice. Shared-window models fold before the reply runs out of room. See #13320, #12773.
|
||||
Reference in new issue
Block a user