返回 DeepSeek-Reasonix
truncate.go
根目录 / internal / agent / truncate.go
1 package agent
2
3 import (
4 "fmt"
5 "strings"
6
7 "reasonix/internal/provider"
8 )
9
10 // Truncation is the lossy last rung of overflow recovery, taken only when no
11 // summary can form: tool results outside the protected tail are elided
12 // oldest-first, then whole replay units are dropped, until the view fits under
13 // the target. It is a projection; canonical storage keeps every byte.
14 const (
15 maintenanceActionTruncate = "truncate"
16 elidedToolResultPrefix = "[tool result elided to fit the context window"
17 truncatedHistoryMarker = "[earlier conversation truncated to fit the context window: %d messages removed]"
18 // truncateProtectShare bounds the verbatim tail to this fraction of the
19 // target so a rescue can always reclaim enough.
20 truncateProtectShare = 4
21 )
22
23 func (a *Agent) truncateToProjectionLocked(trigger string, target int) (bool, error) {
24 canonical, transcriptVersion := a.sess.conversation.snapshotMessagesVersion()
25 a.sess.compactionMu.Lock()
26 stateSnapshot := a.sess.compactionState
27 a.sess.compactionMu.Unlock()
28 visible, _ := a.visibleInputForFold(stateSnapshot, canonical, transcriptVersion)
29 projected, affected := a.truncateView(visible, target)
30 if affected == 0 {
31 return false, nil
32 }
33 return a.installMaintenanceProjection(maintenanceInstall{
34 trigger: trigger, action: maintenanceActionTruncate, state: stateSnapshot,
35 canonical: canonical, transcriptVersion: transcriptVersion,
36 visible: visible, projected: projected, affected: affected,
37 })
38 }
39
40 // truncateView returns the truncated copy of visible and how many messages it
41 // changed; zero means the view already fits or nothing could be cut.
42 func (a *Agent) truncateView(visible []provider.Message, target int) ([]provider.Message, int) {
43 total := a.estimatedVisibleRequestTokens(visible)
44 if target <= 0 || total < target || len(visible) == 0 {
45 return nil, 0
46 }
47 head := a.pinnedPrefixLen(visible)
48 budget := max(1, min(a.recentTailBudget(), target/truncateProtectShare))
49 protect := tailStart(visible, head, budget, a.tokPerChar(), minRecentKeep)
50 projected := append([]provider.Message(nil), visible...)
51 remaining, affected := total, 0
52 for i := head; i < protect && remaining >= target; i++ {
53 elided, ok := elideToolResult(projected[i])
54 if !ok {
55 continue
56 }
57 remaining -= a.messageTokens(projected[i]) - a.messageTokens(elided)
58 projected[i] = elided
59 affected++
60 }
61 if remaining >= target {
62 var dropped int
63 projected, dropped = a.dropOldestUnits(projected, head, protect, target)
64 affected += dropped
65 }
66 if affected == 0 || a.estimatedVisibleRequestTokens(projected) >= total {
67 return nil, 0
68 }
69 return projected, affected
70 }
71
72 func (a *Agent) messageTokens(m provider.Message) int {
73 return a.estimatedPromptTokens([]provider.Message{m})
74 }
75
76 func elideToolResult(m provider.Message) (provider.Message, bool) {
77 if m.Role != provider.RoleTool || m.LocalOnly || m.Content == "" || strings.HasPrefix(m.Content, elidedToolResultPrefix) {
78 return m, false
79 }
80 out := m
81 out.Content = fmt.Sprintf("%s: %d bytes]", elidedToolResultPrefix, len(m.Content))
82 out.RawContent = ""
83 out.ProviderContent = ""
84 out.Images = nil
85 out.ImageInputs = nil
86 return out, true
87 }
88
89 // dropOldestUnits removes whole replay units from the oldest end of the
90 // foldable region until the estimate fits. The latest session context,
91 // compaction digests, and pinned revisions survive behind one marker.
92 func (a *Agent) dropOldestUnits(msgs []provider.Message, head, protect, target int) ([]provider.Message, int) {
93 if protect <= head {
94 return msgs, 0
95 }
96 remaining := a.estimatedVisibleRequestTokens(msgs)
97 latestContext := latestSessionContextIndex(msgs)
98 var kept []provider.Message
99 dropped, end := 0, head
100 for _, u := range extractMessageUnits(msgs[head:protect]) {
101 if remaining < target {
102 break
103 }
104 for i := head + u.lo; i < head+u.hi; i++ {
105 if i == latestContext || isCompactionSummary(msgs[i]) || IsPinnedContextRevision(msgs[i]) {
106 kept = append(kept, msgs[i])
107 continue
108 }
109 remaining -= a.messageTokens(msgs[i])
110 dropped++
111 }
112 end = head + u.hi
113 }
114 if dropped == 0 {
115 return msgs, 0
116 }
117 out := make([]provider.Message, 0, len(msgs)-dropped+1)
118 out = append(out, msgs[:head]...)
119 out = append(out, HostGeneratedUserMessage(fmt.Sprintf(truncatedHistoryMarker, dropped)))
120 out = append(out, kept...)
121 out = append(out, msgs[end:]...)
122 return out, dropped
123 }
124
124 lines GO