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