返回 DeepSeek-Reasonix
post_write_receipt.go
根目录 / internal / tool / builtin / post_write_receipt.go
1 package builtin
2
3 import (
4 "fmt"
5 "strings"
6 "unicode/utf8"
7 )
8
9 // Mutation receipts are appended to the provider-visible conversation after
10 // every edit, so keep their dynamic tail bounded. The receipt contains only the
11 // matched and replacement spans, never unchanged same-line or neighboring data.
12 const maxPostWriteReceiptBytes = 2048
13 const maxCapturedReceiptSpanBytes = 896
14
15 const (
16 postWriteSpanTruncated = "…[replacement span truncated]…"
17 postWriteReceiptTruncated = "…[replacement receipt truncated; use read_file for complete current contents]…"
18 )
19
20 func withActualPostWriteReceipts(summary string, receipts []editReplacementReceipt) string {
21 if len(receipts) == 0 {
22 return summary
23 }
24 body := renderPostWriteReceipts(receipts)
25 if strings.TrimSpace(body) == "" {
26 return summary
27 }
28 return summary + "\nActual replacement receipt after write:\n" + body
29 }
30
31 func renderPostWriteReceipts(receipts []editReplacementReceipt) string {
32 indexes := receiptIndexes(len(receipts))
33 if len(indexes) == 0 {
34 return ""
35 }
36
37 // Share the bounded body between the selected first/last receipts and their
38 // matched/replacement fields. The final clip below remains a defensive cap
39 // for unusually large counts or labels.
40 fieldBudget := (maxPostWriteReceiptBytes - 256) / (2 * len(indexes))
41 if fieldBudget < 128 {
42 fieldBudget = 128
43 }
44 if fieldBudget > maxCapturedReceiptSpanBytes {
45 fieldBudget = maxCapturedReceiptSpanBytes
46 }
47
48 var b strings.Builder
49 b.Grow(maxPostWriteReceiptBytes)
50 for pos, idx := range indexes {
51 if pos > 0 {
52 b.WriteByte('\n')
53 }
54 if len(receipts) > 2 && pos == 1 {
55 fmt.Fprintf(&b, "…[%d intermediate replacement receipt(s) omitted]…\n\n", len(receipts)-2)
56 }
57 r := receipts[idx]
58 occurrences := r.occurrences
59 if occurrences <= 0 {
60 occurrences = 1
61 }
62 fuzzy := ""
63 if r.fuzzy {
64 fuzzy = ", fuzzy match"
65 if occurrences > 1 {
66 fuzzy += ", first matched sample shown"
67 }
68 }
69 fmt.Fprintf(&b, "@@ replacement %d of %d (%d occurrence(s)%s) @@\n", idx+1, len(receipts), occurrences, fuzzy)
70 appendReceiptSpan(&b, '-', clipPostWriteSpan(r.matched, fieldBudget))
71 appendReceiptSpan(&b, '+', clipPostWriteSpan(r.replacement, fieldBudget))
72 }
73 return clipPostWriteReceipt(b.String())
74 }
75
76 func receiptIndexes(count int) []int {
77 switch count {
78 case 0:
79 return nil
80 case 1:
81 return []int{0}
82 case 2:
83 return []int{0, 1}
84 default:
85 return []int{0, count - 1}
86 }
87 }
88
89 func appendReceiptSpan(b *strings.Builder, prefix byte, text string) {
90 if text == "" {
91 b.WriteByte(prefix)
92 b.WriteString("<empty>\n")
93 return
94 }
95 for len(text) > 0 {
96 line := text
97 if i := strings.IndexByte(text, '\n'); i >= 0 {
98 line = text[:i]
99 text = text[i+1:]
100 } else {
101 text = ""
102 }
103 b.WriteByte(prefix)
104 b.WriteString(line)
105 b.WriteByte('\n')
106 }
107 }
108
109 func clipPostWriteSpan(text string, budget int) string {
110 if len(text) <= budget {
111 return text
112 }
113 marker := "\n" + postWriteSpanTruncated + "\n"
114 return clipUTF8HeadTail(text, budget, marker)
115 }
116
117 func clipPostWriteReceipt(text string) string {
118 if len(text) <= maxPostWriteReceiptBytes {
119 return text
120 }
121 marker := "\n" + postWriteReceiptTruncated + "\n"
122 return clipUTF8HeadTail(text, maxPostWriteReceiptBytes, marker)
123 }
124
125 func clipUTF8HeadTail(text string, budget int, marker string) string {
126 available := budget - len(marker)
127 if available <= 0 {
128 return clipUTF8Prefix(marker, budget)
129 }
130 headBytes := available * 3 / 4
131 tailBytes := available - headBytes
132 headEnd := utf8PrefixBoundary(text, headBytes)
133 tailStart := utf8SuffixBoundary(text, len(text)-tailBytes)
134 return text[:headEnd] + marker + text[tailStart:]
135 }
136
137 func clipUTF8Prefix(text string, end int) string {
138 if end >= len(text) {
139 return text
140 }
141 return text[:utf8PrefixBoundary(text, end)]
142 }
143
144 func utf8PrefixBoundary(text string, end int) int {
145 if end >= len(text) {
146 return len(text)
147 }
148 if end < 0 {
149 return 0
150 }
151 for end > 0 && !utf8.RuneStart(text[end]) {
152 end--
153 }
154 return end
155 }
156
157 func utf8SuffixBoundary(text string, start int) int {
158 if start <= 0 {
159 return 0
160 }
161 if start >= len(text) {
162 return len(text)
163 }
164 for start < len(text) && !utf8.RuneStart(text[start]) {
165 start++
166 }
167 return start
168 }
169
169 lines GO