Skip to content
Closed
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
8 changes: 6 additions & 2 deletions internal/app/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,11 @@ func BuildGatewayServerDeps(ctx context.Context, opts BootstrapOptions) (Runtime
// 注册内置工具的内容摘要器,使 micro-compact 在清理旧工具结果时保留关键上下文。
tools.RegisterBuiltinSummarizers(toolRegistry)

var contextBuilder agentcontext.Builder = agentcontext.NewBuilderWithToolPoliciesAndSummarizers(toolRegistry, toolRegistry)
microCompactCfg := agentcontext.MicroCompactConfig{
Policies: toolRegistry,
Summarizers: toolRegistry,
}
var contextBuilder agentcontext.Builder = agentcontext.NewConfiguredBuilder(microCompactCfg)
var memoSvc *memo.Service
if cfg.Memo.Enabled {
memoStore := memo.NewFileStore(sharedDeps.ConfigManager.BaseDir(), cfg.Workdir)
Expand All @@ -181,7 +185,7 @@ func BuildGatewayServerDeps(ctx context.Context, opts BootstrapOptions) (Runtime
if invalidator, ok := memoSource.(interface{ InvalidateCache() }); ok {
sourceInvl = invalidator.InvalidateCache
}
contextBuilder = agentcontext.NewBuilderWithMemoAndSummarizers(toolRegistry, toolRegistry, memoSource)
contextBuilder = agentcontext.NewConfiguredBuilder(microCompactCfg, memoSource)
memoSvc = memo.NewService(memoStore, cfg.Memo, sourceInvl)
toolRegistry.Register(memotool.NewRememberTool(memoSvc))
toolRegistry.Register(memotool.NewRecallTool(memoSvc))
Expand Down
87 changes: 50 additions & 37 deletions internal/context/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,68 +9,75 @@ import (

// DefaultBuilder preserves the current runtime context-building behavior.
type DefaultBuilder struct {
promptSources []promptSectionSource
trimPolicy messageTrimPolicy
microCompactPolicies MicroCompactPolicySource
microCompactSummarizers MicroCompactSummarizerSource
microCompactPinChecker MicroCompactPinChecker
promptSources []promptSectionSource
trimPolicy messageTrimPolicy
microCompactCfg MicroCompactConfig
}

// newDefaultBuilder 统一构建默认上下文构建器,避免多个构造函数重复装配相同依赖。
func newDefaultBuilder(
policies MicroCompactPolicySource,
summarizers MicroCompactSummarizerSource,
memoSource SectionSource,
) Builder {
return &DefaultBuilder{
promptSources: newPromptSources(memoSource),
trimPolicy: spanMessageTrimPolicy{},
microCompactPolicies: policies,
microCompactSummarizers: summarizers,
microCompactPinChecker: NewDefaultPinChecker(),
}
}

// newPromptSources 组装系统提示词来源列表,并按约定将 memoSource 插入到 systemState 之前。
func newPromptSources(memoSource SectionSource) []promptSectionSource {
// newPromptSources 组装系统提示词来源列表,将额外 SectionSource 插入到 systemState 之前。
// nil 元素会被跳过,不会影响来源顺序。
func newPromptSources(extra ...SectionSource) []promptSectionSource {
sources := []promptSectionSource{
corePromptSource{},
&projectRulesSource{},
taskStateSource{},
todosSource{},
skillPromptSource{},
}
if memoSource != nil {
sources = append(sources, memoSource)
for _, src := range extra {
if src != nil {
sources = append(sources, src)
}
}
sources = append(sources, repositoryContextSource{})
return append(sources, &systemStateSource{})
}

// NewConfiguredBuilder 基于聚合配置和可选 SectionSource 列表构建上下文构建器,是推荐的统一构造入口。
// cfg.PinChecker 为 nil 时自动使用默认 pin checker;sources 中 nil 元素会被跳过。
func NewConfiguredBuilder(cfg MicroCompactConfig, sources ...SectionSource) Builder {
if cfg.PinChecker == nil {
cfg.PinChecker = NewDefaultPinChecker()
}
return &DefaultBuilder{
promptSources: newPromptSources(sources...),
trimPolicy: spanMessageTrimPolicy{},
microCompactCfg: cfg,
}
}

// NewBuilder returns the default context builder implementation.
func NewBuilder() Builder {
return NewBuilderWithToolPolicies(nil)
return NewConfiguredBuilder(MicroCompactConfig{})
}

// NewBuilderWithToolPolicies 返回带工具 micro compact 策略源的默认上下文构建器。
//
// Deprecated: 使用 NewConfiguredBuilder 替代。
func NewBuilderWithToolPolicies(policies MicroCompactPolicySource) Builder {
return newDefaultBuilder(policies, nil, nil)
return NewConfiguredBuilder(MicroCompactConfig{Policies: policies})
}

// NewBuilderWithToolPoliciesAndSummarizers 返回带工具策略与内容摘要器的上下文构建器。
//
// Deprecated: 使用 NewConfiguredBuilder 替代。
func NewBuilderWithToolPoliciesAndSummarizers(policies MicroCompactPolicySource, summarizers MicroCompactSummarizerSource) Builder {
return newDefaultBuilder(policies, summarizers, nil)
return NewConfiguredBuilder(MicroCompactConfig{Policies: policies, Summarizers: summarizers})
}

// NewBuilderWithMemo 返回带记忆注入能力的上下文构建器。
// memoSource 为 nil 时等价于 NewBuilderWithToolPolicies。
//
// Deprecated: 使用 NewConfiguredBuilder 替代。
func NewBuilderWithMemo(policies MicroCompactPolicySource, memoSource SectionSource) Builder {
return NewBuilderWithMemoAndSummarizers(policies, nil, memoSource)
return NewConfiguredBuilder(MicroCompactConfig{Policies: policies}, memoSource)
}

// NewBuilderWithMemoAndSummarizers 返回带记忆注入与内容摘要器的上下文构建器。
//
// Deprecated: 使用 NewConfiguredBuilder 替代。
func NewBuilderWithMemoAndSummarizers(policies MicroCompactPolicySource, summarizers MicroCompactSummarizerSource, memoSource SectionSource) Builder {
return newDefaultBuilder(policies, summarizers, memoSource)
return NewConfiguredBuilder(MicroCompactConfig{Policies: policies, Summarizers: summarizers}, memoSource)
}

// Build assembles the provider-facing context for the current round.
Expand All @@ -92,7 +99,7 @@ func (b *DefaultBuilder) Build(ctx context.Context, input BuildInput) (BuildResu
if trimPolicy == nil {
trimPolicy = spanMessageTrimPolicy{}
}
pinChecker := b.microCompactPinChecker
pinChecker := b.microCompactCfg.PinChecker
if pinChecker == nil {
pinChecker = NewDefaultPinChecker()
}
Expand All @@ -103,8 +110,8 @@ func (b *DefaultBuilder) Build(ctx context.Context, input BuildInput) (BuildResu
trimPolicy.Trim(input.Messages, input.Compact),
input.TaskState,
input.Compact,
b.microCompactPolicies,
b.microCompactSummarizers,
b.microCompactCfg.Policies,
b.microCompactCfg.Summarizers,
pinChecker,
),
}, nil
Expand All @@ -119,11 +126,17 @@ func applyReadTimeContextProjection(
summarizers MicroCompactSummarizerSource,
pinChecker MicroCompactPinChecker,
) []providertypes.Message {
projectedMessages := cloneContextMessages(messages)
if options.DisableMicroCompact || !taskState.Established() {
return ProjectToolMessagesForModel(cloneContextMessages(messages))
} else {
return ProjectToolMessagesForModel(
microCompactMessagesWithPolicies(messages, policies, options.MicroCompactRetainedToolSpans, summarizers, pinChecker),
)
return ProjectToolMessagesForModel(projectedMessages)
}

projectedMessages = microCompactMessagesWithPolicies(
messages,
policies,
options.MicroCompactRetainedToolSpans,
summarizers,
pinChecker,
)
return ProjectToolMessagesForModel(projectedMessages)
}
164 changes: 161 additions & 3 deletions internal/context/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ func TestDefaultBuilderBuildUsesSpanTrimPolicyWhenTrimPolicyIsUnset(t *testing.T
promptSources: []promptSectionSource{
stubPromptSectionSource{sections: []promptSection{{Title: "Stub", Content: "body"}}},
},
microCompactCfg: MicroCompactConfig{PinChecker: NewDefaultPinChecker()},
}

got, err := builder.Build(stdcontext.Background(), BuildInput{
Expand Down Expand Up @@ -232,6 +233,7 @@ func TestDefaultBuilderBuildAppliesMicroCompactAfterTrim(t *testing.T) {
promptSources: []promptSectionSource{
stubPromptSectionSource{sections: []promptSection{{Title: "Stub", Content: "body"}}},
},
microCompactCfg: MicroCompactConfig{PinChecker: NewDefaultPinChecker()},
}

messages := []providertypes.Message{
Expand Down Expand Up @@ -292,6 +294,7 @@ func TestDefaultBuilderBuildDefaultsPinCheckerForLiteralBuilder(t *testing.T) {
promptSources: []promptSectionSource{
stubPromptSectionSource{sections: []promptSection{{Title: "Stub", Content: "body"}}},
},
microCompactCfg: MicroCompactConfig{PinChecker: NewDefaultPinChecker()},
}

messages := []providertypes.Message{
Expand Down Expand Up @@ -347,7 +350,7 @@ func TestDefaultBuilderBuildRespectsExplicitPinCheckerOverride(t *testing.T) {
promptSources: []promptSectionSource{
stubPromptSectionSource{sections: []promptSection{{Title: "Stub", Content: "body"}}},
},
microCompactPinChecker: noopPinChecker{},
microCompactCfg: MicroCompactConfig{PinChecker: noopPinChecker{}},
}

messages := []providertypes.Message{
Expand Down Expand Up @@ -461,6 +464,7 @@ func TestDefaultBuilderBuildSkipsMicroCompactWithoutEstablishedTaskState(t *test
promptSources: []promptSectionSource{
stubPromptSectionSource{sections: []promptSection{{Title: "Stub", Content: "body"}}},
},
microCompactCfg: MicroCompactConfig{PinChecker: NewDefaultPinChecker()},
}

messages := []providertypes.Message{
Expand Down Expand Up @@ -497,6 +501,7 @@ func TestDefaultBuilderBuildSkipsMicroCompactWhenDisabled(t *testing.T) {
promptSources: []promptSectionSource{
stubPromptSectionSource{sections: []promptSection{{Title: "Stub", Content: "body"}}},
},
microCompactCfg: MicroCompactConfig{PinChecker: NewDefaultPinChecker()},
}

messages := []providertypes.Message{
Expand Down Expand Up @@ -550,8 +555,9 @@ func TestDefaultBuilderBuildHonorsToolMicroCompactPolicies(t *testing.T) {
promptSources: []promptSectionSource{
stubPromptSectionSource{sections: []promptSection{{Title: "Stub", Content: "body"}}},
},
microCompactPolicies: stubMicroCompactPolicySource{
"custom_tool": tools.MicroCompactPolicyPreserveHistory,
microCompactCfg: MicroCompactConfig{
Policies: stubMicroCompactPolicySource{"custom_tool": tools.MicroCompactPolicyPreserveHistory},
PinChecker: NewDefaultPinChecker(),
},
}

Expand Down Expand Up @@ -879,6 +885,158 @@ func TestNewBuilderWithMemo(t *testing.T) {
})
}

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

t.Run("empty config defaults pin checker", func(t *testing.T) {
builder := NewConfiguredBuilder(MicroCompactConfig{})
input := BuildInput{
Messages: []providertypes.Message{{Role: "user", Parts: []providertypes.ContentPart{providertypes.NewTextPart("hello")}}},
Metadata: testMetadata(t.TempDir()),
}
result, err := builder.Build(stdcontext.Background(), input)
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if result.SystemPrompt == "" {
t.Fatalf("expected non-empty system prompt")
}
})

t.Run("with policies and summarizers", func(t *testing.T) {
cfg := MicroCompactConfig{
Policies: stubMicroCompactPolicySource{},
Summarizers: stubMicroCompactSummarizerSource{
"filesystem_read_file": func(content string, metadata map[string]string, isError bool) string {
return "[summary] read_file"
},
},
}
builder := NewConfiguredBuilder(cfg)
messages := []providertypes.Message{
{Role: providertypes.RoleUser, Parts: []providertypes.ContentPart{providertypes.NewTextPart("older user")}},
{
Role: providertypes.RoleAssistant,
ToolCalls: []providertypes.ToolCall{
{ID: "call-1", Name: "filesystem_read_file", Arguments: "{}"},
},
},
{Role: providertypes.RoleTool, ToolCallID: "call-1", Parts: []providertypes.ContentPart{providertypes.NewTextPart("old read result")}},
{
Role: providertypes.RoleAssistant,
ToolCalls: []providertypes.ToolCall{
{ID: "call-2", Name: "bash", Arguments: "{}"},
},
},
{Role: providertypes.RoleTool, ToolCallID: "call-2", Parts: []providertypes.ContentPart{providertypes.NewTextPart("recent bash result")}},
{
Role: providertypes.RoleAssistant,
ToolCalls: []providertypes.ToolCall{
{ID: "call-3", Name: "bash", Arguments: "{}"},
},
},
{Role: providertypes.RoleTool, ToolCallID: "call-3", Parts: []providertypes.ContentPart{providertypes.NewTextPart("latest bash result")}},
{Role: providertypes.RoleUser, Parts: []providertypes.ContentPart{providertypes.NewTextPart("latest explicit instruction")}},
}
got, err := builder.Build(stdcontext.Background(), BuildInput{
Messages: messages,
TaskState: agentsession.TaskState{Goal: "keep implementing task"},
Compact: CompactOptions{
MicroCompactRetainedToolSpans: 2,
},
Metadata: testMetadata(t.TempDir()),
})
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if renderDisplayParts(got.Messages[2].Parts) != "[summary] read_file" {
t.Fatalf("expected summarized older read result, got %q", renderDisplayParts(got.Messages[2].Parts))
}
})

t.Run("with custom pin checker", func(t *testing.T) {
cfg := MicroCompactConfig{
PinChecker: noopPinChecker{},
}
builder := NewConfiguredBuilder(cfg)
messages := []providertypes.Message{
{Role: providertypes.RoleUser, Parts: []providertypes.ContentPart{providertypes.NewTextPart("older user")}},
{
Role: providertypes.RoleAssistant,
ToolCalls: []providertypes.ToolCall{
{ID: "call-1", Name: "filesystem_write_file", Arguments: `{"path":"README.md"}`},
},
},
{
Role: providertypes.RoleTool,
ToolCallID: "call-1",
Parts: []providertypes.ContentPart{providertypes.NewTextPart("README content")},
ToolMetadata: map[string]string{"path": "/project/README.md"},
},
{
Role: providertypes.RoleAssistant,
ToolCalls: []providertypes.ToolCall{
{ID: "call-2", Name: "bash", Arguments: "{}"},
},
},
{Role: providertypes.RoleTool, ToolCallID: "call-2", Parts: []providertypes.ContentPart{providertypes.NewTextPart("recent bash result")}},
{Role: providertypes.RoleUser, Parts: []providertypes.ContentPart{providertypes.NewTextPart("latest explicit instruction")}},
{Role: providertypes.RoleAssistant, Parts: []providertypes.ContentPart{providertypes.NewTextPart("current reply")}},
}
got, err := builder.Build(stdcontext.Background(), BuildInput{
Messages: messages,
TaskState: agentsession.TaskState{Goal: "keep implementing task"},
Compact: CompactOptions{
MicroCompactRetainedToolSpans: 1,
},
})
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))
}
})

t.Run("with extra section sources", func(t *testing.T) {
extraSource := stubPromptSectionSource{
sections: []promptSection{{Title: "Custom", Content: "custom section body"}},
}
builder := NewConfiguredBuilder(MicroCompactConfig{}, extraSource)
input := BuildInput{
Messages: []providertypes.Message{{Role: "user", Parts: []providertypes.ContentPart{providertypes.NewTextPart("hello")}}},
Metadata: testMetadata(t.TempDir()),
}
result, err := builder.Build(stdcontext.Background(), input)
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if !strings.Contains(result.SystemPrompt, "## Custom") {
t.Errorf("expected Custom section in system prompt")
}
if !strings.Contains(result.SystemPrompt, "custom section body") {
t.Errorf("expected custom section content in system prompt")
}
})

t.Run("nil section sources are skipped", func(t *testing.T) {
builder := NewConfiguredBuilder(MicroCompactConfig{}, nil, stubPromptSectionSource{
sections: []promptSection{{Title: "Extra", Content: "extra body"}},
}, nil)
input := BuildInput{
Messages: []providertypes.Message{{Role: "user", Parts: []providertypes.ContentPart{providertypes.NewTextPart("hello")}}},
Metadata: testMetadata(t.TempDir()),
}
result, err := builder.Build(stdcontext.Background(), input)
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if !strings.Contains(result.SystemPrompt, "## Extra") {
t.Errorf("expected Extra section in system prompt")
}
})
}

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

Expand Down
Loading
Loading