返回 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 := min(max((maxPostWriteReceiptBytes-256)/(2*len(indexes)), 128), maxCapturedReceiptSpanBytes)
41
42 var b strings.Builder
43 b.Grow(maxPostWriteReceiptBytes)
44 for pos, idx := range indexes {
45 if pos > 0 {
46 b.WriteByte('\n')
47 }
48 if len(receipts) > 2 && pos == 1 {
49 fmt.Fprintf(&b, "…[%d intermediate replacement receipt(s) omitted]…\n\n", len(receipts)-2)
50 }
51 r := receipts[idx]
52 occurrences := r.occurrences
53 if occurrences <= 0 {
54 occurrences = 1
55 }
56 fuzzy := ""
57 if r.fuzzy {
58 fuzzy = ", fuzzy match"
59 if occurrences > 1 {
60 fuzzy += ", first matched sample shown"
61 }
62 }
63 fmt.Fprintf(&b, "@@ replacement %d of %d (%d occurrence(s)%s) @@\n", idx+1, len(receipts), occurrences, fuzzy)
64 appendReceiptSpan(&b, '-', clipPostWriteSpan(r.matched, fieldBudget))
65 appendReceiptSpan(&b, '+', clipPostWriteSpan(r.replacement, fieldBudget))
66 }
67 return clipPostWriteReceipt(b.String())
68 }
69
70 func receiptIndexes(count int) []int {
71 switch count {
72 case 0:
73 return nil
74 case 1:
75 return []int{0}
76 case 2:
77 return []int{0, 1}
78 default:
79 return []int{0, count - 1}
80 }
81 }
82
83 func appendReceiptSpan(b *strings.Builder, prefix byte, text string) {
84 if text == "" {
85 b.WriteByte(prefix)
86 b.WriteString("<empty>\n")
87 return
88 }
89 for len(text) > 0 {
90 line := text
91 if i := strings.IndexByte(text, '\n'); i >= 0 {
92 line = text[:i]
93 text = text[i+1:]
94 } else {
95 text = ""
96 }
97 b.WriteByte(prefix)
98 b.WriteString(line)
99 b.WriteByte('\n')
100 }
101 }
102
103 func clipPostWriteSpan(text string, budget int) string {
104 if len(text) <= budget {
105 return text
106 }
107 marker := "\n" + postWriteSpanTruncated + "\n"
108 return clipUTF8HeadTail(text, budget, marker)
109 }
110
111 func clipPostWriteReceipt(text string) string {
112 if len(text) <= maxPostWriteReceiptBytes {
113 return text
114 }
115 marker := "\n" + postWriteReceiptTruncated + "\n"
116 return clipUTF8HeadTail(text, maxPostWriteReceiptBytes, marker)
117 }
118
119 func clipUTF8HeadTail(text string, budget int, marker string) string {
120 available := budget - len(marker)
121 if available <= 0 {
122 return clipUTF8Prefix(marker, budget)
123 }
124 headBytes := available * 3 / 4
125 tailBytes := available - headBytes
126 headEnd := utf8PrefixBoundary(text, headBytes)
127 tailStart := utf8SuffixBoundary(text, len(text)-tailBytes)
128 return text[:headEnd] + marker + text[tailStart:]
129 }
130
131 func clipUTF8Prefix(text string, end int) string {
132 if end >= len(text) {
133 return text
134 }
135 return text[:utf8PrefixBoundary(text, end)]
136 }
137
138 func utf8PrefixBoundary(text string, end int) int {
139 if end >= len(text) {
140 return len(text)
141 }
142 if end < 0 {
143 return 0
144 }
145 for end > 0 && !utf8.RuneStart(text[end]) {
146 end--
147 }
148 return end
149 }
150
151 func utf8SuffixBoundary(text string, start int) int {
152 if start <= 0 {
153 return 0
154 }
155 if start >= len(text) {
156 return len(text)
157 }
158 for start < len(text) && !utf8.RuneStart(text[start]) {
159 start++
160 }
161 return start
162 }
163
163 lines GO