mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 17:10:43 -10:00
feat(agent): add an economic context-maintenance boundary
Compaction was not broken. It faithfully executed a policy that only knew
one boundary: compact_ratio of the declared window. A provider preset
declaring a 1,000,000-token window therefore put the trigger at 850,000,
and a real session ran 2,150 rounds against a prompt that peaked at
321,265 — 37.8% of the budget, so maintenance was never once attempted.
input avg round latency
86K 12.18s
130K 15.16s
158K 20.25s
195K 28.17s
226K 30.28s
314K 37.91s
Capacity and economics are different boundaries. A model may support a 1M
context without making a 300K prompt economical to replay for hundreds of
agent rounds, so the automatic trigger is now whichever is reached first:
compact_ratio of the window, or context_soft_limit_tokens of visible input
(default 160000, negative disables). The verbatim tail follows the boundary
in force — a tail that reaches the trigger leaves nothing to fold, which
would have made the new boundary fire against a prompt it could not reduce.
A ratio placed past the window stays how automatic maintenance is turned
off: economics tightens a live boundary and never revives a retired one, so
an existing compact_ratio = 2 keeps meaning what it meant.
Every attempt now says what it decided. A no-fold verdict carries a code
rather than a sentence, because "already folded this turn" and "nothing
left to fold" read alike and mean opposite things about what the next round
can expect. The verdict reached while a turn is running was previously
returned silently, which is why the trajectory of that 15.6-hour session
cannot say whether maintenance was tried; it now emits once per turn per
reason, writes no receipt and blocks nothing.
This does not make long active turns repeatedly compactable. It makes the
first economic maintenance attempt observable; repeated same-turn
maintenance remains intentionally blocked, and
TestSecondMaintenanceInOneTurnReportsAlreadyCompacted is the regression
test that keeps that fact visible.
Three comments promising an adaptive stop that 21638e356 removed are
corrected to say what actually bounds a turn today: nothing, by default.
Baseline widened by three lines — the floor price of one config setting
(field, one-line comment, render line).
This commit is contained in:
1 parent
ba415e863e
commit
280dfd8c92
22 files changed
+453
-69
No files matched your search
@@ -305,6 +305,23 @@ export interface ExtensionSurface {
|
||||
view?: ExtensionView;
|
||||
}
|
||||
|
||||
// One context-maintenance transaction. status "noop" carries code: the attempt
|
||||
// ran and freed nothing, and code is what tells "already folded this turn" from
|
||||
// "nothing left to fold" — they read alike and mean opposite things about what
|
||||
// the next round can expect. boundary names which threshold was reached.
|
||||
export interface ContextMaintenance {
|
||||
status?: string;
|
||||
action?: string;
|
||||
trigger?: string;
|
||||
code?: string;
|
||||
boundary?: string;
|
||||
triggerTokens?: number;
|
||||
inputTokens?: number;
|
||||
resultTokens?: number;
|
||||
savedTokens?: number;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface Compaction {
|
||||
trigger?: string;
|
||||
messages?: number;
|
||||
@@ -466,6 +483,7 @@ export interface WireEvent {
|
||||
guardian?: Guardian;
|
||||
extension?: ExtensionSurface;
|
||||
compaction?: Compaction;
|
||||
maintenance?: ContextMaintenance;
|
||||
streamAttempt?: StreamAttempt;
|
||||
completion?: CompletionSummary;
|
||||
graph?: GraphDelta;
|
||||
|
||||
@@ -179,8 +179,19 @@ function record(ev: WireEvent): Made | null {
|
||||
case "steer":
|
||||
return { kind: "memory_recall", payload: [{ t: "steer delivered · " }, { b: ev.text ?? "" }] };
|
||||
|
||||
case "context_maintenance":
|
||||
return { kind: "outcome_progress", payload: [{ t: ev.text || "context maintenance" }] };
|
||||
case "context_maintenance": {
|
||||
const m = ev.maintenance;
|
||||
if (!m) return { kind: "outcome_progress", payload: [{ t: ev.text || "context maintenance" }] };
|
||||
const payload: Span[] = [{ t: "context maintenance · " }, { b: m.status ?? "" }];
|
||||
if (m.boundary) payload.push({ t: " · " }, { b: m.boundary });
|
||||
// A verdict without its code is the line that made the last runaway
|
||||
// unreadable: it says maintenance happened, never what it decided.
|
||||
if (m.code) payload.push({ t: " · " }, { b: m.code });
|
||||
if (m.inputTokens) payload.push({ t: " · in " }, { n: String(m.inputTokens) });
|
||||
if (m.resultTokens) payload.push({ t: " → " }, { n: String(m.resultTokens) });
|
||||
if (m.triggerTokens) payload.push({ t: " · trigger " }, { n: String(m.triggerTokens) });
|
||||
return { kind: "outcome_progress", payload };
|
||||
}
|
||||
|
||||
case "completion_summary": {
|
||||
const c = ev.completion;
|
||||
|
||||
@@ -954,9 +954,9 @@ func (a *Agent) reserveParentWrite(runTool tool.Tool, args json.RawMessage, read
|
||||
|
||||
// Run appends the user input and drives the tool loop until the model returns a
|
||||
// final answer, the context is cancelled, or the provider errors. maxSteps <= 0
|
||||
// leaves the loop unbounded here: bounding it is the host's call, and the
|
||||
// adaptive stop is the no-progress ladder rather than a round count. Turn policy
|
||||
// lives in beginRunTurn / runToolLoop / handleFinalResponse / handleToolRound.
|
||||
// leaves the loop unbounded here: bounding it is the host's call, as a TaskBudget
|
||||
// in hours or tokens rather than a round count, and every such budget ships off.
|
||||
// Turn policy lives in beginRunTurn / runToolLoop / handleFinalResponse.
|
||||
func (a *Agent) Run(ctx context.Context, input string) (runErr error) {
|
||||
runMaxSteps := a.maxSteps
|
||||
runMaxStepsKey := a.maxStepsKey
|
||||
|
||||
@@ -42,4 +42,8 @@ type CompactionBudgets struct {
|
||||
UserTurnKeepTokens int
|
||||
FirstTurnPinTokens int
|
||||
CheckpointCeilingRatio float64
|
||||
// ContextSoftLimitTokens is the economic maintenance boundary: the visible
|
||||
// input size at which replaying the prompt each round stops being worth it,
|
||||
// whatever window the provider declares. Negative disables that boundary.
|
||||
ContextSoftLimitTokens int
|
||||
}
|
||||
+51
-17
@@ -16,25 +16,26 @@ import (
|
||||
)
|
||||
|
||||
// Compaction is a low-frequency cache-reset point: the prompt grows append-only
|
||||
// until compactRatio of the window is crossed, then one content-driven summary
|
||||
// until a maintenance boundary is crossed, then one content-driven summary
|
||||
// checkpoint is installed (stable prefix + one structured digest + recent tail).
|
||||
// 50% is only the normal acceptance ceiling — candidates are never padded up to it.
|
||||
const (
|
||||
defaultCompactRatio = 0.85 // sole automatic maintenance trigger (new configs)
|
||||
checkpointCeilingRatio = 0.50 // normal auto-checkpoint acceptance ceiling
|
||||
recentTailBudgetRatio = 0.10 // recent verbatim tail as a fraction of the window
|
||||
minRecentTailTokens = 32 * 1024
|
||||
maxRecentTailTokens = 96 * 1024
|
||||
summaryOutputMaxTokens = 16 * 1024 // max digest output; further clipped by remaining candidate space
|
||||
exceptionalMinSavingsRatio = 0.25 // when fixed prefix alone exceeds 50%, require at least this savings
|
||||
minRecentKeep = 2 // never keep fewer recent messages than this
|
||||
minCompactMessages = 2 // skip compaction below this many compactable messages
|
||||
fallbackTokPerChar = 0.25 // ~4 chars/token, used before any usage is available to calibrate
|
||||
defaultPinnedFirstUserTokens = 1500 // default ceiling on pinning the first user turn verbatim
|
||||
pinnedFirstUserWindowFrac = 0.15 // and never pin a first turn worth more than this fraction of the window
|
||||
keptUserTurnsWindowFrac = 0.05 // default verbatim user-turn budget, as a fraction of the window
|
||||
keptUserTurnsFloorTokens = 1024 // ...with a floor so a small window still holds something
|
||||
protocolReserveTokens = 256 // provider framing and control fields not represented by message estimates
|
||||
defaultCompactRatio = 0.85 // capacity share of the window before maintenance
|
||||
defaultContextSoftLimitTokens = 160_000 // economic maintenance boundary, independent of the window
|
||||
checkpointCeilingRatio = 0.50 // normal auto-checkpoint acceptance ceiling
|
||||
recentTailBudgetRatio = 0.10 // recent verbatim tail as a fraction of the window
|
||||
minRecentTailTokens = 32 * 1024
|
||||
maxRecentTailTokens = 96 * 1024
|
||||
summaryOutputMaxTokens = 16 * 1024 // max digest output; further clipped by remaining candidate space
|
||||
exceptionalMinSavingsRatio = 0.25 // when fixed prefix alone exceeds 50%, require at least this savings
|
||||
minRecentKeep = 2 // never keep fewer recent messages than this
|
||||
minCompactMessages = 2 // skip compaction below this many compactable messages
|
||||
fallbackTokPerChar = 0.25 // ~4 chars/token, used before any usage is available to calibrate
|
||||
defaultPinnedFirstUserTokens = 1500 // default ceiling on pinning the first user turn verbatim
|
||||
pinnedFirstUserWindowFrac = 0.15 // and never pin a first turn worth more than this fraction of the window
|
||||
keptUserTurnsWindowFrac = 0.05 // default verbatim user-turn budget, as a fraction of the window
|
||||
keptUserTurnsFloorTokens = 1024 // ...with a floor so a small window still holds something
|
||||
protocolReserveTokens = 256 // provider framing and control fields not represented by message estimates
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -86,7 +87,8 @@ Rules: be terse — bullet points and fragments, not prose. Preserve identifiers
|
||||
// budgets are intentionally absent: they are clipped against the final request
|
||||
// at send time and must never make compaction happen earlier than the user's
|
||||
// configured compact_ratio.
|
||||
func (a *Agent) compactTrigger() int {
|
||||
// capacityCompactTrigger answers how close a prompt is to not fitting.
|
||||
func (a *Agent) capacityCompactTrigger() int {
|
||||
window := a.effectiveContextWindow()
|
||||
if window <= 0 {
|
||||
return 0
|
||||
@@ -101,6 +103,38 @@ func (a *Agent) compactTrigger() int {
|
||||
return max(1, int(float64(window)*ratio))
|
||||
}
|
||||
|
||||
// economicCompactTrigger answers what a prompt costs to replay, which is a
|
||||
// property of its size and not of what the model could have held: a declared 1M
|
||||
// window makes a 300k prompt legal, never economical. Negative disables it.
|
||||
func (a *Agent) economicCompactTrigger() int {
|
||||
if a.ablation.Off(ablation.Compaction) {
|
||||
return 0
|
||||
}
|
||||
limit := a.budgets.ContextSoftLimitTokens
|
||||
if limit < 0 {
|
||||
return 0
|
||||
}
|
||||
if limit == 0 {
|
||||
limit = defaultContextSoftLimitTokens
|
||||
}
|
||||
return limit
|
||||
}
|
||||
|
||||
// compactTrigger is whichever boundary is reached first. An unmeasured window
|
||||
// leaves both unset rather than guessing at a size, and a ratio placed past the
|
||||
// window is how maintenance is turned off — economics tightens a live boundary,
|
||||
// never revives a retired one.
|
||||
func (a *Agent) compactTrigger() int {
|
||||
capacity := a.capacityCompactTrigger()
|
||||
if capacity <= 0 || a.compactRatio > 1 {
|
||||
return capacity
|
||||
}
|
||||
if economic := a.economicCompactTrigger(); economic > 0 && economic < capacity {
|
||||
return economic
|
||||
}
|
||||
return capacity
|
||||
}
|
||||
|
||||
// hardInputCeiling is a physical input-safety boundary, not another user
|
||||
// compaction threshold. Reply budgets are resolved independently at send time.
|
||||
func (a *Agent) hardInputCeiling() int {
|
||||
|
||||
@@ -144,7 +144,7 @@ func TestCompactionDoneCarriesWhatTheDigestKept(t *testing.T) {
|
||||
a := New(&fakeProvider{reply: "## Files & code\n- internal/parser/lexer.go rewritten"}, reg, sess,
|
||||
Options{ContextWindow: 60_000, CompactRatio: 0.5, RecentKeep: 2, ArchiveDir: testenv.TempDir(t)}, sink)
|
||||
|
||||
if _, err := a.compactToProjection(context.Background(), CompactionTriggerManual, "", true, false); err != nil {
|
||||
if _, _, err := a.compactToProjection(context.Background(), CompactionTriggerManual, "", true, false); err != nil {
|
||||
t.Fatalf("compactToProjection: %v", err)
|
||||
}
|
||||
if len(done) != 1 {
|
||||
|
||||
@@ -380,7 +380,7 @@ func compactionTelemetryFromSummary(trigger, cacheState string, sourceTokens int
|
||||
|
||||
// compact writes a context projection; trigger stays "auto"/"manual" for UI cards.
|
||||
func (a *Agent) compact(ctx context.Context, trigger, instructions string, force bool) error {
|
||||
_, err := a.compactToProjection(ctx, trigger, instructions, force, false)
|
||||
_, _, err := a.compactToProjection(ctx, trigger, instructions, force, false)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -389,12 +389,12 @@ func (a *Agent) compact(ctx context.Context, trigger, instructions string, force
|
||||
// The canonical transcript is never rewritten. CompactionNoop means nothing
|
||||
// was foldable; callers at physical overflow must treat that as hard failure.
|
||||
// mustFree marks the fold the caller cannot proceed without.
|
||||
func (a *Agent) compactToProjection(ctx context.Context, trigger, instructions string, force, mustFree bool) (CompactionOutcome, error) {
|
||||
func (a *Agent) compactToProjection(ctx context.Context, trigger, instructions string, force, mustFree bool) (CompactionOutcome, CompactionNoopReason, error) {
|
||||
a.sess.compactionRunMu.Lock()
|
||||
defer a.sess.compactionRunMu.Unlock()
|
||||
activeTurn := a.activeTurnCreatedAt.Load()
|
||||
if activeTurn != 0 && a.sess.compaction.lastTurn.Load() == activeTurn && trigger != CompactionTriggerManual {
|
||||
return CompactionNoop, nil
|
||||
return CompactionNoop, NoopAlreadyCompactedThisTurn, nil
|
||||
}
|
||||
canonical, transcriptVersion := a.sess.conversation.snapshotMessagesVersion()
|
||||
a.sess.compactionMu.Lock()
|
||||
@@ -405,24 +405,24 @@ func (a *Agent) compactToProjection(ctx context.Context, trigger, instructions s
|
||||
msgs := a.visibleInputForFold(stateSnapshot, canonical, transcriptVersion)
|
||||
viewInputHash := providerVisibleFingerprint(provider.ModelMessages(msgs))
|
||||
if trigger != CompactionTriggerManual && stateSnapshot.LastReceipt != nil && stateSnapshot.LastReceipt.Status == "applied" && stateSnapshot.LastReceipt.Action == "summary" && stateSnapshot.LastReceipt.InputHash == viewInputHash {
|
||||
return CompactionNoop, nil
|
||||
return CompactionNoop, NoopInputUnchanged, nil
|
||||
}
|
||||
head, start, ok := a.planFoldRegion(msgs, force)
|
||||
head, start, ok, planReason := a.planFoldRegion(msgs, force)
|
||||
if !ok {
|
||||
return CompactionNoop, nil
|
||||
return CompactionNoop, planReason, nil
|
||||
}
|
||||
// The annotation rides the projection, not the canonical transcript: the
|
||||
// original stays whole for resume and rewind.
|
||||
kept, fold, retention, policyKeep := a.partitionFoldForProjection(a.annotateFailureDiagnostics(ctx, msgs[head:start]))
|
||||
if len(fold) == 0 || (!force && !a.foldEconomics(fold)) {
|
||||
return CompactionNoop, nil
|
||||
return CompactionNoop, NoopFoldBelowEconomics, nil
|
||||
}
|
||||
fold, priorIndex := stripFoldIndexFromDigests(fold)
|
||||
foldIndex := buildFoldIndex(msgs[head:start], policyKeep, a.toolFactsFor,
|
||||
a.canonicalOriginFor(stateSnapshot, canonical, msgs, head))
|
||||
fixedPrefixTokens := a.estimatedPromptTokens(a.providerProjectionMessages(msgs[:head]))
|
||||
if a.effectiveContextWindow() > 0 && fixedPrefixTokens >= a.compactTrigger() {
|
||||
return CompactionNoop, rejectCheckpoint("fixed prefix (%d tokens) already exceeds trigger (%d)", fixedPrefixTokens, a.compactTrigger())
|
||||
return CompactionNoop, NoopFixedPrefixAboveTrigger, rejectCheckpoint("fixed prefix (%d tokens) already exceeds trigger (%d)", fixedPrefixTokens, a.compactTrigger())
|
||||
}
|
||||
|
||||
sourceTokens := a.estimatedPromptTokens(msgs)
|
||||
@@ -441,18 +441,18 @@ func (a *Agent) compactToProjection(ctx context.Context, trigger, instructions s
|
||||
fold, instructions, err = a.interceptCompactionPrepare(ctx, fold, instructions)
|
||||
if err != nil {
|
||||
a.emitCompactionAborted(trigger)
|
||||
return CompactionNoop, err
|
||||
return CompactionNoop, "", err
|
||||
}
|
||||
if len(fold) == 0 {
|
||||
a.emitCompactionAborted(trigger)
|
||||
return CompactionNoop, nil
|
||||
return CompactionNoop, NoopFoldEmptyAfterHooks, nil
|
||||
}
|
||||
|
||||
res, tele, err := a.foldOrDegrade(ctx, trigger, mustFree, fold, instructions, sourceTokens)
|
||||
if err != nil {
|
||||
a.emitCompactionTelemetry(tele)
|
||||
a.emitCompactionAborted(trigger)
|
||||
return CompactionNoop, err
|
||||
return CompactionNoop, "", err
|
||||
}
|
||||
res.Text = a.attachFoldIndex(res.Text, priorIndex, foldIndex)
|
||||
summary, err := a.interceptCompactionComplete(ctx, res.Text)
|
||||
@@ -460,7 +460,7 @@ func (a *Agent) compactToProjection(ctx context.Context, trigger, instructions s
|
||||
tele.Error = err.Error()
|
||||
a.emitCompactionTelemetry(tele)
|
||||
a.emitCompactionAborted(trigger)
|
||||
return CompactionNoop, err
|
||||
return CompactionNoop, "", err
|
||||
}
|
||||
|
||||
projMsgs := checkpointProjectionMessages(msgs, head, start, kept, summary)
|
||||
@@ -471,7 +471,7 @@ func (a *Agent) compactToProjection(ctx context.Context, trigger, instructions s
|
||||
a.emitCompactionTelemetry(tele)
|
||||
if err := a.acceptCheckpointCandidate(trigger, force, sourceTokens, projTokens, fixedPrefixTokens); err != nil {
|
||||
a.emitCompactionAborted(trigger)
|
||||
return CompactionNoop, err
|
||||
return CompactionNoop, "", err
|
||||
}
|
||||
viewOutputHash := providerVisibleFingerprint(provider.ModelMessages(projMsgs))
|
||||
_, err = a.commitSummaryProjection(summaryProjectionCommit{
|
||||
@@ -483,7 +483,7 @@ func (a *Agent) compactToProjection(ctx context.Context, trigger, instructions s
|
||||
})
|
||||
if err != nil {
|
||||
a.emitCompactionAborted(trigger)
|
||||
return CompactionNoop, err
|
||||
return CompactionNoop, "", err
|
||||
}
|
||||
a.svc.sink.Emit(event.Event{Kind: event.CompactionDone, Compaction: event.Compaction{
|
||||
Trigger: trigger, Messages: len(fold), Summary: summary,
|
||||
@@ -495,7 +495,7 @@ func (a *Agent) compactToProjection(ctx context.Context, trigger, instructions s
|
||||
// Only once the checkpoint is committed: a rejected candidate folded nothing.
|
||||
a.sess.compaction.lastUserTurns = retention
|
||||
a.noticeDroppedUserTurns(retention)
|
||||
return CompactionInstalled, nil
|
||||
return CompactionInstalled, "", nil
|
||||
}
|
||||
|
||||
// visibleInputForFold prefers the prior projection + new history over full canonical.
|
||||
@@ -563,18 +563,27 @@ func (a *Agent) acceptCheckpointCandidate(trigger string, force bool, sourceToke
|
||||
}
|
||||
|
||||
// planFoldRegion returns [head:start] to fold; force shrinks the recent tail.
|
||||
func (a *Agent) planFoldRegion(msgs []provider.Message, force bool) (head, start int, ok bool) {
|
||||
func (a *Agent) planFoldRegion(msgs []provider.Message, force bool) (head, start int, ok bool, reason CompactionNoopReason) {
|
||||
head, start, ok = a.planCompaction(msgs, minCompactMessages, force)
|
||||
if !ok {
|
||||
head, start, ok = a.planCompaction(msgs, 1, force)
|
||||
}
|
||||
if !ok {
|
||||
return head, start, false
|
||||
return head, start, false, NoopNoFoldableRegion
|
||||
}
|
||||
// The running turn stays verbatim, so its start is where the fold has to
|
||||
// end. A turn long enough to reach the trigger on its own leaves nothing
|
||||
// behind it, and that is a different verdict from having nothing to fold.
|
||||
if active := a.activeTurnStart(msgs); active >= head && active < start {
|
||||
start = active
|
||||
if start <= head {
|
||||
return head, start, false, NoopActiveTurnBoundary
|
||||
}
|
||||
}
|
||||
return head, start, start > head
|
||||
if start <= head {
|
||||
return head, start, false, NoopNoFoldableRegion
|
||||
}
|
||||
return head, start, true, ""
|
||||
}
|
||||
|
||||
func (a *Agent) partitionFoldForProjection(region []provider.Message) (kept, fold []provider.Message, retention userTurnRetention, policyKeep []bool) {
|
||||
|
||||
@@ -67,7 +67,7 @@ func prepareContext(ctx context.Context, a *Agent, trigger string) error {
|
||||
func foldRegionOf(a *Agent) []provider.Message {
|
||||
canonical, version := a.sess.conversation.snapshotMessagesVersion()
|
||||
msgs := a.visibleInputForFold(a.sess.compactionState, canonical, version)
|
||||
head, start, ok := a.planFoldRegion(msgs, false)
|
||||
head, start, ok, _ := a.planFoldRegion(msgs, false)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -37,15 +37,28 @@ func TestRecentTailBudgetClamp(t *testing.T) {
|
||||
{2_000_000, maxRecentTailTokens}, // still 96K
|
||||
}
|
||||
for _, tc := range cases {
|
||||
a := &Agent{agentConfig: agentConfig{contextWindow: tc.window, compactRatio: defaultCompactRatio}}
|
||||
a := &Agent{agentConfig: agentConfig{contextWindow: tc.window, compactRatio: defaultCompactRatio,
|
||||
budgets: CompactionBudgets{ContextSoftLimitTokens: -1}}}
|
||||
if got := a.recentTailBudget(); got != tc.want {
|
||||
t.Fatalf("window %d: recentTailBudget = %d, want %d", tc.window, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A tail reaching the trigger leaves nothing to fold, so the tail follows
|
||||
// whichever boundary is in force — including the one that does not come from
|
||||
// the window. Without this the economic boundary would arrive with the whole
|
||||
// prompt still held verbatim.
|
||||
func TestRecentTailFollowsTheEconomicBoundary(t *testing.T) {
|
||||
a := &Agent{agentConfig: agentConfig{contextWindow: 1_000_000, compactRatio: defaultCompactRatio}}
|
||||
if got, want := a.recentTailBudget(), defaultContextSoftLimitTokens/2; got != want {
|
||||
t.Fatalf("recentTailBudget = %d, want %d (half the economic trigger)", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckpointCeilingAndExceptionalSavings(t *testing.T) {
|
||||
a := &Agent{agentConfig: agentConfig{contextWindow: 1_000_000, compactRatio: 0.85}}
|
||||
a := &Agent{agentConfig: agentConfig{contextWindow: 1_000_000, compactRatio: 0.85,
|
||||
budgets: CompactionBudgets{ContextSoftLimitTokens: -1}}}
|
||||
if got := a.checkpointCeiling(); got != 500_000 {
|
||||
t.Fatalf("checkpointCeiling = %d, want 500000", got)
|
||||
}
|
||||
@@ -58,7 +71,8 @@ func TestCheckpointCeilingAndExceptionalSavings(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAcceptCheckpointCandidateRules(t *testing.T) {
|
||||
a := &Agent{agentConfig: agentConfig{contextWindow: 1_000_000, compactRatio: 0.85}}
|
||||
a := &Agent{agentConfig: agentConfig{contextWindow: 1_000_000, compactRatio: 0.85,
|
||||
budgets: CompactionBudgets{ContextSoftLimitTokens: -1}}}
|
||||
// 20% candidate under normal path: accept.
|
||||
if err := a.acceptCheckpointCandidate(CompactionTriggerPressure, false, 850_000, 180_000, 50_000); err != nil {
|
||||
t.Fatalf("20%% candidate: %v", err)
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestLargeFixedPrefixDoesNotRefuseCompaction(t *testing.T) {
|
||||
t.Fatalf("fixture prefix is %d characters, under the %d trigger; it cannot show the bug", len(prefix), a.compactTrigger())
|
||||
}
|
||||
|
||||
if _, err := a.compactToProjection(context.Background(), CompactionTriggerManual, "", true, false); err != nil {
|
||||
if _, _, err := a.compactToProjection(context.Background(), CompactionTriggerManual, "", true, false); err != nil {
|
||||
t.Fatalf("compaction refused a prefix that fits: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +333,7 @@ func TestRangeCompressionSharesSummarySingleflight(t *testing.T) {
|
||||
a := New(prov, tool.NewRegistry(), sess, Options{RecentKeep: 2, ArchiveDir: testenv.TempDir(t)}, event.Discard)
|
||||
autoErr := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := a.compactToProjection(context.Background(), CompactionTriggerPressure, "", true, false)
|
||||
_, _, err := a.compactToProjection(context.Background(), CompactionTriggerPressure, "", true, false)
|
||||
autoErr <- err
|
||||
}()
|
||||
<-prov.started
|
||||
|
||||
@@ -40,13 +40,15 @@ func largeRestartToolHistory(results int) []provider.Message {
|
||||
func newRestartMaintenanceAgent(path string, messages []provider.Message, sink event.Sink) *Agent {
|
||||
// Explicit 0.80 matches the v1.22.0 user scenario and the plan's regression
|
||||
// fixture. New configs default to 0.85; users who set 0.80 keep 0.80.
|
||||
// Economics off: the capacity share is the only trigger under test here.
|
||||
return New(nil, tool.NewRegistry(), &Session{Messages: append([]provider.Message(nil), messages...)}, Options{
|
||||
ContextWindow: restartMaintenanceWindow,
|
||||
CompactRatio: 0.80,
|
||||
RecentKeep: 2,
|
||||
SessionPath: path,
|
||||
WorkspaceID: "workspace",
|
||||
ModelRef: "provider/model",
|
||||
ContextWindow: restartMaintenanceWindow,
|
||||
CompactRatio: 0.80,
|
||||
RecentKeep: 2,
|
||||
SessionPath: path,
|
||||
WorkspaceID: "workspace",
|
||||
ModelRef: "provider/model",
|
||||
CompactionBudgets: CompactionBudgets{ContextSoftLimitTokens: -1},
|
||||
}, sink)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,12 +19,23 @@ type compactionProgress struct {
|
||||
// lastTurn stops the post-turn observer and the pre-send preflight from
|
||||
// paying for two summaries during one active tool loop.
|
||||
lastTurn atomic.Int64
|
||||
// lastNoop is the verdict already reported for the running turn. A crossed
|
||||
// threshold stays crossed, so without it every later round of the same turn
|
||||
// would report the same "folded nothing" again.
|
||||
lastNoop maintenanceNoop
|
||||
// lastUserTurns is what the most recent fold could and could not hold of
|
||||
// the user's own words. The notice fires once, at the moment of the fold;
|
||||
// this is what /context can still answer with afterwards.
|
||||
lastUserTurns userTurnRetention
|
||||
}
|
||||
|
||||
// maintenanceNoop is one reported no-fold verdict, scoped to the turn it was
|
||||
// reached under: the same reason in a later turn is news again.
|
||||
type maintenanceNoop struct {
|
||||
reason CompactionNoopReason
|
||||
turn int64
|
||||
}
|
||||
|
||||
// ContextManager is the sole owner of provider-visible context maintenance.
|
||||
// Canonical session messages are immutable inputs; Prepare evolves only the
|
||||
// durable projection and returns the exact visible view for one sampling round.
|
||||
@@ -122,7 +133,7 @@ func (m ContextManager) foldContext(ctx context.Context, prepared PreparedContex
|
||||
// Where this function would answer ErrCompactionRequired, the fold is the
|
||||
// only way out and a failed summary must degrade rather than strand the turn.
|
||||
mustFree := policy.Trigger != CompactionTriggerManual && (policy.Trigger == CompactionTriggerOverflow || est >= hard)
|
||||
outcome, err := a.compactToProjection(ctx, policy.Trigger, policy.Instructions, forceFold, mustFree)
|
||||
outcome, noopReason, err := a.compactToProjection(ctx, policy.Trigger, policy.Instructions, forceFold, mustFree)
|
||||
if err != nil {
|
||||
// Cancellation is the caller's decision, not a summary that failed:
|
||||
// recording it as one would blame the summarizer for this generation,
|
||||
@@ -134,7 +145,7 @@ func (m ContextManager) foldContext(ctx context.Context, prepared PreparedContex
|
||||
// Transcript changed during the summary call: discard the candidate
|
||||
// and block this generation so we do not pay for a second summary.
|
||||
reason := "context changed during summary; automatic retry blocked for this generation"
|
||||
a.recordContextMaintenanceBlocked(inputHash, policy.Trigger, "summary", reason)
|
||||
a.recordContextMaintenanceBlocked(inputHash, policy.Trigger, "summary", "", reason)
|
||||
if policy.Trigger == CompactionTriggerOverflow || est >= hard {
|
||||
return PreparedContext{}, fmt.Errorf("%w: %s", ErrCompactionRequired, reason)
|
||||
}
|
||||
@@ -145,7 +156,7 @@ func (m ContextManager) foldContext(ctx context.Context, prepared PreparedContex
|
||||
status = "blocked"
|
||||
}
|
||||
reason := fmt.Sprintf("context summary failed: %v", err)
|
||||
a.recordContextMaintenanceOutcome(inputHash, policy.Trigger, "summary", status, reason)
|
||||
a.recordContextMaintenanceOutcome(inputHash, policy.Trigger, "summary", status, "", reason)
|
||||
if policy.Trigger == CompactionTriggerManual {
|
||||
return PreparedContext{}, err
|
||||
}
|
||||
@@ -163,13 +174,17 @@ func (m ContextManager) foldContext(ctx context.Context, prepared PreparedContex
|
||||
if outcome == CompactionNoop {
|
||||
canonical, _ := a.sess.conversation.snapshotMessagesVersion()
|
||||
if policy.Trigger == CompactionTriggerPressure && a.activeTurnStart(canonical) >= 0 {
|
||||
// A running turn may still grow past a threshold it cannot fold
|
||||
// below. Saying so is the only record that the attempt happened:
|
||||
// this path deliberately does not block the next one.
|
||||
a.noteMaintenanceNoop(policy.Trigger, noopReason, est)
|
||||
return prepared, nil
|
||||
}
|
||||
reason := "context is above the maintenance threshold but no foldable region remains"
|
||||
if policy.Trigger == CompactionTriggerManual {
|
||||
reason = "no foldable region remains"
|
||||
}
|
||||
a.recordContextMaintenanceBlocked(inputHash, policy.Trigger, "summary", reason)
|
||||
a.recordContextMaintenanceBlocked(inputHash, policy.Trigger, "summary", noopReason, reason)
|
||||
// Manual carries Force, so without this a hand-typed compact answered
|
||||
// with the overflow error — a sentence about a provider limit nowhere
|
||||
// near being hit. Nothing to fold is a verdict, not a failure.
|
||||
@@ -188,7 +203,7 @@ func (m ContextManager) foldContext(ctx context.Context, prepared PreparedContex
|
||||
}
|
||||
if result.InputTokens >= fold {
|
||||
reason := fmt.Sprintf("summary result remains above fold trigger (%d >= %d)", result.InputTokens, fold)
|
||||
a.recordContextMaintenanceBlocked(a.contextMaintenanceInputHash(result.Messages), policy.Trigger, "summary", reason)
|
||||
a.recordContextMaintenanceBlocked(a.contextMaintenanceInputHash(result.Messages), policy.Trigger, "summary", "", reason)
|
||||
a.sess.compaction.stuck = true
|
||||
if policy.Trigger == CompactionTriggerOverflow || result.InputTokens >= hard {
|
||||
return PreparedContext{}, fmt.Errorf("%w: %s", ErrCompactionRequired, reason)
|
||||
|
||||
@@ -53,18 +53,52 @@ func (a *Agent) emitContextMaintenance(r *ContextMaintenanceReceipt) {
|
||||
InputTokens: r.InputTokens, ResultTokens: r.ResultTokens, SavedTokens: r.SavedTokens,
|
||||
AffectedToolResults: r.AffectedToolResults, ProjectionVersion: r.ProjectionVersion,
|
||||
CacheBreak: r.CacheBreak, Reason: r.Reason,
|
||||
Code: r.Code, Boundary: r.Boundary, TriggerTokens: r.TriggerTokens,
|
||||
}})
|
||||
}
|
||||
|
||||
// compactBoundary names the threshold that decides maintenance right now: the
|
||||
// window share, or the absolute visible-input size that does not come from it.
|
||||
func (a *Agent) compactBoundary() string {
|
||||
capacity := a.capacityCompactTrigger()
|
||||
if capacity <= 0 || a.compactRatio > 1 {
|
||||
return ""
|
||||
}
|
||||
if economic := a.economicCompactTrigger(); economic > 0 && economic < capacity {
|
||||
return "economic"
|
||||
}
|
||||
return "capacity"
|
||||
}
|
||||
|
||||
// noteMaintenanceNoop reports one attempt that folded nothing. It writes no
|
||||
// receipt and advances no generation: nothing was spent and nothing is blocked,
|
||||
// so the next round is free to try again — which is exactly why the attempt
|
||||
// would otherwise leave no trace at all.
|
||||
func (a *Agent) noteMaintenanceNoop(trigger string, reason CompactionNoopReason, inputTokens int) {
|
||||
if a == nil || reason == "" || a.svc.sink == nil {
|
||||
return
|
||||
}
|
||||
turn := a.activeTurnCreatedAt.Load()
|
||||
if a.sess.compaction.lastNoop.reason == reason && a.sess.compaction.lastNoop.turn == turn {
|
||||
return
|
||||
}
|
||||
a.sess.compaction.lastNoop = maintenanceNoop{reason: reason, turn: turn}
|
||||
a.svc.sink.Emit(event.Event{Kind: event.ContextMaintenanceEvent, Maintenance: &event.ContextMaintenance{
|
||||
Status: "noop", Action: "summary", Trigger: trigger, Code: string(reason),
|
||||
Boundary: a.compactBoundary(), TriggerTokens: a.compactTrigger(), InputTokens: inputTokens,
|
||||
}})
|
||||
}
|
||||
|
||||
// recordContextMaintenanceBlocked persists a generation-scoped blocked receipt.
|
||||
func (a *Agent) recordContextMaintenanceBlocked(inputHash, trigger, action, reason string) {
|
||||
a.recordContextMaintenanceOutcome(inputHash, trigger, action, "blocked", reason)
|
||||
// code is empty where the verdict has no identity beyond its sentence.
|
||||
func (a *Agent) recordContextMaintenanceBlocked(inputHash, trigger, action string, code CompactionNoopReason, reason string) {
|
||||
a.recordContextMaintenanceOutcome(inputHash, trigger, action, "blocked", code, reason)
|
||||
}
|
||||
|
||||
// recordContextMaintenanceOutcome records blocked or failed for the current
|
||||
// generation. Automatic Prepare will not re-enter summary until the generation
|
||||
// advances (successful install, manual compress, or lineage change).
|
||||
func (a *Agent) recordContextMaintenanceOutcome(inputHash, trigger, action, status, reason string) {
|
||||
func (a *Agent) recordContextMaintenanceOutcome(inputHash, trigger, action, status string, code CompactionNoopReason, reason string) {
|
||||
if a == nil || a.sess.conversation == nil {
|
||||
return
|
||||
}
|
||||
@@ -110,6 +144,7 @@ func (a *Agent) recordContextMaintenanceOutcome(inputHash, trigger, action, stat
|
||||
Trigger: trigger, SourceProjection: state.Projection.ProjectionVersion,
|
||||
ProjectionVersion: state.Projection.ProjectionVersion, InputHash: inputHash,
|
||||
BlockedInputHash: inputHash, Reason: reason, CreatedAt: now,
|
||||
Code: string(code), Boundary: a.compactBoundary(), TriggerTokens: a.compactTrigger(),
|
||||
}
|
||||
state.UpdatedAt = now
|
||||
a.sess.compactionState = state
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/event"
|
||||
"reasonix/internal/provider"
|
||||
"reasonix/internal/testenv"
|
||||
"reasonix/internal/tool"
|
||||
)
|
||||
|
||||
// Capacity and economics answer different questions: how close the prompt is to
|
||||
// not fitting, and how much it costs to replay. A declared 1M window makes a
|
||||
// 300K prompt legal without making it economical, so the boundary that decides
|
||||
// maintenance is whichever one is reached first.
|
||||
func TestMaintenanceBoundaryIsTheNearerOfCapacityAndEconomics(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
window int
|
||||
ratio float64
|
||||
soft int
|
||||
trigger int
|
||||
boundary string
|
||||
}{
|
||||
{"million-window folds on economics", 1_000_000, 0.85, 0, defaultContextSoftLimitTokens, "economic"},
|
||||
{"small window keeps capacity first", 128_000, 0.85, 0, 108_800, "capacity"},
|
||||
{"configured soft limit wins when lower", 1_000_000, 0.85, 96_000, 96_000, "economic"},
|
||||
{"configured soft limit loses when higher", 200_000, 0.85, 400_000, 170_000, "capacity"},
|
||||
{"negative soft limit disables economics", 1_000_000, 0.85, -1, 850_000, "capacity"},
|
||||
{"no declared window leaves both unset", 0, 0.85, 0, 0, ""},
|
||||
// A ratio past the window is how a user turns automatic maintenance off.
|
||||
{"ratio past the window stays off", 1_000_000, 2, 0, 2_000_000, ""},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
a := New(nil, tool.NewRegistry(), &Session{}, Options{
|
||||
ContextWindow: tc.window,
|
||||
CompactRatio: tc.ratio,
|
||||
CompactionBudgets: CompactionBudgets{ContextSoftLimitTokens: tc.soft},
|
||||
}, event.Discard)
|
||||
if got := a.compactTrigger(); got != tc.trigger {
|
||||
t.Errorf("compactTrigger = %d, want %d", got, tc.trigger)
|
||||
}
|
||||
if got := a.compactBoundary(); got != tc.boundary {
|
||||
t.Errorf("compactBoundary = %q, want %q", got, tc.boundary)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// economicFixture is a session whose visible input sits far above the economic
|
||||
// boundary and far below the capacity one, which is exactly the shape that ran
|
||||
// for 2150 rounds at 321K tokens inside a declared 1M window.
|
||||
const economicActiveTurnAt int64 = 1_700_000_000_000
|
||||
|
||||
func economicFixture(t *testing.T, soft int, activeTurnRound int) (*Agent, *recordSink) {
|
||||
t.Helper()
|
||||
sess := &Session{Messages: []provider.Message{
|
||||
{Role: provider.RoleSystem, Content: "system"},
|
||||
{Role: provider.RoleUser, Content: "long-running task"},
|
||||
}}
|
||||
body := strings.Repeat("x", 24*1024)
|
||||
for i := range 40 {
|
||||
turn := provider.Message{Role: provider.RoleUser, Content: body}
|
||||
if i == activeTurnRound {
|
||||
turn.CreatedAt = economicActiveTurnAt
|
||||
}
|
||||
sess.Messages = append(sess.Messages,
|
||||
provider.Message{Role: provider.RoleAssistant, Content: "step " + string(rune('a'+i%26))},
|
||||
turn)
|
||||
}
|
||||
sink := &recordSink{}
|
||||
a := New(&fakeProvider{reply: "digest"}, tool.NewRegistry(), sess, Options{
|
||||
ContextWindow: 1_000_000,
|
||||
CompactRatio: 0.85,
|
||||
RecentKeep: 2,
|
||||
ArchiveDir: testenv.TempDir(t),
|
||||
CompactionBudgets: CompactionBudgets{ContextSoftLimitTokens: soft},
|
||||
}, sink)
|
||||
return a, sink
|
||||
}
|
||||
|
||||
// A prompt above the economic boundary must be maintained even though it is
|
||||
// nowhere near the window share, and one below it must not be.
|
||||
func TestEconomicTriggerFoldsBelowCapacityShare(t *testing.T) {
|
||||
a, sink := economicFixture(t, 0, -1)
|
||||
visible := a.estimatedVisibleRequestTokens(a.modelVisibleMessages())
|
||||
if visible <= a.economicCompactTrigger() || visible >= a.capacityCompactTrigger() {
|
||||
t.Fatalf("fixture input %d is not between the economic (%d) and capacity (%d) boundaries",
|
||||
visible, a.economicCompactTrigger(), a.capacityCompactTrigger())
|
||||
}
|
||||
if _, err := a.contextManager().Prepare(context.Background(), ContextPreparePolicy{Trigger: CompactionTriggerPressure}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := a.currentProjectionVersion(); got == 0 {
|
||||
t.Fatal("input above the economic boundary installed no projection")
|
||||
}
|
||||
if got := appliedMaintenanceEvents(sink); got != 1 {
|
||||
t.Fatalf("applied maintenance events = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInputBelowBothBoundariesIsNotMaintained(t *testing.T) {
|
||||
a, sink := economicFixture(t, 4_000_000, -1)
|
||||
if _, err := a.contextManager().Prepare(context.Background(), ContextPreparePolicy{Trigger: CompactionTriggerPressure}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := a.currentProjectionVersion(); got != 0 {
|
||||
t.Fatalf("input below both boundaries installed projection version %d", got)
|
||||
}
|
||||
if got := appliedMaintenanceEvents(sink); got != 0 {
|
||||
t.Fatalf("applied maintenance events = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
// growTurn appends rounds to the running turn, which is what a tool loop does
|
||||
// between two Prepare calls and what keeps its history unfoldable.
|
||||
func growTurn(a *Agent, rounds int) {
|
||||
body := strings.Repeat("x", 24*1024)
|
||||
for i := range rounds {
|
||||
a.sess.conversation.Add(provider.Message{Role: provider.RoleAssistant, Content: "more " + string(rune('a'+i%26))})
|
||||
a.sess.conversation.Add(provider.Message{Role: provider.RoleTool, ToolCallID: "t", Name: "read_file", Content: body})
|
||||
}
|
||||
}
|
||||
|
||||
func maintenanceCodes(sink *recordSink, status string) []string {
|
||||
var codes []string
|
||||
for _, got := range sink.kinds(event.ContextMaintenanceEvent) {
|
||||
if got.Maintenance != nil && got.Maintenance.Status == status {
|
||||
codes = append(codes, got.Maintenance.Code)
|
||||
}
|
||||
}
|
||||
return codes
|
||||
}
|
||||
|
||||
// A turn folds at most once, and the rounds after it keep arriving above the
|
||||
// boundary. Without a verdict for those rounds the trajectory shows a session
|
||||
// that never tried to maintain its context, which is what this whole path was
|
||||
// missing: the attempt happened and freed nothing.
|
||||
func TestSecondMaintenanceInOneTurnReportsAlreadyCompacted(t *testing.T) {
|
||||
// The running turn starts late enough that the rounds before it fold, so
|
||||
// the first attempt succeeds and the turn is marked as already compacted.
|
||||
a, sink := economicFixture(t, 0, 30)
|
||||
a.activeTurnCreatedAt.Store(economicActiveTurnAt)
|
||||
ctx := context.Background()
|
||||
if _, err := a.contextManager().Prepare(ctx, ContextPreparePolicy{Trigger: CompactionTriggerPressure}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if a.currentProjectionVersion() == 0 {
|
||||
t.Fatal("first attempt installed no projection; the fixture cannot show the second")
|
||||
}
|
||||
// The turn keeps running. Its own rounds are held verbatim, so the visible
|
||||
// input climbs back over the boundary with nothing new left to fold.
|
||||
growTurn(a, 30)
|
||||
if _, err := a.contextManager().Prepare(ctx, ContextPreparePolicy{Trigger: CompactionTriggerPressure}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
codes := maintenanceCodes(sink, "noop")
|
||||
if len(codes) != 1 || codes[0] != string(NoopAlreadyCompactedThisTurn) {
|
||||
t.Fatalf("noop codes = %v, want exactly [%s]", codes, NoopAlreadyCompactedThisTurn)
|
||||
}
|
||||
}
|
||||
|
||||
// The threshold stays crossed for the rest of the turn, so the same verdict is
|
||||
// reached on every later round. Reporting it once is what keeps a trajectory
|
||||
// readable; reporting it never is what made this invisible.
|
||||
func TestRepeatedNoopInOneTurnReportsOnce(t *testing.T) {
|
||||
a, sink := economicFixture(t, 0, 30)
|
||||
a.activeTurnCreatedAt.Store(economicActiveTurnAt)
|
||||
ctx := context.Background()
|
||||
if _, err := a.contextManager().Prepare(ctx, ContextPreparePolicy{Trigger: CompactionTriggerPressure}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for range 4 {
|
||||
growTurn(a, 10)
|
||||
if _, err := a.contextManager().Prepare(ctx, ContextPreparePolicy{Trigger: CompactionTriggerPressure}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if codes := maintenanceCodes(sink, "noop"); len(codes) != 1 {
|
||||
t.Fatalf("noop codes = %v, want exactly one report for the turn", codes)
|
||||
}
|
||||
}
|
||||
|
||||
// A turn that is the whole transcript has nothing behind it to fold. That is a
|
||||
// different verdict from an exhausted history, and the two are told apart by
|
||||
// the code rather than by reading the sentence beside it.
|
||||
func TestActiveTurnBoundaryLeavesNothingToFold(t *testing.T) {
|
||||
body := strings.Repeat("x", 24*1024)
|
||||
// Too large to be pinned as a brief, so the fixed prefix is the system
|
||||
// message alone and the running turn starts exactly where the fold would.
|
||||
sess := &Session{Messages: []provider.Message{
|
||||
{Role: provider.RoleSystem, Content: "system"},
|
||||
{Role: provider.RoleUser, Content: body, CreatedAt: economicActiveTurnAt},
|
||||
}}
|
||||
for i := range 40 {
|
||||
sess.Messages = append(sess.Messages,
|
||||
provider.Message{Role: provider.RoleAssistant, Content: "step " + string(rune('a'+i%26))},
|
||||
provider.Message{Role: provider.RoleTool, ToolCallID: "t", Name: "read_file", Content: body})
|
||||
}
|
||||
sink := &recordSink{}
|
||||
a := New(&fakeProvider{reply: "digest"}, tool.NewRegistry(), sess, Options{
|
||||
ContextWindow: 1_000_000, CompactRatio: 0.85, RecentKeep: 2, ArchiveDir: testenv.TempDir(t),
|
||||
}, sink)
|
||||
a.activeTurnCreatedAt.Store(economicActiveTurnAt)
|
||||
if _, err := a.contextManager().Prepare(context.Background(), ContextPreparePolicy{Trigger: CompactionTriggerPressure}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
codes := maintenanceCodes(sink, "noop")
|
||||
if len(codes) != 1 || codes[0] != string(NoopActiveTurnBoundary) {
|
||||
t.Fatalf("noop codes = %v, want exactly [%s]", codes, NoopActiveTurnBoundary)
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,9 @@ type ContextMaintenanceReceipt struct {
|
||||
SummaryHash string `json:"summary_hash,omitempty"`
|
||||
CacheBreak bool `json:"cache_break,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Boundary string `json:"boundary,omitempty"`
|
||||
TriggerTokens int `json:"trigger_tokens,omitempty"`
|
||||
BlockedInputHash string `json:"blocked_input_hash,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
}
|
||||
@@ -106,6 +109,21 @@ const (
|
||||
CompactionNoop
|
||||
)
|
||||
|
||||
// CompactionNoopReason identifies why an attempt folded nothing. "Already folded
|
||||
// this turn" and "nothing left to fold" read alike and mean opposite things about
|
||||
// what the next round can expect, so the verdict carries a code, not a sentence.
|
||||
type CompactionNoopReason string
|
||||
|
||||
const (
|
||||
NoopAlreadyCompactedThisTurn CompactionNoopReason = "already_compacted_this_turn"
|
||||
NoopActiveTurnBoundary CompactionNoopReason = "active_turn_boundary"
|
||||
NoopNoFoldableRegion CompactionNoopReason = "no_foldable_region"
|
||||
NoopFoldBelowEconomics CompactionNoopReason = "fold_below_economics"
|
||||
NoopInputUnchanged CompactionNoopReason = "input_unchanged"
|
||||
NoopFoldEmptyAfterHooks CompactionNoopReason = "fold_empty_after_hooks"
|
||||
NoopFixedPrefixAboveTrigger CompactionNoopReason = "fixed_prefix_above_trigger"
|
||||
)
|
||||
|
||||
// RecallLedger is what one projection generation has already pulled back out
|
||||
// of the fold. Carrying the generation it belongs to is what resets the budget:
|
||||
// a stale generation reads as an unspent one, with no reset call to forget.
|
||||
|
||||
@@ -16,5 +16,6 @@ func compactionBudgets(cfg *config.Config) agent.CompactionBudgets {
|
||||
UserTurnKeepTokens: cfg.Agent.UserTurnKeepTokens,
|
||||
FirstTurnPinTokens: cfg.Agent.FirstTurnPinTokens,
|
||||
CheckpointCeilingRatio: cfg.Agent.CheckpointCeilingRatio,
|
||||
ContextSoftLimitTokens: cfg.Agent.ContextSoftLimitTokens,
|
||||
}
|
||||
}
|
||||
@@ -1321,6 +1321,8 @@ type AgentConfig struct {
|
||||
// CheckpointCeilingRatio is how small an automatic fold's result must be,
|
||||
// as a fraction of the window, before it is accepted. 0 selects 0.50.
|
||||
CheckpointCeilingRatio float64 `toml:"checkpoint_ceiling_ratio"`
|
||||
// Folds on input size, not window share: 1M declared makes 300k legal, never economical.
|
||||
ContextSoftLimitTokens int `toml:"context_soft_limit_tokens"`
|
||||
// ColdResumePrune elides stale tool results when a session reopens past the
|
||||
// provider cache window. nil = default enabled.
|
||||
ColdResumePrune *bool `toml:"cold_resume_prune"`
|
||||
@@ -1775,8 +1777,8 @@ func Default() *Config {
|
||||
},
|
||||
Agent: AgentConfig{
|
||||
SystemPrompt: DefaultSystemPrompt,
|
||||
// Normal interactive execution has no configurable total round cap. It
|
||||
// is bounded by adaptive progress guards and context compaction instead.
|
||||
// No total round cap, and nothing else bounds a turn by default: the
|
||||
// task budgets below ship off and the round guards were retired.
|
||||
MaxSteps: 0,
|
||||
PlannerMaxSteps: 0,
|
||||
AutoPlan: "off",
|
||||
|
||||
@@ -207,6 +207,7 @@ func RenderTOMLForScope(c *Config, scope RenderScope) string {
|
||||
} else {
|
||||
b.WriteString("# checkpoint_ceiling_ratio = 0.50 # how small a fold's result must be to be accepted\n")
|
||||
}
|
||||
fmt.Fprintf(&b, "context_soft_limit_tokens = %d # fold at this visible input size whatever the window says; 0 = 160000, negative = off\n", c.Agent.ContextSoftLimitTokens)
|
||||
if c.Agent.RecentKeep > 0 {
|
||||
fmt.Fprintf(&b, "recent_keep = %d # minimum recent messages kept verbatim\n", c.Agent.RecentKeep)
|
||||
} else {
|
||||
|
||||
@@ -401,6 +401,12 @@ type ContextMaintenance struct {
|
||||
ProjectionVersion uint64 `json:"projectionVersion,omitempty"`
|
||||
CacheBreak bool `json:"cacheBreak,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
// Code identifies a no-fold verdict for a reader that must tell them apart;
|
||||
// Reason is the same verdict as a sentence. Boundary names which threshold
|
||||
// was reached (capacity or economic) and TriggerTokens is its size.
|
||||
Code string `json:"code,omitempty"`
|
||||
Boundary string `json:"boundary,omitempty"`
|
||||
TriggerTokens int `json:"triggerTokens,omitempty"`
|
||||
}
|
||||
|
||||
// GuardianResult carries the outcome of a guardian sub-agent safety review.
|
||||
|
||||
@@ -46,7 +46,7 @@ func NewProgressTracker() *ProgressTracker {
|
||||
// round's evidence gain. Reading something new is progress only while it leads
|
||||
// somewhere: past explorationRunLimit look-only rounds the novelty stops
|
||||
// counting, because a loop that keeps opening files it has never opened scored
|
||||
// positive every round and so could never reach the no-progress ladder.
|
||||
// positive every round. No guard consumes this today.
|
||||
func (t *ProgressTracker) ScoreRound(receipts []Receipt) int {
|
||||
if t == nil {
|
||||
return 0
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
"complexity": 1513,
|
||||
"error-text": 12,
|
||||
"essay": 3245,
|
||||
"file-size": 41730,
|
||||
"file-size": 41733,
|
||||
"frontend-parity": 150,
|
||||
"function-size": 6595,
|
||||
"function-size": 6596,
|
||||
"layering": 0,
|
||||
"marker": 0,
|
||||
"narrative": 57,
|
||||
@@ -626,7 +626,7 @@
|
||||
},
|
||||
"internal/config/config.go": {
|
||||
"essay": 50,
|
||||
"file-size": 1436,
|
||||
"file-size": 1438,
|
||||
"narrative": 2
|
||||
},
|
||||
"internal/config/credentials.go": {
|
||||
@@ -704,8 +704,8 @@
|
||||
"internal/config/render.go": {
|
||||
"complexity": 226,
|
||||
"essay": 2,
|
||||
"file-size": 923,
|
||||
"function-size": 857,
|
||||
"file-size": 924,
|
||||
"function-size": 858,
|
||||
"orphan": 1
|
||||
},
|
||||
"internal/config/vision.go": {
|
||||
|
||||
Reference in new issue
Block a user