From 2c75dcaf692f70ac6739c7cc135cbc9a10931991 Mon Sep 17 00:00:00 2001 From: agent-bot Date: Tue, 29 Sep 2026 06:29:51 +0000 Subject: [PATCH] refactor(squad): AssetProvider port replaces direct template dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KTD1 fifth step (behavior preserved): - squad.AssetProvider{DocTemplate,RenderDoc,AgentInstruction, AgentInstructionFile} injected via Options.Assets into Service, WorkspaceEngine and AdaptivePlanner - bizdev templateAssetProvider maps port to internal/template (assembly side) - nil-provider paths degrade to the same fallbacks the old error branches used (placeholder template, role header) — fail-visible, no guessing - squad -> template import eliminated; arch edge moved bizdev->template - verified: build/vet/arch green, squad+bizdev short tests pass --- .doc/system-design/module-boundaries.yaml | 2 +- internal/bizdev/service.go | 1 + internal/bizdev/squad_asset_provider.go | 29 +++++++++++++++++++ internal/squad/context_test.go | 2 +- internal/squad/dialogue_context_test.go | 4 +-- internal/squad/evaluator_test.go | 2 +- internal/squad/gov_test.go | 6 ++-- internal/squad/noncritical_degrade_test.go | 2 +- internal/squad/planner.go | 17 +++++++---- internal/squad/planner_test.go | 6 ++-- internal/squad/quality_gate_test.go | 16 +++++----- .../squad/quality_repair_contract_test.go | 18 ++++++------ internal/squad/rdev_catalog_test.go | 2 +- internal/squad/rdev_leader_prompt_test.go | 4 +-- internal/squad/rdev_overlay_test.go | 2 +- internal/squad/service.go | 21 ++++++++++++-- internal/squad/step_retry_test.go | 2 +- internal/squad/workflow.go | 2 +- internal/squad/workflow_test.go | 2 +- internal/squad/workspace.go | 27 +++++++++++------ internal/squad/workspace_sub_build.go | 13 +++++---- internal/squad/workspace_test.go | 2 +- 22 files changed, 123 insertions(+), 59 deletions(-) create mode 100644 internal/bizdev/squad_asset_provider.go diff --git a/.doc/system-design/module-boundaries.yaml b/.doc/system-design/module-boundaries.yaml index aad7778a..1abea19e 100644 --- a/.doc/system-design/module-boundaries.yaml +++ b/.doc/system-design/module-boundaries.yaml @@ -50,6 +50,7 @@ imports: - squad - storage - stream + - template - textextract - workflowdef - workspace @@ -288,7 +289,6 @@ imports: - agent - config - stream - - template - textextract - workflowdef storage: diff --git a/internal/bizdev/service.go b/internal/bizdev/service.go index 07f1fa44..53aa304f 100644 --- a/internal/bizdev/service.go +++ b/internal/bizdev/service.go @@ -211,6 +211,7 @@ func NewService(cfg *config.Config) (*Service, error) { return agent.NewBackend(name, cfg) }, TaskPreparer: svc.squadTaskPreparer, + Assets: templateAssetProvider{}, }) svc.parts = Parts{ Pool: pool, diff --git a/internal/bizdev/squad_asset_provider.go b/internal/bizdev/squad_asset_provider.go new file mode 100644 index 00000000..90da8cd1 --- /dev/null +++ b/internal/bizdev/squad_asset_provider.go @@ -0,0 +1,29 @@ +package bizdev + +import ( + "chat-agent-system/internal/squad" + "chat-agent-system/internal/template" +) + +// templateAssetProvider 把内容资产端口映射到产品模板包(KTD1 装配侧): +// 模板与指令的内容、版本与磁盘/内嵌来源由产品侧全权决定。 +type templateAssetProvider struct{} + +func (templateAssetProvider) DocTemplate(taskKey string) (string, error) { + return template.GetDocTemplate(taskKey) +} + +func (templateAssetProvider) RenderDoc(tmpl string, vars map[string]string) string { + return template.RenderDoc(tmpl, vars) +} + +func (templateAssetProvider) AgentInstruction(role string) (string, error) { + return template.GetAgentInstruction(role) +} + +func (templateAssetProvider) AgentInstructionFile(file string) (string, error) { + return template.GetAgentInstructionFile(file) +} + +// compile-time port conformance +var _ squad.AssetProvider = templateAssetProvider{} diff --git a/internal/squad/context_test.go b/internal/squad/context_test.go index a9117bb2..b70cfae9 100644 --- a/internal/squad/context_test.go +++ b/internal/squad/context_test.go @@ -12,7 +12,7 @@ import ( func TestContextManager_InjectAndFormat(t *testing.T) { tmpDir := t.TempDir() - engine := squad.NewWorkspaceEngine() + engine := squad.NewWorkspaceEngine(nil) ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ WorkDir: tmpDir, TaskKey: "research", diff --git a/internal/squad/dialogue_context_test.go b/internal/squad/dialogue_context_test.go index fa6a1f65..09d952fb 100644 --- a/internal/squad/dialogue_context_test.go +++ b/internal/squad/dialogue_context_test.go @@ -11,7 +11,7 @@ import ( func TestPrepareWorkspaceWritesDialogueContext(t *testing.T) { dir := t.TempDir() - engine := squad.NewWorkspaceEngine() + engine := squad.NewWorkspaceEngine(nil) ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ WorkDir: dir, TaskKey: "system_reqs", @@ -104,7 +104,7 @@ func prepareDialogueWorkspace(t *testing.T, req squad.WorkspacePrepareRequest) s ID: "s1", Title: "整理", Role: "organizer", Mode: "write", SubWorkspace: "tasks/01_organizer", Status: "pending", }} - if _, err := squad.NewWorkspaceEngine().PrepareWorkspace(req); err != nil { + if _, err := squad.NewWorkspaceEngine(nil).PrepareWorkspace(req); err != nil { t.Fatal(err) } return req.WorkDir diff --git a/internal/squad/evaluator_test.go b/internal/squad/evaluator_test.go index 7e3d8a0d..24725ddd 100644 --- a/internal/squad/evaluator_test.go +++ b/internal/squad/evaluator_test.go @@ -124,7 +124,7 @@ func TestWorkflowDriver_DynamicAdjustmentIntegration(t *testing.T) { } defer os.RemoveAll(tmpDir) - engine := NewWorkspaceEngine() + engine := NewWorkspaceEngine(nil) driver := NewWorkflowDriver(engine) evaluator := NewStepEvaluator() driver.SetEvaluator(evaluator) diff --git a/internal/squad/gov_test.go b/internal/squad/gov_test.go index 40f5b36e..2d1219a0 100644 --- a/internal/squad/gov_test.go +++ b/internal/squad/gov_test.go @@ -207,7 +207,7 @@ func TestDetectRepetition_NoFalsePositive(t *testing.T) { func prepareGovWorkspace(t *testing.T, steps []squad.StepSpec) string { t.Helper() dir := t.TempDir() - engine := squad.NewWorkspaceEngine() + engine := squad.NewWorkspaceEngine(nil) _, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ WorkDir: dir, TaskKey: "intake", @@ -272,7 +272,7 @@ func TestWorkflowDriver_GovernanceOnExecute(t *testing.T) { {ID: "step_02", Role: "writer", Title: "成文", SubWorkspace: "tasks/02_draft"}, } dir := prepareGovWorkspace(t, steps) - driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine()) + driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine(nil)) res, err := driver.Execute(context.Background(), dir, &agent.StubBackend{}, squad.ExecuteOptions{Timeout: 10 * time.Second}) if err != nil || !res.Success { @@ -324,7 +324,7 @@ func TestWorkflowDriver_BudgetHardStopKeepsPartialArchive(t *testing.T) { {ID: "step_03", Role: "leader", Title: "终审", SubWorkspace: "tasks/03_review"}, } dir := prepareGovWorkspace(t, steps) - driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine()) + driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine(nil)) // 预算收紧为 2 步 → 第 3 步触发硬停 tight := squad.BudgetPolicy{MaxSteps: 2, MaxRepairRounds: 1, MaxAgentCalls: 10} diff --git a/internal/squad/noncritical_degrade_test.go b/internal/squad/noncritical_degrade_test.go index 71b03cf9..ba490261 100644 --- a/internal/squad/noncritical_degrade_test.go +++ b/internal/squad/noncritical_degrade_test.go @@ -32,7 +32,7 @@ func (b *failFirstBackend) Execute(ctx context.Context, req agent.Request) (*age func prepareDegradeWS(t *testing.T) (string, *squad.WorkflowDriver) { t.Helper() tmpDir := t.TempDir() - engine := squad.NewWorkspaceEngine() + engine := squad.NewWorkspaceEngine(nil) ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ WorkDir: tmpDir, TaskKey: "degrade_test", diff --git a/internal/squad/planner.go b/internal/squad/planner.go index a7cc1534..82a085b5 100644 --- a/internal/squad/planner.go +++ b/internal/squad/planner.go @@ -5,7 +5,6 @@ import ( "strings" "time" - "chat-agent-system/internal/template" ) // PlannerRequest 任务规划请求 @@ -36,14 +35,15 @@ type PlannerResult struct { // AdaptivePlanner 自适应任务规划器 type AdaptivePlanner struct { registry *RoleRegistry + assets AssetProvider } // NewAdaptivePlanner 创建自适应任务规划器 -func NewAdaptivePlanner(reg *RoleRegistry) *AdaptivePlanner { +func NewAdaptivePlanner(reg *RoleRegistry, assets AssetProvider) *AdaptivePlanner { if reg == nil { reg = NewRoleRegistry() } - return &AdaptivePlanner{registry: reg} + return &AdaptivePlanner{registry: reg, assets: assets} } // Plan 执行动态规划:区分固定工作流自适应微调 vs 非标文档自主规划 @@ -157,8 +157,15 @@ func (p *AdaptivePlanner) planFixedAdaptive(req PlannerRequest) (*PlannerResult, } } - tmplContent, err := template.GetDocTemplate(req.TaskKey) - if err != nil { + var tmplContent string + if p.assets != nil { + var err error + tmplContent, err = p.assets.DocTemplate(req.TaskKey) + if err != nil { + tmplContent = "" + } + } + if strings.TrimSpace(tmplContent) == "" { tmplContent = fmt.Sprintf("# %s\n\n## 1. 结论\n\n待确认\n\n## 2. 正文\n\n待确认\n", title) } diff --git a/internal/squad/planner_test.go b/internal/squad/planner_test.go index a6b9599d..b42b9bc6 100644 --- a/internal/squad/planner_test.go +++ b/internal/squad/planner_test.go @@ -7,7 +7,7 @@ import ( func TestPlanFixedAdaptive_Default(t *testing.T) { reg := NewRoleRegistry() - planner := NewAdaptivePlanner(reg) + planner := NewAdaptivePlanner(reg, nil) req := PlannerRequest{ TaskKey: "intake", @@ -36,7 +36,7 @@ func TestPlanFixedAdaptive_Default(t *testing.T) { func TestPlanFixedAdaptive_MaterialSnifferInsertStep(t *testing.T) { reg := NewRoleRegistry() - planner := NewAdaptivePlanner(reg) + planner := NewAdaptivePlanner(reg, nil) req := PlannerRequest{ TaskKey: "research", @@ -87,7 +87,7 @@ func TestPlanFixedAdaptive_MaterialSnifferInsertStep(t *testing.T) { func TestPlanAutonomousAdhoc_CleanroomDesign(t *testing.T) { reg := NewRoleRegistry() - planner := NewAdaptivePlanner(reg) + planner := NewAdaptivePlanner(reg, nil) req := PlannerRequest{ TaskKey: "cleanroom_spec", diff --git a/internal/squad/quality_gate_test.go b/internal/squad/quality_gate_test.go index e3836ac7..4587e0a7 100644 --- a/internal/squad/quality_gate_test.go +++ b/internal/squad/quality_gate_test.go @@ -15,7 +15,7 @@ import ( func TestQualityGate_Validate(t *testing.T) { tmpDir := t.TempDir() - engine := squad.NewWorkspaceEngine() + engine := squad.NewWorkspaceEngine(nil) ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ WorkDir: tmpDir, TaskKey: "intake", @@ -88,7 +88,7 @@ func TestQualityGate_Validate(t *testing.T) { func TestQualityGate_AutoRepair(t *testing.T) { tmpDir := t.TempDir() - engine := squad.NewWorkspaceEngine() + engine := squad.NewWorkspaceEngine(nil) ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ WorkDir: tmpDir, TaskKey: "intake", @@ -135,7 +135,7 @@ func TestQualityGate_AutoRepair(t *testing.T) { func TestQualityGate_SystemReqsAndTestManager(t *testing.T) { tmpDir := t.TempDir() - engine := squad.NewWorkspaceEngine() + engine := squad.NewWorkspaceEngine(nil) gate := squad.NewQualityGate() // 1. 测试 system_reqs @@ -206,7 +206,7 @@ func TestQualityGate_SystemReqsAndTestManager(t *testing.T) { // 现场证据 output.md:204 — 终稿自检表否定性引用禁词,不得误报为敷衍违规。 func TestQualityGate_NegatedSelfCheckCitation_NoFalsePositive(t *testing.T) { tmpDir := t.TempDir() - engine := squad.NewWorkspaceEngine() + engine := squad.NewWorkspaceEngine(nil) ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ WorkDir: tmpDir, TaskKey: "intake", @@ -291,7 +291,7 @@ func TestQualityGate_NegatedSelfCheckCitation_NoFalsePositive(t *testing.T) { func intakeTemplate(t *testing.T) (workDir string, gate *squad.QualityGate) { t.Helper() - engine := squad.NewWorkspaceEngine() + engine := squad.NewWorkspaceEngine(nil) ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ WorkDir: t.TempDir(), TaskKey: "intake", @@ -463,7 +463,7 @@ func TestQualityGate_ActionPlanStatus_DoesNotTriggerRepair(t *testing.T) { if err := os.WriteFile(filepath.Join(workDir, "output.md"), []byte(content), 0o644); err != nil { t.Fatal(err) } - driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine()) + driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine(nil)) report, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &agent.StubBackend{}, 1, squad.ExecuteOptions{ Timeout: 5 * time.Second, }) @@ -506,7 +506,7 @@ func TestCleanHeadingTitle_MultiDigitChineseOrdinal(t *testing.T) { // 现场 nightly TestManager:LLM 写「## 十一、签署 / ## 十二、说明」,旧清洗只剥到「十、」会误报缺章。 func TestQualityGate_Validate_ChineseOrdinalElevenTwelve(t *testing.T) { tmpDir := t.TempDir() - engine := squad.NewWorkspaceEngine() + engine := squad.NewWorkspaceEngine(nil) ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ WorkDir: filepath.Join(tmpDir, "ws_tm"), TaskKey: "test_manager", @@ -552,7 +552,7 @@ func TestNormalizeHeadingTitle_FoldsSpaceWidthAndCase(t *testing.T) { func TestQualityGate_Validate_HeadingLevelAndSpaceDrift(t *testing.T) { tmpDir := t.TempDir() - engine := squad.NewWorkspaceEngine() + engine := squad.NewWorkspaceEngine(nil) ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ WorkDir: filepath.Join(tmpDir, "ws_research"), TaskKey: "research", diff --git a/internal/squad/quality_repair_contract_test.go b/internal/squad/quality_repair_contract_test.go index c386a7b0..7fa5c1f3 100644 --- a/internal/squad/quality_repair_contract_test.go +++ b/internal/squad/quality_repair_contract_test.go @@ -28,7 +28,7 @@ const repairGoodDraft = `# 用户初始需求收集表 func prepareRepairWS(t *testing.T) string { t.Helper() - engine := squad.NewWorkspaceEngine() + engine := squad.NewWorkspaceEngine(nil) ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ WorkDir: t.TempDir(), TaskKey: "intake", @@ -102,7 +102,7 @@ func (b *localThenErrorBackend) Execute(_ context.Context, req agent.Request) (* // Local repair is promoted only when backend succeeds, parents stay untouched, and the local target is trusted. func TestEnsureQualityOrRepair_LocalTargetPromotesAndRevalidates(t *testing.T) { workDir := prepareRepairWS(t) - driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine()) + driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine(nil)) gate := squad.NewQualityGate() report, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &localRepairBackend{body: repairGoodDraft}, 1, squad.ExecuteOptions{ Timeout: 5 * time.Second, @@ -136,7 +136,7 @@ func TestEnsureQualityOrRepair_LocalTargetPromotesAndRevalidates(t *testing.T) { func TestEnsureQualityOrRepair_ParentEscapeRestoresAndFailsClosed(t *testing.T) { workDir := prepareRepairWS(t) orig, _ := os.ReadFile(filepath.Join(workDir, "output.md")) - driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine()) + driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine(nil)) gate := squad.NewQualityGate() report, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &parentEscapeBackend{body: repairGoodDraft}, 1, squad.ExecuteOptions{ Timeout: 5 * time.Second, @@ -166,7 +166,7 @@ func TestEnsureQualityOrRepair_ParentEscapeRestoresAndFailsClosed(t *testing.T) func TestEnsureQualityOrRepair_BothChangedFailsClosedNoPromote(t *testing.T) { workDir := prepareRepairWS(t) orig, _ := os.ReadFile(filepath.Join(workDir, "output.md")) - driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine()) + driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine(nil)) gate := squad.NewQualityGate() parentDecoy := repairGoodDraft + "\n\n## 越界父级不应被采用\n" report, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &bothWriteBackend{ @@ -195,7 +195,7 @@ func TestEnsureQualityOrRepair_BothChangedFailsClosedNoPromote(t *testing.T) { func TestEnsureQualityOrRepair_RunErrorDoesNotPromoteLocalTarget(t *testing.T) { workDir := prepareRepairWS(t) orig, _ := os.ReadFile(filepath.Join(workDir, "output.md")) - driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine()) + driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine(nil)) gate := squad.NewQualityGate() report, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &localThenErrorBackend{body: repairGoodDraft}, 1, squad.ExecuteOptions{ Timeout: 5 * time.Second, @@ -221,7 +221,7 @@ func TestEnsureQualityOrRepair_CreatedParentInputFailsClosed(t *testing.T) { t.Fatal(err) } orig, _ := os.ReadFile(filepath.Join(workDir, "output.md")) - driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine()) + driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine(nil)) gate := squad.NewQualityGate() report, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &createParentInputBackend{body: repairGoodDraft}, 1, squad.ExecuteOptions{ Timeout: 5 * time.Second, @@ -251,7 +251,7 @@ func TestEnsureQualityOrRepair_RestoreFailureStopsWithoutPromote(t *testing.T) { return fmt.Errorf("restore output.md: injected") }) defer reset() - driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine()) + driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine(nil)) gate := squad.NewQualityGate() report, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &bothWriteBackend{ local: repairGoodDraft, @@ -275,7 +275,7 @@ func TestEnsureQualityOrRepair_RestoreFailureStopsWithoutPromote(t *testing.T) { func TestEnsureQualityOrRepair_RepairBudgetNotRegressed(t *testing.T) { workDir := prepareRepairWS(t) - driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine()) + driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine(nil)) gate := squad.NewQualityGate() _, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &parentEscapeBackend{body: repairGoodDraft}, 1, squad.ExecuteOptions{ Timeout: 5 * time.Second, @@ -290,7 +290,7 @@ func TestEnsureQualityOrRepair_RepairBudgetNotRegressed(t *testing.T) { } func TestAddStep_SnapshotLayoutCopiesNeedsLocally(t *testing.T) { - engine := squad.NewWorkspaceEngine() + engine := squad.NewWorkspaceEngine(nil) ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ WorkDir: t.TempDir(), TaskKey: "intake", diff --git a/internal/squad/rdev_catalog_test.go b/internal/squad/rdev_catalog_test.go index d50929bf..383a0da7 100644 --- a/internal/squad/rdev_catalog_test.go +++ b/internal/squad/rdev_catalog_test.go @@ -69,7 +69,7 @@ func TestRDevRegulatoryFollowsComplexityTier(t *testing.T) { t.Fatal("tasks outside the list stay without regulatory even at high") } - planner := NewAdaptivePlanner(nil) + planner := NewAdaptivePlanner(nil, nil) low, err := planner.Plan(PlannerRequest{ TaskKey: "rd_sw_plan", Fields: map[string]string{"complexity_tier": "low"}, diff --git a/internal/squad/rdev_leader_prompt_test.go b/internal/squad/rdev_leader_prompt_test.go index a7e01cbb..56470082 100644 --- a/internal/squad/rdev_leader_prompt_test.go +++ b/internal/squad/rdev_leader_prompt_test.go @@ -11,7 +11,7 @@ import ( func TestRDevLeaderPrompt_UsesDesignControlNotBusiness(t *testing.T) { tmpDir := t.TempDir() - engine := squad.NewWorkspaceEngine() + engine := squad.NewWorkspaceEngine(nil) ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ WorkDir: filepath.Join(tmpDir, "rd_design_input_spec"), @@ -42,7 +42,7 @@ func TestRDevLeaderPrompt_UsesDesignControlNotBusiness(t *testing.T) { func TestIntakeLeaderPrompt_StillBusiness(t *testing.T) { tmpDir := t.TempDir() - engine := squad.NewWorkspaceEngine() + engine := squad.NewWorkspaceEngine(nil) ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ WorkDir: filepath.Join(tmpDir, "intake"), diff --git a/internal/squad/rdev_overlay_test.go b/internal/squad/rdev_overlay_test.go index 45cb8009..cd1c3366 100644 --- a/internal/squad/rdev_overlay_test.go +++ b/internal/squad/rdev_overlay_test.go @@ -33,7 +33,7 @@ func TestRDevOverlayFiltersRoleAndComplexity(t *testing.T) { func TestRDevWorkspaceWritesOverlay(t *testing.T) { tmpDir := t.TempDir() - engine := NewWorkspaceEngine() + engine := NewWorkspaceEngine(nil) ws, err := engine.PrepareWorkspace(WorkspacePrepareRequest{ WorkDir: filepath.Join(tmpDir, "rd_planning_review"), TaskKey: "rd_planning_review", diff --git a/internal/squad/service.go b/internal/squad/service.go index 909144e4..65814fcf 100644 --- a/internal/squad/service.go +++ b/internal/squad/service.go @@ -122,6 +122,23 @@ type Options struct { // 资产归 Core;Core 只做计算并返回产物,工作区文件由 squad 统一写入)。 // nil 或返回 nil 表示无预处理。 TaskPreparer func(workDir string, req SquadRunRequest) (*TaskPreparation, error) + + // Assets 提供文档模板与成员指令等内容资产(KTD1:与业务绑定的模板、 + // 提示词及版本选择归 Core)。nil 时相关能力失败关闭,不降级猜测。 + Assets AssetProvider +} + +// AssetProvider 是小队执行所需内容资产的唯一来源;磁盘/内嵌来源、 +// 版本与业务绑定由产品侧装配决定。 +type AssetProvider interface { + // DocTemplate 返回任务对应的文档模板正文。 + DocTemplate(taskKey string) (string, error) + // RenderDoc 以变量渲染模板。 + RenderDoc(tmpl string, vars map[string]string) string + // AgentInstruction 返回角色指令。 + AgentInstruction(role string) (string, error) + // AgentInstructionFile 返回指定指令文件内容。 + AgentInstructionFile(file string) (string, error) } // TaskPreparation 是领域准备钩子的产物:附件与工作区内回写内容。 @@ -153,7 +170,7 @@ type Service struct { } func NewService(opts Options) *Service { - engine := NewWorkspaceEngine() + engine := NewWorkspaceEngine(opts.Assets) return &Service{ opts: opts.withDefaults(), engine: engine, @@ -215,7 +232,7 @@ func taskDisplayTitle(req SquadRunRequest) string { // PlanWorkflow 动态规划生成任务工作流 func (s *Service) PlanWorkflow(ctx context.Context, req SquadRunRequest) (*WorkflowPlan, error) { - planner := NewAdaptivePlanner(nil) + planner := NewAdaptivePlanner(nil, s.opts.Assets) res, err := planner.Plan(PlannerRequest{ TaskKey: req.TaskKey, Title: taskDisplayTitle(req), diff --git a/internal/squad/step_retry_test.go b/internal/squad/step_retry_test.go index fd9715ed..e231b847 100644 --- a/internal/squad/step_retry_test.go +++ b/internal/squad/step_retry_test.go @@ -100,7 +100,7 @@ func (b *countingBackend) Execute(ctx context.Context, req agent.Request) (*agen func prepareSingleWriterWS(t *testing.T) (string, *squad.WorkflowDriver) { t.Helper() tmpDir := t.TempDir() - engine := squad.NewWorkspaceEngine() + engine := squad.NewWorkspaceEngine(nil) ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ WorkDir: tmpDir, TaskKey: "prelim_design", diff --git a/internal/squad/workflow.go b/internal/squad/workflow.go index b6f17852..caaad0fb 100644 --- a/internal/squad/workflow.go +++ b/internal/squad/workflow.go @@ -55,7 +55,7 @@ type WorkflowDriver struct { func NewWorkflowDriver(engine *WorkspaceEngine) *WorkflowDriver { if engine == nil { - engine = NewWorkspaceEngine() + engine = NewWorkspaceEngine(nil) } return &WorkflowDriver{engine: engine, evaluator: NewStepEvaluator()} } diff --git a/internal/squad/workflow_test.go b/internal/squad/workflow_test.go index 4bcb4122..349cd5b6 100644 --- a/internal/squad/workflow_test.go +++ b/internal/squad/workflow_test.go @@ -15,7 +15,7 @@ import ( func TestWorkflowDriver_DynamicAndExecute(t *testing.T) { tmpDir := t.TempDir() - engine := squad.NewWorkspaceEngine() + engine := squad.NewWorkspaceEngine(nil) ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ WorkDir: tmpDir, TaskKey: "intake", diff --git a/internal/squad/workspace.go b/internal/squad/workspace.go index ff271db5..7b14143e 100644 --- a/internal/squad/workspace.go +++ b/internal/squad/workspace.go @@ -7,7 +7,6 @@ import ( "strings" "time" - "chat-agent-system/internal/template" ) // WorkspacePrepareRequest 工作区准备请求 @@ -60,10 +59,12 @@ type Workspace struct { } // WorkspaceEngine 工作区生命周期与沙箱构建引擎 -type WorkspaceEngine struct{} +type WorkspaceEngine struct { + assets AssetProvider +} -func NewWorkspaceEngine() *WorkspaceEngine { - return &WorkspaceEngine{} +func NewWorkspaceEngine(assets AssetProvider) *WorkspaceEngine { + return &WorkspaceEngine{assets: assets} } // PrepareWorkspace 初始化自包含工作区目录 @@ -136,7 +137,7 @@ func (e *WorkspaceEngine) PrepareWorkspace(req WorkspacePrepareRequest) (*Worksp Steps: steps, } } else { - planner := NewAdaptivePlanner(nil) + planner := NewAdaptivePlanner(nil, e.assets) pRes, pErr := planner.Plan(PlannerRequest{ TaskKey: req.TaskKey, Title: req.StageTitle, @@ -187,13 +188,16 @@ func (e *WorkspaceEngine) PrepareWorkspace(req WorkspacePrepareRequest) (*Worksp } // 4. 写入自描述模板 template.md - if strings.TrimSpace(tmplContent) == "" { + if strings.TrimSpace(tmplContent) == "" && e.assets != nil { var err error - tmplContent, err = template.GetDocTemplate(req.TaskKey) + tmplContent, err = e.assets.DocTemplate(req.TaskKey) if err != nil { - tmplContent = fmt.Sprintf("# %s\n\n## 1. 结论\n\n待确认\n\n## 2. 正文\n\n待确认\n", req.StageTitle) + tmplContent = "" } } + if strings.TrimSpace(tmplContent) == "" { + tmplContent = fmt.Sprintf("# %s\n\n## 1. 结论\n\n待确认\n\n## 2. 正文\n\n待确认\n", req.StageTitle) + } vars := map[string]string{ "project_name": req.ProjectName, "product_name": req.ProjectName, @@ -207,7 +211,12 @@ func (e *WorkspaceEngine) PrepareWorkspace(req WorkspacePrepareRequest) (*Worksp for k, v := range req.InitialFields { vars[k] = v } - renderedTmpl := template.RenderDoc(tmplContent, vars) + var renderedTmpl string + if e.assets != nil { + renderedTmpl = e.assets.RenderDoc(tmplContent, vars) + } else { + renderedTmpl = tmplContent + } if err := os.WriteFile(filepath.Join(workDir, "template.md"), []byte(renderedTmpl), 0o644); err != nil { return nil, fmt.Errorf("write template.md: %w", err) } diff --git a/internal/squad/workspace_sub_build.go b/internal/squad/workspace_sub_build.go index 946e6684..a343d055 100644 --- a/internal/squad/workspace_sub_build.go +++ b/internal/squad/workspace_sub_build.go @@ -6,7 +6,6 @@ import ( "path/filepath" "strings" - "chat-agent-system/internal/template" ) // buildSubWorkspaces 为 workflow 中的各个子任务构建独立沙箱 @@ -27,11 +26,13 @@ func (e *WorkspaceEngine) buildSubWorkspaces(workDir string, wf WorkflowPlan, sq spec := agentMap[step.Role] var inst string var err error - if file := strings.TrimSpace(spec.InstructionsFile); file != "" { - inst, err = template.GetAgentInstructionFile(file) - } - if err != nil || strings.TrimSpace(inst) == "" { - inst, err = template.GetAgentInstruction(step.Role) + if e.assets != nil { + if file := strings.TrimSpace(spec.InstructionsFile); file != "" { + inst, err = e.assets.AgentInstructionFile(file) + } + if err != nil || strings.TrimSpace(inst) == "" { + inst, err = e.assets.AgentInstruction(step.Role) + } } if err == nil && strings.TrimSpace(inst) != "" { agentsBuilder.WriteString(inst) diff --git a/internal/squad/workspace_test.go b/internal/squad/workspace_test.go index 4bd8ab06..3ce45aed 100644 --- a/internal/squad/workspace_test.go +++ b/internal/squad/workspace_test.go @@ -12,7 +12,7 @@ import ( func TestWorkspaceEngine_PrepareWorkspace(t *testing.T) { tmpDir := t.TempDir() - engine := squad.NewWorkspaceEngine() + engine := squad.NewWorkspaceEngine(nil) req := squad.WorkspacePrepareRequest{ WorkDir: filepath.Join(tmpDir, "test_workspace_01"), -- 2.54.0