Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions internal/context/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,8 +399,8 @@ func TestDefaultBuilderBuildAppliesMicroCompactAfterTrim(t *testing.T) {
if len(got.Messages) != len(messages) {
t.Fatalf("expected builder output to keep message count, got %d want %d", len(got.Messages), len(messages))
}
if renderDisplayParts(got.Messages[2].Parts) != microCompactClearedMessage {
t.Fatalf("expected builder output to clear older tool result, got %q", renderDisplayParts(got.Messages[2].Parts))
if !strings.Contains(renderDisplayParts(got.Messages[2].Parts), "[summary] filesystem_read_file") {
t.Fatalf("expected builder output to summarize older tool result, got %q", renderDisplayParts(got.Messages[2].Parts))
}
if renderDisplayParts(got.Messages[4].Parts) != "recent bash result" {
t.Fatalf("expected recent tool result to stay visible, got %q", renderDisplayParts(got.Messages[4].Parts))
Expand Down Expand Up @@ -513,8 +513,8 @@ func TestDefaultBuilderBuildRespectsExplicitPinCheckerOverride(t *testing.T) {
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if renderDisplayParts(got.Messages[2].Parts) != microCompactClearedMessage {
t.Fatalf("expected explicit noop pin checker to allow compaction, got %q", renderDisplayParts(got.Messages[2].Parts))
if !strings.Contains(renderDisplayParts(got.Messages[2].Parts), "[summary] filesystem_write_file") {
t.Fatalf("expected explicit noop pin checker to allow compaction into summary, got %q", renderDisplayParts(got.Messages[2].Parts))
}
}

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

Expand Down
57 changes: 45 additions & 12 deletions internal/context/microcompact.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package context

import (
"strconv"
"strings"
"unicode/utf8"

"neo-code/internal/config"
"neo-code/internal/context/internalcompact"
Expand Down Expand Up @@ -235,32 +237,63 @@ func summarizeOrClear(
toolNames map[string]string,
summarizers MicroCompactSummarizerSource,
) string {
if summarizers == nil {
return microCompactClearedMessage
}

callID := strings.TrimSpace(message.ToolCallID)
toolName, ok := toolNames[callID]
if !ok {
return microCompactClearedMessage
}

summarizer := summarizers.MicroCompactSummarizer(toolName)
if summarizer == nil {
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 := summarizer(content, message.ToolMetadata, message.IsError)
if summary == "" {
return microCompactClearedMessage
}
summary = sanitizeMicroCompactSummary(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)
Expand Down
61 changes: 47 additions & 14 deletions internal/context/microcompact_summarizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ func TestMicroCompactWithSummarizerProducesSummary(t *testing.T) {
}
}

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

messages := []providertypes.Message{
Expand Down Expand Up @@ -121,9 +121,12 @@ func TestMicroCompactWithoutSummarizerFallsBackToClear(t *testing.T) {
nil,
)

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

Expand Down Expand Up @@ -176,14 +179,17 @@ func TestMicroCompactMixedSpanWithSummarizer(t *testing.T) {
if !strings.Contains(renderDisplayParts(got[2].Parts), "[summary]") {
t.Fatalf("expected bash summary in old span, got %q", renderDisplayParts(got[2].Parts))
}
// call-2 read_file 在旧 span,没有 summarizer,应清除
if renderDisplayParts(got[3].Parts) != microCompactClearedMessage {
t.Fatalf("expected read_file cleared in old span, got %q", renderDisplayParts(got[3].Parts))
summary := renderDisplayParts(got[3].Parts)
if summary == microCompactClearedMessage {
t.Fatalf("expected read_file fallback summary in old span, got cleared placeholder")
}
if !strings.Contains(summary, "[summary] filesystem_read_file") {
t.Fatalf("expected read_file fallback summary to include tool name, got %q", summary)
}
}

// TestMicroCompactSummarizerReturnsEmptyFallsBackToClear 验证 summarizer 返回空字符串时回退到清除
func TestMicroCompactSummarizerReturnsEmptyFallsBackToClear(t *testing.T) {
// TestMicroCompactSummarizerReturnsEmptyFallsBackToSummary 验证 summarizer 返回空字符串时回退到通用摘要
func TestMicroCompactSummarizerReturnsEmptyFallsBackToSummary(t *testing.T) {
t.Parallel()

messages := []providertypes.Message{
Expand Down Expand Up @@ -224,8 +230,12 @@ func TestMicroCompactSummarizerReturnsEmptyFallsBackToClear(t *testing.T) {
nil,
)

if renderDisplayParts(got[2].Parts) != microCompactClearedMessage {
t.Fatalf("expected cleared fallback when summarizer returns empty, got %q", renderDisplayParts(got[2].Parts))
summary := renderDisplayParts(got[2].Parts)
if summary == microCompactClearedMessage {
t.Fatalf("expected fallback summary when summarizer returns empty, got cleared placeholder")
}
if !strings.Contains(summary, "[summary] bash") {
t.Fatalf("expected fallback summary to include tool name, got %q", summary)
}
}

Expand All @@ -244,6 +254,26 @@ func TestSummarizeOrClearWithNilSummarizers(t *testing.T) {
}
}

func TestSummarizeOrClearFallsBackWithoutRegisteredSummarizer(t *testing.T) {
t.Parallel()

got := summarizeOrClear(
providertypes.Message{ToolCallID: "call-1"},
"first line\nsecond line",
map[string]string{"call-1": "mcp.github.issue"},
nil,
)
if got == microCompactClearedMessage {
t.Fatalf("expected fallback summary for MCP tool, got cleared placeholder")
}
if !strings.Contains(got, "[summary] mcp.github.issue") {
t.Fatalf("expected MCP tool name in fallback summary, got %q", got)
}
if !strings.Contains(got, "lines=2") {
t.Fatalf("expected line count in fallback summary, got %q", got)
}
}

// TestSummarizeOrClearWithToolNamesLookup 验证 toolNames map 查找工具名。
func TestSummarizeOrClearWithToolNamesLookup(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -321,8 +351,11 @@ func TestSummarizeOrClearSanitizationEmptyFallback(t *testing.T) {
},
)

if got != microCompactClearedMessage {
t.Fatalf("expected cleared fallback when sanitized summary is empty, got %q", got)
if got == microCompactClearedMessage {
t.Fatalf("expected fallback summary when sanitized summary is empty, got cleared placeholder")
}
if !strings.Contains(got, "[summary] bash") {
t.Fatalf("expected fallback summary to include tool name, got %q", got)
}
}

Expand Down
Loading
Loading