Skip to content

Commit fb5f51c

Browse files
authored
Merge pull request #611 from Yumiue/html_progress
fix(context): 细化 micro compact 保留策略
2 parents a4dca5d + ac11b42 commit fb5f51c

20 files changed

Lines changed: 482 additions & 84 deletions

internal/context/builder_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -399,8 +399,8 @@ func TestDefaultBuilderBuildAppliesMicroCompactAfterTrim(t *testing.T) {
399399
if len(got.Messages) != len(messages) {
400400
t.Fatalf("expected builder output to keep message count, got %d want %d", len(got.Messages), len(messages))
401401
}
402-
if renderDisplayParts(got.Messages[2].Parts) != microCompactClearedMessage {
403-
t.Fatalf("expected builder output to clear older tool result, got %q", renderDisplayParts(got.Messages[2].Parts))
402+
if !strings.Contains(renderDisplayParts(got.Messages[2].Parts), "[summary] filesystem_read_file") {
403+
t.Fatalf("expected builder output to summarize older tool result, got %q", renderDisplayParts(got.Messages[2].Parts))
404404
}
405405
if renderDisplayParts(got.Messages[4].Parts) != "recent bash result" {
406406
t.Fatalf("expected recent tool result to stay visible, got %q", renderDisplayParts(got.Messages[4].Parts))
@@ -513,8 +513,8 @@ func TestDefaultBuilderBuildRespectsExplicitPinCheckerOverride(t *testing.T) {
513513
if err != nil {
514514
t.Fatalf("Build() error = %v", err)
515515
}
516-
if renderDisplayParts(got.Messages[2].Parts) != microCompactClearedMessage {
517-
t.Fatalf("expected explicit noop pin checker to allow compaction, got %q", renderDisplayParts(got.Messages[2].Parts))
516+
if !strings.Contains(renderDisplayParts(got.Messages[2].Parts), "[summary] filesystem_write_file") {
517+
t.Fatalf("expected explicit noop pin checker to allow compaction into summary, got %q", renderDisplayParts(got.Messages[2].Parts))
518518
}
519519
}
520520

@@ -1116,8 +1116,8 @@ func TestNewConfiguredBuilder(t *testing.T) {
11161116
if err != nil {
11171117
t.Fatalf("Build() error = %v", err)
11181118
}
1119-
if renderDisplayParts(got.Messages[2].Parts) != microCompactClearedMessage {
1120-
t.Fatalf("expected noop pin checker to allow compaction, got %q", renderDisplayParts(got.Messages[2].Parts))
1119+
if !strings.Contains(renderDisplayParts(got.Messages[2].Parts), "[summary] filesystem_write_file") {
1120+
t.Fatalf("expected noop pin checker to allow compaction into summary, got %q", renderDisplayParts(got.Messages[2].Parts))
11211121
}
11221122
})
11231123

internal/context/microcompact.go

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package context
22

33
import (
4+
"strconv"
45
"strings"
6+
"unicode/utf8"
57

68
"neo-code/internal/config"
79
"neo-code/internal/context/internalcompact"
@@ -235,32 +237,63 @@ func summarizeOrClear(
235237
toolNames map[string]string,
236238
summarizers MicroCompactSummarizerSource,
237239
) string {
238-
if summarizers == nil {
239-
return microCompactClearedMessage
240-
}
241-
242240
callID := strings.TrimSpace(message.ToolCallID)
243241
toolName, ok := toolNames[callID]
244242
if !ok {
245243
return microCompactClearedMessage
246244
}
247245

248-
summarizer := summarizers.MicroCompactSummarizer(toolName)
249-
if summarizer == nil {
250-
return microCompactClearedMessage
246+
if summarizers != nil {
247+
summarizer := summarizers.MicroCompactSummarizer(toolName)
248+
if summarizer != nil {
249+
summary := summarizer(content, message.ToolMetadata, message.IsError)
250+
if summary != "" {
251+
summary = sanitizeMicroCompactSummary(summary)
252+
if summary != "" {
253+
return summary
254+
}
255+
}
256+
}
251257
}
252258

253-
summary := summarizer(content, message.ToolMetadata, message.IsError)
254-
if summary == "" {
255-
return microCompactClearedMessage
256-
}
257-
summary = sanitizeMicroCompactSummary(summary)
259+
summary := sanitizeMicroCompactSummary(fallbackSummary(toolName, content))
258260
if summary == "" {
259261
return microCompactClearedMessage
260262
}
261263
return summary
262264
}
263265

266+
// fallbackSummary 为缺少专用摘要器的工具生成最小可读摘要,避免静默清空历史。
267+
func fallbackSummary(toolName string, content string) string {
268+
trimmedName := strings.TrimSpace(toolName)
269+
if trimmedName == "" {
270+
return ""
271+
}
272+
273+
parts := []string{
274+
"[summary]",
275+
trimmedName,
276+
"lines=" + strconv.Itoa(stableLineCount(content)),
277+
"chars=" + strconv.Itoa(utf8.RuneCountInString(content)),
278+
}
279+
return strings.Join(parts, " ")
280+
}
281+
282+
// stableLineCount 统计文本行数;空文本返回 0,末尾换行不会产生额外空行计数。
283+
func stableLineCount(text string) int {
284+
if text == "" {
285+
return 0
286+
}
287+
count := strings.Count(text, "\n") + 1
288+
if strings.HasSuffix(text, "\n") {
289+
count--
290+
}
291+
if count < 0 {
292+
return 0
293+
}
294+
return count
295+
}
296+
264297
// sanitizeMicroCompactSummary 对 summarizer 输出做最终净化与限长,避免把不安全文本直接回灌上下文。
265298
func sanitizeMicroCompactSummary(summary string) string {
266299
trimmed := strings.TrimSpace(summary)

internal/context/microcompact_summarizer_test.go

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ func TestMicroCompactWithSummarizerProducesSummary(t *testing.T) {
7878
}
7979
}
8080

81-
// TestMicroCompactWithoutSummarizerFallsBackToClear 验证未注册 summarizer 的工具仍使用清除占位
82-
func TestMicroCompactWithoutSummarizerFallsBackToClear(t *testing.T) {
81+
// TestMicroCompactWithoutSummarizerFallsBackToSummary 验证未注册 summarizer 的工具使用通用兜底摘要
82+
func TestMicroCompactWithoutSummarizerFallsBackToSummary(t *testing.T) {
8383
t.Parallel()
8484

8585
messages := []providertypes.Message{
@@ -121,9 +121,12 @@ func TestMicroCompactWithoutSummarizerFallsBackToClear(t *testing.T) {
121121
nil,
122122
)
123123

124-
// read_file 没有 summarizer,应回退到清除
125-
if renderDisplayParts(got[2].Parts) != microCompactClearedMessage {
126-
t.Fatalf("expected cleared placeholder for read_file without summarizer, got %q", renderDisplayParts(got[2].Parts))
124+
summary := renderDisplayParts(got[2].Parts)
125+
if summary == microCompactClearedMessage {
126+
t.Fatalf("expected fallback summary for read_file without summarizer, got cleared placeholder")
127+
}
128+
if !strings.Contains(summary, "[summary] filesystem_read_file") {
129+
t.Fatalf("expected fallback summary to include tool name, got %q", summary)
127130
}
128131
}
129132

@@ -176,14 +179,17 @@ func TestMicroCompactMixedSpanWithSummarizer(t *testing.T) {
176179
if !strings.Contains(renderDisplayParts(got[2].Parts), "[summary]") {
177180
t.Fatalf("expected bash summary in old span, got %q", renderDisplayParts(got[2].Parts))
178181
}
179-
// call-2 read_file 在旧 span,没有 summarizer,应清除
180-
if renderDisplayParts(got[3].Parts) != microCompactClearedMessage {
181-
t.Fatalf("expected read_file cleared in old span, got %q", renderDisplayParts(got[3].Parts))
182+
summary := renderDisplayParts(got[3].Parts)
183+
if summary == microCompactClearedMessage {
184+
t.Fatalf("expected read_file fallback summary in old span, got cleared placeholder")
185+
}
186+
if !strings.Contains(summary, "[summary] filesystem_read_file") {
187+
t.Fatalf("expected read_file fallback summary to include tool name, got %q", summary)
182188
}
183189
}
184190

185-
// TestMicroCompactSummarizerReturnsEmptyFallsBackToClear 验证 summarizer 返回空字符串时回退到清除
186-
func TestMicroCompactSummarizerReturnsEmptyFallsBackToClear(t *testing.T) {
191+
// TestMicroCompactSummarizerReturnsEmptyFallsBackToSummary 验证 summarizer 返回空字符串时回退到通用摘要
192+
func TestMicroCompactSummarizerReturnsEmptyFallsBackToSummary(t *testing.T) {
187193
t.Parallel()
188194

189195
messages := []providertypes.Message{
@@ -224,8 +230,12 @@ func TestMicroCompactSummarizerReturnsEmptyFallsBackToClear(t *testing.T) {
224230
nil,
225231
)
226232

227-
if renderDisplayParts(got[2].Parts) != microCompactClearedMessage {
228-
t.Fatalf("expected cleared fallback when summarizer returns empty, got %q", renderDisplayParts(got[2].Parts))
233+
summary := renderDisplayParts(got[2].Parts)
234+
if summary == microCompactClearedMessage {
235+
t.Fatalf("expected fallback summary when summarizer returns empty, got cleared placeholder")
236+
}
237+
if !strings.Contains(summary, "[summary] bash") {
238+
t.Fatalf("expected fallback summary to include tool name, got %q", summary)
229239
}
230240
}
231241

@@ -244,6 +254,26 @@ func TestSummarizeOrClearWithNilSummarizers(t *testing.T) {
244254
}
245255
}
246256

257+
func TestSummarizeOrClearFallsBackWithoutRegisteredSummarizer(t *testing.T) {
258+
t.Parallel()
259+
260+
got := summarizeOrClear(
261+
providertypes.Message{ToolCallID: "call-1"},
262+
"first line\nsecond line",
263+
map[string]string{"call-1": "mcp.github.issue"},
264+
nil,
265+
)
266+
if got == microCompactClearedMessage {
267+
t.Fatalf("expected fallback summary for MCP tool, got cleared placeholder")
268+
}
269+
if !strings.Contains(got, "[summary] mcp.github.issue") {
270+
t.Fatalf("expected MCP tool name in fallback summary, got %q", got)
271+
}
272+
if !strings.Contains(got, "lines=2") {
273+
t.Fatalf("expected line count in fallback summary, got %q", got)
274+
}
275+
}
276+
247277
// TestSummarizeOrClearWithToolNamesLookup 验证 toolNames map 查找工具名。
248278
func TestSummarizeOrClearWithToolNamesLookup(t *testing.T) {
249279
t.Parallel()
@@ -321,8 +351,11 @@ func TestSummarizeOrClearSanitizationEmptyFallback(t *testing.T) {
321351
},
322352
)
323353

324-
if got != microCompactClearedMessage {
325-
t.Fatalf("expected cleared fallback when sanitized summary is empty, got %q", got)
354+
if got == microCompactClearedMessage {
355+
t.Fatalf("expected fallback summary when sanitized summary is empty, got cleared placeholder")
356+
}
357+
if !strings.Contains(got, "[summary] bash") {
358+
t.Fatalf("expected fallback summary to include tool name, got %q", got)
326359
}
327360
}
328361

0 commit comments

Comments
 (0)