-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathmicrocompact.go
More file actions
331 lines (291 loc) · 10.4 KB
/
Copy pathmicrocompact.go
File metadata and controls
331 lines (291 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
package context
import (
"strconv"
"strings"
"unicode/utf8"
"neo-code/internal/config"
"neo-code/internal/context/internalcompact"
providertypes "neo-code/internal/provider/types"
"neo-code/internal/tools"
)
const (
// microCompactClearedMessage 是旧工具结果被读时微压缩后的占位符文本。
microCompactClearedMessage = "[Old tool result content cleared]"
// microCompactSummaryMaxRunes 是摘要回灌到上下文前允许的最大 rune 数量。
microCompactSummaryMaxRunes = 200
)
// microCompactMessages 对裁剪后的消息做只读投影式微压缩,优先摘要旧工具结果,失败时回退清理占位。
func microCompactMessages(messages []providertypes.Message) []providertypes.Message {
return microCompactMessagesWithPolicies(messages, nil, 0, nil, nil)
}
// microCompactMessagesWithPolicies 按工具策略对裁剪后的消息做只读投影式微压缩。
// 仅对需要压缩的工具消息做深拷贝,其余消息共享原始引用以减少内存分配。
func microCompactMessagesWithPolicies(messages []providertypes.Message, policies MicroCompactPolicySource, retainedToolSpans int, summarizers MicroCompactSummarizerSource, pinChecker MicroCompactPinChecker) []providertypes.Message {
if retainedToolSpans <= 0 {
retainedToolSpans = config.DefaultMicroCompactRetainedToolSpans
}
if len(messages) == 0 {
return nil
}
spans := internalcompact.BuildMessageSpans(messages)
protectedStart, hasProtectedTail := internalcompact.ProtectedTailStart(spans)
retainedCompactableSpans := 0
modifiedIndices := make(map[int]struct{})
var pendingCompactions []compactionPending
for spanIndex := len(spans) - 1; spanIndex >= 0; spanIndex-- {
span := spans[spanIndex]
if hasProtectedTail && span.Start >= protectedStart {
continue
}
if !isToolCallSpan(messages, span) {
continue
}
compactableIDs, toolNames := compactableToolCallIDs(messages[span.Start].ToolCalls, policies)
if len(compactableIDs) == 0 {
continue
}
if retainedCompactableSpans < retainedToolSpans {
if hasCompactableToolMessage(messages, span, compactableIDs, toolNames, pinChecker) {
retainedCompactableSpans++
}
continue
}
compactableContents := compactableToolMessageContents(messages, span, compactableIDs, toolNames, pinChecker)
if len(compactableContents) == 0 {
continue
}
for messageIndex, content := range compactableContents {
modifiedIndices[messageIndex] = struct{}{}
pendingCompactions = append(pendingCompactions, compactionPending{
index: messageIndex,
content: content,
toolNames: toolNames,
})
}
}
if len(modifiedIndices) == 0 {
return append([]providertypes.Message(nil), messages...)
}
cloned := make([]providertypes.Message, len(messages))
for i, msg := range messages {
if _, needsClone := modifiedIndices[i]; needsClone {
cloned[i] = cloneSingleMessage(msg)
} else {
cloned[i] = msg
}
}
for _, pending := range pendingCompactions {
summary := summarizeOrClear(cloned[pending.index], pending.content, pending.toolNames, summarizers)
cloned[pending.index].Parts = []providertypes.ContentPart{providertypes.NewTextPart(summary)}
}
return cloned
}
// compactionPending 记录待压缩的消息索引和所需上下文。
type compactionPending struct {
index int
content string
toolNames map[string]string
}
// cloneContextMessages 深拷贝消息切片,避免读时投影污染 runtime 持有的原始会话消息。
func cloneContextMessages(messages []providertypes.Message) []providertypes.Message {
if len(messages) == 0 {
return nil
}
cloned := make([]providertypes.Message, 0, len(messages))
for _, message := range messages {
cloned = append(cloned, cloneSingleMessage(message))
}
return cloned
}
// cloneSingleMessage 深拷贝单条消息,隔离 ToolCalls 和 ToolMetadata 的底层引用。
func cloneSingleMessage(msg providertypes.Message) providertypes.Message {
next := msg
next.ToolCalls = append([]providertypes.ToolCall(nil), msg.ToolCalls...)
if len(msg.ToolMetadata) > 0 {
next.ToolMetadata = make(map[string]string, len(msg.ToolMetadata))
for key, value := range msg.ToolMetadata {
next.ToolMetadata[key] = value
}
}
return next
}
// isToolCallSpan 判断当前 span 是否是由 assistant tool call 起始的原子工具块。
func isToolCallSpan(messages []providertypes.Message, span internalcompact.MessageSpan) bool {
if span.Start < 0 || span.Start >= len(messages) {
return false
}
message := messages[span.Start]
return message.Role == providertypes.RoleAssistant && len(message.ToolCalls) > 0
}
// compactableToolCallIDs 返回 assistant tool call 中可参与微压缩的调用 ID 集合及对应的工具名映射。
func compactableToolCallIDs(calls []providertypes.ToolCall, policies MicroCompactPolicySource) (map[string]struct{}, map[string]string) {
if len(calls) == 0 {
return nil, nil
}
ids := make(map[string]struct{}, len(calls))
toolNames := make(map[string]string, len(calls))
for _, call := range calls {
toolName := strings.TrimSpace(call.Name)
if !toolParticipatesInMicroCompact(toolName, policies) {
continue
}
callID := strings.TrimSpace(call.ID)
if callID == "" {
continue
}
ids[callID] = struct{}{}
toolNames[callID] = toolName
}
if len(ids) == 0 {
return nil, nil
}
return ids, toolNames
}
// toolParticipatesInMicroCompact 判断工具是否应参与 micro compact;未知工具默认视为可压缩。
func toolParticipatesInMicroCompact(toolName string, policies MicroCompactPolicySource) bool {
if policies == nil {
return true
}
return policies.MicroCompactPolicy(toolName) != tools.MicroCompactPolicyPreserveHistory
}
// compactableToolMessageContents 收集工具块中可压缩消息的渲染内容,跳过被钉住的结果。
func compactableToolMessageContents(messages []providertypes.Message, span internalcompact.MessageSpan, compactableIDs map[string]struct{}, toolNames map[string]string, pinChecker MicroCompactPinChecker) map[int]string {
var contents map[int]string
for messageIndex := span.Start + 1; messageIndex < span.End; messageIndex++ {
content, ok := isCompactableToolMessage(messages[messageIndex], compactableIDs, toolNames, pinChecker)
if !ok {
continue
}
if contents == nil {
contents = make(map[int]string)
}
contents[messageIndex] = content
}
return contents
}
// hasCompactableToolMessage 判断工具块中是否存在至少一条可压缩且未被钉住的工具消息。
func hasCompactableToolMessage(messages []providertypes.Message, span internalcompact.MessageSpan, compactableIDs map[string]struct{}, toolNames map[string]string, pinChecker MicroCompactPinChecker) bool {
for messageIndex := span.Start + 1; messageIndex < span.End; messageIndex++ {
if _, ok := isCompactableToolMessage(messages[messageIndex], compactableIDs, toolNames, pinChecker); ok {
return true
}
}
return false
}
// isCompactableToolMessage 判断工具消息是否可压缩(非保留策略且未被钉住),返回渲染内容和是否可压缩。
func isCompactableToolMessage(message providertypes.Message, compactableIDs map[string]struct{}, toolNames map[string]string, pinChecker MicroCompactPinChecker) (string, bool) {
content, ok := compactableToolMessageContent(message, compactableIDs)
if !ok {
return "", false
}
callID := strings.TrimSpace(message.ToolCallID)
toolName := toolNameFromCallID(callID, toolNames)
if isPinnedToolMessage(toolName, message.ToolMetadata, pinChecker) {
return "", false
}
return content, true
}
// compactableToolMessageContent 判断 tool 消息是否可压缩,并返回渲染后的内容文本。
func compactableToolMessageContent(message providertypes.Message, compactableIDs map[string]struct{}) (string, bool) {
if message.Role != providertypes.RoleTool || message.IsError {
return "", false
}
callID := strings.TrimSpace(message.ToolCallID)
if _, ok := compactableIDs[callID]; !ok {
return "", false
}
content := strings.TrimSpace(renderDisplayParts(message.Parts))
if content == "" || content == microCompactClearedMessage {
return "", false
}
return content, true
}
// summarizeOrClear 为单条可压缩工具消息生成摘要或回退到默认清除占位。
func summarizeOrClear(
message providertypes.Message,
content string,
toolNames map[string]string,
summarizers MicroCompactSummarizerSource,
) string {
callID := strings.TrimSpace(message.ToolCallID)
toolName, ok := toolNames[callID]
if !ok {
return microCompactClearedMessage
}
if summarizers != nil {
summarizer := summarizers.MicroCompactSummarizer(toolName)
if summarizer != nil {
summary := summarizer(content, message.ToolMetadata, message.IsError)
if summary != "" {
summary = sanitizeMicroCompactSummary(summary)
if summary != "" {
return summary
}
}
}
}
summary := sanitizeMicroCompactSummary(fallbackSummary(toolName, content))
if summary == "" {
return microCompactClearedMessage
}
return summary
}
// fallbackSummary 为缺少专用摘要器的工具生成最小可读摘要,避免静默清空历史。
func fallbackSummary(toolName string, content string) string {
trimmedName := strings.TrimSpace(toolName)
if trimmedName == "" {
return ""
}
parts := []string{
"[summary]",
trimmedName,
"lines=" + strconv.Itoa(stableLineCount(content)),
"chars=" + strconv.Itoa(utf8.RuneCountInString(content)),
}
return strings.Join(parts, " ")
}
// stableLineCount 统计文本行数;空文本返回 0,末尾换行不会产生额外空行计数。
func stableLineCount(text string) int {
if text == "" {
return 0
}
count := strings.Count(text, "\n") + 1
if strings.HasSuffix(text, "\n") {
count--
}
if count < 0 {
return 0
}
return count
}
// sanitizeMicroCompactSummary 对 summarizer 输出做最终净化与限长,避免把不安全文本直接回灌上下文。
func sanitizeMicroCompactSummary(summary string) string {
trimmed := strings.TrimSpace(summary)
if trimmed == "" {
return ""
}
var b strings.Builder
b.Grow(len(trimmed))
for _, r := range trimmed {
if r < 32 || r == 127 {
continue
}
b.WriteRune(r)
}
clean := strings.TrimSpace(b.String())
if clean == "" {
return ""
}
return truncateSummaryRunes(clean, microCompactSummaryMaxRunes)
}
// truncateSummaryRunes 按 rune 数量截断摘要,超限时追加 "..."。
func truncateSummaryRunes(summary string, maxRunes int) string {
if maxRunes <= 0 || summary == "" {
return summary
}
runes := []rune(summary)
if len(runes) <= maxRunes {
return summary
}
return string(runes[:maxRunes]) + "..."
}