返回 DeepSeek-Reasonix
prune.go
根目录 / internal / agent / prune.go
1 package agent
2
3 import (
4 "strings"
5 "unicode/utf8"
6
7 "reasonix/internal/provider"
8 "reasonix/internal/tool"
9 )
10
11 // Legacy snip helpers still support compatibility storage. Their public APIs
12 // are no-ops; pressure-time Harness pruning uses the rune-based policy below.
13 const (
14 snippedMarker = "[snipped tool result — "
15 prunedMarker = "[elided tool result — "
16 minPruneBytes = 1024
17
18 toolPruneThresholdRunes = 8192
19 toolPruneHeadRunes = 4096
20 toolPruneTailRunes = 1024
21 toolPruneMarker = "[... tool result middle pruned ...]"
22 )
23
24 func pruneToolResultContent(content string) (string, bool) {
25 if utf8.RuneCountInString(content) <= toolPruneThresholdRunes {
26 return content, false
27 }
28 headEnd := byteOffsetAfterRunes(content, toolPruneHeadRunes)
29 tailStart := byteOffsetBeforeLastRunes(content, toolPruneTailRunes)
30 var pruned strings.Builder
31 pruned.Grow(headEnd + len(toolPruneMarker) + len(content) - tailStart)
32 pruned.WriteString(content[:headEnd])
33 pruned.WriteString(toolPruneMarker)
34 pruned.WriteString(content[tailStart:])
35 return pruned.String(), true
36 }
37
38 func byteOffsetAfterRunes(content string, count int) int {
39 if count <= 0 {
40 return 0
41 }
42 seen := 0
43 for offset := range content {
44 if seen == count {
45 return offset
46 }
47 seen++
48 }
49 return len(content)
50 }
51
52 func byteOffsetBeforeLastRunes(content string, count int) int {
53 offset := len(content)
54 for range count {
55 if offset == 0 {
56 return 0
57 }
58 _, size := utf8.DecodeLastRuneInString(content[:offset])
59 offset -= size
60 }
61 return offset
62 }
63
64 // pruneToolResultsToProjectionLocked installs a durable, model-visible prune
65 // projection. The caller owns compactionRunMu for the whole maintenance run;
66 // canonical storage, including RawContent, is never modified.
67 func (a *Agent) pruneToolResultsToProjectionLocked(trigger string) (bool, error) {
68 canonical, transcriptVersion := a.sess.conversation.snapshotMessagesVersion()
69 a.sess.compactionMu.Lock()
70 stateSnapshot := a.sess.compactionState
71 a.sess.compactionMu.Unlock()
72 visible, _ := a.visibleInputForFold(stateSnapshot, canonical, transcriptVersion)
73 projected := append([]provider.Message(nil), visible...)
74 affected := 0
75 for i := range projected {
76 if projected[i].Role != provider.RoleTool {
77 continue
78 }
79 source := projected[i].Content
80 if projected[i].ProviderContent != "" {
81 source = projected[i].ProviderContent
82 }
83 if pruned, changed := pruneToolResultContent(source); changed {
84 projected[i].Content = pruned
85 projected[i].RawContent = ""
86 projected[i].ProviderContent = ""
87 affected++
88 }
89 }
90 if affected == 0 {
91 return false, nil
92 }
93 return a.installMaintenanceProjection(maintenanceInstall{
94 trigger: trigger, action: "prune", state: stateSnapshot,
95 canonical: canonical, transcriptVersion: transcriptVersion,
96 visible: visible, projected: projected, affected: affected,
97 })
98 }
99
100 type toolResultMaintenanceMode int
101
102 const (
103 toolResultSnip toolResultMaintenanceMode = iota
104 toolResultPrune
105 )
106
107 // PruneStats reports one maintenance pass.
108 type PruneStats struct {
109 Results int
110 SavedChars int
111 Archive string
112 Mode toolResultMaintenanceMode
113 InputHash string
114 Force bool
115 }
116
117 // SnipStaleToolResults is a no-op: automatic prune/snip projections are gone.
118 func (a *Agent) SnipStaleToolResults() (PruneStats, error) {
119 return PruneStats{Mode: toolResultSnip}, nil
120 }
121
122 // PruneStaleToolResults is a no-op: automatic prune/snip projections are gone.
123 func (a *Agent) PruneStaleToolResults() (PruneStats, error) {
124 return PruneStats{Mode: toolResultPrune}, nil
125 }
126
127 type snipStrategy struct {
128 head int
129 tail int
130 headChars int
131 tailChars int
132 }
133
134 var (
135 defaultReadOnlySnip = snipStrategy{head: 80, tail: 12, headChars: 10000, tailChars: 2000}
136 defaultSideEffectingSnip = snipStrategy{head: 40, tail: 40, headChars: 8000, tailChars: 8000}
137 )
138
139 func (a *Agent) snipStrategyFor(name string) snipStrategy {
140 if a.svc.tools != nil {
141 if t, ok := a.svc.tools.Get(name); ok {
142 if h, ok := t.(tool.SnipHinter); ok {
143 return snipStrategyFromHint(h.SnipHint())
144 }
145 if t.ReadOnly() {
146 return defaultReadOnlySnip
147 }
148 return defaultSideEffectingSnip
149 }
150 }
151 return defaultReadOnlySnip
152 }
153
154 func snipStrategyFromHint(h tool.SnipHint) snipStrategy {
155 return snipStrategy{head: h.Head, tail: h.Tail, headChars: h.HeadChars, tailChars: h.TailChars}
156 }
157
158 func minInt(a, b int) int {
159 if a < b {
160 return a
161 }
162 return b
163 }
164
164 lines GO