From 5919488a6cad4c3f8ef11b686208cce1efe0b604 Mon Sep 17 00:00:00 2001 From: Yumiue <229866007@qq.com> Date: Fri, 24 Apr 2026 21:01:43 +0800 Subject: [PATCH 1/2] =?UTF-8?q?refactor(context):=20=E5=BC=95=E5=85=A5=20M?= =?UTF-8?q?icroCompactConfig=20=E8=81=9A=E5=90=88=E7=BB=93=E6=9E=84?= =?UTF-8?q?=EF=BC=8C=E6=94=B6=E6=95=9B=20Builder=20=E6=9E=84=E9=80=A0?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 MicroCompactConfig 结构体,将 MicroCompactPolicySource、 MicroCompactSummarizerSource、MicroCompactPinChecker 三个接口 打包为单一配置对象,简化 Builder 构造参数 - 新增 NewConfiguredBuilder 作为统一构造入口,支持可变 SectionSource - newPromptSources 改为接受 ...SectionSource 可变参数,nil 自动跳过 - 旧构造函数保留并标记 Deprecated,委托到新入口 - 更新 bootstrap.go 和 runtime.go 使用新构造 API - 补充 TestNewConfiguredBuilder 覆盖 5 个子场景 关联: #416 --- internal/app/bootstrap.go | 8 +- internal/context/builder.go | 67 ++++++++----- internal/context/builder_test.go | 164 ++++++++++++++++++++++++++++++- internal/context/types.go | 8 ++ internal/runtime/runtime.go | 5 +- 5 files changed, 223 insertions(+), 29 deletions(-) diff --git a/internal/app/bootstrap.go b/internal/app/bootstrap.go index 31f8f232..6c5ccce8 100644 --- a/internal/app/bootstrap.go +++ b/internal/app/bootstrap.go @@ -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) @@ -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)) diff --git a/internal/context/builder.go b/internal/context/builder.go index fba0a6dc..81d19e01 100644 --- a/internal/context/builder.go +++ b/internal/context/builder.go @@ -9,11 +9,9 @@ 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 统一构建默认上下文构建器,避免多个构造函数重复装配相同依赖。 @@ -22,17 +20,17 @@ func newDefaultBuilder( summarizers MicroCompactSummarizerSource, memoSource SectionSource, ) Builder { - return &DefaultBuilder{ - promptSources: newPromptSources(memoSource), - trimPolicy: spanMessageTrimPolicy{}, - microCompactPolicies: policies, - microCompactSummarizers: summarizers, - microCompactPinChecker: NewDefaultPinChecker(), + cfg := MicroCompactConfig{ + Policies: policies, + Summarizers: summarizers, + PinChecker: NewDefaultPinChecker(), } + return NewConfiguredBuilder(cfg, memoSource) } -// newPromptSources 组装系统提示词来源列表,并按约定将 memoSource 插入到 systemState 之前。 -func newPromptSources(memoSource SectionSource) []promptSectionSource { +// newPromptSources 组装系统提示词来源列表,将额外 SectionSource 插入到 systemState 之前。 +// nil 元素会被跳过,不会影响来源顺序。 +func newPromptSources(extra ...SectionSource) []promptSectionSource { sources := []promptSectionSource{ corePromptSource{}, &projectRulesSource{}, @@ -40,37 +38,60 @@ func newPromptSources(memoSource SectionSource) []promptSectionSource { 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. @@ -92,7 +113,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() } @@ -103,8 +124,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 diff --git a/internal/context/builder_test.go b/internal/context/builder_test.go index e53cb976..fdd67994 100644 --- a/internal/context/builder_test.go +++ b/internal/context/builder_test.go @@ -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{ @@ -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{ @@ -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{ @@ -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{ @@ -461,6 +464,7 @@ func TestDefaultBuilderBuildSkipsMicroCompactWithoutEstablishedTaskState(t *test promptSources: []promptSectionSource{ stubPromptSectionSource{sections: []promptSection{{Title: "Stub", Content: "body"}}}, }, + microCompactCfg: MicroCompactConfig{PinChecker: NewDefaultPinChecker()}, } messages := []providertypes.Message{ @@ -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{ @@ -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(), }, } @@ -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() diff --git a/internal/context/types.go b/internal/context/types.go index cce9e4ce..356abfee 100644 --- a/internal/context/types.go +++ b/internal/context/types.go @@ -79,6 +79,14 @@ type MicroCompactPinChecker interface { ShouldPin(toolName string, metadata map[string]string) bool } +// MicroCompactConfig 聚合微压缩所需的三个依赖源,简化 Builder 构造参数。 +// 三个子接口仍各自遵循接口隔离原则;MicroCompactConfig 仅作为构造时的参数打包容器。 +type MicroCompactConfig struct { + Policies MicroCompactPolicySource + Summarizers MicroCompactSummarizerSource + PinChecker MicroCompactPinChecker +} + // CompactOptions controls read-time compact behavior inside the context builder. type CompactOptions struct { DisableMicroCompact bool diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index 2878a601..fa3f438a 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -178,7 +178,10 @@ func NewWithFactory( toolManager = tools.NewRegistry() } if contextBuilder == nil { - contextBuilder = agentcontext.NewBuilderWithToolPolicies(toolManager) + contextBuilder = agentcontext.NewConfiguredBuilder(agentcontext.MicroCompactConfig{ + Policies: toolManager, + Summarizers: toolManager, + }) } return &Service{ From 03afb594cb497d4e23aa392a2903ad6d206b9bb0 Mon Sep 17 00:00:00 2001 From: xgopilot Date: Fri, 24 Apr 2026 13:24:56 +0000 Subject: [PATCH 2/2] refactor(context): remove unused default builder helper Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: Yumiue <188874804+Yumiue@users.noreply.github.com> --- internal/context/builder.go | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/internal/context/builder.go b/internal/context/builder.go index 81d19e01..a80d5886 100644 --- a/internal/context/builder.go +++ b/internal/context/builder.go @@ -14,20 +14,6 @@ type DefaultBuilder struct { microCompactCfg MicroCompactConfig } -// newDefaultBuilder 统一构建默认上下文构建器,避免多个构造函数重复装配相同依赖。 -func newDefaultBuilder( - policies MicroCompactPolicySource, - summarizers MicroCompactSummarizerSource, - memoSource SectionSource, -) Builder { - cfg := MicroCompactConfig{ - Policies: policies, - Summarizers: summarizers, - PinChecker: NewDefaultPinChecker(), - } - return NewConfiguredBuilder(cfg, memoSource) -} - // newPromptSources 组装系统提示词来源列表,将额外 SectionSource 插入到 systemState 之前。 // nil 元素会被跳过,不会影响来源顺序。 func newPromptSources(extra ...SectionSource) []promptSectionSource { @@ -140,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) }